@replohq/sdk 0.11.0 → 0.14.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/_vendor/replo-utils/lib/json.mjs +2 -42
- package/_vendor/replo-utils/lib/misc.mjs +3 -0
- package/_vendor/schemas/contentCollection.d.ts +231 -0
- package/_vendor/schemas/contentCollection.mjs +2 -0
- package/_vendor/schemas/contentEntry.d.ts +53 -0
- package/_vendor/schemas/contentEntry.mjs +3 -0
- package/_vendor/schemas/contentFrontmatter.d.ts +18 -0
- package/_vendor/schemas/contentManifest.d.ts +44 -0
- package/_vendor/schemas/contentManifest.mjs +197 -0
- package/_vendor/schemas/generated/consent.d.ts +2 -2
- package/_vendor/schemas/money.mjs +2 -0
- package/_vendor/schemas/routing/locale.mjs +2 -0
- package/_vendor/schemas/routing/rules.mjs +2 -0
- package/analytics/get-analytics-sinks.js +5 -1
- package/analytics/get-analytics-sinks.js.map +2 -2
- package/analytics/sinks/converge-sink.d.ts +21 -0
- package/analytics/sinks/converge-sink.js +51 -0
- package/analytics/sinks/converge-sink.js.map +7 -0
- package/analytics/sinks/northbeam-sink.d.ts +20 -0
- package/analytics/sinks/northbeam-sink.js +41 -0
- package/analytics/sinks/northbeam-sink.js.map +7 -0
- package/cart/cart-actions.js +13 -10
- package/cart/cart-actions.js.map +2 -2
- package/cart/cart-provider.js +5 -4
- package/cart/cart-provider.js.map +2 -2
- package/cart/gateways/cart-constants.d.ts +14 -0
- package/cart/gateways/cart-constants.js +23 -1
- package/cart/gateways/cart-constants.js.map +2 -2
- package/cart/utils/cart-utils.d.ts +8 -0
- package/cart/utils/cart-utils.js +13 -1
- package/cart/utils/cart-utils.js.map +2 -2
- package/cart/utils/cookie-cart-persistence.js +4 -21
- package/cart/utils/cookie-cart-persistence.js.map +2 -2
- package/chunk-AL3F36PH.mjs +1514 -0
- package/chunk-GV5FY7ZZ.mjs +89 -0
- package/chunk-I23U2PUI.mjs +151 -0
- package/chunk-IQ5GRTXO.mjs +182 -0
- package/chunk-VKO7P3ED.mjs +41 -0
- package/consent/script-snippets.js +19 -1
- package/consent/script-snippets.js.map +2 -2
- package/consent/types.d.ts +2 -2
- package/content.d.ts +126 -0
- package/content.js +594 -0
- package/content.js.map +7 -0
- package/lib/buildMetadata.js +3 -3
- package/package.json +12 -4
package/content.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { ContentManifest } from "./_vendor/schemas/contentManifest";
|
|
5
|
+
import { contentCollectionFieldSchema } from "./_vendor/schemas/contentCollection";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/** Public name for the shared collection-field contract. */
|
|
8
|
+
export type ContentField = z.infer<typeof contentCollectionFieldSchema>;
|
|
9
|
+
export interface ContentEntry {
|
|
10
|
+
collection: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
};
|
|
15
|
+
fields: ContentField[];
|
|
16
|
+
slug: string;
|
|
17
|
+
body: string;
|
|
18
|
+
values: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** One entry's slot in a collection listing. */
|
|
21
|
+
export interface ContentCollectionEntry {
|
|
22
|
+
slug: string;
|
|
23
|
+
title: string;
|
|
24
|
+
excerpt: string | null;
|
|
25
|
+
coverImage: string | null;
|
|
26
|
+
coverImageAlt: string | null;
|
|
27
|
+
publishedAt: string | null;
|
|
28
|
+
status: "draft" | "published";
|
|
29
|
+
}
|
|
30
|
+
export interface ContentCollection {
|
|
31
|
+
slug: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
entries: ContentCollectionEntry[];
|
|
35
|
+
}
|
|
36
|
+
/** An ordered content field paired with this entry's value for it. */
|
|
37
|
+
export interface ContentFieldWithValue {
|
|
38
|
+
field: ContentField;
|
|
39
|
+
value: unknown;
|
|
40
|
+
}
|
|
41
|
+
export interface ContentSitemapEntry {
|
|
42
|
+
url: string;
|
|
43
|
+
lastModified?: Date;
|
|
44
|
+
}
|
|
45
|
+
export interface ContentSource {
|
|
46
|
+
read(relativePath: string): Promise<string | null>;
|
|
47
|
+
listCollections(): Promise<string[]>;
|
|
48
|
+
listEntries(collection: string): Promise<string[]>;
|
|
49
|
+
}
|
|
50
|
+
interface ContentOptions {
|
|
51
|
+
contentSource?: ContentSource;
|
|
52
|
+
contentDirectory?: string;
|
|
53
|
+
contentManifest?: ContentManifest;
|
|
54
|
+
}
|
|
55
|
+
export declare function createContentSource({ contentDirectory, contentManifest, }?: {
|
|
56
|
+
contentDirectory?: string;
|
|
57
|
+
contentManifest?: ContentManifest;
|
|
58
|
+
}): ContentSource;
|
|
59
|
+
export declare function getContentEntry({ collection, slug, ...contentOptions }: {
|
|
60
|
+
collection: string;
|
|
61
|
+
slug: string;
|
|
62
|
+
} & ContentOptions): Promise<ContentEntry>;
|
|
63
|
+
/** Static params for one collection's `[slug]` detail route. */
|
|
64
|
+
export declare function getContentCollectionStaticParams({ collection, ...contentOptions }: {
|
|
65
|
+
collection: string;
|
|
66
|
+
} & ContentOptions): Promise<{
|
|
67
|
+
slug: string;
|
|
68
|
+
}[]>;
|
|
69
|
+
/**
|
|
70
|
+
* A collection and its renderable entries for a listing page. Uses the
|
|
71
|
+
* not-found contract when the collection is missing or archived.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getContentCollection({ collection, ...contentOptions }: {
|
|
74
|
+
collection: string;
|
|
75
|
+
} & ContentOptions): Promise<ContentCollection>;
|
|
76
|
+
/**
|
|
77
|
+
* Per-entry head metadata (title/description/OpenGraph/Twitter) from the
|
|
78
|
+
* entry's SEO fields with content fallbacks. Returns an empty object when the
|
|
79
|
+
* entry is not renderable, so `generateMetadata` can call it unconditionally.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getContentEntryMetadata({ collection, slug, ...contentOptions }: {
|
|
82
|
+
collection: string;
|
|
83
|
+
slug: string;
|
|
84
|
+
} & ContentOptions): Promise<Metadata>;
|
|
85
|
+
/**
|
|
86
|
+
* Structured data for an entry as a JSON-LD script tag. Serialization is
|
|
87
|
+
* script-safe (`<` escaped) so entry content can never break out of the tag.
|
|
88
|
+
* The type is a deliberately neutral `WebPage`; collections have no schema
|
|
89
|
+
* type of their own.
|
|
90
|
+
*/
|
|
91
|
+
export declare function ContentEntryJsonLd({ entry }: {
|
|
92
|
+
entry: ContentEntry;
|
|
93
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
94
|
+
/**
|
|
95
|
+
* Absolute sitemap entries for every collection listing and renderable entry.
|
|
96
|
+
* `origin` comes from the caller (e.g. the request's platform-validated Host)
|
|
97
|
+
* because site origins are not fixed at build time.
|
|
98
|
+
*/
|
|
99
|
+
export declare function getContentSitemapEntries({ origin, ...contentOptions }: {
|
|
100
|
+
origin: string;
|
|
101
|
+
} & ContentOptions): Promise<ContentSitemapEntry[]>;
|
|
102
|
+
/**
|
|
103
|
+
* The full sitemap XML document for a site's content (home, collection
|
|
104
|
+
* listings, entries). Kept here so the generated sitemap route stays a tiny,
|
|
105
|
+
* restylable-free shell around one call.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getContentSitemapXml({ origin, ...contentOptions }: {
|
|
108
|
+
origin: string;
|
|
109
|
+
} & ContentOptions): Promise<string>;
|
|
110
|
+
/**
|
|
111
|
+
* The entry's renderable content fields in collection-settings order, paired
|
|
112
|
+
* with values. This is the published half of the shared ordering contract in
|
|
113
|
+
* `schemas/contentCollection`; empty values are omitted so templates never render
|
|
114
|
+
* bare labels.
|
|
115
|
+
*/
|
|
116
|
+
export declare function getContentFields(entry: ContentEntry): ContentFieldWithValue[];
|
|
117
|
+
export declare function ContentFieldValue({ field, value, }: {
|
|
118
|
+
field: ContentField;
|
|
119
|
+
value: unknown;
|
|
120
|
+
}): ReactNode;
|
|
121
|
+
/** A related entry resolved to a public link; unresolvable refs stay plain text. */
|
|
122
|
+
export interface ContentRelatedLink {
|
|
123
|
+
title: string;
|
|
124
|
+
href: string;
|
|
125
|
+
}
|
|
126
|
+
export {};
|
package/content.js
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "server-only";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Fragment } from "react";
|
|
6
|
+
import { notFound } from "next/navigation";
|
|
7
|
+
import ReactMarkdown from "react-markdown";
|
|
8
|
+
import remarkGfm from "remark-gfm";
|
|
9
|
+
import { parseJsonOrNull } from "./_vendor/replo-utils/lib/json.mjs";
|
|
10
|
+
import { isNotNullish } from "./_vendor/replo-utils/lib/misc.mjs";
|
|
11
|
+
import {
|
|
12
|
+
CONTENT_COLLECTION_SCHEMA_FILENAME,
|
|
13
|
+
CONTENT_DIRNAME,
|
|
14
|
+
CONTENT_ENTRY_FILE_EXTENSION,
|
|
15
|
+
CONTENT_RELATED_VALUE_STORAGE_KEY,
|
|
16
|
+
contentCollectionFileSchema,
|
|
17
|
+
getPublishedContentFields,
|
|
18
|
+
isContentEntryPubliclyVisible,
|
|
19
|
+
isSafeContentUrl,
|
|
20
|
+
isValidContentSlug,
|
|
21
|
+
parseContentEntryRef
|
|
22
|
+
} from "./_vendor/schemas/contentCollection.mjs";
|
|
23
|
+
import {
|
|
24
|
+
getContentEntryFieldValidationIssues,
|
|
25
|
+
getContentRenderableEntryValues,
|
|
26
|
+
parseContentEntryDocument
|
|
27
|
+
} from "./_vendor/schemas/contentEntry.mjs";
|
|
28
|
+
import {
|
|
29
|
+
getContentManifestCollectionSlugs,
|
|
30
|
+
getContentManifestEntrySlugs
|
|
31
|
+
} from "./_vendor/schemas/contentManifest.mjs";
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
function createContentSource({
|
|
34
|
+
contentDirectory = getDefaultContentDirectory(),
|
|
35
|
+
contentManifest
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (contentManifest) {
|
|
38
|
+
return {
|
|
39
|
+
read: async (relativePath) => {
|
|
40
|
+
return contentManifest.files[relativePath] ?? null;
|
|
41
|
+
},
|
|
42
|
+
listCollections: async () => {
|
|
43
|
+
return getContentManifestCollectionSlugs(contentManifest);
|
|
44
|
+
},
|
|
45
|
+
listEntries: async (collection) => {
|
|
46
|
+
return getContentManifestEntrySlugs({ collection, contentManifest });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
read: (relativePath) => {
|
|
52
|
+
return readTextFileOrNull(join(contentDirectory, relativePath));
|
|
53
|
+
},
|
|
54
|
+
listCollections: async () => {
|
|
55
|
+
return (await readDirectoryOrEmpty(contentDirectory)).flatMap((entry) => {
|
|
56
|
+
return entry.isDirectory() ? [entry.name] : [];
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
listEntries: async (collection) => {
|
|
60
|
+
return (await readDirectoryOrEmpty(join(contentDirectory, collection))).flatMap((entry) => {
|
|
61
|
+
return entry.isFile() && entry.name.endsWith(CONTENT_ENTRY_FILE_EXTENSION) ? [entry.name.slice(0, -CONTENT_ENTRY_FILE_EXTENSION.length)] : [];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function getContentEntry({
|
|
67
|
+
collection,
|
|
68
|
+
slug,
|
|
69
|
+
...contentOptions
|
|
70
|
+
}) {
|
|
71
|
+
const entry = await findContentEntry({
|
|
72
|
+
collection,
|
|
73
|
+
slug,
|
|
74
|
+
contentSource: getContentSource(contentOptions)
|
|
75
|
+
});
|
|
76
|
+
if (!entry) {
|
|
77
|
+
notFound();
|
|
78
|
+
}
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
async function getContentCollectionStaticParams({
|
|
82
|
+
collection,
|
|
83
|
+
...contentOptions
|
|
84
|
+
}) {
|
|
85
|
+
const contentSource = getContentSource(contentOptions);
|
|
86
|
+
const collectionSchema = await readActiveCollectionSchema({
|
|
87
|
+
collection,
|
|
88
|
+
contentSource
|
|
89
|
+
});
|
|
90
|
+
if (!collectionSchema) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const entries = await listVisibleEntries({
|
|
94
|
+
collectionSchema,
|
|
95
|
+
collection,
|
|
96
|
+
contentSource
|
|
97
|
+
});
|
|
98
|
+
return entries.map((entry) => entry.slug).sort().map((slug) => ({ slug }));
|
|
99
|
+
}
|
|
100
|
+
async function getContentCollection({
|
|
101
|
+
collection,
|
|
102
|
+
...contentOptions
|
|
103
|
+
}) {
|
|
104
|
+
const loaded = await findContentCollection({
|
|
105
|
+
collection,
|
|
106
|
+
contentSource: getContentSource(contentOptions)
|
|
107
|
+
});
|
|
108
|
+
if (!loaded) {
|
|
109
|
+
notFound();
|
|
110
|
+
}
|
|
111
|
+
return loaded;
|
|
112
|
+
}
|
|
113
|
+
async function getContentEntryMetadata({
|
|
114
|
+
collection,
|
|
115
|
+
slug,
|
|
116
|
+
...contentOptions
|
|
117
|
+
}) {
|
|
118
|
+
const entry = await findContentEntry({
|
|
119
|
+
collection,
|
|
120
|
+
slug,
|
|
121
|
+
contentSource: getContentSource(contentOptions)
|
|
122
|
+
});
|
|
123
|
+
if (!entry) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
const title = readNonEmptyString(entry.values.seoTitle) ?? readNonEmptyString(entry.values.title) ?? entry.collection.name;
|
|
127
|
+
const description = readNonEmptyString(entry.values.seoDescription) ?? readNonEmptyString(entry.values.excerpt);
|
|
128
|
+
const image = readSafeImageUrl(entry.values.coverImage);
|
|
129
|
+
return {
|
|
130
|
+
title,
|
|
131
|
+
...description === null ? {} : { description },
|
|
132
|
+
openGraph: {
|
|
133
|
+
title,
|
|
134
|
+
...description === null ? {} : { description },
|
|
135
|
+
type: "article",
|
|
136
|
+
...image === null ? {} : { images: [image] }
|
|
137
|
+
},
|
|
138
|
+
twitter: {
|
|
139
|
+
card: image === null ? "summary" : "summary_large_image",
|
|
140
|
+
title,
|
|
141
|
+
...description === null ? {} : { description },
|
|
142
|
+
...image === null ? {} : { images: [image] }
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function ContentEntryJsonLd({ entry }) {
|
|
147
|
+
const title = readNonEmptyString(entry.values.seoTitle) ?? readNonEmptyString(entry.values.title) ?? entry.collection.name;
|
|
148
|
+
const description = readNonEmptyString(entry.values.seoDescription) ?? readNonEmptyString(entry.values.excerpt);
|
|
149
|
+
const image = readSafeImageUrl(entry.values.coverImage);
|
|
150
|
+
const datePublished = readIsoDate(entry.values.publishedAt);
|
|
151
|
+
const dateModified = readIsoDate(entry.values.updatedAt);
|
|
152
|
+
const jsonLd = {
|
|
153
|
+
"@context": "https://schema.org",
|
|
154
|
+
"@type": "WebPage",
|
|
155
|
+
name: title,
|
|
156
|
+
...description === null ? {} : { description },
|
|
157
|
+
...image === null ? {} : { image },
|
|
158
|
+
...datePublished === null ? {} : { datePublished },
|
|
159
|
+
...dateModified === null ? {} : { dateModified }
|
|
160
|
+
};
|
|
161
|
+
return /* @__PURE__ */ jsx(
|
|
162
|
+
"script",
|
|
163
|
+
{
|
|
164
|
+
type: "application/ld+json",
|
|
165
|
+
dangerouslySetInnerHTML: { __html: serializeJsonLd(jsonLd) }
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
async function getContentSitemapEntries({
|
|
170
|
+
origin,
|
|
171
|
+
...contentOptions
|
|
172
|
+
}) {
|
|
173
|
+
const base = origin.replace(/\/+$/u, "");
|
|
174
|
+
const entries = [];
|
|
175
|
+
const contentSource = getContentSource(contentOptions);
|
|
176
|
+
for (const collection of await listCollectionSlugs({ contentSource })) {
|
|
177
|
+
const collectionSchema = await readActiveCollectionSchema({
|
|
178
|
+
collection,
|
|
179
|
+
contentSource
|
|
180
|
+
});
|
|
181
|
+
if (!collectionSchema) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (collectionSchema.hasListingPage !== false) {
|
|
185
|
+
entries.push({ url: `${base}/${collection}` });
|
|
186
|
+
}
|
|
187
|
+
const visibleEntries = await listVisibleEntries({
|
|
188
|
+
collectionSchema,
|
|
189
|
+
collection,
|
|
190
|
+
contentSource
|
|
191
|
+
});
|
|
192
|
+
visibleEntries.sort((left, right) => left.slug.localeCompare(right.slug));
|
|
193
|
+
for (const entry of visibleEntries) {
|
|
194
|
+
const lastModified = readValidDate(entry.values.updatedAt);
|
|
195
|
+
entries.push({
|
|
196
|
+
url: `${base}/${collection}/${entry.slug}`,
|
|
197
|
+
...lastModified === null ? {} : { lastModified }
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return entries;
|
|
202
|
+
}
|
|
203
|
+
async function getContentSitemapXml({
|
|
204
|
+
origin,
|
|
205
|
+
...contentOptions
|
|
206
|
+
}) {
|
|
207
|
+
const base = origin.replace(/\/+$/u, "");
|
|
208
|
+
const entries = [
|
|
209
|
+
{ url: base },
|
|
210
|
+
...await getContentSitemapEntries({
|
|
211
|
+
origin: base,
|
|
212
|
+
contentSource: getContentSource(contentOptions)
|
|
213
|
+
})
|
|
214
|
+
];
|
|
215
|
+
const items = entries.map(({ url, lastModified }) => {
|
|
216
|
+
const lastmod = lastModified ? `<lastmod>${lastModified.toISOString()}</lastmod>` : "";
|
|
217
|
+
return `<url><loc>${escapeXml(url)}</loc>${lastmod}</url>`;
|
|
218
|
+
});
|
|
219
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
220
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${items.join(
|
|
221
|
+
""
|
|
222
|
+
)}</urlset>
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
function getContentFields(entry) {
|
|
226
|
+
return getPublishedContentFields(entry.fields).map((field) => ({ field, value: entry.values[field.id] })).filter(({ value }) => {
|
|
227
|
+
return value !== void 0 && value !== null && value !== "";
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function ContentFieldValue({
|
|
231
|
+
field,
|
|
232
|
+
value
|
|
233
|
+
}) {
|
|
234
|
+
if (field.type === "rich-text" && typeof value === "string") {
|
|
235
|
+
return /* @__PURE__ */ jsx(SafeMarkdown, { source: value });
|
|
236
|
+
}
|
|
237
|
+
if (field.type === "image" && typeof value === "string") {
|
|
238
|
+
return /* @__PURE__ */ jsx(
|
|
239
|
+
"img",
|
|
240
|
+
{
|
|
241
|
+
alt: field.label,
|
|
242
|
+
src: value,
|
|
243
|
+
style: { display: "block", height: "auto", maxWidth: "100%" }
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (field.type === "url" && typeof value === "string") {
|
|
248
|
+
return /* @__PURE__ */ jsx("a", { href: value, style: CONTENT_LINK_STYLE, children: value });
|
|
249
|
+
}
|
|
250
|
+
if (field.type === "date" && typeof value === "string") {
|
|
251
|
+
const parsed = new Date(value);
|
|
252
|
+
const label = Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString("en-US", {
|
|
253
|
+
year: "numeric",
|
|
254
|
+
month: "long",
|
|
255
|
+
day: "numeric"
|
|
256
|
+
});
|
|
257
|
+
return /* @__PURE__ */ jsx("time", { dateTime: value, children: label });
|
|
258
|
+
}
|
|
259
|
+
if (field.type === "boolean" && typeof value === "boolean") {
|
|
260
|
+
return value ? "Yes" : "No";
|
|
261
|
+
}
|
|
262
|
+
if (field.type === "related" && Array.isArray(value)) {
|
|
263
|
+
const items = value.filter(
|
|
264
|
+
(item) => isContentRelatedLink(item) || typeof item === "string"
|
|
265
|
+
);
|
|
266
|
+
if (items.length === 0) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return items.map((item, index) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
270
|
+
index > 0 && ", ",
|
|
271
|
+
isContentRelatedLink(item) ? /* @__PURE__ */ jsx("a", { href: item.href, style: CONTENT_LINK_STYLE, children: item.title }) : String(item)
|
|
272
|
+
] }, isContentRelatedLink(item) ? item.href : String(item)));
|
|
273
|
+
}
|
|
274
|
+
if (field.type === "tags" && Array.isArray(value)) {
|
|
275
|
+
return value.join(", ");
|
|
276
|
+
}
|
|
277
|
+
return typeof value === "string" ? value : null;
|
|
278
|
+
}
|
|
279
|
+
async function findContentCollection({
|
|
280
|
+
collection,
|
|
281
|
+
contentSource
|
|
282
|
+
}) {
|
|
283
|
+
const collectionSchema = await readActiveCollectionSchema({
|
|
284
|
+
collection,
|
|
285
|
+
contentSource
|
|
286
|
+
});
|
|
287
|
+
if (!collectionSchema) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const visibleEntries = await listVisibleEntries({
|
|
291
|
+
collectionSchema,
|
|
292
|
+
collection,
|
|
293
|
+
contentSource
|
|
294
|
+
});
|
|
295
|
+
const entries = visibleEntries.map((entry) => {
|
|
296
|
+
return {
|
|
297
|
+
slug: entry.slug,
|
|
298
|
+
title: readNonEmptyString(entry.values.title) ?? entry.slug,
|
|
299
|
+
excerpt: readNonEmptyString(entry.values.excerpt),
|
|
300
|
+
coverImage: readSafeImageUrl(entry.values.coverImage),
|
|
301
|
+
coverImageAlt: readNonEmptyString(entry.values.coverImageAlt),
|
|
302
|
+
publishedAt: readIsoDate(entry.values.publishedAt),
|
|
303
|
+
status: entry.values.status === "draft" ? "draft" : "published"
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
entries.sort((left, right) => {
|
|
307
|
+
const leftTime = left.publishedAt ? Date.parse(left.publishedAt) : 0;
|
|
308
|
+
const rightTime = right.publishedAt ? Date.parse(right.publishedAt) : 0;
|
|
309
|
+
return rightTime - leftTime || left.title.localeCompare(right.title);
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
slug: collection,
|
|
313
|
+
name: collectionSchema.name,
|
|
314
|
+
description: collectionSchema.description,
|
|
315
|
+
entries
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function findContentEntry({
|
|
319
|
+
collection,
|
|
320
|
+
slug,
|
|
321
|
+
contentSource
|
|
322
|
+
}) {
|
|
323
|
+
const collectionSchema = await readActiveCollectionSchema({
|
|
324
|
+
collection,
|
|
325
|
+
contentSource
|
|
326
|
+
});
|
|
327
|
+
if (!collectionSchema) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return readVisibleEntry({
|
|
331
|
+
collectionSchema,
|
|
332
|
+
collection,
|
|
333
|
+
slug,
|
|
334
|
+
contentSource
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
async function listCollectionSlugs({
|
|
338
|
+
contentSource
|
|
339
|
+
}) {
|
|
340
|
+
return (await contentSource.listCollections()).filter(isValidContentSlug).sort();
|
|
341
|
+
}
|
|
342
|
+
async function listVisibleEntries({
|
|
343
|
+
collectionSchema,
|
|
344
|
+
collection,
|
|
345
|
+
contentSource
|
|
346
|
+
}) {
|
|
347
|
+
const slugs = await contentSource.listEntries(collection);
|
|
348
|
+
const entries = await Promise.all(
|
|
349
|
+
slugs.map((slug) => {
|
|
350
|
+
return readVisibleEntry({
|
|
351
|
+
collectionSchema,
|
|
352
|
+
collection,
|
|
353
|
+
slug,
|
|
354
|
+
contentSource
|
|
355
|
+
});
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
return entries.filter(isNotNullish);
|
|
359
|
+
}
|
|
360
|
+
async function readActiveCollectionSchema({
|
|
361
|
+
collection,
|
|
362
|
+
contentSource
|
|
363
|
+
}) {
|
|
364
|
+
if (!isValidContentSlug(collection)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const rawSchema = await contentSource.read(
|
|
368
|
+
`${collection}/${CONTENT_COLLECTION_SCHEMA_FILENAME}`
|
|
369
|
+
);
|
|
370
|
+
if (!rawSchema) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const parsedJson = parseJsonOrNull(rawSchema);
|
|
374
|
+
if (parsedJson === null) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const result = contentCollectionFileSchema.safeParse(parsedJson);
|
|
378
|
+
if (!result.success || isNotNullish(result.data.archivedAt)) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return result.data;
|
|
382
|
+
}
|
|
383
|
+
async function readVisibleEntryDocument({
|
|
384
|
+
collection,
|
|
385
|
+
slug,
|
|
386
|
+
contentSource
|
|
387
|
+
}) {
|
|
388
|
+
if (!isValidContentSlug(slug)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const rawEntry = await contentSource.read(
|
|
392
|
+
`${collection}/${slug}${CONTENT_ENTRY_FILE_EXTENSION}`
|
|
393
|
+
);
|
|
394
|
+
if (!rawEntry) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const parsedDocument = parseContentEntryDocument(rawEntry);
|
|
398
|
+
if (parsedDocument.status === "invalid") {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const document = parsedDocument.document;
|
|
402
|
+
const allowDrafts = process.env.NODE_ENV === "development";
|
|
403
|
+
if (!isContentEntryPubliclyVisible({
|
|
404
|
+
status: document.values.status,
|
|
405
|
+
archivedAt: document.values.archivedAt,
|
|
406
|
+
allowDrafts
|
|
407
|
+
})) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return document;
|
|
411
|
+
}
|
|
412
|
+
async function readVisibleEntry({
|
|
413
|
+
collectionSchema,
|
|
414
|
+
collection,
|
|
415
|
+
slug,
|
|
416
|
+
contentSource
|
|
417
|
+
}) {
|
|
418
|
+
const document = await readVisibleEntryDocument({
|
|
419
|
+
collection,
|
|
420
|
+
slug,
|
|
421
|
+
contentSource
|
|
422
|
+
});
|
|
423
|
+
if (!document) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const values = getContentRenderableEntryValues({
|
|
427
|
+
document,
|
|
428
|
+
slug
|
|
429
|
+
});
|
|
430
|
+
if (getContentEntryFieldValidationIssues({
|
|
431
|
+
fields: collectionSchema.fields,
|
|
432
|
+
values,
|
|
433
|
+
requireRequiredFields: document.values.status !== "draft"
|
|
434
|
+
}).length > 0) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const fields = [...collectionSchema.fields];
|
|
438
|
+
const hasRelatedField = fields.some((field) => field.type === "related");
|
|
439
|
+
const storedRelated = values[CONTENT_RELATED_VALUE_STORAGE_KEY];
|
|
440
|
+
if (!hasRelatedField && Array.isArray(storedRelated) && storedRelated.length > 0) {
|
|
441
|
+
fields.push({
|
|
442
|
+
id: CONTENT_RELATED_VALUE_STORAGE_KEY,
|
|
443
|
+
label: "Related content",
|
|
444
|
+
type: "related",
|
|
445
|
+
required: false,
|
|
446
|
+
enabled: true,
|
|
447
|
+
builtin: true
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
collection: {
|
|
452
|
+
name: collectionSchema.name,
|
|
453
|
+
description: collectionSchema.description,
|
|
454
|
+
slug: collection
|
|
455
|
+
},
|
|
456
|
+
fields,
|
|
457
|
+
slug,
|
|
458
|
+
body: document.body,
|
|
459
|
+
values: await resolveRelatedFieldValues({
|
|
460
|
+
fields,
|
|
461
|
+
values,
|
|
462
|
+
contentSource
|
|
463
|
+
})
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function resolveRelatedFieldValues({
|
|
467
|
+
fields,
|
|
468
|
+
values,
|
|
469
|
+
contentSource
|
|
470
|
+
}) {
|
|
471
|
+
const relatedFields = fields.filter((field) => field.type === "related");
|
|
472
|
+
if (relatedFields.length === 0) {
|
|
473
|
+
return values;
|
|
474
|
+
}
|
|
475
|
+
const resolved = { ...values };
|
|
476
|
+
for (const field of relatedFields) {
|
|
477
|
+
const raw = resolved[field.id];
|
|
478
|
+
if (!Array.isArray(raw)) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const links = [];
|
|
482
|
+
for (const item of raw) {
|
|
483
|
+
if (typeof item !== "string") {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const ref = parseContentEntryRef(item);
|
|
487
|
+
if (!ref) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const { collectionSlug: targetCollection, entrySlug: targetSlug } = ref;
|
|
491
|
+
const collectionSchema = await readActiveCollectionSchema({
|
|
492
|
+
collection: targetCollection,
|
|
493
|
+
contentSource
|
|
494
|
+
});
|
|
495
|
+
if (!collectionSchema) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const target = await readVisibleEntryDocument({
|
|
499
|
+
collection: targetCollection,
|
|
500
|
+
slug: targetSlug,
|
|
501
|
+
contentSource
|
|
502
|
+
});
|
|
503
|
+
if (!target) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
links.push({
|
|
507
|
+
title: readNonEmptyString(target.values.title) ?? targetSlug,
|
|
508
|
+
href: `/${targetCollection}/${targetSlug}`
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
resolved[field.id] = links;
|
|
512
|
+
}
|
|
513
|
+
return resolved;
|
|
514
|
+
}
|
|
515
|
+
const contentRelatedLinkSchema = z.object({
|
|
516
|
+
title: z.string(),
|
|
517
|
+
href: z.string()
|
|
518
|
+
});
|
|
519
|
+
function isContentRelatedLink(value) {
|
|
520
|
+
return contentRelatedLinkSchema.safeParse(value).success;
|
|
521
|
+
}
|
|
522
|
+
const CONTENT_LINK_STYLE = { textDecoration: "underline" };
|
|
523
|
+
function SafeMarkdown({ source }) {
|
|
524
|
+
return /* @__PURE__ */ jsx(
|
|
525
|
+
ReactMarkdown,
|
|
526
|
+
{
|
|
527
|
+
remarkPlugins: [remarkGfm],
|
|
528
|
+
skipHtml: true,
|
|
529
|
+
components: {
|
|
530
|
+
a: ({ children, ...props }) => /* @__PURE__ */ jsx("a", { ...props, style: CONTENT_LINK_STYLE, children })
|
|
531
|
+
},
|
|
532
|
+
children: source
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
function readNonEmptyString(value) {
|
|
537
|
+
const parsed = z.string().safeParse(value);
|
|
538
|
+
return parsed.success && parsed.data.trim().length > 0 ? parsed.data : null;
|
|
539
|
+
}
|
|
540
|
+
function readSafeImageUrl(value) {
|
|
541
|
+
const url = readNonEmptyString(value);
|
|
542
|
+
return url !== null && isSafeContentUrl(url, { image: true }) ? url : null;
|
|
543
|
+
}
|
|
544
|
+
function readIsoDate(value) {
|
|
545
|
+
const raw = readNonEmptyString(value);
|
|
546
|
+
return raw !== null && !Number.isNaN(Date.parse(raw)) ? raw : null;
|
|
547
|
+
}
|
|
548
|
+
function readValidDate(value) {
|
|
549
|
+
const iso = readIsoDate(value);
|
|
550
|
+
return iso === null ? null : new Date(iso);
|
|
551
|
+
}
|
|
552
|
+
function serializeJsonLd(value) {
|
|
553
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
554
|
+
}
|
|
555
|
+
function escapeXml(value) {
|
|
556
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
557
|
+
}
|
|
558
|
+
async function readTextFileOrNull(path) {
|
|
559
|
+
try {
|
|
560
|
+
return await readFile(path, "utf8");
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function readDirectoryOrEmpty(path) {
|
|
566
|
+
try {
|
|
567
|
+
return await readdir(path, { withFileTypes: true });
|
|
568
|
+
} catch {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function getContentSource({
|
|
573
|
+
contentSource,
|
|
574
|
+
contentDirectory,
|
|
575
|
+
contentManifest
|
|
576
|
+
}) {
|
|
577
|
+
return contentSource ?? createContentSource({ contentDirectory, contentManifest });
|
|
578
|
+
}
|
|
579
|
+
function getDefaultContentDirectory() {
|
|
580
|
+
return join(process.cwd(), CONTENT_DIRNAME);
|
|
581
|
+
}
|
|
582
|
+
export {
|
|
583
|
+
ContentEntryJsonLd,
|
|
584
|
+
ContentFieldValue,
|
|
585
|
+
createContentSource,
|
|
586
|
+
getContentCollection,
|
|
587
|
+
getContentCollectionStaticParams,
|
|
588
|
+
getContentEntry,
|
|
589
|
+
getContentEntryMetadata,
|
|
590
|
+
getContentFields,
|
|
591
|
+
getContentSitemapEntries,
|
|
592
|
+
getContentSitemapXml
|
|
593
|
+
};
|
|
594
|
+
//# sourceMappingURL=content.js.map
|