@limcpf/everything-is-a-markdown 0.5.0 → 0.5.2
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 +25 -0
- package/README.md +25 -0
- package/package.json +1 -1
- package/src/build.ts +114 -3
- package/src/config.ts +51 -2
- package/src/dev.ts +12 -1
- package/src/runtime/app.js +21 -2
- package/src/template.ts +4 -1
- package/src/types.ts +4 -0
package/README.ko.md
CHANGED
|
@@ -40,6 +40,31 @@ bun run blog [build|dev|clean] [options]
|
|
|
40
40
|
- `--exclude <glob>`: 제외 패턴 추가 (반복 가능)
|
|
41
41
|
- `--port <n>`: 개발 서버 포트 (기본 `3000`)
|
|
42
42
|
|
|
43
|
+
## 설정 파일 (`blog.config.ts`)
|
|
44
|
+
|
|
45
|
+
SEO/UI/정적 파일 설정은 config 파일에서 관리할 수 있습니다.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const config = {
|
|
49
|
+
vaultDir: "./vault",
|
|
50
|
+
outDir: "./dist",
|
|
51
|
+
staticPaths: ["assets", "public/favicon.ico"],
|
|
52
|
+
seo: {
|
|
53
|
+
siteUrl: "https://example.com",
|
|
54
|
+
defaultOgImage: "/assets/og.png",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default config;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`staticPaths`:
|
|
62
|
+
|
|
63
|
+
- 볼트 기준 상대 경로 배열
|
|
64
|
+
- 폴더와 파일 모두 지정 가능
|
|
65
|
+
- 지정한 경로의 파일들을 `dist`에 같은 상대 경로로 복사
|
|
66
|
+
- 예: 볼트 `assets/og.png` -> `dist/assets/og.png`
|
|
67
|
+
|
|
43
68
|
예시:
|
|
44
69
|
|
|
45
70
|
```bash
|
package/README.md
CHANGED
|
@@ -40,6 +40,31 @@ Common options:
|
|
|
40
40
|
- `--exclude <glob>`: Add exclude pattern (repeatable)
|
|
41
41
|
- `--port <n>`: Dev server port (default `3000`)
|
|
42
42
|
|
|
43
|
+
## Config File (`blog.config.ts`)
|
|
44
|
+
|
|
45
|
+
Use a config file for SEO/UI/static assets.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const config = {
|
|
49
|
+
vaultDir: "./vault",
|
|
50
|
+
outDir: "./dist",
|
|
51
|
+
staticPaths: ["assets", "public/favicon.ico"],
|
|
52
|
+
seo: {
|
|
53
|
+
siteUrl: "https://example.com",
|
|
54
|
+
defaultOgImage: "/assets/og.png",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default config;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`staticPaths`:
|
|
62
|
+
|
|
63
|
+
- Array of vault-relative paths
|
|
64
|
+
- Supports both folders and files
|
|
65
|
+
- Copies all matched files into the same relative location in `dist`
|
|
66
|
+
- Example: `assets/og.png` in vault becomes `dist/assets/og.png`
|
|
67
|
+
|
|
43
68
|
Examples:
|
|
44
69
|
|
|
45
70
|
```bash
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import matter from "gray-matter";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
removeEmptyParents,
|
|
17
18
|
removeFileIfExists,
|
|
18
19
|
stripMdExt,
|
|
20
|
+
toPosixPath,
|
|
19
21
|
toDocId,
|
|
20
22
|
toRoute,
|
|
21
23
|
} from "./utils";
|
|
@@ -655,7 +657,7 @@ function isNewByFrontmatterDate(date: string | undefined, newThreshold: number):
|
|
|
655
657
|
return publishedAt != null && publishedAt >= newThreshold;
|
|
656
658
|
}
|
|
657
659
|
|
|
658
|
-
function getRecentSortEpochMs(doc: DocRecord): number {
|
|
660
|
+
function getRecentSortEpochMs(doc: DocRecord): number | null {
|
|
659
661
|
return parseDateToEpochMs(doc.updatedDate) ?? parseDateToEpochMs(doc.date);
|
|
660
662
|
}
|
|
661
663
|
|
|
@@ -804,6 +806,7 @@ function buildManifest(docs: DocRecord[], tree: TreeNode[], options: BuildOption
|
|
|
804
806
|
|
|
805
807
|
return {
|
|
806
808
|
generatedAt: new Date().toISOString(),
|
|
809
|
+
siteTitle: resolveSiteTitle(options),
|
|
807
810
|
defaultBranch: DEFAULT_BRANCH,
|
|
808
811
|
branches,
|
|
809
812
|
ui: {
|
|
@@ -816,6 +819,24 @@ function buildManifest(docs: DocRecord[], tree: TreeNode[], options: BuildOption
|
|
|
816
819
|
};
|
|
817
820
|
}
|
|
818
821
|
|
|
822
|
+
function resolveSiteTitle(options: BuildOptions): string {
|
|
823
|
+
const value = options.siteTitle ?? options.seo?.siteName ?? options.seo?.defaultTitle ?? DEFAULT_SITE_TITLE;
|
|
824
|
+
const trimmed = value.trim();
|
|
825
|
+
return trimmed.length > 0 ? trimmed : DEFAULT_SITE_TITLE;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function composeDocumentTitle(pageTitle: string, siteTitle: string): string {
|
|
829
|
+
const left = pageTitle.trim();
|
|
830
|
+
const right = siteTitle.trim();
|
|
831
|
+
if (!left) {
|
|
832
|
+
return right || DEFAULT_SITE_TITLE;
|
|
833
|
+
}
|
|
834
|
+
if (!right || left === right) {
|
|
835
|
+
return left;
|
|
836
|
+
}
|
|
837
|
+
return `${left} - ${right}`;
|
|
838
|
+
}
|
|
839
|
+
|
|
819
840
|
function pickSeoImageDefaults(
|
|
820
841
|
seo: BuildOptions["seo"],
|
|
821
842
|
): { social: string | null; og: string | null; twitter: string | null } {
|
|
@@ -905,6 +926,92 @@ async function writeOutputIfChanged(
|
|
|
905
926
|
await Bun.write(outputPath, content);
|
|
906
927
|
}
|
|
907
928
|
|
|
929
|
+
async function copyOutputFileIfChanged(
|
|
930
|
+
context: OutputWriteContext,
|
|
931
|
+
relOutputPath: string,
|
|
932
|
+
sourcePath: string,
|
|
933
|
+
): Promise<void> {
|
|
934
|
+
const bytes = new Uint8Array(await Bun.file(sourcePath).arrayBuffer());
|
|
935
|
+
const outputHash = crypto.createHash("sha1").update(bytes).digest("hex");
|
|
936
|
+
context.nextHashes[relOutputPath] = outputHash;
|
|
937
|
+
|
|
938
|
+
const outputPath = path.join(context.outDir, relOutputPath);
|
|
939
|
+
const unchanged = context.previousHashes[relOutputPath] === outputHash;
|
|
940
|
+
if (unchanged && (await Bun.file(outputPath).exists())) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
await ensureDir(path.dirname(outputPath));
|
|
945
|
+
await Bun.write(outputPath, bytes);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function listFilesRecursively(baseDir: string): Promise<string[]> {
|
|
949
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
950
|
+
entries.sort((left, right) => left.name.localeCompare(right.name, "ko-KR"));
|
|
951
|
+
|
|
952
|
+
const files: string[] = [];
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
const absolutePath = path.join(baseDir, entry.name);
|
|
955
|
+
if (entry.isDirectory()) {
|
|
956
|
+
const children = await listFilesRecursively(absolutePath);
|
|
957
|
+
for (const child of children) {
|
|
958
|
+
files.push(path.join(entry.name, child));
|
|
959
|
+
}
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (entry.isFile()) {
|
|
964
|
+
files.push(entry.name);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return files;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function copyStaticPaths(context: OutputWriteContext, options: BuildOptions): Promise<void> {
|
|
972
|
+
for (const staticPath of options.staticPaths) {
|
|
973
|
+
const sourcePath = path.resolve(options.vaultDir, staticPath);
|
|
974
|
+
|
|
975
|
+
let sourceStat;
|
|
976
|
+
try {
|
|
977
|
+
sourceStat = await fs.stat(sourcePath);
|
|
978
|
+
} catch {
|
|
979
|
+
console.warn(`[static] path not found: ${staticPath}`);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (sourceStat.isDirectory()) {
|
|
984
|
+
const files = await listFilesRecursively(sourcePath);
|
|
985
|
+
for (const file of files) {
|
|
986
|
+
const relFilePath = toPosixPath(file);
|
|
987
|
+
const relOutputPath = path.posix.join(staticPath, relFilePath);
|
|
988
|
+
const filePath = path.join(sourcePath, file);
|
|
989
|
+
await copyOutputFileIfChanged(context, relOutputPath, filePath);
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (sourceStat.isFile()) {
|
|
995
|
+
await copyOutputFileIfChanged(context, staticPath, sourcePath);
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
console.warn(`[static] unsupported path type, skipped: ${staticPath}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function removeStaleOutputs(context: OutputWriteContext): Promise<void> {
|
|
1004
|
+
for (const previousPath of Object.keys(context.previousHashes)) {
|
|
1005
|
+
if (Object.hasOwn(context.nextHashes, previousPath)) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const outputPath = path.join(context.outDir, previousPath);
|
|
1010
|
+
await removeFileIfExists(outputPath);
|
|
1011
|
+
await removeEmptyParents(path.dirname(outputPath), context.outDir);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
908
1015
|
function toRelativeAssetPath(fromOutputPath: string, assetOutputPath: string): string {
|
|
909
1016
|
const fromDir = path.posix.dirname(fromOutputPath);
|
|
910
1017
|
const relative = path.posix.relative(fromDir, assetOutputPath);
|
|
@@ -948,11 +1055,13 @@ async function writeRuntimeAssets(context: OutputWriteContext): Promise<RuntimeA
|
|
|
948
1055
|
}
|
|
949
1056
|
|
|
950
1057
|
function buildShellMeta(route: string, doc: DocRecord | null, options: BuildOptions): AppShellMeta {
|
|
951
|
-
const defaultTitle = options.seo?.defaultTitle ?? DEFAULT_SITE_TITLE;
|
|
1058
|
+
const defaultTitle = options.seo?.defaultTitle ?? options.siteTitle ?? DEFAULT_SITE_TITLE;
|
|
1059
|
+
const siteTitle = resolveSiteTitle(options);
|
|
952
1060
|
const defaultDescription = options.seo?.defaultDescription ?? DEFAULT_SITE_DESCRIPTION;
|
|
953
1061
|
const description = typeof doc?.description === "string" && doc.description.trim().length > 0 ? doc.description.trim() : undefined;
|
|
954
1062
|
const canonicalUrl = options.seo ? buildCanonicalUrl(route, options.seo) : undefined;
|
|
955
|
-
const
|
|
1063
|
+
const baseTitle = doc?.title ?? defaultTitle;
|
|
1064
|
+
const title = composeDocumentTitle(baseTitle, siteTitle);
|
|
956
1065
|
const imageDefaults = pickSeoImageDefaults(options.seo);
|
|
957
1066
|
const ogImage = imageDefaults.og ?? imageDefaults.social ?? undefined;
|
|
958
1067
|
const twitterImage = imageDefaults.twitter ?? imageDefaults.social ?? undefined;
|
|
@@ -1221,6 +1330,7 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
|
|
|
1221
1330
|
nextHashes: {},
|
|
1222
1331
|
};
|
|
1223
1332
|
const runtimeAssets = await writeRuntimeAssets(outputContext);
|
|
1333
|
+
await copyStaticPaths(outputContext, options);
|
|
1224
1334
|
|
|
1225
1335
|
const tree = buildTree(docs, options);
|
|
1226
1336
|
const manifest = buildManifest(docs, tree, options);
|
|
@@ -1292,6 +1402,7 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
|
|
|
1292
1402
|
|
|
1293
1403
|
await writeShellPages(outputContext, docs, manifest, options, runtimeAssets, contentByDocId);
|
|
1294
1404
|
await writeSeoArtifacts(outputContext, docs, options);
|
|
1405
|
+
await removeStaleOutputs(outputContext);
|
|
1295
1406
|
|
|
1296
1407
|
await writeCache(cachePath, nextCache);
|
|
1297
1408
|
|
package/src/config.ts
CHANGED
|
@@ -132,6 +132,45 @@ function normalizePinnedMenu(raw: unknown, errorPrefix = "[config]"): PinnedMenu
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function normalizeStaticPaths(raw: unknown, errorPrefix = "[config]"): string[] {
|
|
136
|
+
if (raw == null) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(raw)) {
|
|
140
|
+
throw new Error(`${errorPrefix} "staticPaths" must be an array of strings`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalized = new Set<string>();
|
|
144
|
+
for (const [index, value] of raw.entries()) {
|
|
145
|
+
if (typeof value !== "string") {
|
|
146
|
+
throw new Error(`${errorPrefix} "staticPaths[${index}]" must be a string`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const cleaned = value
|
|
150
|
+
.trim()
|
|
151
|
+
.replace(/\\/g, "/")
|
|
152
|
+
.replace(/^\.\/+/, "")
|
|
153
|
+
.replace(/\/+$/, "");
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
!cleaned ||
|
|
157
|
+
cleaned === "." ||
|
|
158
|
+
cleaned === ".." ||
|
|
159
|
+
cleaned.startsWith("../") ||
|
|
160
|
+
cleaned.startsWith("/") ||
|
|
161
|
+
path.isAbsolute(value.trim())
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`${errorPrefix} "staticPaths[${index}]" must be a non-empty vault-relative path (for example: "assets")`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
normalized.add(cleaned);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Array.from(normalized);
|
|
172
|
+
}
|
|
173
|
+
|
|
135
174
|
export async function loadPinnedMenuConfig(
|
|
136
175
|
configPath: string | undefined,
|
|
137
176
|
cwd = process.cwd(),
|
|
@@ -166,19 +205,29 @@ export function resolveBuildOptions(
|
|
|
166
205
|
pinnedMenu: PinnedMenuOption | null,
|
|
167
206
|
cwd = process.cwd(),
|
|
168
207
|
): BuildOptions {
|
|
208
|
+
const vaultDir = path.resolve(cwd, cli.vaultDir ?? userConfig.vaultDir ?? DEFAULTS.vaultDir);
|
|
209
|
+
const outDir = path.resolve(cwd, cli.outDir ?? userConfig.outDir ?? DEFAULTS.outDir);
|
|
169
210
|
const cfgExclude = userConfig.exclude ?? [];
|
|
170
211
|
const cliExclude = cli.exclude ?? [];
|
|
171
212
|
const mergedExclude = Array.from(new Set([...DEFAULTS.exclude, ...cfgExclude, ...cliExclude]));
|
|
213
|
+
const staticPaths = normalizeStaticPaths(userConfig.staticPaths, "[config]");
|
|
172
214
|
const seo = normalizeSeoConfig(userConfig.seo);
|
|
215
|
+
const siteTitleRaw = userConfig.seo?.siteName ?? userConfig.seo?.defaultTitle;
|
|
216
|
+
const siteTitle =
|
|
217
|
+
typeof siteTitleRaw === "string" && siteTitleRaw.trim().length > 0
|
|
218
|
+
? siteTitleRaw.trim()
|
|
219
|
+
: undefined;
|
|
173
220
|
const configPinnedMenu = normalizePinnedMenu(userConfig.pinnedMenu, "[config]");
|
|
174
221
|
const resolvedPinnedMenu = pinnedMenu ?? configPinnedMenu;
|
|
175
222
|
|
|
176
223
|
return {
|
|
177
|
-
vaultDir
|
|
178
|
-
outDir
|
|
224
|
+
vaultDir,
|
|
225
|
+
outDir,
|
|
179
226
|
exclude: mergedExclude,
|
|
227
|
+
staticPaths,
|
|
180
228
|
newWithinDays: cli.newWithinDays ?? userConfig.ui?.newWithinDays ?? DEFAULTS.newWithinDays,
|
|
181
229
|
recentLimit: cli.recentLimit ?? userConfig.ui?.recentLimit ?? DEFAULTS.recentLimit,
|
|
230
|
+
siteTitle,
|
|
182
231
|
pinnedMenu: resolvedPinnedMenu,
|
|
183
232
|
wikilinks: userConfig.markdown?.wikilinks ?? DEFAULTS.wikilinks,
|
|
184
233
|
imagePolicy: userConfig.markdown?.images ?? DEFAULTS.imagePolicy,
|
package/src/dev.ts
CHANGED
|
@@ -8,6 +8,10 @@ interface DevOptions {
|
|
|
8
8
|
port: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function isStaticPathMatch(relPath: string, staticPath: string): boolean {
|
|
12
|
+
return relPath === staticPath || relPath.startsWith(`${staticPath}/`);
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
async function resolveOutputFile(outDir: string, pathname: string): Promise<string | null> {
|
|
12
16
|
const decoded = decodeURIComponent(pathname);
|
|
13
17
|
const clean = decoded.replace(/^\/+/, "");
|
|
@@ -89,7 +93,14 @@ export async function runDev(options: BuildOptions, devOptions: DevOptions): Pro
|
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
watcher.on("all", (_event, changedPath) => {
|
|
92
|
-
|
|
96
|
+
const relPath = path.relative(options.vaultDir, changedPath).split(path.sep).join("/");
|
|
97
|
+
if (!relPath || relPath.startsWith("..")) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isMarkdownChange = /\.md$/i.test(changedPath);
|
|
102
|
+
const isStaticChange = options.staticPaths.some((staticPath) => isStaticPathMatch(relPath, staticPath));
|
|
103
|
+
if (!isMarkdownChange && !isStaticChange) {
|
|
93
104
|
return;
|
|
94
105
|
}
|
|
95
106
|
scheduleRebuild();
|
package/src/runtime/app.js
CHANGED
|
@@ -11,6 +11,7 @@ const DESKTOP_VIEWER_MIN = 680;
|
|
|
11
11
|
const DESKTOP_SPLITTER_WIDTH = 10;
|
|
12
12
|
const DESKTOP_SPLITTER_STEP = 24;
|
|
13
13
|
const DEFAULT_BRANCH = "dev";
|
|
14
|
+
const DEFAULT_SITE_TITLE = "File-System Blog";
|
|
14
15
|
const BRANCH_KEY = "fsblog.branch";
|
|
15
16
|
const FOCUSABLE_SELECTOR =
|
|
16
17
|
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
@@ -130,6 +131,23 @@ function resolveRouteFromLocation(routeMap) {
|
|
|
130
131
|
return direct;
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function resolveSiteTitle(manifest) {
|
|
135
|
+
const value = typeof manifest?.siteTitle === "string" ? manifest.siteTitle.trim() : "";
|
|
136
|
+
return value || DEFAULT_SITE_TITLE;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function composeDocumentTitle(pageTitle, siteTitle) {
|
|
140
|
+
const left = String(pageTitle ?? "").trim();
|
|
141
|
+
const right = String(siteTitle ?? "").trim();
|
|
142
|
+
if (!left) {
|
|
143
|
+
return right || DEFAULT_SITE_TITLE;
|
|
144
|
+
}
|
|
145
|
+
if (!right || left === right) {
|
|
146
|
+
return left;
|
|
147
|
+
}
|
|
148
|
+
return `${left} - ${right}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
function formatMetaDateTime(value) {
|
|
134
152
|
const parsed = new Date(value);
|
|
135
153
|
if (!Number.isFinite(parsed.getTime())) {
|
|
@@ -1127,6 +1145,7 @@ async function start() {
|
|
|
1127
1145
|
}
|
|
1128
1146
|
manifest = await manifestRes.json();
|
|
1129
1147
|
}
|
|
1148
|
+
const siteTitle = resolveSiteTitle(manifest);
|
|
1130
1149
|
const defaultBranch = normalizeBranch(manifest.defaultBranch) || DEFAULT_BRANCH;
|
|
1131
1150
|
const availableBranchSet = new Set([defaultBranch]);
|
|
1132
1151
|
for (const doc of manifest.docs) {
|
|
@@ -1296,7 +1315,7 @@ async function start() {
|
|
|
1296
1315
|
|
|
1297
1316
|
if (shouldUseInitialView) {
|
|
1298
1317
|
hasHydratedInitialView = true;
|
|
1299
|
-
document.title =
|
|
1318
|
+
document.title = composeDocumentTitle(initialViewData.title, siteTitle);
|
|
1300
1319
|
if (viewerEl instanceof HTMLElement) {
|
|
1301
1320
|
viewerEl.scrollTo(0, 0);
|
|
1302
1321
|
}
|
|
@@ -1320,7 +1339,7 @@ async function start() {
|
|
|
1320
1339
|
|
|
1321
1340
|
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id);
|
|
1322
1341
|
|
|
1323
|
-
document.title =
|
|
1342
|
+
document.title = composeDocumentTitle(doc.title, siteTitle);
|
|
1324
1343
|
if (viewerEl instanceof HTMLElement) {
|
|
1325
1344
|
viewerEl.scrollTo(0, 0);
|
|
1326
1345
|
}
|
package/src/template.ts
CHANGED
|
@@ -183,6 +183,9 @@ export function renderAppShellHtml(
|
|
|
183
183
|
const initialManifestScript = renderInitialManifestScript(manifest);
|
|
184
184
|
const symbolFontStylesheet =
|
|
185
185
|
"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap";
|
|
186
|
+
const appTitle = typeof manifest?.siteTitle === "string" && manifest.siteTitle.trim().length > 0
|
|
187
|
+
? manifest.siteTitle.trim()
|
|
188
|
+
: DEFAULT_TITLE;
|
|
186
189
|
const initialTitle = initialView ? escapeHtmlAttribute(initialView.title) : "문서를 선택하세요";
|
|
187
190
|
const initialBreadcrumb = initialView ? initialView.breadcrumbHtml : "";
|
|
188
191
|
const initialMeta = initialView ? initialView.metaHtml : "";
|
|
@@ -213,7 +216,7 @@ ${headMeta}
|
|
|
213
216
|
<div class="sidebar-header">
|
|
214
217
|
<h1 class="sidebar-title">
|
|
215
218
|
<span class="material-symbols-outlined icon-terminal">terminal</span>
|
|
216
|
-
|
|
219
|
+
${escapeHtmlAttribute(appTitle)}
|
|
217
220
|
</h1>
|
|
218
221
|
<button id="sidebar-close" class="sidebar-close" type="button" aria-label="탐색기 닫기">
|
|
219
222
|
<span class="material-symbols-outlined">close</span>
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface UserConfig {
|
|
|
39
39
|
vaultDir?: string;
|
|
40
40
|
outDir?: string;
|
|
41
41
|
exclude?: string[];
|
|
42
|
+
staticPaths?: string[];
|
|
42
43
|
pinnedMenu?: {
|
|
43
44
|
label?: string;
|
|
44
45
|
sourceDir: string;
|
|
@@ -63,8 +64,10 @@ export interface BuildOptions {
|
|
|
63
64
|
vaultDir: string;
|
|
64
65
|
outDir: string;
|
|
65
66
|
exclude: string[];
|
|
67
|
+
staticPaths: string[];
|
|
66
68
|
newWithinDays: number;
|
|
67
69
|
recentLimit: number;
|
|
70
|
+
siteTitle?: string;
|
|
68
71
|
pinnedMenu: PinnedMenuOption | null;
|
|
69
72
|
wikilinks: boolean;
|
|
70
73
|
imagePolicy: ImagePolicy;
|
|
@@ -123,6 +126,7 @@ export type TreeNode = FolderNode | FileNode;
|
|
|
123
126
|
|
|
124
127
|
export interface Manifest {
|
|
125
128
|
generatedAt: string;
|
|
129
|
+
siteTitle: string;
|
|
126
130
|
defaultBranch: string;
|
|
127
131
|
branches: string[];
|
|
128
132
|
ui: {
|