TL;DR: My personal site is a fully static Next.js export: every article is a markdown file plus one typed entry in a registry, and that registry is the single source of truth for the page metadata, the sitemap, the structured data and an llms.txt file for AI crawlers. Nothing runs on a server at request time. Pages are plain HTML on Firebase Hosting, each with a canonical URL, Article and FAQPage JSON-LD, and an FAQ section that is actually visible on the page. New posts get pushed to search engines with IndexNow after every deploy. The whole setup is boring on purpose, and boring is what makes it easy for Google and for LLMs to read.
Most developer blogs I see are built one of two ways. Either they sit on a hosted platform where the author does not control the markup, or they are a full app with a database, a CMS and a server rendering pages that never change. The first gives you no control over what search engines see. The second gives you a lot of moving parts for content that is, in the end, text.
I wanted something in between: full control over every tag, and nothing that can go down. This is how the blog you are reading is built, and why each piece is there.
Why a static export
The site uses Next.js with output: "export" in the config. At build time Next renders every route to an HTML file, and the deploy is just a folder of files. There is no server, no database and no runtime to patch.
For a content site that buys three things:
- Speed without effort. The HTML is complete when it arrives. No client-side fetching, no loading state, nothing for a crawler to wait on.
- Crawlers see exactly what users see. Some AI crawlers do not run JavaScript at all. If your article body is fetched in the browser, those crawlers see an empty shell. With a static export, the full text is in the response.
- Almost nothing to maintain. I wrote a whole post about what software needs after launch. A static site skips most of it. The main upkeep left is the build tooling itself.
The trade-off is that anything dynamic has to happen at build time. For a blog that is the right trade.
The registry is the source of truth
Each article lives in content/blog/<slug>.md. The body is plain markdown, rendered to HTML at build time with marked. Everything else about the post lives in one TypeScript array, POSTS, with a typed entry per article:
slug,title,descriptionanddatekeywordsandreadMinutesfaq, a list of question and answer pairs
That array drives everything downstream. The dynamic route uses it in generateStaticParams to know which pages to build. generateMetadata reads it to produce the title, description, canonical URL, Open Graph and Twitter tags. The sitemap maps over it. The llms.txt route maps over it. The blog index lists it.
The point of this design is that a new article cannot be half-published. If it is in the registry, it is in the sitemap, it has metadata, it has structured data and it appears in llms.txt. If it is not in the registry, it does not exist. There is no second list to forget to update. Because the registry is typed, a missing field fails the build instead of shipping a page with an empty description.
This is the same instinct I bring to client products: one place where the truth lives, and everything else derived from it. It is also what makes the repo pleasant to work on with tools like Claude Code, which I wrote about in using Claude Code on client projects. Declarative data in git is easy to read, review and diff.
Metadata done properly, once
The per-post metadata is generated in one function, so every page gets the same treatment:
- A canonical URL for each post, so there is never any doubt which address is the real one.
- A real description written by hand for each article, not the first 160 characters of the body.
- Open Graph and Twitter tags so links shared on LinkedIn or X show a proper title and summary.
- Author information on every article.
The hosting config matters here too. Firebase Hosting is set to cleanUrls and trailingSlash, matching trailingSlash: true in the Next config. If those disagree, you end up with two URLs for every page, or redirect chains, and you spend months wondering why some pages are indexed under the wrong address. Pick one form and make the framework, the host and the canonical tags all agree on it.
Structured data that matches the page
Every post page includes two JSON-LD blocks.
The first is an Article: headline, description, date, and the author as a Person with links to LinkedIn, GitHub and X. That author block matters more than it used to. Search engines and AI systems are increasingly trying to work out who wrote something and whether that person is a real practitioner, and linking the article to consistent profiles is how you give them that signal.
The second is an FAQPage, built from the post's faq entries. The important detail is that the same questions and answers are also rendered on the page, in a visible FAQ section under the article. Structured data should describe content the reader can see. Schema for content that is not on the page is the kind of thing that eventually gets ignored or penalized, and it is also simply dishonest.
The FAQ is not decoration. I write those questions as the literal queries a reader would type, and the answers as two or three self-contained sentences. That format is exactly what a search snippet or an AI answer wants to lift.
llms.txt for AI crawlers
There is a route at /llms.txt that returns a plain text summary of who I am, what I work on, and a list of every article with a one-line blurb and its URL. It is generated from the same registry at build time, so a new post appears there automatically.
llms.txt is a proposed convention, not a standard every AI system honours. I added it anyway because the cost is close to zero and the upside is real: it gives any model or agent that does look for it a clean, structured map of the site, without navigation, scripts or styling in the way.
It started life as a static file in public/. I moved it to a route because I needed it to follow a site-wide flag. The site has a single boolean that controls whether my studio is mentioned anywhere, and when it is off, a small function rewrites those mentions in the markdown at render time. A hand-maintained text file would drift from that. A generated one cannot.
Writing for extraction
The technical setup only gets a page read. Whether it gets quoted depends on how the article is written, and this is where I think most developer blogs leave the most on the table.
A few rules I follow on every post:
- Answer first. Every article opens with a TL;DR that answers the question in the title directly. A reader skimming, a search snippet and an LLM summarizing the page all take the first paragraph, so the first paragraph has to be the answer, not a warm-up.
- Real headings. H2s and H3s that say what the section contains. A model splitting a page into chunks uses headings as boundaries, and a heading like "Thoughts" tells it nothing.
- Self-contained sections. Each section should still make sense if it is the only thing someone reads.
- Specifics from real work. The thing an LLM cannot generate on its own is first-hand experience. When I write about growing LectureNotes AI to 15M organic Instagram views, the numbers and the mistakes are the value. Generic advice is already in every model's training data. Your actual experience is not.
- Internal links between related posts, so readers and crawlers can follow a topic across articles instead of hitting a dead end.
Getting new posts discovered: IndexNow
A sitemap tells search engines what exists, but they visit on their own schedule. IndexNow is a protocol for telling them immediately that a URL changed. You host a key file at the root of your site to prove ownership, then send a request listing the new or updated URLs. Bing and Yandex, among others, take part.
After every deploy I ping IndexNow with the new article's URL. It is a single HTTP request. It does not replace the sitemap, and it does not guarantee indexing, but it shortens the gap between publishing and being crawled.
Caching and deploys
Static hosting still needs a caching policy, and getting it wrong means visitors see stale pages after a deploy. The rule in the Firebase config is simple:
- HTML:
max-age=0, must-revalidate, so every visit checks for the latest version. - JS, CSS, fonts and images: cached for a year as immutable, because their filenames change whenever their content does.
Deploying is one command: build, then firebase deploy for hosting. One gotcha that cost me time: the Firebase CLI did not run on the newest Node release I had installed, and the fix was to deploy with an LTS version. Build tooling is the one part of a static site that still ages, so pin the Node version you deploy with.
What I would still improve
Nothing here is finished, and I would rather say so than pretend otherwise:
dateModifiedis just the publish date. When I revise a post, the structured data does not show it. A real last-edited date per article is on the list.- Related posts are naive. The "Keep reading" links are not chosen by topic yet. Matching them by shared keywords would serve readers better.
- No generated social images. Each post shares with a text card. A per-article image would improve click-through from social.
None of these need a server. They are all build-time changes, which is the whole point of the setup.
The short version
If you are building a blog that you want both people and AI systems to find and trust: render it to static HTML, keep one typed registry that everything else is derived from, make the structured data describe what is visible on the page, publish an llms.txt, ping IndexNow on deploy, and write every article so its first paragraph is the answer. None of it is clever. That is why it holds up.
If you want a fast way to decide what is worth building first on a project like this, my guide on what to cut from an MVP applies to content sites too.
I build web products, AI agents and automation, and Unity and AR/XR apps for founders and teams in the US, UK and Europe. If you want a site or product that is fast, simple to maintain and easy for search and AI to understand, more about my background here, or book a call.