@sorane/core 0.2.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/package.json +42 -0
- package/src/ai-disclosure.ts +181 -0
- package/src/asset-provenance.ts +141 -0
- package/src/associated-media.ts +93 -0
- package/src/blog-pages.ts +175 -0
- package/src/build.ts +1109 -0
- package/src/c2pa-pass.ts +116 -0
- package/src/catalog.ts +61 -0
- package/src/config.ts +255 -0
- package/src/diagrams/compile-d2.ts +70 -0
- package/src/diagrams/compile-graphviz.ts +71 -0
- package/src/diagrams/compile-mermaid.ts +102 -0
- package/src/diagrams/diagram-hash.ts +5 -0
- package/src/diagrams/diagram-meta.ts +74 -0
- package/src/diagrams/emit-diagram-assets.ts +135 -0
- package/src/diagrams/mermaid-head.ts +6 -0
- package/src/diagrams/needs-async-compile.ts +12 -0
- package/src/diagrams/parse-diagram-fence.ts +109 -0
- package/src/diagrams/rehype-diagram-pre.ts +39 -0
- package/src/diagrams/remark-inject-built-figures.ts +32 -0
- package/src/diagrams/render-async.ts +241 -0
- package/src/diagrams/render-body-section.ts +52 -0
- package/src/diagrams/validate-diagram-alt.ts +56 -0
- package/src/docs.ts +257 -0
- package/src/emit-page.ts +87 -0
- package/src/index.ts +49 -0
- package/src/iptc-xmp-pass.ts +94 -0
- package/src/markdown-image-refs.ts +135 -0
- package/src/migrate.ts +60 -0
- package/src/not-found.ts +64 -0
- package/src/og-meta.ts +18 -0
- package/src/render.ts +233 -0
- package/src/site-labels.ts +97 -0
- package/src/site-meta.ts +138 -0
- package/src/ssg.ts +676 -0
- package/src/static-assets.ts +198 -0
- package/src/theme-assets.ts +16 -0
- package/src/validate-heading-structure.ts +51 -0
package/src/build.ts
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
import { createFontProcessor, plainTextFromHtml } from "@sorane/font";
|
|
2
|
+
import { emitSearchAssets } from "@sorane/search";
|
|
3
|
+
import {
|
|
4
|
+
parseConcept,
|
|
5
|
+
buildBundleEntries,
|
|
6
|
+
type ParsedConcept,
|
|
7
|
+
} from "@sorane/okf";
|
|
8
|
+
import {
|
|
9
|
+
copyFileSync,
|
|
10
|
+
cpSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
statSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { gzipSync } from "node:zlib";
|
|
20
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
21
|
+
import {
|
|
22
|
+
buildAiBadgeHtml,
|
|
23
|
+
parseAiDisclosure,
|
|
24
|
+
resolveAiDisclosureFlags,
|
|
25
|
+
} from "./ai-disclosure.ts";
|
|
26
|
+
import { buildCatalogJsonLd } from "./catalog.ts";
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_DIAGRAMS_CONFIG,
|
|
29
|
+
mergeConfig,
|
|
30
|
+
resolvePermalink,
|
|
31
|
+
type SoraneConfig,
|
|
32
|
+
} from "./config.ts";
|
|
33
|
+
import {
|
|
34
|
+
buildBlogPostingJsonLd,
|
|
35
|
+
extractDescription,
|
|
36
|
+
renderArticleBodyWithMetaForConfig,
|
|
37
|
+
renderBlogIndexBody,
|
|
38
|
+
renderIndexBody,
|
|
39
|
+
buildSearchMount,
|
|
40
|
+
buildSearchHead,
|
|
41
|
+
isSearchView,
|
|
42
|
+
renderFeaturedExcerpt,
|
|
43
|
+
sanitizeListDescription,
|
|
44
|
+
buildWebSiteJsonLd,
|
|
45
|
+
rootPrefixFromRel,
|
|
46
|
+
type ArticleListEntry,
|
|
47
|
+
type ArticleNav,
|
|
48
|
+
} from "./ssg.ts";
|
|
49
|
+
import type { FeaturedMode } from "./config.ts";
|
|
50
|
+
import {
|
|
51
|
+
groupByTag,
|
|
52
|
+
groupByYear,
|
|
53
|
+
groupByYearMonth,
|
|
54
|
+
paginate,
|
|
55
|
+
renderArchiveListBody,
|
|
56
|
+
renderMonthListForYear,
|
|
57
|
+
renderYearArchiveIndexBody,
|
|
58
|
+
slugifyTag,
|
|
59
|
+
} from "./blog-pages.ts";
|
|
60
|
+
import { emitPage } from "./emit-page.ts";
|
|
61
|
+
import { siteChromeText } from "./site-labels.ts";
|
|
62
|
+
import type { OkfConcept } from "@sorane/okf";
|
|
63
|
+
import {
|
|
64
|
+
buildAtomFeed,
|
|
65
|
+
buildLlmsTxt,
|
|
66
|
+
buildRobotsTxt,
|
|
67
|
+
buildSitemapXml,
|
|
68
|
+
type FeedEntry,
|
|
69
|
+
type SiteEntry,
|
|
70
|
+
} from "./site-meta.ts";
|
|
71
|
+
import {
|
|
72
|
+
diagramHeadForPage,
|
|
73
|
+
emptyDiagramMeta,
|
|
74
|
+
mergeDiagramMeta,
|
|
75
|
+
} from "./diagrams/diagram-meta.ts";
|
|
76
|
+
import {
|
|
77
|
+
contentHasMermaidFences,
|
|
78
|
+
emitDiagramAssets,
|
|
79
|
+
} from "./diagrams/emit-diagram-assets.ts";
|
|
80
|
+
import {
|
|
81
|
+
renderBodySectionForConfig,
|
|
82
|
+
type BodySectionOptions,
|
|
83
|
+
} from "./diagrams/render-body-section.ts";
|
|
84
|
+
import { isD2CompileEnabled } from "./diagrams/compile-d2.ts";
|
|
85
|
+
import { isGraphvizCompileEnabled } from "./diagrams/compile-graphviz.ts";
|
|
86
|
+
import { isMermaidBuildEnabled } from "./diagrams/compile-mermaid.ts";
|
|
87
|
+
import { resolveThemeAssetDir } from "./theme-assets.ts";
|
|
88
|
+
import {
|
|
89
|
+
docsNavFor,
|
|
90
|
+
docsSidebarHtml,
|
|
91
|
+
renderDocsArticleFromConceptWithMetaForConfig,
|
|
92
|
+
renderDocsIndexBody,
|
|
93
|
+
resolveDocsNav,
|
|
94
|
+
} from "./docs.ts";
|
|
95
|
+
import type { DiagramRenderMeta } from "./diagrams/diagram-meta.ts";
|
|
96
|
+
import { buildAssociatedMediaForArticle } from "./associated-media.ts";
|
|
97
|
+
import { loadAssetProvenance } from "./asset-provenance.ts";
|
|
98
|
+
import {
|
|
99
|
+
collectMarkdownImageRefs,
|
|
100
|
+
dedupeMarkdownImageRefs,
|
|
101
|
+
} from "./markdown-image-refs.ts";
|
|
102
|
+
import { processStaticAssets } from "./static-assets.ts";
|
|
103
|
+
import {
|
|
104
|
+
isNotFoundSource,
|
|
105
|
+
notFoundBodySource,
|
|
106
|
+
notFoundLabels,
|
|
107
|
+
renderCustomNotFoundBody,
|
|
108
|
+
renderDefaultNotFoundBody,
|
|
109
|
+
} from "./not-found.ts";
|
|
110
|
+
|
|
111
|
+
export interface BuildOptions {
|
|
112
|
+
readonly cwd: string;
|
|
113
|
+
readonly config: Partial<SoraneConfig>;
|
|
114
|
+
readonly clean?: boolean;
|
|
115
|
+
/** CI スナップショット用: C2PA 署名をスキップ */
|
|
116
|
+
readonly skipC2pa?: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface BuildResult {
|
|
120
|
+
readonly pages: number;
|
|
121
|
+
readonly errors: number;
|
|
122
|
+
readonly durationMs: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function walkMarkdown(root: string): string[] {
|
|
126
|
+
const out: string[] = [];
|
|
127
|
+
function visit(dir: string): void {
|
|
128
|
+
for (const name of readdirSync(dir).sort()) {
|
|
129
|
+
const abs = join(dir, name);
|
|
130
|
+
if (statSync(abs).isDirectory()) visit(abs);
|
|
131
|
+
else if (name.endsWith(".md")) out.push(abs);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
visit(root);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function slugFromRel(relPath: string): string {
|
|
139
|
+
const base = relPath.replace(/\\/g, "/").split("/").pop() ?? relPath;
|
|
140
|
+
return base.replace(/\.md$/i, "");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function outHtmlRelForParsed(p: ParsedConcept, config: SoraneConfig): string {
|
|
144
|
+
const slug = slugFromRel(p.relPath);
|
|
145
|
+
if (p.concept.type === "index" || slug === "index") return "index.html";
|
|
146
|
+
return resolvePermalink(config.build.permalink, slug, p.concept.timestamp);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isSystemPage(concept: ParsedConcept["concept"]): boolean {
|
|
150
|
+
return concept.frontmatter.isSystem === true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isBlogArticle(concept: ParsedConcept["concept"], relPath: string): boolean {
|
|
154
|
+
return (
|
|
155
|
+
concept.type === "article" &&
|
|
156
|
+
!isSystemPage(concept) &&
|
|
157
|
+
!isNotFoundSource(relPath) &&
|
|
158
|
+
!isSearchView(concept.frontmatter) &&
|
|
159
|
+
concept.frontmatter.excludeFromList !== true
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function frontmatterString(
|
|
164
|
+
frontmatter: Record<string, unknown>,
|
|
165
|
+
key: string,
|
|
166
|
+
): string | undefined {
|
|
167
|
+
const v = frontmatter[key];
|
|
168
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** index.md 本文がタイトルと同じ見出しだけなら intro を出さない。 */
|
|
172
|
+
async function introHtmlFromBodyWithMeta(
|
|
173
|
+
body: string,
|
|
174
|
+
title: string,
|
|
175
|
+
sectionOpts: BodySectionOptions,
|
|
176
|
+
): Promise<{ readonly introHtml?: string; readonly diagrams: DiagramRenderMeta }> {
|
|
177
|
+
const trimmed = body.trim();
|
|
178
|
+
if (!trimmed) return { diagrams: emptyDiagramMeta() };
|
|
179
|
+
const onlyH1 = /^#\s+(.+?)\s*$/s.exec(trimmed);
|
|
180
|
+
if (onlyH1 && onlyH1[1]!.trim() === title.trim()) {
|
|
181
|
+
return { diagrams: emptyDiagramMeta() };
|
|
182
|
+
}
|
|
183
|
+
const section = await renderBodySectionForConfig(body, sectionOpts);
|
|
184
|
+
return { introHtml: section.html, diagrams: section.diagrams };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function syntheticConcept(title: string, description?: string): OkfConcept {
|
|
188
|
+
return {
|
|
189
|
+
type: "index",
|
|
190
|
+
title,
|
|
191
|
+
body: "",
|
|
192
|
+
frontmatter: {},
|
|
193
|
+
warnings: [],
|
|
194
|
+
description,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function articleNavFor(
|
|
199
|
+
href: string,
|
|
200
|
+
summaries: readonly ArticleListEntry[],
|
|
201
|
+
): ArticleNav | undefined {
|
|
202
|
+
const i = summaries.findIndex((s) => s.href === href);
|
|
203
|
+
if (i < 0) return undefined;
|
|
204
|
+
const prev =
|
|
205
|
+
i > 0
|
|
206
|
+
? { href: summaries[i - 1]!.href, title: summaries[i - 1]!.title }
|
|
207
|
+
: undefined;
|
|
208
|
+
const next =
|
|
209
|
+
i < summaries.length - 1
|
|
210
|
+
? { href: summaries[i + 1]!.href, title: summaries[i + 1]!.title }
|
|
211
|
+
: undefined;
|
|
212
|
+
if (!prev && !next) return undefined;
|
|
213
|
+
return { prev, next };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function tarBytes(entries: Array<{ path: string; content: string }>): Buffer {
|
|
217
|
+
const blocks: Buffer[] = [];
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const content = Buffer.from(entry.content, "utf8");
|
|
220
|
+
const header = Buffer.alloc(512, 0);
|
|
221
|
+
header.write(entry.path.slice(0, 100), 0, "ascii");
|
|
222
|
+
header.write("0000644\0", 100, "ascii");
|
|
223
|
+
header.write("0000000\0", 108, "ascii");
|
|
224
|
+
header.write("0000000\0", 116, "ascii");
|
|
225
|
+
header.write(content.length.toString(8).padStart(11, "0") + "\0", 124, "ascii");
|
|
226
|
+
header.write("0".padStart(11, "0") + "\0", 136, "ascii");
|
|
227
|
+
header.write(" ", 148, "ascii");
|
|
228
|
+
header.write("ustar\0", 257, "ascii");
|
|
229
|
+
let checksum = 0;
|
|
230
|
+
for (let i = 0; i < 512; i++) checksum += header[i]!;
|
|
231
|
+
header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, "ascii");
|
|
232
|
+
blocks.push(header, content);
|
|
233
|
+
const pad = (512 - (content.length % 512)) % 512;
|
|
234
|
+
if (pad > 0) blocks.push(Buffer.alloc(pad, 0));
|
|
235
|
+
}
|
|
236
|
+
blocks.push(Buffer.alloc(1024, 0));
|
|
237
|
+
return Buffer.concat(blocks);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** サイト cwd または親ディレクトリからテーマ CSS を探す(monorepo の website/ 等)。 */
|
|
241
|
+
export function resolveThemeCss(cwd: string): string | null {
|
|
242
|
+
const rel = join("templates", "default", "assets", "main.css");
|
|
243
|
+
let dir = resolve(cwd);
|
|
244
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
245
|
+
const candidate = join(dir, rel);
|
|
246
|
+
if (existsSync(candidate)) return candidate;
|
|
247
|
+
const parent = dirname(dir);
|
|
248
|
+
if (parent === dir) break;
|
|
249
|
+
dir = parent;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const DEFAULT_CSS = `/* sorane default */
|
|
255
|
+
:root { color-scheme: light; --text: #1a1a1a; --muted: #555; --link: #0b57d0; }
|
|
256
|
+
body { font-family: system-ui, sans-serif; line-height: 1.7; max-width: 42rem; margin: 2rem auto; padding: 0 1rem; color: var(--text); }
|
|
257
|
+
a { color: var(--link); }
|
|
258
|
+
h1 { line-height: 1.25; }
|
|
259
|
+
.article-meta { color: var(--muted); font-size: 0.9rem; }
|
|
260
|
+
.article-list { padding-left: 1.25rem; }
|
|
261
|
+
article pre { overflow-x: auto; }
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
export async function runBuild(opts: BuildOptions): Promise<BuildResult> {
|
|
265
|
+
const startedAt = performance.now();
|
|
266
|
+
const { cwd } = opts;
|
|
267
|
+
const config = mergeConfig(opts.config);
|
|
268
|
+
const contentDir = resolve(cwd, config.build.content_dir);
|
|
269
|
+
const outDir = resolve(cwd, config.build.out_dir);
|
|
270
|
+
|
|
271
|
+
if (!existsSync(contentDir)) {
|
|
272
|
+
throw new Error(`content directory not found: ${contentDir}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (opts.clean && existsSync(outDir)) {
|
|
276
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
mkdirSync(join(outDir, "okf"), { recursive: true });
|
|
279
|
+
mkdirSync(join(outDir, "assets"), { recursive: true });
|
|
280
|
+
|
|
281
|
+
const mdFiles = walkMarkdown(contentDir);
|
|
282
|
+
const parsed: ParsedConcept[] = [];
|
|
283
|
+
let errors = 0;
|
|
284
|
+
|
|
285
|
+
for (const abs of mdFiles) {
|
|
286
|
+
const rel = relative(contentDir, abs);
|
|
287
|
+
const source = readFileSync(abs, "utf8");
|
|
288
|
+
const p = parseConcept(abs, rel, source);
|
|
289
|
+
parsed.push(p);
|
|
290
|
+
if (!p.validation.ok) {
|
|
291
|
+
errors += p.validation.issues.length;
|
|
292
|
+
for (const issue of p.validation.issues) {
|
|
293
|
+
process.stderr.write(`[sorane] ${rel}: ${issue.message}\n`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const w of p.validation.warnings) {
|
|
297
|
+
process.stderr.write(`[sorane] ${rel}: warning: ${w}\n`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (errors > 0) {
|
|
302
|
+
throw new Error(`build aborted: ${errors} validation error(s)`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const baseUrl = config.site.base_url.replace(/\/$/, "");
|
|
306
|
+
const diagramConfig = config.build.diagrams ?? DEFAULT_DIAGRAMS_CONFIG;
|
|
307
|
+
const d2OutDir = join(outDir, "assets", "diagrams", "d2");
|
|
308
|
+
const mermaidOutDir = join(outDir, "assets", "diagrams", "mermaid");
|
|
309
|
+
const graphvizOutDir = join(outDir, "assets", "diagrams", "graphviz");
|
|
310
|
+
if (isD2CompileEnabled(diagramConfig)) mkdirSync(d2OutDir, { recursive: true });
|
|
311
|
+
if (isMermaidBuildEnabled(diagramConfig)) mkdirSync(mermaidOutDir, { recursive: true });
|
|
312
|
+
if (isGraphvizCompileEnabled(diagramConfig)) mkdirSync(graphvizOutDir, { recursive: true });
|
|
313
|
+
const bodySectionOpts = (rootPrefix: string): BodySectionOptions => ({
|
|
314
|
+
diagrams: diagramConfig,
|
|
315
|
+
rootPrefix,
|
|
316
|
+
d2OutDir,
|
|
317
|
+
mermaidOutDir,
|
|
318
|
+
graphvizOutDir,
|
|
319
|
+
onDiagramWarning: (message) => process.stderr.write(`[sorane] ${message}\n`),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const hasAnyDisclosure = parsed.some(
|
|
323
|
+
(p) => parseAiDisclosure(p.concept.frontmatter) !== null,
|
|
324
|
+
);
|
|
325
|
+
const siteAiFlags = resolveAiDisclosureFlags(
|
|
326
|
+
config.build.ai_disclosure,
|
|
327
|
+
hasAnyDisclosure,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const blogOpts = {
|
|
331
|
+
page_size: config.build.blog?.page_size ?? 50,
|
|
332
|
+
index_archive_limit: config.build.blog?.index_archive_limit ?? 15,
|
|
333
|
+
featured_mode: (config.build.blog?.featured_mode ?? "excerpt") as FeaturedMode,
|
|
334
|
+
excerpt_length: config.build.blog?.excerpt_length ?? 400,
|
|
335
|
+
show_list_descriptions: config.build.blog?.show_list_descriptions ?? false,
|
|
336
|
+
archives: config.build.blog?.archives ?? true,
|
|
337
|
+
tags: config.build.blog?.tags ?? true,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const articleSummaries: ArticleListEntry[] = parsed
|
|
341
|
+
.filter((p) => isBlogArticle(p.concept, p.relPath))
|
|
342
|
+
.map((p) => {
|
|
343
|
+
const slug = slugFromRel(p.relPath);
|
|
344
|
+
const outRel = resolvePermalink(config.build.permalink, slug, p.concept.timestamp);
|
|
345
|
+
const rawDesc =
|
|
346
|
+
p.concept.description ?? extractDescription(p.concept.body) ?? undefined;
|
|
347
|
+
const aiDisclosure = parseAiDisclosure(p.concept.frontmatter) ?? undefined;
|
|
348
|
+
return {
|
|
349
|
+
title: p.concept.title,
|
|
350
|
+
href: outRel,
|
|
351
|
+
timestamp: p.concept.timestamp,
|
|
352
|
+
updated: frontmatterString(p.concept.frontmatter, "updated"),
|
|
353
|
+
author: frontmatterString(p.concept.frontmatter, "author"),
|
|
354
|
+
description: rawDesc ? sanitizeListDescription(rawDesc) : undefined,
|
|
355
|
+
tags: p.concept.tags,
|
|
356
|
+
aiDisclosure,
|
|
357
|
+
};
|
|
358
|
+
})
|
|
359
|
+
.sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""));
|
|
360
|
+
|
|
361
|
+
const parsedByHref = new Map<string, ParsedConcept>();
|
|
362
|
+
for (const p of parsed) {
|
|
363
|
+
if (!isBlogArticle(p.concept, p.relPath)) continue;
|
|
364
|
+
const slug = slugFromRel(p.relPath);
|
|
365
|
+
const outRel = resolvePermalink(config.build.permalink, slug, p.concept.timestamp);
|
|
366
|
+
parsedByHref.set(outRel, p);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const indexParsed = parsed.find(
|
|
370
|
+
(p) => p.concept.type === "index" || slugFromRel(p.relPath) === "index",
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const titleByHref = new Map<string, string>();
|
|
374
|
+
for (const p of parsed) {
|
|
375
|
+
const slug = slugFromRel(p.relPath);
|
|
376
|
+
const outRel =
|
|
377
|
+
p.concept.type === "index" || slug === "index"
|
|
378
|
+
? "index.html"
|
|
379
|
+
: resolvePermalink(config.build.permalink, slug, p.concept.timestamp);
|
|
380
|
+
titleByHref.set(outRel, p.concept.title);
|
|
381
|
+
}
|
|
382
|
+
const docsNav = resolveDocsNav(config.docs?.nav, titleByHref);
|
|
383
|
+
const docsMode = docsNav.length > 0;
|
|
384
|
+
const docsHrefSet = new Set(docsNav.map((item) => item.href));
|
|
385
|
+
|
|
386
|
+
const fontProcessor = await createFontProcessor(cwd, config.fonts, outDir);
|
|
387
|
+
|
|
388
|
+
const sourceToUrl = new Map<string, string>();
|
|
389
|
+
let searchPageRel: string | undefined;
|
|
390
|
+
for (const p of parsed) {
|
|
391
|
+
const rel = p.relPath.replace(/\\/g, "/");
|
|
392
|
+
const slug = slugFromRel(p.relPath);
|
|
393
|
+
const outRel =
|
|
394
|
+
p.concept.type === "index" || slug === "index"
|
|
395
|
+
? "index.html"
|
|
396
|
+
: resolvePermalink(config.build.permalink, slug, p.concept.timestamp);
|
|
397
|
+
if (!isSystemPage(p.concept) && !isNotFoundSource(rel)) {
|
|
398
|
+
sourceToUrl.set(rel, outRel);
|
|
399
|
+
}
|
|
400
|
+
if (isSearchView(p.concept.frontmatter) && p.concept.type === "article") {
|
|
401
|
+
searchPageRel = outRel;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const indexDbPath = resolve(cwd, config.search.index);
|
|
406
|
+
let searchIndexReady = false;
|
|
407
|
+
const searchMode = config.search.mode ?? "fts";
|
|
408
|
+
if (searchPageRel && existsSync(indexDbPath)) {
|
|
409
|
+
const { IndexStore } = await import("@sorane/search");
|
|
410
|
+
const probe = new IndexStore(indexDbPath);
|
|
411
|
+
const { chunks } = probe.counts();
|
|
412
|
+
if (chunks > 0) {
|
|
413
|
+
if (searchMode === "hybrid") {
|
|
414
|
+
searchIndexReady = probe.hasVectors();
|
|
415
|
+
} else {
|
|
416
|
+
searchIndexReady = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
probe.close();
|
|
420
|
+
}
|
|
421
|
+
const headerSearchEnabled = searchIndexReady;
|
|
422
|
+
const searchNavPath = headerSearchEnabled || !searchIndexReady ? undefined : searchPageRel;
|
|
423
|
+
const showArchiveInHeader =
|
|
424
|
+
Boolean(indexParsed) && blogOpts.archives && !headerSearchEnabled;
|
|
425
|
+
|
|
426
|
+
function headerSearchFor(
|
|
427
|
+
rootPrefix: string,
|
|
428
|
+
page: { readonly docsLayout?: boolean; readonly isSearch?: boolean },
|
|
429
|
+
): { readonly headerSearchHtml?: string; readonly extraHead?: string[] } {
|
|
430
|
+
if (!headerSearchEnabled || page.isSearch) return {};
|
|
431
|
+
return {
|
|
432
|
+
headerSearchHtml: buildSearchMount(rootPrefix, {
|
|
433
|
+
assetBaseUrl: config.search.asset_base_url,
|
|
434
|
+
mode: searchMode,
|
|
435
|
+
variant: "header",
|
|
436
|
+
}),
|
|
437
|
+
extraHead: buildSearchHead(rootPrefix, searchMode),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const siteEntries: SiteEntry[] = [];
|
|
442
|
+
const catalogInputs: Array<{
|
|
443
|
+
slug: string;
|
|
444
|
+
url: string;
|
|
445
|
+
concept: ParsedConcept["concept"];
|
|
446
|
+
}> = [];
|
|
447
|
+
let builtPages = 0;
|
|
448
|
+
|
|
449
|
+
async function fontCssFor(
|
|
450
|
+
concept: ParsedConcept["concept"],
|
|
451
|
+
rootPrefix: string,
|
|
452
|
+
renderedHtml?: string,
|
|
453
|
+
): Promise<string | undefined> {
|
|
454
|
+
if (!fontProcessor) return undefined;
|
|
455
|
+
const chrome = siteChromeText(
|
|
456
|
+
config.site.lang,
|
|
457
|
+
config.site.title,
|
|
458
|
+
Boolean(searchNavPath),
|
|
459
|
+
);
|
|
460
|
+
const extraText =
|
|
461
|
+
(renderedHtml ? plainTextFromHtml(renderedHtml) : "") + chrome;
|
|
462
|
+
return fontProcessor.fontCssForPage({
|
|
463
|
+
body: concept.body,
|
|
464
|
+
title: concept.title,
|
|
465
|
+
extraText,
|
|
466
|
+
frontmatter: {
|
|
467
|
+
...concept.frontmatter,
|
|
468
|
+
type: concept.type,
|
|
469
|
+
noFontEmbedding: concept.frontmatter.noFontEmbedding,
|
|
470
|
+
},
|
|
471
|
+
rootPrefix,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const assetProvenance = loadAssetProvenance(
|
|
476
|
+
contentDir,
|
|
477
|
+
config.build.image_metadata?.manifest,
|
|
478
|
+
);
|
|
479
|
+
const staticDirName = config.build.static_dir ?? "static";
|
|
480
|
+
|
|
481
|
+
// --- Phase A: articles(SSG の核)---
|
|
482
|
+
for (const p of parsed) {
|
|
483
|
+
if (
|
|
484
|
+
p.concept.type !== "article" ||
|
|
485
|
+
isSystemPage(p.concept) ||
|
|
486
|
+
isNotFoundSource(p.relPath)
|
|
487
|
+
) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const slug = slugFromRel(p.relPath);
|
|
492
|
+
const outRel = resolvePermalink(config.build.permalink, slug, p.concept.timestamp);
|
|
493
|
+
const depth = outRel.replace(/\\/g, "/").split("/").length - 1;
|
|
494
|
+
const rootPrefix = depth > 0 ? "../".repeat(depth) : "./";
|
|
495
|
+
const isSearch = isSearchView(p.concept.frontmatter);
|
|
496
|
+
const isDocsPage = docsMode && docsHrefSet.has(outRel);
|
|
497
|
+
const nav = isSearch
|
|
498
|
+
? undefined
|
|
499
|
+
: isDocsPage
|
|
500
|
+
? docsNavFor(outRel, docsNav)
|
|
501
|
+
: articleNavFor(outRel, articleSummaries);
|
|
502
|
+
const aiDisclosure = parseAiDisclosure(p.concept.frontmatter);
|
|
503
|
+
const pageAiFlags = resolveAiDisclosureFlags(
|
|
504
|
+
config.build.ai_disclosure,
|
|
505
|
+
aiDisclosure !== null,
|
|
506
|
+
);
|
|
507
|
+
const badgeHtml =
|
|
508
|
+
pageAiFlags.badges && aiDisclosure
|
|
509
|
+
? buildAiBadgeHtml(aiDisclosure, {
|
|
510
|
+
lang: config.site.lang,
|
|
511
|
+
rootPrefix,
|
|
512
|
+
policyUrl: pageAiFlags.policyUrl,
|
|
513
|
+
})
|
|
514
|
+
: "";
|
|
515
|
+
let pageDiagrams = emptyDiagramMeta();
|
|
516
|
+
let bodyHtml: string;
|
|
517
|
+
if (isSearch) {
|
|
518
|
+
const searchIntro = p.concept.body.trim()
|
|
519
|
+
? await renderBodySectionForConfig(p.concept.body, bodySectionOpts(rootPrefix))
|
|
520
|
+
: undefined;
|
|
521
|
+
pageDiagrams = searchIntro?.diagrams ?? emptyDiagramMeta();
|
|
522
|
+
bodyHtml =
|
|
523
|
+
buildSearchMount(rootPrefix, {
|
|
524
|
+
assetBaseUrl: config.search.asset_base_url,
|
|
525
|
+
mode: searchMode,
|
|
526
|
+
}) +
|
|
527
|
+
(searchIntro
|
|
528
|
+
? `<div class="search-intro">${searchIntro.html}</div>`
|
|
529
|
+
: "");
|
|
530
|
+
} else if (isDocsPage) {
|
|
531
|
+
const doc = await renderDocsArticleFromConceptWithMetaForConfig(
|
|
532
|
+
p.concept,
|
|
533
|
+
nav,
|
|
534
|
+
config.site.lang,
|
|
535
|
+
{ badgeHtml, ...bodySectionOpts(rootPrefix) },
|
|
536
|
+
);
|
|
537
|
+
bodyHtml = doc.bodyHtml;
|
|
538
|
+
pageDiagrams = doc.diagrams;
|
|
539
|
+
} else {
|
|
540
|
+
const article = await renderArticleBodyWithMetaForConfig(p.concept, nav, {
|
|
541
|
+
badgeHtml,
|
|
542
|
+
...bodySectionOpts(rootPrefix),
|
|
543
|
+
});
|
|
544
|
+
bodyHtml = article.bodyHtml;
|
|
545
|
+
pageDiagrams = article.diagrams;
|
|
546
|
+
}
|
|
547
|
+
const diagramHead = diagramHeadForPage(
|
|
548
|
+
pageDiagrams,
|
|
549
|
+
rootPrefix,
|
|
550
|
+
diagramConfig,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const updated = frontmatterString(p.concept.frontmatter, "updated");
|
|
554
|
+
const author = frontmatterString(p.concept.frontmatter, "author");
|
|
555
|
+
const canonicalUrl = baseUrl.length > 0 ? `${baseUrl}/${outRel}` : undefined;
|
|
556
|
+
const pageImageRefs = collectMarkdownImageRefs({
|
|
557
|
+
body: p.concept.body,
|
|
558
|
+
sourceMdRel: p.relPath,
|
|
559
|
+
outHtmlRel: outRel,
|
|
560
|
+
contentDir,
|
|
561
|
+
cwd,
|
|
562
|
+
staticDirName,
|
|
563
|
+
});
|
|
564
|
+
const associatedMedia =
|
|
565
|
+
pageAiFlags.jsonLd && !isSearch
|
|
566
|
+
? buildAssociatedMediaForArticle({
|
|
567
|
+
refs: pageImageRefs,
|
|
568
|
+
provenance: assetProvenance,
|
|
569
|
+
baseUrl,
|
|
570
|
+
})
|
|
571
|
+
: [];
|
|
572
|
+
|
|
573
|
+
const jsonLd = isSearch
|
|
574
|
+
? ""
|
|
575
|
+
: buildBlogPostingJsonLd({
|
|
576
|
+
title: p.concept.title,
|
|
577
|
+
description: p.concept.description ?? extractDescription(p.concept.body) ?? undefined,
|
|
578
|
+
url: canonicalUrl ?? outRel,
|
|
579
|
+
datePublished: p.concept.timestamp,
|
|
580
|
+
dateModified: updated ?? p.concept.timestamp,
|
|
581
|
+
author,
|
|
582
|
+
siteTitle: config.site.title,
|
|
583
|
+
lang: config.site.lang,
|
|
584
|
+
aiDisclosure:
|
|
585
|
+
pageAiFlags.jsonLd && aiDisclosure ? aiDisclosure : undefined,
|
|
586
|
+
associatedMedia: associatedMedia.length > 0 ? associatedMedia : undefined,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const fontCss = await fontCssFor(p.concept, rootPrefix, bodyHtml);
|
|
590
|
+
const headerSearch = headerSearchFor(rootPrefix, {
|
|
591
|
+
docsLayout: isDocsPage,
|
|
592
|
+
isSearch,
|
|
593
|
+
});
|
|
594
|
+
const extraHead = isSearch
|
|
595
|
+
? [
|
|
596
|
+
...buildSearchHead(rootPrefix, searchMode),
|
|
597
|
+
...(diagramHead ? [diagramHead] : []),
|
|
598
|
+
]
|
|
599
|
+
: [
|
|
600
|
+
...(jsonLd ? [jsonLd] : []),
|
|
601
|
+
...(headerSearch.extraHead ?? []),
|
|
602
|
+
...(diagramHead ? [diagramHead] : []),
|
|
603
|
+
];
|
|
604
|
+
emitPage({
|
|
605
|
+
cwd,
|
|
606
|
+
config,
|
|
607
|
+
outDir,
|
|
608
|
+
outRel,
|
|
609
|
+
concept: p.concept,
|
|
610
|
+
bodyHtml,
|
|
611
|
+
baseUrl,
|
|
612
|
+
fontCss,
|
|
613
|
+
extraHead: extraHead.length > 0 ? extraHead : undefined,
|
|
614
|
+
showArchiveNav: showArchiveInHeader,
|
|
615
|
+
searchPath: searchNavPath,
|
|
616
|
+
docsLayout: isDocsPage,
|
|
617
|
+
docsSidebarHtml: isDocsPage ? docsSidebarHtml(docsNav, outRel, outRel) : undefined,
|
|
618
|
+
headerSearchHtml: headerSearch.headerSearchHtml,
|
|
619
|
+
});
|
|
620
|
+
builtPages += 1;
|
|
621
|
+
|
|
622
|
+
siteEntries.push({ url: outRel, lastmod: p.concept.timestamp, isIndex: false });
|
|
623
|
+
catalogInputs.push({
|
|
624
|
+
slug,
|
|
625
|
+
url: canonicalUrl ?? outRel,
|
|
626
|
+
concept: p.concept,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// --- Phase B: index(任意 — content/index.md がある場合のみ)---
|
|
631
|
+
const listForPagination = indexParsed && articleSummaries[0]
|
|
632
|
+
? articleSummaries.slice(1)
|
|
633
|
+
: articleSummaries;
|
|
634
|
+
const archivePages = paginate(listForPagination, blogOpts.page_size);
|
|
635
|
+
|
|
636
|
+
if (indexParsed) {
|
|
637
|
+
const p = indexParsed;
|
|
638
|
+
const latest = articleSummaries[0];
|
|
639
|
+
const latestParsed = latest ? parsedByHref.get(latest.href) : undefined;
|
|
640
|
+
const useBlogLayout = p.concept.type === "index";
|
|
641
|
+
|
|
642
|
+
let bodyHtml: string;
|
|
643
|
+
let indexDiagrams = emptyDiagramMeta();
|
|
644
|
+
const indexTitle = p.concept.title || config.site.title;
|
|
645
|
+
const intro = await introHtmlFromBodyWithMeta(
|
|
646
|
+
p.concept.body,
|
|
647
|
+
indexTitle,
|
|
648
|
+
bodySectionOpts("./"),
|
|
649
|
+
);
|
|
650
|
+
indexDiagrams = mergeDiagramMeta(indexDiagrams, intro.diagrams);
|
|
651
|
+
|
|
652
|
+
if (docsMode && useBlogLayout) {
|
|
653
|
+
bodyHtml = renderDocsIndexBody({
|
|
654
|
+
siteTitle: indexTitle,
|
|
655
|
+
description: p.concept.description ?? config.site.description,
|
|
656
|
+
profileUrl: frontmatterString(p.concept.frontmatter, "profileUrl"),
|
|
657
|
+
githubUrl: frontmatterString(p.concept.frontmatter, "githubUrl"),
|
|
658
|
+
introHtml: intro.introHtml,
|
|
659
|
+
docsNav,
|
|
660
|
+
lang: config.site.lang,
|
|
661
|
+
});
|
|
662
|
+
} else if (useBlogLayout) {
|
|
663
|
+
const featuredMode = blogOpts.featured_mode;
|
|
664
|
+
let featuredBody = "";
|
|
665
|
+
if (latestParsed && featuredMode !== "off") {
|
|
666
|
+
if (featuredMode === "full") {
|
|
667
|
+
const featured = await renderBodySectionForConfig(
|
|
668
|
+
latestParsed.concept.body,
|
|
669
|
+
bodySectionOpts("./"),
|
|
670
|
+
);
|
|
671
|
+
featuredBody = featured.html;
|
|
672
|
+
indexDiagrams = mergeDiagramMeta(indexDiagrams, featured.diagrams);
|
|
673
|
+
} else {
|
|
674
|
+
featuredBody = renderFeaturedExcerpt(
|
|
675
|
+
latestParsed.concept,
|
|
676
|
+
blogOpts.excerpt_length,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
bodyHtml = renderBlogIndexBody({
|
|
681
|
+
siteTitle: indexTitle,
|
|
682
|
+
description: p.concept.description ?? config.site.description,
|
|
683
|
+
showHeaderTitle: false,
|
|
684
|
+
profileUrl: frontmatterString(p.concept.frontmatter, "profileUrl"),
|
|
685
|
+
githubUrl: frontmatterString(p.concept.frontmatter, "githubUrl"),
|
|
686
|
+
introHtml: intro.introHtml,
|
|
687
|
+
lang: config.site.lang,
|
|
688
|
+
latestArticle:
|
|
689
|
+
latestParsed && featuredMode !== "off" && featuredBody
|
|
690
|
+
? {
|
|
691
|
+
title: latestParsed.concept.title,
|
|
692
|
+
href: latest!.href,
|
|
693
|
+
timestamp: latestParsed.concept.timestamp,
|
|
694
|
+
updated: frontmatterString(latestParsed.concept.frontmatter, "updated"),
|
|
695
|
+
author: frontmatterString(latestParsed.concept.frontmatter, "author"),
|
|
696
|
+
bodyHtml: featuredBody,
|
|
697
|
+
aiDisclosure: latest.aiDisclosure,
|
|
698
|
+
}
|
|
699
|
+
: undefined,
|
|
700
|
+
articles: archivePages[0] ?? [],
|
|
701
|
+
archiveLimit: blogOpts.index_archive_limit,
|
|
702
|
+
showListDescriptions: blogOpts.show_list_descriptions,
|
|
703
|
+
showOnLists: siteAiFlags.showOnLists,
|
|
704
|
+
listRootPrefix: "./",
|
|
705
|
+
moreArticlesHref: archivePages.length > 1 ? "page/2.html" : undefined,
|
|
706
|
+
yearArchiveHref: blogOpts.archives ? "archive/index.html" : undefined,
|
|
707
|
+
});
|
|
708
|
+
} else {
|
|
709
|
+
bodyHtml = renderIndexBody(config.site.title, archivePages[0] ?? articleSummaries);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const fontCss = await fontCssFor(p.concept, "./", bodyHtml);
|
|
713
|
+
const indexCanonical =
|
|
714
|
+
baseUrl.length > 0 ? `${baseUrl}/index.html` : undefined;
|
|
715
|
+
const indexJsonLd = buildWebSiteJsonLd({
|
|
716
|
+
title: p.concept.title || config.site.title,
|
|
717
|
+
description: p.concept.description ?? config.site.description,
|
|
718
|
+
url: indexCanonical,
|
|
719
|
+
lang: config.site.lang,
|
|
720
|
+
});
|
|
721
|
+
const indexHeaderSearch = headerSearchFor("./", { docsLayout: docsMode });
|
|
722
|
+
const indexDiagramHead = diagramHeadForPage(
|
|
723
|
+
indexDiagrams,
|
|
724
|
+
"./",
|
|
725
|
+
diagramConfig,
|
|
726
|
+
);
|
|
727
|
+
const indexExtraHead = [
|
|
728
|
+
indexJsonLd,
|
|
729
|
+
...(indexHeaderSearch.extraHead ?? []),
|
|
730
|
+
...(indexDiagramHead ? [indexDiagramHead] : []),
|
|
731
|
+
];
|
|
732
|
+
emitPage({
|
|
733
|
+
cwd,
|
|
734
|
+
config,
|
|
735
|
+
outDir,
|
|
736
|
+
outRel: "index.html",
|
|
737
|
+
concept: p.concept,
|
|
738
|
+
bodyHtml,
|
|
739
|
+
baseUrl,
|
|
740
|
+
fontCss,
|
|
741
|
+
isIndex: true,
|
|
742
|
+
pageKind: "website",
|
|
743
|
+
extraHead: indexExtraHead,
|
|
744
|
+
showArchiveNav: showArchiveInHeader,
|
|
745
|
+
searchPath: searchNavPath,
|
|
746
|
+
docsLayout: docsMode,
|
|
747
|
+
docsSidebarHtml: docsMode
|
|
748
|
+
? docsSidebarHtml(docsNav, "index.html", "index.html")
|
|
749
|
+
: undefined,
|
|
750
|
+
headerSearchHtml: indexHeaderSearch.headerSearchHtml,
|
|
751
|
+
});
|
|
752
|
+
builtPages += 1;
|
|
753
|
+
siteEntries.push({ url: "index.html", lastmod: undefined, isIndex: true });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// --- Phase C: 派生ブログページ(index がある場合)---
|
|
757
|
+
if (indexParsed) {
|
|
758
|
+
for (let i = 1; i < archivePages.length; i++) {
|
|
759
|
+
const pageNum = i + 1;
|
|
760
|
+
const outRel = `page/${pageNum}.html`;
|
|
761
|
+
const bodyHtml = renderArchiveListBody(
|
|
762
|
+
`${config.site.title} — ページ ${pageNum}`,
|
|
763
|
+
undefined,
|
|
764
|
+
archivePages[i]!,
|
|
765
|
+
{
|
|
766
|
+
fromRel: outRel,
|
|
767
|
+
page: pageNum,
|
|
768
|
+
totalPages: archivePages.length,
|
|
769
|
+
showOnLists: siteAiFlags.showOnLists,
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
const concept = syntheticConcept(`${config.site.title} — ページ ${pageNum}`);
|
|
773
|
+
const rootPrefix = rootPrefixFromRel(outRel);
|
|
774
|
+
const fontCss = await fontCssFor(concept, rootPrefix, bodyHtml);
|
|
775
|
+
const pageChrome = headerSearchFor(rootPrefix, { isSearch: false });
|
|
776
|
+
emitPage({
|
|
777
|
+
cwd,
|
|
778
|
+
config,
|
|
779
|
+
outDir,
|
|
780
|
+
outRel,
|
|
781
|
+
concept,
|
|
782
|
+
bodyHtml,
|
|
783
|
+
baseUrl,
|
|
784
|
+
fontCss,
|
|
785
|
+
showArchiveNav: showArchiveInHeader,
|
|
786
|
+
searchPath: searchNavPath,
|
|
787
|
+
headerSearchHtml: pageChrome.headerSearchHtml,
|
|
788
|
+
extraHead: pageChrome.extraHead,
|
|
789
|
+
});
|
|
790
|
+
builtPages += 1;
|
|
791
|
+
siteEntries.push({ url: outRel, lastmod: undefined, isIndex: false });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (indexParsed && blogOpts.archives && articleSummaries.length > 0) {
|
|
796
|
+
const byYear = groupByYear(articleSummaries);
|
|
797
|
+
const byMonth = groupByYearMonth(articleSummaries);
|
|
798
|
+
|
|
799
|
+
const archiveIndexHtml = renderYearArchiveIndexBody(
|
|
800
|
+
config.site.title,
|
|
801
|
+
byYear,
|
|
802
|
+
"archive/index.html",
|
|
803
|
+
);
|
|
804
|
+
const archiveIndexConcept = syntheticConcept(`${config.site.title} — 年別アーカイブ`);
|
|
805
|
+
const archiveIndexFontCss = await fontCssFor(
|
|
806
|
+
archiveIndexConcept,
|
|
807
|
+
rootPrefixFromRel("archive/index.html"),
|
|
808
|
+
archiveIndexHtml,
|
|
809
|
+
);
|
|
810
|
+
const archiveIndexChrome = headerSearchFor(rootPrefixFromRel("archive/index.html"), {
|
|
811
|
+
isSearch: false,
|
|
812
|
+
});
|
|
813
|
+
emitPage({
|
|
814
|
+
cwd,
|
|
815
|
+
config,
|
|
816
|
+
outDir,
|
|
817
|
+
outRel: "archive/index.html",
|
|
818
|
+
concept: archiveIndexConcept,
|
|
819
|
+
bodyHtml: archiveIndexHtml,
|
|
820
|
+
baseUrl,
|
|
821
|
+
fontCss: archiveIndexFontCss,
|
|
822
|
+
showArchiveNav: showArchiveInHeader,
|
|
823
|
+
searchPath: searchNavPath,
|
|
824
|
+
headerSearchHtml: archiveIndexChrome.headerSearchHtml,
|
|
825
|
+
extraHead: archiveIndexChrome.extraHead,
|
|
826
|
+
});
|
|
827
|
+
builtPages += 1;
|
|
828
|
+
siteEntries.push({ url: "archive/index.html", lastmod: undefined, isIndex: false });
|
|
829
|
+
|
|
830
|
+
for (const year of [...byYear.keys()].sort((a, b) => b.localeCompare(a))) {
|
|
831
|
+
const yearOutRel = `archive/${year}.html`;
|
|
832
|
+
const yearHtml = renderMonthListForYear(year, byMonth, yearOutRel);
|
|
833
|
+
const yearConcept = syntheticConcept(`${year}年`);
|
|
834
|
+
const yearFontCss = await fontCssFor(yearConcept, rootPrefixFromRel(yearOutRel), yearHtml);
|
|
835
|
+
const yearChrome = headerSearchFor(rootPrefixFromRel(yearOutRel), { isSearch: false });
|
|
836
|
+
emitPage({
|
|
837
|
+
cwd,
|
|
838
|
+
config,
|
|
839
|
+
outDir,
|
|
840
|
+
outRel: yearOutRel,
|
|
841
|
+
concept: yearConcept,
|
|
842
|
+
bodyHtml: yearHtml,
|
|
843
|
+
baseUrl,
|
|
844
|
+
fontCss: yearFontCss,
|
|
845
|
+
showArchiveNav: showArchiveInHeader,
|
|
846
|
+
searchPath: searchNavPath,
|
|
847
|
+
headerSearchHtml: yearChrome.headerSearchHtml,
|
|
848
|
+
extraHead: yearChrome.extraHead,
|
|
849
|
+
});
|
|
850
|
+
builtPages += 1;
|
|
851
|
+
siteEntries.push({ url: `archive/${year}.html`, lastmod: undefined, isIndex: false });
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
for (const ym of [...byMonth.keys()].sort((a, b) => b.localeCompare(a))) {
|
|
855
|
+
const monthArticles = byMonth.get(ym)!;
|
|
856
|
+
const [y, m] = ym.split("-");
|
|
857
|
+
const monthOutRel = `archive/${ym}.html`;
|
|
858
|
+
const bodyHtml = renderArchiveListBody(`${y}年${m}月`, undefined, monthArticles, {
|
|
859
|
+
fromRel: monthOutRel,
|
|
860
|
+
showOnLists: siteAiFlags.showOnLists,
|
|
861
|
+
});
|
|
862
|
+
const monthConcept = syntheticConcept(`${y}年${m}月`);
|
|
863
|
+
const monthFontCss = await fontCssFor(monthConcept, rootPrefixFromRel(monthOutRel), bodyHtml);
|
|
864
|
+
const monthChrome = headerSearchFor(rootPrefixFromRel(monthOutRel), { isSearch: false });
|
|
865
|
+
emitPage({
|
|
866
|
+
cwd,
|
|
867
|
+
config,
|
|
868
|
+
outDir,
|
|
869
|
+
outRel: monthOutRel,
|
|
870
|
+
concept: monthConcept,
|
|
871
|
+
bodyHtml,
|
|
872
|
+
baseUrl,
|
|
873
|
+
fontCss: monthFontCss,
|
|
874
|
+
showArchiveNav: showArchiveInHeader,
|
|
875
|
+
searchPath: searchNavPath,
|
|
876
|
+
headerSearchHtml: monthChrome.headerSearchHtml,
|
|
877
|
+
extraHead: monthChrome.extraHead,
|
|
878
|
+
});
|
|
879
|
+
builtPages += 1;
|
|
880
|
+
siteEntries.push({ url: `archive/${ym}.html`, lastmod: undefined, isIndex: false });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (indexParsed && blogOpts.tags) {
|
|
885
|
+
const byTag = groupByTag(articleSummaries);
|
|
886
|
+
for (const [tagSlug, tagged] of byTag) {
|
|
887
|
+
const label = tagged[0]?.tags?.find((t) => slugifyTag(t) === tagSlug) ?? tagSlug;
|
|
888
|
+
const tagOutRel = `tag/${tagSlug}.html`;
|
|
889
|
+
const bodyHtml = renderArchiveListBody(`タグ: ${label}`, undefined, tagged, {
|
|
890
|
+
fromRel: tagOutRel,
|
|
891
|
+
showOnLists: siteAiFlags.showOnLists,
|
|
892
|
+
});
|
|
893
|
+
const tagConcept = syntheticConcept(`タグ: ${label}`);
|
|
894
|
+
const tagFontCss = await fontCssFor(tagConcept, rootPrefixFromRel(tagOutRel), bodyHtml);
|
|
895
|
+
const tagChrome = headerSearchFor(rootPrefixFromRel(tagOutRel), { isSearch: false });
|
|
896
|
+
emitPage({
|
|
897
|
+
cwd,
|
|
898
|
+
config,
|
|
899
|
+
outDir,
|
|
900
|
+
outRel: tagOutRel,
|
|
901
|
+
concept: tagConcept,
|
|
902
|
+
bodyHtml,
|
|
903
|
+
baseUrl,
|
|
904
|
+
fontCss: tagFontCss,
|
|
905
|
+
showArchiveNav: showArchiveInHeader,
|
|
906
|
+
searchPath: searchNavPath,
|
|
907
|
+
headerSearchHtml: tagChrome.headerSearchHtml,
|
|
908
|
+
extraHead: tagChrome.extraHead,
|
|
909
|
+
});
|
|
910
|
+
builtPages += 1;
|
|
911
|
+
siteEntries.push({ url: `tag/${tagSlug}.html`, lastmod: undefined, isIndex: false });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const aiLabeledCount = articleSummaries.filter((a) => a.aiDisclosure).length;
|
|
916
|
+
const feedEntries: FeedEntry[] = articleSummaries.slice(0, 30).map((a) => {
|
|
917
|
+
const absUrl = baseUrl.length > 0 ? `${baseUrl}/${a.href}` : a.href;
|
|
918
|
+
const ts = a.timestamp ?? new Date().toISOString();
|
|
919
|
+
const pageFlags = resolveAiDisclosureFlags(
|
|
920
|
+
config.build.ai_disclosure,
|
|
921
|
+
a.aiDisclosure !== undefined,
|
|
922
|
+
);
|
|
923
|
+
return {
|
|
924
|
+
title: a.title,
|
|
925
|
+
url: absUrl,
|
|
926
|
+
id: absUrl,
|
|
927
|
+
updated: ts.includes("T") ? ts : `${ts}T00:00:00Z`,
|
|
928
|
+
summary: a.description,
|
|
929
|
+
digitalSourceCode:
|
|
930
|
+
pageFlags.atom && a.aiDisclosure
|
|
931
|
+
? a.aiDisclosure.digitalSourceCode
|
|
932
|
+
: undefined,
|
|
933
|
+
};
|
|
934
|
+
});
|
|
935
|
+
writeFileSync(
|
|
936
|
+
join(outDir, "feed.xml"),
|
|
937
|
+
buildAtomFeed(feedEntries, {
|
|
938
|
+
siteTitle: config.site.title,
|
|
939
|
+
siteDescription: config.site.description,
|
|
940
|
+
baseUrl,
|
|
941
|
+
}),
|
|
942
|
+
"utf8",
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
writeFileSync(join(outDir, "robots.txt"), buildRobotsTxt(baseUrl), "utf8");
|
|
946
|
+
writeFileSync(
|
|
947
|
+
join(outDir, "sitemap.xml"),
|
|
948
|
+
buildSitemapXml(siteEntries, baseUrl),
|
|
949
|
+
"utf8",
|
|
950
|
+
);
|
|
951
|
+
writeFileSync(
|
|
952
|
+
join(outDir, "llms.txt"),
|
|
953
|
+
buildLlmsTxt({
|
|
954
|
+
siteTitle: config.site.title,
|
|
955
|
+
siteDescription: config.site.description,
|
|
956
|
+
baseUrl,
|
|
957
|
+
aiLabeledCount: siteAiFlags.machineReadable ? aiLabeledCount : undefined,
|
|
958
|
+
diagramsEnabled: diagramConfig.enabled !== false,
|
|
959
|
+
}),
|
|
960
|
+
"utf8",
|
|
961
|
+
);
|
|
962
|
+
writeFileSync(
|
|
963
|
+
join(outDir, "catalog.jsonld"),
|
|
964
|
+
buildCatalogJsonLd(catalogInputs, config.site.title, baseUrl, {
|
|
965
|
+
machineReadable: siteAiFlags.machineReadable,
|
|
966
|
+
}),
|
|
967
|
+
"utf8",
|
|
968
|
+
);
|
|
969
|
+
if (aiLabeledCount > 0) {
|
|
970
|
+
process.stdout.write(
|
|
971
|
+
`[sorane] AI disclosure: ${aiLabeledCount} labeled article(s)\n`,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const bundleEntries = buildBundleEntries(
|
|
976
|
+
parsed
|
|
977
|
+
.filter(
|
|
978
|
+
(p) =>
|
|
979
|
+
p.concept.type !== "index" &&
|
|
980
|
+
slugFromRel(p.relPath) !== "index" &&
|
|
981
|
+
!isNotFoundSource(p.relPath),
|
|
982
|
+
)
|
|
983
|
+
.map((p) => ({
|
|
984
|
+
concept: p.concept,
|
|
985
|
+
slug: slugFromRel(p.relPath),
|
|
986
|
+
})),
|
|
987
|
+
);
|
|
988
|
+
writeFileSync(join(outDir, "okf/bundle.tar.gz"), gzipSync(tarBytes(bundleEntries)));
|
|
989
|
+
|
|
990
|
+
const templateCss = resolveThemeCss(cwd);
|
|
991
|
+
if (templateCss) {
|
|
992
|
+
copyFileSync(templateCss, join(outDir, "assets/main.css"));
|
|
993
|
+
} else {
|
|
994
|
+
writeFileSync(join(outDir, "assets/main.css"), DEFAULT_CSS, "utf8");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const aiLabelsSrc = resolveThemeAssetDir(cwd, "ai-labels");
|
|
998
|
+
if (aiLabelsSrc) {
|
|
999
|
+
cpSync(aiLabelsSrc, join(outDir, "assets/ai-labels"), { recursive: true });
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
emitDiagramAssets({
|
|
1003
|
+
cwd,
|
|
1004
|
+
outDir,
|
|
1005
|
+
config: diagramConfig,
|
|
1006
|
+
contentHasMermaid: contentHasMermaidFences(mdFiles),
|
|
1007
|
+
onProgress: (message) => process.stdout.write(`[sorane] ${message}\n`),
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// --- 404.html(Cloudflare Pages 等のエラーページ)---
|
|
1011
|
+
const notFoundParsed = parsed.find((p) => isNotFoundSource(p.relPath));
|
|
1012
|
+
const staticSrc = resolve(cwd, staticDirName);
|
|
1013
|
+
const staticNotFoundHtml = join(staticSrc, "404.html");
|
|
1014
|
+
|
|
1015
|
+
if (notFoundParsed) {
|
|
1016
|
+
const rootPrefix = "./";
|
|
1017
|
+
const section = await renderBodySectionForConfig(
|
|
1018
|
+
notFoundBodySource(notFoundParsed.concept),
|
|
1019
|
+
bodySectionOpts(rootPrefix),
|
|
1020
|
+
);
|
|
1021
|
+
const bodyHtml = renderCustomNotFoundBody(notFoundParsed.concept, section.html);
|
|
1022
|
+
const fontCss = await fontCssFor(notFoundParsed.concept, rootPrefix, bodyHtml);
|
|
1023
|
+
const notFoundChrome = headerSearchFor("./", { isSearch: false });
|
|
1024
|
+
emitPage({
|
|
1025
|
+
cwd,
|
|
1026
|
+
config,
|
|
1027
|
+
outDir,
|
|
1028
|
+
outRel: "404.html",
|
|
1029
|
+
concept: notFoundParsed.concept,
|
|
1030
|
+
bodyHtml,
|
|
1031
|
+
baseUrl,
|
|
1032
|
+
fontCss,
|
|
1033
|
+
showArchiveNav: showArchiveInHeader,
|
|
1034
|
+
searchPath: searchNavPath,
|
|
1035
|
+
pageKind: "website",
|
|
1036
|
+
headerSearchHtml: notFoundChrome.headerSearchHtml,
|
|
1037
|
+
extraHead: notFoundChrome.extraHead,
|
|
1038
|
+
});
|
|
1039
|
+
builtPages += 1;
|
|
1040
|
+
} else if (existsSync(staticNotFoundHtml)) {
|
|
1041
|
+
copyFileSync(staticNotFoundHtml, join(outDir, "404.html"));
|
|
1042
|
+
builtPages += 1;
|
|
1043
|
+
} else {
|
|
1044
|
+
const concept = syntheticConcept(
|
|
1045
|
+
notFoundLabels(config.site.lang).heading,
|
|
1046
|
+
config.site.description,
|
|
1047
|
+
);
|
|
1048
|
+
const bodyHtml = renderDefaultNotFoundBody(config.site.lang);
|
|
1049
|
+
const defaultNotFoundChrome = headerSearchFor("./", { isSearch: false });
|
|
1050
|
+
emitPage({
|
|
1051
|
+
cwd,
|
|
1052
|
+
config,
|
|
1053
|
+
outDir,
|
|
1054
|
+
outRel: "404.html",
|
|
1055
|
+
concept,
|
|
1056
|
+
bodyHtml,
|
|
1057
|
+
baseUrl,
|
|
1058
|
+
showArchiveNav: showArchiveInHeader,
|
|
1059
|
+
searchPath: searchNavPath,
|
|
1060
|
+
pageKind: "website",
|
|
1061
|
+
headerSearchHtml: defaultNotFoundChrome.headerSearchHtml,
|
|
1062
|
+
extraHead: defaultNotFoundChrome.extraHead,
|
|
1063
|
+
});
|
|
1064
|
+
builtPages += 1;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const inlineImageCandidates = parsed.flatMap((p) =>
|
|
1068
|
+
collectMarkdownImageRefs({
|
|
1069
|
+
body: p.concept.body,
|
|
1070
|
+
sourceMdRel: p.relPath,
|
|
1071
|
+
outHtmlRel: outHtmlRelForParsed(p, config),
|
|
1072
|
+
contentDir,
|
|
1073
|
+
cwd,
|
|
1074
|
+
staticDirName,
|
|
1075
|
+
}),
|
|
1076
|
+
);
|
|
1077
|
+
const inlineImages = dedupeMarkdownImageRefs(inlineImageCandidates);
|
|
1078
|
+
|
|
1079
|
+
await processStaticAssets({
|
|
1080
|
+
cwd,
|
|
1081
|
+
staticSrc,
|
|
1082
|
+
outDir,
|
|
1083
|
+
staticDirName,
|
|
1084
|
+
contentDir,
|
|
1085
|
+
c2pa: config.build.c2pa,
|
|
1086
|
+
imageMetadata: config.build.image_metadata,
|
|
1087
|
+
skipC2pa: opts.skipC2pa,
|
|
1088
|
+
inlineImages,
|
|
1089
|
+
onWarning: (message) => process.stderr.write(`[sorane] ${message}\n`),
|
|
1090
|
+
onProgress: (message) => process.stdout.write(`[sorane] ${message}\n`),
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
await emitSearchAssets({
|
|
1094
|
+
cwd,
|
|
1095
|
+
outDir,
|
|
1096
|
+
indexPath: indexDbPath,
|
|
1097
|
+
mode: searchMode,
|
|
1098
|
+
modelRoot: config.search.model,
|
|
1099
|
+
modelId: config.search.model_id,
|
|
1100
|
+
bundleModel: config.search.bundle_model,
|
|
1101
|
+
assetBaseUrl: config.search.asset_base_url || undefined,
|
|
1102
|
+
contentDir,
|
|
1103
|
+
machineReadable: siteAiFlags.machineReadable,
|
|
1104
|
+
sourceToUrl: (source) => sourceToUrl.get(source) ?? source.replace(/\.md$/i, ".html"),
|
|
1105
|
+
onProgress: (message) => process.stdout.write(`[sorane] ${message}\n`),
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
return { pages: builtPages, errors: 0, durationMs: performance.now() - startedAt };
|
|
1109
|
+
}
|