· 4 min read · nextjs · seo · structured-data
How I built erion.dev: server components, JSON-LD, and llms.txt
A three-page portfolio has nothing to rank for. Before writing any content worth indexing, I wanted the site itself to be fully machine-readable — for search engines and for the LLMs that increasingly answer questions people used to Google. This post is a tour of how erion.dev is put together, using the site you are reading right now as the case study.
One source of truth: resume.json
Every professional fact on this site lives in a single resume.json file,
following the JSON Resume standard. Four different
surfaces are generated from it:
- the resume page, rendered by a React component that reads the JSON directly
- the JSON-LD structured data describing me as a
Person - the llms.txt file served to AI crawlers
- the Open Graph image, drawn at build time with my name and current role
The point is drift prevention. When I change jobs or add a certification, I edit one file and every surface — human-visible and machine-readable — updates together. There is no way for the resume page to say one thing while the structured data claims another.
Server components, client islands
The site runs on Next.js with the App Router, and every page is a React Server Component. The full text of every page is present in the HTML the server sends — no JavaScript required to read anything. That matters for crawlers that don't execute scripts, and it keeps the JavaScript bundle small for everyone else.
Interactivity lives in small leaf "islands" that hydrate independently: the navbar, the contact form, an FAQ accordion, and a couple of analytics helpers. One detail I care about: the FAQ accordion keeps collapsed answers in the DOM instead of unmounting them, so crawlers see the full Q&A even though a human sees a collapsed list.
The GEO surface
GEO — generative engine optimization — is SEO for the systems that answer questions instead of listing links. Three pieces do the work here:
A single JSON-LD graph. The root layout emits one @graph containing a
Person and a WebSite node, each with a stable @id. Everything else that
needs to reference the author points at https://erion.dev/#person instead of
duplicating the data. Duplicate Person schemas with slightly different facts
are worse than none — which node should a machine trust?
llms.txt. The site serves /llms.txt, a plain-markdown summary
of who I am, my work history, and the site's pages — generated at build time
from the same resume.json, so it can never drift either.
An explicit robots.txt. AI crawlers — GPTBot, ClaudeBot, Claude-SearchBot, PerplexityBot, Google-Extended — are explicitly allowed. Being citable by LLMs is the goal, not a side effect.
The design system
The visual language is deliberately restrained — I think of it as an editorial engineer's notebook. The rules are simple:
- One accent color. Green. No gradients, no second accent.
- Warm paper and ink tokens instead of pure white and black.
- Three typefaces with fixed roles: Newsreader (a serif) for headings, Geist Sans for body text, and Geist Mono for metadata — dates, tags, and the little labels you see around the site.
- Minimal animation, with a global
prefers-reduced-motiongate that disables all of it for readers who ask for that.
Constraints like these are most of what makes a site feel designed rather than generated. Every decision the system makes for me is one I can't get wrong on a Tuesday night.
Build details
A few things under the hood, for the curious:
- Turbopack in development, webpack for production builds. Turbopack's fast refresh is worth it daily; the production build stays on webpack for stability. This split is also why the blog you're reading compiles MDX at render time in Node rather than through a bundler plugin — one code path that behaves identically under both bundlers.
- A Serwist service worker precaches assets and applies stale-while-revalidate policies to fonts, images, and scripts, so repeat visits are fast and the site works as a PWA.
- Open Graph images via
next/og, drawn as JSX at build time — including a per-post image for this blog with the post title on it. - RSS and sitemap are plain route handlers that read the same post frontmatter as this page, generated statically at build time.
What's next
This post is the first output of the content engine it describes: filesystem
MDX, an RSS feed, BlogPosting structured data, and reading-time
metadata, all wired into the sitemap and llms.txt. The plan is case studies
from real work — serverless patterns that survived production, Django REST
lessons, and the numbers behind performance work.
If you're building something similar and want to compare notes, email me or connect on LinkedIn — I enjoy talking shop.