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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAkFH,6FAA6F;AAC7F,MAAM,cAAc,GAA0C;IAC1D,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC/D,MAAM,EAAE;QACJ,KAAK,EAAE,OAAO;QACd,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,SAAS;QAClB,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,YAAY;QACxB,aAAa,EAAE,eAAe;QAC9B,WAAW,EAAE,aAAa;QAC1B,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,QAAQ;QAChB,QAAQ,EAAE,UAAU;KACvB;CACJ,CAAC;AAEF,SAAS,SAAS,CAAC,IAAY;IAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAgD;IACzE,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;IAE/F,OAAO;QACH,KAAK,EAAE,EAAE,GAAG,cAAc,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE;QAClD,MAAM,EAAE,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE;QACrD,MAAM,EAAE;YACJ,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;YAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;YAC5D,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;SACrE;QACD,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACvD,SAAS,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE;QAClF,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;QACjC,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,GAAG;QAC7C,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,OAAO;QAC/B,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,uBAAuB,CAAC;QACjF,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS;KAC9D,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAmB;IAC3C,MAAM,MAAM,GAA2B;QACnC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;QACtD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;KAC7D,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { PressConfig } from "./config.js";
2
+ export interface PublicContent {
3
+ id: string;
4
+ slug?: string;
5
+ contentType?: string;
6
+ createdAt?: string;
7
+ updatedAt?: string;
8
+ data: Record<string, unknown>;
9
+ seo?: Seo;
10
+ }
11
+ export interface Seo {
12
+ title?: string;
13
+ description?: string;
14
+ canonicalUrl?: string;
15
+ imageUrl?: string;
16
+ noIndex?: boolean;
17
+ }
18
+ export interface Paged<T> {
19
+ items: T[];
20
+ page: number;
21
+ pageSize: number;
22
+ totalItems: number;
23
+ totalPages: number;
24
+ hasNextPage: boolean;
25
+ }
26
+ export interface ListOptions {
27
+ page?: number;
28
+ pageSize?: number;
29
+ /** Reference fields to resolve in the same request. The API caps this at five. */
30
+ include?: string[];
31
+ /** `[field, op, value]`, for example ["Author", "eq", id]. The API caps this at five. */
32
+ filter?: [string, string, string][];
33
+ /** Sent to the API, so ordering covers every row rather than the page that came back. */
34
+ sort?: string;
35
+ }
36
+ export declare function list(config: PressConfig, type: string, opts?: ListOptions): Promise<Paged<PublicContent>>;
37
+ export declare function bySlug(config: PressConfig, type: string, slug: string): Promise<PublicContent | null>;
38
+ /**
39
+ * A draft, read with a preview token an editor minted from `POST /api/preview`.
40
+ *
41
+ * An invalid or expired token is not an error: the API falls back to published-only, so this
42
+ * returns whatever is public, or null. That is the safe direction to fail in.
43
+ */
44
+ export declare function bySlugPreview(config: PressConfig, type: string, slug: string, token: string): Promise<PublicContent | null>;
45
+ //# sourceMappingURL=delivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAmB/C,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,GAAG,CAAC,EAAE,GAAG,CAAC;CACb;AAED,MAAM,WAAW,GAAG;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,KAAK,CAAC,CAAC;IACpB,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACxB;AA+BD,MAAM,WAAW,WAAW;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACpC,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,IAAI,CACtB,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,WAAgB,GACvB,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAQ/B;AAED,wBAAsB,MAAM,CACxB,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAW/B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAK/B"}
@@ -0,0 +1,62 @@
1
+ function headers(config) {
2
+ return config.tenant ? { "X-Tenant": config.tenant } : {};
3
+ }
4
+ /** A cached, tagged read. Everything a visitor sees comes through here. */
5
+ async function get(config, path) {
6
+ const res = await fetch(`${config.cmsUrl}${path}`, {
7
+ headers: headers(config),
8
+ next: {
9
+ tags: [config.cacheTag],
10
+ // Zero means no backstop, which Next spells as false.
11
+ revalidate: config.backstopSeconds > 0 ? config.backstopSeconds : false,
12
+ },
13
+ });
14
+ if (!res.ok)
15
+ throw new Error(`${path} answered ${res.status}`);
16
+ return (await res.json());
17
+ }
18
+ /** An uncached read, for a draft. A cached draft would be served to the next visitor. */
19
+ async function getFresh(config, path) {
20
+ const res = await fetch(`${config.cmsUrl}${path}`, {
21
+ headers: headers(config),
22
+ cache: "no-store",
23
+ });
24
+ if (res.status === 404)
25
+ return null;
26
+ if (!res.ok)
27
+ throw new Error(`${path} answered ${res.status}`);
28
+ return (await res.json());
29
+ }
30
+ export async function list(config, type, opts = {}) {
31
+ const q = new URLSearchParams();
32
+ q.set("page", String(opts.page ?? 1));
33
+ q.set("pageSize", String(opts.pageSize ?? config.pageSizes.index));
34
+ if (opts.include?.length)
35
+ q.set("include", opts.include.join(","));
36
+ if (opts.sort)
37
+ q.set("sort", opts.sort);
38
+ for (const [field, op, value] of opts.filter ?? [])
39
+ q.set(`filter[${field}][${op}]`, value);
40
+ return get(config, `/api/public/${encodeURIComponent(type)}?${q}`);
41
+ }
42
+ export async function bySlug(config, type, slug) {
43
+ try {
44
+ return await get(config, `/api/public/${encodeURIComponent(type)}/${encodeURIComponent(slug)}`);
45
+ }
46
+ catch (e) {
47
+ // A missing entry is a not-found page, not a broken site.
48
+ if (e instanceof Error && e.message.includes("404"))
49
+ return null;
50
+ throw e;
51
+ }
52
+ }
53
+ /**
54
+ * A draft, read with a preview token an editor minted from `POST /api/preview`.
55
+ *
56
+ * An invalid or expired token is not an error: the API falls back to published-only, so this
57
+ * returns whatever is public, or null. That is the safe direction to fail in.
58
+ */
59
+ export async function bySlugPreview(config, type, slug, token) {
60
+ return getFresh(config, `/api/public/${encodeURIComponent(type)}/${encodeURIComponent(slug)}?preview=${encodeURIComponent(token)}`);
61
+ }
62
+ //# sourceMappingURL=delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.js","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export { defineConfig, includesFor } from "./config.js";
2
+ export type { PressConfig, PressConfigInput, TypeNames, FieldMap, RouteMap, PageSizes, SiteIdentity, } from "./config.js";
3
+ export { list, bySlug, bySlugPreview } from "./delivery.js";
4
+ export type { PublicContent, Seo, ListOptions, Paged } from "./delivery.js";
5
+ export { listPosts, getPost, getPostPreview, listPostsBy, getTerm, toPost, formatDate, } from "./cms.js";
6
+ export type { Post, Ref, Term } from "./cms.js";
7
+ export { renderMarkdown, isSafeHref, anchor } from "./markdown.js";
8
+ export { createBlogIndex, Card } from "./screens/blog-index.js";
9
+ export { createBlogPost, createBlogPostPreview, createPostMetadata, createPostStaticParams, } from "./screens/blog-post.js";
10
+ export { createArchive, createArchiveStaticParams } from "./screens/archive.js";
11
+ export { PostView } from "./screens/post-view.js";
12
+ export { createRevalidateRoute } from "./routes/revalidate.js";
13
+ export type { RevalidateOptions } from "./routes/revalidate.js";
14
+ export { createFeed } from "./routes/feed.js";
15
+ export { createSitemap } from "./routes/sitemap.js";
16
+ export { createRobots } from "./routes/robots.js";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EACR,WAAW,EACX,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,YAAY,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE5E,OAAO,EACH,SAAS,EACT,OAAO,EACP,cAAc,EACd,WAAW,EACX,OAAO,EACP,MAAM,EACN,UAAU,GACb,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EACH,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,GACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ /*
2
+ * The barakoPress engine.
3
+ *
4
+ * A site depends on this and re-exports the pieces it wants from its own route files, because
5
+ * Next decides routes by file path and a package cannot create files in someone else's app.
6
+ *
7
+ * Everything that differs between sites is in the config passed to these factories: the content
8
+ * type names, the field map, where the routes are mounted, page sizes, site identity, the cache
9
+ * tag and the backstop. barakoCMS content types are defined at runtime, so a client's posts are
10
+ * as likely to be `article` with a `Headline` as the blueprint's `post` and `Title`. An engine
11
+ * that compiled one shape in would be one blog wearing a package name.
12
+ *
13
+ * A consumer's app/page.tsx:
14
+ *
15
+ * import { createBlogIndex } from "barakopress";
16
+ * import { config } from "@/press.config";
17
+ * export default createBlogIndex(config);
18
+ * export const revalidate = 300;
19
+ *
20
+ * Route segment config stays in the consumer's file: Next reads it from the file that owns the
21
+ * route and does not reliably follow a re-export, and the caching window is their decision.
22
+ *
23
+ * The screens live in src/screens rather than src/pages because Next claims both `pages` and
24
+ * `app` as router directories, and a `src/pages` inside a transpiled package is read as a second
25
+ * router.
26
+ */
27
+ export { defineConfig, includesFor } from "./config.js";
28
+ export { list, bySlug, bySlugPreview } from "./delivery.js";
29
+ export { listPosts, getPost, getPostPreview, listPostsBy, getTerm, toPost, formatDate, } from "./cms.js";
30
+ export { renderMarkdown, isSafeHref, anchor } from "./markdown.js";
31
+ export { createBlogIndex, Card } from "./screens/blog-index.js";
32
+ export { createBlogPost, createBlogPostPreview, createPostMetadata, createPostStaticParams, } from "./screens/blog-post.js";
33
+ export { createArchive, createArchiveStaticParams } from "./screens/archive.js";
34
+ export { PostView } from "./screens/post-view.js";
35
+ export { createRevalidateRoute } from "./routes/revalidate.js";
36
+ export { createFeed } from "./routes/feed.js";
37
+ export { createSitemap } from "./routes/sitemap.js";
38
+ export { createRobots } from "./routes/robots.js";
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAWxD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG5D,OAAO,EACH,SAAS,EACT,OAAO,EACP,cAAc,EACd,WAAW,EACX,OAAO,EACP,MAAM,EACN,UAAU,GACb,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EACH,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,GACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare function isSafeHref(href: string): boolean;
2
+ /** Turns a heading into a stable id, so "On this page" links and deep links work. */
3
+ export declare function anchor(text: string): string;
4
+ export declare function renderMarkdown(source: string): string;
5
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAwBA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUhD;AAWD,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3C;AAwCD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrD"}
@@ -0,0 +1,91 @@
1
+ import { Marked } from "marked";
2
+ /*
3
+ * Markdown to HTML, treating the markdown as untrusted.
4
+ *
5
+ * An editor is authenticated, so this is not the first line of defence. It is the second, and it
6
+ * matters because the body of a post is the one field on this site that becomes markup. The
7
+ * threats it closes: an editor account that gets taken over, a contributor who is trusted to write
8
+ * but not to run script on the domain, and content imported in bulk from somewhere else.
9
+ *
10
+ * Three rules:
11
+ * 1. Raw HTML in the source is escaped, never passed through. That removes script tags, event
12
+ * handler attributes and iframes in one move, instead of trying to enumerate them.
13
+ * 2. A link or image destination must be http, https or mailto. That kills javascript: and
14
+ * data: URLs, which are the two that execute.
15
+ * 3. Text is escaped on the way into every attribute, so an alt or a title cannot close its own
16
+ * quote and add another attribute.
17
+ *
18
+ * The trade is real and deliberate: an author cannot embed a YouTube iframe or any raw HTML. When
19
+ * that is wanted, the answer is a content field the frontend renders deliberately, not a hole here.
20
+ */
21
+ const SAFE_SCHEMES = ["http:", "https:", "mailto:"];
22
+ export function isSafeHref(href) {
23
+ const trimmed = href.trim();
24
+ // A relative or anchor link has no scheme and cannot execute.
25
+ if (trimmed.startsWith("/") || trimmed.startsWith("#"))
26
+ return true;
27
+ try {
28
+ return SAFE_SCHEMES.includes(new URL(trimmed).protocol);
29
+ }
30
+ catch {
31
+ // Unparseable means it is not a URL this should emit.
32
+ return false;
33
+ }
34
+ }
35
+ function escapeHtml(value) {
36
+ return value
37
+ .replace(/&/g, "&amp;")
38
+ .replace(/</g, "&lt;")
39
+ .replace(/>/g, "&gt;")
40
+ .replace(/"/g, "&quot;")
41
+ .replace(/'/g, "&#39;");
42
+ }
43
+ /** Turns a heading into a stable id, so "On this page" links and deep links work. */
44
+ export function anchor(text) {
45
+ return text
46
+ .toLowerCase()
47
+ .replace(/[^\w\s-]/g, "")
48
+ .trim()
49
+ .replace(/\s+/g, "-");
50
+ }
51
+ function buildRenderer() {
52
+ const marked = new Marked({ gfm: true, breaks: false });
53
+ marked.use({
54
+ renderer: {
55
+ // Raw HTML blocks and inline HTML are emitted as visible text, not as markup.
56
+ html({ text }) {
57
+ return escapeHtml(text);
58
+ },
59
+ link({ href, title, tokens }) {
60
+ const label = this.parser.parseInline(tokens);
61
+ if (!isSafeHref(href)) {
62
+ // Keep the words, drop the destination. A reader still sees what was written.
63
+ return label;
64
+ }
65
+ const t = title ? ` title="${escapeHtml(title)}"` : "";
66
+ const external = /^https?:/.test(href.trim());
67
+ const rel = external ? ' rel="noopener noreferrer"' : "";
68
+ return `<a href="${escapeHtml(href.trim())}"${t}${rel}>${label}</a>`;
69
+ },
70
+ image({ href, title, text }) {
71
+ if (!isSafeHref(href))
72
+ return escapeHtml(text ?? "");
73
+ const t = title ? ` title="${escapeHtml(title)}"` : "";
74
+ return `<img src="${escapeHtml(href.trim())}" alt="${escapeHtml(text ?? "")}"${t} loading="lazy">`;
75
+ },
76
+ heading({ tokens, depth }) {
77
+ const label = this.parser.parseInline(tokens);
78
+ const plain = tokens.map((t) => ("raw" in t ? t.raw : "")).join("");
79
+ return `<h${depth} id="${escapeHtml(anchor(plain))}">${label}</h${depth}>`;
80
+ },
81
+ },
82
+ });
83
+ return marked;
84
+ }
85
+ const renderer = buildRenderer();
86
+ export function renderMarkdown(source) {
87
+ if (!source)
88
+ return "";
89
+ return renderer.parse(source, { async: false });
90
+ }
91
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { PressConfig } from "../config.js";
2
+ export declare function createFeed(config: PressConfig): () => Promise<Response>;
3
+ //# sourceMappingURL=feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../src/routes/feed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAsBhD,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,2BAwD7C"}
@@ -0,0 +1,73 @@
1
+ import { listPosts } from "../cms.js";
2
+ /*
3
+ * RSS, built here rather than proxied from the CMS.
4
+ *
5
+ * barakoCMS serves /api/public/{type}/feed.xml and it is correct, but its item links come from a
6
+ * server-side config template that has to be kept in step with the site's routes by hand. Built
7
+ * here, the routes have one owner: config.routes. It also costs nothing, because it is the same
8
+ * cached, tagged read the index uses.
9
+ */
10
+ /** XML text escaping. Every value below is content someone typed, so none of it is trusted. */
11
+ function xml(value) {
12
+ return value
13
+ .replace(/&/g, "&amp;")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;")
16
+ .replace(/"/g, "&quot;")
17
+ .replace(/'/g, "&apos;");
18
+ }
19
+ export function createFeed(config) {
20
+ return async function GET() {
21
+ let posts = [];
22
+ try {
23
+ ({ posts } = await listPosts(config, { pageSize: config.pageSizes.feed }));
24
+ }
25
+ catch {
26
+ // An unreachable CMS yields an empty channel, not a failure. This route is prerendered
27
+ // when the consumer gives it a revalidate window, so a throw fails their build, and at
28
+ // runtime it would hand a feed reader a 500.
29
+ posts = [];
30
+ }
31
+ const items = posts
32
+ .map((p) => {
33
+ const url = `${config.site.url}${config.routes.post}/${p.slug}`;
34
+ const date = p.publishedAt ? new Date(p.publishedAt) : null;
35
+ const pubDate = date && !Number.isNaN(date.getTime())
36
+ ? ` <pubDate>${date.toUTCString()}</pubDate>`
37
+ : "";
38
+ return [
39
+ " <item>",
40
+ ` <title>${xml(p.title)}</title>`,
41
+ ` <link>${xml(url)}</link>`,
42
+ ` <guid isPermaLink="true">${xml(url)}</guid>`,
43
+ // Plain text, not rendered HTML: a feed reader that trusts markup is not this
44
+ // site's problem to create.
45
+ p.excerpt ? ` <description>${xml(p.excerpt)}</description>` : "",
46
+ pubDate,
47
+ p.category ? ` <category>${xml(p.category.name)}</category>` : "",
48
+ " </item>",
49
+ ]
50
+ .filter(Boolean)
51
+ .join("\n");
52
+ })
53
+ .join("\n");
54
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
55
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
56
+ <channel>
57
+ <title>${xml(config.site.name)}</title>
58
+ <link>${xml(config.site.url)}</link>
59
+ <description>${xml(config.site.tagline ?? config.site.name)}</description>
60
+ <atom:link href="${xml(`${config.site.url}/feed.xml`)}" rel="self" type="application/rss+xml"/>
61
+ ${items}
62
+ </channel>
63
+ </rss>
64
+ `;
65
+ return new Response(body, {
66
+ headers: {
67
+ "content-type": "application/rss+xml; charset=utf-8",
68
+ "cache-control": "public, max-age=300",
69
+ },
70
+ });
71
+ };
72
+ }
73
+ //# sourceMappingURL=feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feed.js","sourceRoot":"","sources":["../../src/routes/feed.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,SAAS,GAAG,CAAC,KAAa;IACtB,OAAO,KAAK;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAmB;IAC1C,OAAO,KAAK,UAAU,GAAG;QACrB,IAAI,KAAK,GAAmD,EAAE,CAAC;QAC/D,IAAI,CAAC;YACD,CAAC,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC/E,CAAC;QAAC,MAAM,CAAC;YACL,uFAAuF;YACvF,uFAAuF;YACvF,6CAA6C;YAC7C,KAAK,GAAG,EAAE,CAAC;QACf,CAAC;QAED,MAAM,KAAK,GAAG,KAAK;aACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACP,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YAChE,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5D,MAAM,OAAO,GACT,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjC,CAAC,CAAC,kBAAkB,IAAI,CAAC,WAAW,EAAE,YAAY;gBAClD,CAAC,CAAC,EAAE,CAAC;YACb,OAAO;gBACH,YAAY;gBACZ,gBAAgB,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU;gBACtC,eAAe,GAAG,CAAC,GAAG,CAAC,SAAS;gBAChC,kCAAkC,GAAG,CAAC,GAAG,CAAC,SAAS;gBACnD,8EAA8E;gBAC9E,4BAA4B;gBAC5B,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE;gBACrE,OAAO;gBACP,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE;gBACtE,aAAa;aAChB;iBACI,MAAM,CAAC,OAAO,CAAC;iBACf,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhB,MAAM,IAAI,GAAG;;;aAGR,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;YACtB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;mBACb,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;uBACxC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC;EACvD,KAAK;;;CAGN,CAAC;QAEM,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;YACtB,OAAO,EAAE;gBACL,cAAc,EAAE,oCAAoC;gBACpD,eAAe,EAAE,qBAAqB;aACzC;SACJ,CAAC,CAAC;IACP,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { NextResponse, type NextRequest } from "next/server";
2
+ import type { PressConfig } from "../config.js";
3
+ export interface RevalidateOptions {
4
+ /** Defaults to REVALIDATE_SECRET. */
5
+ secret?: string;
6
+ /** Seconds a signature stays valid. Shorter is safer; the sender's clock has to be close. */
7
+ toleranceSeconds?: number;
8
+ maxBodyBytes?: number;
9
+ }
10
+ export declare function createRevalidateRoute(config: PressConfig, options?: RevalidateOptions): {
11
+ POST: (request: NextRequest) => Promise<NextResponse<{
12
+ error: string;
13
+ }> | NextResponse<{
14
+ revalidated: boolean;
15
+ repeated: boolean;
16
+ }> | NextResponse<{
17
+ revalidated: boolean;
18
+ tag: string;
19
+ delivery: string;
20
+ }>>;
21
+ GET: () => Promise<NextResponse<{
22
+ ok: boolean;
23
+ }>>;
24
+ };
25
+ //# sourceMappingURL=revalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidate.d.ts","sourceRoot":"","sources":["../../src/routes/revalidate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AA+BhD,MAAM,WAAW,iBAAiB;IAC9B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AA6BD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,iBAAsB;oBAKzD,WAAW;;;;;;;;;;;;;EAkF3C"}
@@ -0,0 +1,136 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { revalidateTag } from "next/cache";
3
+ import { NextResponse } from "next/server";
4
+ /*
5
+ * The endpoint that makes the cache correct.
6
+ *
7
+ * barakoCMS fires a signed webhook when content changes and this drops the cache tag, so the next
8
+ * request renders fresh. It is the only writer to the cache and it is reachable by anyone who
9
+ * finds the URL, so the order of the checks matters as much as the checks.
10
+ *
11
+ * The signing recipe is barakoCMS docs/webhooks.md:
12
+ *
13
+ * X-Barako-Timestamp unix seconds when it was signed
14
+ * X-Barako-Signature "sha256=" + lowercase hex HMAC-SHA256 over "<timestamp>.<raw body>"
15
+ * X-Barako-Delivery the delivery log row id
16
+ *
17
+ * Four things are load-bearing:
18
+ *
19
+ * 1. The signature covers the RAW body bytes. Parse the JSON first and you have re-serialised
20
+ * it into a different string that will never verify.
21
+ * 2. Constant-time compare, or a few thousand requests recover the expected signature.
22
+ * 3. An old timestamp is refused, so a captured delivery cannot be replayed forever.
23
+ * 4. Cheap checks come first. Reading an unbounded body into memory before authenticating lets
24
+ * an anonymous caller spend this server's memory, so the length is checked first and the
25
+ * read is capped.
26
+ */
27
+ const TOLERANCE_SECONDS = 300;
28
+ /** A signed delivery is small. Anything larger did not come from the CMS. */
29
+ const MAX_BODY_BYTES = 64 * 1024;
30
+ /** Constant-time compare that does not leak length through an early return either. */
31
+ function sameSignature(a, b) {
32
+ const left = Buffer.from(a, "utf8");
33
+ const right = Buffer.from(b, "utf8");
34
+ if (left.length !== right.length)
35
+ return false;
36
+ return timingSafeEqual(left, right);
37
+ }
38
+ /*
39
+ * A replayed delivery is honoured once.
40
+ *
41
+ * A captured signature stays valid for the whole tolerance window, and each accepted purge costs
42
+ * a re-render of every cached page. Remembering what has already been honoured turns "unlimited
43
+ * work for five minutes" into "one". In-process, which is the same scope as the cache it
44
+ * protects: another instance has its own cache and would do its own single purge anyway.
45
+ */
46
+ function makeReplayGuard(windowSeconds) {
47
+ const seen = new Map();
48
+ return function alreadyHonoured(signature) {
49
+ const now = Date.now() / 1000;
50
+ for (const [sig, at] of seen)
51
+ if (now - at > windowSeconds)
52
+ seen.delete(sig);
53
+ if (seen.has(signature))
54
+ return true;
55
+ seen.set(signature, now);
56
+ return false;
57
+ };
58
+ }
59
+ export function createRevalidateRoute(config, options = {}) {
60
+ const tolerance = options.toleranceSeconds ?? TOLERANCE_SECONDS;
61
+ const maxBody = options.maxBodyBytes ?? MAX_BODY_BYTES;
62
+ const alreadyHonoured = makeReplayGuard(tolerance);
63
+ async function POST(request) {
64
+ const secret = options.secret ?? process.env.REVALIDATE_SECRET;
65
+ if (!secret) {
66
+ // Refuse rather than accept unsigned. An unconfigured deployment that quietly accepts
67
+ // anything is an open cache-purge endpoint, which is a free denial of service.
68
+ console.error("revalidate: no secret configured, refusing every delivery");
69
+ return NextResponse.json({ error: "not configured" }, { status: 503 });
70
+ }
71
+ const timestamp = request.headers.get("x-barako-timestamp");
72
+ const signature = request.headers.get("x-barako-signature");
73
+ if (!timestamp || !signature) {
74
+ return NextResponse.json({ error: "unsigned" }, { status: 401 });
75
+ }
76
+ const sent = Number(timestamp);
77
+ if (!Number.isFinite(sent)) {
78
+ return NextResponse.json({ error: "bad timestamp" }, { status: 401 });
79
+ }
80
+ if (Math.abs(Date.now() / 1000 - sent) > tolerance) {
81
+ // Also catches a receiver whose clock has drifted, which looks identical from here.
82
+ return NextResponse.json({ error: "stale" }, { status: 401 });
83
+ }
84
+ // Length before content: the last check that costs nothing.
85
+ const declared = Number(request.headers.get("content-length") ?? "0");
86
+ if (Number.isFinite(declared) && declared > maxBody) {
87
+ return NextResponse.json({ error: "too large" }, { status: 413 });
88
+ }
89
+ const raw = Buffer.from(await request.arrayBuffer());
90
+ if (raw.byteLength > maxBody) {
91
+ // A chunked request declares no length, so the read is bounded again here.
92
+ return NextResponse.json({ error: "too large" }, { status: 413 });
93
+ }
94
+ const material = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), raw]);
95
+ const expected = "sha256=" + createHmac("sha256", secret).update(material).digest("hex");
96
+ if (!sameSignature(expected, signature)) {
97
+ return NextResponse.json({ error: "bad signature" }, { status: 401 });
98
+ }
99
+ if (alreadyHonoured(signature)) {
100
+ // Honest 200: the purge this delivery asked for has already happened, so the CMS has
101
+ // no reason to retry. A 401 here would make a legitimate retry look like an attack.
102
+ return NextResponse.json({ revalidated: true, repeated: true });
103
+ }
104
+ /*
105
+ * No cache warming here, deliberately.
106
+ *
107
+ * An earlier version fetched the main pages after purging, on the theory that Next serves
108
+ * one stale response after a purge and this server should absorb it. Measured against a
109
+ * CMS that logs every read, those fetches caused zero reads and re-rendered nothing:
110
+ * `{ expire: 0 }` already makes the next request a blocking miss, and a page that was
111
+ * never in the warm list was equally fresh for its first reader.
112
+ *
113
+ * It could not have worked in the shipped stack anyway. Caddy sets X-Forwarded-Proto, so
114
+ * the origin resolved to https against the plain HTTP server inside this container, and
115
+ * the failure was swallowed without a log line. Three requests per delivery, latency on
116
+ * the response, and triple the work an attacker gets from one captured signature, for
117
+ * nothing.
118
+ */
119
+ revalidateTag(config.cacheTag, { expire: 0 });
120
+ // The delivery id is echoed only after the signature verified, so an anonymous caller
121
+ // cannot put text of their choosing into this server's log.
122
+ const delivery = request.headers.get("x-barako-delivery") ?? "unknown";
123
+ console.log(`revalidate: dropped tag "${config.cacheTag}" for delivery ${delivery}`);
124
+ return NextResponse.json({ revalidated: true, tag: config.cacheTag, delivery });
125
+ }
126
+ /*
127
+ * GET answers "is this wired up" without a signed request, and reveals nothing: not whether a
128
+ * secret is set, not the tag, not the last delivery. A health check that leaks configuration
129
+ * is how someone learns what to forge.
130
+ */
131
+ async function GET() {
132
+ return NextResponse.json({ ok: true });
133
+ }
134
+ return { POST, GET };
135
+ }
136
+ //# sourceMappingURL=revalidate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidate.js","sourceRoot":"","sources":["../../src/routes/revalidate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { MetadataRoute } from "next";
2
+ import type { PressConfig } from "../config.js";
3
+ export declare function createRobots(config: PressConfig): () => MetadataRoute.Robots;
4
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/routes/robots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAWhD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,SAClB,aAAa,CAAC,MAAM,CAMjD"}
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Generated, because the Sitemap line has to be an absolute URL and the domain is per deployment.
3
+ * A relative Sitemap: is ignored.
4
+ *
5
+ * The URL comes from config rather than process.env read at module scope. That distinction is the
6
+ * whole bug it fixes: this route is prerendered at build, so an env read baked the build
7
+ * machine's value in permanently, and production advertised a localhost sitemap that no cache
8
+ * purge could ever correct.
9
+ */
10
+ export function createRobots(config) {
11
+ return function robots() {
12
+ return {
13
+ rules: { userAgent: "*", allow: "/" },
14
+ sitemap: `${config.site.url}/sitemap.xml`,
15
+ };
16
+ };
17
+ }
18
+ //# sourceMappingURL=robots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/routes/robots.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC5C,OAAO,SAAS,MAAM;QAClB,OAAO;YACH,KAAK,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;YACrC,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,cAAc;SAC5C,CAAC;IACN,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { MetadataRoute } from "next";
2
+ import type { PressConfig } from "../config.js";
3
+ export declare function createSitemap(config: PressConfig): () => Promise<MetadataRoute.Sitemap>;
4
+ //# sourceMappingURL=sitemap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/routes/sitemap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAWhD,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,SACZ,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAqBlE"}