Vai al contenuto

Delivery dei contenuti pubblici

ADR-030 supersede ADR-029. Il public site target usa Astro su Vercel con rendering server/on-demand e cache ISR bounded. Publish e unpublish non provocano una build frontend.

Responsabilità

Confine Owner
API, Editorial MCP, D1 e lifecycle editoriale architecture-coffee su Cloudflare
Astro, rendering, ISR, cache, SEO, ricerca e analytics architecture-coffee-public-site su Vercel
Contenuto pubblico REST bounded autenticata server-to-server

Il backend non renderizza pagine applicative e non orchestra build frontend. Il frontend non scrive stato editoriale e non accede a D1. I secret Access restano esclusivamente nel runtime server-side Vercel e non raggiungono browser o HTML.

Flusso target

flowchart LR
    E[Publish o unpublish] --> D[(D1 Cloudflare)]
    D --> A[REST bounded per modello]
    U[Richiesta lettore] --> V[Astro su Vercel]
    V -->|Access Service Auth, sola lettura| A
    V --> I[Cache ISR bounded]
    I --> U

Il deployment del codice frontend e l'aggiornamento dei contenuti sono lifecycle distinti. I contenuti diventano visibili tramite lettura API e revalidation della cache; una transizione editoriale non seleziona branch, SHA, tag, build, Worker o target. Dopo la prova DEV di #47, #382 ha rimosso la route family /public/v1/* e il relativo catalogo/snapshot globale.

Contratto REST bounded

D1 è l'autorità editoriale runtime. Access autentica il consumer server-side e ammette soltanto i path ADR-032; l'API impone visibilità, shape, bound e metodi.

Modello Collection/detail Visibilità pubblica
Issue /issues, /issues/{slug} PUBLISHED con data pubblicazione
Topic /topics, /topics/{slug} accepted
Knowledge Asset /knowledge-assets, /knowledge-assets/{slug} accepted
Glossary Term /glossary, /glossary/{slug} accepted

Le collection accettano limit 1..100 (default 50) e offset 0..10.000 (default 0). D1 legge al massimo limit + 1 righe del solo modello richiesto; il detail seleziona una sola riga visibile tramite slug canonico. Le response omettono revision, workflow editoriale, campi privati di planning/reference e relazioni cross-model non risolte. I metodi mutativi non hanno route.

Filtri tipizzati delle collection

Ogni collection possiede una specifica chiusa. Parametri diversi si combinano con AND; ripetere un filtro array richiede che il record contenga tutti i valori, mentre ripetere un filtro scalare produce 400. La stringa letterale null seleziona uno scalare pubblico opzionale assente. Valori vuoti, enumerazioni e numeri non validi, dot path non dichiarati e parametri sconosciuti producono 400 senza fallback permissivi.

Ogni campo della matrice può inoltre usare il solo suffisso dichiarato .any. Valori ripetuti di field.any si combinano con OR locale a quel campo; gruppi di campi differenti continuano a combinarsi con AND. Per uno scalare equivale a una equality multi-value; per un array richiede la membership di almeno uno dei valori. La forma base e .any dello stesso campo non possono comparire insieme e producono 400.

Collection Filtri scalari Filtri array ripetibili .any
/admin/editorial/taxonomies kind, canonicalKey, visibility, publicLabel, publicDescription, revision, updatedAt aliases tutti i campi elencati
/topics id, slug, title, titleIt, track, category, learningObjective, intendedAudienceValue, difficulty.level, difficulty.rationale tags tutti i campi elencati
/knowledge-assets id, slug, title, type, summary tags tutti i campi elencati
/issues slug, title, subtitle, issueNumber, category, readingTimeMinutes, publishedAt tags tutti i campi elencati

Non esistono altri suffissi o operatori: .or, .all, .in, confronti, parentesi, filtri JSON e OR tra campi arbitrari sono parametri sconosciuti e producono 400.

I repository D1 traducono soltanto questi attributi in colonne materializzate o path JSON statici, con tutti i valori passati tramite binding. I predicati sono applicati prima di ordinamento, limit, offset e calcolo di hasMore; restano invarianti le selezioni accepted per Topic e Knowledge Asset e PUBLISHED con publishedAt per Issue. Il repository statico delle tassonomie applica la stessa semantica. I gruppi .any scalari usano IN bound; quelli array usano una sola membership existential con valori bound. Colonne, path, operatori e predicati restano allowlist statiche possedute dai repository.

relatedTopics resta intenzionalmente escluso dai filtri di /knowledge-assets: è una relazione interna presente nel modello canonico ma non nella projection pubblica della collection. Sono esclusi allo stesso modo i campi interni Topic di planning/reference e i campi Issue disponibili soltanto nel detail. Aggiungerli richiede prima una modifica esplicita della projection pubblica, non un accesso arbitrario al payload JSON.

GET /topics/{slug}/knowledge-assets espone la projection inversa dei Knowledge Asset accepted il cui relatedTopics canonico contiene il Topic accepted risolto dallo slug. La membership è esatta sull'array persistito; la response riusa i campi card già validi per /knowledge-assets e non restituisce ID interni relatedTopics. Non persiste righe di relazione, grafi o autorità derivata.

La consistenza tra collection è intenzionalmente eventuale. PublicContentCatalog e il relativo snapshot/hash globale non fanno più parte dell'architettura runtime; una richiesta di modello non scansiona collection estranee.

Superfici rimosse dal target

Il target non include:

  • Queue Public Content usata per provocare rebuild;
  • Rebuild Coordinator o Durable Object di rebuild;
  • Workers Builds, Deploy Hook, readback o callback;
  • Event Subscription Queue e correlazione build_uuid;
  • debounce, successore build o credenziali di orchestrazione;
  • sei Worker applicativi;
  • rebuild dopo publish/unpublish.

Il repository Coordinator e le PR storiche restano evidenza, ma non hanno una responsabilità runtime target.

Contratto Vercel

La Story public-site #39 definisce e verifica @astrojs/vercel, output server/on-demand, strategia ibrida prerender/dinamica, TTL ISR code-owned e bounded, homepage, dettaglio, archivi, ricerca, sitemap, feed, 404 e comportamento durante indisponibilità temporanea del backend o di Access. DEV e PROD sono separati tramite configurazione Vercel.

Progetto, domini e variabili Vercel richiedono target, verifica e rollback espliciti nell'Issue. Dopo il merge Fra, Codex esegue gli stage provider già revisionati senza un secondo go/no-go ordinario.

Esito della cleanup backend

La cleanup backend #380, eseguita dopo la prova end-to-end del flusso bounded, ha rimosso dalle superfici applicative public_content.changed, producer Queue, enqueue post-commit, recovery amministrativa, repository outbox e PublicContentEventResolver. Publish e unpublish modificano soltanto il lifecycle editoriale canonico.

L'inventario ha escluso consumer indipendenti: in development il producer era del solo Worker backend e non esisteva un consumer; in production producer e unico consumer appartenevano allo stesso Worker backend; nessuna delle due Queue aveva Event Subscription. D1 non aveva righe pending, ma soltanto evidenza storica terminale.

La tabella D1 storica e la colonna di revisione restano inerti e non sono referenziate dal runtime. Rimuoverle non è necessario per il disaccoppiamento e sarebbe distruttivo prima dell'attivazione del nuovo codice nell'ordine canonico migrazione-prima-del-deploy. Detach/delete delle Queue provider e un'eventuale contrazione D1 fisica richiedono una successiva Issue operativa con inventario e rollback espliciti. Mailjet resta fuori scope.

Governance

  • Project #1 pianifica soltanto backend, API, MCP, D1 e lifecycle editoriale.
  • Project #2 pianifica soltanto Astro, Vercel, SSR/on-demand, ISR, SEO, ricerca e analytics.
  • Project #3 è archiviato, conserva evidenza storica e non riceve nuovi item.

La chiusura nativa della Issue rappresenta il completamento. Project Status governa selezione e priorità, non CI o deploy. Non si usano label status:* come seconda fonte di workflow.

Ordine

architecture-coffee#383
→ architecture-coffee#382 (nuove REST, legacy mantenuta)
→ architecture-coffee-public-site#47
→ architecture-coffee#382 (rimozione legacy dopo evidenza DEV)
→ architecture-coffee-public-site#44 (evidenza di convergenza Production)
→ architecture-coffee#380 (cleanup applicativa delivery-only)

La chiusura di una dipendenza non promuove automaticamente la Story successiva.