
Notes on Making a Desktop Website Stay Honest to the Web
I used to explain this architecture as two layers.
One layer for the desktop UI.
Another layer for full-page SEO routes.
That explanation was useful at the beginning, because it helped me separate two things that are easy to mix up: the interface people see, and the meaning machines need to read.
But after the system grew, I started to feel that the phrase "two layers" could be misunderstood.
It can make the SEO route sound like a separate backup page.
It can make the window UI sound like the real product.
And slowly, that mental model can lead to two versions of the same content drifting apart.
The architecture I trust more now is simpler:
one public URL,
semantic content for the web,
window UI for the human experience,
and one source of truth behind both.
Same address.
Different responsibilities.
That small shift matters.
It lets the website feel like a desktop without pretending the web underneath it does not exist.
This article is the technical companion to the previous note: Desktop-Style Website with Powerful SEO.
There, I wrote about the principle.
Here, I move into how the system works: content source, publish step, semantic route, metadata, sitemap, AI-readable files, and on-demand revalidation.
1. The Problem with App-Like Websites
An app-like website is fun to build.
You can make windows.
You can make a dock.
You can make icons, previews, folders, search, keyboard-like behavior, and little interactions that make the website feel alive.
But the web has a different set of expectations.
A URL should be meaningful.
A page should be readable without waiting for a client-only application to assemble everything.
Metadata should match the content.
The sitemap should expose important pages.
Structured data should clarify what the page represents.
AI-readable files should not tell a different story from the site itself.
This is where many app-like websites become fragile.
The UI looks rich, but the meaning is trapped inside client components.
The content exists, but only after JavaScript runs.
The route exists, but it does not carry enough context.
That is fine for some dashboards or private tools.
But for a public personal website, portfolio, store, and notes system, I do not want the content to be hidden behind the metaphor.
The metaphor should make the experience warmer.
It should not make the web weaker.
2. One URL Should Be Enough
When someone opens:
/insights/building-seo-routes-for-a-desktop-style-website-in-nextjs
that URL should be complete.
It should not be only a shell that says, "Please wait until the app opens the real article."
It should already know its title.
It should already know its description.
It should expose canonical metadata.
It should have Open Graph and Twitter card data.
It should have JSON-LD.
It should render the article in semantic HTML.
It should appear in the sitemap.
It should be represented in llms.txt and llms-full.txt.
But at the same time, when a person opens the URL in a browser, I still want the website to feel like my website.
Not like a plain article page accidentally separated from the desktop.
So the route does two jobs:
public URL
-> semantic route content for SEO and crawlers
-> desktop shell opens the matching window for users
The URL is the meeting point.
That is the heart of the system.
3. Source of Writing vs Source of Runtime
I still like writing in MDX.
It is simple.
It keeps the writing close to the repository.
It lets me edit in VS Code without turning the content process into a heavy CMS ceremony.
But there is a difference between the source I use for writing and the source the deployed website uses at runtime.
Earlier, the local MDX files were both:
- the writing source.
- the runtime source.
That is okay when content is small.
But long-term, the site can grow into hundreds or thousands of notes, products, documents, quotes, audio metadata entries, and desktop items.
If every content file must be parsed during build forever, the build becomes heavier than it needs to be.
So the newer flow is:
content/notes
-> publish script
-> Firestore + Storage
-> content loader
-> route metadata + semantic HTML
-> desktop window state
-> sitemap + llms files
The local file is still the CMS source.
Firestore is the runtime content source.
Storage or CDN is the runtime asset source.
This lets writing stay comfortable, while the deployed website becomes less dependent on local content folders being bundled into the build output.
That is a quieter architecture.
Not flashy.
But calmer.
4. What the Publish Step Actually Solves
The publish step is easy to underestimate.
At first, it can feel like an extra command.
But it solves several practical problems.
It uploads images from the content folder to Storage.
It rewrites local MDX image references into stable public URLs.
It writes normalized documents into Firestore.
It marks removed content as inactive when needed.
It calls revalidation so public routes, sitemap, and AI-readable files do not stay stale.
The important thing is that the deployed website can now treat Firestore as the content source of truth.
The local MDX remains the authoring source.
That distinction is small, but it keeps the system future-proof.
If one day I write from a custom editor instead of VS Code, the runtime architecture does not need to be rewritten.
It would still publish normalized content into the same shape.
That is why I like this flow.
It is not tied too tightly to the tool I use to write.
It is tied to the content contract.
5. The Route Renders Semantic SEO Content
The route page has a quiet job.
It fetches content from the loader.
It builds metadata.
It renders JSON-LD.
It exposes semantic HTML.
For an insight detail route, the shape is:
export const dynamic = "force-static";
export const dynamicParams = true;
export const revalidate = false;
This means I do not need to prebuild every slug.
I also do not need to server-render every request.
The route can be generated on demand, cached as static output, and refreshed through explicit revalidation when the content changes.
This is the part that feels right for content that grows over time.
SSG is excellent when the number of pages is small and stable.
SSR is useful when the page must be fresh on every request.
CSR is fine for account pages, dashboards, and interfaces that do not need to rank.
But for notes, products, quotes, audio pages, and preview pages, I want something in the middle.
I want prerendered meaning without forcing every possible page into every build.
On-demand static generation gives that balance.
6. Metadata Should Be Close to the Content
Metadata drift is one of those problems that looks small until it accumulates.
The article title changes, but the Open Graph title does not.
The description improves, but the sitemap still points to an old route.
The page body says one thing, while llms-full.txt describes something else.
No single mismatch feels fatal.
But together, they make the website feel less maintained.
That is why I prefer metadata to be derived from the same published content.
For a detail route, the page can use the same content source for:
titledescription- canonical URL
- Open Graph
- Twitter card
- JSON-LD
- article body
- sitemap entry
- AI-readable summary
Not every field must be fully automatic.
Editorial control still matters.
But the source should be close enough that the public meaning does not split into many versions.
7. DesktopShell Reads the Same Pathname
The desktop UI does not need a separate route to know what to open.
It reads the current pathname.
In the desktop layout, the page-specific route content is rendered first:
{children}
<div className="fixed inset-0 overflow-hidden">
<DesktopShell ... />
</div>
The children are route-level content: metadata, JSON-LD, and semantic article HTML.
The DesktopShell is the interface layer that sits above it.
Inside the shell, the pathname is matched to content:
const noteForPath = noteItems.find((item) => item.routePath === pathname);
If the pathname is an insight route, the Notes window opens to that insight.
If it is a work route, the Notes window opens to that work item.
If it is a product route, the Store window opens to that product.
If it is an audio route, the Music window opens to the right category, album, or track.
If it is a preview route, the Preview window opens to the item.
This means the route is not separate from the experience.
The route is the experience trigger.
That is why the model now feels cleaner than the older "SEO page vs window page" explanation.
8. The SEO Renderer and the Window Renderer Should Not Be Forced Together
This is one boundary I still care about.
The content source can be shared.
The renderer should respect its context.
The Notes window renderer is allowed to care about the reading experience inside a window.
It can care about rhythm, panel width, scroll behavior, and visual mood.
The SEO renderer has a different responsibility.
It should produce semantic article HTML.
Headings should remain headings.
Paragraphs should remain paragraphs.
Tables should remain tables.
Images should have stable URLs.
Code blocks should not break mobile layout.
Blockquotes should remain meaningful.
Custom blocks like quote, hadith, or quran blocks should still become readable HTML.
Forcing the window renderer and SEO renderer to become exactly the same can feel DRY, but it is not always better.
Sometimes "one renderer for everything" creates hidden compromises.
The better boundary is:
same content
-> semantic SEO renderer
-> desktop window renderer
Two presentations.
One truth.
9. Sitemap Is the Formal Map
In a desktop UI, not every route is discovered through ordinary navigation.
Some content is opened through a dock icon.
Some through Spotlight.
Some through a desktop item.
Some through a folder.
Some through a window state.
That is good for the experience.
But crawlers still need a formal map.
The sitemap should list the real public routes:
/insights/insights/[slug]/work/work/[slug]/doa/doa/[slug]/quotes/quotes/[author]/quotes/[author]/[quote]/store/store/[category]/store/[category]/[slug]/preview/preview/[slug]/audio- audio category, album, and track routes
The important point is not the exact priority number.
The important point is that public content can be discovered without understanding the desktop metaphor.
The sitemap is the formal map.
The desktop is the experience map.
Both should point to the same world.
10. Robots and AI-Readable Files Should Stay Boring
I like boring robots rules.
If the website should be indexed, allow it.
If the sitemap exists, point to it.
Do not make crawlers solve a puzzle.
The same is true for llms.txt and llms-full.txt.
They are not replacements for good HTML.
They are not replacements for structured data.
They are a readable map for machines and humans who want a quick understanding of the website.
For this site, they should include the real public content:
- profile and about pages.
- notes and insights.
- work.
- doa.
- quotes.
- store products.
- audio pages.
- preview items.
If these files become stale, they become less useful.
So they are revalidated together with content changes.
Again, the goal is alignment.
One website.
One public meaning.
Many surfaces.
11. On-Demand Revalidation Keeps the System Calm
The content publishing flow should end by refreshing the public pages that depend on that content.
For notes, that means the changed note route, collection routes, sitemap, llms.txt, and llms-full.txt.
For products, that means product routes, category routes, store index, sitemap, and AI-readable files.
For desktop items, that means preview routes and the global content map.
The revalidation endpoint makes that possible:
publish content
-> write Firestore / upload assets
-> call /api/revalidate
-> refresh affected paths
This avoids two extremes.
I do not need to rebuild the whole site for every content edit.
I also do not need to make every route SSR just to stay fresh.
The content changes when I publish it.
The static output refreshes when I ask it to.
That feels like the right amount of control.
12. What This Architecture Protects
The goal is not to make the code look impressive.
The goal is to protect the website from quiet problems.
Content should not be trapped inside a window component.
SEO should not be a separate page that slowly becomes outdated.
Metadata should not tell an older story than the article.
Sitemap should not forget routes that the UI can open.
AI-readable files should not become a museum of old assumptions.
The build should not get heavier just because the writing grows.
And the desktop experience should not become so playful that it forgets the web underneath it.
That is the balance I am trying to keep.
I want the surface to feel alive.
But I want the structure underneath it to stay boring in the best way.
Routes.
Metadata.
Semantic HTML.
Sitemap.
Structured data.
Readable content.
No drama.
Just a clear system doing its job.
13. The Trade-Offs Are Worth Naming
This architecture is not free.
It adds a publish step.
It asks the content model to be consistent.
It requires a revalidation endpoint.
It needs discipline so the SEO renderer and window renderer do not drift.
It also means the local content folder is not the only place that matters anymore. Firestore and Storage become part of the publishing story.
But I think the trade-off is fair.
The build gets lighter.
The content can grow.
The UI stays expressive.
The route stays meaningful.
And SEO does not depend on hiding a second version of the page somewhere else.
That is enough reason for me.
Closing
I still like the desktop metaphor.
Maybe because it makes a personal website feel less like a brochure and more like a small space that can be explored.
But the web has its own dignity.
A URL should mean something.
A page should be readable.
Content should be shareable.
Search engines and AI crawlers should not need to guess what is hidden behind the interface.
So the architecture I trust now is not "desktop first, SEO later."
It is:
one URL
-> semantic web content
-> desktop window experience
-> one content source behind both
Not separate stories.
One story, rendered with care.
