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,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("location");
|
|
8
|
+
const cards = await Promise.all(
|
|
9
|
+
entities.map(async (entity) => ({
|
|
10
|
+
...entity,
|
|
11
|
+
figure: await loadAssetFigure(String(entity.metadata.id ?? `location:${entity.slug}`), String(entity.metadata.name ?? entity.slug)),
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<BaseLayout title="Locations" description="Browse the book locations." activeSection="locations">
|
|
17
|
+
<section class="hero">
|
|
18
|
+
<p class="eyebrow">Canon Atlas</p>
|
|
19
|
+
<h1>Locations</h1>
|
|
20
|
+
<p class="lede">Track atmosphere, landmarks, risks, and narrative function across the world of the book.</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={`locations/${entity.slug}/`}>
|
|
29
|
+
{entity.figure && <AssetFigure figure={entity.figure} className="catalog-media" />}
|
|
30
|
+
<div class="chapter-number">{String(entity.metadata.location_kind ?? "location")}</div>
|
|
31
|
+
<h2 class="chapter-title">{String(entity.metadata.name ?? entity.slug)}</h2>
|
|
32
|
+
<div class="chapter-meta">{String(entity.metadata.atmosphere ?? entity.metadata.function_in_book ?? "Open the entry to inspect place notes.")}</div>
|
|
33
|
+
<div class="chip-row">
|
|
34
|
+
{entity.metadata.region && <span class="chip">{String(entity.metadata.region)}</span>}
|
|
35
|
+
{entity.metadata.based_on_real_place && <span class="chip">real-world basis</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,67 @@
|
|
|
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, "secret");
|
|
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("secret", slug);
|
|
24
|
+
const html = await marked.parse(entity.body);
|
|
25
|
+
const metaEntries = [
|
|
26
|
+
["Kind", entity.metadata.secret_kind],
|
|
27
|
+
["Function in book", entity.metadata.function_in_book],
|
|
28
|
+
["Stakes", entity.metadata.stakes],
|
|
29
|
+
["Holders", entity.metadata.holders],
|
|
30
|
+
["Protected by", entity.metadata.protected_by],
|
|
31
|
+
["False beliefs", entity.metadata.false_beliefs],
|
|
32
|
+
["Reveal strategy", entity.metadata.reveal_strategy],
|
|
33
|
+
["Reveal in", entity.metadata.reveal_in],
|
|
34
|
+
["Known from", entity.metadata.known_from],
|
|
35
|
+
["Timeline", entity.metadata.timeline_ref],
|
|
36
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
37
|
+
const relatedLinks = await loadRelatedCanonLinks(String(entity.metadata.id ?? `secret:${slug}`), [entity.metadata.refs, ...metaEntries.map(([, value]) => value)]);
|
|
38
|
+
const figure = await loadAssetFigure(String(entity.metadata.id ?? `secret:${slug}`), String(entity.metadata.title ?? slug));
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<BaseLayout title={String(entity.metadata.title ?? slug)} description={String(entity.metadata.function_in_book ?? entity.metadata.stakes ?? "Secret entry")} activeSection="secrets">
|
|
42
|
+
<section class="hero">
|
|
43
|
+
<p class="eyebrow">Secret</p>
|
|
44
|
+
<h1>{String(entity.metadata.title ?? slug)}</h1>
|
|
45
|
+
<p class="lede">{String(entity.metadata.function_in_book ?? entity.metadata.stakes ?? "Canonical secret entry.")}</p>
|
|
46
|
+
<div class="chapter-meta"><a href="secrets/">Back to secrets</a></div>
|
|
47
|
+
{figure && <AssetFigure figure={figure} className="hero-media" />}
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section class="section">
|
|
51
|
+
<div class="empty">Spoiler warning: this page exposes secret holders, reveal thresholds, and consequences.</div>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
<section class="section">
|
|
55
|
+
<MetadataSection entries={metaEntries} />
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
{relatedLinks.length > 0 && (
|
|
59
|
+
<section class="section">
|
|
60
|
+
<RelatedLinks links={relatedLinks} />
|
|
61
|
+
</section>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<section class="section">
|
|
65
|
+
<article class="scene prose" set:html={html} />
|
|
66
|
+
</section>
|
|
67
|
+
</BaseLayout>
|
|
@@ -0,0 +1,46 @@
|
|
|
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("secret");
|
|
8
|
+
const cards = await Promise.all(
|
|
9
|
+
entities.map(async (entity) => ({
|
|
10
|
+
...entity,
|
|
11
|
+
figure: await loadAssetFigure(String(entity.metadata.id ?? `secret:${entity.slug}`), String(entity.metadata.title ?? entity.slug)),
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<BaseLayout title="Secrets" description="Browse the spoiler vault of the book." activeSection="secrets">
|
|
17
|
+
<section class="hero">
|
|
18
|
+
<p class="eyebrow">Canon Atlas</p>
|
|
19
|
+
<h1>Secrets</h1>
|
|
20
|
+
<p class="lede">Spoiler-facing vault for hidden truths, reveal strategy, and narrative stakes across the repository.</p>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
{ready ? (
|
|
24
|
+
<section class="section">
|
|
25
|
+
<div class="empty">Warning: this section is spoiler-heavy and surfaces canonical secrets directly.</div>
|
|
26
|
+
<div class="catalog-grid">
|
|
27
|
+
{cards.map((entity) => (
|
|
28
|
+
<article class="card catalog-card">
|
|
29
|
+
<a href={`secrets/${entity.slug}/`}>
|
|
30
|
+
{entity.figure && <AssetFigure figure={entity.figure} className="catalog-media" />}
|
|
31
|
+
<div class="chapter-number">{String(entity.metadata.secret_kind ?? "secret")}</div>
|
|
32
|
+
<h2 class="chapter-title">{String(entity.metadata.title ?? entity.slug)}</h2>
|
|
33
|
+
<div class="chapter-meta">{String(entity.metadata.function_in_book ?? entity.metadata.stakes ?? "Open the entry to inspect holders, thresholds, and consequences.")}</div>
|
|
34
|
+
<div class="chip-row">
|
|
35
|
+
{entity.metadata.reveal_in && <span class="chip">{String(entity.metadata.reveal_in)}</span>}
|
|
36
|
+
{entity.metadata.known_from && <span class="chip">{String(entity.metadata.known_from)}</span>}
|
|
37
|
+
</div>
|
|
38
|
+
</a>
|
|
39
|
+
</article>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
) : (
|
|
44
|
+
<div class="empty">No book repository detected for the reader.</div>
|
|
45
|
+
)}
|
|
46
|
+
</BaseLayout>
|
|
@@ -0,0 +1,58 @@
|
|
|
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, "timeline-event");
|
|
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("timeline-event", slug);
|
|
24
|
+
const html = await marked.parse(entity.body);
|
|
25
|
+
const metaEntries = [
|
|
26
|
+
["Date", entity.metadata.date],
|
|
27
|
+
["Participants", entity.metadata.participants],
|
|
28
|
+
["Significance", entity.metadata.significance],
|
|
29
|
+
["Function in book", entity.metadata.function_in_book],
|
|
30
|
+
["Consequences", entity.metadata.consequences],
|
|
31
|
+
].filter(([, value]) => value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0));
|
|
32
|
+
const relatedLinks = await loadRelatedCanonLinks(String(entity.metadata.id ?? `timeline-event:${slug}`), [entity.metadata.refs, ...metaEntries.map(([, value]) => value)]);
|
|
33
|
+
const figure = await loadAssetFigure(String(entity.metadata.id ?? `timeline-event:${slug}`), String(entity.metadata.title ?? slug));
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<BaseLayout title={String(entity.metadata.title ?? slug)} description={String(entity.metadata.significance ?? entity.metadata.function_in_book ?? "Timeline event") } activeSection="timeline">
|
|
37
|
+
<section class="hero">
|
|
38
|
+
<p class="eyebrow">Timeline Event</p>
|
|
39
|
+
<h1>{String(entity.metadata.title ?? slug)}</h1>
|
|
40
|
+
<p class="lede">{String(entity.metadata.significance ?? entity.metadata.function_in_book ?? "Canonical timeline event.")}</p>
|
|
41
|
+
<div class="chapter-meta"><a href="timeline/">Back to timeline</a></div>
|
|
42
|
+
{figure && <AssetFigure figure={figure} className="hero-media" />}
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section class="section">
|
|
46
|
+
<MetadataSection entries={metaEntries} />
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
{relatedLinks.length > 0 && (
|
|
50
|
+
<section class="section">
|
|
51
|
+
<RelatedLinks links={relatedLinks} />
|
|
52
|
+
</section>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
<section class="section">
|
|
56
|
+
<article class="scene prose" set:html={html} />
|
|
57
|
+
</section>
|
|
58
|
+
</BaseLayout>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
4
|
+
import { loadTimelinePageData } from "../../lib/book";
|
|
5
|
+
import AssetFigure from "../../components/AssetFigure.astro";
|
|
6
|
+
import { loadAssetFigure } from "../../lib/assets";
|
|
7
|
+
|
|
8
|
+
const { ready, main, events } = await loadTimelinePageData();
|
|
9
|
+
const mainHtml = main?.body ? await marked.parse(main.body) : "";
|
|
10
|
+
const cards = await Promise.all(
|
|
11
|
+
events.map(async (entity) => ({
|
|
12
|
+
...entity,
|
|
13
|
+
figure: await loadAssetFigure(String(entity.metadata.id ?? `timeline-event:${entity.slug}`), String(entity.metadata.title ?? entity.slug)),
|
|
14
|
+
})),
|
|
15
|
+
);
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<BaseLayout title="Timeline" description="Browse the chronology of the book." activeSection="timeline">
|
|
19
|
+
<section class="hero">
|
|
20
|
+
<p class="eyebrow">Canon Atlas</p>
|
|
21
|
+
<h1>Timeline</h1>
|
|
22
|
+
<p class="lede">Follow the main chronology and inspect event notes used to anchor continuity.</p>
|
|
23
|
+
</section>
|
|
24
|
+
|
|
25
|
+
{ready ? (
|
|
26
|
+
<>
|
|
27
|
+
{main && (
|
|
28
|
+
<section class="section">
|
|
29
|
+
<article class="scene prose" set:html={mainHtml} />
|
|
30
|
+
</section>
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
<section class="section">
|
|
34
|
+
<h2>Events</h2>
|
|
35
|
+
<div class="catalog-grid">
|
|
36
|
+
{cards.map((entity) => (
|
|
37
|
+
<article class="card catalog-card">
|
|
38
|
+
<a href={`timeline/${entity.slug}/`}>
|
|
39
|
+
{entity.figure && <AssetFigure figure={entity.figure} className="catalog-media" />}
|
|
40
|
+
<div class="chapter-number">{String(entity.metadata.date ?? "timeline event")}</div>
|
|
41
|
+
<h2 class="chapter-title">{String(entity.metadata.title ?? entity.slug)}</h2>
|
|
42
|
+
<div class="chapter-meta">{String(entity.metadata.significance ?? entity.metadata.function_in_book ?? "Open the event for chronology and consequences.")}</div>
|
|
43
|
+
</a>
|
|
44
|
+
</article>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
</>
|
|
49
|
+
) : (
|
|
50
|
+
<div class="empty">No book repository detected for the reader.</div>
|
|
51
|
+
)}
|
|
52
|
+
</BaseLayout>
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { cp, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
type ScaffoldOptions = {
|
|
6
|
+
bookRoot?: string;
|
|
7
|
+
packageName?: string;
|
|
8
|
+
coreDependency?: string;
|
|
9
|
+
pagesDomain?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function scaffoldReaderSite(targetDir: string, options: ScaffoldOptions = {}) {
|
|
13
|
+
const targetRoot = path.resolve(targetDir);
|
|
14
|
+
const packageRoot = path.dirname(fileURLToPath(new URL("../package.json", import.meta.url)));
|
|
15
|
+
const bookRoot = options.bookRoot ?? "..";
|
|
16
|
+
const packageName = options.packageName ?? inferPackageName(targetRoot);
|
|
17
|
+
const coreDependency = options.coreDependency ?? `^${await readCurrentPackageVersion(packageRoot)}`;
|
|
18
|
+
const pagesDomain = options.pagesDomain?.trim() || undefined;
|
|
19
|
+
|
|
20
|
+
await mkdir(path.join(targetRoot, "src", "layouts"), { recursive: true });
|
|
21
|
+
await mkdir(path.join(targetRoot, "src", "lib"), { recursive: true });
|
|
22
|
+
await mkdir(path.join(targetRoot, "src", "pages"), { recursive: true });
|
|
23
|
+
await mkdir(path.join(targetRoot, "src", "components"), { recursive: true });
|
|
24
|
+
await mkdir(path.join(targetRoot, "scripts"), { recursive: true });
|
|
25
|
+
await mkdir(path.join(targetRoot, ".github", "workflows"), { recursive: true });
|
|
26
|
+
await mkdir(path.join(targetRoot, "public", "downloads"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
await Promise.all([
|
|
29
|
+
copyFile(path.join(packageRoot, "astro.config.mjs"), path.join(targetRoot, "astro.config.mjs")),
|
|
30
|
+
copyFile(path.join(packageRoot, "tsconfig.json"), path.join(targetRoot, "tsconfig.json")),
|
|
31
|
+
cp(path.join(packageRoot, "src", "components"), path.join(targetRoot, "src", "components"), { recursive: true }),
|
|
32
|
+
cp(path.join(packageRoot, "src", "lib"), path.join(targetRoot, "src", "lib"), { recursive: true }),
|
|
33
|
+
cp(path.join(packageRoot, "src", "layouts"), path.join(targetRoot, "src", "layouts"), { recursive: true }),
|
|
34
|
+
cp(path.join(packageRoot, "src", "pages"), path.join(targetRoot, "src", "pages"), { recursive: true }),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
await writeFile(
|
|
38
|
+
path.join(targetRoot, "package.json"),
|
|
39
|
+
JSON.stringify(
|
|
40
|
+
{
|
|
41
|
+
name: packageName,
|
|
42
|
+
private: true,
|
|
43
|
+
type: "module",
|
|
44
|
+
scripts: {
|
|
45
|
+
"export:epub": "node ./scripts/export-epub.mjs",
|
|
46
|
+
dev: "npm run export:epub && astro dev",
|
|
47
|
+
build: "npm run export:epub && astro build",
|
|
48
|
+
preview: "astro preview",
|
|
49
|
+
},
|
|
50
|
+
dependencies: {
|
|
51
|
+
"narrarium": coreDependency,
|
|
52
|
+
astro: "^5.14.1",
|
|
53
|
+
marked: "^16.3.0",
|
|
54
|
+
},
|
|
55
|
+
devDependencies: {
|
|
56
|
+
"@types/node": "^24.6.0",
|
|
57
|
+
typescript: "^5.9.3",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
null,
|
|
61
|
+
2,
|
|
62
|
+
) + "\n",
|
|
63
|
+
"utf8",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await writeFile(
|
|
67
|
+
path.join(targetRoot, "src", "lib", "book-config.ts"),
|
|
68
|
+
`export const defaultBookRoot = ${JSON.stringify(toPosix(bookRoot))};\n`,
|
|
69
|
+
"utf8",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await writeFile(path.join(targetRoot, "scripts", "export-epub.mjs"), buildExportEpubScript(bookRoot), "utf8");
|
|
73
|
+
await writeFile(
|
|
74
|
+
path.join(targetRoot, ".github", "workflows", "deploy-pages.yml"),
|
|
75
|
+
buildPagesWorkflow(pagesDomain),
|
|
76
|
+
"utf8",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (pagesDomain) {
|
|
80
|
+
await writeFile(path.join(targetRoot, "public", "CNAME"), `${pagesDomain}\n`, "utf8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await writeFile(path.join(targetRoot, ".env.example"), `NARRARIUM_BOOK_ROOT=${toPosix(bookRoot)}\n`, "utf8");
|
|
84
|
+
await writeFile(path.join(targetRoot, ".gitignore"), "node_modules/\ndist/\n.astro/\n.env\npublic/downloads/\n", "utf8");
|
|
85
|
+
await writeFile(
|
|
86
|
+
path.join(targetRoot, "README.md"),
|
|
87
|
+
buildReaderReadme(bookRoot),
|
|
88
|
+
"utf8",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
targetRoot,
|
|
93
|
+
packageName,
|
|
94
|
+
coreDependency,
|
|
95
|
+
bookRoot,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildReaderReadme(bookRoot: string): string {
|
|
100
|
+
return `# Narrarium Reader Site
|
|
101
|
+
|
|
102
|
+
This site was scaffolded from \`narrarium-astro-reader\`.
|
|
103
|
+
|
|
104
|
+
## Configure
|
|
105
|
+
|
|
106
|
+
Set the book root in a local environment file:
|
|
107
|
+
|
|
108
|
+
\`\`\`bash
|
|
109
|
+
NARRARIUM_BOOK_ROOT=${toPosix(bookRoot)}
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
## Run
|
|
113
|
+
|
|
114
|
+
\`\`\`bash
|
|
115
|
+
npm install
|
|
116
|
+
npm run dev
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
The dev server exports a fresh EPUB to \`public/downloads/book.epub\` before Astro starts.
|
|
120
|
+
|
|
121
|
+
## Build
|
|
122
|
+
|
|
123
|
+
\`\`\`bash
|
|
124
|
+
npm run build
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
The build also refreshes the EPUB automatically and ships a ready-to-deploy static site.
|
|
128
|
+
|
|
129
|
+
## GitHub Pages
|
|
130
|
+
|
|
131
|
+
A starter workflow already exists in \`.github/workflows/deploy-pages.yml\`.
|
|
132
|
+
By default it deploys to standard GitHub Pages using the repository name as the base path.
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildExportEpubScript(bookRoot: string): string {
|
|
137
|
+
return `import { mkdir } from "node:fs/promises";
|
|
138
|
+
import path from "node:path";
|
|
139
|
+
import { exportEpub } from "narrarium";
|
|
140
|
+
|
|
141
|
+
const configured = process.env.NARRARIUM_BOOK_ROOT ?? process.env.GHOSTWRITER_BOOK_ROOT;
|
|
142
|
+
const root = path.resolve(process.cwd(), configured ?? ${JSON.stringify(toPosix(bookRoot))});
|
|
143
|
+
const outputPath = path.resolve(process.cwd(), "public", "downloads", "book.epub");
|
|
144
|
+
|
|
145
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
146
|
+
const result = await exportEpub(root, { outputPath });
|
|
147
|
+
console.log(\`Exported EPUB with \${result.chapterCount} chapters to \${result.outputPath}\`);
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildPagesWorkflow(pagesDomain?: string): string {
|
|
152
|
+
const envBlock = pagesDomain
|
|
153
|
+
? [" SITE_BASE: /", ` SITE_URL: https://${pagesDomain}`].join("\n")
|
|
154
|
+
: " SITE_BASE: /${{ github.event.repository.name }}/";
|
|
155
|
+
|
|
156
|
+
return `name: Deploy Reader To GitHub Pages
|
|
157
|
+
|
|
158
|
+
on:
|
|
159
|
+
workflow_dispatch:
|
|
160
|
+
push:
|
|
161
|
+
branches:
|
|
162
|
+
- main
|
|
163
|
+
- master
|
|
164
|
+
|
|
165
|
+
permissions:
|
|
166
|
+
contents: read
|
|
167
|
+
pages: write
|
|
168
|
+
id-token: write
|
|
169
|
+
|
|
170
|
+
concurrency:
|
|
171
|
+
group: github-pages
|
|
172
|
+
cancel-in-progress: true
|
|
173
|
+
|
|
174
|
+
jobs:
|
|
175
|
+
build:
|
|
176
|
+
runs-on: ubuntu-latest
|
|
177
|
+
steps:
|
|
178
|
+
- name: Checkout
|
|
179
|
+
uses: actions/checkout@v4
|
|
180
|
+
|
|
181
|
+
- name: Setup Node
|
|
182
|
+
uses: actions/setup-node@v4
|
|
183
|
+
with:
|
|
184
|
+
node-version: 22
|
|
185
|
+
cache: npm
|
|
186
|
+
|
|
187
|
+
- name: Install dependencies
|
|
188
|
+
run: npm ci
|
|
189
|
+
|
|
190
|
+
- name: Configure Pages
|
|
191
|
+
uses: actions/configure-pages@v5
|
|
192
|
+
|
|
193
|
+
- name: Build site
|
|
194
|
+
env:
|
|
195
|
+
${envBlock}
|
|
196
|
+
run: npm run build
|
|
197
|
+
|
|
198
|
+
- name: Upload artifact
|
|
199
|
+
uses: actions/upload-pages-artifact@v3
|
|
200
|
+
with:
|
|
201
|
+
path: dist
|
|
202
|
+
|
|
203
|
+
deploy:
|
|
204
|
+
needs: build
|
|
205
|
+
runs-on: ubuntu-latest
|
|
206
|
+
environment:
|
|
207
|
+
name: github-pages
|
|
208
|
+
url: \${{ steps.deployment.outputs.page_url }}
|
|
209
|
+
steps:
|
|
210
|
+
- name: Deploy
|
|
211
|
+
id: deployment
|
|
212
|
+
uses: actions/deploy-pages@v4
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function toPosix(value: string): string {
|
|
217
|
+
return value.split(path.sep).join("/");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function inferPackageName(targetRoot: string): string {
|
|
221
|
+
const base = path.basename(targetRoot)
|
|
222
|
+
.toLowerCase()
|
|
223
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
224
|
+
.replace(/^-+|-+$/g, "") || "narrarium-reader-site";
|
|
225
|
+
return base;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function readCurrentPackageVersion(packageRoot: string): Promise<string> {
|
|
229
|
+
const raw = await readFile(path.join(packageRoot, "package.json"), "utf8");
|
|
230
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
231
|
+
return parsed.version ?? "0.1.0";
|
|
232
|
+
}
|