narrarium-astro-reader 0.1.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/README.md +52 -0
- package/astro.config.mjs +12 -0
- package/cli-dist/cli.d.ts +3 -0
- package/cli-dist/cli.d.ts.map +1 -0
- package/cli-dist/cli.js +78 -0
- package/cli-dist/cli.js.map +1 -0
- package/cli-dist/lib/assets.d.ts +8 -0
- package/cli-dist/lib/assets.d.ts.map +1 -0
- package/cli-dist/lib/assets.js +35 -0
- package/cli-dist/lib/assets.js.map +1 -0
- package/cli-dist/lib/book-config.d.ts +2 -0
- package/cli-dist/lib/book-config.d.ts.map +1 -0
- package/cli-dist/lib/book-config.js +2 -0
- package/cli-dist/lib/book-config.js.map +1 -0
- package/cli-dist/lib/book.d.ts +103 -0
- package/cli-dist/lib/book.d.ts.map +1 -0
- package/cli-dist/lib/book.js +89 -0
- package/cli-dist/lib/book.js.map +1 -0
- package/cli-dist/lib/canon.d.ts +16 -0
- package/cli-dist/lib/canon.d.ts.map +1 -0
- package/cli-dist/lib/canon.js +164 -0
- package/cli-dist/lib/canon.js.map +1 -0
- package/cli-dist/lib/glossary.d.ts +25 -0
- package/cli-dist/lib/glossary.d.ts.map +1 -0
- package/cli-dist/lib/glossary.js +195 -0
- package/cli-dist/lib/glossary.js.map +1 -0
- package/cli-dist/lib/search.d.ts +9 -0
- package/cli-dist/lib/search.d.ts.map +1 -0
- package/cli-dist/lib/search.js +55 -0
- package/cli-dist/lib/search.js.map +1 -0
- package/cli-dist/scaffold.d.ts +14 -0
- package/cli-dist/scaffold.d.ts.map +1 -0
- package/cli-dist/scaffold.js +190 -0
- package/cli-dist/scaffold.js.map +1 -0
- package/package.json +58 -0
- package/scripts/export-epub.mjs +13 -0
- package/src/cli.ts +96 -0
- package/src/components/AssetFigure.astro +17 -0
- package/src/components/ChapterPager.astro +40 -0
- package/src/components/LinkedValue.astro +18 -0
- package/src/components/MetadataSection.astro +22 -0
- package/src/components/ReaderRuntime.astro +310 -0
- package/src/components/RelatedLinks.astro +19 -0
- package/src/components/SiteSearch.astro +91 -0
- package/src/layouts/BaseLayout.astro +676 -0
- package/src/lib/assets.ts +44 -0
- package/src/lib/book-config.ts +1 -0
- package/src/lib/book.ts +116 -0
- package/src/lib/canon.ts +212 -0
- package/src/lib/glossary.ts +247 -0
- package/src/lib/search.ts +74 -0
- package/src/pages/chapters/[chapter].astro +102 -0
- package/src/pages/characters/[slug].astro +65 -0
- package/src/pages/characters/index.astro +45 -0
- package/src/pages/factions/[slug].astro +64 -0
- package/src/pages/factions/index.astro +45 -0
- package/src/pages/index.astro +129 -0
- package/src/pages/items/[slug].astro +63 -0
- package/src/pages/items/index.astro +45 -0
- package/src/pages/locations/[slug].astro +61 -0
- package/src/pages/locations/index.astro +45 -0
- package/src/pages/secrets/[slug].astro +67 -0
- package/src/pages/secrets/index.astro +46 -0
- package/src/pages/timeline/[slug].astro +58 -0
- package/src/pages/timeline/index.astro +52 -0
- package/src/scaffold.ts +232 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
4
|
+
import { getBookRoot, loadChapterPageData } from "../../lib/book";
|
|
5
|
+
import LinkedValue from "../../components/LinkedValue.astro";
|
|
6
|
+
import MetadataSection from "../../components/MetadataSection.astro";
|
|
7
|
+
import RelatedLinks from "../../components/RelatedLinks.astro";
|
|
8
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
9
|
+
import ChapterPager from "../../components/ChapterPager.astro";
|
|
10
|
+
import { loadRelatedCanonLinks } from "../../lib/canon";
|
|
11
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
12
|
+
import { listChapters, pathExists } from "narrarium";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
export async function getStaticPaths() {
|
|
16
|
+
const root = getBookRoot();
|
|
17
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
18
|
+
if (!ready) return [];
|
|
19
|
+
|
|
20
|
+
const chapters = await listChapters(root);
|
|
21
|
+
return chapters.map((chapter) => ({
|
|
22
|
+
params: { chapter: chapter.slug },
|
|
23
|
+
props: { chapterSlug: chapter.slug },
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { chapterSlug } = Astro.props;
|
|
28
|
+
const chapter = await loadChapterPageData(chapterSlug);
|
|
29
|
+
const allChapters = await listChapters(getBookRoot());
|
|
30
|
+
const chapterHtml = await marked.parse(chapter.body);
|
|
31
|
+
const chapterMetaEntries = [
|
|
32
|
+
["Point of view", chapter.metadata.pov],
|
|
33
|
+
["Timeline", chapter.metadata.timeline_ref],
|
|
34
|
+
["Tags", chapter.metadata.tags],
|
|
35
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
36
|
+
const chapterRelatedLinks = await loadRelatedCanonLinks(String(chapter.metadata.id), [chapter.metadata.pov, chapter.metadata.timeline_ref]);
|
|
37
|
+
const chapterFigure = await loadAssetFigure(String(chapter.metadata.id), chapter.metadata.title);
|
|
38
|
+
const renderedParagraphs = await Promise.all(
|
|
39
|
+
chapter.paragraphs.map(async (paragraph) => ({
|
|
40
|
+
...paragraph,
|
|
41
|
+
anchorId: `scene-${path.basename(paragraph.path, ".md")}`,
|
|
42
|
+
figure: await loadAssetFigure(String(paragraph.metadata.id), paragraph.metadata.title),
|
|
43
|
+
html: await marked.parse(paragraph.body),
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<BaseLayout title={chapter.metadata.title} description={chapter.metadata.summary} activeSection="chapters">
|
|
49
|
+
<section class="hero">
|
|
50
|
+
<p class="eyebrow">Chapter {String(chapter.metadata.number).padStart(3, "0")}</p>
|
|
51
|
+
<h1>{chapter.metadata.title}</h1>
|
|
52
|
+
{chapter.metadata.summary && <p class="lede">{chapter.metadata.summary}</p>}
|
|
53
|
+
<div class="chapter-meta">
|
|
54
|
+
<a href="./">Back to contents</a>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<ChapterPager currentSlug={chapterSlug} chapters={allChapters} />
|
|
59
|
+
|
|
60
|
+
{chapter.body && (
|
|
61
|
+
<section class="section">
|
|
62
|
+
<article class="scene prose" set:html={chapterHtml} />
|
|
63
|
+
{chapterFigure && <AssetFigure figure={chapterFigure} className="scene-media" />}
|
|
64
|
+
</section>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{chapterMetaEntries.length > 0 && (
|
|
68
|
+
<section class="section">
|
|
69
|
+
<MetadataSection entries={chapterMetaEntries} heading="Canon links" />
|
|
70
|
+
</section>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{chapterRelatedLinks.length > 0 && (
|
|
74
|
+
<section class="section">
|
|
75
|
+
<RelatedLinks links={chapterRelatedLinks} />
|
|
76
|
+
</section>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<section class="section">
|
|
80
|
+
<h2>Scenes</h2>
|
|
81
|
+
{renderedParagraphs.length > 0 ? (
|
|
82
|
+
renderedParagraphs.map((paragraph) => (
|
|
83
|
+
<article class="scene" id={paragraph.anchorId}>
|
|
84
|
+
<div class="chapter-number">Scene {String(paragraph.metadata.number).padStart(3, "0")}</div>
|
|
85
|
+
<h3>{paragraph.metadata.title}</h3>
|
|
86
|
+
{paragraph.metadata.summary && <p class="chapter-meta">{paragraph.metadata.summary}</p>}
|
|
87
|
+
{paragraph.metadata.viewpoint && (
|
|
88
|
+
<p class="chapter-meta">
|
|
89
|
+
Viewpoint: <LinkedValue value={paragraph.metadata.viewpoint} />
|
|
90
|
+
</p>
|
|
91
|
+
)}
|
|
92
|
+
<div class="prose" set:html={paragraph.html} />
|
|
93
|
+
{paragraph.figure && <AssetFigure figure={paragraph.figure} className="scene-media" />}
|
|
94
|
+
</article>
|
|
95
|
+
))
|
|
96
|
+
) : (
|
|
97
|
+
<div class="empty">No paragraph files exist for this chapter yet.</div>
|
|
98
|
+
)}
|
|
99
|
+
</section>
|
|
100
|
+
|
|
101
|
+
<ChapterPager currentSlug={chapterSlug} chapters={allChapters} />
|
|
102
|
+
</BaseLayout>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
4
|
+
import { getBookRoot, loadEntityPageData } from "../../lib/book";
|
|
5
|
+
import MetadataSection from "../../components/MetadataSection.astro";
|
|
6
|
+
import RelatedLinks from "../../components/RelatedLinks.astro";
|
|
7
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
8
|
+
import { loadRelatedCanonLinks } from "../../lib/canon";
|
|
9
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
10
|
+
import { listEntities, pathExists } from "narrarium";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const root = getBookRoot();
|
|
15
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
16
|
+
if (!ready) return [];
|
|
17
|
+
|
|
18
|
+
const entities = await listEntities(root, "character");
|
|
19
|
+
return entities.map((entity) => ({ params: { slug: entity.slug }, props: { slug: entity.slug } }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { slug } = Astro.props;
|
|
23
|
+
const entity = await loadEntityPageData("character", slug);
|
|
24
|
+
const html = await marked.parse(entity.body);
|
|
25
|
+
const metaEntries = [
|
|
26
|
+
["Role tier", entity.metadata.role_tier],
|
|
27
|
+
["Story role", entity.metadata.story_role],
|
|
28
|
+
["Speaking style", entity.metadata.speaking_style],
|
|
29
|
+
["Function in book", entity.metadata.function_in_book],
|
|
30
|
+
["Occupation", entity.metadata.occupation],
|
|
31
|
+
["Origin", entity.metadata.origin],
|
|
32
|
+
["Age", entity.metadata.age],
|
|
33
|
+
["Introduced in", entity.metadata.introduced_in],
|
|
34
|
+
["Factions", entity.metadata.factions],
|
|
35
|
+
["Traits", entity.metadata.traits],
|
|
36
|
+
["Desires", entity.metadata.desires],
|
|
37
|
+
["Fears", entity.metadata.fears],
|
|
38
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
39
|
+
const relatedLinks = await loadRelatedCanonLinks(String(entity.metadata.id ?? `character:${slug}`), [entity.metadata.refs, ...metaEntries.map(([, value]) => value)]);
|
|
40
|
+
const figure = await loadAssetFigure(String(entity.metadata.id ?? `character:${slug}`), String(entity.metadata.name ?? slug));
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<BaseLayout title={String(entity.metadata.name ?? slug)} description={String(entity.metadata.function_in_book ?? "Character entry") } activeSection="characters">
|
|
44
|
+
<section class="hero">
|
|
45
|
+
<p class="eyebrow">Character</p>
|
|
46
|
+
<h1>{String(entity.metadata.name ?? slug)}</h1>
|
|
47
|
+
<p class="lede">{String(entity.metadata.function_in_book ?? entity.metadata.story_role ?? "Canonical character entry.")}</p>
|
|
48
|
+
<div class="chapter-meta"><a href="characters/">Back to characters</a></div>
|
|
49
|
+
{figure && <AssetFigure figure={figure} className="hero-media" />}
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section class="section">
|
|
53
|
+
<MetadataSection entries={metaEntries} />
|
|
54
|
+
</section>
|
|
55
|
+
|
|
56
|
+
{relatedLinks.length > 0 && (
|
|
57
|
+
<section class="section">
|
|
58
|
+
<RelatedLinks links={relatedLinks} />
|
|
59
|
+
</section>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<section class="section">
|
|
63
|
+
<article class="scene prose" set:html={html} />
|
|
64
|
+
</section>
|
|
65
|
+
</BaseLayout>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
3
|
+
import { loadEntityIndexData } from "../../lib/book";
|
|
4
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
5
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
6
|
+
|
|
7
|
+
const { ready, entities } = await loadEntityIndexData("character");
|
|
8
|
+
const cards = await Promise.all(
|
|
9
|
+
entities.map(async (entity) => ({
|
|
10
|
+
...entity,
|
|
11
|
+
figure: await loadAssetFigure(String(entity.metadata.id ?? `character:${entity.slug}`), String(entity.metadata.name ?? entity.slug)),
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<BaseLayout title="Characters" description="Browse the book cast." activeSection="characters">
|
|
17
|
+
<section class="hero">
|
|
18
|
+
<p class="eyebrow">Canon Atlas</p>
|
|
19
|
+
<h1>Characters</h1>
|
|
20
|
+
<p class="lede">Browse the cast register, voice notes, roles, and relationships stored in the repository.</p>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
{ready ? (
|
|
24
|
+
<section class="section">
|
|
25
|
+
<div class="catalog-grid">
|
|
26
|
+
{cards.map((entity) => (
|
|
27
|
+
<article class="card catalog-card">
|
|
28
|
+
<a href={`characters/${entity.slug}/`}>
|
|
29
|
+
{entity.figure && <AssetFigure figure={entity.figure} className="catalog-media" />}
|
|
30
|
+
<div class="chapter-number">{String(entity.metadata.role_tier ?? "character")}</div>
|
|
31
|
+
<h2 class="chapter-title">{String(entity.metadata.name ?? entity.slug)}</h2>
|
|
32
|
+
<div class="chapter-meta">{String(entity.metadata.function_in_book ?? entity.metadata.story_role ?? "Open the entry to inspect role, voice, and notes.")}</div>
|
|
33
|
+
<div class="chip-row">
|
|
34
|
+
{entity.metadata.story_role && <span class="chip">{String(entity.metadata.story_role)}</span>}
|
|
35
|
+
{entity.metadata.historical && <span class="chip">historical</span>}
|
|
36
|
+
</div>
|
|
37
|
+
</a>
|
|
38
|
+
</article>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
) : (
|
|
43
|
+
<div class="empty">No book repository detected for the reader.</div>
|
|
44
|
+
)}
|
|
45
|
+
</BaseLayout>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
4
|
+
import { getBookRoot, loadEntityPageData } from "../../lib/book";
|
|
5
|
+
import MetadataSection from "../../components/MetadataSection.astro";
|
|
6
|
+
import RelatedLinks from "../../components/RelatedLinks.astro";
|
|
7
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
8
|
+
import { loadRelatedCanonLinks } from "../../lib/canon";
|
|
9
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
10
|
+
import { listEntities, pathExists } from "narrarium";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const root = getBookRoot();
|
|
15
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
16
|
+
if (!ready) return [];
|
|
17
|
+
|
|
18
|
+
const entities = await listEntities(root, "faction");
|
|
19
|
+
return entities.map((entity) => ({ params: { slug: entity.slug }, props: { slug: entity.slug } }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { slug } = Astro.props;
|
|
23
|
+
const entity = await loadEntityPageData("faction", slug);
|
|
24
|
+
const html = await marked.parse(entity.body);
|
|
25
|
+
const metaEntries = [
|
|
26
|
+
["Kind", entity.metadata.faction_kind],
|
|
27
|
+
["Mission", entity.metadata.mission],
|
|
28
|
+
["Ideology", entity.metadata.ideology],
|
|
29
|
+
["Function in book", entity.metadata.function_in_book],
|
|
30
|
+
["Public image", entity.metadata.public_image],
|
|
31
|
+
["Hidden agenda", entity.metadata.hidden_agenda],
|
|
32
|
+
["Leaders", entity.metadata.leaders],
|
|
33
|
+
["Allies", entity.metadata.allies],
|
|
34
|
+
["Enemies", entity.metadata.enemies],
|
|
35
|
+
["Methods", entity.metadata.methods],
|
|
36
|
+
["Base location", entity.metadata.base_location],
|
|
37
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
38
|
+
const relatedLinks = await loadRelatedCanonLinks(String(entity.metadata.id ?? `faction:${slug}`), [entity.metadata.refs, ...metaEntries.map(([, value]) => value)]);
|
|
39
|
+
const figure = await loadAssetFigure(String(entity.metadata.id ?? `faction:${slug}`), String(entity.metadata.name ?? slug));
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<BaseLayout title={String(entity.metadata.name ?? slug)} description={String(entity.metadata.function_in_book ?? entity.metadata.mission ?? "Faction entry")} activeSection="factions">
|
|
43
|
+
<section class="hero">
|
|
44
|
+
<p class="eyebrow">Faction</p>
|
|
45
|
+
<h1>{String(entity.metadata.name ?? slug)}</h1>
|
|
46
|
+
<p class="lede">{String(entity.metadata.function_in_book ?? entity.metadata.mission ?? "Canonical faction entry.")}</p>
|
|
47
|
+
<div class="chapter-meta"><a href="factions/">Back to factions</a></div>
|
|
48
|
+
{figure && <AssetFigure figure={figure} className="hero-media" />}
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section class="section">
|
|
52
|
+
<MetadataSection entries={metaEntries} />
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
{relatedLinks.length > 0 && (
|
|
56
|
+
<section class="section">
|
|
57
|
+
<RelatedLinks links={relatedLinks} />
|
|
58
|
+
</section>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
<section class="section">
|
|
62
|
+
<article class="scene prose" set:html={html} />
|
|
63
|
+
</section>
|
|
64
|
+
</BaseLayout>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
3
|
+
import { loadEntityIndexData } from "../../lib/book";
|
|
4
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
5
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
6
|
+
|
|
7
|
+
const { ready, entities } = await loadEntityIndexData("faction");
|
|
8
|
+
const cards = await Promise.all(
|
|
9
|
+
entities.map(async (entity) => ({
|
|
10
|
+
...entity,
|
|
11
|
+
figure: await loadAssetFigure(String(entity.metadata.id ?? `faction:${entity.slug}`), String(entity.metadata.name ?? entity.slug)),
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<BaseLayout title="Factions" description="Browse the factions of the book." activeSection="factions">
|
|
17
|
+
<section class="hero">
|
|
18
|
+
<p class="eyebrow">Canon Atlas</p>
|
|
19
|
+
<h1>Factions</h1>
|
|
20
|
+
<p class="lede">Map institutions, cults, guilds, and hidden power structures shaping the story.</p>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
{ready ? (
|
|
24
|
+
<section class="section">
|
|
25
|
+
<div class="catalog-grid">
|
|
26
|
+
{cards.map((entity) => (
|
|
27
|
+
<article class="card catalog-card">
|
|
28
|
+
<a href={`factions/${entity.slug}/`}>
|
|
29
|
+
{entity.figure && <AssetFigure figure={entity.figure} className="catalog-media" />}
|
|
30
|
+
<div class="chapter-number">{String(entity.metadata.faction_kind ?? "faction")}</div>
|
|
31
|
+
<h2 class="chapter-title">{String(entity.metadata.name ?? entity.slug)}</h2>
|
|
32
|
+
<div class="chapter-meta">{String(entity.metadata.function_in_book ?? entity.metadata.mission ?? "Open the entry to inspect power, ideology, and methods.")}</div>
|
|
33
|
+
<div class="chip-row">
|
|
34
|
+
{entity.metadata.base_location && <span class="chip">{String(entity.metadata.base_location)}</span>}
|
|
35
|
+
{entity.metadata.historical && <span class="chip">historical</span>}
|
|
36
|
+
</div>
|
|
37
|
+
</a>
|
|
38
|
+
</article>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
) : (
|
|
43
|
+
<div class="empty">No book repository detected for the reader.</div>
|
|
44
|
+
)}
|
|
45
|
+
</BaseLayout>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../layouts/BaseLayout.astro";
|
|
3
|
+
import { loadHomePageData } from "../lib/book";
|
|
4
|
+
import AssetFigure from "../components/AssetFigure.astro";
|
|
5
|
+
import { loadAssetFigure } from "../lib/assets";
|
|
6
|
+
|
|
7
|
+
const { ready, root, book, chapters, characters, locations, factions, items, secrets, timelineEvents } = await loadHomePageData();
|
|
8
|
+
const title = book?.frontmatter.title ?? "Narrarium Reader";
|
|
9
|
+
const coverFigure = await loadAssetFigure("book", `${title} cover`);
|
|
10
|
+
const chapterCards = await Promise.all(
|
|
11
|
+
chapters.map(async (chapter) => ({
|
|
12
|
+
...chapter,
|
|
13
|
+
figure: await loadAssetFigure(String(chapter.metadata.id ?? `chapter:${chapter.slug}`), chapter.metadata.title),
|
|
14
|
+
})),
|
|
15
|
+
);
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<BaseLayout title={title} description="Read a Narrarium repository chapter by chapter." activeSection="home">
|
|
19
|
+
<section class="hero">
|
|
20
|
+
<p class="eyebrow">Narrarium Reader</p>
|
|
21
|
+
<h1>{title}</h1>
|
|
22
|
+
<p class="lede">
|
|
23
|
+
{ready
|
|
24
|
+
? "A local-first reading view over the canonical book repository. Chapters, scenes, and metadata stay in markdown; the site simply renders them."
|
|
25
|
+
: "Set NARRARIUM_BOOK_ROOT to a Narrarium book repository and this reader will render its chapters automatically."}
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
{ready && <div class="chip-row"><a class="chip" href="downloads/book.epub" download>Download EPUB</a></div>}
|
|
29
|
+
|
|
30
|
+
{coverFigure && <AssetFigure figure={coverFigure} className="hero-media" />}
|
|
31
|
+
|
|
32
|
+
<div class="grid stats">
|
|
33
|
+
<div class="card">
|
|
34
|
+
<div class="label">Book Root</div>
|
|
35
|
+
<div class="value">{root}</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="card">
|
|
38
|
+
<div class="label">Chapters</div>
|
|
39
|
+
<div class="value">{chapters.length}</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="card">
|
|
42
|
+
<div class="label">Author</div>
|
|
43
|
+
<div class="value">{book?.frontmatter.author ?? "Unknown"}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="card">
|
|
46
|
+
<div class="label">Language</div>
|
|
47
|
+
<div class="value">{book?.frontmatter.language ?? "n/a"}</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
{ready ? (
|
|
53
|
+
<>
|
|
54
|
+
<section class="section">
|
|
55
|
+
<h2>Canon Atlas</h2>
|
|
56
|
+
<div class="catalog-grid">
|
|
57
|
+
<article class="card catalog-card">
|
|
58
|
+
<a href="characters/">
|
|
59
|
+
<div class="chapter-number">Characters</div>
|
|
60
|
+
<h3 class="chapter-title">Cast Register</h3>
|
|
61
|
+
<div class="chapter-meta">Browse protagonists, supporting roles, and reference notes.</div>
|
|
62
|
+
<div class="chip-row"><span class="chip">{characters.length} entries</span></div>
|
|
63
|
+
</a>
|
|
64
|
+
</article>
|
|
65
|
+
<article class="card catalog-card">
|
|
66
|
+
<a href="locations/">
|
|
67
|
+
<div class="chapter-number">Locations</div>
|
|
68
|
+
<h3 class="chapter-title">World Map</h3>
|
|
69
|
+
<div class="chapter-meta">See atmosphere, narrative use, and key landmarks.</div>
|
|
70
|
+
<div class="chip-row"><span class="chip">{locations.length} entries</span></div>
|
|
71
|
+
</a>
|
|
72
|
+
</article>
|
|
73
|
+
<article class="card catalog-card">
|
|
74
|
+
<a href="factions/">
|
|
75
|
+
<div class="chapter-number">Factions</div>
|
|
76
|
+
<h3 class="chapter-title">Power Map</h3>
|
|
77
|
+
<div class="chapter-meta">Inspect factions, ideology, methods, and narrative pressure.</div>
|
|
78
|
+
<div class="chip-row"><span class="chip">{factions.length} entries</span></div>
|
|
79
|
+
</a>
|
|
80
|
+
</article>
|
|
81
|
+
<article class="card catalog-card">
|
|
82
|
+
<a href="items/">
|
|
83
|
+
<div class="chapter-number">Items</div>
|
|
84
|
+
<h3 class="chapter-title">Artifacts</h3>
|
|
85
|
+
<div class="chapter-meta">Track objects, symbols, tools, and plot-driving artifacts.</div>
|
|
86
|
+
<div class="chip-row"><span class="chip">{items.length} entries</span></div>
|
|
87
|
+
</a>
|
|
88
|
+
</article>
|
|
89
|
+
<article class="card catalog-card">
|
|
90
|
+
<a href="secrets/">
|
|
91
|
+
<div class="chapter-number">Secrets</div>
|
|
92
|
+
<h3 class="chapter-title">Spoiler Vault</h3>
|
|
93
|
+
<div class="chapter-meta">Review hidden truths, reveal strategy, and narrative stakes.</div>
|
|
94
|
+
<div class="chip-row"><span class="chip">{secrets.length} entries</span></div>
|
|
95
|
+
</a>
|
|
96
|
+
</article>
|
|
97
|
+
<article class="card catalog-card">
|
|
98
|
+
<a href="timeline/">
|
|
99
|
+
<div class="chapter-number">Timeline</div>
|
|
100
|
+
<h3 class="chapter-title">Chronology</h3>
|
|
101
|
+
<div class="chapter-meta">Follow major events and continuity anchors.</div>
|
|
102
|
+
<div class="chip-row"><span class="chip">{timelineEvents.length} events</span></div>
|
|
103
|
+
</a>
|
|
104
|
+
</article>
|
|
105
|
+
</div>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<section class="section" id="chapters">
|
|
109
|
+
<h2>Chapters</h2>
|
|
110
|
+
<div class="chapter-list">
|
|
111
|
+
{chapterCards.map((chapter) => (
|
|
112
|
+
<article class="chapter-card">
|
|
113
|
+
<a href={`chapters/${chapter.slug}/`}>
|
|
114
|
+
{chapter.figure && <AssetFigure figure={chapter.figure} className="catalog-media" />}
|
|
115
|
+
<div class="chapter-number">Chapter {String(chapter.metadata.number).padStart(3, "0")}</div>
|
|
116
|
+
<h3 class="chapter-title">{chapter.metadata.title}</h3>
|
|
117
|
+
<div class="chapter-meta">{chapter.metadata.summary ?? "Open the chapter to read scenes and notes."}</div>
|
|
118
|
+
</a>
|
|
119
|
+
</article>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
</>
|
|
124
|
+
) : (
|
|
125
|
+
<div class="empty">
|
|
126
|
+
No book repository detected. Initialize one with the Narrarium MCP server, then run this site again with the correct `NARRARIUM_BOOK_ROOT`.
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</BaseLayout>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
4
|
+
import { getBookRoot, loadEntityPageData } from "../../lib/book";
|
|
5
|
+
import MetadataSection from "../../components/MetadataSection.astro";
|
|
6
|
+
import RelatedLinks from "../../components/RelatedLinks.astro";
|
|
7
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
8
|
+
import { loadRelatedCanonLinks } from "../../lib/canon";
|
|
9
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
10
|
+
import { listEntities, pathExists } from "narrarium";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const root = getBookRoot();
|
|
15
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
16
|
+
if (!ready) return [];
|
|
17
|
+
|
|
18
|
+
const entities = await listEntities(root, "item");
|
|
19
|
+
return entities.map((entity) => ({ params: { slug: entity.slug }, props: { slug: entity.slug } }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { slug } = Astro.props;
|
|
23
|
+
const entity = await loadEntityPageData("item", slug);
|
|
24
|
+
const html = await marked.parse(entity.body);
|
|
25
|
+
const metaEntries = [
|
|
26
|
+
["Kind", entity.metadata.item_kind],
|
|
27
|
+
["Appearance", entity.metadata.appearance],
|
|
28
|
+
["Purpose", entity.metadata.purpose],
|
|
29
|
+
["Function in book", entity.metadata.function_in_book],
|
|
30
|
+
["Significance", entity.metadata.significance],
|
|
31
|
+
["Origin story", entity.metadata.origin_story],
|
|
32
|
+
["Powers", entity.metadata.powers],
|
|
33
|
+
["Limitations", entity.metadata.limitations],
|
|
34
|
+
["Owner", entity.metadata.owner],
|
|
35
|
+
["Introduced in", entity.metadata.introduced_in],
|
|
36
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
37
|
+
const relatedLinks = await loadRelatedCanonLinks(String(entity.metadata.id ?? `item:${slug}`), [entity.metadata.refs, ...metaEntries.map(([, value]) => value)]);
|
|
38
|
+
const figure = await loadAssetFigure(String(entity.metadata.id ?? `item:${slug}`), String(entity.metadata.name ?? slug));
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<BaseLayout title={String(entity.metadata.name ?? slug)} description={String(entity.metadata.function_in_book ?? entity.metadata.purpose ?? "Item entry")} activeSection="items">
|
|
42
|
+
<section class="hero">
|
|
43
|
+
<p class="eyebrow">Item</p>
|
|
44
|
+
<h1>{String(entity.metadata.name ?? slug)}</h1>
|
|
45
|
+
<p class="lede">{String(entity.metadata.function_in_book ?? entity.metadata.purpose ?? "Canonical item entry.")}</p>
|
|
46
|
+
<div class="chapter-meta"><a href="items/">Back to items</a></div>
|
|
47
|
+
{figure && <AssetFigure figure={figure} className="hero-media" />}
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section class="section">
|
|
51
|
+
<MetadataSection entries={metaEntries} />
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
{relatedLinks.length > 0 && (
|
|
55
|
+
<section class="section">
|
|
56
|
+
<RelatedLinks links={relatedLinks} />
|
|
57
|
+
</section>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<section class="section">
|
|
61
|
+
<article class="scene prose" set:html={html} />
|
|
62
|
+
</section>
|
|
63
|
+
</BaseLayout>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
3
|
+
import { loadEntityIndexData } from "../../lib/book";
|
|
4
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
5
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
6
|
+
|
|
7
|
+
const { ready, entities } = await loadEntityIndexData("item");
|
|
8
|
+
const cards = await Promise.all(
|
|
9
|
+
entities.map(async (entity) => ({
|
|
10
|
+
...entity,
|
|
11
|
+
figure: await loadAssetFigure(String(entity.metadata.id ?? `item:${entity.slug}`), String(entity.metadata.name ?? entity.slug)),
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<BaseLayout title="Items" description="Browse the items and artifacts of the book." activeSection="items">
|
|
17
|
+
<section class="hero">
|
|
18
|
+
<p class="eyebrow">Canon Atlas</p>
|
|
19
|
+
<h1>Items</h1>
|
|
20
|
+
<p class="lede">Track weapons, documents, relics, tools, and symbolic objects driving the narrative.</p>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
{ready ? (
|
|
24
|
+
<section class="section">
|
|
25
|
+
<div class="catalog-grid">
|
|
26
|
+
{cards.map((entity) => (
|
|
27
|
+
<article class="card catalog-card">
|
|
28
|
+
<a href={`items/${entity.slug}/`}>
|
|
29
|
+
{entity.figure && <AssetFigure figure={entity.figure} className="catalog-media" />}
|
|
30
|
+
<div class="chapter-number">{String(entity.metadata.item_kind ?? "item")}</div>
|
|
31
|
+
<h2 class="chapter-title">{String(entity.metadata.name ?? entity.slug)}</h2>
|
|
32
|
+
<div class="chapter-meta">{String(entity.metadata.function_in_book ?? entity.metadata.purpose ?? "Open the entry to inspect use, origin, and significance.")}</div>
|
|
33
|
+
<div class="chip-row">
|
|
34
|
+
{entity.metadata.owner && <span class="chip">{String(entity.metadata.owner)}</span>}
|
|
35
|
+
{entity.metadata.introduced_in && <span class="chip">{String(entity.metadata.introduced_in)}</span>}
|
|
36
|
+
</div>
|
|
37
|
+
</a>
|
|
38
|
+
</article>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
) : (
|
|
43
|
+
<div class="empty">No book repository detected for the reader.</div>
|
|
44
|
+
)}
|
|
45
|
+
</BaseLayout>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
4
|
+
import { getBookRoot, loadEntityPageData } from "../../lib/book";
|
|
5
|
+
import MetadataSection from "../../components/MetadataSection.astro";
|
|
6
|
+
import RelatedLinks from "../../components/RelatedLinks.astro";
|
|
7
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
8
|
+
import { loadRelatedCanonLinks } from "../../lib/canon";
|
|
9
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
10
|
+
import { listEntities, pathExists } from "narrarium";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const root = getBookRoot();
|
|
15
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
16
|
+
if (!ready) return [];
|
|
17
|
+
|
|
18
|
+
const entities = await listEntities(root, "location");
|
|
19
|
+
return entities.map((entity) => ({ params: { slug: entity.slug }, props: { slug: entity.slug } }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { slug } = Astro.props;
|
|
23
|
+
const entity = await loadEntityPageData("location", slug);
|
|
24
|
+
const html = await marked.parse(entity.body);
|
|
25
|
+
const metaEntries = [
|
|
26
|
+
["Kind", entity.metadata.location_kind],
|
|
27
|
+
["Region", entity.metadata.region],
|
|
28
|
+
["Atmosphere", entity.metadata.atmosphere],
|
|
29
|
+
["Function in book", entity.metadata.function_in_book],
|
|
30
|
+
["Landmarks", entity.metadata.landmarks],
|
|
31
|
+
["Risks", entity.metadata.risks],
|
|
32
|
+
["Factions present", entity.metadata.factions_present],
|
|
33
|
+
["Timeline", entity.metadata.timeline_ref],
|
|
34
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
35
|
+
const relatedLinks = await loadRelatedCanonLinks(String(entity.metadata.id ?? `location:${slug}`), [entity.metadata.refs, ...metaEntries.map(([, value]) => value)]);
|
|
36
|
+
const figure = await loadAssetFigure(String(entity.metadata.id ?? `location:${slug}`), String(entity.metadata.name ?? slug));
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<BaseLayout title={String(entity.metadata.name ?? slug)} description={String(entity.metadata.function_in_book ?? entity.metadata.atmosphere ?? "Location entry") } activeSection="locations">
|
|
40
|
+
<section class="hero">
|
|
41
|
+
<p class="eyebrow">Location</p>
|
|
42
|
+
<h1>{String(entity.metadata.name ?? slug)}</h1>
|
|
43
|
+
<p class="lede">{String(entity.metadata.atmosphere ?? entity.metadata.function_in_book ?? "Canonical location entry.")}</p>
|
|
44
|
+
<div class="chapter-meta"><a href="locations/">Back to locations</a></div>
|
|
45
|
+
{figure && <AssetFigure figure={figure} className="hero-media" />}
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<section class="section">
|
|
49
|
+
<MetadataSection entries={metaEntries} />
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
{relatedLinks.length > 0 && (
|
|
53
|
+
<section class="section">
|
|
54
|
+
<RelatedLinks links={relatedLinks} />
|
|
55
|
+
</section>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<section class="section">
|
|
59
|
+
<article class="scene prose" set:html={html} />
|
|
60
|
+
</section>
|
|
61
|
+
</BaseLayout>
|