Render each site’s own layer from the CMS’s windows inside the site package, cached at the edge by tag
Context and Problem Statement
ADR-0031 gave every site the shared layer as one package, but left the own layer (ADR-0023:
products, services, cases, articles) to each site’s pages, written into the repository. That is
not how the company works. Since September 2026 akribis.info’s cms-integration branch renders
its catalogue, its sections and its publications from the CMS (Wagtail, PIM-de-Akribis-One,
cms.akribis.one) at request time on its Worker: an editor publishes and the page changes, with
no build in between. The CMS serves every department through the same windows
(/api/<department>/…, cms/CONTRACT.md); AKRIMET’s tree already exists there, unpublished
“until its site launches”. The code that reads the windows (catalogo.ts, cmsResources.ts,
cmsEmbeds.ts, CmsBody.astro, the edge-cache middleware) lives in akribis.info alone, styled
with akribis.info’s own tokens. How does every site read the CMS, with one client, the brand’s
patterns and the standards checked, and how is what it renders kept fast and fresh?
Decision Drivers
Criteria 1 (trust), 2 (one family), 4 (accessibility), 7 (three languages) and 9 (generated and tested) of ADR-0009; ADR-0023 (the layers), ADR-0025 (the standards), ADR-0031 (the package); akribis.info’s docs/02 §17–§21 (prefixes, redirects, the axes, the language chain, the portfolio, the registration form); the CMS contract, verified against the live CMS on 2026-10-06.
Considered Options
- The CMS mode in
@akribis/site: one option,cms: { department }, turns a site into a CMS client rendered on request, with the client, the router, the templates and the cache inside the package - Each site keeps its own CMS client, as akribis.info does
- A build-time export: the site fetches the windows during
astro buildand ships static pages
Decision Outcome
Proposed: the first option.
- One option.
akribis({ brand, kind, site, cms: { department, catalog, segments } })with the Cloudflare adapter andcache: { provider: cacheCloudflare() }inastro.config. The department defaults to the brand. The integration declaresCMS_ORIGINandCMS_KEYas Worker secrets read per request (astro:env), injects the routes below and the middleware, and fails at configuration time without the adapter or the cache provider. - The client moves, the rules with it.
src/cms/is akribis.info’s code in the package: every request throughfetchFromCms, the one place that sendsX-CMS-Key(a test enforces it); the department in the path, never a parameter; the language chain visitor → es → en → pt with the text markedlangwhen another language answered; products by importance then code, paged by the CMS; the embed allow-list with exact domains. Each window’s answer is normalised in one place, because the deployed CMS lags the contract (2026-10-06: products withoutslugorrelated_products; publications withtime_rangeas one string,organizersnull). - The tree is the site.
/[locale]/[...path]asks window 4 for the page at that path and draws it by itstype: a section with its children, a quantity with its products, the catalogue’s index with the whole storefront, a rubric with its publications, the contact page./<lang>/<catalog>/<product>/<slug>/is a product (window 2); a path under a rubric’s index that window 4 does not know is a publication (window 9). A page’s paths per language come from its parent’s children (window 5): another language’s path answers 301 to the page’s own, so no page has two addresses. A site’s own page claims its address before the CMS route; a path the CMS does not know is the shared 404. While the CMS’s slugs are Spanish in every language,cms.segmentsmaps the first segment per language (akribis.info’sSECTIONS), so its addresses stay as they are. - The brand draws it.
CmsBodyrenders the fourteen block types with the tokens only;PageHead(breadcrumbs from the tree, with BreadcrumbList JSON-LD),ProductGrid,Pagination(?page=N, page 1 without it, out of range a 404, prev and next in the head),RegistrationForm(the campaign’s Form Handler, fields in the editor’s order). Products carry Product JSON-LD, events Event, notes Article. The words around the content are Brand’s strings (strings.yaml,cms). - Cached at the edge, purged by tag. Each page sets
Astro.cache.set({ maxAge, swr, tags }):cms:<department>,page:<path>,product:<code>,category:<key>,rubric:<kind>,resource:<key>. Cloudflare’s Worker cache layer serves it; Django is asked only on a miss. It replaces akribis.info’s hand-madecaches.defaultmiddleware, whose premise (that the network does not cache what a Worker generates) the Worker cache layer no longer holds.POST /_cms/purge/with the key and{"tags": […]}empties every cached page with those tags: a product’s tag reaches its page and every grid that shows it, on every site, without the CMS knowing any site’s routes. - The tree is the navigation.
MegaNav(anatomy:site-nav, the second header pattern ADR-0031 left open) draws the root’s children as the menu; an item’s children are its panel, and a child with children of its own becomes a column (Instrumentación → Magnitud → twenty quantities); a rubric’s index stays a link. It is built on<details>, so every panel opens and every link works without JavaScript; the script adds one panel at a time, Escape, a click outside, and the folded list behind a menu button on a phone.CmsShellmounts it on every page of a CMS site. - The home is composed from what the department publishes. The CMS has no window for a
home: the page shows the brand’s verb and area with the canonical CTAs, the sections with their
summaries, the storefront’s six most important instruments, the events ahead and the latest
news, each block only when the CMS has something for it, and the AKRIBIS One module. A site that
writes its own home claims
/<lang>/first. - The shared pages render on request too. Contact, thank-you, privacy and the legal notice
carry the same navigation, so they are injected per language at their slugs (never as a
[page]pattern that could claim a CMS address) and cached like the rest.POST /contact/and/(the visitor’s language) become routes: the adapter owns the Worker, socreateWorker()’s jobs move into the package, sharing its code. The middleware gives every rendered response the header baseline, which_headerscovers only for files. - Checked twice. The build checks what it wrote (the 404 and the headers). The
live check (
src/check-live.ts) crawls a running site from each home and the shared pages, checks every response’s headers as served, saves the pages and runs the same checker over them.
Consequences
- Good, because a new unit’s site is its brand, its department and its tree in the CMS; its pages exist when an editor publishes them.
- Good, because the CMS client, its edge cases and its tests have one home; akribis.info’s copy ends when it is rebuilt on the package.
- Good, because a page cached at the edge does not wait on Django, and a publish purges exactly what changed.
- Good, because the live check found on its first run what the build cannot see: 564 missing headers, 26 titles over the maximum, one publication answering at two addresses; all fixed in the package.
- Bad, because the edge cache and the purge cannot be verified locally:
wrangler dev’s runtime has nocache.purge. They are verified on a preview version before a site depends on them. - Bad, because content now reaches a page without passing a build: the voice, claims and media rules run in the live check, not before publishing. The CMS should run them when an editor saves.
- Open: the CMS calling
/_cms/purge/on publish (today it purges by URL through the zone API,cms/content/cache.py); translated slugs in the CMS, after whichsegmentsgoes; the deep header with the mega menu from window 5; faceted filtering (CONTRACT, open questions); the embeds the CMS offers (YouTube, Vimeo, LinkedIn, maps) against ADR-0034, which admits Cloudflare Stream only; the KV binding the adapter adds for sessions, which no CMS page uses.
More Information
The proof is packages/cms-proof: akribis.info’s settings against the akribis department,
built on every Brand build and run by hand against the live CMS (wrangler dev --env-file). On
2026-10-06 the live check crawled 423 pages and left 120 findings, all content or policy: voseo in
the CMS’s text (40), headings that skip a level (66), titles over the maximum by themselves (7),
YouTube embeds (6) and one claim (“ISO/IEC 17025 accredited”, on a manufacturer’s product page).
With the navigation on every page, the CMS’s contact page title, «Hablá con un especialista», adds
140 voice findings: one title in the CMS, whose canonical form is «Hablar con un especialista».