---
title: My new website wasn't designed for humans first
description: "Every page on this site now exists twice. One version for you, one in plain markdown for the AI agents that increasingly read it first. The actual architecture, what it cost, and the one thing I still can't measure."
date: 2026-07-27
tags: [ai, agents, build-in-public, web]
url: "https://mahdif.com/blog/my-new-website-wasnt-designed-for-humans-first/"
---

# My new website wasn't designed for humans first

Every page on this site now exists twice. One version for you, one in plain markdown for the AI agents that increasingly read it first. The actual architecture, what it cost, and the one thing I still can't measure.

A growing share of visits to this site will never render a pixel of it. Not this page either. The reader is a recruiter's AI assistant, a research tool, a model someone asked about me. It fetches the text, takes what it needs, and leaves.

I am a product designer. I have had a personal site for most of my 17 years doing this, and every version of it was built for a person with 40 seconds and a scroll wheel. This month I rebuilt this one for the other reader. Every page now exists twice, once as the page you are looking at and once as plain markdown at its own URL. And there is a feature for the humans too: you can now interview me without me being in the room. I'll get to that.

This post is the actual architecture. Not a prediction about AI and the open web.

<figure style="margin:2.75rem 0;">
<img src="/images/blog/my-new-website-wasnt-designed-for-humans-first/split-view.jpg" alt="The home page rendered two ways, side by side. On the left, the agent view: plain markdown with a YAML block listing name, role, location, status and URL, followed by the bio in headings and paragraphs. On the right, the same page as a human sees it, with type, color and layout." width="1664" height="874" style="display:block;width:100%;height:auto;" />
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">The home page, both ways. Left is what an agent reads. Right is what you see.</figcaption>
</figure>

## The reader changed and the page didn't

When someone wants to know about me now, there is a decent chance they never open this site. They paste the link into a chat and ask.

What that reader gets from a modern site is bad. A JS bundle, nav chrome, a consent banner, a footer, and somewhere inside it all, a few hundred words that are actually about me. The content is a small fraction of what it has to parse, and the parse is lossy.

I make the page. I know exactly what is on it. Giving that reader clean text is not doing a robot a favor. It is the same job as writing a good page title: make the thing legible to whoever is actually reading.

## Every page exists twice

Add `.md` to the URL in your address bar. Right now, on this page. You get this exact post as plain markdown, frontmatter and all.

<figure style="margin:2.75rem 0;">
<div style="border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:var(--card);">
<svg viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:auto" role="img" aria-label="A terminal running curl against this post's markdown URL. The response is clean markdown: a YAML frontmatter block with title, description, date, tags and the canonical URL, then the heading and the opening paragraph of the post.">
<defs><linearGradient id="tmgrad" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6366F1"/><stop offset="0.5" stop-color="#A855F7"/><stop offset="1" stop-color="#EC4899"/></linearGradient></defs>
<style>.tm-bg{fill:var(--card)}.tm-win{fill:var(--background);stroke:var(--border);stroke-width:1}.tm-line{stroke:var(--border);stroke-width:1}.tm-title{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:16px}.tm-code{font-family:var(--font-mono);font-size:18px;fill:var(--foreground)}.tm-dim{font-family:var(--font-mono);font-size:18px;fill:var(--muted-foreground)}.tm-eye{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:19px;letter-spacing:4px}</style>
<rect class="tm-bg" width="1200" height="630"/>
<text class="tm-eye" x="110" y="72">ADD .MD TO ANY URL</text>
<rect class="tm-win" x="110" y="106" width="980" height="452" rx="16"/>
<circle cx="146" cy="140" r="6.5" fill="var(--border)"/><circle cx="170" cy="140" r="6.5" fill="var(--border)"/><circle cx="194" cy="140" r="6.5" fill="var(--border)"/>
<text class="tm-title" x="600" y="146" text-anchor="middle">mahdif.com</text>
<line class="tm-line" x1="110" y1="174" x2="1090" y2="174"/>
<text class="tm-code" x="150" y="212"><tspan fill="url(#tmgrad)">$</tspan> curl mahdif.com/blog/my-new-website-wasnt-designed-for-humans-first.md</text>
<text class="tm-dim" x="150" y="258">---</text>
<text class="tm-dim" x="150" y="284">title: <tspan class="tm-code">My new website wasn't designed for humans first</tspan></text>
<text class="tm-dim" x="150" y="310">description: <tspan class="tm-code">Every page on this site now exists twice. One...</tspan></text>
<text class="tm-dim" x="150" y="336">date: <tspan class="tm-code">2026-07-27</tspan></text>
<text class="tm-dim" x="150" y="362">tags: <tspan class="tm-code">[ai, agents, build-in-public, web]</tspan></text>
<text class="tm-dim" x="150" y="388">url: <tspan class="tm-code">https://mahdif.com/blog/my-new-website-wasnt-desi...</tspan></text>
<text class="tm-dim" x="150" y="414">---</text>
<text class="tm-code" x="150" y="452"><tspan fill="url(#tmgrad)"># </tspan>My new website wasn't designed for humans first</text>
<text class="tm-code" x="150" y="490">A growing share of visits to this site will never render</text>
<text class="tm-code" x="150" y="516">a pixel of it. Not this page either.</text>
</svg>
</div>
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">This post, as an agent reads it. No chrome, no bundle, no parsing.</figcaption>
</figure>

That works everywhere on the site. `mahdif.com/index.md` is the home page, `mahdif.com/work.md` the work page, and so on down. Real headings, and internal links that point at other `.md` files, so a model can traverse the whole site markdown to markdown and never touch HTML. (One plumbing note: the `.md` rewrite has to run before the router matches, or this post's `.md` URL would get swallowed by the blog's slug route. That cost me an hour.)

Two more doors. `/llms.txt` is a curated map of the site linking the markdown mirrors. `/llms-full.txt` is the entire site concatenated into one file, for a reader that would rather take everything at once than crawl.

## One serializer, or the whole thing is a lie

This was the real decision.

All of it renders from a single server side function. The `.md` routes, `llms.txt`, `llms-full.txt`, the "copy for AI" action you can see on this page, and an animated agent view you can toggle from the rail. Five surfaces, one serializer.

What you see in agent view is byte for byte what a crawler gets.

<figure style="margin:2.75rem 0;">
<div style="border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:var(--card);">
<svg viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:auto" role="img" aria-label="One server side serializer feeds five surfaces: the markdown mirror of every page, llms.txt, llms-full.txt, the copy for AI action, and the on page agent view. Gating is enforced inside the serializer, so all five outputs are identical.">
<defs><linearGradient id="szgrad" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6366F1"/><stop offset="0.5" stop-color="#A855F7"/><stop offset="1" stop-color="#EC4899"/></linearGradient></defs>
<style>.sz-bg{fill:var(--card)}.sz-panel{fill:var(--background);stroke:var(--border);stroke-width:1}.sz-line{stroke:var(--border);stroke-width:1}.sz-eye{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:19px;letter-spacing:4px}.sz-file{fill:var(--foreground);font-family:var(--font-mono);font-size:23px}.sz-sub{fill:var(--muted-foreground);font-family:var(--font-sans);font-size:18px}.sz-lock{fill:var(--foreground);font-family:var(--font-mono);font-size:15px}.sz-out{fill:var(--foreground);font-family:var(--font-mono);font-size:21px}.sz-outsub{fill:var(--muted-foreground);font-family:var(--font-sans);font-size:15px}.sz-cap{fill:var(--muted-foreground);font-family:var(--font-mono);font-size:16px;letter-spacing:3px}</style>
<rect class="sz-bg" width="1200" height="630"/>
<text class="sz-eye" x="110" y="82">ONE SERIALIZER, FIVE SURFACES</text>
<rect class="sz-panel" x="110" y="150" width="380" height="300" rx="16"/>
<text class="sz-file" x="146" y="204">serialize.ts</text>
<line class="sz-line" x1="110" y1="228" x2="490" y2="228"/>
<text class="sz-sub" x="146" y="272">Server side. The single</text>
<text class="sz-sub" x="146" y="299">source of truth for every</text>
<text class="sz-sub" x="146" y="326">machine readable output.</text>
<rect x="146" y="365" width="300" height="46" rx="10" fill="none" stroke="url(#szgrad)" stroke-width="1.5"/>
<g transform="translate(168,378)"><rect x="0" y="7" width="13" height="10" rx="2" fill="url(#szgrad)"/><path d="M2.5,7 v-3.5 a4,4 0 0 1 8,0 v3.5" fill="none" stroke="url(#szgrad)" stroke-width="1.8"/></g>
<text class="sz-lock" x="196" y="394">gating enforced here</text>
<g fill="none" stroke="url(#szgrad)" stroke-width="2">
<path d="M490,300 C560,300 570,186 640,186"/>
<path d="M490,300 C560,300 570,243 640,243"/>
<path d="M490,300 C570,300 570,300 640,300"/>
<path d="M490,300 C560,300 570,357 640,357"/>
<path d="M490,300 C560,300 570,414 640,414"/>
</g>
<g>
<rect x="646" y="180" width="12" height="12" rx="3" fill="url(#szgrad)"/><text class="sz-out" x="682" y="182">*.md routes</text><text class="sz-outsub" x="682" y="205">every public page, mirrored</text>
<rect x="646" y="237" width="12" height="12" rx="3" fill="url(#szgrad)"/><text class="sz-out" x="682" y="239">/llms.txt</text><text class="sz-outsub" x="682" y="262">the curated map</text>
<rect x="646" y="294" width="12" height="12" rx="3" fill="url(#szgrad)"/><text class="sz-out" x="682" y="296">/llms-full.txt</text><text class="sz-outsub" x="682" y="319">the whole site, one file</text>
<rect x="646" y="351" width="12" height="12" rx="3" fill="url(#szgrad)"/><text class="sz-out" x="682" y="353">Copy for AI</text><text class="sz-outsub" x="682" y="376">the in page action</text>
<rect x="646" y="408" width="12" height="12" rx="3" fill="url(#szgrad)"/><text class="sz-out" x="682" y="410">Agent view</text><text class="sz-outsub" x="682" y="433">what a visitor can toggle</text>
</g>
<line class="sz-line" x1="110" y1="520" x2="1090" y2="520"/>
<text class="sz-cap" x="600" y="566" text-anchor="middle">BYTE FOR BYTE, THE SAME OUTPUT</text>
</svg>
</div>
<figcaption style="margin-top:0.85rem;text-align:center;font-size:0.85rem;color:var(--muted-foreground);">Five surfaces, one function. Two code paths would mean two sites.</figcaption>
</figure>

That constraint is the point. Two code paths means two sites, and the machine one drifts, quietly, until it is describing a person I stopped being a year ago. It also forces the gating down into the serializer: password protected case studies are excluded server side in the same function that builds the markdown, so there is no side entrance where the mirror hands out what the page won't.

The markdown declares itself as an alternate of each page, not a competitor to it. Every page emits `<link rel="alternate" type="text/markdown">`. Getting that wrong is how you hand a search engine two versions of yourself and let it pick.

## You can interview me without me in the room

The other half of the rebuild, and the half built for people.

The site has a chat, grounded in the same content, that answers as me. Ask it about a project, how I think about a problem, why something got built the way it did.

A portfolio has always been one directional. I publish it and hope the right person opens the right page in the right order. Someone with a specific question about my work had no way to ask it without getting on my calendar. Now they can, at 11pm, on a Sunday, without me.

## The part I can't measure

Here is the honest section.

I can see requests to the `.md` routes in my analytics. I cannot see whether a model used what it fetched, or whether a single sentence I wrote made it into an answer about me. There is no attribution layer. The loop that makes normal design work, ship it and watch what people do, does not exist here yet.

`llms.txt` is a convention somebody proposed, not a standard anyone committed to. It may go the way of every other well meaning text file at the root of a domain. I built on it anyway, because the cost was a weekend and the downside is a few files nobody reads.

And the title oversells one thing. "Not designed for humans first" is true about the architecture and a little dishonest about the effort. The visible site got most of the work. The shader on the home page, the three click sounds I auditioned before picking one, the chat frame that preloads in the background so the first open has no loading state. I did not stop caring about the version you are reading. I stopped assuming it was the only one.

## If you want to try it

Start with one page. Serve your home page as markdown at a stable URL and add the alternate link tag. That is an afternoon, and it is most of the value.

Generate it from the same source as the page. Hand written markdown will be wrong within a month, and a stale machine version is worse than none.

Put your gating in the generator, not the template. The markdown route is a door, and doors need the same lock as the front one.

## The test I didn't mean to build

Designing for a reader that cannot see did something to the work itself. The markdown version of this site has no layout, no shader, no sound, no photography, no order I chose for visual reasons. It is just the claims, in sequence.

If the work doesn't hold up in plain text, the design was carrying it.
