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.
Files changed (67) hide show
  1. package/README.md +52 -0
  2. package/astro.config.mjs +12 -0
  3. package/cli-dist/cli.d.ts +3 -0
  4. package/cli-dist/cli.d.ts.map +1 -0
  5. package/cli-dist/cli.js +78 -0
  6. package/cli-dist/cli.js.map +1 -0
  7. package/cli-dist/lib/assets.d.ts +8 -0
  8. package/cli-dist/lib/assets.d.ts.map +1 -0
  9. package/cli-dist/lib/assets.js +35 -0
  10. package/cli-dist/lib/assets.js.map +1 -0
  11. package/cli-dist/lib/book-config.d.ts +2 -0
  12. package/cli-dist/lib/book-config.d.ts.map +1 -0
  13. package/cli-dist/lib/book-config.js +2 -0
  14. package/cli-dist/lib/book-config.js.map +1 -0
  15. package/cli-dist/lib/book.d.ts +103 -0
  16. package/cli-dist/lib/book.d.ts.map +1 -0
  17. package/cli-dist/lib/book.js +89 -0
  18. package/cli-dist/lib/book.js.map +1 -0
  19. package/cli-dist/lib/canon.d.ts +16 -0
  20. package/cli-dist/lib/canon.d.ts.map +1 -0
  21. package/cli-dist/lib/canon.js +164 -0
  22. package/cli-dist/lib/canon.js.map +1 -0
  23. package/cli-dist/lib/glossary.d.ts +25 -0
  24. package/cli-dist/lib/glossary.d.ts.map +1 -0
  25. package/cli-dist/lib/glossary.js +195 -0
  26. package/cli-dist/lib/glossary.js.map +1 -0
  27. package/cli-dist/lib/search.d.ts +9 -0
  28. package/cli-dist/lib/search.d.ts.map +1 -0
  29. package/cli-dist/lib/search.js +55 -0
  30. package/cli-dist/lib/search.js.map +1 -0
  31. package/cli-dist/scaffold.d.ts +14 -0
  32. package/cli-dist/scaffold.d.ts.map +1 -0
  33. package/cli-dist/scaffold.js +190 -0
  34. package/cli-dist/scaffold.js.map +1 -0
  35. package/package.json +58 -0
  36. package/scripts/export-epub.mjs +13 -0
  37. package/src/cli.ts +96 -0
  38. package/src/components/AssetFigure.astro +17 -0
  39. package/src/components/ChapterPager.astro +40 -0
  40. package/src/components/LinkedValue.astro +18 -0
  41. package/src/components/MetadataSection.astro +22 -0
  42. package/src/components/ReaderRuntime.astro +310 -0
  43. package/src/components/RelatedLinks.astro +19 -0
  44. package/src/components/SiteSearch.astro +91 -0
  45. package/src/layouts/BaseLayout.astro +676 -0
  46. package/src/lib/assets.ts +44 -0
  47. package/src/lib/book-config.ts +1 -0
  48. package/src/lib/book.ts +116 -0
  49. package/src/lib/canon.ts +212 -0
  50. package/src/lib/glossary.ts +247 -0
  51. package/src/lib/search.ts +74 -0
  52. package/src/pages/chapters/[chapter].astro +102 -0
  53. package/src/pages/characters/[slug].astro +65 -0
  54. package/src/pages/characters/index.astro +45 -0
  55. package/src/pages/factions/[slug].astro +64 -0
  56. package/src/pages/factions/index.astro +45 -0
  57. package/src/pages/index.astro +129 -0
  58. package/src/pages/items/[slug].astro +63 -0
  59. package/src/pages/items/index.astro +45 -0
  60. package/src/pages/locations/[slug].astro +61 -0
  61. package/src/pages/locations/index.astro +45 -0
  62. package/src/pages/secrets/[slug].astro +67 -0
  63. package/src/pages/secrets/index.astro +46 -0
  64. package/src/pages/timeline/[slug].astro +58 -0
  65. package/src/pages/timeline/index.astro +52 -0
  66. package/src/scaffold.ts +232 -0
  67. 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>
@@ -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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "baseUrl": "."
5
+ }
6
+ }