GEO Automation in Eleventy: JSON-LD, BLUF & Tables Without Manual Markup

Published: (June 10, 2026 at 05:01 AM EDT)
9 min read
Source: Dev.to

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.
0 views
Back to Blog

Related posts

Read more »