Home Blog How to Build a Multi-Author Blog with Jekyll
Tutorial

How to Build a Multi-Author Blog with Jekyll

Set up a multi-author Jekyll blog with author pages, post attribution, bio cards, and per-author archives — using Jekyll collections for a fully scalable approach.

How to Build a Multi-Author Blog with Jekyll

A single-author blog is straightforward in Jekyll — the site’s global author key in _config.yml covers everything. But when multiple people contribute to a blog, you need a system that tracks who wrote what, displays each author’s biography and social links on their posts, and generates archive pages showing all content from each contributor.

Jekyll handles this elegantly using collections: a dedicated _authors/ collection where each author has their own file. The approach scales from two contributors to dozens, keeps author data consistent across every post, and generates fully featured author profile pages automatically.


Two approaches — which to choose

Before diving in, it helps to understand your options.

Option 1: _data/ file approach. Store all authors in a single _data/authors.yml file. Posts reference an author key. Good for a small, stable set of contributors.

Option 2: _authors/ collection approach (recommended). Each author has their own file in an _authors/ collection. Jekyll generates a dedicated page for each author automatically. Better for larger teams, guest authors, and when you want individual author archive pages with their own URLs.

This guide focuses on the collection approach, which is more powerful. The _data/ approach is covered at the end for cases where you want something lighter-weight.


Setting up the authors collection

Add the collection to _config.yml:

collections:
  authors:
    output: true          # Generate a page per author file
    permalink: /authors/:name/

defaults:
  - scope:
      path: ""
      type: authors
    values:
      layout: author

The output: true and permalink settings mean each file in _authors/ automatically becomes a page at /authors/[slug]/. The default layout author means you do not have to add layout: author to every author file.


Author files

Create the _authors/ directory at the root of your project. Add one .md file per contributor:

---
name: Marcus Webb
slug: marcus-webb
title: Senior Editor
avatar: /assets/images/authors/marcus-webb.jpg
bio: >
  Marcus has been building with Jekyll since 2015. He writes about static
  site generators, performance, and developer tooling. Based in Edinburgh.
email: marcus@example.com
github: marcuswebb
twitter: marcuswebb
linkedin: marcuswebb
website: https://marcuswebb.dev
expertise:
  - Jekyll
  - Performance
  - Developer Tools
---

<!-- Optional extended bio in markdown below the front matter -->
Marcus Webb is the senior editor at JekyllHub. He has contributed to several
open-source Jekyll plugins and runs the annual Jekyll Community Survey.

The front matter fields you include are up to you — anything listed here will be available as author.field in your templates. A few fields worth including for every author:

  • name — the display name, used in post front matter to attribute posts
  • slug — URL-safe identifier (matches the filename, minus the extension)
  • avatar — path to the author’s photo
  • bio — short one-to-two sentence biography for display in post bio cards

Attributing posts to authors

In each post’s front matter, add the author’s name matching the name field in their author file:

---
layout: post
title: "Getting Started with Jekyll"
date: 2026-05-01
author: Marcus Webb
---

The value should match author.name exactly — case-sensitive. You will filter for this match in your templates.


The author layout

Create _layouts/author.html to render individual author pages. This becomes the profile page at /authors/marcus-webb/:


---
layout: default
---

<div class="author-profile">
  <div class="author-header">
    {% if page.avatar %}
      <img src="{{ page.avatar }}" alt="{{ page.name }}" class="author-avatar-large">
    {% endif %}

    <div class="author-info">
      <h1>{{ page.name }}</h1>
      {% if page.title %}<p class="author-title">{{ page.title }}</p>{% endif %}
      <p class="author-bio">{{ page.bio }}</p>

      <div class="author-social">
        {% if page.twitter %}
          <a href="https://twitter.com/{{ page.twitter }}" rel="noopener">Twitter</a>
        {% endif %}
        {% if page.github %}
          <a href="https://github.com/{{ page.github }}" rel="noopener">GitHub</a>
        {% endif %}
        {% if page.linkedin %}
          <a href="https://linkedin.com/in/{{ page.linkedin }}" rel="noopener">LinkedIn</a>
        {% endif %}
        {% if page.website %}
          <a href="{{ page.website }}" rel="noopener">Website</a>
        {% endif %}
      </div>

      {% if page.expertise %}
        <div class="author-expertise">
          {% for skill in page.expertise %}
            <span class="tag">{{ skill }}</span>
          {% endfor %}
        </div>
      {% endif %}
    </div>
  </div>

  <!-- Extended bio from body of author file -->
  {% if content != "" %}
    <div class="author-extended-bio">
      {{ content }}
    </div>
  {% endif %}

  <!-- Posts by this author -->
  {% assign author_posts = site.posts | where: "author", page.name %}
  <section class="author-posts">
    <h2>Posts by {{ page.name }} ({{ author_posts.size }})</h2>

    {% for post in author_posts %}
      <article class="post-card">
        <h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
        <time>{{ post.date | date: "%B %-d, %Y" }}</time>
        {% if post.description %}
          <p>{{ post.description }}</p>
        {% endif %}
      </article>
    {% endfor %}
  </section>
</div>

The key line is {% assign author_posts = site.posts | where: "author", page.name %} — this filters all posts to just those where the author field matches the current author’s name. This is why the name must match exactly.


Displaying author info on posts

Create a reusable author bio card include at _includes/author-card.html:


{% assign author_obj = site.authors | where: "name", page.author | first %}

{% if author_obj %}
<div class="author-card">
  {% if author_obj.avatar %}
    <a href="{{ author_obj.url }}">
      <img src="{{ author_obj.avatar }}" alt="{{ author_obj.name }}" class="author-avatar">
    </a>
  {% endif %}

  <div class="author-card-content">
    <p class="author-card-label">Written by</p>
    <h3 class="author-card-name">
      <a href="{{ author_obj.url }}">{{ author_obj.name }}</a>
    </h3>
    {% if author_obj.title %}
      <p class="author-card-title">{{ author_obj.title }}</p>
    {% endif %}
    <p class="author-card-bio">{{ author_obj.bio }}</p>

    <div class="author-card-social">
      {% if author_obj.twitter %}
        <a href="https://twitter.com/{{ author_obj.twitter }}" rel="noopener">@{{ author_obj.twitter }}</a>
      {% endif %}
      {% if author_obj.github %}
        <a href="https://github.com/{{ author_obj.github }}" rel="noopener">GitHub</a>
      {% endif %}
    </div>
  </div>
</div>
{% endif %}

Include this in your post layout after the main post content:


<!-- In _layouts/post.html -->
<article>
  {{ content }}
</article>

{% include author-card.html %}

The include looks up the full author object from site.authors by matching on the post’s author field, then renders the full bio card with avatar, name, title, bio, and social links.


Author listing page

Create authors.html (or authors/index.html) at the root of your project to list all contributors:


---
layout: default
title: Our Authors
description: Meet the writers behind JekyllHub.
---

<div class="authors-page">
  <h1>Our Authors</h1>

  <div class="authors-grid">
    {% assign all_authors = site.authors | sort: "name" %}
    {% for author in all_authors %}
      {% assign author_posts = site.posts | where: "author", author.name %}

      <div class="author-card-small">
        {% if author.avatar %}
          <a href="{{ author.url }}">
            <img src="{{ author.avatar }}" alt="{{ author.name }}" class="author-avatar">
          </a>
        {% endif %}

        <div class="author-card-info">
          <h2><a href="{{ author.url }}">{{ author.name }}</a></h2>
          {% if author.title %}<p class="author-title">{{ author.title }}</p>{% endif %}
          <p class="author-bio">{{ author.bio }}</p>
          <p class="author-post-count">{{ author_posts.size }} posts</p>
        </div>
      </div>
    {% endfor %}
  </div>
</div>


Showing author in post metadata

In your post layout, display a compact author line in the post header — before the full bio card at the end:


{% assign author_obj = site.authors | where: "name", page.author | first %}

<header class="post-header">
  <h1>{{ page.title }}</h1>

  <div class="post-meta">
    {% if author_obj %}
      <span class="post-author">
        {% if author_obj.avatar %}
          <img src="{{ author_obj.avatar }}" alt="{{ author_obj.name }}" class="post-meta-avatar">
        {% endif %}
        <a href="{{ author_obj.url }}">{{ author_obj.name }}</a>
      </span>
    {% elsif page.author %}
      <span class="post-author">{{ page.author }}</span>
    {% endif %}

    <time datetime="{{ page.date | date_to_xmlschema }}">
      {{ page.date | date: "%B %-d, %Y" }}
    </time>

    <span class="post-read-time">{{ content | number_of_words | divided_by: 200 }} min read</span>
  </div>
</header>

The {% elsif page.author %} fallback handles guest posts where the author is named in front matter but does not have an author file — the name still displays, just without a link.


Slug-based matching as an alternative

Instead of matching on the full author name (which is case-sensitive and breaks if a name is misspelled), you can match on a slug:

In post front matter:

author: marcus-webb   # slug, not full name

In your template, find the author object by slug:


{% assign author_obj = site.authors | where: "slug", page.author | first %}

Slugs are simpler to type consistently, less likely to have capitalisation mismatches, and more URL-friendly if you use them in author page URLs.

The trade-off: the author name shown in post metadata requires looking up the author object. If you use slugs, always look up author_obj.name for display rather than using page.author directly.


Handling guest authors

For one-time guest contributors who do not need a permanent author page:

Option 1: Create a minimal author file with just name and bio. The author page will exist but is very simple.

Option 2: Use a guest: true flag in the author file to suppress them from the main authors listing:

---
name: Jane Smith
slug: jane-smith
bio: Jane is a freelance Jekyll consultant.
guest: true
---

Then in your authors listing page, filter them out:


{% assign core_authors = site.authors | where_exp: "a", "a.guest != true" %}

Guest authors still get author pages and their posts still show bio cards — they just do not appear in the main team listing.


The _data/ approach for simpler cases

If you only have two or three authors and do not need individual author archive pages, a single data file is simpler to maintain. Create _data/authors.yml:

marcus-webb:
  name: Marcus Webb
  bio: Senior editor and Jekyll specialist.
  avatar: /assets/images/authors/marcus-webb.jpg
  twitter: marcuswebb

sarah-chen:
  name: Sarah Chen
  bio: Frontend developer and CSS enthusiast.
  avatar: /assets/images/authors/sarah-chen.jpg
  github: sarahchen

In post front matter, use the key:

author: marcus-webb

In templates, look up the author:


{% assign author = site.data.authors[page.author] %}
<p>By {{ author.name }}</p>

The limitation: this approach does not generate individual author archive pages. You would need to create them manually or build them with a custom generator plugin.


Keeping authors consistent

A few practices that prevent common problems on multi-author sites:

Enforce author names via a shared reference. Keep a simple text file or wiki page listing every author’s exact name and slug. Writers copy from this list rather than typing from memory.

Use front matter defaults for common author fields. If most posts belong to one or two authors, set a default in _config.yml:

defaults:
  - scope:
      path: ""
      type: posts
    values:
      author: Marcus Webb

This way, posts only need the author field if they differ from the default.

Validate author attribution in CI. A simple build step can check that every post’s author field matches an author in the _authors/ collection:

ruby -e "
require 'yaml'

authors = Dir['_authors/*.md'].map { |f| YAML.load_file(f)['name'] }
posts = Dir['_posts/*.md'].map { |f| YAML.load_file(f)['author'] }.compact

missing = posts.reject { |a| authors.include?(a) }.uniq
if missing.any?
  puts 'Posts with unrecognised authors: ' + missing.join(', ')
  exit 1
end
puts 'All post authors valid.'
"

Add this as a build step in your GitHub Actions workflow to catch author mismatches before they go live.


The collection approach gives you a complete multi-author system: individual profile pages, bio cards on every post, post counts per author, and a team listing — all driven by simple Markdown files that any contributor can update. Browse Jekyll themes on JekyllHub to find themes that include built-in multi-author support.


Per-author RSS feeds

A single site RSS feed covers all posts. For a multi-author site, some readers may want to follow only one contributor’s content. Jekyll’s jekyll-feed plugin generates the main feed, but per-author feeds need a small template.

Create feed/authors/[slug].xml via a layout. First, add a feed layout at _layouts/feed.xml:


---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ page.feed_author.name }}{{ site.title }}</title>
    <description>Posts by {{ page.feed_author.name }} on {{ site.title }}</description>
    <link>{{ site.url }}</link>
    <atom:link href="{{ page.url | absolute_url }}" rel="self" type="application/rss+xml"/>
    <pubDate>{{ site.time | date_to_rfc822 }}</pubDate>

    {% assign author_posts = site.posts | where: "author", page.feed_author.name | limit: 20 %}
    {% for post in author_posts %}
      <item>
        <title>{{ post.title | xml_escape }}</title>
        <description>{{ post.excerpt | xml_escape }}</description>
        <pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
        <link>{{ post.url | absolute_url }}</link>
        <guid isPermaLink="true">{{ post.url | absolute_url }}</guid>
      </item>
    {% endfor %}
  </channel>
</rss>

Then create a file per author in feed/authors/marcus-webb.xml:

---
layout: feed
feed_author:
  name: Marcus Webb
permalink: /feed/authors/marcus-webb/
---

Link to the feed from each author’s profile page so readers can subscribe.


Structured data for author pages

Adding JSON-LD Person schema to author pages strengthens Google’s understanding of each contributor’s expertise and online presence — particularly valuable for E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness):


<!-- In _layouts/author.html, inside <head> -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": {{ page.name | jsonify }},
  "description": {{ page.bio | jsonify }},
  "url": {{ page.url | absolute_url | jsonify }},
  {% if page.avatar %}"image": "{{ site.url }}{{ page.avatar }}",{% endif %}
  "knowsAbout": {{ page.expertise | jsonify }},
  "sameAs": [
    {% if page.twitter %}"https://twitter.com/{{ page.twitter }}"{% endif %}
    {% if page.github %}{% if page.twitter %},{% endif %}"https://github.com/{{ page.github }}"{% endif %}
    {% if page.linkedin %},"https://linkedin.com/in/{{ page.linkedin }}"{% endif %}
  ]
}
</script>

The knowsAbout array comes from the expertise: list in the author’s front matter. It helps establish subject matter authority for each contributor.


Filtering posts by multiple criteria

The author archive page shows all posts by one author. You can extend this with filtering by category or tag. Here is an author posts section that groups by category:


{% assign author_posts = site.posts | where: "author", page.name %}
{% assign categories = author_posts | map: "category" | compact | uniq | sort %}

{% for cat in categories %}
  {% assign cat_posts = author_posts | where: "category", cat %}
  <section class="author-category-group">
    <h3>{{ cat }} <span class="post-count">({{ cat_posts.size }})</span></h3>
    {% for post in cat_posts %}
      <article>
        <a href="{{ post.url }}">{{ post.title }}</a>
        <time>{{ post.date | date: "%B %-d, %Y" }}</time>
      </article>
    {% endfor %}
  </section>
{% endfor %}

This groups each author’s posts by category — useful for contributors who write across multiple topic areas.


Migrating from single-author to multi-author

If you have an existing single-author Jekyll blog and want to add contributors, the transition is smooth but requires a few coordinated steps.

First, create the _authors/ collection and add author files for all contributors including yourself. If your existing posts use site.author (from _config.yml) rather than page.author, you have two options: add an explicit author: field to every existing post (tedious but clean), or update your templates to fall back to site.author when page.author is not set.

The fallback approach is cleaner for large existing archives:


{% assign post_author = page.author | default: site.author %}
{% assign author_obj = site.authors | where: "name", post_author | first %}

New posts by contributors include an explicit author: field. Old posts inherit site.author as the default and display the original author’s bio card without any front matter changes.


Displaying a co-authors field

Some posts are genuinely collaborative — co-authored by two people. Jekyll’s standard approach only handles a single author: string, but you can extend it with a co_authors: list:

---
title: "Building a Jekyll Theme Together"
author: Marcus Webb
co_authors:
  - Sarah Chen
  - James Liu
---

In the post layout, display all contributors:


{% assign primary_author = site.authors | where: "name", page.author | first %}
{% if primary_author %}
  <div class="post-authors">
    <span>By <a href="{{ primary_author.url }}">{{ primary_author.name }}</a></span>
    {% if page.co_authors %}
      {% for co_name in page.co_authors %}
        {% assign co_author = site.authors | where: "name", co_name | first %}
        <span>and 
          {% if co_author %}<a href="{{ co_author.url }}">{% endif %}
          {{ co_name }}
          {% if co_author %}</a>{% endif %}
        </span>
      {% endfor %}
    {% endif %}
  </div>
{% endif %}

Note that co-authored posts will only appear on the primary author’s archive page (filtered by page.author). If you want co-authored posts to appear on all contributing authors’ pages, you need a more complex filter — iterating over both author and co_authors fields.


Keeping the authors collection maintainable over time

As a blog grows, author management can become a source of bugs — typos in names, authors who change their display name, contributors who leave. A few structural choices prevent these problems from accumulating.

Use slugs as the canonical identifier, not names. Names change (someone gets married, prefers a different rendering of their name); slugs should not. If you use slug-based matching throughout your templates, updating an author’s display name only requires changing the name: field in their author file — no post front matter needs updating.

Write an author validation script and run it as a build step in CI:

ruby -e "
require 'yaml'
authors = Dir['_authors/*.md'].map { |f| YAML.load_file(f)['slug'] }
errors = []
Dir['_posts/*.md'].each do |f|
  fm = YAML.load_file(f)
  next unless fm['author']
  unless authors.include?(fm['author'])
    errors << \"#{f}: unknown author '#{fm['author']}'\"
  end
end
if errors.any?
  puts errors.join(\"\\n\")
  exit 1
end
puts 'All author references valid.'
"

Archive author pages rather than deleting them when contributors leave. Remove the author from the team listing with active: false, but keep their page and posts intact. Readers who find old posts via search should still be able to see who wrote them.

The collection-based approach described in this guide scales from two contributors to an entire editorial team. The author files are simple enough for non-technical contributors to maintain themselves with a pull request — no database, no admin interface, just a Markdown file with front matter. That simplicity is what makes Jekyll an excellent platform for collaborative publishing.

Share LinkedIn