@getquick/content 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  The configured content runtime for WordPress HTML and WPGraphQL. This slice
4
4
  provides pure helpers, the configured WordPress client, durable delivery and
5
- installed SQL migrations, and event/refresh HTTP handlers. This is not a
6
- completed or published 0.1.0 release pair.
5
+ installed SQL migrations, and event/refresh HTTP handlers. It is released as a
6
+ pair with the Composer package `getquick/gq-content`, at the same version and tag.
7
7
 
8
8
  ```ts
9
9
  import { renderWordPressBlocks, htmlLang } from "@getquick/content";
@@ -37,8 +37,8 @@ manual redirects and an eight-second deadline; failed reads return unavailable,
37
37
  confirmed absence returns missing, and successful reads return found.
38
38
 
39
39
  The root entry contains pure helpers and public structural types. The server
40
- entry exports `createContentRuntime`, `ContentConfig`, `ContentBindings`,
41
- `ContentRuntime` and `WordPressClient`. Runtime properties contain languages,
40
+ entry exports `createContentRuntime`, `ContentConfig`, `ContentExtension`,
41
+ `ExtensionNode`, `CollectionConfig`, `ContentBindings`, `ContentRuntime` and `WordPressClient`. Runtime properties contain languages,
42
42
  routing functions and the WordPress methods; credentials/configuration are kept
43
43
  in closures. Language configuration retains the source's normalization policy,
44
44
  and routing returns the same language objects held in that instance's array.
@@ -50,7 +50,9 @@ The reusable client methods retain their argument order:
50
50
  - `getDesignPresets()`
51
51
  - `getPublishedEntries()`
52
52
  - `getPublishedRoutes()`
53
- - `getSiteChrome(siteOrigin?, { location?, language? }?)`
53
+ - `getArchives()`
54
+ - `getSiteChrome(siteOrigin?, { location?, language? }?)` (menu, logo, icon,
55
+ header and footer patterns)
54
56
 
55
57
  From the repository root, use `vp install` and `vp run validate`. Validation
56
58
  builds compiled ESM/declarations through Vite+ Pack, checks formatting/lint/types,
@@ -62,7 +64,8 @@ untracked. The npm tarball allowlist contains only compiled output, the five ori
62
64
  separate consent database migrations and package metadata (npm also includes this README).
63
65
 
64
66
  The public delivery methods are `publishedHome(siteOrigin?, language?)`,
65
- `publishedEntry(path, siteOrigin?)` and `pageChrome(siteOrigin?)`. Supply a
67
+ `publishedEntry(path, siteOrigin?)`, `pageChrome(siteOrigin?)` and
68
+ `designPresets()`. Supply a
66
69
  `PUBLICATION_DB` structural binding to serve durable publications. Stored reads
67
70
  need no event or refresh secret and retain last-known-good content without an
68
71
  age limit. Cold entry lookups read the CMS once, preserving missing versus
@@ -72,6 +75,152 @@ Production without a store returns unavailable (chrome returns null). Only
72
75
  activates that fallback. All public reads guard server context before bindings
73
76
  or transport.
74
77
 
78
+ `designPresets()` returns the stored design presets for a page that isn't a
79
+ publication (live only in explicit development, null otherwise), and
80
+ `internalOrigins(siteOrigin?)` the origins `menuItemHref` treats as this Site's.
81
+ `blockPresetStyles(spacing, colors, scope?)` scopes the preset classes to a
82
+ container (`.prose-wp` by default); `presetRootStyle(spacing, colors)` declares
83
+ the presets on `:root`.
84
+
85
+ ## A Site's own content
86
+
87
+ A Site whose WordPress plugin adds fields to its pages and posts declares them
88
+ as an `extension`. The runtime reads them with each front page, page and post
89
+ (with its blocks), stores the Site's value with the publication and serves it,
90
+ typed, through outages:
91
+
92
+ ```ts
93
+ const content = createContentRuntime<SiteBlocks>({
94
+ // …the configuration above…
95
+ extension: {
96
+ fields: { page: "wpBlocksJson pageBackgroundColor", post: "wpBlocksJson" },
97
+ // The Site's value for a node WordPress returned, or undefined for none.
98
+ read: ({ kind, fields }) => siteBlocks(fields.wpBlocksJson, fields.pageBackgroundColor),
99
+ // The stored value (JSON) read back, or undefined when it can't be served.
100
+ parse: (stored) => storedSiteBlocks.safeParse(stored).data,
101
+ },
102
+ });
103
+ const page = await content.publishedEntry("/about/");
104
+ if (page.kind === "found" && page.content.entry.extension) {
105
+ // Render page.content.entry.extension (SiteBlocks); the front page's is
106
+ // home.page.extension.
107
+ }
108
+ ```
109
+
110
+ Rows stored before the Site declared it serve without it; a refresh stores it.
111
+ A read throwing fails as WordPress's answer not matching the schema, and a read
112
+ without its blocks stores nothing, so the stored page stays served. See ADR 0004.
113
+
114
+ ## Header and footer patterns
115
+
116
+ The chrome also carries `header` and `footer`: the synced patterns GQ Design's
117
+ Header & Footer selects for the design route `designTemplate` (default
118
+ `"page"`; also `"front-page"`, `"home"`, `"archive"`, `"single-post"` or a
119
+ Site-defined route). Each is `{ title, blocks }`, or null when the region uses
120
+ a template part or nothing published is selected: keep the Site's own header
121
+ or footer then. They are stored, refreshed and reconciled with the chrome, so
122
+ visitors keep the last-known-good regions through CMS outages. A selected
123
+ pattern GQ Design resolves to no blocks fails the chrome read and keeps what
124
+ is stored. A `patterns` settings event refreshes the chrome. See
125
+ [ADR 0006](../docs/adr/0006-serve-cms-selected-header-and-footer-patterns-with-site-chrome.md).
126
+
127
+ The configured route is shared globally, including the homepage and posts;
128
+ Content does not choose a different Design route for each visitor URL. Use a
129
+ key registered in Design's manifest: an unknown key also returns null, and
130
+ Content never retries another key.
131
+
132
+ **Required GQ Design: 0.4.1 or newer** for the default `page` (`single` and
133
+ `search` need 0.4.2). GQ Design 0.2.17 to 0.4.0 answer null for `page`, so the
134
+ Site keeps its own regions. A GQ Design without `designPattern` at all (before
135
+ 0.2.17) doesn't fail the chrome: it is read without patterns, both null, and a
136
+ warning names GQ Design and the version it needs (`WordPress: GQ Design has no
137
+ designPattern field …`). Public selection excludes unpublished
138
+ route/part sources and unpublished, protected or unsynced final patterns.
139
+ Authoritative null removes a previous selection on a successful refresh;
140
+ failed or malformed reads retain it.
141
+
142
+ The ordinary menu and publication-store row remain language-specific, but
143
+ `designPattern` takes no language. Every language receives the same unqualified
144
+ pattern selection and its serialized data. Translated pattern trees are blocked
145
+ by [gq-design #53](https://github.com/Quick-Release/gq-design/issues/53).
146
+
147
+ Render a pattern with the Site's block renderers. Blocks with saved HTML
148
+ render it with their children inside; `core/site-logo` has a built-in
149
+ renderer; any other block without HTML (a dynamic or Site block) needs a
150
+ renderer. Containers receive a basic WordPress wrapper, and GQ Design's
151
+ `getquick-design/container` its layout (width, gap, padding) as content's
152
+ containers do; unresolved dynamic leaves render nothing and are reported to
153
+ `onUnrendered`:
154
+
155
+ ```ts
156
+ import { designMenuItems, escapeHtml, type DesignBlockRenderer } from "@getquick/content";
157
+
158
+ const headerNavigation: DesignBlockRenderer = (block, { href }) =>
159
+ `<nav aria-label="Primary"><ul>${designMenuItems(block.attributes.menuItems, href)
160
+ .map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
161
+ .join("")}</ul></nav>`;
162
+
163
+ const header = content.renderDesignPattern(chrome?.header, {
164
+ siteOrigin: Astro.site?.origin,
165
+ home: language.home,
166
+ blocks: { "getquick/header-navigation": headerNavigation },
167
+ onUnrendered: (block) => console.warn(`No renderer for ${block.name}`),
168
+ });
169
+ ```
170
+
171
+ A renderer receives the block (`name`, `attributes`, `content`, `children`)
172
+ and a context: `children` (rendered on demand), `render(blocks)`,
173
+ `ancestors`, `home` and `href(url)`, which serves CMS and Site links as paths
174
+ and replaces unsafe schemes with `#`. Its output is inserted as HTML: escape
175
+ CMS text with `escapeHtml`. Saved HTML is trusted published CMS markup,
176
+ not sanitized by this helper; full custom spacing/layout styling needs a Site
177
+ renderer. With `consent` configured, the saved HTML in served patterns has its
178
+ embeds rewritten into consent placeholders, as bodies do; a renderer building
179
+ an embed from attributes uses `embedPlaceholder` (`@getquick/content/consent`)
180
+ itself. The pure `renderDesignPattern` from the root entry
181
+ takes `internalOrigins` instead of `siteOrigin`.
182
+
183
+ ## Collections and archive Pages
184
+
185
+ A Site stores its custom post types' items as collections, and lists them from
186
+ the publication store alone (never WordPress), for an archive Page's listing
187
+ block:
188
+
189
+ ```ts
190
+ const content = createContentRuntime({
191
+ // …the configuration above…
192
+ collections: {
193
+ biblioteca: {
194
+ postType: "biblioteca", // WordPress's name
195
+ graphqlType: "Biblioteca", // its graphql_single_name
196
+ taxonomies: ["categoria_biblioteca"], // each show_in_graphql
197
+ },
198
+ },
199
+ });
200
+ const page = await content.list({
201
+ collection: "biblioteca",
202
+ terms: { categoria_biblioteca: ["legislacao"] }, // any slug; every taxonomy given slugs
203
+ search: "lei", // title and excerpt, ignoring case and accents
204
+ page: 1, // a number (or numeric string); anything else throws
205
+ perPage: 12,
206
+ });
207
+ // { kind: "found", items: [{ uri, title, date, modifiedAt, excerpt,
208
+ // featuredImage, terms, extension?, language? }], total, page, pages }
209
+ // or { kind: "unavailable", failure }
210
+ const filters = await content.terms("biblioteca", "categoria_biblioteca");
211
+ // { kind: "found", terms: [{ slug, name, count }] }
212
+ const archive = await content.archiveUri("biblioteca"); // "/biblioteca/" or null
213
+ ```
214
+
215
+ Items are entries: events, withdrawals and reconciliation keep them, and
216
+ `publishedEntry` serves them with `entry.collection` (`name`, `terms`). The
217
+ extension's `fields.collections[<name>]` selects an item's own fields (`read`
218
+ gets `kind: "item"` and `collection`). In WordPress, GETQUICK → Archives
219
+ assigns each type a Page, which takes over the type's archive address; saving
220
+ it reads every archive address again. A type or taxonomy WPGraphQL doesn't
221
+ show, a `graphqlType` that isn't the type's, or a taxonomy not connected to
222
+ it fails the archives read, by name. See ADR 0005.
223
+
75
224
  Infrastructure imports the Node-only migration entry separately:
76
225
 
77
226
  ```ts
@@ -1,6 +1,6 @@
1
1
  import { S as SERVICE_CATALOG, T as copyLanguage, c as grantedCategories, d as promptReason, f as readConsentCookie, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
2
2
  import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-p9SyGNZI.js";
3
- import { t as CONSENT_UI_VERSION } from "./version-Dn6VLZJO.js";
3
+ import { t as CONSENT_UI_VERSION } from "./version-D_Ao31D3.js";
4
4
  import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
5
5
  import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
6
6
  //#region frontend/src/consent/astro.ts
package/dist/consent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
2
2
  import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
3
3
  import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-p9SyGNZI.js";
4
- import { t as CONSENT_UI_VERSION } from "./version-Dn6VLZJO.js";
4
+ import { t as CONSENT_UI_VERSION } from "./version-D_Ao31D3.js";
5
5
  export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };