@glw907/cairn-cms 0.6.0 → 0.8.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/dist/components/ConceptList.svelte +8 -4
- package/dist/components/ConceptList.svelte.d.ts.map +1 -1
- package/dist/components/EditPage.svelte +5 -5
- package/dist/components/EditPage.svelte.d.ts +3 -1
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/content/compose.d.ts +2 -2
- package/dist/content/compose.d.ts.map +1 -1
- package/dist/content/compose.js +3 -3
- package/dist/content/concepts.d.ts +7 -6
- package/dist/content/concepts.d.ts.map +1 -1
- package/dist/content/concepts.js +13 -5
- package/dist/content/ids.d.ts +14 -0
- package/dist/content/ids.d.ts.map +1 -1
- package/dist/content/ids.js +40 -0
- package/dist/content/permalink.d.ts +12 -0
- package/dist/content/permalink.d.ts.map +1 -0
- package/dist/content/permalink.js +30 -0
- package/dist/content/types.d.ts +23 -3
- package/dist/content/types.d.ts.map +1 -1
- package/dist/delivery/content-index.d.ts +47 -0
- package/dist/delivery/content-index.d.ts.map +1 -0
- package/dist/delivery/content-index.js +86 -0
- package/dist/delivery/excerpt.d.ts +11 -0
- package/dist/delivery/excerpt.d.ts.map +1 -0
- package/dist/delivery/excerpt.js +38 -0
- package/dist/delivery/feeds.d.ts +27 -0
- package/dist/delivery/feeds.d.ts.map +1 -0
- package/dist/delivery/feeds.js +80 -0
- package/dist/delivery/paginate.d.ts +13 -0
- package/dist/delivery/paginate.d.ts.map +1 -0
- package/dist/delivery/paginate.js +20 -0
- package/dist/delivery/robots.d.ts +6 -0
- package/dist/delivery/robots.d.ts.map +1 -0
- package/dist/delivery/robots.js +10 -0
- package/dist/delivery/seo.d.ts +34 -0
- package/dist/delivery/seo.d.ts.map +1 -0
- package/dist/delivery/seo.js +46 -0
- package/dist/delivery/site-index.d.ts +28 -0
- package/dist/delivery/site-index.d.ts.map +1 -0
- package/dist/delivery/site-index.js +38 -0
- package/dist/delivery/sitemap.d.ts +8 -0
- package/dist/delivery/sitemap.d.ts.map +1 -0
- package/dist/delivery/sitemap.js +21 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/nav/site-config.d.ts +5 -0
- package/dist/nav/site-config.d.ts.map +1 -1
- package/dist/nav/site-config.js +4 -0
- package/dist/sveltekit/content-routes.d.ts.map +1 -1
- package/dist/sveltekit/content-routes.js +18 -8
- package/dist/sveltekit/index.d.ts +2 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/public-routes.d.ts +50 -0
- package/dist/sveltekit/public-routes.d.ts.map +1 -0
- package/dist/sveltekit/public-routes.js +45 -0
- package/package.json +1 -1
- package/src/lib/components/ConceptList.svelte +8 -4
- package/src/lib/components/EditPage.svelte +5 -5
- package/src/lib/content/compose.ts +4 -3
- package/src/lib/content/concepts.ts +15 -5
- package/src/lib/content/ids.ts +44 -0
- package/src/lib/content/permalink.ts +40 -0
- package/src/lib/content/types.ts +21 -4
- package/src/lib/delivery/content-index.ts +130 -0
- package/src/lib/delivery/excerpt.ts +41 -0
- package/src/lib/delivery/feeds.ts +112 -0
- package/src/lib/delivery/paginate.ts +32 -0
- package/src/lib/delivery/robots.ts +10 -0
- package/src/lib/delivery/seo.ts +72 -0
- package/src/lib/delivery/site-index.ts +68 -0
- package/src/lib/delivery/sitemap.ts +29 -0
- package/src/lib/index.ts +35 -1
- package/src/lib/nav/site-config.ts +8 -0
- package/src/lib/sveltekit/content-routes.ts +17 -7
- package/src/lib/sveltekit/index.ts +8 -0
- package/src/lib/sveltekit/public-routes.ts +83 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// cairn-cms: RSS and JSON Feed builders (public-delivery design). Pure functions over a
|
|
2
|
+
// channel and a list of items, so they unit-test without a render or a network. The caller
|
|
3
|
+
// (a template +server.ts shim) assembles items from the content index and passes absolute
|
|
4
|
+
// URLs built from PUBLIC_ORIGIN.
|
|
5
|
+
function escapeXml(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"');
|
|
11
|
+
}
|
|
12
|
+
/** Make a string safe inside a CDATA section by splitting any `]]>` across two sections. */
|
|
13
|
+
function cdataSafe(value) {
|
|
14
|
+
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
15
|
+
}
|
|
16
|
+
/** Format a YYYY-MM-DD (or ISO) string as an RFC-822 date in UTC, as RSS wants. */
|
|
17
|
+
function rfc822(date) {
|
|
18
|
+
return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toUTCString();
|
|
19
|
+
}
|
|
20
|
+
/** Format a YYYY-MM-DD (or ISO) string as an ISO-8601 instant in UTC. */
|
|
21
|
+
function iso(date) {
|
|
22
|
+
return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toISOString();
|
|
23
|
+
}
|
|
24
|
+
/** Build an RSS 2.0 document. */
|
|
25
|
+
export function buildRssFeed(channel, items) {
|
|
26
|
+
const entries = items
|
|
27
|
+
.map((item) => {
|
|
28
|
+
const content = item.contentHtml ?? item.summary;
|
|
29
|
+
return [
|
|
30
|
+
' <item>',
|
|
31
|
+
` <title>${escapeXml(item.title)}</title>`,
|
|
32
|
+
` <link>${escapeXml(item.url)}</link>`,
|
|
33
|
+
` <guid isPermaLink="true">${escapeXml(item.url)}</guid>`,
|
|
34
|
+
` <pubDate>${rfc822(item.date)}</pubDate>`,
|
|
35
|
+
` <description>${escapeXml(item.summary)}</description>`,
|
|
36
|
+
// CDATA cannot contain `]]>`, so split that one sequence rather than escape the body.
|
|
37
|
+
` <content:encoded><![CDATA[${cdataSafe(content)}]]></content:encoded>`,
|
|
38
|
+
' </item>',
|
|
39
|
+
].join('\n');
|
|
40
|
+
})
|
|
41
|
+
.join('\n');
|
|
42
|
+
return [
|
|
43
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
44
|
+
'<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
45
|
+
' <channel>',
|
|
46
|
+
` <title>${escapeXml(channel.title)}</title>`,
|
|
47
|
+
` <link>${escapeXml(channel.siteUrl)}</link>`,
|
|
48
|
+
` <description>${escapeXml(channel.description)}</description>`,
|
|
49
|
+
channel.language ? ` <language>${escapeXml(channel.language)}</language>` : '',
|
|
50
|
+
` <atom:link href="${escapeXml(channel.feedUrl)}" rel="self" type="application/rss+xml" />`,
|
|
51
|
+
entries,
|
|
52
|
+
' </channel>',
|
|
53
|
+
'</rss>',
|
|
54
|
+
'',
|
|
55
|
+
]
|
|
56
|
+
.filter((line) => line !== '')
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** Build a JSON Feed 1.1 document. */
|
|
60
|
+
export function buildJsonFeed(channel, items) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
version: 'https://jsonfeed.org/version/1.1',
|
|
63
|
+
title: channel.title,
|
|
64
|
+
description: channel.description,
|
|
65
|
+
home_page_url: channel.siteUrl,
|
|
66
|
+
feed_url: channel.feedUrl,
|
|
67
|
+
...(channel.language ? { language: channel.language } : {}),
|
|
68
|
+
...(channel.author ? { authors: [channel.author] } : {}),
|
|
69
|
+
items: items.map((item) => ({
|
|
70
|
+
id: item.url,
|
|
71
|
+
url: item.url,
|
|
72
|
+
title: item.title,
|
|
73
|
+
summary: item.summary,
|
|
74
|
+
date_published: iso(item.date),
|
|
75
|
+
...(item.updated ? { date_modified: iso(item.updated) } : {}),
|
|
76
|
+
...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
|
|
77
|
+
...(item.tags && item.tags.length ? { tags: item.tags } : {}),
|
|
78
|
+
})),
|
|
79
|
+
}, null, 2);
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A page of items plus its navigation state. */
|
|
2
|
+
export interface Page<T> {
|
|
3
|
+
items: T[];
|
|
4
|
+
page: number;
|
|
5
|
+
perPage: number;
|
|
6
|
+
total: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
hasPrev: boolean;
|
|
9
|
+
hasNext: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
|
|
12
|
+
export declare function paginate<T>(items: T[], page: number, perPage: number): Page<T>;
|
|
13
|
+
//# sourceMappingURL=paginate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paginate.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/paginate.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,MAAM,WAAW,IAAI,CAAC,CAAC;IACrB,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAgB9E"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// cairn-cms: pagination helper (public-delivery design). Pure slice math; the template renders
|
|
2
|
+
// the controls. An out-of-range page clamps into bounds.
|
|
3
|
+
/** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
|
|
4
|
+
export function paginate(items, page, perPage) {
|
|
5
|
+
const total = items.length;
|
|
6
|
+
// A non-positive page size would make totalPages Infinity, so clamp it to one.
|
|
7
|
+
const size = Math.max(1, Math.floor(perPage) || 1);
|
|
8
|
+
const totalPages = Math.max(1, Math.ceil(total / size));
|
|
9
|
+
const current = Math.min(Math.max(1, Math.floor(page) || 1), totalPages);
|
|
10
|
+
const start = (current - 1) * size;
|
|
11
|
+
return {
|
|
12
|
+
items: items.slice(start, start + size),
|
|
13
|
+
page: current,
|
|
14
|
+
perPage: size,
|
|
15
|
+
total,
|
|
16
|
+
totalPages,
|
|
17
|
+
hasPrev: current > 1,
|
|
18
|
+
hasNext: current < totalPages,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/robots.ts"],"names":[],"mappings":"AAGA,+BAA+B;AAC/B,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,CAKrF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// cairn-cms: robots.txt builder (public-delivery design). A permissive default that points
|
|
2
|
+
// at the sitemap, with optional disallow rules.
|
|
3
|
+
/** Build a robots.txt body. */
|
|
4
|
+
export function buildRobots(opts) {
|
|
5
|
+
const lines = ['User-agent: *', 'Allow: /'];
|
|
6
|
+
for (const path of opts.disallow ?? [])
|
|
7
|
+
lines.push(`Disallow: ${path}`);
|
|
8
|
+
lines.push('', `Sitemap: ${opts.sitemapUrl}`, '');
|
|
9
|
+
return lines.join('\n');
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Inputs for the head. All URLs are absolute (built from PUBLIC_ORIGIN). */
|
|
2
|
+
export interface SeoInput {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
canonicalUrl: string;
|
|
6
|
+
siteName: string;
|
|
7
|
+
type?: 'website' | 'article';
|
|
8
|
+
published?: string;
|
|
9
|
+
modified?: string;
|
|
10
|
+
feeds?: {
|
|
11
|
+
rss?: string;
|
|
12
|
+
json?: string;
|
|
13
|
+
};
|
|
14
|
+
image?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Plain-data head: a title, meta tags, link tags, and one JSON-LD object. */
|
|
17
|
+
export interface SeoMeta {
|
|
18
|
+
title: string;
|
|
19
|
+
meta: {
|
|
20
|
+
name?: string;
|
|
21
|
+
property?: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}[];
|
|
24
|
+
links: {
|
|
25
|
+
rel: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
href: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
}[];
|
|
30
|
+
jsonLd: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
/** Build the head data for a page. */
|
|
33
|
+
export declare function buildSeoMeta(input: SeoInput): SeoMeta;
|
|
34
|
+
//# sourceMappingURL=seo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/seo.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,sCAAsC;AACtC,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CA6CrD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// cairn-cms: the SEO head builder (public-delivery design, decision 6). Returns plain data so
|
|
2
|
+
// the template renders it inside <svelte:head>. It covers the universal, mechanical tags;
|
|
3
|
+
// og:image art and richer JSON-LD types stay a template or plugin concern.
|
|
4
|
+
/** Build the head data for a page. */
|
|
5
|
+
export function buildSeoMeta(input) {
|
|
6
|
+
const type = input.type ?? 'website';
|
|
7
|
+
const meta = [
|
|
8
|
+
{ name: 'description', content: input.description },
|
|
9
|
+
{ property: 'og:title', content: input.title },
|
|
10
|
+
{ property: 'og:description', content: input.description },
|
|
11
|
+
{ property: 'og:type', content: type },
|
|
12
|
+
{ property: 'og:url', content: input.canonicalUrl },
|
|
13
|
+
{ property: 'og:site_name', content: input.siteName },
|
|
14
|
+
{ name: 'twitter:card', content: input.image ? 'summary_large_image' : 'summary' },
|
|
15
|
+
];
|
|
16
|
+
if (input.image) {
|
|
17
|
+
meta.push({ property: 'og:image', content: input.image });
|
|
18
|
+
meta.push({ name: 'twitter:image', content: input.image });
|
|
19
|
+
}
|
|
20
|
+
const links = [{ rel: 'canonical', href: input.canonicalUrl }];
|
|
21
|
+
if (input.feeds?.rss) {
|
|
22
|
+
links.push({ rel: 'alternate', type: 'application/rss+xml', href: input.feeds.rss, title: input.siteName });
|
|
23
|
+
}
|
|
24
|
+
if (input.feeds?.json) {
|
|
25
|
+
links.push({ rel: 'alternate', type: 'application/feed+json', href: input.feeds.json, title: input.siteName });
|
|
26
|
+
}
|
|
27
|
+
const jsonLd = type === 'article'
|
|
28
|
+
? {
|
|
29
|
+
'@context': 'https://schema.org',
|
|
30
|
+
'@type': 'Article',
|
|
31
|
+
headline: input.title,
|
|
32
|
+
description: input.description,
|
|
33
|
+
url: input.canonicalUrl,
|
|
34
|
+
...(input.published ? { datePublished: input.published } : {}),
|
|
35
|
+
...(input.modified ? { dateModified: input.modified } : {}),
|
|
36
|
+
...(input.image ? { image: input.image } : {}),
|
|
37
|
+
}
|
|
38
|
+
: {
|
|
39
|
+
'@context': 'https://schema.org',
|
|
40
|
+
'@type': 'WebSite',
|
|
41
|
+
name: input.siteName,
|
|
42
|
+
description: input.description,
|
|
43
|
+
url: input.canonicalUrl,
|
|
44
|
+
};
|
|
45
|
+
return { title: input.title, meta, links, jsonLd };
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
|
+
import type { ContentEntry, ContentIndex, ContentSummary } from './content-index.js';
|
|
3
|
+
/** One concept's descriptor paired with its built index. */
|
|
4
|
+
export interface ConceptIndex {
|
|
5
|
+
descriptor: ConceptDescriptor;
|
|
6
|
+
index: ContentIndex;
|
|
7
|
+
}
|
|
8
|
+
/** The cross-concept query surface a catch-all route and the sitemap read. */
|
|
9
|
+
export interface SiteIndex {
|
|
10
|
+
/** Resolve a request path (with or without a trailing slash) to its entry, or undefined. */
|
|
11
|
+
byPermalink(path: string): ContentEntry | undefined;
|
|
12
|
+
/** Newer/older neighbors within the entry's own concept, for prev/next links. */
|
|
13
|
+
adjacent(entry: ContentSummary): {
|
|
14
|
+
newer?: ContentSummary;
|
|
15
|
+
older?: ContentSummary;
|
|
16
|
+
};
|
|
17
|
+
/** Every entry's path across concepts, leading slash stripped, for SvelteKit `[...path]` prerender. */
|
|
18
|
+
entries(): {
|
|
19
|
+
path: string;
|
|
20
|
+
}[];
|
|
21
|
+
/** One concept's index, for its archive, tag, and feed loaders. */
|
|
22
|
+
concept(id: string): ContentIndex | undefined;
|
|
23
|
+
/** Every non-draft summary across concepts, for the site-wide sitemap. */
|
|
24
|
+
all(): ContentSummary[];
|
|
25
|
+
}
|
|
26
|
+
/** Union per-concept indexes into a site-level resolver; throw on a duplicate permalink. */
|
|
27
|
+
export declare function createSiteIndex(concepts: ConceptIndex[]): SiteIndex;
|
|
28
|
+
//# sourceMappingURL=site-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAErF,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,4FAA4F;IAC5F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACpF,uGAAuG;IACvG,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC9C,0EAA0E;IAC1E,GAAG,IAAI,cAAc,EAAE,CAAC;CACzB;AAOD,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,SAAS,CAkCnE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Strip a trailing slash from a path, keeping the root "/" intact. */
|
|
2
|
+
function normalizePath(path) {
|
|
3
|
+
return path.length > 1 ? path.replace(/\/+$/, '') : path;
|
|
4
|
+
}
|
|
5
|
+
/** Union per-concept indexes into a site-level resolver; throw on a duplicate permalink. */
|
|
6
|
+
export function createSiteIndex(concepts) {
|
|
7
|
+
const byPath = new Map();
|
|
8
|
+
const byId = new Map();
|
|
9
|
+
for (const { descriptor, index } of concepts) {
|
|
10
|
+
byId.set(descriptor.id, index);
|
|
11
|
+
for (const summary of index.all()) {
|
|
12
|
+
const existing = byPath.get(summary.permalink);
|
|
13
|
+
if (existing) {
|
|
14
|
+
throw new Error(`site index: "${existing.id}" and "${summary.id}" both resolve to "${summary.permalink}"`);
|
|
15
|
+
}
|
|
16
|
+
byPath.set(summary.permalink, { index, id: summary.id });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
byPermalink(path) {
|
|
21
|
+
const hit = byPath.get(normalizePath(path));
|
|
22
|
+
return hit ? hit.index.byId(hit.id) : undefined;
|
|
23
|
+
},
|
|
24
|
+
adjacent(entry) {
|
|
25
|
+
const hit = byPath.get(entry.permalink);
|
|
26
|
+
return hit ? hit.index.adjacent(entry.id) : {};
|
|
27
|
+
},
|
|
28
|
+
entries() {
|
|
29
|
+
return [...byPath.keys()].map((p) => ({ path: p.replace(/^\//, '') }));
|
|
30
|
+
},
|
|
31
|
+
concept(id) {
|
|
32
|
+
return byId.get(id);
|
|
33
|
+
},
|
|
34
|
+
all() {
|
|
35
|
+
return concepts.flatMap(({ index }) => index.all());
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** One sitemap URL. `lastmod` is a YYYY-MM-DD date. */
|
|
2
|
+
export interface SitemapUrl {
|
|
3
|
+
loc: string;
|
|
4
|
+
lastmod?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Build a sitemap XML document from a list of URLs. */
|
|
7
|
+
export declare function buildSitemap(urls: SitemapUrl[]): string;
|
|
8
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/sitemap.ts"],"names":[],"mappings":"AAGA,uDAAuD;AACvD,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CAcvD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// cairn-cms: sitemap builder (public-delivery design). Pure over a URL list; the caller
|
|
2
|
+
// derives the list from the content index and the routable concepts.
|
|
3
|
+
function escapeXml(value) {
|
|
4
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
+
}
|
|
6
|
+
/** Build a sitemap XML document from a list of URLs. */
|
|
7
|
+
export function buildSitemap(urls) {
|
|
8
|
+
const entries = urls
|
|
9
|
+
.map((url) => {
|
|
10
|
+
const lastmod = url.lastmod ? `\n <lastmod>${escapeXml(url.lastmod)}</lastmod>` : '';
|
|
11
|
+
return ` <url>\n <loc>${escapeXml(url.loc)}</loc>${lastmod}\n </url>`;
|
|
12
|
+
})
|
|
13
|
+
.join('\n');
|
|
14
|
+
return [
|
|
15
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
16
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
17
|
+
entries,
|
|
18
|
+
'</urlset>',
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,12 +2,13 @@ export { requireOrigin } from './env.js';
|
|
|
2
2
|
export type { Role, Editor, AuthEnv } from './auth/types.js';
|
|
3
3
|
export type { AuthBranding, MagicLinkMessage, SendMagicLink } from './email.js';
|
|
4
4
|
export { buildMagicLinkMessage, cloudflareSend } from './email.js';
|
|
5
|
-
export type { CairnAdapter, ConceptConfig, FrontmatterField, TextField, TextareaField, DateField, BooleanField, TagsField, FreeTagsField, ValidationResult, BackendConfig, SenderConfig, NavMenuConfig, AssetConfig, RoutingRule, ConceptDescriptor, CairnExtension, CairnRuntime, AdminPanel, FieldTypeDef, } from './content/types.js';
|
|
5
|
+
export type { CairnAdapter, ConceptConfig, FrontmatterField, TextField, TextareaField, DateField, BooleanField, TagsField, FreeTagsField, ValidationResult, BackendConfig, SenderConfig, NavMenuConfig, AssetConfig, RoutingRule, ConceptDescriptor, ConceptUrlPolicy, CairnExtension, CairnRuntime, AdminPanel, FieldTypeDef, } from './content/types.js';
|
|
6
6
|
export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
|
|
7
7
|
export { composeRuntime } from './content/compose.js';
|
|
8
8
|
export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
|
|
9
9
|
export { validateFields } from './content/validate.js';
|
|
10
|
-
export { isValidId, idFromFilename, filenameFromId, slugify } from './content/ids.js';
|
|
10
|
+
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
11
|
+
export type { DatePrefix } from './content/ids.js';
|
|
11
12
|
export { defineRegistry } from './render/registry.js';
|
|
12
13
|
export type { ComponentDef, ComponentRegistry } from './render/registry.js';
|
|
13
14
|
export { glyph } from './render/glyph.js';
|
|
@@ -23,6 +24,21 @@ export { appJwt, installationToken, signingSelfTest } from './github/signing.js'
|
|
|
23
24
|
export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
|
|
24
25
|
export { appCredentials } from './github/credentials.js';
|
|
25
26
|
export type { GithubKeyEnv } from './github/credentials.js';
|
|
26
|
-
export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
27
|
+
export { parseSiteConfig, urlPolicyFrom, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
27
28
|
export type { NavNode, SiteConfig } from './nav/site-config.js';
|
|
29
|
+
export { permalink } from './content/permalink.js';
|
|
30
|
+
export { createContentIndex, fromGlob } from './delivery/content-index.js';
|
|
31
|
+
export type { RawFile, ContentSummary, ContentEntry, ContentIndex, } from './delivery/content-index.js';
|
|
32
|
+
export { createSiteIndex } from './delivery/site-index.js';
|
|
33
|
+
export type { SiteIndex, ConceptIndex } from './delivery/site-index.js';
|
|
34
|
+
export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
|
|
35
|
+
export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
|
|
36
|
+
export type { FeedChannel, FeedItem } from './delivery/feeds.js';
|
|
37
|
+
export { buildSitemap } from './delivery/sitemap.js';
|
|
38
|
+
export type { SitemapUrl } from './delivery/sitemap.js';
|
|
39
|
+
export { buildRobots } from './delivery/robots.js';
|
|
40
|
+
export { buildSeoMeta } from './delivery/seo.js';
|
|
41
|
+
export type { SeoInput, SeoMeta } from './delivery/seo.js';
|
|
42
|
+
export { paginate } from './delivery/paginate.js';
|
|
43
|
+
export type { Page } from './delivery/paginate.js';
|
|
28
44
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGnE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/conce
|
|
|
6
6
|
export { composeRuntime } from './content/compose.js';
|
|
7
7
|
export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
|
|
8
8
|
export { validateFields } from './content/validate.js';
|
|
9
|
-
export { isValidId, idFromFilename, filenameFromId, slugify } from './content/ids.js';
|
|
9
|
+
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
10
10
|
// Render engine (Plan 04): generic directive pipeline; sites own the component registry.
|
|
11
11
|
export { defineRegistry } from './render/registry.js';
|
|
12
12
|
export { glyph } from './render/glyph.js';
|
|
@@ -18,4 +18,16 @@ export { appJwt, installationToken, signingSelfTest } from './github/signing.js'
|
|
|
18
18
|
export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
|
|
19
19
|
export { appCredentials } from './github/credentials.js';
|
|
20
20
|
// Nav tree and site-config helpers (Plan 06).
|
|
21
|
-
export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
21
|
+
export { parseSiteConfig, urlPolicyFrom, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
|
|
22
|
+
// Public content delivery (public-delivery design): the query index, syndication, and
|
|
23
|
+
// discovery surface that sites read. Pure builders plus the one permalink resolver; the
|
|
24
|
+
// SvelteKit loaders live under the /sveltekit subpath.
|
|
25
|
+
export { permalink } from './content/permalink.js';
|
|
26
|
+
export { createContentIndex, fromGlob } from './delivery/content-index.js';
|
|
27
|
+
export { createSiteIndex } from './delivery/site-index.js';
|
|
28
|
+
export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
|
|
29
|
+
export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
|
|
30
|
+
export { buildSitemap } from './delivery/sitemap.js';
|
|
31
|
+
export { buildRobots } from './delivery/robots.js';
|
|
32
|
+
export { buildSeoMeta } from './delivery/seo.js';
|
|
33
|
+
export { paginate } from './delivery/paginate.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConceptUrlPolicy } from '../content/types.js';
|
|
1
2
|
/** One navigation node. An omitted or empty `url` is a label-only grouping header; no `children` is a leaf. */
|
|
2
3
|
export interface NavNode {
|
|
3
4
|
label: string;
|
|
@@ -31,6 +32,8 @@ export interface SiteConfig {
|
|
|
31
32
|
locale?: string;
|
|
32
33
|
/** Named navigation menus, each a NavNode[] (normalized by extractMenu). */
|
|
33
34
|
menus?: Record<string, unknown>;
|
|
35
|
+
/** Per-concept URL policy: the permalink pattern and date-prefix granularity, keyed by concept id. */
|
|
36
|
+
content?: Record<string, ConceptUrlPolicy>;
|
|
34
37
|
[key: string]: unknown;
|
|
35
38
|
}
|
|
36
39
|
export declare class SiteConfigError extends Error {
|
|
@@ -40,6 +43,8 @@ export declare class SiteConfigError extends Error {
|
|
|
40
43
|
export declare function parseSiteConfig(raw: string): SiteConfig;
|
|
41
44
|
/** Extract one named menu from a parsed config and validate it. Returns [] when the menu is absent. */
|
|
42
45
|
export declare function extractMenu(config: SiteConfig, name: string, maxDepth: number): NavNode[];
|
|
46
|
+
/** The per-concept URL policy from a parsed config, or an empty policy when the `content` key is absent. */
|
|
47
|
+
export declare function urlPolicyFrom(config: SiteConfig): Record<string, ConceptUrlPolicy>;
|
|
43
48
|
/**
|
|
44
49
|
* Replace one named menu in the YAML site-config text and reserialize, preserving every other
|
|
45
50
|
* top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/lib/nav/site-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/lib/nav/site-config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,+GAA+G;AAC/G,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,+EAA+E;AAC/E,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,+CAA+C;AAC/C,eAAO,MAAM,cAAc,OAAO,CAAC;AAKnC,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CA6B3E;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,sGAAsG;IACtG,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAUvD;AAED,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAIzF;AAED,4GAA4G;AAC5G,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAElF;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAO1E"}
|
package/dist/nav/site-config.js
CHANGED
|
@@ -84,6 +84,10 @@ export function extractMenu(config, name, maxDepth) {
|
|
|
84
84
|
return [];
|
|
85
85
|
return validateNavTree(menu, maxDepth);
|
|
86
86
|
}
|
|
87
|
+
/** The per-concept URL policy from a parsed config, or an empty policy when the `content` key is absent. */
|
|
88
|
+
export function urlPolicyFrom(config) {
|
|
89
|
+
return config.content ?? {};
|
|
90
|
+
}
|
|
87
91
|
/**
|
|
88
92
|
* Replace one named menu in the YAML site-config text and reserialize, preserving every other
|
|
89
93
|
* top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/content-routes.ts"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7E,OAAO,KAAK,EAAE,YAAY,EAAqB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAErD,wGAAwG;AACxG,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gGAAgG;AAChG,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1C,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,yGAAyG;IACzG,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACnC,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AAgBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,GAAE,iBAAsB;wBAK1D,YAAY,KAAG,UAAU;yBAa1B,KAAK;sBAqBA,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;0BAqB5B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"content-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/content-routes.ts"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7E,OAAO,KAAK,EAAE,YAAY,EAAqB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAErD,wGAAwG;AACxG,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gGAAgG;AAChG,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1C,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,yGAAyG;IACzG,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACnC,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AAgBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,GAAE,iBAAsB;wBAK1D,YAAY,KAAG,UAAU;yBAa1B,KAAK;sBAqBA,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;0BAqB5B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;sBAyCjC,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;wBAgC9B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;qBAtJ5C,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC;EA+LnD"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { redirect, error } from '@sveltejs/kit';
|
|
6
6
|
import { findConcept } from '../content/concepts.js';
|
|
7
7
|
import { frontmatterFromForm, parseMarkdown, dateInputValue, serializeMarkdown } from '../content/frontmatter.js';
|
|
8
|
-
import { isValidId, slugify, filenameFromId } from '../content/ids.js';
|
|
8
|
+
import { isValidId, slugify, filenameFromId, composeDatedId } from '../content/ids.js';
|
|
9
9
|
import { appCredentials } from '../github/credentials.js';
|
|
10
10
|
import { listMarkdown, readRaw, commitFile } from '../github/repo.js';
|
|
11
11
|
import { installationToken } from '../github/signing.js';
|
|
@@ -82,22 +82,32 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
82
82
|
return { ...base, entries: [], error: 'Could not load this content type from GitHub.' };
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
/** Create a new entry: validate the slug,
|
|
85
|
+
/** Create a new entry: validate the slug, compose a dated id when the concept is dated, refuse to clobber. */
|
|
86
86
|
async function createAction(event) {
|
|
87
87
|
sessionOf(event);
|
|
88
88
|
const concept = conceptOf(runtime, event.params);
|
|
89
89
|
const form = await event.request.formData();
|
|
90
|
-
const
|
|
90
|
+
const slug = String(form.get('slug') ?? '').trim() || slugify(String(form.get('title') ?? ''));
|
|
91
|
+
const date = String(form.get('date') ?? '').trim();
|
|
91
92
|
const bounce = (msg) => {
|
|
92
93
|
throw redirect(303, `/admin/${concept.id}?error=${encodeURIComponent(msg)}`);
|
|
93
94
|
};
|
|
94
|
-
if (!isValidId(
|
|
95
|
-
bounce('Enter a valid slug: lowercase letters, numbers, and hyphens.');
|
|
95
|
+
if (!isValidId(slug))
|
|
96
|
+
return bounce('Enter a valid slug: lowercase letters, numbers, and hyphens.');
|
|
97
|
+
let id = slug;
|
|
98
|
+
if (concept.routing.dated) {
|
|
99
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
|
|
100
|
+
return bounce('Pick a date for this entry.');
|
|
101
|
+
if (/^\d{4}-/.test(slug)) {
|
|
102
|
+
return bounce('Leave the date out of the slug; set it in the date field.');
|
|
103
|
+
}
|
|
104
|
+
id = composeDatedId(date, slug, concept.datePrefix);
|
|
105
|
+
}
|
|
96
106
|
const token = await mintToken(event.platform?.env ?? {});
|
|
97
|
-
const existing = await readRaw(runtime.backend, `${concept.dir}/${filenameFromId(
|
|
107
|
+
const existing = await readRaw(runtime.backend, `${concept.dir}/${filenameFromId(id)}`, token);
|
|
98
108
|
if (existing !== null)
|
|
99
|
-
bounce('An entry with that slug already exists.');
|
|
100
|
-
throw redirect(303, `/admin/${concept.id}/${
|
|
109
|
+
return bounce('An entry with that slug already exists.');
|
|
110
|
+
throw redirect(303, `/admin/${concept.id}/${id}?new=1`);
|
|
101
111
|
}
|
|
102
112
|
/** Coerce parsed frontmatter to the form-ready values the editor inputs expect. */
|
|
103
113
|
function formValues(fields, frontmatter) {
|
|
@@ -7,4 +7,6 @@ export { createNavRoutes } from './nav-routes.js';
|
|
|
7
7
|
export type { NavLoadData, NavPageOption, NavRoutesDeps } from './nav-routes.js';
|
|
8
8
|
export { healthLoad, type HealthData } from './health.js';
|
|
9
9
|
export type { RequestContext, CookieJar, HandleInput } from './types.js';
|
|
10
|
+
export { createPublicRoutes } from './public-routes.js';
|
|
11
|
+
export type { PublicRoutesDeps, ListData as PublicListData, TagData, TagIndexData, EntryData, } from './public-routes.js';
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EACV,gBAAgB,EAChB,QAAQ,IAAI,cAAc,EAC1B,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,oBAAoB,CAAC"}
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ContentSummary, ContentEntry } from '../delivery/content-index.js';
|
|
2
|
+
import type { SiteIndex } from '../delivery/site-index.js';
|
|
3
|
+
/** Injected dependencies for the public loaders. */
|
|
4
|
+
export interface PublicRoutesDeps {
|
|
5
|
+
site: SiteIndex;
|
|
6
|
+
render: (md: string, opts?: {
|
|
7
|
+
stagger?: boolean;
|
|
8
|
+
}) => string | Promise<string>;
|
|
9
|
+
origin: string;
|
|
10
|
+
}
|
|
11
|
+
/** The archive and tag list data: summaries the template renders. */
|
|
12
|
+
export interface ListData {
|
|
13
|
+
entries: ContentSummary[];
|
|
14
|
+
}
|
|
15
|
+
/** A single tag's data plus the tag it filtered on. */
|
|
16
|
+
export interface TagData extends ListData {
|
|
17
|
+
tag: string;
|
|
18
|
+
}
|
|
19
|
+
/** The tag-index data: every tag with its count. */
|
|
20
|
+
export interface TagIndexData {
|
|
21
|
+
tags: {
|
|
22
|
+
tag: string;
|
|
23
|
+
count: number;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
/** One entry's data: the detail entry, its rendered html, and its canonical URL. */
|
|
27
|
+
export interface EntryData {
|
|
28
|
+
entry: ContentEntry;
|
|
29
|
+
html: string;
|
|
30
|
+
canonicalUrl: string;
|
|
31
|
+
newer?: ContentSummary;
|
|
32
|
+
older?: ContentSummary;
|
|
33
|
+
}
|
|
34
|
+
/** Build the public loaders for a site's unified index. */
|
|
35
|
+
export declare function createPublicRoutes(deps: PublicRoutesDeps): {
|
|
36
|
+
entryLoad: (event: {
|
|
37
|
+
url: URL;
|
|
38
|
+
}) => Promise<EntryData>;
|
|
39
|
+
archiveLoad: (conceptId: string) => ListData;
|
|
40
|
+
tagIndexLoad: (conceptId: string) => TagIndexData;
|
|
41
|
+
tagLoad: (conceptId: string, event: {
|
|
42
|
+
params: {
|
|
43
|
+
tag: string;
|
|
44
|
+
};
|
|
45
|
+
}) => TagData;
|
|
46
|
+
entries: () => {
|
|
47
|
+
path: string;
|
|
48
|
+
}[];
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=public-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/public-routes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE3D,oDAAoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,uDAAuD;AACvD,MAAM,WAAW,OAAQ,SAAQ,QAAQ;IACvC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACxC;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB;uBAWvB;QAAE,GAAG,EAAE,GAAG,CAAA;KAAE,KAAG,OAAO,CAAC,SAAS,CAAC;6BAQjC,MAAM,KAAG,QAAQ;8BAKhB,MAAM,KAAG,YAAY;yBAK1B,MAAM,SAAS;QAAE,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAG,OAAO;mBAO5D;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE;EAKvC"}
|