# Built for Two Readers: How I Put khadijazaman.com Together

> The build behind khadijazaman.com: Eleventy, a Git-backed CMS, and markdown twins, answers.json, CSP and lastmod all generated from the rendered page.

Source: https://khadijazaman.com/blog/built-for-two-readers/  ·  Last modified: 2026-10-03

[Home](/) / [Blog](/blog/) / Built for Two Readers: Why Every Machine Layer on This Site Is Generated From the Page

[Optimization](/blog/category/optimization/ "More posts on Optimization")2 Oct 2026 · 7 min read · by Khadija Zaman

# Built for Two Readers: Why Every Machine Layer on This Site Is Generated From the Page

## How is khadijazaman.com built?

khadijazaman.com is a static Eleventy site edited through Sveltia CMS, built by GitHub Actions and served from Hostinger. Every layer a machine reads, from the markdown twins and answers.json to the CSP hashes and sitemap dates, is generated from the rendered pages at build time, so the human page and the machine page cannot disagree.

For the first three weeks this site was live, none of its section anchors existed in the HTML it served. The table of contents on every post worked fine in a browser, because a script gave each heading an `id` after the page loaded. A crawler that doesn't run JavaScript got headings with no ids, and a link to `#why-the-top-ranking-stopped-guaranteeing-the-citation` pointed at nothing.

I fixed it on 26 August by moving the ids into the build. The fix was small. The rule it left behind runs the whole site now: anything a machine reads is generated from the page a person reads, at build time, never written a second time by hand.

## The stack is deliberately boring

The site is [Eleventy](https://www.11ty.dev/) 2, plain HTML and one stylesheet. Home, About, Work, Tools and Contact are hand-built pages that Eleventy copies through untouched. Posts, frameworks and studies are Markdown files rendered through three shared layouts.

I write in [Sveltia CMS](https://github.com/sveltia/sveltia-cms) at `/admin/`. It logs in with GitHub through a small Cloudflare Worker, and publishing a post commits straight to `main`. A GitHub Actions workflow builds the site and force-pushes the output to a `deploy` branch, which Hostinger serves. From pressing publish to the page being live takes about a minute.

There is no framework on the front end and no bundler. `site.js` is one file that checks whether each feature's element exists before it does anything, so the same script runs on every page.

## Every machine layer is generated from the human page

None of these layers is a confirmed citation lever, and in my own audits I rank files like llms.txt as hygiene, not strategy. They're here because they're cheap, and because generating them from the built HTML, never from the source, keeps the machine version honest:

| Layer | What generates it | What it reads from |
| --- | --- | --- |
| Markdown twin of every page | scripts/markdown-twins.js | The rendered <main> of each page |
| answers.json | The same script | Every direct-answer block on the site |
| Content-Security-Policy | scripts/csp.js | Every inline script in the built HTML, hashed |
| Sitemap lastmod | An Eleventy filter | The last git commit that touched each source file |
| The "machine view" panel on About | site.js, in the browser | That page's own JSON-LD |

The twins are the clearest example. On the 2 October build, the homepage was 71,835 bytes of HTML and 11,219 bytes as Markdown, the same content at 84% fewer bytes for an agent that asks for `text/markdown`. Because the twin is converted from the rendered page, I can't update one and forget the other.

The CSP works the same way. Instead of allowing `'unsafe-inline'`, the build hashes each inline script it finds, 7 distinct hashes across 34 blocks on the 2 October build, and writes a policy that allows exactly those. A script I add tomorrow is allowed on the next build; one I delete stops being allowed.

The sitemap dates come from git rather than from the build clock, so a page only looks fresh to a crawler when its content changed. Rebuilding the site to fix a typo on one post doesn't tell Google that every page is new.

## What I took out

Two things I built in September didn't survive the month. On 10 September I added a provenance registry, a page tracing every published number to its source, and removed it on 11 September. The same day I added a topical map page, with layer badges on every post; it came out on 18 September. Both came out within eight days of going in, and the site is simpler for it.

The homepage figures used to count up from zero as you scrolled to them. That meant every visitor saw a smaller number than the real one for the first second. They're printed once, server-side, now.

One regression turned up while I was writing this post. The retrieval animation I added to the homepage dims each step until it lights up, and I dimmed it with opacity. Lighthouse flagged five descriptions under the contrast threshold before they lit, and accessibility dropped to 96. Dimming by colour instead keeps the effect and passes.

## What Lighthouse reported

These are Lighthouse 12.8.2 runs on 2 October 2026 against the production build, served locally rather than from Hostinger, so the server headers and CDN are not in them. Treat performance as indicative; accessibility and SEO don't depend on the server.

HomepagePerformanceAccessibilityBest practicesSEO

Desktop**100****100****96****92**

Mobile**96****100****96****92**

Mobile LCP was 2.8 seconds, desktop 0.7, with zero layout shift and zero blocking time on both. Accessibility is 100 on every page I changed this week, and an [axe-core](https://github.com/dequelabs/axe-core) pass on 17 September found zero WCAG 2.2 AA violations across all 27 public pages.

Two categories land below 100, and I'm leaving both. Best practices loses four points for console errors, and both are Google Fonts and the Cloudflare analytics beacon failing to load inside my test sandbox, which blocks outside requests. SEO loses eight because Lighthouse doesn't recognise the `Content-Signal` lines in `robots.txt`. Those lines are deliberate: they tell AI crawlers this site allows search, AI input and training. I'd rather keep the signal than the eight points.

## Where this is overkill

If your site is three pages and you don't publish, most of this is wasted effort. A clean static page with a Person entity in its JSON-LD gets you most of the way, and markdown twins add nothing when there's nothing to read.

I also can't tell you yet how often agents use the twins. Cloudflare Web Analytics runs as a script in the page, and a request for `index.md` never runs that script, so the analytics on this site can't see those fetches at all. Server logs would be the place to look.

---
Markdown twin of https://khadijazaman.com/blog/built-for-two-readers/, generated from the rendered page at build time. Cite the HTML URL. How to cite: https://khadijazaman.com/llms.txt
