NNabeel Hassan

Blog · November 2, 2026 · 8 min read

How I Built My Developer Blog for Google and AI Search with a Static Next.js Export

By Nabeel Hassan — AI Engineer · ICPC World Finalist

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:

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:

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:

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:

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:

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:

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.

FAQ

Is a static Next.js export good for SEO?

Yes, for content sites it is one of the best options. Every page is complete HTML at request time, so search engines and AI crawlers that do not run JavaScript see the full article, and pages load fast with no server to maintain.

What is llms.txt and should my blog have one?

llms.txt is a proposed convention: a plain text file at the root of a site that summarizes who you are and lists your key pages for AI systems. Not every AI crawler uses it, but it costs almost nothing to generate from the same data as your sitemap, so it is worth adding.

Does FAQ schema still help a blog?

FAQPage structured data is useful when the same questions and answers are visible on the page. Write the questions as real search queries and the answers as short, self-contained sentences, which is also the format AI answers tend to quote.

What does IndexNow do?

IndexNow lets you notify participating search engines, such as Bing and Yandex, that a URL was added or changed, instead of waiting for them to recrawl your sitemap. You host a key file to prove ownership and send one request per deploy.

How do I make blog posts more likely to be cited by AI search?

Answer the title's question in the first paragraph, use descriptive headings, keep sections self-contained, and include specifics from your own work that a model cannot generate on its own. Make sure the full text is in the HTML and the author is clearly identified.

Building something in this space?

I take on AI-agent, automation and product work directly — scoped fast, shipped fast.

Book a discovery call →

Keep reading