The Display Content Seam That Kept Astro Pages Small
Problem
A valid content document is not always ready for a page. Internal links still contain Route Keys. Images still contain Sanity asset references. Featured collections still need limits and ordering. Page copy can also live in a separate chrome tree that each route must join with its data.
My first content abstraction exposed a bag of getters. Pages still knew which getter pairs belonged together. They also prepared images and combined page chrome with editorial records. The abstraction moved access behind functions, but it did not remove enough knowledge from each page.
This was a depth problem. A shallow module hides syntax while its callers still understand the work. I wanted one seam that accepted validated Content and returned the complete view that routes render.
Decision
I made Display Content the page seam. Display Content is one render-ready document for the site. It contains resolved internal links, prepared Content Media, selected featured items, lookup indexes, and chrome inside each page slice.
Pages and layouts read this document through Astro.locals.content. They do not receive the Content Contract directly. They do not call Sanity, resolve Route Keys, or prepare Content Media.
The factory createDisplayContentFromContent() owns the conversion. This boundary lets the shared Content Contract describe editor-owned facts while the portfolio owns presentation-ready choices.
Implementation
The load path starts with unknown external data. A Snapshot adapter calls parseSnapshotEnvelope() and extracts validated Content. The local Sanity adapter calls the shared Snapshot Query, which also returns Contract Content. Both adapters pass Content into createDisplayContentFromContent().
The factory resolves several frontend concerns in one pass. resolveContentLink() converts each internal Route Key into a real path and leaves an external URL as an external destination. resolveSiteSettings() converts Sanity brand media into ogImage, faviconUrl, and appleTouchIconUrl. The returned site object does not expose Contract brandMedia.
The factory prepares Content Media through optimizeRichContentBody(), getOptimizedProject(), and getOptimizedCertification(). Those functions create image URLs and source sets from Sanity references. Pages receive an OptimizedImage shape with src, srcSet, dimensions, and sizes when applicable.
Featured selection also belongs in the factory. Home gets at most two featured projects and one featured blog post. The blog choice sorts marked posts by publishedAt and selects the newest one. If no item carries a featured flag, the factory uses the first items as a fallback. The home page receives the completed choices:
const content = Astro.locals.content
const { home } = content
const projects = home.featuredProjects
const post = home.featuredPostThe page does not repeat limits or sorting. It does not require the full collection to make the same decision again.
Page chrome follows the same rule. Page chrome is visitor-facing copy around the main records, such as section headings and view-all links. The factory puts each chrome group inside its owning slice. content.projects.chrome travels with project items. content.home.chrome.featuredProjects travels with home selections. Shared footer and exploration links sit at the document level because layouts and multiple pages use them.
Middleware creates the request boundary. src/middleware.ts calls loadDisplayContent() and assigns the result to context.locals.content. The loader caches its promise and result for the build scope. Public routes read the same Display Content document.
Dynamic routes need one exception because Astro computes their paths before route rendering. src/pages/blogs/[slug].astro and src/pages/projects/[slug].astro call loadDisplayContent() in getStaticPaths. During page rendering, they still read site and shared values from Astro.locals.content. The exception uses the same loader and does not create a second content path.
The contact API bypasses Content loading because it is the request-time route. shouldBypassContentLoading() prevents middleware from loading the site document for that endpoint.
Structural tests enforce the seam. data-import-audit.test.ts scans components, layouts, and pages. It rejects imports from @/data/*, direct Sanity clients, Contract Content use, and Content Media preparation helpers. Unit tests also prove route resolution, image preparation, chrome placement, and featured limits at the factory.
Rejected Options
I rejected the getter bag because pages still coordinated several getters. Each page needed knowledge of media recipes and related chrome. The interface looked small, but the caller responsibility stayed large.
I rejected a sibling page chrome tree. That structure made every page rejoin copy with the data that it described. Chrome inside each slice gives a route one coherent input.
I rejected Content Media preparation in pages. That option spread CDN details, width recipes, and source-set rules across route files. It also made a future image policy change require edits in many consumers.
I rejected putting prepared images into the shared Contract. The Contract crosses systems and stores durable content facts. Image delivery recipes belong to the frontend that knows its layouts.
Accepted Tradeoffs
The factory is a central module with broad knowledge of the page-facing model. A change to Content shape or page composition often touches it. This concentration is intentional, but it needs focused tests and clear function names.
Display Content can duplicate references. Certifications appear in home and About slices, for example. The structure favors simple consumers over a fully normalized object graph.
The middleware also gives every public page the full document, even when a page reads one slice. Static generation and build-scoped caching make that acceptable for this portfolio. A much larger content set can require smaller loading boundaries.
Structural tests inspect source patterns, so they can need updates after harmless syntax changes. Their value comes from protecting architecture rules that type checks cannot express.
Lesson
A useful frontend seam removes policy from consumers. It does more than rename access. Pages became smaller when the adapter owned route mapping, media recipes, selection rules, and chrome joins together.
The seam also gave tests a stable target. Most content behavior can run in a unit test without rendering an Astro page. Structural tests then make sure that new pages do not bypass that tested path.
Next-Project Rule
For my next content-backed site, I will design a render-ready view model before many pages exist. I will place route resolution, media preparation, collection limits, and related copy in one owning adapter. Pages will read fields from that model. I will add structural tests for boundaries that imports and types do not protect by themselves.