Skip to main content
Software Engineering

A content architecture that survives a redesign

Websites are rebuilt every few years and the content outlives every one of them. Model it so the next redesign is a template change.

Osynova engineering 5 min read Updated 19 Sep 2026
Screen showing SVG and CSS source code in a dark editor.

A company website gets rebuilt roughly every three years. The content does not change nearly that often — the same services, the same portfolio, the same articles, wearing a new layout.

Yet most rebuilds are full rewrites, because the content was never separable from its presentation. It got hard-coded into templates, or modelled as page-builder blocks that only one theme can render.

The goal is a content model where a redesign is a template change.

Model content, not pages

The most common mistake is a pages table with a big HTML body, edited in a rich-text field.

It works immediately and fails slowly. Once markup lives in the database, the content is welded to one design. Changing how services are presented means opening every service in an editor and reworking the HTML by hand.

Model the thing itself. A service has a title, a tagline, a set of capabilities, a set of outcomes and a set of deliverables — as structured fields, not as a <ul> somebody typed.

The template decides that capabilities render as a two-column list. When the design changes, the template changes and the data does not move.

Where free-form text belongs

Long-form prose still needs to be free-form. Store it as Markdown rather than HTML.

Markdown constrains authors to semantic structure — headings, lists, emphasis — rather than arbitrary markup with inline styles. Rendering is yours: you control heading anchors, table wrapping, image handling and link behaviour at render time, consistently, across content written years apart.

Escape HTML on render rather than trusting stored input. Content in a database is not automatically safe just because an administrator typed it.

Publication state is not a boolean

is_published seems sufficient until the requirements arrive.

A nullable published_at timestamp handles more with less: null is a draft, a past date is live, a future date is scheduled. One column, one index, and scheduling comes free.

Visibility is a separate axis from publication, and conflating them causes real problems. On this site, a project has three visibility states:

  • Public — fully visible
  • Confidential — listed and crawlable, but client identity, external links and repository links are suppressed
  • Private — never appears in any public context, including the sitemap

"Show the work but not the client" is a genuinely common requirement and a boolean cannot express it.

The important part is enforcement. Define one query scope that applies both the publication check and the visibility exclusion, and require every public entry point — listings, detail pages, the sitemap, related-content queries — to use it. A private record leaks the moment one route filters slightly differently, and that route is always the one nobody remembered.

Resolve SEO metadata in exactly one place

SEO decays predictably. A title is set in a controller, another in a template, a default in a layout, and eventually nobody can say which one wins on a given page.

Resolve it once, with a fixed precedence:

  1. Administrator-defined metadata for that record
  2. Controller-supplied fallbacks
  3. Site-wide configured defaults

Controllers build a metadata object and hand it to the layout, which renders the entire head — title, description, canonical, Open Graph, Twitter card, JSON-LD. No template reads metadata directly. Once templates cannot set a title, an entire class of duplication becomes impossible rather than merely discouraged.

Attach the overrides polymorphically so any content type can carry them without a separate table each. Eager-load the relation in detail actions, or lazy-load prevention will find it for you.

Generate the sitemap from the same rules

A sitemap maintained separately from the visibility rules will disagree with them. That is not a risk, it is a certainty on a long enough timeline.

Build it from the same scopes the public pages use. If a project is private, it is absent from listings and from the sitemap for the same reason, expressed once.

Let editors add pages without letting them break routes

Serving CMS pages from the site root — /about-us rather than /pages/about-us — is better for SEO and for humans, and it introduces a hazard: a catch-all route that can swallow real ones.

Two things make it safe. Load the catch-all last, after every real route is registered. And keep a reserved-slug list enforced at validation, so an editor cannot create a page called login and quietly take down authentication.

We know the ordering matters because it bit us once, on exactly that route.

Handle images at upload, not in the template

An image uploaded once is served millions of times. Process it on the way in.

Generate responsive variants at fixed widths in a modern format, store the variant map alongside the record, and render a <picture> with a source set. Self-host them — a third-party image CDN is another dependency in the critical path for no benefit at typical volumes.

Then make alt text a first-class field on the upload form. If it is optional and buried, it will be empty, and the accessibility problem you have created is invisible to everyone who built it.

Watch what you cache

A practical note that cost us time: if your cache store rejects deserialising arbitrary classes — a sensible security default — caching an Eloquent model appears to work on write and returns an incomplete object on the next request.

Cache plain arrays and rehydrate them into models on read. The failure is confusing precisely because the write succeeds.

The test

A useful check on any content architecture: could you rewrite every template, change the entire visual design, and leave the database untouched?

If yes, the next redesign is a project measured in weeks. If no, it is another rewrite, and the content will be migrated by hand again.

Share this article

Working on something similar?

If this is the kind of problem you are facing, tell us about it.