barakopress 0.8.0 → 0.10.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 +83 -1875
- package/dist/analytics.d.ts +24 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +105 -0
- package/dist/analytics.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/delivery.d.ts +1 -1
- package/dist/delivery.js +5 -5
- package/dist/delivery.js.map +1 -1
- package/dist/env.d.ts +3 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +1 -0
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/routes/binding-report.d.ts +5 -4
- package/dist/routes/binding-report.d.ts.map +1 -1
- package/dist/routes/binding-report.js.map +1 -1
- package/dist/screens/site-layout.d.ts +9 -1
- package/dist/screens/site-layout.d.ts.map +1 -1
- package/dist/screens/site-layout.js +19 -3
- package/dist/screens/site-layout.js.map +1 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +10 -4
- package/dist/secret.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +3 -0
- package/dist/site.js.map +1 -1
- package/dist/theme.d.ts +6 -6
- package/package.json +3 -3
- package/src/analytics.ts +115 -0
- package/src/config.ts +4 -0
- package/src/delivery.ts +5 -5
- package/src/env.ts +3 -0
- package/src/index.ts +1 -1
- package/src/routes/binding-report.ts +12 -9
- package/src/screens/site-layout.tsx +29 -2
- package/src/secret.ts +10 -4
- package/src/site.ts +3 -0
- package/src/theme.ts +6 -6
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# barakoPress
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The renderer for barakoCMS sites: pages from blocks, collections and docs trees, cached until the CMS
|
|
4
|
+
says otherwise.
|
|
5
|
+
|
|
6
|
+
It ships as an npm package, `barakopress`, that a Next app mounts route by route, and as a container
|
|
7
|
+
image, `ghcr.io/baryodev/barako-press`, that serves every site a [barakoCMS](https://github.com/BaryoDev/barakoCMS)
|
|
8
|
+
instance knows. Pages are server rendered and cached per tenant, so ordinary traffic never reaches
|
|
9
|
+
Postgres and a publish is live in one request. You own it and run it where you choose. Nothing is
|
|
10
|
+
metered per seat, record or environment.
|
|
6
11
|
|
|
7
12
|
It is an engine, not a site. Content types in barakoCMS are defined at runtime, so a client's posts
|
|
8
13
|
are as likely to be an `article` with a `Headline` as the blog blueprint's `post` with a `Title`.
|
|
@@ -85,8 +90,8 @@ decision, not the engine's.
|
|
|
85
90
|
| --- | --- | --- |
|
|
86
91
|
| `app/page.tsx` | `default`, `generateMetadata` | `createHome(config, blocks)`, `createHomeMetadata(config)` |
|
|
87
92
|
| `app/blog/[slug]/page.tsx` | `default`, `generateMetadata` | `createBlogPost(config)`, `createPostMetadata(config)` |
|
|
88
|
-
| `app/authors/[slug]/page.tsx` | `default` | `
|
|
89
|
-
| `app/categories/[slug]/page.tsx` | `default` | `
|
|
93
|
+
| `app/authors/[slug]/page.tsx` | `default` | `createCollectionDetail(config, "author", { related: { collection: "post", via: "Author" } })` |
|
|
94
|
+
| `app/categories/[slug]/page.tsx` | `default` | `createCollectionDetail(config, "category", { related: { collection: "post", via: "Category" } })` |
|
|
90
95
|
| `app/feed.xml/route.ts` | `GET` | `createFeed(config)` |
|
|
91
96
|
| `app/sitemap.ts` | `default` | `createSitemap(config)` |
|
|
92
97
|
| `app/robots.ts` | `default` | `createRobots(config)` |
|
|
@@ -104,13 +109,17 @@ decision, not the engine's.
|
|
|
104
109
|
`HomeCollection`, or the post index when it picked neither, which is what `createBlogIndex` did and
|
|
105
110
|
still does for a site that mounts that instead.
|
|
106
111
|
|
|
112
|
+
`via` is the post's field that references the author or category. `createArchive(config, "author")`
|
|
113
|
+
makes that same call and is `@deprecated` since 0.7.0, as is `createArchiveStaticParams`, which is
|
|
114
|
+
`createCollectionStaticParams`.
|
|
115
|
+
|
|
107
116
|
Mount only what you want. Nothing requires anything else. The paths only have to agree with the
|
|
108
117
|
`routes` in your config, which is what every generated link is built from.
|
|
109
118
|
|
|
110
119
|
`createBlogPostPreview` replaces `createBlogPost` when you want `?preview=TOKEN` to render a draft.
|
|
111
120
|
It reads `searchParams`, which forces the route dynamic, so a site using `output: "export"` takes
|
|
112
|
-
`createBlogPost` and gives up preview. `createPostStaticParams` and `
|
|
113
|
-
for that static case.
|
|
121
|
+
`createBlogPost` and gives up preview. `createPostStaticParams` and `createCollectionStaticParams`
|
|
122
|
+
exist for that static case.
|
|
114
123
|
|
|
115
124
|
A site that draws its own post page keeps preview by giving the detail route its own view, and maps
|
|
116
125
|
the item with the engine's own mapping:
|
|
@@ -126,951 +135,39 @@ The binding report says why a binding on a page did not resolve, so an editor fi
|
|
|
126
135
|
instead of asking whoever can read the server log. Ask for one page at a time, by `?slug=` or
|
|
127
136
|
`?path=`, and each problem names the binding as typed, the reason (`unknown scope`, `unbound scope`
|
|
128
137
|
or `no value`), and the block and field it came from. It reports the page as rendered, not as
|
|
129
|
-
stored, so a `{{item.X}}` outside a repeat is reported as an unbound scope. Give it the
|
|
130
|
-
option the page route
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
stored, so a `{{item.X}}` outside a repeat is reported as an unbound scope. Give it the `query`
|
|
139
|
+
option the page's route works out to. `createPage` and `createHome` default it by where they are
|
|
140
|
+
mounted: off under a request-time site's `app/%5Fpress/[site]`, where a `{{query.X}}` is an unbound
|
|
141
|
+
scope for every visitor, and on for a route outside it, such as a build-time site's
|
|
142
|
+
`app/[...path]`. The report defaults to off wherever it is mounted, so for a page on a route outside
|
|
143
|
+
the tenant segment pass `{ query: true }`, or the report calls a `{{query.X}}` that works on the page
|
|
144
|
+
an unbound scope. It is never anonymous:
|
|
133
145
|
the caller presents the tenant's key as `Authorization: Bearer`, derived from `PRESS_SECRET` and
|
|
134
146
|
printed by `barakopress bindings-key <tenant>`. Answers are never cached.
|
|
135
147
|
|
|
136
|
-
`Card` and `PostView` are exported too, for a site that wants its own page but the engine's markup.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
160
|
-
"doctors": {
|
|
161
|
-
"type": "doctor", "route": "/doctors", "sort": "Name",
|
|
162
|
-
"fields": { "title": "Name", "summary": "Specialty", "image": "Photo" },
|
|
163
|
-
"references": { "Department": { "collection": "departments", "label": "in" } },
|
|
164
|
-
"noun": ["doctor", "doctors"]
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
| Key | What |
|
|
170
|
-
| --- | --- |
|
|
171
|
-
| `type` | The content type. Required |
|
|
172
|
-
| `route` | The index is served at the route and an item at `route/slug`. Absent, items are listed and never linked |
|
|
173
|
-
| `fields` | `title` (required), `slug`, `summary`, `body` (markdown), `date`, `image`, `imageAlt`, `featured`, `tags`, `url`, `photo`, `progress`, `progressCount`, `progressTotal`, `href`. Each is a field name or a list tried in order; `@createdAt` and `@updatedAt` read the entry itself. `photo` is a portrait drawn above the title, `image` the wide one, and `progress` is a number from 0 to 100 that `progressList` draws. `progressCount` and `progressTotal` draw the same bar from two counts instead, for a source that gives those rather than a figure already worked out; `progress` wins when both are set. `href` is where a card links when that is not the item's own route: a collection filled by a sync usually carries the source's own URL and has no route on this site |
|
|
174
|
-
| `references` | Reference fields, each naming the collection it points into and the word a card puts before the link. Resolved in the same request |
|
|
175
|
-
| `sort` | Sent to the API, for example `-PublishedAt` |
|
|
176
|
-
| `feed` | Whether `createFeed(config, key)` serves it |
|
|
177
|
-
| `sitemap` | On unless `false`. A collection is paged to `pageSizes.sitemap` entries; the file stops at the standard's 50,000 URLs and says so in the log |
|
|
178
|
-
| `index` | Whether the root catch-all serves an index at the route. On unless `false`; the derived `author` and `category` have it off, so `/authors` stays a 404 unless a route file mounts it. An object is the index's copy, and the index is on. See below |
|
|
179
|
-
| `indexPage` | A site path such as `/site/blog`. The blocks of the page served there are drawn above the list on the index. See below |
|
|
180
|
-
| `defaultAuthor` | The byline name on an entry that names no author, on its card and on its `article` page |
|
|
181
|
-
| `pageSize`, `label`, `noun` | Items on the index, its heading, and how a count reads |
|
|
182
|
-
| `colorBy` | A choice field whose option colours the item. See below |
|
|
183
|
-
| `related` | What an item page lists under the item: `"semantic"` for the items of this collection closest to it by meaning, `false` for none. Unset, the first collection that references this one |
|
|
184
|
-
| `readingTime` | Whether an item page shows a read time worked out from its body. Off unless `true` |
|
|
185
|
-
| `layout` | How an item page is drawn: `"list"`, the shell markup every collection has had, or `"article"`, the reading column the blog's posts are drawn in. See below |
|
|
186
|
-
| `tree` | Set when the collection is a manual rather than a flat list: sections, an order, nesting, products. See below |
|
|
187
|
-
|
|
188
|
-
A settings entry that does not read as a collection is left out whole: a type or field that is not a
|
|
189
|
-
plain identifier, a route that is not a plain site path, or no title field. `post`, `author` and
|
|
190
|
-
`category` are replaceable by key like any other collection, since every read goes through the
|
|
191
|
-
collection's own field map. A build-time collection mounted at `/` throws in `defineConfig`. When two
|
|
192
|
-
routes match, the longer one wins.
|
|
193
|
-
|
|
194
|
-
A request-time site cannot add a route file per tenant, so the root catch-all from the pages section
|
|
195
|
-
also serves every collection with a route: its index at the route, and an item one segment below. A
|
|
196
|
-
route file wins where there is one. A collection's first segment is reserved from pages at the root.
|
|
197
|
-
|
|
198
|
-
A detail page lists the items of the first collection that references it, so `/departments/cardiology`
|
|
199
|
-
lists its doctors, the way an author's archive lists their posts. `listRelated(config, "doctors",
|
|
200
|
-
department, { via: "Department" })` returns the same list; `listRelated(config, post)` still returns
|
|
201
|
-
related posts by semantic search.
|
|
202
|
-
|
|
203
|
-
With `related: "semantic"` it lists the items of its own collection nearest it by meaning instead, so
|
|
204
|
-
an agency's case study gets the band a post has. That needs the CMS AI module: without it the search
|
|
205
|
-
answers nothing and the page draws no band at all, which is the same way the post page degrades.
|
|
206
|
-
`listRelatedItems(config, key, item)` returns that list on its own.
|
|
207
|
-
|
|
208
|
-
**The index's own words.** An editor sets what a collection's index says in the collection's
|
|
209
|
-
settings, rather than a theme writing a route file for it:
|
|
210
|
-
|
|
211
|
-
```json
|
|
212
|
-
{
|
|
213
|
-
"blog": {
|
|
214
|
-
"type": "post", "route": "/blog", "fields": { "title": "Title" },
|
|
215
|
-
"index": {
|
|
216
|
-
"eyebrow": "Changelog", "heading": "What shipped",
|
|
217
|
-
"lede": "Every release, newest first.",
|
|
218
|
-
"empty": "Nothing shipped yet.", "unavailable": "The changelog is resting. Try again shortly."
|
|
219
|
-
},
|
|
220
|
-
"indexPage": "/site/blog",
|
|
221
|
-
"defaultAuthor": "The barakoCMS team"
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
`eyebrow` is a short line above the heading, `heading` replaces `label`, `lede` replaces the site's
|
|
227
|
-
tagline under it, `empty` is what an index with nothing published says, and `unavailable` what one
|
|
228
|
-
whose read failed says, each in place of the `labels` lines that say it otherwise. Every line is
|
|
229
|
-
optional, one of the wrong kind or too long is dropped on its own, and a route file's own `heading`
|
|
230
|
-
still wins. `indexPage` draws a page's blocks above the list, so the index is composed like any page
|
|
231
|
-
and the route stays the collection's. With `defaultAuthor`, an entry whose first reference (the
|
|
232
|
-
byline) did not come back, or a collection with no reference at all, is signed with that name after
|
|
233
|
-
`labels.by`. Set none of the three and the index and the byline draw what they always did.
|
|
234
|
-
|
|
235
|
-
A page named by `indexPage` only holds data, the way a header or footer region page does: it answers
|
|
236
|
-
404 at its own path and is left out of the menu and the sitemap.
|
|
237
|
-
|
|
238
|
-
**Filtering.** `listCollection(config, "doctors", { filter: { Department: "cardiology" } })` takes a
|
|
239
|
-
reference field by the target's slug and any other field by the value it holds, a choice field by its
|
|
240
|
-
option. The API takes five filters. `createCollectionIndex(config, key, { filter })` takes the same,
|
|
241
|
-
and the `collection` block has `filterField` and `filterValue`.
|
|
242
|
-
|
|
243
|
-
**A style per option.** With `colorBy: "AreaOfFocus"` on a `project` collection, the site settings
|
|
244
|
-
`OptionStyles` entry says how each option of that field is shown:
|
|
245
|
-
|
|
246
|
-
```json
|
|
247
|
-
{
|
|
248
|
-
"OptionStyles": {
|
|
249
|
-
"project.AreaOfFocus": {
|
|
250
|
-
"Providing clean water": { "tone": "sky", "icon": "location", "label": "Water" }
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
`tone` names a colour from `Colors`, a theme slot, or a colour written out, and each card, item page
|
|
257
|
-
and collection block item carries it as a left border. `icon` is one of the engine's icon names, and
|
|
258
|
-
`label` is the word a visitor reads in place of the option's own value; the value stays on the element
|
|
259
|
-
as `data-option`, where a site's own CSS can still find it. A tone that does not resolve to something
|
|
260
|
-
readable as a colour is dropped, a label past 40 characters is dropped, and an icon name nothing
|
|
261
|
-
draws draws nothing, each on its own rather than losing the whole entry.
|
|
262
|
-
|
|
263
|
-
`OptionColors` is the same thing said shorter, `{ "project.AreaOfFocus": { "Providing clean water":
|
|
264
|
-
"sky" } }`, an option whose style is a tone and nothing else. A tenant that saved colours keeps them,
|
|
265
|
-
and a style for the same option wins field by field. A build-time site passes `optionStyles`, or
|
|
266
|
-
`optionColors`, in the same shapes.
|
|
267
|
-
|
|
268
|
-
`Card` and `ItemView` are exported for a site that wants its own page, and `Card` still takes a `post`.
|
|
269
|
-
`getItem`, `listCollection` and `getGlobals(config)`, the tenant's settings entry as stored, are
|
|
270
|
-
exported too.
|
|
271
|
-
|
|
272
|
-
**A long-form layout.** `layout: "article"` draws an item as a reading column: a back link, the title
|
|
273
|
-
and standfirst, a byline from the collection's first reference, the date, a read time worked out from
|
|
274
|
-
the body, the other references, the tags, the cover image, the prose and a band of neighbours under
|
|
275
|
-
it. That is the page the blog's posts have always had, and it is a collection setting rather than a
|
|
276
|
-
blog screen, so a law firm's briefings or a newsroom's features get it by asking. Styled inline from
|
|
277
|
-
the theme, so it looks right whether or not the consumer imports `barakopress/styles.css`.
|
|
278
|
-
`PostView` is a wrapper over the same layout and is `@deprecated` for 1.0.0.
|
|
279
|
-
|
|
280
|
-
**Docs: a collection as a tree.** A manual is a collection plus four fields saying where each page
|
|
281
|
-
sits. `tree` names them, and the settings beside them say what the sidebar and the switcher show:
|
|
282
|
-
|
|
283
|
-
```json
|
|
284
|
-
{
|
|
285
|
-
"docs": {
|
|
286
|
-
"type": "doc", "route": "/docs", "label": "Documentation", "layout": "article",
|
|
287
|
-
"fields": { "title": "Title", "slug": "Slug", "body": "Body" },
|
|
288
|
-
"tree": {
|
|
289
|
-
"section": "Section", "order": "Order", "parent": "Parent", "product": "Product",
|
|
290
|
-
"sections": ["Getting started", "Guides", "Reference"],
|
|
291
|
-
"searchPath": "/docs",
|
|
292
|
-
"editPath": "Source",
|
|
293
|
-
"editBase": "https://github.com/owner/repo/edit/master/",
|
|
294
|
-
"products": [
|
|
295
|
-
{ "key": "cms", "label": "barakoCMS", "href": "/docs" },
|
|
296
|
-
{ "key": "press", "label": "barakoPress", "href": "/docs/press" }
|
|
297
|
-
]
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
| Key | What |
|
|
304
|
-
| --- | --- |
|
|
305
|
-
| `section` | The field holding the heading a page is grouped under |
|
|
306
|
-
| `order` | The field holding its position in that section. A page with none comes after those with one |
|
|
307
|
-
| `parent` | The field holding the slug of the page it hangs under, or a reference to it. A parent nobody has leaves the page at the top of its section rather than dropping it |
|
|
308
|
-
| `product` | The field naming the product it documents, matched against a product's `key`. One field name and not a list, since this one goes into an API filter |
|
|
309
|
-
| `sections` | The sections in the order the sidebar shows them. One not named here follows those that are. Named rather than worked out, because no ordering of the pages says which section comes first |
|
|
310
|
-
| `products` | What the switcher offers: a key, the word a reader sees, and where it goes. A destination that is not a site path or an http URL is dropped |
|
|
311
|
-
| `searchPath` | Where the search box submits, and whether one is drawn at all. Unset, no box, because only the site knows which of its routes reads the query |
|
|
312
|
-
| `editPath` | The field holding the page's path in whatever repository it is written in. Its slug when unset |
|
|
313
|
-
| `editBase` | Where "edit this page" points, with that path appended. Unset, no such link is drawn |
|
|
314
|
-
| `limit` | The most pages read to build the tree. 500 unless set, and 500 is the ceiling as well as the default |
|
|
315
|
-
| `variant` | How the parts are laid out. See "Styling the tree" below. Every part is today's layout when unset |
|
|
316
|
-
| `searchIndex` | `true` to search the tree's titles and headings in the page as the reader types. The box is drawn whenever this is set; with `searchPath` too, a reader with no script submits there |
|
|
317
|
-
| `icons` | `{ "search": "#id", "chevron": "#id" }`: symbols already on the page (a site's own sprite) for the compact search box's magnifier and the closed disclosure's chevron. A value that is not `#` and a name is dropped, and the engine draws its own |
|
|
318
|
-
|
|
319
|
-
A product may also carry a `note`, a few words drawn after its label in the list switcher (`"on GitHub"`).
|
|
320
|
-
|
|
321
|
-
An item page in such a collection draws the sidebar with the page being read marked, the switcher,
|
|
322
|
-
a search box, previous and next from the tree's reading order, and the edit link. The sidebar is a
|
|
323
|
-
`details` element, so it collapses on a phone with no script. `collectionTree(config, key, { product })`
|
|
324
|
-
and `treeNeighbours(order, slug)` return the same tree and the same neighbours for a site's own page.
|
|
325
|
-
|
|
326
|
-
An item links where the collection's `href` field says it is read, when the collection maps one and it
|
|
327
|
-
is a path on this site, and at its route and slug otherwise: the sidebar, previous and next, the
|
|
328
|
-
in-page index and the sitemap all follow it (`treeItemHref`). An absolute address is not followed. Slugs are unique across a tenant, so a manual whose products each have a
|
|
329
|
-
quickstart keeps `cms-quickstart` as the slug and names `/docs/cms/quickstart` in the field. The route
|
|
330
|
-
serving that path is the site's own. A site drawing the sidebar on a page the tree does not hold, an
|
|
331
|
-
index, gives `TreeAside` or `TreeSidebar` a `summary` (`{ line, title }`) for the closed control to say.
|
|
332
|
-
|
|
333
|
-
**Search.** `searchCollection(config, key, query)` goes through barakoCMS's `/api/public/{type}/search`,
|
|
334
|
-
which matches only over the fields a type publishes, so a draft or a field held back from public
|
|
335
|
-
delivery can never come back. `createCollectionIndex(config, key, { search: true })` answers `?q=` with
|
|
336
|
-
what matched instead of the index; that reads the query, so the route is dynamic and `output: "export"`
|
|
337
|
-
refuses it, which is why it is off unless asked for. The box is a `form`, the results are links, and
|
|
338
|
-
the keyboard handling on top ("/" to focus, the arrow keys to walk the results, escape to clear) is the
|
|
339
|
-
one client component in the package. The root catch-all answers `?q=` on a collection index too, but
|
|
340
|
-
only where the route may be dynamic: a request-time site rewrites to a kept route, and a kept route
|
|
341
|
-
asking for the query fails rather than bailing out, so there the index lists and a page of blocks
|
|
342
|
-
holding the `search` block is where a reader searches.
|
|
343
|
-
|
|
344
|
-
Which is why `searchPath` exists rather than the box pointing at the collection's own route. Only the
|
|
345
|
-
site knows which of its routes reads the query, and a box submitting somewhere that ignores `q` sends
|
|
346
|
-
a reader to an unfiltered index that looks like a search which matched everything. Name the route that
|
|
347
|
-
answers, or name nothing and get no box.
|
|
348
|
-
|
|
349
|
-
**As blocks.** `docsSidebar`, `docsSwitcher` and `search` draw the same three on a page of blocks, each
|
|
350
|
-
taking a collection key. `search` takes a bindable `query`, so a landing page binds `{{query.q}}` and
|
|
351
|
-
the route file passes the query with `createPage(config, blocks, { query: true })`. `docsSwitcher`
|
|
352
|
-
takes a `variant`, `tabs` or `list`, and follows the collection's own when it is left unset.
|
|
353
|
-
|
|
354
|
-
**Styling the tree.** A site restyles the tree screens without drawing them itself, three ways.
|
|
355
|
-
|
|
356
|
-
*Tokens.* Every colour, gap, padding, margin, radius, font size, weight and line height in them is read as
|
|
357
|
-
`var(--t-tree-<name>, <today's value>)`. Letter spacing, and the flex bases that decide when a row wraps, are fixed.
|
|
358
|
-
A token named in the tenant's `Tokens` setting (or `theme.tokens`) lands on the root as `--t-<name>`,
|
|
359
|
-
so `"Tokens": { "tree-link-current-bg": "#FDEBD3" }` restyles the current page's row and nothing else.
|
|
360
|
-
A token in the settings is held to a colour, a length or a font stack; a weight or a unitless line
|
|
361
|
-
height is set from the site's stylesheet under the same name (`:root { --t-tree-link-weight: 500 }`).
|
|
362
|
-
A site that names none draws what it drew before, pixel for pixel.
|
|
363
|
-
|
|
364
|
-
| Token | Default | What |
|
|
365
|
-
| --- | --- | --- |
|
|
366
|
-
| `tree-gap` | `space.lg` | Between the sidebar and the page, `plain` |
|
|
367
|
-
| `tree-shell-width`, `tree-shell-pad-y`, `tree-shell-pad-x` | `layout.wide`, `space.lg`, `layout.gutter` | The shell's width and padding, `plain` |
|
|
368
|
-
| `tree-edge` | `hairline` | The rules between the columns and above them, `boxed` |
|
|
369
|
-
| `tree-min-height` | `0px` | The shell's least height, `boxed` |
|
|
370
|
-
| `tree-sidebar-width` | `280px` | The sidebar column (its most, `plain`; its width, `boxed`) |
|
|
371
|
-
| `tree-sidebar-bg`, `tree-sidebar-pad-y`, `tree-sidebar-pad-x` | `surface`, `space.lg`, `space.md` | The sidebar column, `boxed` |
|
|
372
|
-
| `tree-body-pad-y`, `tree-body-pad-x` | `space.xl`, `space.xl` | The page column, `boxed` |
|
|
373
|
-
| `tree-aside-gap` | `space.md` | Between the switcher, the search box and the sidebar |
|
|
374
|
-
| `tree-section-gap` | `space.md` | Between sections, and between the list switcher and the sections |
|
|
375
|
-
| `tree-label-gap`, `tree-label-size`, `tree-label-ink` | `space.xs`, `text.meta`, `muted` | Section labels, the list switcher's label and the rail's heading. The size is also the search box's label and the phone control's |
|
|
376
|
-
| `tree-summary-bg`, `tree-summary-edge`, `tree-summary-ink`, `tree-summary-radius`, `tree-summary-space`, `tree-summary-pad-y`, `tree-summary-pad-x` | `surface`, `hairline`, `muted`, `radii.control`, `space.sm`, `10px`, `12px` | The sidebar's control on a phone, and the space under it while open |
|
|
377
|
-
| `tree-summary-pad-y`, `tree-summary-pad-x`, `tree-summary-min-height`, `tree-summary-gap`, `tree-summary-line-gap` | `10px`, `12px`, `0px`, `space.sm`, `3px` | The closed disclosure's control |
|
|
378
|
-
| `tree-summary-title-size`, `tree-summary-title-weight`, `tree-summary-title-ink` | `text.small`, `700`, `ink` | The page it names |
|
|
379
|
-
| `tree-summary-action-size`, `tree-summary-action-weight`, `tree-summary-action-ink`, `tree-summary-action-gap`, `tree-summary-icon-size` | `text.small`, `600`, `accentInk`, `6px`, `13px` | Its "Contents" and "Close", and the chevron |
|
|
380
|
-
| `tree-link-gap`, `tree-indent` | `0px`, `space.md` | Between rows, and a child's indent |
|
|
381
|
-
| `tree-link-pad-y`, `tree-link-pad-x`, `tree-link-radius`, `tree-link-size`, `tree-link-leading` | `6px`, `10px`, `radii.control`, `text.small`, `1.45` | A row in the sidebar, and a product in the list switcher |
|
|
382
|
-
| `tree-link-ink`, `tree-link-weight` | `secondaryInk`, `inherit` | A row that is not the page being read |
|
|
383
|
-
| `tree-link-current-bg`, `tree-link-current-ink`, `tree-link-current-weight` | `accentTint`, `accentInk`, `600` | The page (or product) being read |
|
|
384
|
-
| `tree-note-gap`, `tree-note-size`, `tree-note-weight`, `tree-note-ink` | `6px`, `text.meta`, `inherit`, `muted` | A product's note |
|
|
385
|
-
| `tree-tab-gap`, `tree-tab-pad-y`, `tree-tab-pad-x`, `tree-tab-radius`, `tree-tab-size`, `tree-tab-weight` | `space.xs`, `6px`, `12px`, `radii.pill`, `text.small`, `600` | The tabs switcher |
|
|
386
|
-
| `tree-tab-bg`, `tree-tab-edge`, `tree-tab-ink`, `tree-tab-current-bg`, `tree-tab-current-ink` | `surface`, `hairline`, `secondaryInk`, `accent`, `inverseInk` | The tabs, and the current one |
|
|
387
|
-
| `tree-search-bg`, `tree-search-edge`, `tree-search-ink`, `tree-search-radius`, `tree-search-size`, `tree-search-pad-y`, `tree-search-pad-x` | `surface`, `hairline`, `ink`, `radii.control`, `text.small`, `9px`, `12px` | The search box |
|
|
388
|
-
| `tree-search-label-gap`, `tree-search-label-ink` | `space.xs`, `muted` | Its label |
|
|
389
|
-
| `tree-search-results-gap`, `tree-search-hit-gap`, `tree-search-hit-radius`, `tree-search-hit-ink`, `tree-search-hit-weight`, `tree-search-empty-ink`, `tree-search-hit-pad-y`, `tree-search-hit-pad-x` | `space.sm`, `2px`, `radii.control`, `ink`, `600`, `muted`, `7px`, `10px` | Its results |
|
|
390
|
-
| `tree-search-height`, `tree-search-gap`, `tree-search-icon-size` | `36px`, `9px`, `13px` | The compact well. It also reads `tree-search-bg` (`pageBg` here), `tree-search-radius`, `tree-search-pad-x`, `tree-search-size` (`text.meta` here), `tree-search-ink` and, for the well's own ink, `tree-search-label-ink` |
|
|
391
|
-
| `tree-search-key-pad-y`, `tree-search-key-pad-x`, `tree-search-key-radius`, `tree-search-key-bg`, `tree-search-key-edge`, `tree-search-key-size`, `tree-search-key-weight` | `2px`, `6px`, `6px`, `surface`, `hairline`, `text.meta`, `700` | The "/" key hint |
|
|
392
|
-
| `tree-search-panel-gap`, `tree-search-panel-pad`, `tree-search-panel-bg`, `tree-search-panel-edge`, `tree-search-panel-radius`, `tree-search-panel-shadow` | `6px`, `6px`, `surface`, `hairline`, `radii.panel`, `0 10px 24px -12px rgba(16,18,35,.25)` | The compact box's floating results |
|
|
393
|
-
| `tree-search-hit-size`, `tree-search-empty-pad-y`, `tree-search-empty-pad-x` | `text.small`, `8px`, `10px` | A compact result, and the line saying nothing matched |
|
|
394
|
-
| `tree-rail-width`, `tree-rail-pad-y`, `tree-rail-pad-x`, `tree-rail-label-gap`, `tree-rail-gap` | `layout.columnMin`, `space.xl`, `space.md`, `space.sm`, `2px` | The rail (its padding only when `boxed`) |
|
|
395
|
-
| `tree-rail-link-pad-y`, `tree-rail-link-pad-x`, `tree-rail-link-size`, `tree-rail-link-weight`, `tree-rail-link-ink`, `tree-rail-link-edge` | `6px`, `10px`, `text.small`, `inherit`, `secondaryInk`, `hairline` | A heading in the rail |
|
|
396
|
-
| `tree-pager-top`, `tree-pager-gap`, `tree-pager-pad-y`, `tree-pager-pad-x`, `tree-pager-radius`, `tree-pager-bg`, `tree-pager-edge` | `space.lg`, `space.sm`, `16px`, `18px`, `radii.panel`, `surface`, `hairline` | Previous and next |
|
|
397
|
-
| `tree-pager-label-size`, `tree-pager-label-ink`, `tree-pager-title-gap`, `tree-pager-title-size`, `tree-pager-title-weight`, `tree-pager-title-ink` | `text.meta`, `muted`, `space.xs`, `text.small`, `600`, `ink` | Their words |
|
|
398
|
-
| `tree-edit-top`, `tree-edit-ink`, `tree-edit-size` | `space.md`, `muted`, `text.meta` | "Edit this page" |
|
|
399
|
-
|
|
400
|
-
*Variants.* What a token cannot say, because it changes which elements are drawn or where they sit, is
|
|
401
|
-
the tree's `variant`, and a `variant` prop on the component:
|
|
402
|
-
|
|
403
|
-
```json
|
|
404
|
-
"tree": { "variant": { "switcher": "list", "sidebar": "boxed", "rail": true, "pager": "halves" } }
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
| Part | Values | What |
|
|
408
|
-
| --- | --- | --- |
|
|
409
|
-
| `switcher` | `tabs` (default), `list` | A row of pills above the search box, or a labelled column of rows inside the sidebar above the sections, under the search box, folding away with the pages on a phone |
|
|
410
|
-
| `sidebar` | `plain` (default), `boxed` | The sidebar beside the page in the wide column, or the page split edge to edge with the sidebar a surface column and a hairline between. `boxed` stacks on a phone |
|
|
411
|
-
| `rail` | `false` (default), `true` | An "on this page" column of the item's second level headings at the inline end, linking to the ids its body renders. Hidden below 64rem |
|
|
412
|
-
| `pager` | `wide` (default), `halves` | Previous and next taking the room there is and wrapping on a phone, or two halves that never wrap, next on the right even with no previous |
|
|
413
|
-
| `search` | `box` (default), `compact` | A labelled input with its results under it, or one well holding a magnifier, the input and a "/" key hint, the input named by `aria-label` with no label on screen, and the results in a panel floating over what follows. "/" focuses either |
|
|
414
|
-
| `disclosure` | `open` (default), `closed` | On a phone, the sidebar open under a "Contents" control, or closed until tapped, the control naming the product, the section and the page being read, with "Contents" and a chevron that turn to `closeContents` and point up while open. Above the phone breakpoint both show the whole sidebar and no control. `closed` needs no script: the content is shown above the breakpoint through `::details-content`, and a browser without it keeps the control there too, so the sidebar is one tap away rather than gone |
|
|
415
|
-
|
|
416
|
-
A value the engine does not know is today's layout for that part.
|
|
417
|
-
|
|
418
|
-
*Classes.* Each part carries a class a stylesheet can reach, for what neither a token nor a variant
|
|
419
|
-
covers (a hover, a transition): `bp-tree-shell` (and `bp-tree-shell-boxed`), `bp-tree-aside`,
|
|
420
|
-
`bp-tree-body`, `bp-tree-rail`, `bp-tree-sidebar`, `bp-tree-summary`, `bp-tree-sidebar-body`,
|
|
421
|
-
`bp-tree-sections`, `bp-tree-section`, `bp-tree-section-label`, `bp-tree-list`, `bp-tree-item`,
|
|
422
|
-
`bp-tree-link`, `bp-tree-link-current`, `bp-tree-switcher` (with `bp-tree-switcher-tabs` or
|
|
423
|
-
`bp-tree-switcher-list`), `bp-tree-switcher-label`, `bp-tree-tab`, `bp-tree-tab-current`,
|
|
424
|
-
`bp-tree-product`, `bp-tree-product-note`, `bp-tree-search`, `bp-tree-search-label`,
|
|
425
|
-
`bp-tree-search-input`, `bp-tree-search-results`, `bp-tree-search-index`, `bp-tree-search-hit` (a
|
|
426
|
-
result a route answered with), `bp-tree-search-empty`, `bp-tree-rail-nav`,
|
|
427
|
-
`bp-tree-rail-label`, `bp-tree-rail-link`, `bp-tree-pager`, `bp-tree-pager-link`,
|
|
428
|
-
`bp-tree-pager-prev`, `bp-tree-pager-next`, `bp-tree-pager-label`, `bp-tree-pager-title`,
|
|
429
|
-
`bp-tree-pager-empty`, `bp-tree-edit`, and for the variants `bp-tree-search-compact`,
|
|
430
|
-
`bp-tree-search-box`, `bp-tree-search-icon`, `bp-tree-search-key`, `bp-tree-nav-closed`, `bp-tree-summary-closed`, `bp-tree-summary-group`,
|
|
431
|
-
`bp-tree-summary-title`, `bp-tree-summary-action`, `bp-tree-summary-show`, `bp-tree-summary-hide` and
|
|
432
|
-
`bp-tree-summary-chevron`. Every label paragraph (a section's, the list switcher's, the pager's, the
|
|
433
|
-
rail's) also carries `bp-label`, so a rule aimed at running text can skip them all with
|
|
434
|
-
`:not(.bp-label)`. An index entry carries no class of its own, to stay small: it is
|
|
435
|
-
`.bp-tree-search-index li > a`, holding a `span` with the words that matched and, for a heading, a
|
|
436
|
-
second `span` naming its page. The styles are inline, so a stylesheet rule that sets a property the
|
|
437
|
-
part already sets needs `!important`; a token does not. The index's entries are the exception: they
|
|
438
|
-
are styled from one stylesheet scoped to their box, so an ordinary rule of the same or higher
|
|
439
|
-
specificity reaches them.
|
|
440
|
-
|
|
441
|
-
**The search index.** `treeSearchIndex(tree)` lists every page with a route and then its second level
|
|
442
|
-
headings, in reading order, each heading linked to its anchor. It is built once per tree read and
|
|
443
|
-
shared by everything drawn from that read, and a body's headings (`itemHeadings(item)`, from
|
|
444
|
-
`markdownHeadings` in `barakopress/markdown`) are tokenised once per distinct body and kept, so a static
|
|
445
|
-
build of a manual of n pages lexes each body once rather than n times. With `searchIndex`, the box
|
|
446
|
-
draws the index into the page hidden and its client code shows the entries holding every word typed,
|
|
447
|
-
eight at a time.
|
|
448
|
-
|
|
449
|
-
What that costs: an entry is a list item, a plain link and its words, styled from one stylesheet, so
|
|
450
|
-
it is its href and its text plus about 50 bytes. The index is part of the server render, so Next
|
|
451
|
-
carries it in the page's payload as well as its HTML, like any other server markup; it is never also
|
|
452
|
-
handed to the client component as props. At most 2000 entries (`TREE_INDEX_LIMIT`) go into a page.
|
|
453
|
-
Past that it is headings that are left out, never a page: every page's title is counted first and
|
|
454
|
-
headings fill the rest in reading order, and the server log says once which manual lost how many.
|
|
455
|
-
A body gives at most 100 headings (`HEADINGS_PER_BODY`), and what is kept between reads is those
|
|
456
|
-
headings under a digest of the body, never the body.
|
|
457
|
-
|
|
458
|
-
The results are put away, with what was typed kept, when focus leaves the box, when something outside
|
|
459
|
-
it is pressed, when one of them is followed, and on Escape; they come back when the box has focus again. Enter goes to the first
|
|
460
|
-
match. With no `searchPath` there is no form at all, so Enter can never reload the page with the
|
|
461
|
-
query: with script the box answers in the page, and a reader with no script gets a field that does
|
|
462
|
-
nothing, not one that reloads the page and empties itself.
|
|
463
|
-
|
|
464
|
-
### Pages and navigation from the Pages module
|
|
465
|
-
|
|
466
|
-
With `BarakoCMS.Pages` installed, the CMS owns the page tree: which pages are in the menu, their
|
|
467
|
-
order, their nesting and each page's path. Mount it with `pages`:
|
|
468
|
-
|
|
469
|
-
```ts
|
|
470
|
-
export const config = defineConfig({ sites: {}, pages: "" });
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
`""` is the site root. `"/docs"` serves the CMS page at `/about` on `/docs/about`. Leaving `pages` out
|
|
474
|
-
means the site has no page tree, and nothing below asks for one. Then one catch-all route,
|
|
475
|
-
`app/[[...path]]/page.tsx`, or `app/[...path]/page.tsx` when `app/page.tsx` keeps `/`:
|
|
476
|
-
|
|
477
|
-
```tsx
|
|
478
|
-
import { createPage, createPageMetadata } from "barakopress";
|
|
479
|
-
import { blocks, config } from "@/press.config";
|
|
480
|
-
|
|
481
|
-
export default createPage(config, blocks);
|
|
482
|
-
export const generateMetadata = createPageMetadata(config);
|
|
483
|
-
export const revalidate = 300;
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
- `createPage` reads `GET /api/public/pages/resolve?path=` and renders the page's blocks, or its
|
|
487
|
-
body, with breadcrumbs above the title when the page has a parent. Mounted on a `[slug]` route
|
|
488
|
-
instead, it reads the page type by slug as before.
|
|
489
|
-
- `createSiteLayout` draws `GET /api/public/pages/navigation` in the header. `Navigation` draws the
|
|
490
|
-
items in the order it received them and links to the paths it was given, under the mount. It does
|
|
491
|
-
not sort, nest, filter or derive a path. A menu that cannot be read is no menu, not a broken page,
|
|
492
|
-
and while holding the holding page is left out of it.
|
|
493
|
-
- A miss asks `GET /api/public/redirects/resolve?path=` before it is a 404, because a catch-all is
|
|
494
|
-
where a rebuilt site's old URLs land. A 301 answers as a permanent redirect and anything else as a
|
|
495
|
-
temporary one. A destination that is not a site path or an http or https URL is ignored.
|
|
496
|
-
- **Reserved slugs.** Next answers a static route before a catch-all, so a root page slugged `blog`
|
|
497
|
-
or `feed.xml` would silently never render. At the root mount, a path whose first segment is
|
|
498
|
-
reserved is a 404 and is never asked for, and it is left out of the sitemap and static params with
|
|
499
|
-
a warning. The list is the first segment of each configured route plus `api`, `feed.xml`,
|
|
500
|
-
`sitemap.xml`, `robots.txt`, `_next`, `_share` and `favicon.ico`; `reservedSlugs` adds to it. Give
|
|
501
|
-
barakoCMS the same list as `Modules:Pages:ReservedSlugs` and an editor is refused the slug on save.
|
|
502
|
-
- **Contract.** Both bodies carry `contract`, and this renderer reads the range in `PAGES_CONTRACT`
|
|
503
|
-
(1 to 1). A body outside it logs a warning and reads as absent: no menu, no page. A public site does
|
|
504
|
-
not stop rendering because a menu shape moved.
|
|
505
|
-
- The sitemap lists the pages in the menu, and `createPageStaticParams` returns their path segments on
|
|
506
|
-
a build-time site, because the module publishes no other public list of paths. A page outside the
|
|
507
|
-
menu renders on request.
|
|
508
|
-
|
|
509
|
-
`Navigation`, `Breadcrumbs`, `getNavigation(config)`, `getPageByPath(config, path)`,
|
|
510
|
-
`getRedirect(config, path)` and `pageHref(config, path)` are exported for a site that draws its own
|
|
511
|
-
layout. Every read goes through the same tagged, cached, per tenant read as the rest of the site.
|
|
512
|
-
|
|
513
|
-
### Pages built from blocks
|
|
514
|
-
|
|
515
|
-
A page can hold an ordered list of blocks in a json field, each `{ "type": ..., "props": { ... } }`.
|
|
516
|
-
The blueprint's `page` type has no such field, so add one:
|
|
517
|
-
|
|
518
|
-
```bash
|
|
519
|
-
curl -X POST "$CMS_URL/api/content-types/page/fields" -H "authorization: Bearer $TOKEN" \
|
|
520
|
-
-H 'content-type: application/json' -d '{"fieldName":"Blocks","displayName":"Blocks","type":"json"}'
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
A page with a non-empty list renders its blocks. A page without one renders its `Body` as before.
|
|
524
|
-
Field names come from `pageFields` in the config, and the type from `types.page`.
|
|
525
|
-
|
|
526
|
-
`PageView` draws the page's `Title` above its blocks. A page whose blocks open with their own
|
|
527
|
-
heading sets a boolean `HideTitle` field to stop it drawing twice: `Title` still names the page for
|
|
528
|
-
a tab and for search either way.
|
|
529
|
-
|
|
530
|
-
```json
|
|
531
|
-
[
|
|
532
|
-
{ "type": "richText", "props": { "markdown": "## Hello" } },
|
|
533
|
-
{ "type": "columns", "props": { "columns": [
|
|
534
|
-
[{ "type": "image", "props": { "src": "https://example.com/a.png", "alt": "A" } }],
|
|
535
|
-
[{ "type": "callToAction", "props": { "heading": "Talk to us", "label": "Contact", "href": "/contact" } }]
|
|
536
|
-
] } },
|
|
537
|
-
{ "type": "collection", "props": { "collection": "post", "limit": 3, "heading": "Latest" } }
|
|
538
|
-
]
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
Blocks come in four layers.
|
|
542
|
-
|
|
543
|
-
**Layout primitives** hold blocks and no content: `section` (a tone, a width and a padding step),
|
|
544
|
-
`stack`, `row` (side by side, wrapping on a phone), `grid`, `flow`, `panel` (a card with a tone and
|
|
545
|
-
a frame), `stickyBar` (a band that stays put while the page moves under it, at the top or the
|
|
546
|
-
bottom), `spacer` and `divider`.
|
|
547
|
-
|
|
548
|
-
A `stack` or a `panel` given an `href` is a link, the whole of it: a card whose face goes somewhere.
|
|
549
|
-
A site path is drawn with Next's `Link`, anything with a scheme as a plain anchor, and the address is
|
|
550
|
-
held to the same check as any link field. What is inside is the link's name, so nothing inside should
|
|
551
|
-
be a link of its own: one that holds a link, a button, a linked container, a control or a text with
|
|
552
|
-
a markdown link in it is drawn without its `href`, and the server log says so once, since a link
|
|
553
|
-
inside a link is markup a browser rewrites. A `section`, `stack` or `panel` given an `anchor` carries
|
|
554
|
-
it as its `id`, for a link to `#start` elsewhere; one that is not a plain name is left off.
|
|
555
|
-
|
|
556
|
-
`row` and `grid` take one list per cell, which suits a designer placing each one. `flow` takes a
|
|
557
|
-
single list and lays out whatever is in it, which is what a `repeat` and a preset's `slot` produce:
|
|
558
|
-
a card per row that came back, however many that is. Its column count is a choice and not a number,
|
|
559
|
-
because only a string field takes a binding, and a preset has to pass its own `columns` through.
|
|
560
|
-
|
|
561
|
-
**Content primitives** hold content and no layout: `text` (a variant from the theme's type scale,
|
|
562
|
-
and `format: "inline"` for code, emphasis, strong, links and an accent in the line; see
|
|
563
|
-
[Inline marks](#inline-marks-in-a-text-block); `tag` draws it as `p`, `span`, `code`, `strong`,
|
|
564
|
-
`em` or `h1` to `h4` rather than the variant's element, `decorative` hides it from a screen reader,
|
|
565
|
-
for an arrow after a link's words, unless its inline marks hold a link, and `title` is shown on hover),
|
|
566
|
-
`richText` (markdown), `image`, `video`, `embed` (an iframe, only for a host in `embedHosts`),
|
|
567
|
-
`icon`, `button`, `link`, `list`, `disclosure` (a labelled section that opens; give several the
|
|
568
|
-
same `group` and only one is open at a time), `comparisonTable` (rows typed as lines with `|`
|
|
569
|
-
between the cells, drawn as a real table with a heading on every column and every row) and
|
|
570
|
-
`progressBar` (how far along one thing is, as `role="progressbar"` so it is read and not only seen,
|
|
571
|
-
from a `value` already worked out, or from a `count` with either a `total` or what is `remaining`).
|
|
572
|
-
|
|
573
|
-
A comparison's first line is the column headings and the first cell of every line after it is that
|
|
574
|
-
row's heading. What goes in a cell is whatever the tenant types, ticks and dashes included: a mark
|
|
575
|
-
chosen inside the engine would be one more piece of English in the markup.
|
|
576
|
-
|
|
577
|
-
**Motion primitives** are drawn finished and animate only away from that: `reveal` (a wrapper whose
|
|
578
|
-
content lifts and fades in as it scrolls into view), `rotatingText` (a list of words in
|
|
579
|
-
`words`, one shown at a time; the older comma separated `items` still reads), `typingTerminal` (lines typed in sequence, held, then started again) and
|
|
580
|
-
`codeSample` (a snippet with its language and one click to select the whole of it). `text` takes a
|
|
581
|
-
`motion` of `countUp`, which counts a plain number up to the figure already written in the markup,
|
|
582
|
-
and `flow` takes a `hueRotate` of `subtle` or `wide`, which turns each cell's hue through the
|
|
583
|
-
theme's own colours in a cycle of three.
|
|
584
|
-
|
|
585
|
-
All of it is CSS. There is no script, so a page with JavaScript off renders the terminal typed out,
|
|
586
|
-
the figure at its number and the first word of the rotation standing, and every animation sits inside
|
|
587
|
-
`prefers-reduced-motion: no-preference`, so a visitor who asked for less motion gets exactly the same
|
|
588
|
-
finished page. "When it comes into view" is `animation-timeline: view()`, and a browser without it
|
|
589
|
-
shows the finished state too. `src/blocks/motion.test.tsx` strips the guards out of every stylesheet
|
|
590
|
-
these emit and fails if anything left animates or hides anything.
|
|
591
|
-
|
|
592
|
-
Every primitive takes theme tokens and never a colour or a pixel value. Tones are `page`, `surface`,
|
|
593
|
-
`accent`, `inverse`, `gradient` (the inverse band with the theme's three dark roles spread across
|
|
594
|
-
it) and `wash`, plus any the site names in `Tones` (see [Tokens and tones](#tokens-and-tones)); spacing is `none` to `xxl` from `theme.space`; type is a role from
|
|
595
|
-
`theme.text`; corners are `none`, `control`, `panel` or `pill`. A tenant that changes the scale
|
|
596
|
-
changes every page built from primitives, and nobody can put one client's blue into a block.
|
|
597
|
-
|
|
598
|
-
The one way past the tokens is a [style recipe](#style-recipes): every primitive takes `recipe`, the
|
|
599
|
-
name of a look the site defines in its settings, and draws with it in place of its own.
|
|
600
|
-
|
|
601
|
-
A tone belongs to the band and everything in it: a `section` or a `panel` publishes its ink, its
|
|
602
|
-
accent and its hairline, and the blocks inside read those rather than the page's. That is what makes
|
|
603
|
-
an inverse band readable, and it is why a block dropped anywhere still looks like it belongs.
|
|
604
|
-
|
|
605
|
-
**Presets** are named arrangements of primitives, stored as data: the shipped library below, plus
|
|
606
|
-
whatever a tenant saves of its own.
|
|
607
|
-
|
|
608
|
-
**Data blocks** load and choose rather than draw: `source`, `repeat`, `showIf`, `pager`, `filterBar`
|
|
609
|
-
and `slot`.
|
|
610
|
-
See bindings below.
|
|
611
|
-
|
|
612
|
-
Also built in, from before the layers: `columns` (up to four lists of blocks), `callToAction` and
|
|
613
|
-
`collection` (the newest posts, or authors or categories when the site has those routes). Pages
|
|
614
|
-
already hold these and they render unchanged.
|
|
615
|
-
|
|
616
|
-
A site adds its own blocks, or replaces a built-in, by registering a definition. The fields are the
|
|
617
|
-
only description of the props: the renderer reads props through them, and `app/api/blocks` publishes
|
|
618
|
-
them for an editor, so the two cannot drift.
|
|
619
|
-
|
|
620
|
-
```tsx
|
|
621
|
-
import { createBlockRegistry, defineBlock } from "barakopress";
|
|
622
|
-
|
|
623
|
-
const pricing = defineBlock<{ plan: string; price?: number }>({
|
|
624
|
-
type: "pricing",
|
|
625
|
-
label: "Pricing",
|
|
626
|
-
fields: [
|
|
627
|
-
{ name: "plan", kind: "text", required: true },
|
|
628
|
-
{ name: "price", kind: "number", min: 0 },
|
|
629
|
-
],
|
|
630
|
-
component: ({ props, theme }) => <p style={{ color: theme.colors.ink }}>{props.plan}: {props.price}</p>,
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
export const blocks = createBlockRegistry(config, [pricing]);
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
**Reading the schema from barakoBrew.** The console usually runs on another origin than the site, so
|
|
637
|
-
a browser only lets its block editor read `app/api/blocks` when the site names that origin. Set
|
|
638
|
-
`PRESS_CONSOLE_ORIGINS` to a comma separated list, for example
|
|
639
|
-
`PRESS_CONSOLE_ORIGINS=https://brew.example.com`. It is read per request. Unset, no origin is
|
|
640
|
-
allowed. A listed origin gets `Access-Control-Allow-Origin` echoed back on `GET` and on the `OPTIONS`
|
|
641
|
-
preflight, never `*` and never credentials, and every answer carries `Vary: Origin`. Pass
|
|
642
|
-
`{ consoleOrigins: [...] }` to both factories to set the list in code instead.
|
|
643
|
-
|
|
644
|
-
**Which tenant's schema.** Given the config, `createBlockSchemaRoute(config, blocks)` answers a
|
|
645
|
-
request-time site with the schema of the tenant the request belongs to: its own presets, and its own
|
|
646
|
-
tones on every tone field. The tenant is found as a page finds it, from `CMS_TENANT`, the header the
|
|
647
|
-
operator named, the host through the CMS, or `CMS_DEFAULT_TENANT`, so barakoBrew reads a tenant's
|
|
648
|
-
schema at that tenant's domain. Nothing else a caller sends picks the tenant. A host with no tenant
|
|
649
|
-
gets 404 `{ "error": "no site" }`, and a lookup that fails gets 503. The settings behind it are the
|
|
650
|
-
cached read every page makes, under the tenant's cache tag, so a settings change shows after the
|
|
651
|
-
delivery that purges it. A build-time site with no plugins gets the same answer as
|
|
652
|
-
`createBlockSchemaRoute(blocks)`, which still works and still ignores the request. Given only the
|
|
653
|
-
registry, the route cannot tell which plugins the site enabled, so it offers no plugin's blocks and
|
|
654
|
-
lists every plugin as off; a site with plugins passes the config.
|
|
655
|
-
|
|
656
|
-
Field kinds are `text`, `markdown`, `url`, `number`, `boolean`, `select` (with `options`), `slots`
|
|
657
|
-
(lists of nested blocks, handed to the component already rendered), `list` and `group` (below). The list is editor input, so a
|
|
658
|
-
block renders only when its type is registered and every prop passes its field. A present but wrong
|
|
659
|
-
value fails the whole block, a `url` must pass the same check markdown links do, and a component
|
|
660
|
-
never receives a prop its fields did not declare. A page reads at most 400 blocks in total, nested
|
|
661
|
-
ones included, and ten levels deep. The count is spent on every block the binder walks through as
|
|
662
|
-
well as every one that comes out, so a band from the library costs eight or ten of it, and a page
|
|
663
|
-
that goes over loses its tail with nothing said. What a `repeat` draws for its rows is counted
|
|
664
|
-
against a budget of its own; see `mode: "all"` under Bindings.
|
|
665
|
-
|
|
666
|
-
**Lists and groups.** A block that needs several of one thing declares a `list`, and a thing made
|
|
667
|
-
of parts declares a `group`, rather than numbered fields or text split in the component:
|
|
668
|
-
|
|
669
|
-
```ts
|
|
670
|
-
fields: [
|
|
671
|
-
{ name: "stages", kind: "list", min: 2, max: 6, item: { kind: "group", label: "Stage", fields: [
|
|
672
|
-
{ name: "label", kind: "text", required: true },
|
|
673
|
-
{ name: "body", kind: "markdown" },
|
|
674
|
-
] } },
|
|
675
|
-
{ name: "tags", kind: "list", max: 8, item: { kind: "text" } },
|
|
676
|
-
{ name: "cta", kind: "group", fields: [{ name: "label", kind: "text" }, { name: "href", kind: "url" }] },
|
|
677
|
-
]
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
A list's `item` is `text`, `url`, `number` (with its own `min` and `max`) or `group`, and the list's
|
|
681
|
-
own `min` and `max` count its entries. A list with no `max` holds at most 100. A group's `fields`
|
|
682
|
-
are any field kind except `slots`, and lists and groups nest three deep. A stored list checks every
|
|
683
|
-
entry, and one wrong entry, a list too short or too long, or a group missing a required part fails
|
|
684
|
-
the block like any wrong value. An empty list reads as absent. A group hands its component only the
|
|
685
|
-
keys its fields declare.
|
|
686
|
-
|
|
687
|
-
The schema at `app/api/blocks` stays version 2 and publishes `item` and `fields` for these, with
|
|
688
|
-
`bindable` resolved at every level. They only add a kind and two keys, and a console that does not
|
|
689
|
-
know a kind edits that one field as JSON, so a block with no list or group publishes exactly what it
|
|
690
|
-
did before.
|
|
691
|
-
|
|
692
|
-
`defineBlock<Props, SlotNames>` checks the fields against the props at compile time: every field
|
|
693
|
-
names a prop, its kind suits the prop's type, and a prop that is not optional must be `required`.
|
|
694
|
-
|
|
695
|
-
A component gets `props`, `slots` and `theme`, not the config, because a client component's props
|
|
696
|
-
are serialised into the page and the config holds the CMS address. A server block that needs the
|
|
697
|
-
config closes over it.
|
|
698
|
-
|
|
699
|
-
A block that shows something depending on who is looking sets `perViewer: true`. `createPage` leaves
|
|
700
|
-
such blocks out, because its output is cached and shared. `createViewerPage` renders them and calls
|
|
701
|
-
`connection()` first, so that route is always dynamic. Signing a viewer in, and gating a block by
|
|
702
|
-
role, is issue #7.
|
|
703
|
-
|
|
704
|
-
### Bindings
|
|
705
|
-
|
|
706
|
-
Any string prop can hold a placeholder, which is barakoCMS's workflow template syntax with an
|
|
707
|
-
optional format and fallback:
|
|
708
|
-
|
|
709
|
-
```json
|
|
710
|
-
{ "type": "text", "props": { "value": "Welcome to {{site.Name}}", "variant": "display" } }
|
|
711
|
-
{ "type": "text", "props": { "value": "{{item.Fee | money ?? Free}}" } }
|
|
712
|
-
{ "type": "text", "props": { "value": "{{page.PublishedAt | date}}" } }
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
Scopes are `site` (the tenant's settings and resolved identity), `page` (the entry the page
|
|
716
|
-
renders), `item` (the row inside a `source` or a `repeat`) and `query` (URL parameters). An item
|
|
717
|
-
reads its entry's own fields by name, with the collection's roles (`Title`, `Summary`, `Body`,
|
|
718
|
-
`Date` and the rest) laid over them only where the collection's `fields` maps that role, so an
|
|
719
|
-
unmapped `Body` is still the entry's own `Body`. `count`, `sum` and `group` are about a set of rows
|
|
720
|
-
rather than one, and are described with `source` below. `viewer` arrives with #7. Formats are `text`, `date`, `datetime`, `time`, `money`, `number`, `upper` and
|
|
721
|
-
`lower`; `money` uses the tenant's `Currency` setting, or a plain amount when it has none.
|
|
722
|
-
|
|
723
|
-
Paths, formats and fallbacks only. There are no expressions and no JavaScript. Everything resolves
|
|
724
|
-
on the server as the request's tenant, before a component is called, so a binding never makes the
|
|
725
|
-
browser call the API. A placeholder that finds no value renders its fallback and is reported in the
|
|
726
|
-
server log, never a crash. What a binding resolves to is checked against the field again, so a link
|
|
727
|
-
whose stored field holds `javascript:` drops the block; and a resolved value is never rescanned, so
|
|
728
|
-
one field cannot reach another through its own contents.
|
|
729
|
-
|
|
730
|
-
A `list` or a `group` takes its whole value from one placeholder with nothing around it, no format
|
|
731
|
-
and no fallback: `"tags": "{{item.Tags}}"` fills the list with the array itself, and
|
|
732
|
-
`"cta": "{{item.Link}}"` a group with the object. What it resolves to is data. Each entry is checked
|
|
733
|
-
against the field, an entry that fails is left out rather than failing the block, the list is cut at
|
|
734
|
-
its `max` since the data is not the editor's to shorten, and one that comes up short of `min` drops
|
|
735
|
-
the block. Nothing in it is scanned for placeholders. A list or a group typed out in full binds
|
|
736
|
-
each string inside it like any string prop.
|
|
737
|
-
|
|
738
|
-
`source` loads one entry or a page of them and puts it in scope. `repeat` renders its content once
|
|
739
|
-
per row. `pager` draws prev and next links for the `source` it sits in, paged by the API. `showIf`
|
|
740
|
-
keeps its content only when a bound value has something, or equals what it names.
|
|
741
|
-
|
|
742
|
-
```json
|
|
743
|
-
{ "type": "source", "props": {
|
|
744
|
-
"collection": "enrolments", "mode": "list", "pageSize": 20, "pageParam": "p",
|
|
745
|
-
"filterField": "Class", "filterValue": "{{query.class}}",
|
|
746
|
-
"content": [[
|
|
747
|
-
{ "type": "repeat", "props": { "empty": "No one yet.", "content": [[
|
|
748
|
-
{ "type": "text", "props": { "value": "{{item.Title}}" } }
|
|
749
|
-
]] } },
|
|
750
|
-
{ "type": "pager", "props": {} }
|
|
751
|
-
]]
|
|
752
|
-
} }
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
A filter narrows what the API already lets the reader see. It is never access control: who may read
|
|
756
|
-
which rows is decided in barakoCMS. A page reads at most eight sources, and a `source` at most fifty
|
|
757
|
-
rows a page.
|
|
758
|
-
|
|
759
|
-
`mode: "all"` is for a page that is the whole of a collection rather than a page of it, a changelog
|
|
760
|
-
say: the source reads every row, fifty to a request, up to 500 (`MAX_ALL_ROWS`). Its first request
|
|
761
|
-
is one of the page's eight reads, and the rest are made at once, out of 16 more a page's `all`
|
|
762
|
-
sources share between them (`MAX_ALL_REQUESTS`). A source past 500 rows, or past what the requests
|
|
763
|
-
cover, reads what it can and says so in the server log, and `{{count}}` stays what the collection
|
|
764
|
-
holds. A `pager` inside it draws nothing, and sums, distinct counts, groups and a filter bar cover
|
|
765
|
-
every row it read. A `repeat` draws every row its source read unless its `limit` names fewer. What
|
|
766
|
-
the rows of a repeat and the groups of a grouped source draw spends a budget of its own, 10,000
|
|
767
|
-
blocks (`MAX_ROW_BLOCKS`), rather than the page's 400, so a long list neither takes the blocks after
|
|
768
|
-
it off the page nor renders without end; rows or groups past it are left out and the log says how
|
|
769
|
-
many were drawn.
|
|
770
|
-
|
|
771
|
-
**Counts, sums and groups.** `{{count.<collection>}}` anywhere on a page is how many published
|
|
772
|
-
entries the collection has, for example `{{count.posts}}`. It is the delivery API's `totalItems` for
|
|
773
|
-
a page of one row, cached like every other read, and the public API lists published entries only, so
|
|
774
|
-
it is the number of rows the page could list. Inside a `source`, `{{count}}` on its own is how many
|
|
775
|
-
rows that source's filter matched, all of them and not just the page it read:
|
|
776
|
-
|
|
777
|
-
```json
|
|
778
|
-
{ "type": "source", "props": {
|
|
779
|
-
"collection": "packages", "mode": "list", "pageSize": 1,
|
|
780
|
-
"filterField": "Category", "filterValue": "Auth",
|
|
781
|
-
"content": [[ { "type": "text", "props": { "value": "{{count}} auth modules" } } ]]
|
|
782
|
-
} }
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
A filtered count is a `source` rather than something like `{{count.packages where Category=Auth}}`
|
|
786
|
-
because a placeholder is paths, formats and fallbacks, and a filter value is free text: a value with a
|
|
787
|
-
space or a hyphen cannot be a path segment, and a placeholder that could carry a condition would be
|
|
788
|
-
the start of an expression language. `filterField` and `filterValue` already bind, so the value can
|
|
789
|
-
come from `{{query.c}}` or an item, and the count costs the one read the source makes anyway. Set
|
|
790
|
-
`pageSize` to 1 when the count is all the source is for.
|
|
791
|
-
|
|
792
|
-
Each collection counted is one of the page's eight reads, spent the first time a placeholder names
|
|
793
|
-
it and shared by every other placeholder that names it. Sources and counts draw on the same eight, in
|
|
794
|
-
the order the page is written. A count past the budget, one for a collection the tenant does not
|
|
795
|
-
have, and one whose read failed all render the fallback and are reported as `no value`.
|
|
796
|
-
|
|
797
|
-
`{{sum.<Field>}}` inside a `source` adds that field over the rows the source read, for example the
|
|
798
|
-
open issues across a roadmap's milestones. A number stored as text counts. A field with a word in
|
|
799
|
-
any row is not a sum and renders its fallback, and a source with no rows has no sums, so write
|
|
800
|
-
`{{sum.Open ?? 0}}` where zero is the right answer. A sum covers the rows read: fifty at most in
|
|
801
|
-
`list` mode, where a source that pages sums the page it is on, and up to 500 in `all` mode.
|
|
802
|
-
|
|
803
|
-
`{{distinct.<Field>}}` is how many different values that field holds among the same rows: the
|
|
804
|
-
repositories a list of issues spans, "16 issues across 3 repositories". Values compare as the text a
|
|
805
|
-
placeholder prints, each entry of a list field counts on its own, and a row with nothing in the field
|
|
806
|
-
adds nothing, so a field no row holds renders its fallback. Inside a group it counts that group's rows.
|
|
807
|
-
It counts only what the source read, so a source that matched more rows than it read (a page of a
|
|
808
|
-
longer list) has no distinct counts and renders the fallback: `{{distinct.Repo ?? several}}`. It
|
|
809
|
-
reads a top-level field; a dotted path such as `Owner.Login` renders the fallback.
|
|
810
|
-
|
|
811
|
-
`groupBy` names a field, and the source's content then renders once per distinct value of it among
|
|
812
|
-
the rows read, in the order first seen. `groupOrder` is a comma separated list of values to put
|
|
813
|
-
first, in that order; the rest follow in the order first seen. Inside each group `{{group.key}}` is
|
|
814
|
-
the value and `{{group.count}}` how many rows have it, and `repeat` and `{{sum.X}}` work over that
|
|
815
|
-
group's rows. `{{count}}` stays what the whole source matched. A row with nothing in the field is
|
|
816
|
-
kept, in a group whose key is empty, so `{{group.key ?? Other}}` names it. A pager inside a grouped
|
|
817
|
-
source renders nothing, since a page of rows is not a page of groups. One grouped source is one read,
|
|
818
|
-
where a source per value was one read each:
|
|
819
|
-
|
|
820
|
-
```json
|
|
821
|
-
{ "type": "source", "props": {
|
|
822
|
-
"collection": "milestones", "mode": "list", "pageSize": 50,
|
|
823
|
-
"groupBy": "Repository", "groupOrder": "barakoCMS, barakoPress",
|
|
824
|
-
"content": [[
|
|
825
|
-
{ "type": "text", "props": { "value": "{{group.key}}: {{sum.Open}} open in {{group.count}} milestones" } },
|
|
826
|
-
{ "type": "repeat", "props": { "content": [[ { "type": "text", "props": { "value": "{{item.Title}}" } } ]] } }
|
|
827
|
-
]]
|
|
828
|
-
} }
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
All of it resolves on the server as the request's tenant, so a count or a sum never makes the
|
|
832
|
-
browser call the API. A page that uses none of it reads and renders exactly what it did before.
|
|
833
|
-
|
|
834
|
-
**Filter buttons.** A `filterBar` inside a `source` draws one button per distinct value of `field`
|
|
835
|
-
among the rows the source read, after an "all" button (`allLabel`, which binds, so `All {{count}}`
|
|
836
|
-
works). The values come in the order first seen; `order` is a comma separated list to put first, and
|
|
837
|
-
a value it names that no row holds gets no button. `separator` splits a text field that holds several
|
|
838
|
-
values, such as `4.4.0, 4.3.0`, and a list field gives one value per entry. A bar offers at most a
|
|
839
|
-
hundred values (`MAX_FILTER_VALUES`), the first hundred in that order, and a row carries only values
|
|
840
|
-
the bar offers. Fewer than two values is no choice, so the bar draws nothing.
|
|
841
|
-
|
|
842
|
-
```json
|
|
843
|
-
{ "type": "source", "props": {
|
|
844
|
-
"collection": "packages", "mode": "list", "pageSize": 50,
|
|
845
|
-
"content": [[
|
|
846
|
-
{ "type": "filterBar", "props": { "field": "Category", "allLabel": "All {{count}}", "label": "Filter by category" } },
|
|
847
|
-
{ "type": "repeat", "props": { "content": [[ { "type": "text", "props": { "value": "{{item.Title}}" } } ]] } }
|
|
848
|
-
]]
|
|
849
|
-
} }
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
The rows stay server-rendered and the buttons read nothing. The binder marks each row's own blocks
|
|
853
|
-
with `data-bp-filter`, the bar's id, and `data-bp-filter-values`, its values, each written as
|
|
854
|
-
`<id>:<value>` with the value percent-encoded so a value with a space is one token. A row of a source
|
|
855
|
-
nested in another source's row belongs to both bars, so each attribute can hold two, and each bar's
|
|
856
|
-
rule reads only its own. The id is `f<n>-<scope>`, where the scope is the part of the page the bind
|
|
857
|
-
draws (`body`, `header`, `footer`, or `index` for a collection's index page, and `scope` in
|
|
858
|
-
`bindBlocks` options for a site that binds its own), so a bar in the header and an identical one in the
|
|
859
|
-
body never share an id, and the same page renders the same bytes every time. A click sets `aria-pressed` on the button and
|
|
860
|
-
`data-bp-filter-value` on the bar, and writes one rule that hides every row of that bar without the
|
|
861
|
-
value, with the value escaped as a CSS string and `!important` so it wins over a row's inline
|
|
862
|
-
`display`. A site writes no rule of its own, so a value nobody planned for still filters, and a
|
|
863
|
-
reader with no script sees every row. The buttons are toggles with `aria-pressed` inside a labelled
|
|
864
|
-
group, not a tablist, since the tab pattern promises arrow keys and panels this does not have. A row
|
|
865
|
-
with nothing in the field has no value, so any choice hides it.
|
|
866
|
-
|
|
867
|
-
The bar draws its buttons from the theme's colours. `recipe` names a style recipe for the row,
|
|
868
|
-
`buttonRecipe` one for each button and `pressedRecipe` one for the button that is pressed, each
|
|
869
|
-
replacing that look outright as a recipe does on a primitive; a button keeps `cursor: pointer`.
|
|
870
|
-
|
|
871
|
-
In a grouped source the bar is drawn once, ahead of the groups, and filters all of them. With
|
|
872
|
-
`hideEmptyGroups`, a group's own blocks carry every value its rows hold, so the same rule hides a
|
|
873
|
-
group that has no row left. Without it a group stays with its heading and no rows. Only the first bar
|
|
874
|
-
in a source counts. A grouped source honours only a bar at the top level of its content; one inside a
|
|
875
|
-
band there would repeat with every group, so it draws nothing and marks no row. The
|
|
876
|
-
values are those of the rows read, so a source that pages filters the page it is on.
|
|
877
|
-
|
|
878
|
-
### Presets
|
|
879
|
-
|
|
880
|
-
A preset is a named block saved as data, not code: a few props and an arrangement of primitives that
|
|
881
|
-
reads them through the `props` scope. A designer saves one in barakoBrew and every site on the
|
|
882
|
-
published image can use it, with no barakoPress release.
|
|
883
|
-
|
|
884
|
-
```json
|
|
885
|
-
{
|
|
886
|
-
"type": "band",
|
|
887
|
-
"label": "Band",
|
|
888
|
-
"fields": [
|
|
889
|
-
{ "name": "heading", "kind": "text", "required": true },
|
|
890
|
-
{ "name": "tone", "kind": "select", "options": ["page", "accent"] },
|
|
891
|
-
{ "name": "content", "kind": "slots" }
|
|
892
|
-
],
|
|
893
|
-
"blocks": [
|
|
894
|
-
{ "type": "section", "props": { "tone": "{{props.tone}}", "content": [[
|
|
895
|
-
{ "type": "text", "props": { "value": "{{props.heading}}", "variant": "title" } },
|
|
896
|
-
{ "type": "slot", "props": { "name": "content" } }
|
|
897
|
-
]] } }
|
|
898
|
-
]
|
|
899
|
-
}
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
A preset's fields may be lists and groups, in the same shape a block declares them, and
|
|
903
|
-
`{{props.x}}` of a list passes the list itself into a primitive's list field:
|
|
904
|
-
|
|
905
|
-
```json
|
|
906
|
-
{
|
|
907
|
-
"type": "taglines",
|
|
908
|
-
"fields": [{ "name": "words", "kind": "list", "max": 8, "item": { "kind": "text" } }],
|
|
909
|
-
"blocks": [{ "type": "rotatingText", "props": { "words": "{{props.words}}" } }]
|
|
910
|
-
}
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
A request-time site reads its tenant's presets from the `Presets` site setting. A build-time site
|
|
914
|
-
passes them as `presets` in the config. `/api/blocks` lists a tenant's own presets beside the shipped ones when
|
|
915
|
-
it is mounted with the config. A preset never replaces a block that is code. It does
|
|
916
|
-
replace a preset, which is how a tenant adjusts one of the shipped blocks below without waiting for
|
|
917
|
-
a release. A preset body may not use another preset saved in the same pass, so a cycle cannot form;
|
|
918
|
-
it may use one compiled earlier, which is how a tenant's own block builds on a shipped one. `slot` marks where the content an editor dropped into the preset
|
|
919
|
-
goes, and that content binds in the page's scope rather than the preset's.
|
|
920
|
-
|
|
921
|
-
A preset that is not used says why, once, in the server log, with one line per reason rather than one
|
|
922
|
-
catchall: an entry that is not a preset, a type that is not a name, a type an earlier preset already
|
|
923
|
-
uses, a field list that is not a list, fields inside it that are not fields, a name a registered
|
|
924
|
-
block already has, and the presets past the point where a tenant's bodies hold more than 400 blocks
|
|
925
|
-
between them. A setting that quietly does nothing reads as done, which is worse than one that is
|
|
926
|
-
missing. A bad paste is spelled out five times and then counted.
|
|
927
|
-
|
|
928
|
-
### The block library
|
|
929
|
-
|
|
930
|
-
Every site gets these, compiled in from the primitives. None of them is code, so a site that wants
|
|
931
|
-
one to look different saves its own under the same name and that one wins.
|
|
932
|
-
|
|
933
|
-
| Block | What it is | Props |
|
|
934
|
-
| --- | --- | --- |
|
|
935
|
-
| `hero` | The band at the head of a page | heading, body, image, imageAlt, primaryLabel, primaryHref, secondaryLabel, secondaryHref, tone, columns, align, padding |
|
|
936
|
-
| `band` | Copy with one call to action | tone, heading, body, label, href, align, padding, width |
|
|
937
|
-
| `statBand` | A row of figures | heading, tone, columns, padding, items |
|
|
938
|
-
| `cardGrid` | Cards from a collection | heading, collection, filterField, filterValue, empty, tone, columns, padding, hueRotate, option |
|
|
939
|
-
| `peopleGrid` | People from a collection, typed in place, or both | heading, collection, filterField, filterValue, role, tone, columns, padding, items |
|
|
940
|
-
| `timeline` | Dated entries | heading, tone, padding, width, items |
|
|
941
|
-
| `steps` | Numbered entries | heading, tone, padding, width, items |
|
|
942
|
-
| `tiers` | Giving or pricing tiers | heading, tone, columns, padding, items |
|
|
943
|
-
| `keyValueTable` | A panel of facts | heading, tone, padding, radius, rows |
|
|
944
|
-
| `tabs` | Sections that open | heading, tone, padding, width, items |
|
|
945
|
-
| `announcement` | The one line above everything, and it stays there | message, label, href, tone, edge, align |
|
|
946
|
-
| `codeTabs` | A snippet to run, with the other ways behind it | heading, body, code, language, selectLabel, tone, padding, width, items |
|
|
947
|
-
| `progressList` | How far along each entry of a collection is | heading, collection, filterField, filterValue, empty, tone, padding, width |
|
|
948
|
-
| `changelogList` | Release entries, with the word for the kind of release | heading, collection, filterField, filterValue, empty, tone, padding, width |
|
|
949
|
-
| `faq` | Questions, all openable at once | heading, body, tone, padding, width, items |
|
|
950
|
-
| `map` | An embedded map, held to `embedHosts` | src, title, heading, aspect, tone, padding |
|
|
951
|
-
| `stat` | One figure and its label | value, label, align, motion |
|
|
952
|
-
| `timelineEntry` | One dated entry | date, title, body |
|
|
953
|
-
| `step` | One numbered step | number, title, body |
|
|
954
|
-
| `tier` | One tier | name, amount, body, label, href, tone |
|
|
955
|
-
| `person` | One person | name, role, photo, href, linkLabel, align |
|
|
956
|
-
| `keyValueRow` | One fact | label, value |
|
|
957
|
-
| `codeTab` | One more way to run it | label, code, language, selectLabel, group |
|
|
958
|
-
| `faqItem` | One question and its answer | question, answer, tone |
|
|
959
|
-
|
|
960
|
-
The blocks in the second half go in the first half's slots: stats in a `statBand`, entries in a
|
|
961
|
-
`timeline`, `disclosure` blocks in `tabs`, `codeTab` blocks in `codeTabs`, `faqItem` blocks in
|
|
962
|
-
`faq`. A card grid reads its entries through `{{item.Title}}`, `{{item.Summary}}`,
|
|
963
|
-
`{{item.Date | date}}` and `{{item.Href}}`, so it works against whatever the tenant calls those
|
|
964
|
-
fields, and it takes the collection as a prop rather than knowing any name. `{{item.Href}}` is the
|
|
965
|
-
collection's `href` field when it has one, and the item's own route otherwise, so a collection with
|
|
966
|
-
no route on this site (one filled by a sync, say) still gets a working card once its entries carry
|
|
967
|
-
their own link.
|
|
968
|
-
|
|
969
|
-
Set a card grid's `option` to `show` and each card carries the glyph and the word the site declared
|
|
970
|
-
for that entry's option, read through `{{item.Icon}}` and `{{item.Word}}`. With `filterField` and
|
|
971
|
-
`filterValue` picking the category, that is a module grid: one band per category, each card marked
|
|
972
|
-
with its own. It is off by default, so a grid that did not ask for it draws what it always drew.
|
|
973
|
-
|
|
974
|
-
`progressList` reads its figure through `{{item.Progress}}`, which is the collection's `progress`
|
|
975
|
-
field role: the tenant says which of its own fields holds a number from 0 to 100. Where a source
|
|
976
|
-
gives two counts instead, `progressCount` and `progressTotal` carry those through and the bar does
|
|
977
|
-
the division, so a GitHub milestone's open and closed issues need no field computing a percentage
|
|
978
|
-
first. `changelogList` does not group by itself, because grouping means knowing which field holds
|
|
979
|
-
the kind and that is the tenant's field name. One band per kind with `filterField` and `filterValue`
|
|
980
|
-
is the grouping, and the chip on each entry is the option's own word.
|
|
981
|
-
|
|
982
|
-
### Plugin packages
|
|
983
|
-
|
|
984
|
-
When no block covers a need, a developer writes one as a plugin package, and the published image
|
|
985
|
-
stays as it is. A plugin is an npm package that ships built JavaScript, like this one does, and whose
|
|
986
|
-
default export is `definePlugin`:
|
|
987
|
-
|
|
988
|
-
```tsx
|
|
989
|
-
import { defineBlock, definePlugin } from "barakopress";
|
|
990
|
-
|
|
991
|
-
type TallyProps = { count: number; label: string };
|
|
992
|
-
|
|
993
|
-
const tally = defineBlock<TallyProps>({
|
|
994
|
-
type: "tally",
|
|
995
|
-
label: "Tally",
|
|
996
|
-
fields: [
|
|
997
|
-
{ name: "count", kind: "number", required: true },
|
|
998
|
-
{ name: "label", kind: "text", required: true },
|
|
999
|
-
],
|
|
1000
|
-
component: ({ props, theme }) => (
|
|
1001
|
-
<p style={{ color: theme.colors.accent }}>{props.count} {props.label}</p>
|
|
1002
|
-
),
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
export default definePlugin({ name: "tally", blocks: [tally] });
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
`defineBlock` checks the fields against the props at compile time, the same as the built-ins: a field
|
|
1009
|
-
name the props do not have, a kind that does not suit the prop's type, or an optional field for a prop
|
|
1010
|
-
the component treats as always there does not compile. `examples/plugin-sample` is a complete one,
|
|
1011
|
-
with its `package.json` and `tsconfig.json`; CI packs it, builds an image with it and renders it.
|
|
1012
|
-
|
|
1013
|
-
A plugin block is passed what every block is passed: its props, checked against its fields, its
|
|
1014
|
-
slots, rendered, and the theme. It is not passed the config, the CMS address or a token, and data from
|
|
1015
|
-
the CMS should reach it through a binding in its props or a module endpoint, not a credential. That is
|
|
1016
|
-
what it is handed, not what it can reach: its code runs in the server with full access, `process.env`
|
|
1017
|
-
and every `barakopress` export included. Installing a plugin means trusting it with the deployment.
|
|
1018
|
-
|
|
1019
|
-
Its name may not be one a built-in, a library block, the site or another plugin already registered.
|
|
1020
|
-
Replacing one would change that block for every tenant, including the ones that never enabled the
|
|
1021
|
-
plugin.
|
|
1022
|
-
|
|
1023
|
-
**Installing: a derived image.** Blocks are registered when the Next build runs, so a package cannot
|
|
1024
|
-
be added to a built image. The published image holds the built server and not the source or the
|
|
1025
|
-
toolchain, so the derived image is built from the engine's source at a release tag, which is what the
|
|
1026
|
-
published image of that tag was built from, with the plugins handed in as a build context:
|
|
1027
|
-
|
|
1028
|
-
```bash
|
|
1029
|
-
mkdir plugins
|
|
1030
|
-
(cd ../my-plugin && npm pack --pack-destination ../site/plugins) # your own plugin
|
|
1031
|
-
npm pack barakopress-plugin-tally@1.2.0 --pack-destination plugins # one from npm
|
|
1032
|
-
|
|
1033
|
-
docker buildx build \
|
|
1034
|
-
--build-context plugins=./plugins \
|
|
1035
|
-
-t my-press:0.8.0-plugins \
|
|
1036
|
-
https://github.com/BaryoDev/barakoPress.git#v0.8.0
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
`examples/derived-image/compose.yml` is the same thing in compose. The `plugins` directory holds
|
|
1040
|
-
tarballs and nothing else, so what is built is exactly the bytes that were packed. The install runs
|
|
1041
|
-
offline, so a plugin's own dependencies must travel inside its tarball: list each one in
|
|
1042
|
-
`bundleDependencies`, and a tarball with a dependency it does not bundle is refused. So is a plugin
|
|
1043
|
-
named like a package the engine already has (`react`, `next`, anything in its lockfile), which would
|
|
1044
|
-
otherwise be linked over it. The build installs them beside the engine with no install scripts,
|
|
1045
|
-
and writes `press.plugins.ts`, which the reference
|
|
1046
|
-
`press.config.ts` passes to `createBlockRegistry(config, [], { plugins })`. An overlay with its own
|
|
1047
|
-
`press.config.ts` imports `plugins` from `@/press.plugins` and passes it the same way. Pin the tag,
|
|
1048
|
-
and next to it the commit it points at (`git ls-remote https://github.com/BaryoDev/barakoPress.git
|
|
1049
|
-
v0.8.0`), since a tag can be moved; building from `#<commit>` is the strict form. The `v0.8.0` tag
|
|
1050
|
-
exists from the 0.8.0 release on. Moving it is an engine upgrade, and a plugin should be rebuilt and
|
|
1051
|
-
checked against it.
|
|
1052
|
-
|
|
1053
|
-
**Enabling: per tenant.** One derived image carries every plugin the deployment installs. A tenant
|
|
1054
|
-
renders a plugin's blocks only when the `Plugins` setting in its `site` settings entry names it:
|
|
1055
|
-
|
|
1056
|
-
```json
|
|
1057
|
-
{ "Plugins": ["tally"] }
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
Until then, it is as if the plugin were not installed for that tenant: `/api/blocks` does not offer its
|
|
1061
|
-
blocks, a page that holds one renders everything around it and not the block, and a preset whose body
|
|
1062
|
-
draws one is left out. A list saved empty turns every plugin off; a missing field leaves the ones the
|
|
1063
|
-
config names (`plugins` in `defineConfig`, empty unless set). A build-time site enables plugins with
|
|
1064
|
-
`plugins` in its config. `/api/blocks` also answers `plugins`, every installed plugin with whether the
|
|
1065
|
-
tenant enabled it, which is what barakoBrew reads to show the switch. Each block a plugin added carries
|
|
1066
|
-
`plugin` with its name.
|
|
1067
|
-
|
|
1068
|
-
What enablement does not do is keep code apart. Every plugin's module is loaded in the container for
|
|
1069
|
-
every tenant it serves, and a tenant that has not enabled it is only kept from rendering it. So a
|
|
1070
|
-
plugin nobody on the deployment trusts is not installed there. When tenants must not share plugins,
|
|
1071
|
-
the answer is a separate deployment, its own barakoCMS, barakoBrew and barakoPress, not a second
|
|
1072
|
-
barakoPress against the same API. A hotel's branch landing pages share one deployment; its booking
|
|
1073
|
-
system gets its own.
|
|
148
|
+
`Card` and `PostView` are exported too, for a site that wants its own page but the engine's markup. `SiteAnalytics` is exported too, for a site that draws its own root layout: it goes in that layout's `<head>`.
|
|
149
|
+
|
|
150
|
+
## What it renders
|
|
151
|
+
|
|
152
|
+
Each of these is its own page in `docs/`, because npm shows only the first 64 KiB of a README.
|
|
153
|
+
|
|
154
|
+
- [Collections](https://github.com/BaryoDev/barakoPress/blob/master/docs/collections.md): any content type as a list and a detail page, with
|
|
155
|
+
references, related items, a long-form article layout, and a collection drawn as a docs tree with
|
|
156
|
+
sections, products, search and an in-page index.
|
|
157
|
+
- [Pages](https://github.com/BaryoDev/barakoPress/blob/master/docs/pages.md): the Pages module's page tree and menus at a mount, reserved slugs,
|
|
158
|
+
and pages built from blocks: layout and content primitives, lists and groups, the block budget.
|
|
159
|
+
- [Bindings](https://github.com/BaryoDev/barakoPress/blob/master/docs/bindings.md): `{{site.X}}`, `{{page.X}}`, `{{item.X}}` and `{{query.X}}`
|
|
160
|
+
in a block's props, sources that read a collection, repeats, counts, sums and groups.
|
|
161
|
+
- [Presets, the block library and plugins](https://github.com/BaryoDev/barakoPress/blob/master/docs/blocks.md): named arrangements of primitives,
|
|
162
|
+
the blocks every site gets, and plugin packages built into a derived image.
|
|
163
|
+
- [Sites and the header](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md): one image serving many sites, identity and theme from
|
|
164
|
+
each tenant's settings, labels, holding mode and share links, and the built-in header.
|
|
165
|
+
- [The look](https://github.com/BaryoDev/barakoPress/blob/master/docs/look.md): the theme, tokens and tones, style recipes, inline marks, and assets
|
|
166
|
+
used exactly as supplied.
|
|
167
|
+
- [Caching and the webhook](https://github.com/BaryoDev/barakoPress/blob/master/docs/caching.md): what a publish drops, running more than one
|
|
168
|
+
container, the render cache, the one secret, and a key per tenant.
|
|
169
|
+
- [Deploying](https://github.com/BaryoDev/barakoPress/blob/master/docs/deploying.md): the reference app, its container image, the compose stack and
|
|
170
|
+
the BaryoVM release manifests.
|
|
1074
171
|
|
|
1075
172
|
## Configuring it
|
|
1076
173
|
|
|
@@ -1097,7 +194,7 @@ for a post type with no such field.
|
|
|
1097
194
|
|
|
1098
195
|
| Key | Default | Tenant setting | What |
|
|
1099
196
|
| --- | --- | --- | --- |
|
|
1100
|
-
| `site` | required | `Name`, `Tagline`, `Url` and one per field | Name, tagline and absolute origin. Every absolute link is built from the origin |
|
|
197
|
+
| `site` | required | `Name`, `Tagline`, `Url` and one per field | Name, tagline and absolute origin. Every absolute link is built from the origin. `analyticsScript` and `analyticsWebsiteId` (`AnalyticsScript`, `AnalyticsWebsiteId`) put a Umami script in the head, from an origin in `PRESS_SCRIPT_ORIGINS` |
|
|
1101
198
|
| `types` | `post`, `author`, `category` | operator only | The content type names |
|
|
1102
199
|
| `fields` | the blueprint's PascalCase names | operator only | Which field holds what |
|
|
1103
200
|
| `pageFields` | the blueprint's PascalCase names | operator only | Which field on the page type holds what |
|
|
@@ -1109,20 +206,20 @@ for a post type with no such field.
|
|
|
1109
206
|
| `locale` | `en-GB` | `Locale` | Passed to `toLocaleDateString` |
|
|
1110
207
|
| `currency` | none | `Currency` | The ISO code a `money` binding formats with. Unset, an amount renders as a plain number |
|
|
1111
208
|
| `embedHosts` | seven player hosts | `EmbedHosts` | The hosts an `embed` block may frame |
|
|
1112
|
-
| `presets` | none | `Presets` | Named blocks saved as arrangements of primitives. See Presets |
|
|
209
|
+
| `presets` | none | `Presets` | Named blocks saved as arrangements of primitives. See [Presets](https://github.com/BaryoDev/barakoPress/blob/master/docs/blocks.md#presets) |
|
|
1113
210
|
| `cmsUrl` | `CMS_URL`, or `http://localhost:5005` | operator only | Where the CMS is, from this server. The variable is read when the CMS is called, not when this file runs |
|
|
1114
211
|
| `tenant` | `CMS_TENANT` | operator only | Tenant slug, for a multi-tenant deployment. On a request-time site, pins every host to it. The variable is read when the CMS is called, not when this file runs |
|
|
1115
|
-
| `sites` | off | operator only | Request-time identity and theme from the tenant's site settings. See
|
|
212
|
+
| `sites` | off | operator only | Request-time identity and theme from the tenant's site settings. See [One build, many sites](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md#one-build-many-sites) |
|
|
1116
213
|
| `pages` | off | operator only | Where the Pages module's pages are mounted. `""` is the site root. It has to match a route file on disk, which is why it is not a tenant's to set |
|
|
1117
214
|
| `reservedSlugs` | the routes and the engine's files | `ReservedSlugs`, added to them | First path segments a root-mounted page may not take. Adds to the defaults |
|
|
1118
215
|
| `regions` | off | `HeaderPath`, `HeaderTone`, `FooterPath`, `FooterTone` | The header and the footer as block regions |
|
|
1119
|
-
| `collections` | the blog's `post`, `author` and `category` | `Collections` | Content types rendered as lists and detail pages, with each index's copy, index page and default byline. See Collections |
|
|
216
|
+
| `collections` | the blog's `post`, `author` and `category` | `Collections` | Content types rendered as lists and detail pages, with each index's copy, index page and default byline. See [Collections](https://github.com/BaryoDev/barakoPress/blob/master/docs/collections.md) |
|
|
1120
217
|
| `optionStyles` | none | `OptionStyles` | Tone, icon and label by `type.field` and option, for `colorBy` |
|
|
1121
218
|
| `optionColors` | none | `OptionColors` | The same, when a tone is all an option has. Read as `optionStyles` |
|
|
1122
|
-
| `labels` | English | `Labels` | The words the screens print for a visitor. See
|
|
1123
|
-
| `store` | in process | operator only | Where the state a fleet has to agree on is kept: kept answers, the host map, the replay guard, the generation of each cache tag. Needed only when more than one container serves the site. See
|
|
1124
|
-
| `home` | the post index | `HomePath`, `HomeCollection` | What `createHome` serves at `/`. See
|
|
1125
|
-
| `theme` | the barakoCMS palette | `Colors`, `Fonts`, `Radii`, `Layout`, `Space`, `Text`, `Tokens`, `Tones`, `StyleRecipes` | Colours, faces, radii, column widths, a site's own named values and tones, and the looks its blocks can name. See
|
|
219
|
+
| `labels` | English | `Labels` | The words the screens print for a visitor. See [Sites](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md) |
|
|
220
|
+
| `store` | in process | operator only | Where the state a fleet has to agree on is kept: kept answers, the host map, the replay guard, the generation of each cache tag. Needed only when more than one container serves the site. See [Running more than one container](https://github.com/BaryoDev/barakoPress/blob/master/docs/caching.md#running-more-than-one-container) |
|
|
221
|
+
| `home` | the post index | `HomePath`, `HomeCollection` | What `createHome` serves at `/`. See [Sites](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md) |
|
|
222
|
+
| `theme` | the barakoCMS palette | `Colors`, `Fonts`, `Radii`, `Layout`, `Space`, `Text`, `Tokens`, `Tones`, `StyleRecipes` | Colours, faces, radii, column widths, a site's own named values and tones, and the looks its blocks can name. See [The look](https://github.com/BaryoDev/barakoPress/blob/master/docs/look.md) |
|
|
1126
223
|
|
|
1127
224
|
The third column is the whole of the split. A key marked operator only is one the image decides for
|
|
1128
225
|
every tenant it serves, and each is that for a reason you can name: `types`, `fields`, `pageFields`
|
|
@@ -1145,664 +242,28 @@ per request, so when a value was read depended on which value it was.
|
|
|
1145
242
|
| `CMS_URL` | Where the delivery API is. `cmsUrl` in the config wins |
|
|
1146
243
|
| `CMS_TENANT` | Pins the process to one tenant. `tenant` in the config wins |
|
|
1147
244
|
| `CMS_DEFAULT_TENANT` | The tenant for a host the CMS does not know. `sites.defaultTenant` wins |
|
|
1148
|
-
| `CMS_RENDERER_KEY` | Sent to the CMS when a share link is redeemed. See [Share links](#one-build-many-sites) |
|
|
245
|
+
| `CMS_RENDERER_KEY` | Sent to the CMS as `X-Barako-Renderer-Key` on every delivery read when the CMS URL is https or loopback, and when a share link is redeemed, so the CMS rate limits by renderer. See [Share links](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md#one-build-many-sites) |
|
|
1149
246
|
| `PRESS_CONSOLE_ORIGINS` | Browser origins allowed to read the block schema, comma separated |
|
|
1150
|
-
| `PRESS_SECRET` | The HMAC key for everything this renderer signs. See [One secret](#one-secret) |
|
|
247
|
+
| `PRESS_SECRET` | The HMAC key for everything this renderer signs. See [One secret](https://github.com/BaryoDev/barakoPress/blob/master/docs/caching.md#one-secret) |
|
|
1151
248
|
| `REVALIDATE_SECRET` | The webhook key before `PRESS_SECRET`, read only while that is unset |
|
|
1152
249
|
| `PRESS_PREVIEW_SECRET` | The share key before `PRESS_SECRET`, read only while that is unset |
|
|
250
|
+
| `PRESS_FONT_ORIGINS` | The origins a font stylesheet may be linked from. Unset, Google Fonts only. See [One build, many sites](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md#one-build-many-sites) |
|
|
251
|
+
| `PRESS_SCRIPT_ORIGINS` | The origins a site's Umami script (`AnalyticsScript`) may be loaded from. Unset, none, so no script is rendered. Read when a page renders, so a cached page keeps its tag until it renders again. See [One build, many sites](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md#one-build-many-sites) |
|
|
1153
252
|
|
|
1154
253
|
Values are used exactly as the environment has them, untrimmed. A secret with a trailing space is a
|
|
1155
254
|
different HMAC key, so trimming one here would stop a webhook that verifies today.
|
|
1156
255
|
|
|
256
|
+
The reference app's `Dockerfile` also takes two build arguments, read when the image is built and
|
|
257
|
+
not by the package: `PRESS_DEPLOY` names a directory under `deploy/` to lay over the reference app,
|
|
258
|
+
and `PRESS_TRAILING_SLASH=true` sets Next's `trailingSlash` in `next.config.ts`. See
|
|
259
|
+
[The reference deployment](https://github.com/BaryoDev/barakoPress/blob/master/docs/deploying.md#the-reference-deployment-in-this-repository).
|
|
260
|
+
|
|
1157
261
|
**A site is build time or request time.** Without `sites`, identity is build time: the index, the
|
|
1158
262
|
feed, the sitemap and robots are prerendered, so anything *your own* `press.config.ts` reads from the
|
|
1159
263
|
environment is baked when you build, not when the server starts. Write per-site values as literals in
|
|
1160
264
|
that file. Getting this wrong is how a client site ships with the vendor's name in its masthead. With
|
|
1161
|
-
`sites`, identity is data in the CMS and read per request
|
|
1162
|
-
|
|
1163
|
-
### One build, many sites
|
|
1164
|
-
|
|
1165
|
-
barakoCMS D22 reverses "identity is build time" for the shared renderer: every site runs the same
|
|
1166
|
-
image, and what makes a site that site is its tenant's `site` settings entry, edited in barakoBrew.
|
|
1167
|
-
|
|
1168
|
-
```ts
|
|
1169
|
-
export const config = defineConfig({ sites: {} });
|
|
1170
|
-
```
|
|
1171
|
-
|
|
1172
|
-
A request-time site puts its page routes under one tenant segment and adds a `proxy.ts` beside
|
|
1173
|
-
`app/`:
|
|
1174
|
-
|
|
1175
|
-
```
|
|
1176
|
-
proxy.ts export default createPressProxy(config)
|
|
1177
|
-
app/
|
|
1178
|
-
%5Fpress/[site]/ the tenant segment. %5F is `_`, which Next reads as a
|
|
1179
|
-
layout.tsx private folder when it is written plainly
|
|
1180
|
-
page.tsx
|
|
1181
|
-
[...path]/page.tsx
|
|
1182
|
-
blog/[slug]/page.tsx
|
|
1183
|
-
feed.xml/route.ts these resolve their own tenant and stay where they are
|
|
1184
|
-
robots.ts
|
|
1185
|
-
sitemap.ts
|
|
1186
|
-
api/...
|
|
1187
|
-
%5Fshare/route.ts
|
|
1188
|
-
```
|
|
1189
|
-
|
|
1190
|
-
```ts
|
|
1191
|
-
// proxy.ts
|
|
1192
|
-
import { createPressProxy } from "barakopress";
|
|
1193
|
-
import { config } from "@/press.config";
|
|
1194
|
-
|
|
1195
|
-
export default createPressProxy(config);
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
|
-
The proxy resolves the tenant and the share session once and rewrites to
|
|
1199
|
-
`/_press/<tenant>~<gate>~<host>/<path>`. The pages read all three out of that segment, so nothing in
|
|
1200
|
-
a page reads a header or a cookie, and Next can keep the render. Why the tenant has to be in the
|
|
1201
|
-
path, and what is cached and what is not, is [The render cache](#the-render-cache) below. A build-time
|
|
1202
|
-
site needs none of this and keeps its routes where they are.
|
|
1203
|
-
|
|
1204
|
-
On each request the engine resolves the tenant, reads that tenant's settings, and renders with them:
|
|
1205
|
-
|
|
1206
|
-
1. `CMS_TENANT` (or `tenant`) set: every host is that tenant, with no lookup.
|
|
1207
|
-
2. `sites.tenantHeader` set and the request carries a valid handle in it: that tenant. Off by default.
|
|
1208
|
-
3. The host, from `sites.hostHeader` (default `host`), looked up with `GET /api/tenants/by-host/{host}`.
|
|
1209
|
-
4. `sites.defaultTenant`, or `CMS_DEFAULT_TENANT`, read at request time.
|
|
1210
|
-
5. None of those: a 404. Never another tenant's site.
|
|
1211
|
-
|
|
1212
|
-
A handle is only ever read from the request through a header the operator named. `X-Tenant` and
|
|
1213
|
-
`X-Forwarded-Host` sent by a caller are ignored unless you configure them, and you should configure
|
|
1214
|
-
them only behind a proxy that sets the header and strips a caller's value.
|
|
1215
|
-
|
|
1216
|
-
The settings are the singleton `site` type from barakoCMS `docs/site-settings.md`
|
|
1217
|
-
(`POST /api/content-types/blueprints/site`, then publish its one entry). The engine reads `Name`,
|
|
1218
|
-
`Tagline`, `Url`, `Locale`, `Logo`, `LogoAlt`, `FooterLogo`, `Favicon`, `ShareImage`, `Copyright`,
|
|
1219
|
-
`Colors` (the theme slots), `Fonts` (a family name per role, and the stylesheet that loads it),
|
|
1220
|
-
`Radii`, `Layout`, `Tokens` and `Tones` (see [Tokens and tones](#tokens-and-tones)), `StyleRecipes` (see [Style recipes](#style-recipes)), `TopBar`, `HeaderLinks`, `MenuLinks`, `HeaderActions`, `FooterColumns`, `SocialLinks`, `HeaderPath`,
|
|
1221
|
-
`HeaderTone`, `FooterPath`, `FooterTone`, `AssetsAsSupplied`, `LogoAsSupplied`, `LogoClearSpace`,
|
|
1222
|
-
`PageSizes`, `ReservedSlugs`, `Labels`, `HomePath` and `HomeCollection`. `Collections`, `OptionStyles` and `OptionColors` are read as the collections section
|
|
1223
|
-
describes. `Variants` are not rendered yet. Every value is checked for shape; one that fails, and any the
|
|
1224
|
-
entry leaves out, keeps the configured value, so a half-filled theme renders. A link is a path on the
|
|
1225
|
-
site or an absolute http or https URL. Set `Url`: without it the feed and sitemap fall back to the
|
|
1226
|
-
host the tenant was found by. A link in `HeaderLinks`, `TopBar` or `FooterColumns` may also carry
|
|
1227
|
-
`badge`, a marker of up to 12 characters drawn beside the label, and `external: true`, which the
|
|
1228
|
-
built-in header does not need (every header link is already a plain anchor) and a theme can read.
|
|
1229
|
-
A link in `HeaderLinks` or `MenuLinks` may also carry `activeOn` and `children`; see
|
|
1230
|
-
[The built-in header](#the-built-in-header).
|
|
1231
|
-
|
|
1232
|
-
`createSiteLayout` and `createSiteMetadata` render the root layout from all of this: `lang`, the
|
|
1233
|
-
faces, the palette, the top bar, header links, footer columns, social links and the copyright line.
|
|
1234
|
-
|
|
1235
|
-
**What the site serves at `/`.** Every site used to be a blog at the root, because the root route
|
|
1236
|
-
mounted the post index and nothing else could be named:
|
|
1237
|
-
|
|
1238
|
-
| Field | Type | What |
|
|
1239
|
-
| --- | --- | --- |
|
|
1240
|
-
| `HomePath` | string | A site path such as `/home`. The page the Pages module serves there is the home page. Nothing served there falls back to the index, so naming a page before writing it is safe |
|
|
1241
|
-
| `HomeCollection` | string | The key of a collection. Its index is the home page. `HomePath` wins when both are set |
|
|
1242
|
-
|
|
1243
|
-
Neither set, `/` is the post index, which is what it was. The blog's own `post`, `author` and
|
|
1244
|
-
`category` are ordinary `Collections` entries now, so a school whose news lives in `article` with a
|
|
1245
|
-
`Headline` replaces `post` in its settings and gets both its list and its item pages from that entry.
|
|
1246
|
-
The RSS link in the built-in header, and the feed alternate in the page metadata, appear only when
|
|
1247
|
-
some collection has `feed` on, so a clinic with no posts stops advertising an empty feed.
|
|
1248
|
-
|
|
1249
|
-
**The words a visitor reads.** `Labels` is the visitor-facing copy, key by key. A school setting
|
|
1250
|
-
`Locale` to `fil-PH` used to get Filipino dates beside English "min read" and "Related":
|
|
1251
|
-
|
|
1252
|
-
```json
|
|
1253
|
-
{ "Labels": { "minRead": "minutong pagbasa", "by": "ni", "related": "Kaugnay" } }
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
| Key | English |
|
|
1257
|
-
| --- | --- |
|
|
1258
|
-
| `minRead` | `min read` |
|
|
1259
|
-
| `by` | `by` |
|
|
1260
|
-
| `related` | `Related` |
|
|
1261
|
-
| `relatedNote` | `cosine similarity, computed on load, not curated` |
|
|
1262
|
-
| `featured` | `Featured` |
|
|
1263
|
-
| `back` | `Back` |
|
|
1264
|
-
| `home` | `Home` |
|
|
1265
|
-
| `preview` | The banner over a draft being previewed |
|
|
1266
|
-
| `untitled` | `Untitled` |
|
|
1267
|
-
| `feed` | `RSS` |
|
|
1268
|
-
| `empty`, `emptyNote` | The notice on an index with nothing published |
|
|
1269
|
-
| `failed`, `failedNote` | The notice on an index whose read failed |
|
|
1270
|
-
| `shareInvalid` | `This link is not valid or has expired.` |
|
|
1271
|
-
| `openMenu`, `closeMenu` | `Open menu`, `Close menu`: the header's phone menu button |
|
|
1272
|
-
| `menu` | `Menu`: the name of the phone menu's links |
|
|
1273
|
-
| `submenu` | `{label} links`: the button beside a header link with children, `{label}` its label |
|
|
1274
|
-
| `onThisPage` | `On this page`: the heading over a tree page's rail |
|
|
1275
|
-
| `closeContents` | `Close`: the closed phone disclosure's control while it is open |
|
|
1276
|
-
|
|
1277
|
-
A key left out, or saved as anything but a word, keeps the English, so a half-filled map reads. A
|
|
1278
|
-
build-time site passes `labels` to `defineConfig`. Nothing about a site's own content is here: a
|
|
1279
|
-
collection's heading is its `label` and how its count reads is its `noun`.
|
|
1280
|
-
|
|
1281
|
-
**The colour slots, by role.** `Colors` sets any slot of the palette, one at a time. The slots are
|
|
1282
|
-
`pageBg`, `surface`, `ink`, `proseInk`, `secondaryInk`, `muted`, `hairline`, `accent`, `accentHover`,
|
|
1283
|
-
`accentInk`, `accentTint`, `accentTintBorder`, `accentBorderStrong`, `inverse`, `inverseChrome`,
|
|
1284
|
-
`inverseInk`, `inverseAccent`, `code` and `success`. A role says where a colour goes rather than what
|
|
1285
|
-
it looks like: `inverse` is the band that reverses the page, the footer and a code panel and an
|
|
1286
|
-
inverse block, so a bakery with a cream footer sets `inverse` to cream and reads right doing it.
|
|
1287
|
-
|
|
1288
|
-
```json
|
|
1289
|
-
{ "Colors": { "accent": "#17458F", "inverse": "#F4E3C1", "inverseInk": "#3B2A17" } }
|
|
1290
|
-
```
|
|
1291
|
-
|
|
1292
|
-
Six slots shipped in 0.3.0 under barakocms.com's own names, and those still work: `darkPanel`,
|
|
1293
|
-
`darkPanelChrome`, `darkPanelInk`, `darkPanelAccent`, `codeGreen` and `accentTintBorderStrong` are
|
|
1294
|
-
read into `inverse`, `inverseChrome`, `inverseInk`, `inverseAccent`, `code` and `accentBorderStrong`.
|
|
1295
|
-
Setting either name sets both, so a tenant saved before the rename keeps its site and a consumer's
|
|
1296
|
-
own component reading `theme.colors.darkPanel` keeps compiling. The old names are deprecated and go
|
|
1297
|
-
in 2.0.0. An `OptionStyles` or `OptionColors` entry naming an old slot resolves too.
|
|
1298
|
-
|
|
1299
|
-
Sizes work the same way. `Text` is the type scale by role, `meta`, `small`, `body`, `lead`,
|
|
1300
|
-
`subheading`, `heading`, `title`, `display` and `pageTitle`, and `Space` is the spacing scale. The
|
|
1301
|
-
blocks and the screens read those names, so a tenant that wants bigger headings sets `title` once
|
|
1302
|
-
instead of asking for a release.
|
|
1303
|
-
|
|
1304
|
-
**How many items, per tenant.** `PageSizes` sets the four counts for this site, each on its own:
|
|
1305
|
-
|
|
1306
|
-
```json
|
|
1307
|
-
{ "PageSizes": { "index": 50 } }
|
|
1308
|
-
```
|
|
1309
|
-
|
|
1310
|
-
A bakery listing 50 products and an agency listing 9 case studies run the same image. A key the
|
|
1311
|
-
entry leaves out keeps the configured count, and a collection's own `pageSize` still wins over the
|
|
1312
|
-
site's. barakoCMS clamps a public list at 100 whatever is asked for, so a larger number is not an
|
|
1313
|
-
error and does not buy more rows.
|
|
1314
|
-
|
|
1315
|
-
**Paths this site does not serve.** `ReservedSlugs` adds to the configured reserved list:
|
|
1316
|
-
|
|
1317
|
-
```json
|
|
1318
|
-
{ "ReservedSlugs": ["shop", "status"] }
|
|
1319
|
-
```
|
|
1320
|
-
|
|
1321
|
-
A first segment on it is left out of the menu and the sitemap, and a page there is never asked for.
|
|
1322
|
-
It is what a proxy in front of this domain answers instead of the renderer, which is a fact about
|
|
1323
|
-
one tenant rather than about the image. It only ever adds: a tenant cannot free a segment the app's
|
|
1324
|
-
own routes already hold, because a page there would sit behind a route file and render nowhere. A
|
|
1325
|
-
tenant's collection routes need no entry, since those are read off its settings already.
|
|
1326
|
-
|
|
1327
|
-
**Fonts, from an allow list.** A family name on its own is loaded from Google Fonts, which is what it
|
|
1328
|
-
has always meant:
|
|
1329
|
-
|
|
1330
|
-
```json
|
|
1331
|
-
{ "Fonts": { "heading": "Zilla Slab" } }
|
|
1332
|
-
```
|
|
1333
|
-
|
|
1334
|
-
A site that cannot use Google Fonts, a school with a licensed face on its own host or a tenant that
|
|
1335
|
-
must not send visitor addresses to a third party, names the stylesheet instead:
|
|
1336
|
-
|
|
1337
|
-
```json
|
|
1338
|
-
{ "Fonts": { "heading": { "family": "Zilla Slab", "url": "https://type.school.example/zilla.css" } } }
|
|
1339
|
-
```
|
|
1340
|
-
|
|
1341
|
-
That URL is a tenant's setting on its way into a `<link>` in every visitor's page, so which origins a
|
|
1342
|
-
page may reach is the deployment's decision and not the tenant's. `PRESS_FONT_ORIGINS` is the list:
|
|
1343
|
-
origins separated by commas or spaces, each `https://host`, a bare host read as https.
|
|
1344
|
-
|
|
1345
|
-
| `PRESS_FONT_ORIGINS` | What a page may link |
|
|
1346
|
-
| --- | --- |
|
|
1347
|
-
| unset or blank | Google Fonts, and nothing else. This is what every site rendered before the list existed |
|
|
1348
|
-
| `https://type.school.example` | That origin only. Nothing goes to Google Fonts, the built-in link and its preconnects included |
|
|
1349
|
-
| `fonts.googleapis.com, type.school.example` | Both |
|
|
1350
|
-
|
|
1351
|
-
A URL on any other origin is refused: no link to it is rendered, the role falls back to its family
|
|
1352
|
-
name, and the server log says so once rather than once a page view. The list replaces the default
|
|
1353
|
-
rather than adding to it, which is what gives a tenant that must not reach Google Fonts a deployment
|
|
1354
|
-
where nothing can. `createSiteLayout(config, { loadFonts: false })` still turns off every font link
|
|
1355
|
-
for the whole image.
|
|
1356
|
-
|
|
1357
|
-
Only an absolute https URL is kept, for the whole chain: http is blocked as mixed content on every
|
|
1358
|
-
site this serves, so allowing it would mean rendering a link that never loads. A role the tenant names
|
|
1359
|
-
is the tenant's, family and stylesheet together, so a family set with no url clears a configured
|
|
1360
|
-
stylesheet rather than leaving the page loading a face it no longer uses. A build-time site sets the
|
|
1361
|
-
same thing in `theme.fontSources`, and it is held to the same list.
|
|
1362
|
-
|
|
1363
|
-
**Header and footer as block regions.** The built-in header and footer take links and text and
|
|
1364
|
-
nothing else, so a clinic that wants a light footer with opening hours and a map cannot have one, and
|
|
1365
|
-
a school that wants an enrolment banner with a button cannot either. Point a region at a page and its
|
|
1366
|
-
blocks are drawn there instead, resolved and bound exactly as the page route resolves and binds them.
|
|
1367
|
-
|
|
1368
|
-
| Field | Type | What |
|
|
1369
|
-
| --- | --- | --- |
|
|
1370
|
-
| `HeaderPath` | string | A site path such as `/site/header`. The page served there is drawn in place of the top bar and the header band |
|
|
1371
|
-
| `HeaderTone` | string | `page`, `surface`, `accent`, `inverse`, `gradient`, `wash`, or a name from `Tones`: the tone behind the header region. `page` when unset or not one of those |
|
|
1372
|
-
| `FooterPath` | string | A site path such as `/site/footer`. The page served there is drawn in place of the footer |
|
|
1373
|
-
| `FooterTone` | string | The same names, behind the footer region |
|
|
1374
|
-
|
|
1375
|
-
Set neither and nothing changes: `TopBar`, `HeaderLinks`, `FooterColumns`, `SocialLinks` and
|
|
1376
|
-
`Copyright` draw the built-in chrome with the markup they always had, which is what keeps a site
|
|
1377
|
-
whose own CSS keys off that markup rendering. A path with nothing served at it does the same, so
|
|
1378
|
-
naming a page before writing it is safe, and so is a typo.
|
|
1379
|
-
|
|
1380
|
-
### The built-in header
|
|
1381
|
-
|
|
1382
|
-
With no `HeaderPath`, the header is the logo, the page menu, `HeaderLinks` and the feed link. Four
|
|
1383
|
-
more settings shape it:
|
|
1384
|
-
|
|
1385
|
-
| Setting | What |
|
|
1386
|
-
| --- | --- |
|
|
1387
|
-
| `activeOn` on a link | Space separated site paths the link is current on. `/` is the home page alone; any other path covers itself and everything below it, so `/docs` is current on `/docs/intro`. A trailing slash is ignored. The current link gets `aria-current="page"` and the class `bp-current`. It can name a path the link does not point to: a chat server's link can be current on `/community` |
|
|
1388
|
-
| `children` on a link | Links under this one, one level deep, each with its own `label`, `href`, `badge`, `external` and `activeOn`. Drawn as a dropdown that opens on hover and on keyboard focus and closes on Escape and when focus leaves, beside a button with `aria-expanded` that opens it too. A child's `activeOn` marks its parent current as well. The parent keeps its own `href` |
|
|
1389
|
-
| `MenuLinks` | The rows of the phone menu, in the shape of `HeaderLinks`. Children are drawn as an indented group. Unset or empty, the phone menu shows `HeaderLinks` |
|
|
1390
|
-
| `HeaderActions` | Up to four call to action links after the header links, each `{ label, href, variant }` with `badge` and `external` as on any link. `variant` is `primary` (filled with the accent), `secondary` (outlined) or `plain`; unset or unknown is `primary` |
|
|
1391
|
-
|
|
1392
|
-
Below 48rem, a site that uses any of these gets a menu button in place of the header links and the
|
|
1393
|
-
actions, opening a sheet with the `MenuLinks` rows, the feed link and the actions. The button is a
|
|
1394
|
-
`details` element and every link is in the server's HTML, so the menu and the dropdowns work
|
|
1395
|
-
without JavaScript; the script adds Escape and keeps `aria-expanded` true to what is shown. The
|
|
1396
|
-
words on the controls are `openMenu`, `closeMenu`, `menu` and `submenu` in `Labels` (`submenu` is
|
|
1397
|
-
`{label} links`, with the parent's label in place of `{label}`).
|
|
1398
|
-
|
|
1399
|
-
A site that sets none of them gets the header it had before, byte for byte.
|
|
1400
|
-
|
|
1401
|
-
A region page is chrome rather than somewhere to go, so it is left out of the menu and the sitemap,
|
|
1402
|
-
and it answers 404 at its own route: what it holds is already on every page. A collection's
|
|
1403
|
-
`indexPage` is treated the same way.
|
|
1404
|
-
|
|
1405
|
-
A header region replaces the whole band, the site name, the menu and the RSS link along with the top
|
|
1406
|
-
bar. There is no navigation or logo block yet, so a header region lists its own links until the block
|
|
1407
|
-
library has one.
|
|
1408
|
-
|
|
1409
|
-
A build-time site sets the same thing in its config, and a tenant's settings win over it:
|
|
1410
|
-
|
|
1411
|
-
```ts
|
|
1412
|
-
export const config = defineConfig({
|
|
1413
|
-
site: { name: "Mabini Clinic", url: "https://clinic.example" },
|
|
1414
|
-
pages: "",
|
|
1415
|
-
regions: { footer: { path: "/site/footer", tone: "surface" } },
|
|
1416
|
-
});
|
|
1417
|
-
```
|
|
1418
|
-
|
|
1419
|
-
The registry the regions render with is the one passed to `createSiteLayout(config, { blocks })`, and
|
|
1420
|
-
the built-in blocks when none was passed.
|
|
1421
|
-
|
|
1422
|
-
**Caching per tenant.** Every read carries the tenant in `X-Tenant` and in its cache tag,
|
|
1423
|
-
`<cacheTag>:<tenant>`, and a narrower tag beside it: `<tenant tag>:entry:<type>:<slug>` on a read of
|
|
1424
|
-
one entry, `<tenant tag>:type:<type>` on a list, a search, the settings and the page tree. The webhook
|
|
1425
|
-
purges the tags of the tenant its host resolves to, so point each tenant's webhook at
|
|
1426
|
-
`https://<that tenant's domain>/api/revalidate`, signed with that tenant's own key (see
|
|
1427
|
-
[One key per tenant](#one-key-per-tenant)). A publish on one tenant leaves every other tenant's cached
|
|
1428
|
-
reads in place.
|
|
1429
|
-
|
|
1430
|
-
**When the CMS is down.** Each successful read is also kept in the store, keyed by CMS, tenant and path.
|
|
1431
|
-
A read that fails with a network error, a 5xx, or no answer within `cmsTimeoutMs` answers from the
|
|
1432
|
-
last good copy and logs a warning; the next successful read replaces it. For ten seconds after such a
|
|
1433
|
-
failure that read answers from the copy without asking the CMS, so an outage costs one request per
|
|
1434
|
-
read every ten seconds rather than one per visitor. The same holds after a purge: a publish that lands
|
|
1435
|
-
within those ten seconds shows once the ten seconds are up. Known hosts keep resolving the same way. A page that was cached
|
|
1436
|
-
for a tenant keeps answering 200 with that tenant's identity and theme. A tenant never gets another
|
|
1437
|
-
tenant's kept answer.
|
|
1438
|
-
|
|
1439
|
-
**Holding mode.** A tenant can show a holding page on every route in place of its site, for a launch,
|
|
1440
|
-
maintenance or a seasonal break, and share the real site with a few people through site share links.
|
|
1441
|
-
Three fields on the `site` entry control it:
|
|
1442
|
-
|
|
1443
|
-
| Field | Type | What |
|
|
1444
|
-
| --- | --- | --- |
|
|
1445
|
-
| `Mode` | string | `Live` or `Holding`. Unset, or anything else, is `Live` |
|
|
1446
|
-
| `HoldingPath` | string | A site path such as `/coming-soon`. The page the Pages module serves there is the holding page. Empty, or nothing served there, renders the default holding page: the name, the tagline and `HoldingMessage`, in the theme |
|
|
1447
|
-
| `HoldingMessage` | string | The line the default holding page shows under the name and tagline, as text, for example `Closed until 6 January.` Unset or blank shows no line. Read only while holding. For more than a sentence, use `HoldingPath` |
|
|
1448
|
-
|
|
1449
|
-
Holding is presentation, not access control: the API still delivers every published entry. Content
|
|
1450
|
-
that must stay hidden before launch stays unpublished with a scheduled publish.
|
|
1451
|
-
|
|
1452
|
-
While a tenant is holding, for that tenant only:
|
|
1453
|
-
|
|
1454
|
-
- every page answers the holding page with `noindex` and `Cache-Control: private, no-store`. A page
|
|
1455
|
-
stops in `siteConfig` with a not-found before it reads anything, so its content and its title never
|
|
1456
|
-
reach the response. Next has already sent the status by then, so it is 200, the same for a path
|
|
1457
|
-
that exists and one that does not;
|
|
1458
|
-
- the holding page is read with `GET /api/public/pages/resolve?path=<HoldingPath>` and rendered with
|
|
1459
|
-
the page renderer and the registry passed to `createSiteLayout(config, { blocks })`;
|
|
1460
|
-
- a link to `HoldingPath` is left out of the header, top bar and footer. `feed.xml` and `sitemap.xml`
|
|
1461
|
-
are 404 for everyone, session or not, and `robots.txt` answers `Disallow: /` with no sitemap line;
|
|
1462
|
-
- route handlers (revalidate, blocks, `/_share`, `/api/share/redeem`) and `/_next/static` are served
|
|
1463
|
-
as usual.
|
|
1464
|
-
|
|
1465
|
-
Switching `Mode` is a publish: the webhook drops the tag the settings read carries and the next
|
|
1466
|
-
request reads the new settings. No deploy.
|
|
1467
|
-
|
|
1468
|
-
**Site share links.** barakoCMS creates, lists and revokes them; anyone who may update the `site` type
|
|
1469
|
-
can. A client is given `{site Url}/_share#{key}`. The key is in the fragment, so no server log, proxy
|
|
1470
|
-
or referrer sees it:
|
|
1471
|
-
|
|
1472
|
-
1. `/_share` is a tiny page. Its script reads the fragment, removes it from the address and history,
|
|
1473
|
-
and posts the key in a form to `/api/share/redeem`. Without JavaScript it says the link needs
|
|
1474
|
-
JavaScript. No key is ever accepted in a query string.
|
|
1475
|
-
2. `/api/share/redeem` asks barakoCMS once, `POST /api/public/site/share-links/redeem` with the key in
|
|
1476
|
-
the body and the tenant header. On 200 it sets `__Host-press-share` (HttpOnly, Secure,
|
|
1477
|
-
SameSite=Lax, Path=/, no Domain, so only that host gets it) and answers a `no-store` 303 to `/`.
|
|
1478
|
-
On 404, 429 or any failure it sets nothing and sends the visitor to `/#share-invalid`, the holding
|
|
1479
|
-
page with "This link is not valid or has expired." A post whose `Origin` or `Sec-Fetch-Site` names
|
|
1480
|
-
another site is refused the same way. The key is never logged.
|
|
1481
|
-
3. With a valid cookie that visitor sees the real site for that tenant. A forged, expired or
|
|
1482
|
-
other-tenant cookie is ignored.
|
|
1483
|
-
|
|
1484
|
-
The cookie holds no key. It is an expiry and an HMAC-SHA256 over the tenant and that expiry, so each
|
|
1485
|
-
later request is checked in process without asking the CMS, and a cookie made for one tenant opens no
|
|
1486
|
-
other. It needs one setting:
|
|
1487
|
-
|
|
1488
|
-
| Variable | What |
|
|
1489
|
-
| --- | --- |
|
|
1490
|
-
| `PRESS_SECRET` | The HMAC key, at least 32 characters, for example `openssl rand -base64 48`. Read per request. Unset or shorter, no session is issued or accepted and everyone gets the holding page. Every instance behind one domain needs the same value. `PRESS_PREVIEW_SECRET` is read in its place when `PRESS_SECRET` is unset. See [One secret](#one-secret) |
|
|
1491
|
-
| `CMS_RENDERER_KEY` | Optional. Sent to barakoCMS as `X-Barako-Renderer-Key` when a link is redeemed, and must match the renderer key barakoCMS is configured with. Read per request and never logged. Unset, no key header is sent |
|
|
1492
|
-
|
|
1493
|
-
**Redemption is rate limited per tenant and visitor.** Every redemption leaves this container from the
|
|
1494
|
-
same address, so barakoCMS needs the visitor's address to tell visitors apart. Name the header a
|
|
1495
|
-
proxy in front sets it in, `sites: { visitorIpHeader: "x-real-ip" }`, and it is sent on as
|
|
1496
|
-
`X-Barako-Visitor-IP`. Name it only behind a proxy that sets that header and strips a caller's value,
|
|
1497
|
-
the same rule as `hostHeader` and `tenantHeader`. A value that is not exactly one IPv4 or IPv6
|
|
1498
|
-
address, a comma separated list included, is not sent. barakoCMS trusts the address only when
|
|
1499
|
-
`CMS_RENDERER_KEY` matches, so both are needed for a per visitor limit. With no header named nothing
|
|
1500
|
-
is sent: a Next route handler never sees the socket's address, and the `X-Forwarded-For` Next adds
|
|
1501
|
-
keeps whatever a caller put there.
|
|
1502
|
-
|
|
1503
|
-
**A session lasts until the link expires or for 24 hours, whichever is sooner.** Opening the link
|
|
1504
|
-
again starts a new one while the link is valid. **A revoked link can keep working for up to 24
|
|
1505
|
-
hours** for someone who already opened it, because the session is checked here, not in barakoCMS. To
|
|
1506
|
-
end every session now, change `PRESS_SECRET`, which does it for every tenant on that
|
|
1507
|
-
deployment, and changes every tenant's webhook key with it.
|
|
1508
|
-
|
|
1509
|
-
Whether a request gets the holding page is decided by the proxy, from the cookie, once per request,
|
|
1510
|
-
and the answer is a segment of the path the render is kept under. A render made for a visitor with a
|
|
1511
|
-
session is never served to one without, or the reverse, because the two are different paths. A page
|
|
1512
|
-
you write by hand must call `siteConfig(config, params)` before it reads or renders anything, which
|
|
1513
|
-
is what keeps it behind the holding page and what gives it its tenant. A build-time site has no
|
|
1514
|
-
settings entry and no holding mode.
|
|
1515
|
-
|
|
1516
|
-
`generateStaticParams` factories return nothing on a request-time site, since there is no tenant at
|
|
1517
|
-
build. `scripts/two-hosts.sh` runs the built reference app against a stand-in CMS with two tenants
|
|
1518
|
-
and checks all of the above; CI runs it on every push.
|
|
1519
|
-
|
|
1520
|
-
### The look is configuration too
|
|
1521
|
-
|
|
1522
|
-
The post screen styles itself from `config.theme`, inline, so it renders correctly whether or not you
|
|
1523
|
-
import `barakopress/styles.css`. That import is optional and a screen that needs it renders unstyled
|
|
1524
|
-
for anyone who skips it, which is exactly what happened: barakocms.com never imported it, got an
|
|
1525
|
-
article at x=0 on a blank page, and stopped using the screen at all.
|
|
1526
|
-
|
|
1527
|
-
```ts
|
|
1528
|
-
export const config = defineConfig({
|
|
1529
|
-
site: { name: "Client Three", url: "https://clientthree.example" },
|
|
1530
|
-
theme: { colors: { accent: "#008060" }, layout: { prose: "68ch" } },
|
|
1531
|
-
});
|
|
1532
|
-
```
|
|
1533
|
-
|
|
1534
|
-
Each group merges over the defaults on its own, so setting one colour keeps the other seventeen. The
|
|
1535
|
-
groups are `colors` (18 values), `fonts` (`heading`, `body`, `mono`), `radii` (`panel`, `control`,
|
|
1536
|
-
`pill`) and `layout` (`prose`, `wide`, `gutter`). `DEFAULT_THEME` is exported if you want to read the
|
|
1537
|
-
values or build a palette from them.
|
|
1538
|
-
|
|
1539
|
-
Loading the faces is the site's job, not the engine's. The default theme names Sora, Manrope and
|
|
1540
|
-
JetBrains Mono with real fallback stacks, and a `<link>` in your root layout is what makes them
|
|
1541
|
-
arrive. `createSiteLayout` writes that link for you: a family name from Google Fonts, or the
|
|
1542
|
-
stylesheet named in `theme.fontSources` when the face is not loaded from there, held to
|
|
1543
|
-
`PRESS_FONT_ORIGINS` either way.
|
|
1544
|
-
|
|
1545
|
-
**Three slots**, for what belongs to the site rather than the engine:
|
|
1546
|
-
|
|
1547
|
-
```tsx
|
|
1548
|
-
<PostView
|
|
1549
|
-
config={config}
|
|
1550
|
-
post={post}
|
|
1551
|
-
headerBackdrop={<Bean />} {/* decoration behind the header band */}
|
|
1552
|
-
beforeBody={<RoleStrip />} {/* a wide band under the header */}
|
|
1553
|
-
afterBody={<Newsletter />} {/* the foot of the reading column */}
|
|
1554
|
-
/>
|
|
1555
|
-
```
|
|
1556
|
-
|
|
1557
|
-
They are not a way to compose a post out of arbitrary sections. Anything that has to sit between two
|
|
1558
|
-
paragraphs belongs to the block model, which is issue #6, because only the body knows where it goes.
|
|
1559
|
-
|
|
1560
|
-
**A theme that draws the page itself.** A deployment that ports an existing design registers its own
|
|
1561
|
-
blocks under the built-in type names and ships its own stylesheet. Two things would still be the
|
|
1562
|
-
engine's, and neither can be undone from a stylesheet because both are inline: the page frame
|
|
1563
|
-
(`main`, its padding, the title and the breadcrumbs) and the gap between blocks. So:
|
|
1564
|
-
|
|
1565
|
-
```tsx
|
|
1566
|
-
export default createPage(config, blocks, { bare: true }); // only the blocks
|
|
1567
|
-
```
|
|
1568
|
-
|
|
1569
|
-
```css
|
|
1570
|
-
body { --bp-gap: 0; } /* every block list, at every level */
|
|
1571
|
-
```
|
|
1572
|
-
|
|
1573
|
-
`bare` is also taken by `createHome` and `createViewerPage`. `--bp-gap` falls back to
|
|
1574
|
-
`theme.space.lg`, so a site that sets neither renders as it did.
|
|
1575
|
-
|
|
1576
|
-
### Tokens and tones
|
|
1577
|
-
|
|
1578
|
-
The theme's slots are the engine's roles. A design has its own palette on top of them: a colour per
|
|
1579
|
-
product, a tint for a badge, a length the design repeats. Those are `Tokens` and `Tones` in the site
|
|
1580
|
-
settings, or `theme.tokens` and `theme.tones` in `defineConfig`. A tenant's entries merge over the
|
|
1581
|
-
configured ones name by name.
|
|
1582
|
-
|
|
1583
|
-
```json
|
|
1584
|
-
"Tokens": { "accent": "#E4572E", "cms-ink": "#1D3A8A", "cms-bg": "#E8EEFD", "gutter": "24px", "serif": "'Zilla Slab', Georgia, serif" },
|
|
1585
|
-
"Tones": { "cms": { "ink": "cms-ink", "bg": "cms-bg", "edge": "#B9C8F5" } }
|
|
1586
|
-
```
|
|
1587
|
-
|
|
1588
|
-
A token is a name and one value: a colour (checked as `Colors` is), a length or a `clamp()` of three
|
|
1589
|
-
(checked as `Text` is), or a font stack of plain or quoted family names. A name is a letter, then
|
|
1590
|
-
letters, digits and hyphens, up to 40. Each token is emitted on the root as `--t-<name>` by
|
|
1591
|
-
`createSiteLayout` (through `themeVariablesCss`), so a site's stylesheet and its blocks write
|
|
1592
|
-
`var(--t-accent)` instead of a hex value. A name or a value that fails its check is dropped and the
|
|
1593
|
-
rest are kept. Up to 200.
|
|
1594
|
-
|
|
1595
|
-
A tone is a name and three colours: `ink`, `bg` and `edge` (hairlines and borders). Each is a token
|
|
1596
|
-
name, a `Colors` slot name, or a colour written out, looked up in that order when the tone is drawn,
|
|
1597
|
-
so changing a token changes every tone that names it. Text of every kind on the tone is the `ink`,
|
|
1598
|
-
and a filled accent is the `ink` with the `bg` as its text. A name is lower case letters, digits and
|
|
1599
|
-
hyphens; a built-in name is refused, since those follow `Colors`. A tone with a colour that resolves
|
|
1600
|
-
to nothing is dropped. Up to 40.
|
|
1601
|
-
|
|
1602
|
-
Every block field that picks a tone offers the site's tones after the built-in six, and a block
|
|
1603
|
-
stores `"tone": "cms"` the way it stores `"tone": "accent"`. `HeaderTone` and `FooterTone` take them
|
|
1604
|
-
too. `/api/blocks` lists them on every tone field: a build-time site's own, and on a request-time
|
|
1605
|
-
site the requesting tenant's, when the route is mounted as `createBlockSchemaRoute(config, blocks)`.
|
|
1606
|
-
|
|
1607
|
-
A site that sets neither gets no `--t-` property and no new option anywhere, and renders byte for
|
|
1608
|
-
byte as it did.
|
|
1609
|
-
|
|
1610
|
-
Read time is derived from the body at 200 words a minute, with fenced code blocks excluded, so there
|
|
1611
|
-
is no field to fill in and nothing to keep in sync.
|
|
1612
|
-
|
|
1613
|
-
### Style recipes
|
|
1614
|
-
|
|
1615
|
-
A primitive draws itself from tokens, which keeps every page one design and also means a designed
|
|
1616
|
-
section cannot be built from primitives: a card with its own padding, border, corner and shadow is
|
|
1617
|
-
not something any token names. A recipe is that card, said once in the site settings as
|
|
1618
|
-
`StyleRecipes` (or `theme.recipes` in `defineConfig`), and a block wears it with `recipe`.
|
|
1619
|
-
|
|
1620
|
-
```json
|
|
1621
|
-
"StyleRecipes": {
|
|
1622
|
-
"card": {
|
|
1623
|
-
"class": "lift",
|
|
1624
|
-
"style": {
|
|
1625
|
-
"padding": "22px 24px",
|
|
1626
|
-
"background": "{colors.surface}",
|
|
1627
|
-
"border": "1px solid {colors.hairline}",
|
|
1628
|
-
"border-radius": "16px",
|
|
1629
|
-
"box-shadow": "0 1px 2px rgba(16,18,35,.04)",
|
|
1630
|
-
"display": "flex",
|
|
1631
|
-
"flex-direction": "column",
|
|
1632
|
-
"gap": "{space.sm}"
|
|
1633
|
-
}
|
|
1634
|
-
},
|
|
1635
|
-
"eyebrow": {
|
|
1636
|
-
"style": {
|
|
1637
|
-
"font-family": "{fonts.mono}",
|
|
1638
|
-
"font-size": "11px",
|
|
1639
|
-
"letter-spacing": ".16em",
|
|
1640
|
-
"text-transform": "uppercase",
|
|
1641
|
-
"color": "{colors.muted}"
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
```
|
|
1646
|
-
|
|
1647
|
-
```json
|
|
1648
|
-
{ "type": "panel", "props": { "recipe": "card", "content": [[
|
|
1649
|
-
{ "type": "text", "props": { "value": "01 Products", "recipe": "eyebrow" } }
|
|
1650
|
-
]] } }
|
|
1651
|
-
```
|
|
1652
|
-
|
|
1653
|
-
A recipe is a name and two keys, both optional but not both empty:
|
|
1654
|
-
|
|
1655
|
-
- `class`: class names, space separated, put on the element beside the style. A style attribute
|
|
1656
|
-
cannot say `:hover`, a focus ring or a media query, so those go in the site's own stylesheet under
|
|
1657
|
-
this class. Up to 8, each a letter or underscore, then letters, digits, `_` and `-`.
|
|
1658
|
-
- `style`: CSS property names, written as a stylesheet writes them, to values. Up to 40.
|
|
1659
|
-
|
|
1660
|
-
A value is CSS text, and `{name}` in it stands for a theme value: `{accent}` is the token of that
|
|
1661
|
-
name from `Tokens`, and `{colors.<slot>}`, `{space.<step>}`, `{radii.<name>}`, `{text.<role>}`,
|
|
1662
|
-
`{fonts.<role>}` and `{layout.<name>}` are the theme's own. References are resolved when the block
|
|
1663
|
-
draws, against the requesting tenant's theme, so changing a token changes every recipe that names
|
|
1664
|
-
it. A declaration whose reference does not resolve is left out and the rest of the recipe draws.
|
|
1665
|
-
|
|
1666
|
-
The properties a recipe may set:
|
|
1667
|
-
|
|
1668
|
-
| Group | Properties |
|
|
1669
|
-
| --- | --- |
|
|
1670
|
-
| box | `display`, `position` (`static` or `relative` only), `box-sizing`, `width`, `min-width`, `max-width`, `height`, `min-height`, `max-height`, `aspect-ratio`, `overflow`, `overflow-x`, `overflow-y`, `vertical-align`, `opacity` |
|
|
1671
|
-
| spacing | `margin`, `padding` and their four sides, `margin-block`, `margin-inline`, `padding-block`, `padding-inline`, `gap`, `row-gap`, `column-gap` |
|
|
1672
|
-
| typography | `font-family`, `font-size`, `font-weight`, `font-style`, `font-variant-numeric`, `line-height`, `letter-spacing`, `text-align`, `text-transform`, `text-decoration`, `text-underline-offset`, `text-wrap`, `text-overflow`, `white-space`, `overflow-wrap`, `word-break` |
|
|
1673
|
-
| colour | `color`, `background`, `background-color` |
|
|
1674
|
-
| border | `border`, `border-top`, `border-right`, `border-bottom`, `border-left`, `border-color`, `border-style`, `border-width` |
|
|
1675
|
-
| radius | `border-radius` |
|
|
1676
|
-
| shadow | `box-shadow`, `text-shadow` |
|
|
1677
|
-
| grid | `grid-template-columns`, `grid-template-rows`, `grid-auto-flow`, `grid-auto-rows`, `grid-column`, `grid-row`, `justify-items`, `place-items`, `place-content` |
|
|
1678
|
-
| flex | `flex`, `flex-direction`, `flex-wrap`, `flex-grow`, `flex-shrink`, `flex-basis`, `align-items`, `align-content`, `align-self`, `justify-content`, `justify-self`, `order` |
|
|
1679
|
-
| engine | `--bp-ink`, `--bp-ink-soft`, `--bp-muted`, `--bp-hairline`, `--bp-accent`, `--bp-on-accent` (the tone the blocks inside read), `--bp-gap`, `--bp-list` (a block list's gap and display), `--bp-code-ink`, `--bp-code-bg`, `--bp-code-size`, `--bp-code-pad`, `--bp-code-radius` (inline code in a text block) |
|
|
1680
|
-
|
|
1681
|
-
The list is also exported as `RECIPE_PROPERTY_GROUPS`, for an editor.
|
|
1682
|
-
|
|
1683
|
-
The values reach a style attribute, which React writes by joining `name:value;` with no CSS
|
|
1684
|
-
escaping, so a value is held to a narrow shape rather than cleaned. It is letters, digits, spaces
|
|
1685
|
-
and `# % . , ( ) / + * -`, up to 240 characters, with quotes only around a plain family name
|
|
1686
|
-
(`'JetBrains Mono'`). Parentheses balance, and a function is one of `calc`, `min`, `max`, `clamp`,
|
|
1687
|
-
`minmax`, `repeat`, `fit-content`, `var` (naming a custom property and nothing else), the colour
|
|
1688
|
-
functions (`rgb`, `rgba`, `hsl`, `hsla`, `hwb`, `lab`, `lch`, `oklab`, `oklch`, `color-mix`) and the
|
|
1689
|
-
gradients. So `;`, `:`, braces, angle brackets, a backslash, `!important`, `@`, a comment, `url()`,
|
|
1690
|
-
`expression()`, `image-set()` and `attr()` are all refused: a recipe cannot load anything and
|
|
1691
|
-
cannot leave its own declaration. A property off the list, or a value that fails, is dropped and
|
|
1692
|
-
the rest of the recipe kept, the way `Colors` drops one bad colour. The check runs again when the
|
|
1693
|
-
block draws, on the resolved value, so a theme built by hand gets it too. A name is lower case
|
|
1694
|
-
letters, digits and hyphens, up to 40. Up to 400 recipes, merged over the configured ones name by
|
|
1695
|
-
name: barakocms.com's pages are about three hundred looks, one per element its design styles.
|
|
1696
|
-
|
|
1697
|
-
What wearing one does to a block:
|
|
1698
|
-
|
|
1699
|
-
- The block is its own cell. Every block in a list sits in a wrapper, and under a recipe the wrapper
|
|
1700
|
-
is `display: contents`, as a transparent block's is, so the recipe's element is what its parent
|
|
1701
|
-
lays out: a lede that takes `flex: 1 1 420px` beside a claim, a card that is the grid's item. A
|
|
1702
|
-
name the site has no recipe for keeps the wrapper along with the block's own look.
|
|
1703
|
-
That holds for an inline element too. A `link` or a `span` text wearing `display: inline-block`
|
|
1704
|
-
in a list, which is a column, is that column's flex item and is stretched to its width like any
|
|
1705
|
-
other; without a recipe it sits inside a block wrapper at its own width. A recipe that wants its
|
|
1706
|
-
own width says so: `align-self: flex-start`, or `width: fit-content`.
|
|
1707
|
-
- The recipe replaces the block's own inline look on its outer element outright. It is not merged
|
|
1708
|
-
over it: the card's own padding under a recipe that only set the corner is a look nobody drew.
|
|
1709
|
-
The block's token props for that element (`padding`, `radius`, `border` and so on) are not
|
|
1710
|
-
applied.
|
|
1711
|
-
- What makes the block work stays: a `stickyBar` keeps `position: sticky`, a `flow` keeps its cells
|
|
1712
|
-
(`--bp-list: contents`), a `row` and a `tabGroup` stay wrapping rows, a `list` keeps its marker, a
|
|
1713
|
-
`comparisonTable` keeps its own horizontal scroll, an `embed` keeps its width, shape and no
|
|
1714
|
-
border, a `figure` keeps no margin, and a `rotatingText` keeps the box its words stack in.
|
|
1715
|
-
- So does the layout the block's own props ask for: a `grid` or `flow` with `columns` keeps its
|
|
1716
|
-
grid and track list, and `align` and `justify` on a `stack`, `row` or `flow` are kept. A prop left
|
|
1717
|
-
unset leaves the recipe to say it, so a `flow` with no `columns` takes its grid from the recipe.
|
|
1718
|
-
- A `tone` named beside a recipe still sets the tone the blocks inside read. Without one, a recipe
|
|
1719
|
-
that changes the background sets `--bp-ink` and its neighbours itself.
|
|
1720
|
-
- A `tabPanel`'s recipe is its tab in the strip, and the open tab keeps its colours over it.
|
|
1721
|
-
- `recipe` is a text field, so a preset passes its own prop through, and a bound name picks a look
|
|
1722
|
-
per row: `"recipe": "card-{{item.Product}}"`.
|
|
1723
|
-
- A name the site has no recipe for draws the block's own look. A block that names none renders
|
|
1724
|
-
byte for byte as it did.
|
|
1725
|
-
|
|
1726
|
-
### Inline marks in a text block
|
|
1727
|
-
|
|
1728
|
-
`text` takes `format`, `plain` (the default) or `inline`. Inline, its value is one line with marks
|
|
1729
|
-
in it: `` `code` ``, `*emphasis*`, `**strong**`, `[links](/docs)` and `==an accent==`, which is drawn
|
|
1730
|
-
as `<span class="bp-accent">`. Accents do not nest: `==` inside one ends it. Nothing that makes a
|
|
1731
|
-
block of its own is read, so a heading stays one element: `# x`, a list or a quote is the text it
|
|
1732
|
-
is, and an image keeps only its alt text. A value longer than 2000 characters is read as plain text,
|
|
1733
|
-
since emphasis parsing is quadratic at worst and the value may be bound from content.
|
|
1734
|
-
|
|
1735
|
-
It goes through the same safe renderer as a body (`renderInlineMarkdown` in `barakopress/markdown`):
|
|
1736
|
-
raw HTML is escaped, a link must be a path, an anchor, http, https or mailto or it keeps its words
|
|
1737
|
-
and loses the link, and every attribute is escaped. A path starting `//` or `/\` is another site
|
|
1738
|
-
to a browser, so it is refused here and in a body alike.
|
|
1739
|
-
|
|
1740
|
-
The marks are styled under `:where(.bp-inline)`, so each rule weighs no more than the element it
|
|
1741
|
-
names and a site's own `.lede code` wins. The accent and a link take the band's accent. Code is the
|
|
1742
|
-
mono face, and reads `--bp-code-ink`, `--bp-code-bg`, `--bp-code-size`, `--bp-code-pad` and
|
|
1743
|
-
`--bp-code-radius` first, so a recipe gives the code in one block a tint and a corner:
|
|
1744
|
-
|
|
1745
|
-
```json
|
|
1746
|
-
"StyleRecipes": {
|
|
1747
|
-
"grabs-body": { "style": { "font-size": "14px", "--bp-code-bg": "#EEEBFD", "--bp-code-ink": "#4034A8", "--bp-code-pad": "1px 5px", "--bp-code-radius": "5px" } }
|
|
1748
|
-
}
|
|
1749
|
-
```
|
|
1750
|
-
|
|
1751
|
-
### Assets used exactly as supplied
|
|
1752
|
-
|
|
1753
|
-
Some marks come with an identity manual: never recoloured, never outlined, never put in a box, never
|
|
1754
|
-
crowded. A renderer that rounds a corner or drops a logo into a tinted panel breaks that rule, and on
|
|
1755
|
-
screen it looks like a nice touch. Mark the asset and it is drawn from the file, with a minimum clear
|
|
1756
|
-
space held around it, everywhere the engine draws an image.
|
|
1757
|
-
|
|
1758
|
-
```ts
|
|
1759
|
-
export const config = defineConfig({
|
|
1760
|
-
site: { name: "The club", url: "https://club.example", logo: "/mark.svg" },
|
|
1761
|
-
theme: { asSupplied: [{ url: "/mark.svg", clearSpace: "lg" }] },
|
|
1762
|
-
});
|
|
1763
|
-
```
|
|
1764
|
-
|
|
1765
|
-
A tenant says the same thing in its site settings:
|
|
1766
|
-
|
|
1767
|
-
| Field | Type | What |
|
|
1768
|
-
| --- | --- | --- |
|
|
1769
|
-
| `AssetsAsSupplied` | list | URLs, or `{ "url": "...", "clearSpace": "lg" }` for one that needs more room. A list saved empty clears the configured one |
|
|
1770
|
-
| `LogoAsSupplied` | boolean | Marks `Logo` and `FooterLogo`, so replacing the logo file does not mean editing a second setting that names the old one |
|
|
1771
|
-
| `LogoClearSpace` | string | The clear space around those two |
|
|
1772
|
-
|
|
1773
|
-
The clear space is a name from the theme's spacing scale (`none`, `xs`, `sm`, `md`, `lg`, `xl`,
|
|
1774
|
-
`xxl`), `md` when unset, and it is a minimum: a block asking for more gets more, a block asking for
|
|
1775
|
-
less gets the site's. A URL is matched without its query, so a mark the CMS resized with `?w=480` is
|
|
1776
|
-
still that mark.
|
|
1777
|
-
|
|
1778
|
-
What a marked asset gets is the file: no tint, no border, no corner, no shadow, no filter, no crop,
|
|
1779
|
-
and the clear space around it at every width. A block that asked for a frame draws the mark alone
|
|
1780
|
-
instead, which is what the `image` block's own `asSupplied` and `clearSpace` props are for when the
|
|
1781
|
-
site has not listed the file. It holds in the header, the footer, the holding page, a header or
|
|
1782
|
-
footer region, a page block, a preset, a post's cover image, a collection item's image and an image
|
|
1783
|
-
inside markdown, and `src/assets.test.tsx` walks all of those and fails if a new way to draw an image
|
|
1784
|
-
skips the rule.
|
|
1785
|
-
|
|
1786
|
-
One thing it cannot do: it does not know what is behind the mark. A tenant that puts a marked asset
|
|
1787
|
-
on an `inverse` band gets the supplied file on that band, drawn plainly. Choosing the band is the
|
|
1788
|
-
tenant's, and a white box behind the mark would itself be the boxing the rule forbids.
|
|
1789
|
-
|
|
1790
|
-
### Related posts, if the CMS has the AI module
|
|
1791
|
-
|
|
1792
|
-
`listRelated` asks `BarakoCMS.AI` for the posts closest to this one and hands them to `PostView`,
|
|
1793
|
-
which renders a band of cards with the similarity score on each. `createBlogPost` already does the
|
|
1794
|
-
call, so a site using the factory gets it for nothing.
|
|
1795
|
-
|
|
1796
|
-
Nothing about it is required. A CMS without the module answers 404, a type that is not publicly
|
|
1797
|
-
deliverable answers 404, a module installed but not enabled answers an empty list, and an
|
|
1798
|
-
unreachable CMS throws. All four end the same way: an empty array, no band, no heading, no empty
|
|
1799
|
-
state. The reader of a site without the module never learns the feature exists.
|
|
1800
|
-
|
|
1801
|
-
The post being read is its own closest match, so the fetch asks for more than it renders and drops
|
|
1802
|
-
itself. A hit with no slug is dropped too, because there is nothing to link it to.
|
|
1803
|
-
|
|
1804
|
-
Turning it on is the CMS's job, not this package's: `dotnet add package BarakoCMS.AI`, point
|
|
1805
|
-
`Ai:EmbeddingBaseUrl` at an Ollama instance, set `Ai:Enabled`, and index the type.
|
|
265
|
+
`sites`, identity is data in the CMS and read per request: see
|
|
266
|
+
[One build, many sites](https://github.com/BaryoDev/barakoPress/blob/master/docs/sites.md#one-build-many-sites).
|
|
1806
267
|
|
|
1807
268
|
## The caching design, which is the whole point
|
|
1808
269
|
|
|
@@ -1816,195 +277,23 @@ publish -> signed webhook -> revalidate -> next render reads once
|
|
|
1816
277
|
|
|
1817
278
|
Every read in `src/delivery.ts` is tagged with your `cacheTag`. `POST /api/revalidate` drops that tag
|
|
1818
279
|
the moment barakoCMS says something changed, so a publish is live in one request and the steady state
|
|
1819
|
-
is no database reads.
|
|
1820
|
-
|
|
1821
|
-
### What a publish drops
|
|
280
|
+
is no database reads. [Caching and the webhook](https://github.com/BaryoDev/barakoPress/blob/master/docs/caching.md) has the rest: what a publish
|
|
281
|
+
drops, the render cache, running more than one container, and wiring the webhook.
|
|
1822
282
|
|
|
1823
|
-
|
|
1824
|
-
type's when it read a list, a search, the site settings or the page tree. barakoCMS names the content
|
|
1825
|
-
type and sends the entry's public data with every delivery, so a publish drops that type's tag and
|
|
1826
|
-
that entry's tag, and leaves every other entry of the type cached. Correcting one typo on a school
|
|
1827
|
-
with a thousand news posts re-renders that post and the lists it appears in, not the other 999.
|
|
283
|
+
## Deploying
|
|
1828
284
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
that path and every later read of it asks the CMS uncached, carrying no tag. That is how a type that
|
|
1835
|
-
has to be fresh to the minute lives beside pages cached for hours.
|
|
1836
|
-
|
|
1837
|
-
### Running more than one container
|
|
1838
|
-
|
|
1839
|
-
The kept answers, the host to tenant map, the webhook replay guard and the generation of each cache
|
|
1840
|
-
tag are in process by default, which is right for one container and wrong for two: a purge reaches
|
|
1841
|
-
one of them through the load balancer, and the others keep serving their own copies until the
|
|
1842
|
-
backstop runs out.
|
|
1843
|
-
|
|
1844
|
-
Pass a `store` on the config and they share it. It is three methods over anything the deployment
|
|
1845
|
-
already runs:
|
|
1846
|
-
|
|
1847
|
-
```ts
|
|
1848
|
-
import type { PressStore } from "barakopress";
|
|
1849
|
-
|
|
1850
|
-
const store: PressStore = {
|
|
1851
|
-
async get(key) { return (await redis.get(key)) ?? null; },
|
|
1852
|
-
async set(key, value, ttlSeconds) { await (ttlSeconds ? redis.set(key, value, { EX: ttlSeconds }) : redis.set(key, value)); },
|
|
1853
|
-
async delete(key) { await redis.del(key); },
|
|
1854
|
-
// Only when the key is absent, atomically. This is what honours a replayed delivery once.
|
|
1855
|
-
async add(key, value, ttlSeconds) { return (await redis.set(key, value, { NX: true, ...(ttlSeconds ? { EX: ttlSeconds } : {}) })) !== null; },
|
|
1856
|
-
};
|
|
1857
|
-
```
|
|
1858
|
-
|
|
1859
|
-
A purge one container receives then reaches the rest: the honoured delivery writes a generation
|
|
1860
|
-
against each tag it dropped, and a read carries the newest generation of its own tags in the URL it
|
|
1861
|
-
asks the CMS for, so a container that was never told is asking for something it has never cached.
|
|
1862
|
-
Next's own data cache stays per container; what crosses is the knowledge that it is out of date.
|
|
1863
|
-
|
|
1864
|
-
Each read also carries a backstop, 300 seconds by default. That is not for correctness, it is for the
|
|
1865
|
-
deployment where nobody ever created the webhook: without it their blog would be empty forever and
|
|
1866
|
-
nothing would say why. With it, a missing webhook degrades publishing from instant to a few minutes.
|
|
1867
|
-
|
|
1868
|
-
**The cache invalidation is configuration, not code.** In barakoBrew: a workflow on your post content
|
|
1869
|
-
type, trigger `Published`, one Webhook action with the URL and a shared secret. Nothing is deployed
|
|
1870
|
-
to change it.
|
|
1871
|
-
|
|
1872
|
-
### The render cache
|
|
1873
|
-
|
|
1874
|
-
Reads being cached is half of it. The other half is not rendering the page again, and for a
|
|
1875
|
-
request-time site that used to be impossible: resolving the tenant meant reading the request host,
|
|
1876
|
-
reading a header makes a route dynamic, and Next never keeps a dynamic route. One container serving
|
|
1877
|
-
forty domains re-rendered every page for every visitor, with every read under it already cached.
|
|
1878
|
-
|
|
1879
|
-
Next keys what it keeps by the path and by nothing else. There is no `Vary` and no second key to
|
|
1880
|
-
add. So whatever a render depends on has to be in the path, and three things are:
|
|
1881
|
-
|
|
1882
|
-
| In the segment | Why |
|
|
1883
|
-
| --- | --- |
|
|
1884
|
-
| the tenant | two tenants must never share an entry. This is the whole reason the segment exists |
|
|
1885
|
-
| the gate | `public`, or `shared` for a request carrying a valid share session. A holding tenant answers differently for each, so each gets its own entry |
|
|
1886
|
-
| the host | the site's origin when the tenant's settings leave `Url` empty. Two hosts on one tenant are two renders, not one |
|
|
1887
|
-
|
|
1888
|
-
The proxy writes them, as `/_press/<tenant>~<gate>~<host>/<path>`, and it is the only thing that
|
|
1889
|
-
does: a path that arrives under `/_press` from outside is a 404, because otherwise
|
|
1890
|
-
`https://one.example/_press/other~public~other.example/` would render another tenant's site on this
|
|
1891
|
-
tenant's domain. A host the CMS does not know is a 404 before anything renders, so an invented host
|
|
1892
|
-
never makes an entry: only a host a tenant owns can put one there.
|
|
1893
|
-
|
|
1894
|
-
**What is cached.** The index, the pages tree, the collection indexes and the archives, per tenant
|
|
1895
|
-
and per path. `revalidateTag` drops a tenant's renders along with its reads, so the webhook that was
|
|
1896
|
-
already purging one purges both, and a publish on one tenant leaves every other tenant's renders in
|
|
1897
|
-
place. The first few views of a path answer `STALE` rather than `HIT` while the reads under them are
|
|
1898
|
-
still filling Next's data cache; after that it is `HIT` with no render and no read.
|
|
1899
|
-
|
|
1900
|
-
**What is not cached, deliberately.**
|
|
1901
|
-
|
|
1902
|
-
| Not cached | Why |
|
|
1903
|
-
| --- | --- |
|
|
1904
|
-
| a route that reads `?preview=` | a draft must never be in a shared cache, and reading the query is what keeps the route out of it. `createBlogPostPreview` is such a route; `createBlogPost` is not |
|
|
1905
|
-
| a page binding `{{query.X}}` | same reason. On a kept route the query is not handed to the blocks at all, so the binding is reported as an unbound scope and renders as nothing. A site that wants it writes `createPage(config, blocks, { query: true })` and leaves `generateStaticParams` out of that file |
|
|
1906
|
-
| `createViewerPage` | per viewer by definition. It calls `connection()`, and a route that does cannot be kept |
|
|
1907
|
-
| the feed, the sitemap and robots | `sitemap.ts` and `robots.ts` have to sit at the root of the app tree, so neither can carry a tenant segment. They resolve their own tenant and stay dynamic |
|
|
1908
|
-
| `/_share` and the redeem route | per visitor |
|
|
1909
|
-
|
|
1910
|
-
**Which routes are kept is the consumer's call, in the consumer's file**, the same as `revalidate`.
|
|
1911
|
-
A page route that exports `createSiteStaticParams()` is kept; one that does not is rendered per
|
|
1912
|
-
request. That is also the switch for the two rows above: a route that reads the query leaves it out.
|
|
1913
|
-
|
|
1914
|
-
**Nothing downstream may keep a copy.** Every rewritten answer carries `Cache-Control: private,
|
|
1915
|
-
no-store`, which is what a request-time site has always sent. Next's cache is the server's own and is
|
|
1916
|
-
not that header, so the render is still kept here. What stays true is that a browser or a proxy
|
|
1917
|
-
holding `/` is never holding one visitor's gate for the next visitor.
|
|
1918
|
-
|
|
1919
|
-
### One secret
|
|
1920
|
-
|
|
1921
|
-
`PRESS_SECRET` keys everything this renderer signs: each tenant's webhook key and each share session
|
|
1922
|
-
cookie. Every signature puts its purpose first in what it signs (`revalidate.` or `press-share.`), so
|
|
1923
|
-
one made for one purpose never verifies as another. It is read per request and has one rule for every
|
|
1924
|
-
purpose: at least 32 characters, for example `openssl rand -base64 48`.
|
|
1925
|
-
|
|
1926
|
-
| When `PRESS_SECRET` is | Webhooks | Share sessions |
|
|
1927
|
-
| --- | --- | --- |
|
|
1928
|
-
| 32 characters or more | verified with it | signed with it |
|
|
1929
|
-
| set but shorter | refused with 503 | none issued or accepted |
|
|
1930
|
-
| unset | `REVALIDATE_SECRET`, as in 0.3.0 | `PRESS_PREVIEW_SECRET` |
|
|
1931
|
-
|
|
1932
|
-
The older names are read only when `PRESS_SECRET` is unset, so a site that set them keeps working, and
|
|
1933
|
-
the keys derive byte for byte as before, so a key already pasted into a tenant's workflow keeps
|
|
1934
|
-
verifying. A `REVALIDATE_SECRET` shorter than 32 characters still verifies and logs a warning once,
|
|
1935
|
-
until 1.0.0. A `PRESS_PREVIEW_SECRET` shorter than 32 characters opens no session, as before.
|
|
1936
|
-
|
|
1937
|
-
Moving to the new name is copying the value: `PRESS_SECRET` set to what `REVALIDATE_SECRET` held
|
|
1938
|
-
derives the same tenant keys, provided it is 32 characters or more. A shorter one has to be replaced,
|
|
1939
|
-
and each tenant given its new key.
|
|
1940
|
-
|
|
1941
|
-
### Wiring the webhook
|
|
1942
|
-
|
|
1943
|
-
1. Put a random value of at least 32 characters in `PRESS_SECRET` where your app runs.
|
|
1944
|
-
2. In barakoBrew, create a workflow on your post content type, event `Published`.
|
|
1945
|
-
3. Add a Webhook action with `Url` set to `https://your-site/api/revalidate` and `Secret` set to the
|
|
1946
|
-
same value. On a site with `sites` configured, the `Secret` is the tenant's key instead, below.
|
|
1947
|
-
|
|
1948
|
-
### One key per tenant
|
|
1949
|
-
|
|
1950
|
-
With `sites` configured, one app serves many tenants, and each tenant's admin can read and edit its
|
|
1951
|
-
own workflows. So no tenant is given `PRESS_SECRET`. Each delivery is verified with a key derived
|
|
1952
|
-
for the tenant its host resolves to:
|
|
1953
|
-
|
|
1954
|
-
```
|
|
1955
|
-
key = lowercase hex HMAC-SHA256(PRESS_SECRET, "revalidate." + tenant handle)
|
|
1956
|
-
```
|
|
1957
|
-
|
|
1958
|
-
A delivery signed with one tenant's key gets a 401 on every other tenant's host, and so does one
|
|
1959
|
-
signed with `PRESS_SECRET` itself. Print a tenant's key where `PRESS_SECRET` (or `REVALIDATE_SECRET`) is set:
|
|
1960
|
-
|
|
1961
|
-
```bash
|
|
1962
|
-
npx barakopress revalidate-key baryo
|
|
1963
|
-
# or, with no Node:
|
|
1964
|
-
printf 'revalidate.%s' baryo | openssl dgst -sha256 -hmac "${PRESS_SECRET:-$REVALIDATE_SECRET}" | sed 's/^.* //'
|
|
1965
|
-
```
|
|
1966
|
-
|
|
1967
|
-
Both read the secret from the environment and print only the key. Paste it into the `Secret` of that
|
|
1968
|
-
tenant's Webhook action, with `Url` on that tenant's domain. A new tenant needs nothing on the
|
|
1969
|
-
renderer. Changing `PRESS_SECRET` changes every tenant's key, so each workflow needs its new one.
|
|
1970
|
-
|
|
1971
|
-
A site without `sites` (one build, one site) verifies with `PRESS_SECRET` itself, as it did with
|
|
1972
|
-
`REVALIDATE_SECRET`. Its workflow holds that value, so give it a value no other deployment uses.
|
|
1973
|
-
|
|
1974
|
-
**Upgrading a multi-tenant site to 0.4.0 is a breaking change.** Every tenant's workflow holds
|
|
1975
|
-
`REVALIDATE_SECRET` today, and after the upgrade that value verifies for no tenant: deliveries get a
|
|
1976
|
-
401 and purges stop, with the backstop the only refresh. Before or right after deploying, print each
|
|
1977
|
-
tenant's key and put it in that tenant's Webhook `Secret`. Then set a new `PRESS_SECRET` and print
|
|
1978
|
-
the keys again, because every tenant admin has seen the old value and could derive any tenant's key
|
|
1979
|
-
from it.
|
|
1980
|
-
|
|
1981
|
-
The barakoCMS webhook body does not name its tenant yet, so the host is what picks the key. Once the
|
|
1982
|
-
body carries the tenant (barakoCMS #868), a delivery whose body names a different tenant than its host
|
|
1983
|
-
resolves to is to be refused.
|
|
1984
|
-
|
|
1985
|
-
**If your site sets `trailingSlash: true`, the webhook URL needs the slash.** Next redirects
|
|
1986
|
-
`/api/revalidate` to `/api/revalidate/` with a 308, and barakoCMS does not follow redirects on a
|
|
1987
|
-
webhook, deliberately: following one was an SSRF hole, because the guard checked only the first hop.
|
|
1988
|
-
So a URL without the slash gets a 308, the delivery is recorded as failed, it retries five times and
|
|
1989
|
-
gives up, and the cache is never purged. The site keeps working because of the backstop, so the only
|
|
1990
|
-
symptom is that publishing feels slow.
|
|
1991
|
-
|
|
1992
|
-
barakoCMS refuses to sign a delivery to an `http://` URL unless `Webhooks:AllowInsecureSignedUrls` is
|
|
1993
|
-
on, which is for a loopback receiver in a lab. In production the URL is https, so this is not in your
|
|
1994
|
-
way.
|
|
1995
|
-
|
|
1996
|
-
Verification follows the recipe in the API's `docs/webhooks.md`: HMAC-SHA256 over
|
|
1997
|
-
`"<timestamp>.<raw body>"`, compared in constant time, with a 300 second tolerance so a captured
|
|
1998
|
-
delivery cannot be replayed later, and each signature honoured once per tenant. An unsigned, missigned or stale
|
|
1999
|
-
delivery gets a 401. If neither `PRESS_SECRET` nor `REVALIDATE_SECRET` is set, or `PRESS_SECRET` is
|
|
2000
|
-
shorter than 32 characters, the endpoint answers 503 and purges nothing,
|
|
2001
|
-
because an open cache-purge endpoint is a free denial of service.
|
|
285
|
+
The reference app in `app/` is a site that consumes this package by its name, the way yours does. It
|
|
286
|
+
is published as `ghcr.io/baryodev/barako-press` for linux/amd64 and linux/arm64, tagged with each
|
|
287
|
+
release's version and `latest`, and `dev` from master. The compose stack, the site-only compose file,
|
|
288
|
+
the BaryoVM release manifests and building a site's own image with an overlay are in
|
|
289
|
+
[Deploying](https://github.com/BaryoDev/barakoPress/blob/master/docs/deploying.md).
|
|
2002
290
|
|
|
2003
291
|
## What it reads
|
|
2004
292
|
|
|
2005
293
|
The `blog` blueprint barakoCMS ships: `post`, `category`, `author`, `page`. Apply it with
|
|
2006
294
|
`POST /api/content-types/blueprints/blog`. Field names are PascalCase because that is what the
|
|
2007
|
-
blueprint creates
|
|
295
|
+
blueprint creates. The default names live in `src/config.ts`, the blueprint's field map and
|
|
296
|
+
`REFERENCE_FIELDS`, and a site's config replaces any of them.
|
|
2008
297
|
|
|
2009
298
|
Only published entries of a type opted into public delivery appear. Ordering is asked of the API
|
|
2010
299
|
rather than applied to the page that came back, because sorting one page of results gives you the
|
|
@@ -2043,90 +332,6 @@ should absorb it rather than a reader. Measured against a CMS that logged every
|
|
|
2043
332
|
caused zero reads: `{ expire: 0 }` already makes the next request a blocking miss. It cost three
|
|
2044
333
|
requests per delivery and tripled what an attacker got from one captured signature, for nothing.
|
|
2045
334
|
|
|
2046
|
-
## The reference deployment in this repository
|
|
2047
|
-
|
|
2048
|
-
`app/` is not the product. It is a site that consumes this package by its name, exactly as yours
|
|
2049
|
-
does, so the package cannot quietly depend on something only its own repository has. It comes with a
|
|
2050
|
-
container, a compose stack with Caddy terminating TLS, and a BaryoVM release manifest.
|
|
2051
|
-
|
|
2052
|
-
```bash
|
|
2053
|
-
cp .env.example .env # point CMS_URL at your instance, set PRESS_SECRET
|
|
2054
|
-
npm install
|
|
2055
|
-
npm run build && npm start
|
|
2056
|
-
```
|
|
2057
|
-
|
|
2058
|
-
Deploying that stack to a VM:
|
|
2059
|
-
|
|
2060
|
-
```bash
|
|
2061
|
-
baryovm vm provision blog1 # or bring your own VM
|
|
2062
|
-
baryovm vm bootstrap blog1 # installs Docker, and only Docker
|
|
2063
|
-
baryovm vm harden blog1 # sshd policy and fail2ban
|
|
2064
|
-
baryovm stack add blog --vm blog1 --sudo --release-file ./baryovm.release.json
|
|
2065
|
-
baryovm stack release blog
|
|
2066
|
-
```
|
|
2067
|
-
|
|
2068
|
-
Before the first release, three DNS names have to point at the machine (`SITE_DOMAIN`, `API_DOMAIN`,
|
|
2069
|
-
`CONSOLE_DOMAIN`) and a `.env` has to exist on it at `/opt/barakopress/.env`. The release refuses to
|
|
2070
|
-
start without that file, and refuses again if `PRESS_SECRET` and `REVALIDATE_SECRET` are both empty, because a stack that
|
|
2071
|
-
comes up with no secret can never be told that content changed. Only ports 80 and 443 are published:
|
|
2072
|
-
the API, the console and the site are reachable only through Caddy on the compose network.
|
|
2073
|
-
|
|
2074
|
-
### The site alone, on a host that already has a proxy
|
|
2075
|
-
|
|
2076
|
-
That stack brings its own Caddy, so on a machine already terminating TLS it fights the existing
|
|
2077
|
-
proxy for 80 and 443, and on a machine serving other sites that failure is somebody else's outage.
|
|
2078
|
-
`compose.site.yml` is the other case: one container on a loopback port, joined to the edge network
|
|
2079
|
-
the existing stack created, reached through the proxy the host already runs. The host supplies the
|
|
2080
|
-
server block and the certificate.
|
|
2081
|
-
|
|
2082
|
-
It is a different compose file and a different manifest, so it is a different registration. `--path`
|
|
2083
|
-
is the compose directory, which is where the `.env` lives, and `--file` is what stops BaryoVM
|
|
2084
|
-
reaching for `compose.yml` and starting Caddy:
|
|
2085
|
-
|
|
2086
|
-
```bash
|
|
2087
|
-
baryovm stack add barakopress --vm oracle --sudo \
|
|
2088
|
-
--path /opt/barakopress \
|
|
2089
|
-
--file compose.site.yml \
|
|
2090
|
-
--release-file ./baryovm.site.json
|
|
2091
|
-
baryovm stack release barakopress
|
|
2092
|
-
```
|
|
2093
|
-
|
|
2094
|
-
The manifest syncs to `/opt/barakopress-src`, not to the compose directory, because it syncs with
|
|
2095
|
-
`--delete` and that directory holds the `.env`. Its `.env` needs `SITE_PORT`, `CMS_URL` pointing at
|
|
2096
|
-
the API's container name on the shared network, `SITE_URL` and `PRESS_SECRET`; `SITE_DOMAIN`,
|
|
2097
|
-
`API_DOMAIN` and `CONSOLE_DOMAIN` belong to the full stack and are not read here.
|
|
2098
|
-
|
|
2099
|
-
A site deployed this way writes its identity at build time, so it keeps its config and its root
|
|
2100
|
-
routes under `deploy/<name>/` and the manifest passes `PRESS_DEPLOY=<name>` to the build. See
|
|
2101
|
-
`deploy/press.baryo.dev/`. The reason it cannot use the reference app as it stands: every page there
|
|
2102
|
-
lives under `app/%5Fpress/[site]`, which only the proxy's rewrite reaches, and the rewrite only fires
|
|
2103
|
-
when the config sets `sites`. With no tenant to resolve to, every page answers 404 while the feed and
|
|
2104
|
-
the sitemap still work.
|
|
2105
|
-
|
|
2106
|
-
A site with its own theme keeps the overlay in its own repository and passes the directory as a
|
|
2107
|
-
build context instead of copying it into `deploy/`:
|
|
2108
|
-
|
|
2109
|
-
```bash
|
|
2110
|
-
docker buildx build \
|
|
2111
|
-
--build-context overlay=../barakocms-site/theme \
|
|
2112
|
-
--build-arg PRESS_TRAILING_SLASH=true \
|
|
2113
|
-
-t barako-press:barakocms .
|
|
2114
|
-
```
|
|
2115
|
-
|
|
2116
|
-
In compose that is `build.additional_contexts: { overlay: ../barakocms-site/theme }`. The overlay is
|
|
2117
|
-
laid over the reference app the same way. If it ships its own `app/%5Fpress/`, it is a request-time
|
|
2118
|
-
site with its own layout and routes, and its tree replaces the reference one; otherwise the tenant
|
|
2119
|
-
tree is removed as above. `PRESS_TRAILING_SLASH=true` sets Next's `trailingSlash`, for a site whose
|
|
2120
|
-
URLs end in a slash; configure its webhook URL with the slash too (see the revalidate endpoint).
|
|
2121
|
-
|
|
2122
|
-
Extra blocks go in the same way, as a `plugins` build context: see
|
|
2123
|
-
[Plugin packages](#plugin-packages).
|
|
2124
|
-
|
|
2125
|
-
One thing to expect on a first release: the image prerenders during `docker build`, where the CMS is
|
|
2126
|
-
not reachable, so the index, the feed and the sitemap are built empty and correct themselves one
|
|
2127
|
-
revalidate window later. That is why the manifest's `verify` greps the page rather than reading the
|
|
2128
|
-
status code, and why it waits long enough to see it happen.
|
|
2129
|
-
|
|
2130
335
|
## The look check
|
|
2131
336
|
|
|
2132
337
|
Before a site's domain moves to this stack, the look check proves the rebuilt pages look like the
|
|
@@ -2148,7 +353,7 @@ jobs:
|
|
|
2148
353
|
```
|
|
2149
354
|
|
|
2150
355
|
The pair list format, the per page threshold and the list of things that would otherwise make a run
|
|
2151
|
-
flake are in [docs/look-check.md](docs/look-check.md). `npm run look:selftest` runs the check twice
|
|
356
|
+
flake are in [docs/look-check.md](https://github.com/BaryoDev/barakoPress/blob/master/docs/look-check.md). `npm run look:selftest` runs the check twice
|
|
2152
357
|
over fixture pages, once green and once against a deliberate colour change, which is how anyone can
|
|
2153
358
|
see for themselves that it fails when it should.
|
|
2154
359
|
|
|
@@ -2168,11 +373,14 @@ run the package build first. While changing `src/`, leave `npm run watch` going
|
|
|
2168
373
|
`next dev` in another.
|
|
2169
374
|
|
|
2170
375
|
Why there is a build step at all, when every consumer is a Next app that could compile TypeScript
|
|
2171
|
-
itself: the decision and what it costs are recorded in [CLAUDE.md](CLAUDE.md).
|
|
376
|
+
itself: the decision and what it costs are recorded in [CLAUDE.md](https://github.com/BaryoDev/barakoPress/blob/master/CLAUDE.md).
|
|
2172
377
|
|
|
2173
|
-
Releasing is in [RELEASING.md](RELEASING.md). No npm token is stored anywhere.
|
|
378
|
+
Releasing is in [RELEASING.md](https://github.com/BaryoDev/barakoPress/blob/master/RELEASING.md). No npm token is stored anywhere.
|
|
2174
379
|
|
|
2175
380
|
## Status
|
|
2176
381
|
|
|
2177
|
-
|
|
2178
|
-
|
|
382
|
+
0.8.0, on npm as `barakopress` and on ghcr as `ghcr.io/baryodev/barako-press`. It renders
|
|
383
|
+
barakocms.com and press.baryo.dev against real barakoCMS instances. barakocms.com still runs a
|
|
384
|
+
derived image of the engine, with a plugin and a small overlay; #166 to #170 track what it takes to
|
|
385
|
+
run it on the published image as configuration. Other open gaps are in the issues, among them
|
|
386
|
+
visitor-switchable theme variants (#80) and a generated Open Graph image (#81).
|