@glw907/cairn-cms 0.6.0-rc.1 → 0.7.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/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.js +1 -1
- package/dist/content/concepts.d.ts.map +1 -1
- package/dist/content/concepts.js +5 -0
- package/dist/content/permalink.d.ts +11 -0
- package/dist/content/permalink.d.ts.map +1 -0
- package/dist/content/permalink.js +30 -0
- package/dist/content/types.d.ts +17 -3
- package/dist/content/types.d.ts.map +1 -1
- package/dist/delivery/content-index.d.ts +46 -0
- package/dist/delivery/content-index.d.ts.map +1 -0
- package/dist/delivery/content-index.js +84 -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/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 +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- 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 +51 -0
- package/dist/sveltekit/public-routes.d.ts.map +1 -0
- package/dist/sveltekit/public-routes.js +44 -0
- package/package.json +1 -1
- package/src/lib/components/EditPage.svelte +5 -5
- package/src/lib/content/compose.ts +1 -1
- package/src/lib/content/concepts.ts +6 -0
- package/src/lib/content/permalink.ts +40 -0
- package/src/lib/content/types.ts +14 -4
- package/src/lib/delivery/content-index.ts +127 -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/sitemap.ts +29 -0
- package/src/lib/index.ts +22 -0
- package/src/lib/sveltekit/index.ts +8 -0
- package/src/lib/sveltekit/public-routes.ts +81 -0
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
|
|
6
|
+
/** Feed channel metadata. URLs are absolute. */
|
|
7
|
+
export interface FeedChannel {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
siteUrl: string;
|
|
11
|
+
feedUrl: string;
|
|
12
|
+
language?: string;
|
|
13
|
+
author?: { name: string; email?: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One feed entry. `contentHtml` carries the rendered body for a full-content feed. */
|
|
17
|
+
export interface FeedItem {
|
|
18
|
+
title: string;
|
|
19
|
+
url: string;
|
|
20
|
+
date: string;
|
|
21
|
+
updated?: string;
|
|
22
|
+
summary: string;
|
|
23
|
+
contentHtml?: string;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function escapeXml(value: string): string {
|
|
28
|
+
return value
|
|
29
|
+
.replace(/&/g, '&')
|
|
30
|
+
.replace(/</g, '<')
|
|
31
|
+
.replace(/>/g, '>')
|
|
32
|
+
.replace(/"/g, '"');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Make a string safe inside a CDATA section by splitting any `]]>` across two sections. */
|
|
36
|
+
function cdataSafe(value: string): string {
|
|
37
|
+
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format a YYYY-MM-DD (or ISO) string as an RFC-822 date in UTC, as RSS wants. */
|
|
41
|
+
function rfc822(date: string): string {
|
|
42
|
+
return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toUTCString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Format a YYYY-MM-DD (or ISO) string as an ISO-8601 instant in UTC. */
|
|
46
|
+
function iso(date: string): string {
|
|
47
|
+
return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toISOString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build an RSS 2.0 document. */
|
|
51
|
+
export function buildRssFeed(channel: FeedChannel, items: FeedItem[]): string {
|
|
52
|
+
const entries = items
|
|
53
|
+
.map((item) => {
|
|
54
|
+
const content = item.contentHtml ?? item.summary;
|
|
55
|
+
return [
|
|
56
|
+
' <item>',
|
|
57
|
+
` <title>${escapeXml(item.title)}</title>`,
|
|
58
|
+
` <link>${escapeXml(item.url)}</link>`,
|
|
59
|
+
` <guid isPermaLink="true">${escapeXml(item.url)}</guid>`,
|
|
60
|
+
` <pubDate>${rfc822(item.date)}</pubDate>`,
|
|
61
|
+
` <description>${escapeXml(item.summary)}</description>`,
|
|
62
|
+
// CDATA cannot contain `]]>`, so split that one sequence rather than escape the body.
|
|
63
|
+
` <content:encoded><![CDATA[${cdataSafe(content)}]]></content:encoded>`,
|
|
64
|
+
' </item>',
|
|
65
|
+
].join('\n');
|
|
66
|
+
})
|
|
67
|
+
.join('\n');
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
71
|
+
'<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
72
|
+
' <channel>',
|
|
73
|
+
` <title>${escapeXml(channel.title)}</title>`,
|
|
74
|
+
` <link>${escapeXml(channel.siteUrl)}</link>`,
|
|
75
|
+
` <description>${escapeXml(channel.description)}</description>`,
|
|
76
|
+
channel.language ? ` <language>${escapeXml(channel.language)}</language>` : '',
|
|
77
|
+
` <atom:link href="${escapeXml(channel.feedUrl)}" rel="self" type="application/rss+xml" />`,
|
|
78
|
+
entries,
|
|
79
|
+
' </channel>',
|
|
80
|
+
'</rss>',
|
|
81
|
+
'',
|
|
82
|
+
]
|
|
83
|
+
.filter((line) => line !== '')
|
|
84
|
+
.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Build a JSON Feed 1.1 document. */
|
|
88
|
+
export function buildJsonFeed(channel: FeedChannel, items: FeedItem[]): string {
|
|
89
|
+
return JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
version: 'https://jsonfeed.org/version/1.1',
|
|
92
|
+
title: channel.title,
|
|
93
|
+
description: channel.description,
|
|
94
|
+
home_page_url: channel.siteUrl,
|
|
95
|
+
feed_url: channel.feedUrl,
|
|
96
|
+
...(channel.language ? { language: channel.language } : {}),
|
|
97
|
+
...(channel.author ? { authors: [channel.author] } : {}),
|
|
98
|
+
items: items.map((item) => ({
|
|
99
|
+
id: item.url,
|
|
100
|
+
url: item.url,
|
|
101
|
+
title: item.title,
|
|
102
|
+
summary: item.summary,
|
|
103
|
+
date_published: iso(item.date),
|
|
104
|
+
...(item.updated ? { date_modified: iso(item.updated) } : {}),
|
|
105
|
+
...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
|
|
106
|
+
...(item.tags && item.tags.length ? { tags: item.tags } : {}),
|
|
107
|
+
})),
|
|
108
|
+
},
|
|
109
|
+
null,
|
|
110
|
+
2,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
|
|
4
|
+
/** A page of items plus its navigation state. */
|
|
5
|
+
export interface Page<T> {
|
|
6
|
+
items: T[];
|
|
7
|
+
page: number;
|
|
8
|
+
perPage: number;
|
|
9
|
+
total: number;
|
|
10
|
+
totalPages: number;
|
|
11
|
+
hasPrev: boolean;
|
|
12
|
+
hasNext: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
|
|
16
|
+
export function paginate<T>(items: T[], page: number, perPage: number): Page<T> {
|
|
17
|
+
const total = items.length;
|
|
18
|
+
// A non-positive page size would make totalPages Infinity, so clamp it to one.
|
|
19
|
+
const size = Math.max(1, Math.floor(perPage) || 1);
|
|
20
|
+
const totalPages = Math.max(1, Math.ceil(total / size));
|
|
21
|
+
const current = Math.min(Math.max(1, Math.floor(page) || 1), totalPages);
|
|
22
|
+
const start = (current - 1) * size;
|
|
23
|
+
return {
|
|
24
|
+
items: items.slice(start, start + size),
|
|
25
|
+
page: current,
|
|
26
|
+
perPage: size,
|
|
27
|
+
total,
|
|
28
|
+
totalPages,
|
|
29
|
+
hasPrev: current > 1,
|
|
30
|
+
hasNext: current < totalPages,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -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
|
+
|
|
4
|
+
/** Build a robots.txt body. */
|
|
5
|
+
export function buildRobots(opts: { sitemapUrl: string; disallow?: string[] }): string {
|
|
6
|
+
const lines = ['User-agent: *', 'Allow: /'];
|
|
7
|
+
for (const path of opts.disallow ?? []) lines.push(`Disallow: ${path}`);
|
|
8
|
+
lines.push('', `Sitemap: ${opts.sitemapUrl}`, '');
|
|
9
|
+
return lines.join('\n');
|
|
10
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
|
|
5
|
+
/** Inputs for the head. All URLs are absolute (built from PUBLIC_ORIGIN). */
|
|
6
|
+
export interface SeoInput {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
canonicalUrl: string;
|
|
10
|
+
siteName: string;
|
|
11
|
+
type?: 'website' | 'article';
|
|
12
|
+
published?: string;
|
|
13
|
+
modified?: string;
|
|
14
|
+
feeds?: { rss?: string; json?: string };
|
|
15
|
+
image?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Plain-data head: a title, meta tags, link tags, and one JSON-LD object. */
|
|
19
|
+
export interface SeoMeta {
|
|
20
|
+
title: string;
|
|
21
|
+
meta: { name?: string; property?: string; content: string }[];
|
|
22
|
+
links: { rel: string; type?: string; href: string; title?: string }[];
|
|
23
|
+
jsonLd: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Build the head data for a page. */
|
|
27
|
+
export function buildSeoMeta(input: SeoInput): SeoMeta {
|
|
28
|
+
const type = input.type ?? 'website';
|
|
29
|
+
const meta: SeoMeta['meta'] = [
|
|
30
|
+
{ name: 'description', content: input.description },
|
|
31
|
+
{ property: 'og:title', content: input.title },
|
|
32
|
+
{ property: 'og:description', content: input.description },
|
|
33
|
+
{ property: 'og:type', content: type },
|
|
34
|
+
{ property: 'og:url', content: input.canonicalUrl },
|
|
35
|
+
{ property: 'og:site_name', content: input.siteName },
|
|
36
|
+
{ name: 'twitter:card', content: input.image ? 'summary_large_image' : 'summary' },
|
|
37
|
+
];
|
|
38
|
+
if (input.image) {
|
|
39
|
+
meta.push({ property: 'og:image', content: input.image });
|
|
40
|
+
meta.push({ name: 'twitter:image', content: input.image });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const links: SeoMeta['links'] = [{ rel: 'canonical', href: input.canonicalUrl }];
|
|
44
|
+
if (input.feeds?.rss) {
|
|
45
|
+
links.push({ rel: 'alternate', type: 'application/rss+xml', href: input.feeds.rss, title: input.siteName });
|
|
46
|
+
}
|
|
47
|
+
if (input.feeds?.json) {
|
|
48
|
+
links.push({ rel: 'alternate', type: 'application/feed+json', href: input.feeds.json, title: input.siteName });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const jsonLd: Record<string, unknown> =
|
|
52
|
+
type === 'article'
|
|
53
|
+
? {
|
|
54
|
+
'@context': 'https://schema.org',
|
|
55
|
+
'@type': 'Article',
|
|
56
|
+
headline: input.title,
|
|
57
|
+
description: input.description,
|
|
58
|
+
url: input.canonicalUrl,
|
|
59
|
+
...(input.published ? { datePublished: input.published } : {}),
|
|
60
|
+
...(input.modified ? { dateModified: input.modified } : {}),
|
|
61
|
+
...(input.image ? { image: input.image } : {}),
|
|
62
|
+
}
|
|
63
|
+
: {
|
|
64
|
+
'@context': 'https://schema.org',
|
|
65
|
+
'@type': 'WebSite',
|
|
66
|
+
name: input.siteName,
|
|
67
|
+
description: input.description,
|
|
68
|
+
url: input.canonicalUrl,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return { title: input.title, meta, links, jsonLd };
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
|
|
4
|
+
/** One sitemap URL. `lastmod` is a YYYY-MM-DD date. */
|
|
5
|
+
export interface SitemapUrl {
|
|
6
|
+
loc: string;
|
|
7
|
+
lastmod?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function escapeXml(value: string): string {
|
|
11
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Build a sitemap XML document from a list of URLs. */
|
|
15
|
+
export function buildSitemap(urls: SitemapUrl[]): string {
|
|
16
|
+
const entries = urls
|
|
17
|
+
.map((url) => {
|
|
18
|
+
const lastmod = url.lastmod ? `\n <lastmod>${escapeXml(url.lastmod)}</lastmod>` : '';
|
|
19
|
+
return ` <url>\n <loc>${escapeXml(url.loc)}</loc>${lastmod}\n </url>`;
|
|
20
|
+
})
|
|
21
|
+
.join('\n');
|
|
22
|
+
return [
|
|
23
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
24
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
25
|
+
entries,
|
|
26
|
+
'</urlset>',
|
|
27
|
+
'',
|
|
28
|
+
].join('\n');
|
|
29
|
+
}
|
package/src/lib/index.ts
CHANGED
|
@@ -84,3 +84,25 @@ export {
|
|
|
84
84
|
SiteConfigError,
|
|
85
85
|
} from './nav/site-config.js';
|
|
86
86
|
export type { NavNode, SiteConfig } from './nav/site-config.js';
|
|
87
|
+
|
|
88
|
+
// Public content delivery (public-delivery design): the query index, syndication, and
|
|
89
|
+
// discovery surface that sites read. Pure builders plus the one permalink resolver; the
|
|
90
|
+
// SvelteKit loaders live under the /sveltekit subpath.
|
|
91
|
+
export { permalink } from './content/permalink.js';
|
|
92
|
+
export { createContentIndex, fromGlob } from './delivery/content-index.js';
|
|
93
|
+
export type {
|
|
94
|
+
RawFile,
|
|
95
|
+
ContentSummary,
|
|
96
|
+
ContentEntry,
|
|
97
|
+
ContentIndex,
|
|
98
|
+
} from './delivery/content-index.js';
|
|
99
|
+
export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
|
|
100
|
+
export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
|
|
101
|
+
export type { FeedChannel, FeedItem } from './delivery/feeds.js';
|
|
102
|
+
export { buildSitemap } from './delivery/sitemap.js';
|
|
103
|
+
export type { SitemapUrl } from './delivery/sitemap.js';
|
|
104
|
+
export { buildRobots } from './delivery/robots.js';
|
|
105
|
+
export { buildSeoMeta } from './delivery/seo.js';
|
|
106
|
+
export type { SeoInput, SeoMeta } from './delivery/seo.js';
|
|
107
|
+
export { paginate } from './delivery/paginate.js';
|
|
108
|
+
export type { Page } from './delivery/paginate.js';
|
|
@@ -17,3 +17,11 @@ export { createNavRoutes } from './nav-routes.js';
|
|
|
17
17
|
export type { NavLoadData, NavPageOption, NavRoutesDeps } from './nav-routes.js';
|
|
18
18
|
export { healthLoad, type HealthData } from './health.js';
|
|
19
19
|
export type { RequestContext, CookieJar, HandleInput } from './types.js';
|
|
20
|
+
export { createPublicRoutes } from './public-routes.js';
|
|
21
|
+
export type {
|
|
22
|
+
PublicRoutesDeps,
|
|
23
|
+
ListData as PublicListData,
|
|
24
|
+
TagData,
|
|
25
|
+
TagIndexData,
|
|
26
|
+
EntryData,
|
|
27
|
+
} from './public-routes.js';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// cairn-cms: public route loaders (public-delivery design, decision 6). A factory closes over
|
|
2
|
+
// a concept's index, the runtime render, and the origin, and returns thin load functions plus
|
|
3
|
+
// entries() for prerender. A site route file stays a one-line shim. The index is built in site
|
|
4
|
+
// code from a glob, so it stays in the prerender graph and out of the runtime Worker.
|
|
5
|
+
import { error } from '@sveltejs/kit';
|
|
6
|
+
import type { ContentIndex, ContentSummary, ContentEntry } from '../delivery/content-index.js';
|
|
7
|
+
|
|
8
|
+
/** Injected dependencies for the public loaders. */
|
|
9
|
+
export interface PublicRoutesDeps {
|
|
10
|
+
index: ContentIndex;
|
|
11
|
+
render: (md: string, opts?: { stagger?: boolean }) => string | Promise<string>;
|
|
12
|
+
origin: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The archive and tag list data: summaries the template renders. */
|
|
16
|
+
export interface ListData {
|
|
17
|
+
entries: ContentSummary[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A single tag's data plus the tag it filtered on. */
|
|
21
|
+
export interface TagData extends ListData {
|
|
22
|
+
tag: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The tag-index data: every tag with its count. */
|
|
26
|
+
export interface TagIndexData {
|
|
27
|
+
tags: { tag: string; count: number }[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One entry's data: the detail entry, its rendered html, and its canonical URL. */
|
|
31
|
+
export interface EntryData {
|
|
32
|
+
entry: ContentEntry;
|
|
33
|
+
html: string;
|
|
34
|
+
canonicalUrl: string;
|
|
35
|
+
newer?: ContentSummary;
|
|
36
|
+
older?: ContentSummary;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build the public loaders for one concept's index. */
|
|
40
|
+
export function createPublicRoutes(deps: PublicRoutesDeps) {
|
|
41
|
+
const { index, render, origin } = deps;
|
|
42
|
+
|
|
43
|
+
/** The chronological archive: every non-draft summary, newest-first. */
|
|
44
|
+
function archiveLoad(): ListData {
|
|
45
|
+
return { entries: index.all() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** All tags with counts, for a tag index page. */
|
|
49
|
+
function tagIndexLoad(): TagIndexData {
|
|
50
|
+
return { tags: index.allTags() };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One tag's entries, or a 404 when the tag has none. */
|
|
54
|
+
function tagLoad(event: { params: { tag: string } }): TagData {
|
|
55
|
+
const tag = event.params.tag;
|
|
56
|
+
const entries = index.byTag(tag);
|
|
57
|
+
if (entries.length === 0) throw error(404, `No entries tagged "${tag}"`);
|
|
58
|
+
return { tag, entries };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One entry by slug, rendered through the site renderer, or a 404. */
|
|
62
|
+
async function entryLoad(event: { params: { slug: string } }): Promise<EntryData> {
|
|
63
|
+
const entry = index.byId(event.params.slug);
|
|
64
|
+
if (!entry) throw error(404, `Not found: ${event.params.slug}`);
|
|
65
|
+
const { newer, older } = index.adjacent(entry.id);
|
|
66
|
+
return {
|
|
67
|
+
entry,
|
|
68
|
+
html: await render(entry.body, { stagger: true }),
|
|
69
|
+
canonicalUrl: origin + entry.permalink,
|
|
70
|
+
newer,
|
|
71
|
+
older,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Prerender enumeration: one `{ slug }` per non-draft entry. */
|
|
76
|
+
function entries(): { slug: string }[] {
|
|
77
|
+
return index.all().map((entry) => ({ slug: entry.id }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { archiveLoad, tagIndexLoad, tagLoad, entryLoad, entries };
|
|
81
|
+
}
|