Web Engineering
Type-Safe Content: Building a Maintenance-Free Website
Why we keep app metadata in structured files, validate it before publish, and let the website render from content instead of one-off page edits.
The usual website drift problem
Marketing sites tend to decay in predictable ways. An app description changes in one place but not another. A screenshot set gets updated on one page while an older thumbnail survives somewhere else. A store link works in the hero but is broken in the footer or support copy.
That is usually a content-model problem disguised as a front-end problem. The fix is not to be more careful when editing. The fix is to make the same careless edit impossible to make in the first place by creating a single source of truth.
When a team adds pages by copying markup and editing text inline, every update becomes a search-and-replace exercise across a growing number of files. There is no guarantee of consistency — only a set of pages that happen to agree until someone edits one without updating the others. For a product portfolio with five apps and a dozen pages per app, that breakdown happens quickly.
JSON as the content layer
For XAppNova, we chose a simpler starting point than a headless CMS: all app and category data lives in typed JSON files inside the repository, and the website reads from that structured layer at build time. The goal was to remove unnecessary editing surfaces, not add another admin system before it was needed.
Every app entry is a JSON file that conforms to a TypeScript App interface. It has required fields — name, slug, description, platforms, pricing model, store links, icon, hero image, screenshots, and features — and optional ones that get added when the app supports them. The interface is the contract. If the JSON breaks the interface, the TypeScript compiler tells you before the site ever builds.
The website never contains hardcoded app data. The home page, app detail pages, category pages, support pages, and sitemap all read from the same JSON files. When a store URL or tagline changes, it changes once and updates everywhere automatically. There is nothing to search, nothing to miss, and no opportunity to forget a second occurrence.
TypeScript across the monorepo
The content package exports its types with a simple index that re-exports from the individual schema files. The web app imports those types directly using workspace package resolution. There are no build steps needed for the types themselves — the TypeScript compiler reads the source files through the path alias configured in each package's tsconfig.
That design means you get full autocomplete and error checking when writing new page templates or when accessing properties from a fetched App object. A page template that tries to read a field that does not exist on the type fails to compile, not at runtime in production. The error surfaces at the point of confusion, not hours later on a live site.
The strictness pays off particularly when you have multiple different pages that each use a different subset of the same App interface. Adding a new field to the type surfaces every page that might want to use it, and TypeScript flags any page that accesses a field incorrectly. That kind of immediate feedback is worth more than documentation.
Validation is the real guardrail
The important part is not just that the content is typed. It is that changes can be checked against a publishing contract before they go live. Required fields, URL format requirements, image dimension ranges, platform values, and other structural assumptions get verified automatically by a validation script that runs before deployment.
The publishing contract lives in a separate JSON file that describes what a valid published app entry looks like. The validation script reads both the data files and the contract, then returns a list of violations with specific error messages. That gives a tight feedback loop: a broken app entry fails fast with a useful message instead of turning into a quiet production bug.
This matters most when adding a new app to the portfolio. It is easy to forget a required field, use an unsupported platform value, or write a slug that does not match the expected format when working from memory. The contract catches those errors before the pull request is merged.
Static generation changes the deployment equation
Because the content is static JSON read at build time, every page can be fully pre-rendered. Next.js App Router's default is Server Components, which means each page fetches its content during the build, not on each request. The site ships as flat HTML and CSS served from the edge.
The practical effect is that performance is almost entirely a function of asset sizes and image optimization, not server response time. There is no database query in the critical path. There is no application server that can slow down under load or fail during a request.
It also simplifies deployment considerably. The build output is a set of static files that can be served by Nginx, a CDN, or any static hosting provider. The current setup runs on a VPS with Nginx serving the Next.js output directly. There is no process manager needed for request handling and no memory leak that accumulates between restarts.
When a CMS makes sense
This approach has a ceiling. If the people responsible for editing content are not comfortable with JSON files and pull requests, the friction becomes a real problem. A CMS with a visual editor is the right answer for a team where non-engineers need to publish content regularly without a developer in the loop.
For a single developer managing a handful of apps, the JSON-in-repo model removes more overhead than it adds. The editing surface is a text editor and Git. The review process is a pull request. Content changes go through the same tooling as code changes, which makes rollbacks straightforward and audit history automatic.
The right time to reach for a headless CMS is when the content volume outgrows a manageable file structure, or when content editors need to work independently from the development cycle. Neither condition applies to XAppNova's current size, but the data model is deliberately structured so that a migration to a CMS later would be straightforward — the interfaces are already defined, and the fetch functions are already abstracted.
Less maintenance through better structure
A maintenance-friendly website is not one with more tooling. It is one where adding a new product or updating an existing one follows an obvious path and does not require manual cleanup in multiple places.
For XAppNova, that means content-first pages, static generation at build time, and enough type safety to make the easy path also the correct path. Adding a new app requires one JSON file, a set of image assets, and a run of the validation script. Nothing else needs to change. The sitemap updates automatically. The category pages update automatically. The app directory updates automatically.
That discipline keeps the site consistent without requiring ongoing vigilance. The structure enforces the standard. The validator catches exceptions before they reach production. The build handles distribution. The goal is to make quality the default outcome, not the result of remembering to check.