@limcpf/everything-is-a-markdown 0.5.4 → 0.5.5
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.ko.md +9 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/src/build.ts +30 -15
- package/src/config.ts +19 -2
- package/src/runtime/app.js +90 -47
- package/src/types.ts +1 -0
package/README.ko.md
CHANGED
|
@@ -38,6 +38,8 @@ bun run blog [build|dev|clean] [options]
|
|
|
38
38
|
- `--vault <path>`: Markdown 루트 디렉터리 (기본 `.`)
|
|
39
39
|
- `--out <path>`: 출력 디렉터리 (기본 `dist`)
|
|
40
40
|
- `--exclude <glob>`: 제외 패턴 추가 (반복 가능)
|
|
41
|
+
- `--new-within-days <n>`: NEW 배지 기준 일수 (정수 `>= 0`, 기본 `7`)
|
|
42
|
+
- `--recent-limit <n>`: Recent 폴더 문서 수 제한 (정수 `>= 1`, 기본 `5`)
|
|
41
43
|
- `--port <n>`: 개발 서버 포트 (기본 `3000`)
|
|
42
44
|
|
|
43
45
|
## 설정 파일 (`blog.config.ts`)
|
|
@@ -51,6 +53,7 @@ const config = {
|
|
|
51
53
|
staticPaths: ["assets", "public/favicon.ico"],
|
|
52
54
|
seo: {
|
|
53
55
|
siteUrl: "https://example.com",
|
|
56
|
+
pathBase: "/blog",
|
|
54
57
|
defaultOgImage: "/assets/og.png",
|
|
55
58
|
},
|
|
56
59
|
};
|
|
@@ -65,6 +68,12 @@ export default config;
|
|
|
65
68
|
- 지정한 경로의 파일들을 `dist`에 같은 상대 경로로 복사
|
|
66
69
|
- 예: 볼트 `assets/og.png` -> `dist/assets/og.png`
|
|
67
70
|
|
|
71
|
+
`seo.pathBase`:
|
|
72
|
+
|
|
73
|
+
- 서브패스 배포(예: `/blog`)를 정식 지원합니다.
|
|
74
|
+
- 내부 라우팅/본문 fetch 링크에 동일한 base path가 적용됩니다.
|
|
75
|
+
- 루트 배포는 빈 문자열(`""`)을 사용합니다.
|
|
76
|
+
|
|
68
77
|
예시:
|
|
69
78
|
|
|
70
79
|
```bash
|
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ Common options:
|
|
|
38
38
|
- `--vault <path>`: Markdown root directory (default `.`)
|
|
39
39
|
- `--out <path>`: Output directory (default `dist`)
|
|
40
40
|
- `--exclude <glob>`: Add exclude pattern (repeatable)
|
|
41
|
+
- `--new-within-days <n>`: NEW badge threshold days (integer `>= 0`, default `7`)
|
|
42
|
+
- `--recent-limit <n>`: Recent folder item limit (integer `>= 1`, default `5`)
|
|
41
43
|
- `--port <n>`: Dev server port (default `3000`)
|
|
42
44
|
|
|
43
45
|
## Config File (`blog.config.ts`)
|
|
@@ -51,6 +53,7 @@ const config = {
|
|
|
51
53
|
staticPaths: ["assets", "public/favicon.ico"],
|
|
52
54
|
seo: {
|
|
53
55
|
siteUrl: "https://example.com",
|
|
56
|
+
pathBase: "/blog",
|
|
54
57
|
defaultOgImage: "/assets/og.png",
|
|
55
58
|
},
|
|
56
59
|
};
|
|
@@ -65,6 +68,12 @@ export default config;
|
|
|
65
68
|
- Copies all matched files into the same relative location in `dist`
|
|
66
69
|
- Example: `assets/og.png` in vault becomes `dist/assets/og.png`
|
|
67
70
|
|
|
71
|
+
`seo.pathBase`:
|
|
72
|
+
|
|
73
|
+
- Deploy under a subpath (for example `/blog`)
|
|
74
|
+
- Internal routing/content fetch links are generated with this base path
|
|
75
|
+
- Keep empty (`""`) for root deployment
|
|
76
|
+
|
|
68
77
|
Examples:
|
|
69
78
|
|
|
70
79
|
```bash
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -866,6 +866,7 @@ function buildManifest(docs: DocRecord[], tree: TreeNode[], options: BuildOption
|
|
|
866
866
|
return {
|
|
867
867
|
generatedAt: new Date().toISOString(),
|
|
868
868
|
siteTitle: resolveSiteTitle(options),
|
|
869
|
+
pathBase: options.seo?.pathBase ?? "",
|
|
869
870
|
defaultBranch: DEFAULT_BRANCH,
|
|
870
871
|
branches,
|
|
871
872
|
ui: {
|
|
@@ -1206,7 +1207,23 @@ function renderInitialMeta(doc: DocRecord): string {
|
|
|
1206
1207
|
return items.join("");
|
|
1207
1208
|
}
|
|
1208
1209
|
|
|
1209
|
-
function
|
|
1210
|
+
function toPathWithBase(pathname: string, pathBase: string): string {
|
|
1211
|
+
const cleanBase = pathBase.trim().replace(/\\/g, "/");
|
|
1212
|
+
const normalizedBase = !cleanBase || cleanBase === "/"
|
|
1213
|
+
? ""
|
|
1214
|
+
: `/${cleanBase.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
|
1215
|
+
if (!normalizedBase) {
|
|
1216
|
+
return pathname;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (pathname === "/") {
|
|
1220
|
+
return `${normalizedBase}/`;
|
|
1221
|
+
}
|
|
1222
|
+
const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1223
|
+
return `${normalizedBase}${normalizedPathname}`;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function renderInitialNav(docs: DocRecord[], currentId: string, pathBase: string): string {
|
|
1210
1227
|
const currentIndex = docs.findIndex((doc) => doc.id === currentId);
|
|
1211
1228
|
if (currentIndex === -1) {
|
|
1212
1229
|
return "";
|
|
@@ -1217,16 +1234,16 @@ function renderInitialNav(docs: DocRecord[], currentId: string): string {
|
|
|
1217
1234
|
|
|
1218
1235
|
let html = "";
|
|
1219
1236
|
if (prev) {
|
|
1220
|
-
html += `<a href="${escapeHtmlAttribute(prev.route)}" class="nav-link nav-link-prev" data-route="${escapeHtmlAttribute(prev.route)}"><div class="nav-link-label"><span class="material-symbols-outlined">arrow_back</span>Previous</div><div class="nav-link-title">${escapeHtmlAttribute(prev.title)}</div></a>`;
|
|
1237
|
+
html += `<a href="${escapeHtmlAttribute(toPathWithBase(prev.route, pathBase))}" class="nav-link nav-link-prev" data-route="${escapeHtmlAttribute(prev.route)}"><div class="nav-link-label"><span class="material-symbols-outlined">arrow_back</span>Previous</div><div class="nav-link-title">${escapeHtmlAttribute(prev.title)}</div></a>`;
|
|
1221
1238
|
}
|
|
1222
1239
|
if (next) {
|
|
1223
|
-
html += `<a href="${escapeHtmlAttribute(next.route)}" class="nav-link nav-link-next" data-route="${escapeHtmlAttribute(next.route)}"><div class="nav-link-label">Next<span class="material-symbols-outlined">arrow_forward</span></div><div class="nav-link-title">${escapeHtmlAttribute(next.title)}</div></a>`;
|
|
1240
|
+
html += `<a href="${escapeHtmlAttribute(toPathWithBase(next.route, pathBase))}" class="nav-link nav-link-next" data-route="${escapeHtmlAttribute(next.route)}"><div class="nav-link-label">Next<span class="material-symbols-outlined">arrow_forward</span></div><div class="nav-link-title">${escapeHtmlAttribute(next.title)}</div></a>`;
|
|
1224
1241
|
}
|
|
1225
1242
|
|
|
1226
1243
|
return html;
|
|
1227
1244
|
}
|
|
1228
1245
|
|
|
1229
|
-
function renderInitialBacklinks(backlinks: Manifest["docs"][number]["backlinks"]): string {
|
|
1246
|
+
function renderInitialBacklinks(backlinks: Manifest["docs"][number]["backlinks"], pathBase: string): string {
|
|
1230
1247
|
if (backlinks.length === 0) {
|
|
1231
1248
|
return "";
|
|
1232
1249
|
}
|
|
@@ -1236,7 +1253,7 @@ function renderInitialBacklinks(backlinks: Manifest["docs"][number]["backlinks"]
|
|
|
1236
1253
|
const prefixHtml = backlink.prefix
|
|
1237
1254
|
? `<span class="backlink-prefix">${escapeHtmlAttribute(backlink.prefix)}</span>`
|
|
1238
1255
|
: "";
|
|
1239
|
-
html += `<li class="backlinks-item"><a href="${escapeHtmlAttribute(backlink.route)}" class="backlink-link" data-route="${escapeHtmlAttribute(backlink.route)}">${prefixHtml}<span class="backlink-text">${escapeHtmlAttribute(backlink.title)}</span></a></li>`;
|
|
1256
|
+
html += `<li class="backlinks-item"><a href="${escapeHtmlAttribute(toPathWithBase(backlink.route, pathBase))}" class="backlink-link" data-route="${escapeHtmlAttribute(backlink.route)}">${prefixHtml}<span class="backlink-text">${escapeHtmlAttribute(backlink.title)}</span></a></li>`;
|
|
1240
1257
|
}
|
|
1241
1258
|
html += "</ul>";
|
|
1242
1259
|
return html;
|
|
@@ -1247,6 +1264,7 @@ function buildInitialView(
|
|
|
1247
1264
|
docs: DocRecord[],
|
|
1248
1265
|
contentHtml: string,
|
|
1249
1266
|
manifestDocById: Map<string, Manifest["docs"][number]>,
|
|
1267
|
+
pathBase: string,
|
|
1250
1268
|
): AppShellInitialView {
|
|
1251
1269
|
const manifestDoc = manifestDocById.get(doc.id);
|
|
1252
1270
|
return {
|
|
@@ -1256,8 +1274,8 @@ function buildInitialView(
|
|
|
1256
1274
|
breadcrumbHtml: renderInitialBreadcrumb(doc.route),
|
|
1257
1275
|
metaHtml: renderInitialMeta(doc),
|
|
1258
1276
|
contentHtml,
|
|
1259
|
-
backlinksHtml: renderInitialBacklinks(manifestDoc?.backlinks ?? []),
|
|
1260
|
-
navHtml: renderInitialNav(docs, doc.id),
|
|
1277
|
+
backlinksHtml: renderInitialBacklinks(manifestDoc?.backlinks ?? [], pathBase),
|
|
1278
|
+
navHtml: renderInitialNav(docs, doc.id, pathBase),
|
|
1261
1279
|
};
|
|
1262
1280
|
}
|
|
1263
1281
|
|
|
@@ -1270,10 +1288,11 @@ async function writeShellPages(
|
|
|
1270
1288
|
contentByDocId: Map<string, string>,
|
|
1271
1289
|
): Promise<void> {
|
|
1272
1290
|
const manifestDocById = new Map(manifest.docs.map((doc) => [doc.id, doc]));
|
|
1291
|
+
const pathBase = options.seo?.pathBase ?? "";
|
|
1273
1292
|
const indexDoc = pickHomeDoc(docs);
|
|
1274
1293
|
const indexOutputPath = "index.html";
|
|
1275
1294
|
const indexInitialView = indexDoc
|
|
1276
|
-
? buildInitialView(indexDoc, docs, contentByDocId.get(indexDoc.id) ?? "", manifestDocById)
|
|
1295
|
+
? buildInitialView(indexDoc, docs, contentByDocId.get(indexDoc.id) ?? "", manifestDocById, pathBase)
|
|
1277
1296
|
: null;
|
|
1278
1297
|
const shell = renderAppShellHtml(
|
|
1279
1298
|
buildShellMeta("/", null, options),
|
|
@@ -1291,7 +1310,7 @@ async function writeShellPages(
|
|
|
1291
1310
|
|
|
1292
1311
|
for (const doc of docs) {
|
|
1293
1312
|
const routeOutputPath = toRouteOutputPath(doc.route);
|
|
1294
|
-
const initialView = buildInitialView(doc, docs, contentByDocId.get(doc.id) ?? "", manifestDocById);
|
|
1313
|
+
const initialView = buildInitialView(doc, docs, contentByDocId.get(doc.id) ?? "", manifestDocById, pathBase);
|
|
1295
1314
|
await writeOutputIfChanged(
|
|
1296
1315
|
context,
|
|
1297
1316
|
routeOutputPath,
|
|
@@ -1495,16 +1514,12 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
|
|
|
1495
1514
|
outputContext.nextHashes[contentRelPath] = sourceHash;
|
|
1496
1515
|
|
|
1497
1516
|
if (unchanged) {
|
|
1498
|
-
skippedDocs += 1;
|
|
1499
1517
|
const outputFile = Bun.file(outputPath);
|
|
1500
1518
|
if (await outputFile.exists()) {
|
|
1519
|
+
skippedDocs += 1;
|
|
1501
1520
|
contentByDocId.set(doc.id, await outputFile.text());
|
|
1502
|
-
|
|
1503
|
-
const resolver = createWikiResolver(wikiLookup, doc);
|
|
1504
|
-
const renderResult = await markdownRenderer.render(doc.body, resolver);
|
|
1505
|
-
contentByDocId.set(doc.id, renderResult.html);
|
|
1521
|
+
continue;
|
|
1506
1522
|
}
|
|
1507
|
-
continue;
|
|
1508
1523
|
}
|
|
1509
1524
|
|
|
1510
1525
|
const resolver = createWikiResolver(wikiLookup, doc);
|
package/src/config.ts
CHANGED
|
@@ -199,6 +199,13 @@ export async function loadPinnedMenuConfig(
|
|
|
199
199
|
return normalizePinnedMenu((parsed as Record<string, unknown>).pinnedMenu, "[menu-config]");
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
function ensureIntegerOption(value: unknown, optionLabel: string, min: number): number {
|
|
203
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < min) {
|
|
204
|
+
throw new Error(`[config] "${optionLabel}" must be an integer >= ${min}`);
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
|
|
202
209
|
export function resolveBuildOptions(
|
|
203
210
|
cli: CliArgs,
|
|
204
211
|
userConfig: UserConfig,
|
|
@@ -219,14 +226,24 @@ export function resolveBuildOptions(
|
|
|
219
226
|
: undefined;
|
|
220
227
|
const configPinnedMenu = normalizePinnedMenu(userConfig.pinnedMenu, "[config]");
|
|
221
228
|
const resolvedPinnedMenu = pinnedMenu ?? configPinnedMenu;
|
|
229
|
+
const newWithinDays = ensureIntegerOption(
|
|
230
|
+
cli.newWithinDays ?? userConfig.ui?.newWithinDays ?? DEFAULTS.newWithinDays,
|
|
231
|
+
"--new-within-days (or ui.newWithinDays)",
|
|
232
|
+
0,
|
|
233
|
+
);
|
|
234
|
+
const recentLimit = ensureIntegerOption(
|
|
235
|
+
cli.recentLimit ?? userConfig.ui?.recentLimit ?? DEFAULTS.recentLimit,
|
|
236
|
+
"--recent-limit (or ui.recentLimit)",
|
|
237
|
+
1,
|
|
238
|
+
);
|
|
222
239
|
|
|
223
240
|
return {
|
|
224
241
|
vaultDir,
|
|
225
242
|
outDir,
|
|
226
243
|
exclude: mergedExclude,
|
|
227
244
|
staticPaths,
|
|
228
|
-
newWithinDays
|
|
229
|
-
recentLimit
|
|
245
|
+
newWithinDays,
|
|
246
|
+
recentLimit,
|
|
230
247
|
siteTitle,
|
|
231
248
|
pinnedMenu: resolvedPinnedMenu,
|
|
232
249
|
wikilinks: userConfig.markdown?.wikilinks ?? DEFAULTS.wikilinks,
|
package/src/runtime/app.js
CHANGED
|
@@ -37,15 +37,63 @@ function toSafeUrlPath(input) {
|
|
|
37
37
|
.join("/");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function normalizePathname(pathname) {
|
|
41
|
+
let normalized = "/";
|
|
42
|
+
try {
|
|
43
|
+
normalized = decodeURIComponent(pathname || "/");
|
|
44
|
+
} catch {
|
|
45
|
+
normalized = String(pathname || "/");
|
|
46
|
+
}
|
|
47
|
+
if (!normalized.startsWith("/")) {
|
|
48
|
+
normalized = `/${normalized}`;
|
|
49
|
+
}
|
|
50
|
+
return normalized.replace(/\/+/g, "/") || "/";
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
function normalizeRoute(pathname) {
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
54
|
+
const normalized = normalizePathname(pathname);
|
|
55
|
+
if (normalized.endsWith("/")) {
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
return `${normalized}/`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizePathBase(pathBase) {
|
|
62
|
+
if (typeof pathBase !== "string") {
|
|
63
|
+
return "";
|
|
44
64
|
}
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
|
|
66
|
+
const cleaned = pathBase.trim().replace(/\\/g, "/");
|
|
67
|
+
if (!cleaned || cleaned === "/") {
|
|
68
|
+
return "";
|
|
47
69
|
}
|
|
48
|
-
|
|
70
|
+
|
|
71
|
+
return `/${cleaned.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stripPathBase(pathname, pathBase) {
|
|
75
|
+
const normalizedPath = normalizePathname(pathname);
|
|
76
|
+
if (!pathBase) {
|
|
77
|
+
return normalizedPath;
|
|
78
|
+
}
|
|
79
|
+
if (normalizedPath === pathBase) {
|
|
80
|
+
return "/";
|
|
81
|
+
}
|
|
82
|
+
if (normalizedPath.startsWith(`${pathBase}/`)) {
|
|
83
|
+
return normalizedPath.slice(pathBase.length) || "/";
|
|
84
|
+
}
|
|
85
|
+
return normalizedPath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toPathWithBase(pathname, pathBase) {
|
|
89
|
+
const normalizedPath = normalizePathname(pathname);
|
|
90
|
+
if (!pathBase) {
|
|
91
|
+
return toSafeUrlPath(normalizedPath);
|
|
92
|
+
}
|
|
93
|
+
if (normalizedPath === "/") {
|
|
94
|
+
return toSafeUrlPath(`${pathBase}/`);
|
|
95
|
+
}
|
|
96
|
+
return toSafeUrlPath(`${pathBase}${normalizedPath}`);
|
|
49
97
|
}
|
|
50
98
|
|
|
51
99
|
function loadInitialViewData() {
|
|
@@ -114,21 +162,9 @@ function loadInitialManifestData() {
|
|
|
114
162
|
}
|
|
115
163
|
}
|
|
116
164
|
|
|
117
|
-
function resolveRouteFromLocation(routeMap) {
|
|
118
|
-
const direct = normalizeRoute(location.pathname);
|
|
119
|
-
|
|
120
|
-
return direct;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (location.search.length > 1) {
|
|
124
|
-
const recovered = normalizeRoute(`${location.pathname}?${location.search.slice(1)}`);
|
|
125
|
-
if (routeMap[recovered]) {
|
|
126
|
-
history.replaceState(null, "", toSafeUrlPath(recovered));
|
|
127
|
-
return recovered;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return direct;
|
|
165
|
+
function resolveRouteFromLocation(routeMap, pathBase) {
|
|
166
|
+
const direct = normalizeRoute(stripPathBase(location.pathname, pathBase));
|
|
167
|
+
return routeMap[direct] ? direct : direct;
|
|
132
168
|
}
|
|
133
169
|
|
|
134
170
|
function resolveSiteTitle(manifest) {
|
|
@@ -612,7 +648,7 @@ function initializeTreeLabelTooltip(treeRoot, tooltipEl) {
|
|
|
612
648
|
};
|
|
613
649
|
}
|
|
614
650
|
|
|
615
|
-
function createFolderNode(node, expandedSet, fileRowsById, depth = 0) {
|
|
651
|
+
function createFolderNode(node, expandedSet, fileRowsById, pathBase, depth = 0) {
|
|
616
652
|
const wrapper = document.createElement("div");
|
|
617
653
|
wrapper.className = node.virtual ? "tree-folder virtual" : "tree-folder";
|
|
618
654
|
wrapper.style.setProperty("--tree-depth", String(depth));
|
|
@@ -627,7 +663,7 @@ function createFolderNode(node, expandedSet, fileRowsById, depth = 0) {
|
|
|
627
663
|
const isExpanded = node.virtual ? true : expandedSet.has(node.path);
|
|
628
664
|
const iconName = isExpanded ? "folder_open" : "folder";
|
|
629
665
|
|
|
630
|
-
row.innerHTML = `<span class="material-symbols-outlined">${iconName}</span><span class="tree-label">${node.name}</span>`;
|
|
666
|
+
row.innerHTML = `<span class="material-symbols-outlined">${iconName}</span><span class="tree-label">${escapeHtmlAttr(node.name)}</span>`;
|
|
631
667
|
row.setAttribute("aria-expanded", String(isExpanded));
|
|
632
668
|
|
|
633
669
|
const children = document.createElement("div");
|
|
@@ -638,9 +674,9 @@ function createFolderNode(node, expandedSet, fileRowsById, depth = 0) {
|
|
|
638
674
|
|
|
639
675
|
for (const child of node.children) {
|
|
640
676
|
if (child.type === "folder") {
|
|
641
|
-
children.appendChild(createFolderNode(child, expandedSet, fileRowsById, depth + 1));
|
|
677
|
+
children.appendChild(createFolderNode(child, expandedSet, fileRowsById, pathBase, depth + 1));
|
|
642
678
|
} else {
|
|
643
|
-
children.appendChild(createFileNode(child, fileRowsById, depth + 1));
|
|
679
|
+
children.appendChild(createFileNode(child, fileRowsById, pathBase, depth + 1));
|
|
644
680
|
}
|
|
645
681
|
}
|
|
646
682
|
|
|
@@ -649,9 +685,9 @@ function createFolderNode(node, expandedSet, fileRowsById, depth = 0) {
|
|
|
649
685
|
return wrapper;
|
|
650
686
|
}
|
|
651
687
|
|
|
652
|
-
function createFileNode(node, fileRowsById, depth = 0) {
|
|
688
|
+
function createFileNode(node, fileRowsById, pathBase, depth = 0) {
|
|
653
689
|
const row = document.createElement("a");
|
|
654
|
-
row.href = node.route;
|
|
690
|
+
row.href = toPathWithBase(node.route, pathBase);
|
|
655
691
|
row.className = "tree-row tree-file-row";
|
|
656
692
|
row.dataset.rowType = "file";
|
|
657
693
|
row.dataset.route = node.route;
|
|
@@ -751,7 +787,7 @@ function renderMeta(doc) {
|
|
|
751
787
|
return items.join("");
|
|
752
788
|
}
|
|
753
789
|
|
|
754
|
-
function renderNav(docs, docIndexById, currentId) {
|
|
790
|
+
function renderNav(docs, docIndexById, currentId, pathBase) {
|
|
755
791
|
const currentIndex = docIndexById.get(currentId) ?? -1;
|
|
756
792
|
if (currentIndex === -1) return "";
|
|
757
793
|
|
|
@@ -761,23 +797,23 @@ function renderNav(docs, docIndexById, currentId) {
|
|
|
761
797
|
let html = "";
|
|
762
798
|
|
|
763
799
|
if (prev) {
|
|
764
|
-
html += `<a href="${
|
|
800
|
+
html += `<a href="${toPathWithBase(prev.route, pathBase)}" class="nav-link nav-link-prev" data-route="${escapeHtmlAttr(prev.route)}">
|
|
765
801
|
<div class="nav-link-label"><span class="material-symbols-outlined">arrow_back</span>Previous</div>
|
|
766
|
-
<div class="nav-link-title">${prev.title}</div>
|
|
802
|
+
<div class="nav-link-title">${escapeHtmlAttr(prev.title)}</div>
|
|
767
803
|
</a>`;
|
|
768
804
|
}
|
|
769
805
|
|
|
770
806
|
if (next) {
|
|
771
|
-
html += `<a href="${
|
|
807
|
+
html += `<a href="${toPathWithBase(next.route, pathBase)}" class="nav-link nav-link-next" data-route="${escapeHtmlAttr(next.route)}">
|
|
772
808
|
<div class="nav-link-label">Next<span class="material-symbols-outlined">arrow_forward</span></div>
|
|
773
|
-
<div class="nav-link-title">${next.title}</div>
|
|
809
|
+
<div class="nav-link-title">${escapeHtmlAttr(next.title)}</div>
|
|
774
810
|
</a>`;
|
|
775
811
|
}
|
|
776
812
|
|
|
777
813
|
return html;
|
|
778
814
|
}
|
|
779
815
|
|
|
780
|
-
function renderBacklinks(doc) {
|
|
816
|
+
function renderBacklinks(doc, pathBase) {
|
|
781
817
|
const backlinks = Array.isArray(doc.backlinks) ? doc.backlinks : [];
|
|
782
818
|
if (backlinks.length === 0) {
|
|
783
819
|
return "";
|
|
@@ -790,7 +826,7 @@ function renderBacklinks(doc) {
|
|
|
790
826
|
: "";
|
|
791
827
|
const route = typeof backlink.route === "string" ? normalizeRoute(backlink.route) : "/";
|
|
792
828
|
const title = typeof backlink.title === "string" && backlink.title.trim().length > 0 ? backlink.title : route;
|
|
793
|
-
html += `<li class="backlinks-item"><a href="${
|
|
829
|
+
html += `<li class="backlinks-item"><a href="${toPathWithBase(route, pathBase)}" class="backlink-link" data-route="${escapeHtmlAttr(route)}">${prefix}<span class="backlink-text">${escapeHtmlAttr(title)}</span></a></li>`;
|
|
794
830
|
}
|
|
795
831
|
html += "</ul>";
|
|
796
832
|
return html;
|
|
@@ -1209,13 +1245,15 @@ async function start() {
|
|
|
1209
1245
|
});
|
|
1210
1246
|
|
|
1211
1247
|
let manifest = loadInitialManifestData();
|
|
1248
|
+
const initialPathBase = normalizePathBase(manifest?.pathBase);
|
|
1212
1249
|
if (!manifest) {
|
|
1213
|
-
const manifestRes = await fetch("/manifest.json");
|
|
1250
|
+
const manifestRes = await fetch(toPathWithBase("/manifest.json", initialPathBase));
|
|
1214
1251
|
if (!manifestRes.ok) {
|
|
1215
1252
|
throw new Error(`Failed to load manifest: ${manifestRes.status}`);
|
|
1216
1253
|
}
|
|
1217
1254
|
manifest = await manifestRes.json();
|
|
1218
1255
|
}
|
|
1256
|
+
const pathBase = normalizePathBase(manifest.pathBase);
|
|
1219
1257
|
const siteTitle = resolveSiteTitle(manifest);
|
|
1220
1258
|
const defaultBranch = normalizeBranch(manifest.defaultBranch) || DEFAULT_BRANCH;
|
|
1221
1259
|
const availableBranchSet = new Set([defaultBranch]);
|
|
@@ -1312,9 +1350,9 @@ async function start() {
|
|
|
1312
1350
|
|
|
1313
1351
|
for (const node of view.tree) {
|
|
1314
1352
|
if (node.type === "folder") {
|
|
1315
|
-
treeRoot.appendChild(createFolderNode(node, state.expanded, treeFileRowsById));
|
|
1353
|
+
treeRoot.appendChild(createFolderNode(node, state.expanded, treeFileRowsById, pathBase));
|
|
1316
1354
|
} else {
|
|
1317
|
-
treeRoot.appendChild(createFileNode(node, treeFileRowsById));
|
|
1355
|
+
treeRoot.appendChild(createFileNode(node, treeFileRowsById, pathBase));
|
|
1318
1356
|
}
|
|
1319
1357
|
}
|
|
1320
1358
|
|
|
@@ -1333,7 +1371,7 @@ async function start() {
|
|
|
1333
1371
|
backlinksEl.hidden = true;
|
|
1334
1372
|
return;
|
|
1335
1373
|
}
|
|
1336
|
-
const html = renderBacklinks(doc);
|
|
1374
|
+
const html = renderBacklinks(doc, pathBase);
|
|
1337
1375
|
backlinksEl.innerHTML = html;
|
|
1338
1376
|
backlinksEl.hidden = html.length === 0;
|
|
1339
1377
|
};
|
|
@@ -1377,7 +1415,7 @@ async function start() {
|
|
|
1377
1415
|
markActive(treeFileRowsById, activeFileState, "");
|
|
1378
1416
|
announceA11yStatus("탐색 실패: 요청한 문서를 찾을 수 없습니다.");
|
|
1379
1417
|
if (push) {
|
|
1380
|
-
history.pushState(null, "",
|
|
1418
|
+
history.pushState(null, "", toPathWithBase(route, pathBase));
|
|
1381
1419
|
}
|
|
1382
1420
|
return;
|
|
1383
1421
|
}
|
|
@@ -1388,7 +1426,7 @@ async function start() {
|
|
|
1388
1426
|
}
|
|
1389
1427
|
|
|
1390
1428
|
if (push) {
|
|
1391
|
-
history.pushState(null, "",
|
|
1429
|
+
history.pushState(null, "", toPathWithBase(route, pathBase));
|
|
1392
1430
|
}
|
|
1393
1431
|
|
|
1394
1432
|
state.currentDocId = id;
|
|
@@ -1406,7 +1444,7 @@ async function start() {
|
|
|
1406
1444
|
titleEl.textContent = doc.title;
|
|
1407
1445
|
metaEl.innerHTML = renderMeta(doc);
|
|
1408
1446
|
updateBacklinks(doc);
|
|
1409
|
-
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id);
|
|
1447
|
+
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id, pathBase);
|
|
1410
1448
|
document.title = composeDocumentTitle(doc.title, siteTitle);
|
|
1411
1449
|
if (viewerEl instanceof HTMLElement) {
|
|
1412
1450
|
viewerEl.scrollTo(0, 0);
|
|
@@ -1419,7 +1457,7 @@ async function start() {
|
|
|
1419
1457
|
titleEl.textContent = doc.title;
|
|
1420
1458
|
metaEl.innerHTML = renderMeta(doc);
|
|
1421
1459
|
|
|
1422
|
-
const res = await fetch(
|
|
1460
|
+
const res = await fetch(toPathWithBase(doc.contentUrl, pathBase));
|
|
1423
1461
|
if (!res.ok) {
|
|
1424
1462
|
contentEl.innerHTML = '<p class="placeholder">본문을 불러오지 못했습니다.</p>';
|
|
1425
1463
|
updateBacklinks(null);
|
|
@@ -1431,7 +1469,7 @@ async function start() {
|
|
|
1431
1469
|
contentEl.innerHTML = await res.text();
|
|
1432
1470
|
|
|
1433
1471
|
updateBacklinks(doc);
|
|
1434
|
-
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id);
|
|
1472
|
+
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id, pathBase);
|
|
1435
1473
|
|
|
1436
1474
|
document.title = composeDocumentTitle(doc.title, siteTitle);
|
|
1437
1475
|
if (viewerEl instanceof HTMLElement) {
|
|
@@ -1594,7 +1632,7 @@ async function start() {
|
|
|
1594
1632
|
updateBranchInfo();
|
|
1595
1633
|
renderTree(state);
|
|
1596
1634
|
|
|
1597
|
-
const currentRoute = resolveRouteFromLocation(view.routeMap);
|
|
1635
|
+
const currentRoute = resolveRouteFromLocation(view.routeMap, pathBase);
|
|
1598
1636
|
if (view.routeMap[currentRoute]) {
|
|
1599
1637
|
await state.navigate(currentRoute, false);
|
|
1600
1638
|
return;
|
|
@@ -1608,20 +1646,25 @@ async function start() {
|
|
|
1608
1646
|
updateBranchInfo();
|
|
1609
1647
|
renderTree(state);
|
|
1610
1648
|
|
|
1611
|
-
const currentRoute = resolveRouteFromLocation(view.routeMap);
|
|
1649
|
+
const currentRoute = resolveRouteFromLocation(view.routeMap, pathBase);
|
|
1612
1650
|
const initialRoute = currentRoute === "/" ? pickHomeRoute(view) : currentRoute;
|
|
1613
1651
|
handleLayoutChange();
|
|
1614
1652
|
await state.navigate(initialRoute, currentRoute === "/" && initialRoute !== "/");
|
|
1615
1653
|
|
|
1616
1654
|
window.addEventListener("popstate", async () => {
|
|
1617
|
-
await state.navigate(resolveRouteFromLocation(view.routeMap), false);
|
|
1655
|
+
await state.navigate(resolveRouteFromLocation(view.routeMap, pathBase), false);
|
|
1618
1656
|
});
|
|
1619
1657
|
}
|
|
1620
1658
|
|
|
1621
1659
|
start().catch((error) => {
|
|
1622
1660
|
const contentEl = document.getElementById("viewer-content");
|
|
1623
1661
|
if (contentEl) {
|
|
1624
|
-
|
|
1662
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1663
|
+
contentEl.innerHTML = "";
|
|
1664
|
+
const placeholder = document.createElement("p");
|
|
1665
|
+
placeholder.className = "placeholder";
|
|
1666
|
+
placeholder.textContent = `초기화 실패: ${message}`;
|
|
1667
|
+
contentEl.appendChild(placeholder);
|
|
1625
1668
|
}
|
|
1626
1669
|
console.error(error);
|
|
1627
1670
|
});
|