How to Add Schema Markup to Your Jekyll Site
Add JSON-LD structured data to your Jekyll site — Article, BreadcrumbList, FAQPage, and WebSite schemas — to improve search appearance and rich snippets.
Schema markup tells search engines what your content is — not just the words, but the nature of the content. A blog post, a FAQ, an author profile, a software product. When Google understands the type of content on your page, it can display enhanced results: FAQ dropdowns that expand directly in the SERP, article bylines and publish dates, breadcrumb trails, and sitelinks search boxes. None of these are guaranteed — Google decides whether to show them — but well-implemented structured data makes your site eligible for them, and that gives you a meaningful edge over sites that skip it.
For Jekyll specifically, schema markup is easy to implement and extremely maintainable. Jekyll’s Liquid templating lets you generate JSON-LD dynamically from front matter data, so adding structured data to a new post requires zero extra effort once the templates are set up.
The right format: JSON-LD
There are three formats for structured data: Microdata (attributes embedded in your HTML elements), RDFa (similar but uses different attribute names), and JSON-LD (a separate <script> block containing JSON). Google recommends JSON-LD and it is what you should use. It sits separately from your HTML — you can edit structured data without touching the content markup, and you can include it in <head> or anywhere in <body>.
Article schema for blog posts
This is the most useful schema for a Jekyll blog. It communicates the article’s title, description, publish and modification dates, author, publisher, and cover image.
Add this inside _layouts/post.html, either inside <head> or just before </body>:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": {{ page.title | jsonify }},
"description": {{ page.description | default: page.excerpt | strip_html | truncatewords: 30 | jsonify }},
"datePublished": "{{ page.date | date_to_xmlschema }}",
"dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
"author": {
"@type": "Person",
"name": {{ page.author | default: site.author | jsonify }},
"url": {{ site.url | append: "/authors/" | append: page.author | downcase | replace: " ", "-" | jsonify }}
},
"publisher": {
"@type": "Organization",
"name": {{ site.title | jsonify }},
"logo": {
"@type": "ImageObject",
"url": "{{ site.url }}/assets/images/logo.png"
}
},
{% if page.image %}
"image": {
"@type": "ImageObject",
"url": "{{ site.url }}{{ page.image }}"
},
{% endif %}
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "{{ page.url | absolute_url }}"
}
}
</script>
A few details worth noting: date_to_xmlschema produces the correct ISO 8601 format Google requires — 2026-02-20T00:00:00+00:00. The jsonify filter handles escaping special characters in strings. The last_modified_at field, if you maintain it, tells Google when the article was last updated, which can improve freshness signals.
If your posts have a last_modified_at: field in front matter, maintaining it is worthwhile. You can automate it with a Git pre-commit hook that updates the field to the current date whenever the file changes.
WebSite schema for the sitelinks search box
The WebSite schema, placed only on the homepage, enables the sitelinks search box feature — a search box that appears directly under your site listing in Google for branded queries. It is not guaranteed, but Google uses this schema to understand what URL to send search queries to.
{% if page.url == '/' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": {{ site.title | jsonify }},
"url": {{ site.url | jsonify }},
"description": {{ site.description | jsonify }},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "{{ site.url }}/search/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
{% endif %}
This belongs in your _layouts/default.html so it only appears on the homepage. The urlTemplate should point to your actual search page URL with the q parameter that your search implementation reads.
BreadcrumbList schema
Breadcrumbs in Google results look like: JekyllHub > SEO > How to Add Schema Markup. They replace the URL display, which is more readable and more clickable. The schema must match the visible breadcrumbs on your page.
For blog posts:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": {{ site.url | jsonify }}
},
{% if page.category %}
{
"@type": "ListItem",
"position": 2,
"name": {{ page.category | jsonify }},
"item": "{{ site.url }}/category/{{ page.category | downcase | replace: ' ', '-' }}/"
},
{
"@type": "ListItem",
"position": 3,
"name": {{ page.title | jsonify }},
"item": {{ page.url | absolute_url | jsonify }}
}
{% else %}
{
"@type": "ListItem",
"position": 2,
"name": {{ page.title | jsonify }},
"item": {{ page.url | absolute_url | jsonify }}
}
{% endif %}
]
}
</script>
The item field on each ListItem is the URL for that breadcrumb level — it must be a real, crawlable URL on your site. If your categories do not have their own pages, omit the category level rather than pointing to a 404.
FAQPage schema
FAQPage schema can earn accordion dropdowns directly in Google’s search results — the question expands to show the answer inline. This is some of the most valuable SERP real estate available and dramatically increases click-through rates on FAQ-heavy pages.
The schema works best when your page contains genuine Q&A content and you have a FAQ layout or include that renders the questions and answers visibly on the page. The structured data must match visible page content — Google will reject it if it does not.
Store FAQs in front matter:
---
layout: faq
title: Jekyll FAQ
faqs:
- question: "How do I install a Jekyll theme?"
answer: "Download the theme files or add the gem to your Gemfile, run bundle install, and update _config.yml with theme: theme-name."
- question: "Are Jekyll themes free?"
answer: "Many Jekyll themes are free and open-source under MIT licences. Premium themes are also available with additional features and commercial support."
- question: "Does Jekyll work with GitHub Pages?"
answer: "Yes. GitHub Pages has built-in Jekyll support. Push to a gh-pages branch or main branch, and your site builds automatically."
---
In _layouts/faq.html (or _includes/faq-schema.html):
{% if page.faqs %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{% for faq in page.faqs %}
{
"@type": "Question",
"name": {{ faq.question | jsonify }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ faq.answer | jsonify }}
}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}
</script>
{% endif %}
You can also add FAQPage schema to any post that contains a genuine FAQ section — embed the faqs: list in that post’s front matter and include the schema block conditionally.
Person schema for author pages
If you have an _authors/ collection (see the multi-author blog guide for setup), add Person schema to the author layout:
{% if page.layout == 'author' %}
<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 %}
"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>
{% endif %}
The sameAs array links the Person entity to their social profiles. This helps Google associate your author with their presence elsewhere on the web, which strengthens E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness) signals.
SoftwareApplication schema for theme pages
If you run a theme marketplace, the SoftwareApplication schema is appropriate for individual theme pages:
{% if page.layout == 'theme' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": {{ page.title | jsonify }},
"description": {{ page.description | jsonify }},
"applicationCategory": "WebApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "{{ page.price | default: '0.00' }}",
"priceCurrency": "USD"
},
{% if page.stars %}
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": {{ page.stars | jsonify }}
},
{% endif %}
"url": {{ page.url | absolute_url | jsonify }}
}
</script>
{% endif %}
Keeping schema maintainable
As your site grows, you will add more schema types. A few practices keep this manageable:
Use includes for each schema type. Create _includes/schema-article.html, _includes/schema-breadcrumb.html, and so on. Include them conditionally in the relevant layouts rather than embedding all schema in default.html.
Maintain last_modified_at in post front matter. Update it whenever you substantially revise a post. This freshness signal matters for news-style content.
Test after every template change. A typo in a Liquid filter can produce invalid JSON, which silently fails. Test with the Rich Results Test tool after significant template changes.
Testing your structured data
Three tools you should know:
Google Rich Results Test at search.google.com/test/rich-results — paste a URL or code snippet. It shows which rich result types are eligible and flags any errors. This is the primary tool to check before you consider structured data done.
Schema Markup Validator at validator.schema.org — validates syntax and structure against the Schema.org specification. Catches issues the Rich Results Test might not surface.
Google Search Console → Enhancements — after deploying, Search Console shows structured data errors across your entire site as Google crawls it. Check this a week after deploying to catch any issues on specific page types.
Common mistakes to avoid
Invalid date format — dates must be ISO 8601. Use date_to_xmlschema in Liquid, not date: "%Y-%m-%d" (which omits the time and timezone and may be rejected).
Mismatched content — structured data must reflect what is visible on the page. If you claim a rating in schema but there is no rating UI visible to users, Google will ignore the schema or penalise the page.
Missing required fields — every schema type has required properties. Check the Rich Results Test — it lists required fields and flags missing ones clearly.
Broken JSON — a missing comma, an extra curly brace, an unescaped quote in content. Always run the validator after changing schema templates. The jsonify filter in Liquid handles most escaping automatically, but conditional blocks with trailing commas are a common source of invalid JSON.
Schema markup is one of the few technical SEO improvements that directly affects how your listings appear — not just where they rank. Once implemented in your layouts, it applies to every current and future post automatically. Looking for a Jekyll theme with SEO built in? Browse JekyllHub themes for options that include jekyll-seo-tag and structured data out of the box.
Organising schema markup across your site
As you add more schema types, keeping everything organised in _layouts/default.html becomes unwieldy. The better approach is to create a dedicated include for each schema type and call them from the appropriate layouts.
Create _includes/schema.html as a dispatcher:
<!-- _includes/schema.html -->
<!-- WebSite schema on homepage only -->
{% if page.url == '/' %}
{% include schema-website.html %}
{% endif %}
<!-- Article schema on posts -->
{% if page.layout == 'post' %}
{% include schema-article.html %}
{% include schema-breadcrumb.html %}
{% endif %}
<!-- Page schema on static pages -->
{% if page.layout == 'page' %}
{% include schema-breadcrumb.html %}
{% endif %}
<!-- Author schema on author pages -->
{% if page.layout == 'author' %}
{% include schema-person.html %}
{% endif %}
<!-- FAQ schema when front matter includes faqs: list -->
{% if page.faqs %}
{% include schema-faq.html %}
{% endif %}
<!-- Software schema on theme pages -->
{% if page.layout == 'theme' %}
{% include schema-software.html %}
{% endif %}
Then include this single file near the bottom of <head> in _layouts/default.html:
{% include schema.html %}
This structure means adding a new schema type is just creating a new include and adding one line to the dispatcher. Debugging is easy because each schema type is isolated.
Automating last_modified_at with a Git hook
The dateModified field in Article schema is only as good as your last_modified_at front matter. If you never update that field, Google sees every post as never having been revised, which hurts freshness signals for evergreen content you keep updated.
A pre-commit Git hook can update the field automatically. Create .git/hooks/pre-commit:
#!/bin/bash
# Update last_modified_at in front matter for any staged _posts/ files
for file in $(git diff --cached --name-only | grep '^_posts/'); do
if [ -f "$file" ]; then
today=$(date -u +"%Y-%m-%d")
# Update last_modified_at if it exists, otherwise leave it
sed -i "s/^last_modified_at:.*/last_modified_at: $today/" "$file"
git add "$file"
fi
done
Make it executable:
chmod +x .git/hooks/pre-commit
Now every time you commit a change to a post, the hook updates last_modified_at to today’s date and re-stages the file. The Article schema’s dateModified field will be accurate automatically.
How Google decides to show rich results
Understanding this prevents frustration when your structured data does not produce rich snippets. Google’s documentation is clear: structured data makes you eligible for rich results, not guaranteed them. Google considers several factors:
Does the schema type support rich results? Not all schema types do. Article, FAQPage, HowTo, Product, Review, BreadcrumbList, WebSite, and SitelinksSearchbox have documented rich result types. Generic Thing or CreativeWork schemas do not produce rich results.
Is the content trustworthy? For FAQPage, Google is increasingly selective about which sites get the accordion display — authority and relevance of the content matters.
Does the schema match the visible content? This is the most common reason structured data is valid but not shown. If your FAQPage schema has five questions but the page only displays three of them to users, Google may reject the schema.
Is the page indexed? Schema on a page with noindex or blocked by robots.txt does not produce rich results because Google cannot canonically associate the structured data with a live page.
Check Google Search Console’s Enhancements section regularly. It shows the number of items with valid and invalid structured data, and links to specific errors by page.
Advanced: adding schema to every post via a plugin
If you build your site with GitHub Actions (not the built-in GitHub Pages builder), you can use a Jekyll generator plugin to add JSON-LD to every post automatically without adding code to each layout manually.
Create _plugins/schema_generator.rb:
module Jekyll
class SchemaGenerator < Generator
safe true
priority :low
def generate(site)
site.posts.docs.each do |post|
post.data['schema_json'] = build_article_schema(post, site)
end
end
private
def build_article_schema(post, site)
{
"@context" => "https://schema.org",
"@type" => "Article",
"headline" => post.data['title'],
"datePublished" => post.data['date'].iso8601,
"dateModified" => (post.data['last_modified_at'] || post.data['date']).iso8601,
"author" => {
"@type" => "Person",
"name" => post.data['author'] || site.config['author']
},
"publisher" => {
"@type" => "Organization",
"name" => site.config['title']
},
"mainEntityOfPage" => {
"@type" => "WebPage",
"@id" => site.config['url'] + post.url
}
}.to_json
end
end
end
Then in _layouts/post.html:
{% if page.schema_json %}
<script type="application/ld+json">{{ page.schema_json }}</script>
{% endif %}
This approach generates valid JSON programmatically from Ruby rather than relying on Liquid string concatenation, which reduces the risk of malformed JSON from special characters in post titles or descriptions.