barakopress 0.2.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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/cms.d.ts +47 -0
- package/dist/cms.d.ts.map +1 -0
- package/dist/cms.js +131 -0
- package/dist/cms.js.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +75 -0
- package/dist/config.js.map +1 -0
- package/dist/delivery.d.ts +45 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +62 -0
- package/dist/delivery.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +91 -0
- package/dist/markdown.js.map +1 -0
- package/dist/routes/feed.d.ts +3 -0
- package/dist/routes/feed.d.ts.map +1 -0
- package/dist/routes/feed.js +73 -0
- package/dist/routes/feed.js.map +1 -0
- package/dist/routes/revalidate.d.ts +25 -0
- package/dist/routes/revalidate.d.ts.map +1 -0
- package/dist/routes/revalidate.js +136 -0
- package/dist/routes/revalidate.js.map +1 -0
- package/dist/routes/robots.d.ts +4 -0
- package/dist/routes/robots.d.ts.map +1 -0
- package/dist/routes/robots.js +18 -0
- package/dist/routes/robots.js.map +1 -0
- package/dist/routes/sitemap.d.ts +4 -0
- package/dist/routes/sitemap.d.ts.map +1 -0
- package/dist/routes/sitemap.js +33 -0
- package/dist/routes/sitemap.js.map +1 -0
- package/dist/screens/archive.d.ts +13 -0
- package/dist/screens/archive.d.ts.map +1 -0
- package/dist/screens/archive.js +47 -0
- package/dist/screens/archive.js.map +1 -0
- package/dist/screens/blog-index.d.ts +9 -0
- package/dist/screens/blog-index.d.ts.map +1 -0
- package/dist/screens/blog-index.js +38 -0
- package/dist/screens/blog-index.js.map +1 -0
- package/dist/screens/blog-post.d.ts +20 -0
- package/dist/screens/blog-post.d.ts.map +1 -0
- package/dist/screens/blog-post.js +103 -0
- package/dist/screens/blog-post.js.map +1 -0
- package/dist/screens/post-view.d.ts +8 -0
- package/dist/screens/post-view.d.ts.map +1 -0
- package/dist/screens/post-view.js +19 -0
- package/dist/screens/post-view.js.map +1 -0
- package/package.json +76 -0
- package/src/cms.ts +196 -0
- package/src/config.ts +159 -0
- package/src/delivery.ts +134 -0
- package/src/index.ts +69 -0
- package/src/markdown.ts +96 -0
- package/src/routes/feed.ts +79 -0
- package/src/routes/revalidate.ts +157 -0
- package/src/routes/robots.ts +20 -0
- package/src/routes/sitemap.ts +35 -0
- package/src/screens/archive.tsx +79 -0
- package/src/screens/blog-index.tsx +122 -0
- package/src/screens/blog-post.tsx +110 -0
- package/src/screens/post-view.tsx +84 -0
- package/src/styles.css +314 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arnel Robles
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# barakoPress
|
|
2
|
+
|
|
3
|
+
A blog engine for [barakoCMS](https://github.com/BaryoDev/barakoCMS), shipped as a package. Server
|
|
4
|
+
rendered, cached until the CMS says otherwise, so ordinary traffic never reaches Postgres and a
|
|
5
|
+
publish is live in one request.
|
|
6
|
+
|
|
7
|
+
It is an engine, not a site. Content types in barakoCMS are defined at runtime, so a client's posts
|
|
8
|
+
are as likely to be an `article` with a `Headline` as the blog blueprint's `post` with a `Title`.
|
|
9
|
+
Every name is configuration, which is why the same package serves a site mounted at `/writing` with
|
|
10
|
+
no authors and no categories.
|
|
11
|
+
|
|
12
|
+
Part of BaryoDev, alongside [barakoCMS](https://github.com/BaryoDev/barakoCMS) (the API) and
|
|
13
|
+
[barakoBrew](https://github.com/BaryoDev/barakoBrew) (the console).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install barakopress
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Needs Next 16 or later, React 19 or later, and Node 20.9 or later. Next and React are peer
|
|
22
|
+
dependencies, so your app's copies are the ones that run.
|
|
23
|
+
|
|
24
|
+
**No consumer build configuration.** The package ships compiled JavaScript and type declarations, so
|
|
25
|
+
there is no `transpilePackages` line to add and nothing to configure in `next.config.ts`. If you are
|
|
26
|
+
coming from 0.1.0, which shipped TypeScript sources, you can delete the
|
|
27
|
+
`transpilePackages: ["barakopress"]` line when you upgrade. Leaving it in does no harm, it just does
|
|
28
|
+
nothing.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
One config file, then one route file per route you want. A route file is two or three lines: a Next
|
|
33
|
+
route is decided by its path, and a package cannot create files in your app.
|
|
34
|
+
|
|
35
|
+
**`press.config.ts`**
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { defineConfig } from "barakopress";
|
|
39
|
+
|
|
40
|
+
export const config = defineConfig({
|
|
41
|
+
site: {
|
|
42
|
+
name: "My Blog",
|
|
43
|
+
tagline: "Things I wrote",
|
|
44
|
+
url: "https://myblog.example",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That is everything a site on the `blog` blueprint needs. `CMS_URL` comes from the environment.
|
|
50
|
+
|
|
51
|
+
**`app/page.tsx`**
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { createBlogIndex } from "barakopress";
|
|
55
|
+
import { config } from "@/press.config";
|
|
56
|
+
|
|
57
|
+
export default createBlogIndex(config);
|
|
58
|
+
export const revalidate = 300;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**`app/blog/[slug]/page.tsx`**
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { createBlogPost, createPostMetadata } from "barakopress";
|
|
65
|
+
import { config } from "@/press.config";
|
|
66
|
+
|
|
67
|
+
export default createBlogPost(config);
|
|
68
|
+
export const generateMetadata = createPostMetadata(config);
|
|
69
|
+
export const revalidate = 300;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`app/layout.tsx`**
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import "barakopress/styles.css";
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`export const revalidate` stays in your file on purpose. Next reads route segment config from the
|
|
79
|
+
file that owns the route and does not reliably follow a re-export, and the caching window is your
|
|
80
|
+
decision, not the engine's.
|
|
81
|
+
|
|
82
|
+
### The rest of the routes
|
|
83
|
+
|
|
84
|
+
| Create | Export | From |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `app/page.tsx` | `default` | `createBlogIndex(config)` |
|
|
87
|
+
| `app/blog/[slug]/page.tsx` | `default`, `generateMetadata` | `createBlogPost(config)`, `createPostMetadata(config)` |
|
|
88
|
+
| `app/authors/[slug]/page.tsx` | `default` | `createArchive(config, "author")` |
|
|
89
|
+
| `app/categories/[slug]/page.tsx` | `default` | `createArchive(config, "category")` |
|
|
90
|
+
| `app/feed.xml/route.ts` | `GET` | `createFeed(config)` |
|
|
91
|
+
| `app/sitemap.ts` | `default` | `createSitemap(config)` |
|
|
92
|
+
| `app/robots.ts` | `default` | `createRobots(config)` |
|
|
93
|
+
| `app/api/revalidate/route.ts` | `POST`, `GET` | `createRevalidateRoute(config)` |
|
|
94
|
+
|
|
95
|
+
Mount only what you want. Nothing requires anything else. The paths only have to agree with the
|
|
96
|
+
`routes` in your config, which is what every generated link is built from.
|
|
97
|
+
|
|
98
|
+
`createBlogPostPreview` replaces `createBlogPost` when you want `?preview=TOKEN` to render a draft.
|
|
99
|
+
It reads `searchParams`, which forces the route dynamic, so a site using `output: "export"` takes
|
|
100
|
+
`createBlogPost` and gives up preview. `createPostStaticParams` and `createArchiveStaticParams` exist
|
|
101
|
+
for that static case.
|
|
102
|
+
|
|
103
|
+
`Card` and `PostView` are exported too, for a site that wants its own page but the engine's markup.
|
|
104
|
+
|
|
105
|
+
## Configuring it
|
|
106
|
+
|
|
107
|
+
`press.config.ts` is the one file a site owns, and the seam the whole package turns on.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
export const config = defineConfig({
|
|
111
|
+
types: { post: "article", author: undefined, category: undefined },
|
|
112
|
+
fields: { title: "Headline", slug: "Permalink", body: "Story", publishedAt: "RunDate" },
|
|
113
|
+
routes: { post: "/writing" },
|
|
114
|
+
site: { name: "Client Three", url: "https://clientthree.example" },
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
That is a test, not an illustration. CI installs the packed tarball into a separate app, points it at
|
|
119
|
+
a content type called `article` with none of the blueprint's field names, no authors and no
|
|
120
|
+
categories, and asserts the site renders, links to `/writing`, and produces a feed carrying the
|
|
121
|
+
client's own identity.
|
|
122
|
+
|
|
123
|
+
Only `title` and `body` are required in `fields`. Anything you leave out is a feature the site does
|
|
124
|
+
without: the engine never asks the API for that data and never renders an empty slot for it. Naming a
|
|
125
|
+
reference type you do not have is worse than useless, because `include=Author` is a 400 from the API
|
|
126
|
+
for a post type with no such field.
|
|
127
|
+
|
|
128
|
+
| Key | Default | What |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `site` | required | Name, tagline and absolute origin. Every absolute link is built from the origin |
|
|
131
|
+
| `types` | `post`, `author`, `category` | The content type names |
|
|
132
|
+
| `fields` | the blueprint's PascalCase names | Which field holds what |
|
|
133
|
+
| `routes` | `/blog`, `/authors`, `/categories` | Where you mounted each route |
|
|
134
|
+
| `pageSizes` | 20, 50, 1000, 50 | Index, feed, sitemap, archive |
|
|
135
|
+
| `cacheTag` | `cms` | The tag this site purges. Two sites on one server need two tags |
|
|
136
|
+
| `backstopSeconds` | 300 | How long a cached read may live with no webhook. 0 disables it |
|
|
137
|
+
| `locale` | `en-GB` | Passed to `toLocaleDateString` |
|
|
138
|
+
| `cmsUrl` | `CMS_URL`, or `http://localhost:5005` | Where the CMS is, from this server |
|
|
139
|
+
| `tenant` | `CMS_TENANT` | Tenant slug, for a multi-tenant deployment |
|
|
140
|
+
|
|
141
|
+
**Identity is build time.** The index, the feed, the sitemap and robots are prerendered, so anything
|
|
142
|
+
`press.config.ts` reads from `process.env` is baked when you build, not when the server starts. Write
|
|
143
|
+
per-site values as literals in that file. Only per-environment values (`CMS_URL`,
|
|
144
|
+
`REVALIDATE_SECRET`) come from the environment, and neither of those is rendered. Getting this wrong
|
|
145
|
+
is how a client site ships with the vendor's name in its masthead.
|
|
146
|
+
|
|
147
|
+
## The caching design, which is the whole point
|
|
148
|
+
|
|
149
|
+
A static site reads content at build time, so an edit is invisible until someone rebuilds and
|
|
150
|
+
redeploys. A naive dynamic site reads the database on every page view. This does neither.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
request -> Next cache -> HTML (no database)
|
|
154
|
+
publish -> signed webhook -> revalidate -> next render reads once
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Every read in `src/delivery.ts` is tagged with your `cacheTag`. `POST /api/revalidate` drops that tag
|
|
158
|
+
the moment barakoCMS says something changed, so a publish is live in one request and the steady state
|
|
159
|
+
is no database reads.
|
|
160
|
+
|
|
161
|
+
Each read also carries a backstop, 300 seconds by default. That is not for correctness, it is for the
|
|
162
|
+
deployment where nobody ever created the webhook: without it their blog would be empty forever and
|
|
163
|
+
nothing would say why. With it, a missing webhook degrades publishing from instant to a few minutes.
|
|
164
|
+
|
|
165
|
+
**The cache invalidation is configuration, not code.** In barakoBrew: a workflow on your post content
|
|
166
|
+
type, trigger `Published`, one Webhook action with the URL and a shared secret. Nothing is deployed
|
|
167
|
+
to change it.
|
|
168
|
+
|
|
169
|
+
### Wiring the webhook
|
|
170
|
+
|
|
171
|
+
1. Put a long random value in `REVALIDATE_SECRET` where your app runs.
|
|
172
|
+
2. In barakoBrew, create a workflow on your post content type, event `Published`.
|
|
173
|
+
3. Add a Webhook action with `Url` set to `https://your-site/api/revalidate` and `Secret` set to the
|
|
174
|
+
same value.
|
|
175
|
+
|
|
176
|
+
**If your site sets `trailingSlash: true`, the webhook URL needs the slash.** Next redirects
|
|
177
|
+
`/api/revalidate` to `/api/revalidate/` with a 308, and barakoCMS does not follow redirects on a
|
|
178
|
+
webhook, deliberately: following one was an SSRF hole, because the guard checked only the first hop.
|
|
179
|
+
So a URL without the slash gets a 308, the delivery is recorded as failed, it retries five times and
|
|
180
|
+
gives up, and the cache is never purged. The site keeps working because of the backstop, so the only
|
|
181
|
+
symptom is that publishing feels slow.
|
|
182
|
+
|
|
183
|
+
barakoCMS refuses to sign a delivery to an `http://` URL unless `Webhooks:AllowInsecureSignedUrls` is
|
|
184
|
+
on, which is for a loopback receiver in a lab. In production the URL is https, so this is not in your
|
|
185
|
+
way.
|
|
186
|
+
|
|
187
|
+
Verification follows the recipe in the API's `docs/webhooks.md`: HMAC-SHA256 over
|
|
188
|
+
`"<timestamp>.<raw body>"`, compared in constant time, with a 300 second tolerance so a captured
|
|
189
|
+
delivery cannot be replayed later, and each signature honoured once. An unsigned, missigned or stale
|
|
190
|
+
delivery gets a 401. If `REVALIDATE_SECRET` is unset the endpoint answers 503 and purges nothing,
|
|
191
|
+
because an open cache-purge endpoint is a free denial of service.
|
|
192
|
+
|
|
193
|
+
## What it reads
|
|
194
|
+
|
|
195
|
+
The `blog` blueprint barakoCMS ships: `post`, `category`, `author`, `page`. Apply it with
|
|
196
|
+
`POST /api/content-types/blueprints/blog`. Field names are PascalCase because that is what the
|
|
197
|
+
blueprint creates, and `src/cms.ts` is the only file in the package that knows any field name.
|
|
198
|
+
|
|
199
|
+
Only published entries of a type opted into public delivery appear. Ordering is asked of the API
|
|
200
|
+
rather than applied to the page that came back, because sorting one page of results gives you the
|
|
201
|
+
newest of an arbitrary page.
|
|
202
|
+
|
|
203
|
+
## Security
|
|
204
|
+
|
|
205
|
+
Markdown is rendered as untrusted input in `src/markdown.ts`: raw HTML is escaped rather than passed
|
|
206
|
+
through, link and image destinations must be http, https or mailto, and text is escaped into every
|
|
207
|
+
attribute. An editor is authenticated, so this is the second line of defence, not the first. The
|
|
208
|
+
trade is that an author cannot embed raw HTML or an iframe. When you want that, the answer is a
|
|
209
|
+
content field your frontend renders deliberately, not a hole here.
|
|
210
|
+
|
|
211
|
+
## Two behaviours that were measured, not assumed
|
|
212
|
+
|
|
213
|
+
**`revalidateTag` needs `{ expire: 0 }`, not a named profile.** Next 16 made the second argument
|
|
214
|
+
mandatory. Passing `"max"` left the prerendered sitemap stale indefinitely.
|
|
215
|
+
|
|
216
|
+
**Cache warming after a purge does nothing, so there is none.** An earlier version fetched the main
|
|
217
|
+
pages after purging, on the theory that Next serves one stale response after a purge and the server
|
|
218
|
+
should absorb it rather than a reader. Measured against a CMS that logged every read, those fetches
|
|
219
|
+
caused zero reads: `{ expire: 0 }` already makes the next request a blocking miss. It cost three
|
|
220
|
+
requests per delivery and tripled what an attacker got from one captured signature, for nothing.
|
|
221
|
+
|
|
222
|
+
## The reference deployment in this repository
|
|
223
|
+
|
|
224
|
+
`app/` is not the product. It is a site that consumes this package by its name, exactly as yours
|
|
225
|
+
does, so the package cannot quietly depend on something only its own repository has. It comes with a
|
|
226
|
+
container, a compose stack with Caddy terminating TLS, and a BaryoVM release manifest.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
cp .env.example .env # point CMS_URL at your instance, set REVALIDATE_SECRET
|
|
230
|
+
npm install
|
|
231
|
+
npm run build && npm start
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Deploying that stack to a VM:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
baryovm vm provision blog1 # or bring your own VM
|
|
238
|
+
baryovm vm bootstrap blog1 # installs Docker, and only Docker
|
|
239
|
+
baryovm vm harden blog1 # sshd policy and fail2ban
|
|
240
|
+
baryovm stack add blog --vm blog1 --sudo --release-file ./baryovm.release.json
|
|
241
|
+
baryovm stack release blog
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Before the first release, three DNS names have to point at the machine (`SITE_DOMAIN`, `API_DOMAIN`,
|
|
245
|
+
`CONSOLE_DOMAIN`) and a `.env` has to exist on it at `/opt/barakopress/.env`. The release refuses to
|
|
246
|
+
start without that file, and refuses again if `REVALIDATE_SECRET` is empty, because a stack that
|
|
247
|
+
comes up with no secret can never be told that content changed. Only ports 80 and 443 are published:
|
|
248
|
+
the API, the console and the site are reachable only through Caddy on the compose network.
|
|
249
|
+
|
|
250
|
+
## Working on the engine
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm install
|
|
254
|
+
npm run build:package # src to dist: JS, declarations and source maps
|
|
255
|
+
npm run watch # the same, in watch mode, while iterating
|
|
256
|
+
npm run typecheck
|
|
257
|
+
npm run build # builds the package, then the reference app
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`app/` imports `barakopress`, not `../src`, so it resolves through `dist` like any other consumer.
|
|
261
|
+
That means `dist` has to exist before the app builds, which is why `npm run dev` and `npm run build`
|
|
262
|
+
run the package build first. While changing `src/`, leave `npm run watch` going in one terminal and
|
|
263
|
+
`next dev` in another.
|
|
264
|
+
|
|
265
|
+
Why there is a build step at all, when every consumer is a Next app that could compile TypeScript
|
|
266
|
+
itself: the decision and what it costs are recorded in [CLAUDE.md](CLAUDE.md).
|
|
267
|
+
|
|
268
|
+
Releasing is in [RELEASING.md](RELEASING.md). No npm token is stored anywhere.
|
|
269
|
+
|
|
270
|
+
## Status
|
|
271
|
+
|
|
272
|
+
Early. The blog works end to end against a real instance. Known gaps, all tracked upstream: no media
|
|
273
|
+
picker, no rich editor, no site settings object, and a content type's fields cannot be changed after
|
|
274
|
+
it is created.
|
package/dist/cms.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PressConfig } from "./config.js";
|
|
2
|
+
import { type PublicContent, type Seo } from "./delivery.js";
|
|
3
|
+
export type { Seo };
|
|
4
|
+
export interface Post {
|
|
5
|
+
id: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
title: string;
|
|
8
|
+
excerpt?: string;
|
|
9
|
+
body: string;
|
|
10
|
+
publishedAt?: string;
|
|
11
|
+
coverImage?: string;
|
|
12
|
+
coverImageAlt?: string;
|
|
13
|
+
featured: boolean;
|
|
14
|
+
tags: string[];
|
|
15
|
+
author?: Ref;
|
|
16
|
+
category?: Ref;
|
|
17
|
+
seo?: Seo;
|
|
18
|
+
}
|
|
19
|
+
export interface Ref {
|
|
20
|
+
id: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function toPost(config: PressConfig, c: PublicContent): Post;
|
|
25
|
+
export declare function listPosts(config: PressConfig, opts?: {
|
|
26
|
+
page?: number;
|
|
27
|
+
pageSize?: number;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
posts: Post[];
|
|
30
|
+
total: number;
|
|
31
|
+
hasNextPage: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function getPost(config: PressConfig, slug: string): Promise<Post | null>;
|
|
34
|
+
export declare function getPostPreview(config: PressConfig, slug: string, token: string): Promise<Post | null>;
|
|
35
|
+
/** Posts by author or category. Null when the site has no such type, or the slug is unknown. */
|
|
36
|
+
export declare function listPostsBy(config: PressConfig, which: "author" | "category", slug: string): Promise<Post[] | null>;
|
|
37
|
+
export interface Term {
|
|
38
|
+
id: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
name: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
photo?: string;
|
|
43
|
+
website?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function getTerm(config: PressConfig, which: "author" | "category", slug: string): Promise<Term | null>;
|
|
46
|
+
export declare function formatDate(config: PressConfig, value?: string): string;
|
|
47
|
+
//# sourceMappingURL=cms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cms.d.ts","sourceRoot":"","sources":["../src/cms.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAA+B,KAAK,aAAa,EAAE,KAAK,GAAG,EAAE,MAAM,eAAe,CAAC;AAE1F,YAAY,EAAE,GAAG,EAAE,CAAC;AAWpB,MAAM,WAAW,IAAI;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,GAAG,CAAC,EAAE,GAAG,CAAC;CACb;AAED,MAAM,WAAW,GAAG;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB;AAuBD,wBAAgB,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,aAAa,GAAG,IAAI,CAmBlE;AAYD,wBAAsB,SAAS,CAC3B,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,OAAO,CAAC;IAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAYjE;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAGrF;AAED,wBAAsB,cAAc,CAChC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAGtB;AAWD,gGAAgG;AAChG,wBAAsB,WAAW,CAC7B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,QAAQ,GAAG,UAAU,EAC5B,IAAI,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAexB;AAED,MAAM,WAAW,IAAI;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAUD,wBAAsB,OAAO,CACzB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,QAAQ,GAAG,UAAU,EAC5B,IAAI,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAetB;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAMtE"}
|
package/dist/cms.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { includesFor } from "./config.js";
|
|
2
|
+
import { bySlug, bySlugPreview, list } from "./delivery.js";
|
|
3
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
4
|
+
/** Reads a field by its configured name, or undefined when the site has no such field. */
|
|
5
|
+
function field(data, name) {
|
|
6
|
+
return name ? data[name] : undefined;
|
|
7
|
+
}
|
|
8
|
+
/*
|
|
9
|
+
* A resolved reference. `include` gives back the whole target entry, so this handles that and a
|
|
10
|
+
* bare id, and gives up rather than inventing a label when neither a name nor a slug is there.
|
|
11
|
+
*/
|
|
12
|
+
function toRef(config, v) {
|
|
13
|
+
if (!v || typeof v !== "object")
|
|
14
|
+
return undefined;
|
|
15
|
+
const d = v;
|
|
16
|
+
const data = (d.data ?? d);
|
|
17
|
+
const name = str(data.Name) || str(data.Title) || str(field(data, config.fields.title));
|
|
18
|
+
const slug = str(d.slug) || str(field(data, config.fields.slug)) || str(data.Slug);
|
|
19
|
+
if (!name && !slug)
|
|
20
|
+
return undefined;
|
|
21
|
+
return { id: str(d.id), slug, name: name || slug };
|
|
22
|
+
}
|
|
23
|
+
export function toPost(config, c) {
|
|
24
|
+
const d = c.data;
|
|
25
|
+
const f = config.fields;
|
|
26
|
+
const tags = field(d, f.tags);
|
|
27
|
+
return {
|
|
28
|
+
id: c.id,
|
|
29
|
+
slug: c.slug ?? str(field(d, f.slug)),
|
|
30
|
+
title: str(field(d, f.title)) || "Untitled",
|
|
31
|
+
excerpt: str(field(d, f.excerpt)) || undefined,
|
|
32
|
+
body: str(field(d, f.body)),
|
|
33
|
+
publishedAt: str(field(d, f.publishedAt)) || c.createdAt || undefined,
|
|
34
|
+
coverImage: str(field(d, f.coverImage)) || undefined,
|
|
35
|
+
coverImageAlt: str(field(d, f.coverImageAlt)) || undefined,
|
|
36
|
+
featured: field(d, f.featured) === true,
|
|
37
|
+
tags: Array.isArray(tags) ? tags.filter((t) => typeof t === "string") : [],
|
|
38
|
+
author: toRef(config, field(d, f.author)),
|
|
39
|
+
category: toRef(config, field(d, f.category)),
|
|
40
|
+
seo: c.seo ?? undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/*
|
|
44
|
+
* Ordering is asked of the API, not done here.
|
|
45
|
+
*
|
|
46
|
+
* Sorting the page that came back only orders those rows, so on a blog past one page the "newest"
|
|
47
|
+
* list is newest-of-an-arbitrary-page. The API can order every row, so it does.
|
|
48
|
+
*/
|
|
49
|
+
function sortKey(config) {
|
|
50
|
+
return config.fields.publishedAt ? `-${config.fields.publishedAt}` : undefined;
|
|
51
|
+
}
|
|
52
|
+
export async function listPosts(config, opts = {}) {
|
|
53
|
+
const res = await list(config, config.types.post, {
|
|
54
|
+
page: opts.page ?? 1,
|
|
55
|
+
pageSize: opts.pageSize ?? config.pageSizes.index,
|
|
56
|
+
include: includesFor(config),
|
|
57
|
+
sort: sortKey(config),
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
posts: res.items.map((c) => toPost(config, c)),
|
|
61
|
+
total: res.totalItems,
|
|
62
|
+
hasNextPage: res.hasNextPage,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function getPost(config, slug) {
|
|
66
|
+
const c = await bySlug(config, config.types.post, slug);
|
|
67
|
+
return c ? toPost(config, c) : null;
|
|
68
|
+
}
|
|
69
|
+
export async function getPostPreview(config, slug, token) {
|
|
70
|
+
const c = await bySlugPreview(config, config.types.post, slug, token);
|
|
71
|
+
return c ? toPost(config, c) : null;
|
|
72
|
+
}
|
|
73
|
+
/*
|
|
74
|
+
* Filtering by a reference takes the target's id, not its slug, so an archive is two calls:
|
|
75
|
+
* resolve the slug, then filter. Both are cached and tagged, so it costs two reads once.
|
|
76
|
+
*/
|
|
77
|
+
async function idForSlug(config, type, slug) {
|
|
78
|
+
const c = await bySlug(config, type, slug);
|
|
79
|
+
return c?.id ?? null;
|
|
80
|
+
}
|
|
81
|
+
/** Posts by author or category. Null when the site has no such type, or the slug is unknown. */
|
|
82
|
+
export async function listPostsBy(config, which, slug) {
|
|
83
|
+
const type = config.types[which];
|
|
84
|
+
const fieldName = config.fields[which];
|
|
85
|
+
if (!type || !fieldName)
|
|
86
|
+
return null;
|
|
87
|
+
const id = await idForSlug(config, type, slug);
|
|
88
|
+
if (!id)
|
|
89
|
+
return null;
|
|
90
|
+
const res = await list(config, config.types.post, {
|
|
91
|
+
pageSize: config.pageSizes.archive,
|
|
92
|
+
include: includesFor(config),
|
|
93
|
+
filter: [[fieldName, "eq", id]],
|
|
94
|
+
sort: sortKey(config),
|
|
95
|
+
});
|
|
96
|
+
return res.items.map((c) => toPost(config, c));
|
|
97
|
+
}
|
|
98
|
+
/*
|
|
99
|
+
* An author or category entry, for the heading of its archive page.
|
|
100
|
+
*
|
|
101
|
+
* These secondary types keep the blueprint's names with the post type's title and slug as
|
|
102
|
+
* fallbacks. One flat field map cannot describe three types, and a model whose author type
|
|
103
|
+
* differs wholesale wants its own screen. That is a fair boundary for the engine, and it is
|
|
104
|
+
* stated here so nobody discovers it by reading the source.
|
|
105
|
+
*/
|
|
106
|
+
export async function getTerm(config, which, slug) {
|
|
107
|
+
const type = config.types[which];
|
|
108
|
+
if (!type)
|
|
109
|
+
return null;
|
|
110
|
+
const c = await bySlug(config, type, slug);
|
|
111
|
+
if (!c)
|
|
112
|
+
return null;
|
|
113
|
+
const d = c.data;
|
|
114
|
+
return {
|
|
115
|
+
id: c.id,
|
|
116
|
+
slug: c.slug ?? (str(field(d, config.fields.slug)) || str(d.Slug)),
|
|
117
|
+
name: str(d.Name) || str(d.Title) || str(field(d, config.fields.title)) || "Untitled",
|
|
118
|
+
description: str(d.Description) || str(d.Bio) || undefined,
|
|
119
|
+
photo: str(d.Photo) || undefined,
|
|
120
|
+
website: str(d.Website) || undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function formatDate(config, value) {
|
|
124
|
+
if (!value)
|
|
125
|
+
return "";
|
|
126
|
+
const d = new Date(value);
|
|
127
|
+
return Number.isNaN(d.getTime())
|
|
128
|
+
? ""
|
|
129
|
+
: d.toLocaleDateString(config.locale, { day: "numeric", month: "long", year: "numeric" });
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=cms.js.map
|
package/dist/cms.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cms.js","sourceRoot":"","sources":["../src/cms.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export interface TypeNames {
|
|
2
|
+
/** The content type holding posts. */
|
|
3
|
+
post: string;
|
|
4
|
+
/** The type an author reference points at. Omit if the model has no authors. */
|
|
5
|
+
author?: string;
|
|
6
|
+
/** The type a category reference points at. Omit if the model has no categories. */
|
|
7
|
+
category?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Which field on the post type holds what.
|
|
11
|
+
*
|
|
12
|
+
* Only `title` and `body` are required; everything else is a feature the site does without if the
|
|
13
|
+
* model has no field for it. An absent name means the engine never asks for that data and never
|
|
14
|
+
* renders it, rather than rendering an empty slot.
|
|
15
|
+
*/
|
|
16
|
+
export interface FieldMap {
|
|
17
|
+
title: string;
|
|
18
|
+
body: string;
|
|
19
|
+
slug?: string;
|
|
20
|
+
excerpt?: string;
|
|
21
|
+
publishedAt?: string;
|
|
22
|
+
coverImage?: string;
|
|
23
|
+
coverImageAlt?: string;
|
|
24
|
+
featured?: string;
|
|
25
|
+
tags?: string;
|
|
26
|
+
/** The reference field pointing at the author type. */
|
|
27
|
+
author?: string;
|
|
28
|
+
/** The reference field pointing at the category type. */
|
|
29
|
+
category?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Where the consumer mounted each route, so generated links match the app's real shape. */
|
|
32
|
+
export interface RouteMap {
|
|
33
|
+
/** The post route's prefix, for example "/blog" or "/writing". */
|
|
34
|
+
post: string;
|
|
35
|
+
author?: string;
|
|
36
|
+
category?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface PageSizes {
|
|
39
|
+
index: number;
|
|
40
|
+
feed: number;
|
|
41
|
+
sitemap: number;
|
|
42
|
+
archive: number;
|
|
43
|
+
}
|
|
44
|
+
export interface SiteIdentity {
|
|
45
|
+
name: string;
|
|
46
|
+
tagline?: string;
|
|
47
|
+
/** Absolute origin, used for every absolute link in the feed, sitemap and robots. */
|
|
48
|
+
url: string;
|
|
49
|
+
}
|
|
50
|
+
export interface PressConfig {
|
|
51
|
+
types: TypeNames;
|
|
52
|
+
fields: FieldMap;
|
|
53
|
+
routes: RouteMap;
|
|
54
|
+
site: SiteIdentity;
|
|
55
|
+
pageSizes: PageSizes;
|
|
56
|
+
/** The cache tag this site purges. Two sites on one server need two tags. */
|
|
57
|
+
cacheTag: string;
|
|
58
|
+
/** How long a cached read may live with no webhook. Zero disables the backstop. */
|
|
59
|
+
backstopSeconds: number;
|
|
60
|
+
/** Passed to toLocaleDateString. */
|
|
61
|
+
locale: string;
|
|
62
|
+
/** Where the CMS is, from this server. */
|
|
63
|
+
cmsUrl: string;
|
|
64
|
+
/** Tenant slug, for a multi-tenant deployment. */
|
|
65
|
+
tenant?: string;
|
|
66
|
+
}
|
|
67
|
+
export type PressConfigInput = {
|
|
68
|
+
types?: Partial<TypeNames>;
|
|
69
|
+
fields?: Partial<FieldMap>;
|
|
70
|
+
routes?: Partial<RouteMap>;
|
|
71
|
+
site?: Partial<SiteIdentity>;
|
|
72
|
+
pageSizes?: Partial<PageSizes>;
|
|
73
|
+
} & Partial<Omit<PressConfig, "types" | "fields" | "routes" | "site" | "pageSizes">>;
|
|
74
|
+
/**
|
|
75
|
+
* Builds a complete config from a partial one.
|
|
76
|
+
*
|
|
77
|
+
* Site name and URL have no sensible default: a fallback of the engine's own name is how a client
|
|
78
|
+
* site ends up with the vendor's brand in its masthead, so they are required and the type says so.
|
|
79
|
+
*/
|
|
80
|
+
export declare function defineConfig(input: PressConfigInput & {
|
|
81
|
+
site: SiteIdentity;
|
|
82
|
+
}): PressConfig;
|
|
83
|
+
/**
|
|
84
|
+
* The reference fields worth resolving in one request, as the API's `include` expects them.
|
|
85
|
+
*
|
|
86
|
+
* Only names the site actually has. Sending `include=Author,Category` unconditionally is a 400
|
|
87
|
+
* from the API for any post type without both fields, which is every model that is not the blog
|
|
88
|
+
* blueprint.
|
|
89
|
+
*/
|
|
90
|
+
export declare function includesFor(config: PressConfig): string[];
|
|
91
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,SAAS;IACtB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACrB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,eAAe,EAAE,MAAM,CAAC;IACxB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC;AAwBrF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GAAG,WAAW,CAmB1F;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,EAAE,CAMzD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The seam.
|
|
3
|
+
*
|
|
4
|
+
* Everything a site differs on lives here, so a consumer configures the engine instead of forking
|
|
5
|
+
* it. That matters because barakoCMS content types are defined at runtime: a client's posts are as
|
|
6
|
+
* likely to be `article` with a `Headline` as they are to be the blog blueprint's `post` and
|
|
7
|
+
* `Title`. An engine that compiles one shape in is one client's blog wearing a package name.
|
|
8
|
+
*
|
|
9
|
+
* The defaults are the `blog` blueprint the API ships, so a site that used the blueprint passes
|
|
10
|
+
* nothing and gets the same behaviour as before this existed.
|
|
11
|
+
*
|
|
12
|
+
* Config reaches the screens through factories rather than a global, because a Next route is a
|
|
13
|
+
* file and a package cannot write files into someone else's app. A consumer's route file calls
|
|
14
|
+
* `createBlogIndex(config)` and exports the result. One import, one call, full control.
|
|
15
|
+
*/
|
|
16
|
+
/** The `blog` blueprint, which is what `POST /api/content-types/blueprints/blog` creates. */
|
|
17
|
+
const BLOG_BLUEPRINT = {
|
|
18
|
+
types: { post: "post", author: "author", category: "category" },
|
|
19
|
+
fields: {
|
|
20
|
+
title: "Title",
|
|
21
|
+
slug: "Slug",
|
|
22
|
+
excerpt: "Excerpt",
|
|
23
|
+
body: "Body",
|
|
24
|
+
coverImage: "CoverImage",
|
|
25
|
+
coverImageAlt: "CoverImageAlt",
|
|
26
|
+
publishedAt: "PublishedAt",
|
|
27
|
+
featured: "Featured",
|
|
28
|
+
tags: "Tags",
|
|
29
|
+
author: "Author",
|
|
30
|
+
category: "Category",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
function trimSlash(path) {
|
|
34
|
+
return path.replace(/\/+$/, "");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds a complete config from a partial one.
|
|
38
|
+
*
|
|
39
|
+
* Site name and URL have no sensible default: a fallback of the engine's own name is how a client
|
|
40
|
+
* site ends up with the vendor's brand in its masthead, so they are required and the type says so.
|
|
41
|
+
*/
|
|
42
|
+
export function defineConfig(input) {
|
|
43
|
+
const routes = { post: "/blog", author: "/authors", category: "/categories", ...input.routes };
|
|
44
|
+
return {
|
|
45
|
+
types: { ...BLOG_BLUEPRINT.types, ...input.types },
|
|
46
|
+
fields: { ...BLOG_BLUEPRINT.fields, ...input.fields },
|
|
47
|
+
routes: {
|
|
48
|
+
post: trimSlash(routes.post),
|
|
49
|
+
author: routes.author ? trimSlash(routes.author) : undefined,
|
|
50
|
+
category: routes.category ? trimSlash(routes.category) : undefined,
|
|
51
|
+
},
|
|
52
|
+
site: { ...input.site, url: trimSlash(input.site.url) },
|
|
53
|
+
pageSizes: { index: 20, feed: 50, sitemap: 1000, archive: 50, ...input.pageSizes },
|
|
54
|
+
cacheTag: input.cacheTag ?? "cms",
|
|
55
|
+
backstopSeconds: input.backstopSeconds ?? 300,
|
|
56
|
+
locale: input.locale ?? "en-GB",
|
|
57
|
+
cmsUrl: trimSlash(input.cmsUrl ?? process.env.CMS_URL ?? "http://localhost:5005"),
|
|
58
|
+
tenant: input.tenant ?? process.env.CMS_TENANT ?? undefined,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The reference fields worth resolving in one request, as the API's `include` expects them.
|
|
63
|
+
*
|
|
64
|
+
* Only names the site actually has. Sending `include=Author,Category` unconditionally is a 400
|
|
65
|
+
* from the API for any post type without both fields, which is every model that is not the blog
|
|
66
|
+
* blueprint.
|
|
67
|
+
*/
|
|
68
|
+
export function includesFor(config) {
|
|
69
|
+
const wanted = [
|
|
70
|
+
config.types.author ? config.fields.author : undefined,
|
|
71
|
+
config.types.category ? config.fields.category : undefined,
|
|
72
|
+
];
|
|
73
|
+
return wanted.filter((f) => Boolean(f));
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=config.js.map
|