@limcpf/everything-is-a-markdown 0.5.1 → 0.5.3

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limcpf/everything-is-a-markdown",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
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
 
@@ -924,6 +926,92 @@ async function writeOutputIfChanged(
924
926
  await Bun.write(outputPath, content);
925
927
  }
926
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
+
927
1015
  function toRelativeAssetPath(fromOutputPath: string, assetOutputPath: string): string {
928
1016
  const fromDir = path.posix.dirname(fromOutputPath);
929
1017
  const relative = path.posix.relative(fromDir, assetOutputPath);
@@ -1242,6 +1330,7 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
1242
1330
  nextHashes: {},
1243
1331
  };
1244
1332
  const runtimeAssets = await writeRuntimeAssets(outputContext);
1333
+ await copyStaticPaths(outputContext, options);
1245
1334
 
1246
1335
  const tree = buildTree(docs, options);
1247
1336
  const manifest = buildManifest(docs, tree, options);
@@ -1313,6 +1402,7 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
1313
1402
 
1314
1403
  await writeShellPages(outputContext, docs, manifest, options, runtimeAssets, contentByDocId);
1315
1404
  await writeSeoArtifacts(outputContext, docs, options);
1405
+ await removeStaleOutputs(outputContext);
1316
1406
 
1317
1407
  await writeCache(cachePath, nextCache);
1318
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,9 +205,12 @@ 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);
173
215
  const siteTitleRaw = userConfig.seo?.siteName ?? userConfig.seo?.defaultTitle;
174
216
  const siteTitle =
@@ -179,9 +221,10 @@ export function resolveBuildOptions(
179
221
  const resolvedPinnedMenu = pinnedMenu ?? configPinnedMenu;
180
222
 
181
223
  return {
182
- vaultDir: path.resolve(cwd, cli.vaultDir ?? userConfig.vaultDir ?? DEFAULTS.vaultDir),
183
- outDir: path.resolve(cwd, cli.outDir ?? userConfig.outDir ?? DEFAULTS.outDir),
224
+ vaultDir,
225
+ outDir,
184
226
  exclude: mergedExclude,
227
+ staticPaths,
185
228
  newWithinDays: cli.newWithinDays ?? userConfig.ui?.newWithinDays ?? DEFAULTS.newWithinDays,
186
229
  recentLimit: cli.recentLimit ?? userConfig.ui?.recentLimit ?? DEFAULTS.recentLimit,
187
230
  siteTitle,
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
- if (!/\.md$/i.test(changedPath)) {
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();
@@ -64,6 +64,8 @@
64
64
  --badge-new-fg: #ffffff;
65
65
  --mobile-toggle-bg: var(--latte-mauve);
66
66
  --mobile-toggle-fg: #ffffff;
67
+ --content-heading-accent-soft: rgba(136, 57, 239, 0.32);
68
+ --content-heading-subtle: var(--latte-subtext1);
67
69
  --desktop-sidebar-default: 420px;
68
70
  --desktop-sidebar-min: 320px;
69
71
  --desktop-viewer-min: 680px;
@@ -111,6 +113,8 @@
111
113
  --badge-new-fg: var(--mocha-crust);
112
114
  --mobile-toggle-bg: var(--mocha-mauve);
113
115
  --mobile-toggle-fg: var(--mocha-crust);
116
+ --content-heading-accent-soft: rgba(203, 166, 247, 0.34);
117
+ --content-heading-subtle: var(--mocha-subtext0);
114
118
  }
115
119
 
116
120
  * {
@@ -884,20 +888,55 @@ body.mobile-toggle-left .mobile-menu-toggle {
884
888
  color: var(--latte-subtext1);
885
889
  }
886
890
 
891
+ .viewer-content > :first-child {
892
+ margin-top: 0;
893
+ }
894
+
887
895
  .viewer-content h1,
888
896
  .viewer-content h2,
889
897
  .viewer-content h3,
890
898
  .viewer-content h4 {
891
899
  color: var(--latte-text);
892
- font-weight: 600;
893
- line-height: 1.35;
894
- margin-top: 2rem;
895
- margin-bottom: 1rem;
900
+ font-weight: 650;
901
+ line-height: 1.33;
902
+ letter-spacing: -0.015em;
903
+ text-wrap: balance;
904
+ margin-top: 2.2rem;
905
+ margin-bottom: 0.95rem;
906
+ scroll-margin-top: 20px;
907
+ }
908
+
909
+ .viewer-content h1 {
910
+ font-size: 2.08rem;
911
+ font-weight: 760;
912
+ letter-spacing: -0.02em;
913
+ margin-top: 2.8rem;
914
+ margin-bottom: 1.1rem;
915
+ padding-bottom: 0.4rem;
916
+ border-bottom: 1px solid var(--latte-surface0);
917
+ }
918
+
919
+ .viewer-content h2 {
920
+ font-size: 1.62rem;
921
+ margin-top: 2.6rem;
922
+ margin-bottom: 1.04rem;
923
+ padding: 0.1rem 0 0.18rem 0.55rem;
924
+ border-left: 3px solid var(--content-heading-accent-soft);
896
925
  }
897
926
 
898
- .viewer-content h1 { font-size: 2rem; }
899
- .viewer-content h2 { font-size: 1.5rem; }
900
- .viewer-content h3 { font-size: 1.25rem; }
927
+ .viewer-content h3 {
928
+ font-size: 1.34rem;
929
+ color: var(--content-heading-subtle);
930
+ margin-top: 2.15rem;
931
+ margin-bottom: 0.92rem;
932
+ }
933
+
934
+ .viewer-content h4 {
935
+ font-size: 1.14rem;
936
+ color: var(--content-heading-subtle);
937
+ margin-top: 1.85rem;
938
+ margin-bottom: 0.8rem;
939
+ }
901
940
 
902
941
  .viewer-content p {
903
942
  margin-bottom: 1.25rem;
@@ -1318,15 +1357,15 @@ body.mobile-toggle-left .mobile-menu-toggle {
1318
1357
  }
1319
1358
 
1320
1359
  .viewer-content h1 {
1321
- font-size: 1.72rem;
1360
+ font-size: 1.82rem;
1322
1361
  }
1323
1362
 
1324
1363
  .viewer-content h2 {
1325
- font-size: 1.34rem;
1364
+ font-size: 1.42rem;
1326
1365
  }
1327
1366
 
1328
1367
  .viewer-content h3 {
1329
- font-size: 1.16rem;
1368
+ font-size: 1.22rem;
1330
1369
  }
1331
1370
 
1332
1371
  .viewer-content table {
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,6 +64,7 @@ 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;
68
70
  siteTitle?: string;