DEV Community

Cover image for How I Built a Programmatic SEO Website with Jekyll, Structured Data, and Vibe-Coded Maps
Stadium Gigs
Stadium Gigs

Posted on

How I Built a Programmatic SEO Website with Jekyll, Structured Data, and Vibe-Coded Maps

There's a category of websites that doesn't get enough attention in the developer community: programmatic SEO sites. These are sites where dozens or hundreds of pages are generated from structured data and templates, each targeting specific long-tail search queries. It's a technique used by giants like Zapier (their /apps directory), Nomad List, and Wise's currency pages.

I built StadiumGigs — a job board for stadium and arena employment across the United States — using exactly this approach. No React. No database. No CMS. Just Jekyll, Markdown, Liquid templates, and GitHub Pages.

In this article, I'll break down the architecture, the SEO patterns, the vibe-coded interactive map, and the lessons learned from building a real programmatic site that's live in production.

The Architecture: Jekyll Collections as a Poor Man's Database

The core idea behind programmatic SEO is simple: define a data schema once, create a template once, then generate N pages from N data entries. The site currently serves five distinct content types, each defined as a Jekyll collection:

# _config.yml
collections:
  blog:
    output: true
    sort_by: weight
  jobs:
    output: true
    sort_by: weight
  stadien:
    output: true
  companies:
    output: true
  cities:
    output: true
Enter fullscreen mode Exit fullscreen mode

Each collection maps to a folder of Markdown files with structured front matter. Here's what a stadium entry looks like:

---
title: "SoFi Stadium in Inglewood"
description: "SoFi Stadium is a modern indoor-outdoor venue in Inglewood, California, hosting NFL games and major events year-round."
layout: stadium
permalink: /stadiums/sofi-stadium/
image: "/images/stadiums/sofi-stadium.jpg"
lat: 33.9535
lng: -118.3392
city: Inglewood
state: CA
---
Enter fullscreen mode Exit fullscreen mode

This single file generates a full page with structured content, a Leaflet map pinpointing the stadium's location, and JSON-LD structured data — all from one layout template shared across every stadium in the collection.

The key insight is that Jekyll collections behave like a flat-file database. You can query them with Liquid filters:

{% assign city_jobs = site.jobs | where: "city", page.collection_key %}
{% for job in city_jobs %}
  <!-- render job card -->
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

This creates relational-style joins without a database. City pages automatically display only jobs in that city. The home page filters featured jobs by location. Stadium pages link to their hospitality operators. It's all driven by front matter fields acting as foreign keys.

Content Generation with SEO Constraints

Programmatic SEO only works if each generated page is genuinely useful and optimized for search. Here are the constraints I enforce at the content level:

Title Tags: The 155–160 Character Sweet Spot

Google truncates title tags at roughly 580 pixels, which translates to about 50–60 characters for the <title> element. But for meta descriptions, the sweet spot is 150–160 characters. Every page on the site has a hand-crafted description in its front matter:

description: "Find stadium jobs in Inglewood, California at SoFi Stadium, home to the Los Angeles Rams and Los Angeles Chargers."
Enter fullscreen mode Exit fullscreen mode

The template conditionally renders this as both a meta description and Open Graph description:

{% if page.description %}
  <meta name="description" content="{{ page.description }}" />
{% endif %}
<meta property="og:description" content="{{ page.description }}"/>
Enter fullscreen mode Exit fullscreen mode

For page titles, the format follows the pattern [Venue/Role] in [City] — a structure that naturally targets location-based search queries like "stadium jobs in Inglewood" or "SoFi Stadium jobs."

Structured Data: Speaking Google's Language

Every job listing outputs JobPosting JSON-LD, which is the schema Google uses for its job search feature:

<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "JobPosting",
  "datePosted": "{{ page.date | date: '%Y-%m-%d' }}",
  "validThrough": "{{ page.validThrough | date: '%Y-%m-%d' }}",
  "title": "{{ page.title }}",
  "description": "{{ page.description }}",
  "employmentType": "{{ page.employmentType }}",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "{{ page.name }}"
  },
  "jobLocation": {
    "@type": "Place",
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "{{ page.streetAddress }}",
      "addressLocality": "{{ page.addressLocality }}",
      "addressRegion": "{{ page.addressRegion }}",
      "postalCode": "{{ page.postalCode }}",
      "addressCountry": "{{ page.addressCountry }}"
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Company pages get Organization schema. The home page uses FAQPage microdata for the FAQ accordion. This layered structured data strategy means different page types "speak" to different Google features — job search, knowledge panels, and rich FAQ snippets.

The Permalink Strategy

Every page has an explicit, human-readable permalink:

permalink: /stadiums/sofi-stadium/
permalink: /inglewood/guest-services/
permalink: /blog/raymond-james-stadium-partners-with-ubeya-for-workforce-management/
Enter fullscreen mode Exit fullscreen mode

Combined with the jekyll-redirect-from plugin, old URLs gracefully redirect to current ones — critical for preserving link equity when you refactor your URL structure:

redirect_from:
  - /blog/2026-04-11-raymond-james-stadium-partners-with-ubeya/
Enter fullscreen mode Exit fullscreen mode

Vibe-Coding the Interactive Stadium Map

The interactive stadium map on StadiumGigs is probably the most visually interesting feature — and it was largely vibe-coded. By that I mean I described what I wanted to an AI coding assistant and iteratively refined the result.

The implementation uses Leaflet.js with OpenStreetMap tiles. The clever part is how Jekyll's build step generates the JavaScript data layer:

var stadiums = [
  {% for stadium in site.stadien %}
  {% if stadium.lat and stadium.lng %}
  {
    name: "{{ stadium.title }}",
    lat: {{ stadium.lat }},
    lng: {{ stadium.lng }},
    city: "{{ stadium.city }}",
    state: "{{ stadium.state }}",
    url: "{{ stadium.url | relative_url }}",
    image: "{{ stadium.image | relative_url }}"
  }{% unless forloop.last %},{% endunless %}
  {% endif %}
  {% endfor %}
];
Enter fullscreen mode Exit fullscreen mode

This is server-rendered JavaScript — Liquid templates generate a JavaScript array at build time that Leaflet consumes at runtime. No API calls. No loading spinners. The data is baked directly into the HTML.

Each marker uses a custom SVG stadium icon, and clicking one opens a popup with the venue name, location, and a link to its dedicated page:

stadiums.forEach(function(stadium) {
  var popupContent = '<div class="stadium-popup">' +
    '<h3>' + stadium.name + '</h3>' +
    '<p>' + stadium.city + ', ' + stadium.state + '</p>' +
    '<a href="' + stadium.url + '">View stadium →</a>' +
    '</div>';

  L.marker([stadium.lat, stadium.lng], {icon: stadiumIcon})
    .addTo(map)
    .bindPopup(popupContent);
});
Enter fullscreen mode Exit fullscreen mode

Individual stadium pages also get their own zoomed-in map, conditionally rendered only when coordinates exist in the front matter:

{% if page.lat and page.lng %}
<div id="stadium-location-map"></div>
<script>
  var map = L.map('stadium-location-map')
    .setView([{{ page.lat }}, {{ page.lng }}], 15);
</script>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

The entire map feature — the overview map, per-stadium maps, custom icons, popups, responsive styling — took about two hours of back-and-forth with an AI assistant. That's what vibe-coding looks like in practice: you describe intent, review output, adjust, repeat.

The Cross-Linking Graph: How Templates Create Internal Links

One of the most underrated SEO techniques is strategic internal linking. In a programmatic site, this comes almost for free through templates.

The home page automatically features jobs from three cities and links to their hub pages:

{% assign featured_jobs = site.jobs | where: "city", "inglewood" %}
{% for job in featured_jobs limit: 3 %}
  <a href="{{ job.url | relative_url }}">{{ job.title }}</a>
{% endfor %}
<a href="/inglewood/">See all Inglewood jobs</a>
Enter fullscreen mode Exit fullscreen mode

City pages link to individual jobs. Stadium pages link to hospitality companies. Company pages link back to stadiums they operate at. The result is a tightly connected graph where every page is reachable within 2–3 clicks from the home page — exactly what search engines reward.

This cross-linking isn't manually maintained. It's a natural consequence of the data model. Add a new stadium with city: Inglewood in its front matter, and it automatically appears on the Inglewood city page, the stadium map, and the home page's featured section.

Deployment: Zero-Cost, Zero-Ops

The site deploys via GitHub Actions to GitHub Pages:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
      - run: bundle exec jekyll build
        env:
          JEKYLL_ENV: production
      - uses: actions/upload-pages-artifact@v3

  deploy:
    needs: build
    uses: actions/deploy-pages@v4
Enter fullscreen mode Exit fullscreen mode

Push to main, and the site is live within minutes. Total hosting cost: $0. The jekyll-sitemap plugin auto-generates a sitemap.xml on every build, so search engines always have the latest page inventory.

Why Not a Framework? The Case for Boring Technology

You might wonder: why Jekyll in 2026? Why not Next.js, Astro, or Hugo?

The answer is operational simplicity. Jekyll collections give you 90% of what a headless CMS + framework combo provides, with 10% of the complexity:

Feature Jekyll Approach Framework Approach
Data layer Markdown files with YAML front matter Database + API + ORM
Templates Liquid JSX/React components
Routing File-based + explicit permalinks File-based or config
Structured data Inline JSON-LD in layouts Same, but more abstraction
Deployment GitHub Pages (free) Vercel/Netlify/AWS
Build time Seconds for ~40 pages Seconds (but more moving parts)
Dependencies Ruby + 3 plugins Node + dozens of packages

For a niche job board like StadiumGigs, where the goal is connecting people with part-time stadium employment opportunities, a static site that builds in seconds and costs nothing to host is the right call. Every dependency you don't have is a dependency that can't break.

Scaling the Approach: What Comes Next

The programmatic pattern scales naturally. Adding a new city means creating one Markdown file with the right collection_key, and the entire template system picks it up. Want to add 50 stadiums? Write 50 Markdown files (or generate them from a CSV with a script), each with the same front matter schema, and the map, listing pages, and cross-links all update automatically.

Some technical improvements on the roadmap:

  • Build-time image optimization with jekyll-picture-tag for responsive images
  • Canonical URLs in the <head> to prevent duplicate content issues
  • Twitter Card meta tags alongside existing Open Graph tags
  • Automated content generation from structured data sources — think scraping venue APIs for event schedules and auto-generating blog posts

Key Takeaways for Developers

  1. Programmatic SEO is a legitimate engineering challenge. It's not just about churning out pages — it's about data modeling, template design, structured data, and internal linking strategy.

  2. Jekyll collections are surprisingly powerful. The where, where_exp, and sort_by filters turn flat Markdown files into a queryable data layer.

  3. Vibe-coding interactive features works. The Leaflet map was built through iterative AI-assisted development. The result is clean, performant, and required no deep JavaScript expertise.

  4. Static sites can be sophisticated. JSON-LD structured data, conditional rendering, build-time JavaScript generation, and automated deployments — all without a single line of server-side code.

  5. Start boring, optimize later. Jekyll + GitHub Pages gets you from zero to production with zero infrastructure cost. You can always migrate to something fancier when you outgrow it.


If you're interested in programmatic SEO or building niche content sites, check out StadiumGigs as a living example, or browse the interactive stadium map to see the Leaflet integration in action. The entire approach described here is general enough to apply to any domain — real estate listings, restaurant directories, event guides — anywhere you have structured data that maps to location-specific search intent.

Top comments (0)