GEO Automation in Eleventy: JSON-LD, BLUF & Tables Without Manual Markup
Source: Dev.to
BLUF - Part 3 of the GEO/SEO 2026 series
Problem: manually writing JSON-LD, BLUF blocks, and HTML tables for every post is time-consuming and error-prone.
Solution: Nunjucks includes and shortcodes in Eleventy - set up the template once and every new post gets full GEO infrastructure from frontmatter automatically.
What we automate: Article Schema, FAQPage Schema, HowTo Schema, BLUF block, HTML tables with data-label, and robots.txt.
Setup time: 1.5-2 hours once → 0 minutes per post afterward.
Part 1: GEO Technical Architecture: robots.txt, JSON-LD, and Semantic HTML
Part 2: Content Engineering for LLMs: Information Density and BLUF Structure
Why Manual Micro-Markup Doesn’t Scale
In Part 1 we manually wrote JSON-LD for each Schema type. In Part 2 we added BLUF blocks and HTML tables to each post individually. With a 5-post blog that’s acceptable. With 50 posts it becomes technical debt that guarantees drift: one post missing FAQPage Schema, another with a stale author URL, a third without a BLUF.
The right solution: one change in the template = an update across all posts simultaneously.
Step 1. Base Configuration: _data/metadata.json
All Nunjucks templates will pull global site data from here. If the file already exists, verify it contains these fields:
{
"title": "Your Blog Title",
"description": "A technical blog about development and AI tools",
"url": "https://your-domain.com",
"language": "en",
"author": {
"name": "Your Name",
"url": "https://your-domain.com/about/",
"email": "contact@your-domain.com"
},
"logo": "https://your-domain.com/images/logo.png"
}
Enter fullscreen mode
Exit fullscreen mode
Step 2. Article Schema - Automatic for Every Post
Create _includes/schema-article.njk. It reads title, description, date, and thumbnail from the current page’s frontmatter and generates valid JSON-LD:
{% raw %}{%- if title and page.url -%}
{
"@context": "https://schema.org",
"@type": "Article",
"headline": {{ title | dump | safe }},
"description": {{ description | dump | safe }},
"datePublished": "{{ date | dateToISO }}",
"dateModified": "{{ date | dateToISO }}",
"author": {
"@type": "Person",
"name": {{ metadata.author.name | dump | safe }},
"url": {{ metadata.author.url | dump | safe }}
},
"publisher": {
"@type": "Organization",
"name": {{ metadata.title | dump | safe }},
"url": {{ metadata.url | dump | safe }},
"logo": {
"@type": "ImageObject",
"url": {{ metadata.logo | dump | safe }}
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": {{ (metadata.url + page.url) | dump | safe }}
}
{%- if thumbnail -%}
,
"image": {{ (metadata.url + "/" + thumbnail.image) | dump | safe }}
{%- endif -%}
}
{%- endif -%}{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
The dateToISO Filter in .eleventy.js
Eleventy doesn’t have a built-in ISO filter. Add this to .eleventy.js:
module.exports = function(eleventyConfig) {
// Filter: Date → ISO 8601 string for JSON-LD
eleventyConfig.addFilter("dateToISO", (date) => {
if (!date) return "";
return new Date(date).toISOString();
});
// ... other settings
};
Enter fullscreen mode
Exit fullscreen mode
Adding to the Base Layout
In _includes/base.njk or _includes/layouts/post.njk, inside the “ block:
{% raw %}
{{ title }} | {{ metadata.title }}
{# ── GEO Schema Markup ──────────────────────────── #}
{% include "schema-article.njk" %}
{% include "schema-faq.njk" %}
{% include "schema-howto.njk" %}
{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
Every new post now automatically gets Article Schema with no additional effort.
Step 3. FAQPage Schema via a Frontmatter Array
The idea: FAQ questions and answers are defined directly in the post’s frontmatter. The template generates the Schema automatically, and the same data renders the HTML FAQ section on the page.
Frontmatter Structure
---
title: "Post Title"
faq:
- q: "What is GEO and how does it differ from SEO?"
a: "GEO is optimization for citation in AI responses; SEO is for ranking in search results."
- q: "Which AI bots should be allowed in robots.txt?"
a: "GPTBot, PerplexityBot, Google-Extended, ClaudeBot, anthropic-ai, FacebookBot."
- q: "Are GEO and SEO compatible?"
a: "Yes, most GEO techniques reinforce classical SEO."
---
Enter fullscreen mode
Exit fullscreen mode
Template _includes/schema-faq.njk
{% raw %}{%- if faq and faq.length -%}
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{%- for item in faq -%}
{
"@type": "Question",
"name": {{ item.q | dump | safe }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ item.a | dump | safe }}
}
}{% if not loop.last %},{% endif %}
{%- endfor -%}
]
}
{%- endif -%}{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
Rendering the FAQ Section from the Same Data
In the post template (in , not ):
{% raw %}{%- if faq and faq.length -%}
## Frequently Asked Questions
{%- for item in faq -%}
**{{ item.q }}**
{{ item.a }}
{%- endfor -%}
{%- endif -%}{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
One frontmatter array → Schema for Google/AI and HTML for readers. Synchronization is guaranteed.
Step 4. HowTo Schema for Step-by-Step Guides
Add to the frontmatter of guide posts:
howto:
name: "How to Set Up SSH Deployment"
totalTime: "PT1H"
steps:
- name: "Generate SSH Keys"
text: "Run ssh-keygen -t ed25519 -C deploy@mysite.com on your local machine."
- name: "Copy the Key to the Server"
text: "Use ssh-copy-id -i ~/.ssh/key.pub user@server-ip for authorization."
- name: "Set Up a Git Bare Repository"
text: "On the server run mkdir -p ~/repos/mysite.git && git init --bare."
Enter fullscreen mode
Exit fullscreen mode
Template _includes/schema-howto.njk
{% raw %}{%- if howto and howto.steps -%}
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": {{ howto.name | dump | safe }},
"totalTime": {{ howto.totalTime | dump | safe }},
"step": [
{%- for step in howto.steps -%}
{
"@type": "HowToStep",
"position": {{ loop.index }},
"name": {{ step.name | dump | safe }},
"text": {{ step.text | dump | safe }}
}{% if not loop.last %},{% endif %}
{%- endfor -%}
]
}
{%- endif -%}{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
Step 5. Automatic BLUF Block
Instead of writing the BLUF by hand in every post, move the bullet points into a frontmatter array bluf and let the template render it.
Frontmatter
bluf:
- "**Goal:** automate GEO markup so we never write JSON-LD by hand."
- "**Time:** 2 hours of setup, then 0 minutes per new post."
- "**Stack:** Eleventy (11ty) + Nunjucks includes + addShortcode in .eleventy.js."
Enter fullscreen mode
Exit fullscreen mode
Template _includes/bluf.njk
{% raw %}{%- if bluf and bluf.length -%}
BLUF - Summary for AI Crawlers
{%- for item in bluf -%}
- {{ item | markdownify | safe }}
{%- endfor -%}
{%- endif -%}{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
Include it at the top of the post template, right after “:
{% raw %}
{# H1 is rendered from frontmatter title via layout - don't duplicate it #}
{% include "bluf.njk" %}
{{ content | safe }}
{% endraw %}
Enter fullscreen mode
Exit fullscreen mode
The markdownify Filter
// .eleventy.js - render inline markdown in frontmatter strings
const markdownIt = require("markdown-it");
const md = new markdownIt({ html: true });
eleventyConfig.addFilter("markdownify", (str) => {
if (!str) return "";
return md.renderInline(String(str));
});
Enter fullscreen mode
Exit fullscreen mode
Step 6. Shortcode for GEO Tables
Instead of manually writing “ with all the data-label attributes every time - one shortcode in .eleventy.js and clean pipe-separated syntax in Markdown.
Registering the Shortcode in .eleventy.js
// Paired shortcode: {% geotable "Header 1,Header 2,Header 3" %}
// Data rows: Value 1 | Value 2 | Value 3
// {% endgeotable %}
eleventyConfig.addPairedShortcode("geotable", function(content, headers) {
const headerList = headers.split(',').map(h => h.trim());
// Parse rows - one line = one
const rows = content
.trim()
.split('\n')
.filter(row => row.trim().length > 0);
// Generate thead
let html = `\n \n \n`;
headerList.forEach(h => {
html += ` ${h}\n`;
});
html += ` \n \n \n`;
// Generate tbody
rows.forEach(row => {
const cells = row.split('|').map(c => c.trim());
html += ` \n`;
cells.forEach((cell, i) => {
const label = headerList[i] || '';
html += ` ${cell}\n`;
});
html += ` \n`;
});
html += ` \n`;
return html;
});
Enter fullscreen mode
Exit fullscreen mode
Usage in a Markdown Post
{% raw %}{% geotable "Platform,TTFB,Price/mo,Control" %}
Eleventy + NVMe VPS | `
- [ ] `_includes/schema-faq.njk` is included in the base layout
- [ ] `_includes/schema-howto.njk` is included in the base layout
- [ ] `_includes/bluf.njk` is included in the post template immediately after H1
[ ] The `geotable` shortcode is registered in `.eleventy.js`
- [ ] `robots.njk` with `permalink: /robots.txt` is in the project root
- [ ] Verified with [Google Rich Results Test](https://search.google.com/test/rich-results) after deploying the first post with the new templates
FAQ
**Is this approach compatible with both Eleventy v2.x and v3.x?**
Yes. All templates and shortcodes are written for Eleventy v2.x and v3.x - the `addFilter`, `addPairedShortcode`, and `addShortcode` APIs have not changed between versions. The `markdownify` filter requires `npm install markdown-it` if the package isn't already in your dependencies.
**Does `| dump | safe` correctly escape special characters in JSON-LD?**
Yes. Nunjucks's `dump` filter serializes a string into valid JSON, escaping quotes, backslashes, and special characters. The `| dump | safe` combination is the standard pattern for inserting dynamic strings into JSON-LD via Nunjucks.
**How do I verify the Schema was generated correctly after deployment?**
Three tools: [Google Rich Results Test](https://search.google.com/test/rich-results) - validates Article, FAQPage, and HowTo. [Schema Markup Validator](https://validator.schema.org/) - extended validation. Bing Webmaster Tools → "URL inspection" - shows how Copilot sees the markup.
**Can the same approach be used for BreadcrumbList Schema?**
Yes. Add `_includes/schema-breadcrumb.njk` with a `{% if series and order %}` condition and populate values from frontmatter. This is particularly useful for series posts - AI search engines see the relationship between parts and boost the authority of the entire cluster.
What's Next: Part 4
**Part 4 - Measuring the GEO Effect:** how to systematically track whether ChatGPT Search, Perplexity, and Gemini are citing your site, which metrics replace classical organic CTR, and how to build a monitoring dashboard without paid tools - using only curl, Python, and Google Sheets.