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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { listPosts } from "../cms.js";
|
|
2
|
+
/*
|
|
3
|
+
* Built from the same cached read as every other page, so it costs nothing in the steady state
|
|
4
|
+
* and cannot disagree with what the site serves.
|
|
5
|
+
*
|
|
6
|
+
* A post whose SEO block says noIndex is left out: the CMS resolves that flag per entry and its
|
|
7
|
+
* own sitemap honours it, so a site that ignored it here would contradict the CMS on the one
|
|
8
|
+
* signal an editor set deliberately.
|
|
9
|
+
*/
|
|
10
|
+
export function createSitemap(config) {
|
|
11
|
+
return async function sitemap() {
|
|
12
|
+
let indexable = [];
|
|
13
|
+
try {
|
|
14
|
+
const { posts } = await listPosts(config, { pageSize: config.pageSizes.sitemap });
|
|
15
|
+
indexable = posts.filter((p) => !p.seo?.noIndex);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// A sitemap that throws is worse than a short one: it fails the build when
|
|
19
|
+
// prerendered, and serves a crawler a 500 at runtime.
|
|
20
|
+
indexable = [];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
{ url: config.site.url, changeFrequency: "daily", priority: 1 },
|
|
24
|
+
...indexable.map((p) => ({
|
|
25
|
+
url: `${config.site.url}${config.routes.post}/${p.slug}`,
|
|
26
|
+
lastModified: p.publishedAt ? new Date(p.publishedAt) : undefined,
|
|
27
|
+
changeFrequency: "monthly",
|
|
28
|
+
priority: 0.7,
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../src/routes/sitemap.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAmB;IAC7C,OAAO,KAAK,UAAU,OAAO;QACzB,IAAI,SAAS,GAAmD,EAAE,CAAC;QACnE,IAAI,CAAC;YACD,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;YAClF,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACL,2EAA2E;YAC3E,sDAAsD;YACtD,SAAS,GAAG,EAAE,CAAC;QACnB,CAAC;QAED,OAAO;YACH,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE;YAC/D,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACrB,GAAG,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE;gBACxD,YAAY,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS;gBACjE,eAAe,EAAE,SAAkB;gBACnC,QAAQ,EAAE,GAAG;aAChB,CAAC,CAAC;SACN,CAAC;IACN,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PressConfig } from "../config.js";
|
|
2
|
+
type SlugParams = {
|
|
3
|
+
params: Promise<{
|
|
4
|
+
slug: string;
|
|
5
|
+
}>;
|
|
6
|
+
};
|
|
7
|
+
export declare function createArchive(config: PressConfig, which: "author" | "category"): ({ params }: SlugParams) => Promise<import("react").JSX.Element>;
|
|
8
|
+
/** Slugs for a static export of an archive route. */
|
|
9
|
+
export declare function createArchiveStaticParams(config: PressConfig, which: "author" | "category"): () => Promise<{
|
|
10
|
+
slug: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/screens/archive.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,KAAK,UAAU,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AASxD,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,GAAG,UAAU,gBAC7B,UAAU,0CA0C3D;AAED,qDAAqD;AACrD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,GAAG,UAAU,SACzC,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAe5E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import { getTerm, listPostsBy } from "../cms.js";
|
|
5
|
+
import { renderMarkdown } from "../markdown.js";
|
|
6
|
+
import { Card } from "./blog-index.js";
|
|
7
|
+
/*
|
|
8
|
+
* Posts by author, or by category.
|
|
9
|
+
*
|
|
10
|
+
* One screen for both, because they differ only in which type is resolved and which reference is
|
|
11
|
+
* filtered on, and both of those are in the config. A site with no author type simply never
|
|
12
|
+
* mounts the route; the factory still refuses cleanly if it is mounted anyway.
|
|
13
|
+
*/
|
|
14
|
+
export function createArchive(config, which) {
|
|
15
|
+
return async function ArchivePage({ params }) {
|
|
16
|
+
if (!config.types[which])
|
|
17
|
+
notFound();
|
|
18
|
+
const { slug } = await params;
|
|
19
|
+
const [term, posts] = await Promise.all([
|
|
20
|
+
getTerm(config, which, slug),
|
|
21
|
+
listPostsBy(config, which, slug),
|
|
22
|
+
]);
|
|
23
|
+
if (!term || !posts)
|
|
24
|
+
notFound();
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { className: "meta", children: _jsx(Link, { href: "/", children: "Back" }) }), _jsx("h1", { children: term.name }), term.description && (_jsx("div", { className: "prose", dangerouslySetInnerHTML: { __html: renderMarkdown(term.description) } })), which === "author" && term.website && (_jsx("p", { className: "meta", children: _jsx("a", { href: term.website, rel: "noopener noreferrer", children: term.website }) })), _jsxs("h2", { style: { marginTop: "2.5rem" }, children: [posts.length, " ", posts.length === 1 ? "post" : "posts"] }), posts.map((p) => (_jsx(Card, { config: config, post: p }, p.id)))] }));
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Slugs for a static export of an archive route. */
|
|
29
|
+
export function createArchiveStaticParams(config, which) {
|
|
30
|
+
return async function generateStaticParams() {
|
|
31
|
+
const type = config.types[which];
|
|
32
|
+
if (!type)
|
|
33
|
+
return [];
|
|
34
|
+
const { list } = await import("../delivery.js");
|
|
35
|
+
try {
|
|
36
|
+
const res = await list(config, type, { pageSize: 100 });
|
|
37
|
+
return res.items
|
|
38
|
+
.map((c) => c.slug ?? "")
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((slug) => ({ slug }));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../../src/screens/archive.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PressConfig } from "../config.js";
|
|
2
|
+
import { type Post } from "../cms.js";
|
|
3
|
+
export declare function Card({ config, post, featured, }: {
|
|
4
|
+
config: PressConfig;
|
|
5
|
+
post: Post;
|
|
6
|
+
featured?: boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function createBlogIndex(config: PressConfig): () => Promise<import("react").JSX.Element>;
|
|
9
|
+
//# sourceMappingURL=blog-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-index.d.ts","sourceRoot":"","sources":["../../src/screens/blog-index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAyB,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAiB7D,wBAAgB,IAAI,CAAC,EACjB,MAAM,EACN,IAAI,EACJ,QAAgB,GACnB,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,+BAwCA;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,8CAoDlD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { formatDate, listPosts } from "../cms.js";
|
|
4
|
+
/*
|
|
5
|
+
* The post list.
|
|
6
|
+
*
|
|
7
|
+
* A factory rather than a component, because a Next route is a file and a package cannot write
|
|
8
|
+
* files into someone else's app. The consumer's page.tsx is:
|
|
9
|
+
*
|
|
10
|
+
* import { createBlogIndex } from "barakopress";
|
|
11
|
+
* import { config } from "@/press.config";
|
|
12
|
+
* export default createBlogIndex(config);
|
|
13
|
+
* export const revalidate = 300;
|
|
14
|
+
*
|
|
15
|
+
* `Card` is exported too, so a site that wants its own list but the engine's card can have one
|
|
16
|
+
* without copying the markup.
|
|
17
|
+
*/
|
|
18
|
+
export function Card({ config, post, featured = false, }) {
|
|
19
|
+
return (_jsxs("article", { className: featured ? "card featured-card" : "card", children: [featured && _jsx("span", { className: "chip", children: "Featured" }), _jsx("h2", { children: _jsx(Link, { href: `${config.routes.post}/${post.slug}`, children: post.title }) }), _jsxs("p", { className: "meta", children: [post.publishedAt && (_jsx("time", { dateTime: post.publishedAt, children: formatDate(config, post.publishedAt) })), post.author && config.routes.author && (_jsxs(_Fragment, { children: [" by ", _jsx(Link, { href: `${config.routes.author}/${post.author.slug}`, children: post.author.name })] })), post.category && config.routes.category && (_jsxs(_Fragment, { children: [" in ", _jsx(Link, { href: `${config.routes.category}/${post.category.slug}`, children: post.category.name })] }))] }), post.excerpt && _jsx("p", { className: "excerpt", children: post.excerpt }), post.tags.length > 0 && (_jsx("p", { className: "tags", children: post.tags.map((t) => (_jsx("span", { className: "tag", children: t }, t))) }))] }));
|
|
20
|
+
}
|
|
21
|
+
export function createBlogIndex(config) {
|
|
22
|
+
return async function BlogIndex() {
|
|
23
|
+
let posts = [];
|
|
24
|
+
let failure = null;
|
|
25
|
+
try {
|
|
26
|
+
({ posts } = await listPosts(config));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
// An unreachable CMS is the likeliest thing to be wrong, so it gets a readable page
|
|
30
|
+
// rather than a stack trace. This render is not cached, so the next request retries.
|
|
31
|
+
failure = e instanceof Error ? e.message : String(e);
|
|
32
|
+
}
|
|
33
|
+
const featured = posts.filter((p) => p.featured);
|
|
34
|
+
const rest = posts.filter((p) => !p.featured);
|
|
35
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: "masthead", children: [_jsx("h1", { children: config.site.name }), config.site.tagline && _jsx("p", { className: "tagline", children: config.site.tagline })] }), failure && (_jsxs("div", { className: "notice error", children: [_jsx("p", { children: _jsx("strong", { children: "This page could not be loaded." }) }), _jsx("p", { children: "Please try again shortly." })] })), !failure && posts.length === 0 && (_jsxs("div", { className: "notice", children: [_jsx("p", { children: _jsx("strong", { children: "Nothing published yet." }) }), _jsx("p", { children: "Only published entries of a type opted into public delivery appear here." })] })), featured.map((p) => (_jsx(Card, { config: config, post: p, featured: true }, p.id))), rest.map((p) => (_jsx(Card, { config: config, post: p }, p.id)))] }));
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=blog-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-index.js","sourceRoot":"","sources":["../../src/screens/blog-index.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import type { PressConfig } from "../config.js";
|
|
3
|
+
type SlugParams = {
|
|
4
|
+
params: Promise<{
|
|
5
|
+
slug: string;
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
type PreviewParams = SlugParams & {
|
|
9
|
+
searchParams: Promise<{
|
|
10
|
+
preview?: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export declare function createBlogPost(config: PressConfig): ({ params }: SlugParams) => Promise<import("react").JSX.Element>;
|
|
14
|
+
export declare function createBlogPostPreview(config: PressConfig): ({ params, searchParams }: PreviewParams) => Promise<import("react").JSX.Element>;
|
|
15
|
+
export declare function createPostMetadata(config: PressConfig): ({ params }: SlugParams) => Promise<Metadata>;
|
|
16
|
+
export declare function createPostStaticParams(config: PressConfig): () => Promise<{
|
|
17
|
+
slug: string;
|
|
18
|
+
}[]>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=blog-post.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post.d.ts","sourceRoot":"","sources":["../../src/screens/blog-post.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,KAAK,UAAU,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AACxD,KAAK,aAAa,GAAG,UAAU,GAAG;IAAE,YAAY,EAAE,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AAYlF,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,gBACH,UAAU,0CAMxD;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,8BACW,aAAa,0CAUhF;AASD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,gBACC,UAAU,KAAG,OAAO,CAAC,QAAQ,CAAC,CA8BpF;AASD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,SACR,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAoB5E"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
3
|
+
import { getPost, getPostPreview, listPosts } from "../cms.js";
|
|
4
|
+
import { PostView } from "./post-view.js";
|
|
5
|
+
/*
|
|
6
|
+
* The post page, in two shapes.
|
|
7
|
+
*
|
|
8
|
+
* `createBlogPost` reads only `params`, so it can be prerendered and works under
|
|
9
|
+
* `output: "export"`. `createBlogPostPreview` also reads `searchParams` for a preview token,
|
|
10
|
+
* which forces dynamic rendering: a static export refuses it outright with "couldn't be rendered
|
|
11
|
+
* statically because it used searchParams". A static site takes the first and gives up draft
|
|
12
|
+
* preview, which is the honest trade and the reason these are two exports rather than a flag.
|
|
13
|
+
*/
|
|
14
|
+
export function createBlogPost(config) {
|
|
15
|
+
return async function PostPage({ params }) {
|
|
16
|
+
const { slug } = await params;
|
|
17
|
+
const post = await getPost(config, slug);
|
|
18
|
+
if (!post)
|
|
19
|
+
notFound();
|
|
20
|
+
return _jsx(PostView, { config: config, post: post });
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createBlogPostPreview(config) {
|
|
24
|
+
return async function PostPreviewPage({ params, searchParams }) {
|
|
25
|
+
const { slug } = await params;
|
|
26
|
+
const { preview } = await searchParams;
|
|
27
|
+
// A token routes to the uncached read, so a draft never enters the shared cache.
|
|
28
|
+
const post = preview ? await getPostPreview(config, slug, preview) : await getPost(config, slug);
|
|
29
|
+
if (!post)
|
|
30
|
+
notFound();
|
|
31
|
+
return _jsx(PostView, { config: config, post: post, preview: Boolean(preview) });
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/*
|
|
35
|
+
* Metadata from the API's resolved `seo` block rather than assembled here.
|
|
36
|
+
*
|
|
37
|
+
* A type opted into SEO fields returns title, description, canonical, social image and noIndex on
|
|
38
|
+
* every public response, with the title already falling back to the entry's title. So this maps,
|
|
39
|
+
* and an editor changing the meta description in the console changes the page.
|
|
40
|
+
*/
|
|
41
|
+
export function createPostMetadata(config) {
|
|
42
|
+
return async function generateMetadata({ params }) {
|
|
43
|
+
const { slug } = await params;
|
|
44
|
+
const post = await getPost(config, slug);
|
|
45
|
+
if (!post)
|
|
46
|
+
return { title: "Not found" };
|
|
47
|
+
const seo = post.seo;
|
|
48
|
+
const title = seo?.title ?? post.title;
|
|
49
|
+
const description = seo?.description ?? post.excerpt;
|
|
50
|
+
const image = seo?.imageUrl ?? post.coverImage;
|
|
51
|
+
return {
|
|
52
|
+
title,
|
|
53
|
+
description,
|
|
54
|
+
alternates: seo?.canonicalUrl ? { canonical: seo.canonicalUrl } : undefined,
|
|
55
|
+
robots: seo?.noIndex ? { index: false, follow: false } : undefined,
|
|
56
|
+
openGraph: {
|
|
57
|
+
title,
|
|
58
|
+
description,
|
|
59
|
+
type: "article",
|
|
60
|
+
publishedTime: post.publishedAt,
|
|
61
|
+
images: image ? [image] : undefined,
|
|
62
|
+
},
|
|
63
|
+
twitter: {
|
|
64
|
+
card: image ? "summary_large_image" : "summary",
|
|
65
|
+
title,
|
|
66
|
+
description,
|
|
67
|
+
images: image ? [image] : undefined,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/*
|
|
73
|
+
* Slugs for a static export, paged to the end rather than to one hardcoded limit.
|
|
74
|
+
*
|
|
75
|
+
* Every consumer of an export-mode site needs this, and every one that hand-writes it caps the
|
|
76
|
+
* page size and silently drops the posts past it. Doing it once here is the difference between an
|
|
77
|
+
* engine and a snippet in a README.
|
|
78
|
+
*/
|
|
79
|
+
export function createPostStaticParams(config) {
|
|
80
|
+
return async function generateStaticParams() {
|
|
81
|
+
const slugs = [];
|
|
82
|
+
const pageSize = 100;
|
|
83
|
+
for (let page = 1; page <= 200; page++) {
|
|
84
|
+
let batch;
|
|
85
|
+
try {
|
|
86
|
+
batch = await listPosts(config, { page, pageSize });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A build with no CMS reachable produces no routes. Under `output: "export"` Next
|
|
90
|
+
// then refuses the build, which is the correct outcome: a static site with no
|
|
91
|
+
// content is not something to ship quietly.
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
for (const p of batch.posts)
|
|
95
|
+
if (p.slug)
|
|
96
|
+
slugs.push({ slug: p.slug });
|
|
97
|
+
if (!batch.hasNextPage)
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
return slugs;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=blog-post.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post.js","sourceRoot":"","sources":["../../src/screens/blog-post.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PressConfig } from "../config.js";
|
|
2
|
+
import { type Post } from "../cms.js";
|
|
3
|
+
export declare function PostView({ config, post, preview, }: {
|
|
4
|
+
config: PressConfig;
|
|
5
|
+
post: Post;
|
|
6
|
+
preview?: boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=post-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-view.d.ts","sourceRoot":"","sources":["../../src/screens/post-view.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAclD,wBAAgB,QAAQ,CAAC,EACrB,MAAM,EACN,IAAI,EACJ,OAAe,GAClB,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB,+BA2DA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { formatDate } from "../cms.js";
|
|
4
|
+
import { renderMarkdown } from "../markdown.js";
|
|
5
|
+
/*
|
|
6
|
+
* The rendering, shared by the static screen and the preview screen.
|
|
7
|
+
*
|
|
8
|
+
* It is its own component because reading `searchParams` forces a route to render dynamically,
|
|
9
|
+
* and a site built with `output: "export"` is refused outright for it. Preview needs the query
|
|
10
|
+
* string; a published post does not. Splitting the fetch from the render is what lets one set of
|
|
11
|
+
* markup serve a static site and a server-rendered one.
|
|
12
|
+
*
|
|
13
|
+
* Every link is built from `config.routes`, so a site that mounts posts at /writing gets /writing
|
|
14
|
+
* links here, in the feed and in the sitemap, instead of three files disagreeing.
|
|
15
|
+
*/
|
|
16
|
+
export function PostView({ config, post, preview = false, }) {
|
|
17
|
+
return (_jsxs("article", { children: [preview && (_jsx("div", { className: "notice preview-banner", children: "Preview. This is how the post will look. It is not published, and it is served uncached so nothing here reaches another reader." })), _jsx("p", { className: "meta", children: _jsx(Link, { href: "/", children: "Back" }) }), _jsx("h1", { children: post.title }), _jsxs("p", { className: "meta", children: [post.publishedAt && (_jsx("time", { dateTime: post.publishedAt, children: formatDate(config, post.publishedAt) })), post.author && config.routes.author && (_jsxs(_Fragment, { children: [" by ", _jsx(Link, { href: `${config.routes.author}/${post.author.slug}`, children: post.author.name })] })), post.author && !config.routes.author && _jsx(_Fragment, { children: ` by ${post.author.name}` }), post.category && config.routes.category && (_jsxs(_Fragment, { children: [" in ", _jsx(Link, { href: `${config.routes.category}/${post.category.slug}`, children: post.category.name })] }))] }), post.coverImage && (_jsx("img", { className: "cover", src: post.coverImage, alt: post.coverImageAlt ?? "" })), _jsx("div", { className: "prose", dangerouslySetInnerHTML: { __html: renderMarkdown(post.body) } }), post.tags.length > 0 && (_jsx("p", { className: "tags", children: post.tags.map((t) => (_jsx("span", { className: "tag", children: t }, t))) }))] }));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=post-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-view.js","sourceRoot":"","sources":["../../src/screens/post-view.tsx"],"names":[],"mappings":";AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAa,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAC,EACrB,MAAM,EACN,IAAI,EACJ,OAAO,GAAG,KAAK,GAKlB;IACG,OAAO,CACH,8BACK,OAAO,IAAI,CACR,cAAK,SAAS,EAAC,uBAAuB,gJAGhC,CACT,EAED,YAAG,SAAS,EAAC,MAAM,YACf,KAAC,IAAI,IAAC,IAAI,EAAC,GAAG,qBAAY,GAC1B,EAEJ,uBAAK,IAAI,CAAC,KAAK,GAAM,EAErB,aAAG,SAAS,EAAC,MAAM,aACd,IAAI,CAAC,WAAW,IAAI,CACjB,eAAM,QAAQ,EAAE,IAAI,CAAC,WAAW,YAAG,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,GAAQ,CAClF,EACA,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CACpC,8BACK,MAAM,EACP,KAAC,IAAI,IAAC,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,YACpD,IAAI,CAAC,MAAM,CAAC,IAAI,GACd,IACR,CACN,EACA,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,4BAAG,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAI,EACxE,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,CACxC,8BACK,MAAM,EACP,KAAC,IAAI,IAAC,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,YACxD,IAAI,CAAC,QAAQ,CAAC,IAAI,GAChB,IACR,CACN,IACD,EAEH,IAAI,CAAC,UAAU,IAAI,CAIhB,cAAK,SAAS,EAAC,OAAO,EAAC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC,aAAa,IAAI,EAAE,GAAI,CACjF,EAED,cAAK,SAAS,EAAC,OAAO,EAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAI,EAExF,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CACrB,YAAG,SAAS,EAAC,MAAM,YACd,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAClB,eAAc,SAAS,EAAC,KAAK,YACxB,CAAC,IADK,CAAC,CAEL,CACV,CAAC,GACF,CACP,IACK,CACb,CAAC;AACN,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "barakopress",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A configurable blog engine for barakoCMS: Next.js server components, cached until the CMS says otherwise.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Arnel Robles",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./styles.css": "./src/styles.css",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.9.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build:package": "tsc -p tsconfig.build.json",
|
|
29
|
+
"watch": "tsc -p tsconfig.build.json --watch",
|
|
30
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
31
|
+
"dev": "npm run build:package && next dev",
|
|
32
|
+
"build": "npm run build:package && next build",
|
|
33
|
+
"start": "next start",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"prepare": "npm run build:package"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"marked": "^18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"next": ">=16",
|
|
43
|
+
"react": ">=19"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^26.5.0",
|
|
47
|
+
"@types/react": "^19.0.0",
|
|
48
|
+
"@types/react-dom": "^19.0.0",
|
|
49
|
+
"next": "^16.0.0",
|
|
50
|
+
"react": "^19.0.0",
|
|
51
|
+
"react-dom": "^19.0.0",
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
|
+
"vitest": "^5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"barakocms",
|
|
57
|
+
"headless-cms",
|
|
58
|
+
"blog",
|
|
59
|
+
"blog-engine",
|
|
60
|
+
"nextjs",
|
|
61
|
+
"react-server-components",
|
|
62
|
+
"rss",
|
|
63
|
+
"sitemap"
|
|
64
|
+
],
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/BaryoDev/barakoPress.git"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/BaryoDev/barakoPress#readme",
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/BaryoDev/barakoPress/issues"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/cms.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { PressConfig } from "./config.js";
|
|
2
|
+
import { includesFor } from "./config.js";
|
|
3
|
+
import { bySlug, bySlugPreview, list, type PublicContent, type Seo } from "./delivery.js";
|
|
4
|
+
|
|
5
|
+
export type { Seo };
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Mapping a stored entry onto what a page renders.
|
|
9
|
+
*
|
|
10
|
+
* Every field name comes from the config's field map, so a client whose post type calls its title
|
|
11
|
+
* `Headline` configures that instead of forking this file. An absent name means the site has no
|
|
12
|
+
* such field: the value is undefined and the screens leave the slot out rather than render it
|
|
13
|
+
* empty.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface Post {
|
|
17
|
+
id: string;
|
|
18
|
+
slug: string;
|
|
19
|
+
title: string;
|
|
20
|
+
excerpt?: string;
|
|
21
|
+
body: string;
|
|
22
|
+
publishedAt?: string;
|
|
23
|
+
coverImage?: string;
|
|
24
|
+
coverImageAlt?: string;
|
|
25
|
+
featured: boolean;
|
|
26
|
+
tags: string[];
|
|
27
|
+
author?: Ref;
|
|
28
|
+
category?: Ref;
|
|
29
|
+
seo?: Seo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Ref {
|
|
33
|
+
id: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
name: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
39
|
+
|
|
40
|
+
/** Reads a field by its configured name, or undefined when the site has no such field. */
|
|
41
|
+
function field(data: Record<string, unknown>, name: string | undefined): unknown {
|
|
42
|
+
return name ? data[name] : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* A resolved reference. `include` gives back the whole target entry, so this handles that and a
|
|
47
|
+
* bare id, and gives up rather than inventing a label when neither a name nor a slug is there.
|
|
48
|
+
*/
|
|
49
|
+
function toRef(config: PressConfig, v: unknown): Ref | undefined {
|
|
50
|
+
if (!v || typeof v !== "object") return undefined;
|
|
51
|
+
const d = v as Record<string, unknown>;
|
|
52
|
+
const data = (d.data ?? d) as Record<string, unknown>;
|
|
53
|
+
const name = str(data.Name) || str(data.Title) || str(field(data, config.fields.title));
|
|
54
|
+
const slug = str(d.slug) || str(field(data, config.fields.slug)) || str(data.Slug);
|
|
55
|
+
if (!name && !slug) return undefined;
|
|
56
|
+
return { id: str(d.id), slug, name: name || slug };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function toPost(config: PressConfig, c: PublicContent): Post {
|
|
60
|
+
const d = c.data;
|
|
61
|
+
const f = config.fields;
|
|
62
|
+
const tags = field(d, f.tags);
|
|
63
|
+
return {
|
|
64
|
+
id: c.id,
|
|
65
|
+
slug: c.slug ?? str(field(d, f.slug)),
|
|
66
|
+
title: str(field(d, f.title)) || "Untitled",
|
|
67
|
+
excerpt: str(field(d, f.excerpt)) || undefined,
|
|
68
|
+
body: str(field(d, f.body)),
|
|
69
|
+
publishedAt: str(field(d, f.publishedAt)) || c.createdAt || undefined,
|
|
70
|
+
coverImage: str(field(d, f.coverImage)) || undefined,
|
|
71
|
+
coverImageAlt: str(field(d, f.coverImageAlt)) || undefined,
|
|
72
|
+
featured: field(d, f.featured) === true,
|
|
73
|
+
tags: Array.isArray(tags) ? tags.filter((t): t is string => typeof t === "string") : [],
|
|
74
|
+
author: toRef(config, field(d, f.author)),
|
|
75
|
+
category: toRef(config, field(d, f.category)),
|
|
76
|
+
seo: c.seo ?? undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* Ordering is asked of the API, not done here.
|
|
82
|
+
*
|
|
83
|
+
* Sorting the page that came back only orders those rows, so on a blog past one page the "newest"
|
|
84
|
+
* list is newest-of-an-arbitrary-page. The API can order every row, so it does.
|
|
85
|
+
*/
|
|
86
|
+
function sortKey(config: PressConfig): string | undefined {
|
|
87
|
+
return config.fields.publishedAt ? `-${config.fields.publishedAt}` : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function listPosts(
|
|
91
|
+
config: PressConfig,
|
|
92
|
+
opts: { page?: number; pageSize?: number } = {},
|
|
93
|
+
): Promise<{ posts: Post[]; total: number; hasNextPage: boolean }> {
|
|
94
|
+
const res = await list(config, config.types.post, {
|
|
95
|
+
page: opts.page ?? 1,
|
|
96
|
+
pageSize: opts.pageSize ?? config.pageSizes.index,
|
|
97
|
+
include: includesFor(config),
|
|
98
|
+
sort: sortKey(config),
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
posts: res.items.map((c) => toPost(config, c)),
|
|
102
|
+
total: res.totalItems,
|
|
103
|
+
hasNextPage: res.hasNextPage,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getPost(config: PressConfig, slug: string): Promise<Post | null> {
|
|
108
|
+
const c = await bySlug(config, config.types.post, slug);
|
|
109
|
+
return c ? toPost(config, c) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getPostPreview(
|
|
113
|
+
config: PressConfig,
|
|
114
|
+
slug: string,
|
|
115
|
+
token: string,
|
|
116
|
+
): Promise<Post | null> {
|
|
117
|
+
const c = await bySlugPreview(config, config.types.post, slug, token);
|
|
118
|
+
return c ? toPost(config, c) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
* Filtering by a reference takes the target's id, not its slug, so an archive is two calls:
|
|
123
|
+
* resolve the slug, then filter. Both are cached and tagged, so it costs two reads once.
|
|
124
|
+
*/
|
|
125
|
+
async function idForSlug(config: PressConfig, type: string, slug: string): Promise<string | null> {
|
|
126
|
+
const c = await bySlug(config, type, slug);
|
|
127
|
+
return c?.id ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Posts by author or category. Null when the site has no such type, or the slug is unknown. */
|
|
131
|
+
export async function listPostsBy(
|
|
132
|
+
config: PressConfig,
|
|
133
|
+
which: "author" | "category",
|
|
134
|
+
slug: string,
|
|
135
|
+
): Promise<Post[] | null> {
|
|
136
|
+
const type = config.types[which];
|
|
137
|
+
const fieldName = config.fields[which];
|
|
138
|
+
if (!type || !fieldName) return null;
|
|
139
|
+
|
|
140
|
+
const id = await idForSlug(config, type, slug);
|
|
141
|
+
if (!id) return null;
|
|
142
|
+
|
|
143
|
+
const res = await list(config, config.types.post, {
|
|
144
|
+
pageSize: config.pageSizes.archive,
|
|
145
|
+
include: includesFor(config),
|
|
146
|
+
filter: [[fieldName, "eq", id]],
|
|
147
|
+
sort: sortKey(config),
|
|
148
|
+
});
|
|
149
|
+
return res.items.map((c) => toPost(config, c));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface Term {
|
|
153
|
+
id: string;
|
|
154
|
+
slug: string;
|
|
155
|
+
name: string;
|
|
156
|
+
description?: string;
|
|
157
|
+
photo?: string;
|
|
158
|
+
website?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* An author or category entry, for the heading of its archive page.
|
|
163
|
+
*
|
|
164
|
+
* These secondary types keep the blueprint's names with the post type's title and slug as
|
|
165
|
+
* fallbacks. One flat field map cannot describe three types, and a model whose author type
|
|
166
|
+
* differs wholesale wants its own screen. That is a fair boundary for the engine, and it is
|
|
167
|
+
* stated here so nobody discovers it by reading the source.
|
|
168
|
+
*/
|
|
169
|
+
export async function getTerm(
|
|
170
|
+
config: PressConfig,
|
|
171
|
+
which: "author" | "category",
|
|
172
|
+
slug: string,
|
|
173
|
+
): Promise<Term | null> {
|
|
174
|
+
const type = config.types[which];
|
|
175
|
+
if (!type) return null;
|
|
176
|
+
const c = await bySlug(config, type, slug);
|
|
177
|
+
if (!c) return null;
|
|
178
|
+
|
|
179
|
+
const d = c.data;
|
|
180
|
+
return {
|
|
181
|
+
id: c.id,
|
|
182
|
+
slug: c.slug ?? (str(field(d, config.fields.slug)) || str(d.Slug)),
|
|
183
|
+
name: str(d.Name) || str(d.Title) || str(field(d, config.fields.title)) || "Untitled",
|
|
184
|
+
description: str(d.Description) || str(d.Bio) || undefined,
|
|
185
|
+
photo: str(d.Photo) || undefined,
|
|
186
|
+
website: str(d.Website) || undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatDate(config: PressConfig, value?: string): string {
|
|
191
|
+
if (!value) return "";
|
|
192
|
+
const d = new Date(value);
|
|
193
|
+
return Number.isNaN(d.getTime())
|
|
194
|
+
? ""
|
|
195
|
+
: d.toLocaleDateString(config.locale, { day: "numeric", month: "long", year: "numeric" });
|
|
196
|
+
}
|