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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/dist/cms.d.ts +47 -0
  4. package/dist/cms.d.ts.map +1 -0
  5. package/dist/cms.js +131 -0
  6. package/dist/cms.js.map +1 -0
  7. package/dist/config.d.ts +91 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +75 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/delivery.d.ts +45 -0
  12. package/dist/delivery.d.ts.map +1 -0
  13. package/dist/delivery.js +62 -0
  14. package/dist/delivery.js.map +1 -0
  15. package/dist/index.d.ts +17 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +39 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/markdown.d.ts +5 -0
  20. package/dist/markdown.d.ts.map +1 -0
  21. package/dist/markdown.js +91 -0
  22. package/dist/markdown.js.map +1 -0
  23. package/dist/routes/feed.d.ts +3 -0
  24. package/dist/routes/feed.d.ts.map +1 -0
  25. package/dist/routes/feed.js +73 -0
  26. package/dist/routes/feed.js.map +1 -0
  27. package/dist/routes/revalidate.d.ts +25 -0
  28. package/dist/routes/revalidate.d.ts.map +1 -0
  29. package/dist/routes/revalidate.js +136 -0
  30. package/dist/routes/revalidate.js.map +1 -0
  31. package/dist/routes/robots.d.ts +4 -0
  32. package/dist/routes/robots.d.ts.map +1 -0
  33. package/dist/routes/robots.js +18 -0
  34. package/dist/routes/robots.js.map +1 -0
  35. package/dist/routes/sitemap.d.ts +4 -0
  36. package/dist/routes/sitemap.d.ts.map +1 -0
  37. package/dist/routes/sitemap.js +33 -0
  38. package/dist/routes/sitemap.js.map +1 -0
  39. package/dist/screens/archive.d.ts +13 -0
  40. package/dist/screens/archive.d.ts.map +1 -0
  41. package/dist/screens/archive.js +47 -0
  42. package/dist/screens/archive.js.map +1 -0
  43. package/dist/screens/blog-index.d.ts +9 -0
  44. package/dist/screens/blog-index.d.ts.map +1 -0
  45. package/dist/screens/blog-index.js +38 -0
  46. package/dist/screens/blog-index.js.map +1 -0
  47. package/dist/screens/blog-post.d.ts +20 -0
  48. package/dist/screens/blog-post.d.ts.map +1 -0
  49. package/dist/screens/blog-post.js +103 -0
  50. package/dist/screens/blog-post.js.map +1 -0
  51. package/dist/screens/post-view.d.ts +8 -0
  52. package/dist/screens/post-view.d.ts.map +1 -0
  53. package/dist/screens/post-view.js +19 -0
  54. package/dist/screens/post-view.js.map +1 -0
  55. package/package.json +76 -0
  56. package/src/cms.ts +196 -0
  57. package/src/config.ts +159 -0
  58. package/src/delivery.ts +134 -0
  59. package/src/index.ts +69 -0
  60. package/src/markdown.ts +96 -0
  61. package/src/routes/feed.ts +79 -0
  62. package/src/routes/revalidate.ts +157 -0
  63. package/src/routes/robots.ts +20 -0
  64. package/src/routes/sitemap.ts +35 -0
  65. package/src/screens/archive.tsx +79 -0
  66. package/src/screens/blog-index.tsx +122 -0
  67. package/src/screens/blog-post.tsx +110 -0
  68. package/src/screens/post-view.tsx +84 -0
  69. 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cms.js","sourceRoot":"","sources":["../src/cms.ts"],"names":[],"mappings":"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"}
@@ -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