@limcpf/everything-is-a-markdown 0.6.7 → 0.6.8

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
@@ -3,11 +3,13 @@
3
3
  언어: [English](README.md) | **한국어**
4
4
 
5
5
  Everything-Is-A-Markdown은 로컬 Markdown 볼트를 정적 웹사이트로 빌드해, 폴더/파일 탐색 구조를 유지한 채 공개할 수 있게 해주는 CLI 도구입니다.
6
+ 공개 URL은 `prefix` 기준으로 생성되고, 사이드바 폴더 구조는 실제 파일 경로가 아니라 `category_path` 기준으로 만들어집니다.
6
7
 
7
8
  ## 이 앱은 무엇을 하나요
8
9
 
9
10
  - Markdown 볼트에서 정적 문서/블로그 사이트를 생성
10
11
  - `publish: true` 문서만 선택적으로 공개
12
+ - 공개 문서는 `prefix`와 `category_path`를 필수로 사용
11
13
  - 비공개 노트와 공개 콘텐츠 분리
12
14
 
13
15
  ## Obsidian 사용자에게 특히 잘 맞습니다
@@ -73,6 +75,10 @@ const config = {
73
75
  vaultDir: "./vault",
74
76
  outDir: "./dist",
75
77
  staticPaths: ["assets", "public/favicon.ico"],
78
+ pinnedMenu: {
79
+ label: "NOTICE",
80
+ categoryPath: "announcements",
81
+ },
76
82
  seo: {
77
83
  siteUrl: "https://example.com",
78
84
  pathBase: "/blog",
@@ -97,6 +103,12 @@ export default config;
97
103
  - 지정한 경로의 파일들을 `dist`에 같은 상대 경로로 복사
98
104
  - 예: 볼트 `assets/og.png` -> `dist/assets/og.png`
99
105
 
106
+ `pinnedMenu`:
107
+
108
+ - `categoryPath`: 문서 frontmatter `category_path` prefix 기준으로 가상 폴더를 구성
109
+ - `sourceDir`: 기존 파일 경로 prefix 기준
110
+ - 둘 다 있으면 `categoryPath`가 우선
111
+
100
112
  `seo.pathBase`:
101
113
 
102
114
  - 서브패스 배포(예: `/blog`)를 정식 지원합니다.
@@ -123,7 +135,9 @@ bun run build -- --vault ./test-vault --out ./dist
123
135
  이 값이 `true`인 문서만 빌드 결과에 포함됩니다.
124
136
  - `prefix: "A-01"`
125
137
  문서의 공개 식별자이자 라우트(`/A-01/`) 기준입니다.
126
- `publish: true`인데 `prefix`가 없으면 빌드 경고를 출력하고 문서를 제외합니다.
138
+ - `category_path: "engineering/blog/frontend"`
139
+ 사이드바 폴더 구조 기준입니다.
140
+ `publish: true`인데 `prefix`나 `category_path`가 없으면 빌드 경고를 출력하고 문서를 제외합니다.
127
141
 
128
142
  선택:
129
143
 
@@ -150,6 +164,7 @@ bun run build -- --vault ./test-vault --out ./dist
150
164
  ---
151
165
  publish: true
152
166
  prefix: "DEV-01"
167
+ category_path: "guides/setup"
153
168
  branch: dev
154
169
  title: Setup Guide
155
170
  date: "2024-09-15"
package/README.md CHANGED
@@ -13,7 +13,8 @@ The generated site keeps a two-panel experience:
13
13
 
14
14
  - Builds a static site from a local Markdown vault
15
15
  - Publishes only notes with `publish: true`
16
- - Requires a `prefix` field for every published note and uses it as the public route
16
+ - Requires `prefix` and `category_path` for every published note
17
+ - Uses `prefix` as the public route and `category_path` as the sidebar folder path
17
18
  - Supports Obsidian-style wikilinks such as `[[note]]` and `[[note|label]]`
18
19
  - Renders code blocks with Shiki
19
20
  - Renders Mermaid blocks in the browser with runtime fallback handling
@@ -25,7 +26,7 @@ The generated site keeps a two-panel experience:
25
26
 
26
27
  ## Important Behavior
27
28
 
28
- This project currently uses `prefix`-based public routes, not vault-relative path routes.
29
+ This project currently uses `prefix`-based public routes, not vault-relative path routes. Sidebar folders are built from `category_path`, not from the actual file location.
29
30
 
30
31
  Example:
31
32
 
@@ -125,8 +126,9 @@ Only documents with `publish: true` are considered for output.
125
126
 
126
127
  - `publish: true`
127
128
  - `prefix: "BC-VO-02"`
129
+ - `category_path: "engineering/blog/frontend"`
128
130
 
129
- If `publish: true` is set but `prefix` is missing, the note is skipped and a build warning is emitted.
131
+ If `publish: true` is set but `prefix` or `category_path` is missing, the note is skipped and a build warning is emitted.
130
132
 
131
133
  ### Supported fields
132
134
 
@@ -144,6 +146,7 @@ Example:
144
146
  ---
145
147
  publish: true
146
148
  prefix: BC-VO-02
149
+ category_path: engineering/blog/frontend
147
150
  branch: dev
148
151
  title: Setup Guide
149
152
  date: "2024-09-15"
@@ -157,6 +160,8 @@ tags: ["tutorial", "setup"]
157
160
 
158
161
  Public routes are derived from `prefix`, not from the file path.
159
162
 
163
+ Sidebar folders are derived from `category_path`, not from the file path.
164
+
160
165
  Normalization rules:
161
166
 
162
167
  - trims whitespace
@@ -225,7 +230,7 @@ export default {
225
230
  staticPaths: ["assets", "public/favicon.ico"],
226
231
  pinnedMenu: {
227
232
  label: "NOTICE",
228
- sourceDir: "announcements",
233
+ categoryPath: "announcements",
229
234
  },
230
235
  ui: {
231
236
  newWithinDays: 7,
@@ -269,6 +274,8 @@ export default {
269
274
  - `exclude`: extra exclude globs.
270
275
  - `staticPaths`: vault-relative files or directories copied into output.
271
276
  - `pinnedMenu`: optional virtual folder shown above `Recent`.
277
+ - `pinnedMenu.sourceDir`: optional legacy file-path prefix matcher.
278
+ - `pinnedMenu.categoryPath`: optional category-path prefix matcher for the sidebar virtual folder.
272
279
  - `ui.newWithinDays`: threshold for NEW badge.
273
280
  - `ui.recentLimit`: number of items in `Recent`.
274
281
  - `markdown.wikilinks`: enable or disable wikilink resolution.
@@ -287,14 +294,19 @@ export default {
287
294
 
288
295
  ### `pinnedMenu`
289
296
 
290
- `pinnedMenu` creates a virtual folder at the top of the sidebar by collecting published docs whose vault-relative path starts with the configured `sourceDir`.
297
+ `pinnedMenu` creates a virtual folder at the top of the sidebar by collecting published docs that match either:
298
+
299
+ - `categoryPath`: the document `category_path` equals or starts with the configured prefix
300
+ - `sourceDir`: the vault-relative file path starts with the configured prefix
301
+
302
+ If both are present, `categoryPath` wins.
291
303
 
292
304
  Example:
293
305
 
294
306
  ```ts
295
307
  pinnedMenu: {
296
308
  label: "NOTICE",
297
- sourceDir: "announcements",
309
+ categoryPath: "announcements",
298
310
  }
299
311
  ```
300
312
 
@@ -310,7 +322,7 @@ JSON shape:
310
322
  {
311
323
  "pinnedMenu": {
312
324
  "label": "NOTICE",
313
- "sourceDir": "announcements"
325
+ "categoryPath": "announcements"
314
326
  }
315
327
  }
316
328
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limcpf/everything-is-a-markdown",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
package/src/build.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  toDocId,
22
22
  } from "./utils";
23
23
 
24
- const CACHE_VERSION = 3;
24
+ const CACHE_VERSION = 4;
25
25
  const CACHE_DIR_NAME = ".cache";
26
26
  const CACHE_FILE_NAME = "build-index.json";
27
27
  const DEFAULT_BRANCH = "dev";
@@ -125,6 +125,7 @@ function normalizeCachedSourceEntry(value: unknown): CachedSourceEntry | null {
125
125
  const draft = value.draft === true;
126
126
  const title = typeof value.title === "string" && value.title.trim().length > 0 ? value.title.trim() : undefined;
127
127
  const prefix = typeof value.prefix === "string" && value.prefix.trim().length > 0 ? value.prefix.trim() : undefined;
128
+ const categoryPath = normalizeCategoryPath(value.categoryPath);
128
129
  const date = typeof value.date === "string" && value.date.trim().length > 0 ? value.date.trim() : undefined;
129
130
  const updatedDate =
130
131
  typeof value.updatedDate === "string" && value.updatedDate.trim().length > 0 ? value.updatedDate.trim() : undefined;
@@ -147,6 +148,7 @@ function normalizeCachedSourceEntry(value: unknown): CachedSourceEntry | null {
147
148
  draft,
148
149
  title,
149
150
  prefix,
151
+ categoryPath,
150
152
  date,
151
153
  updatedDate,
152
154
  description,
@@ -276,6 +278,24 @@ function normalizeFrontmatterDate(value: unknown): string | null {
276
278
  return null;
277
279
  }
278
280
 
281
+ function normalizeCategoryPath(value: unknown): string | undefined {
282
+ if (typeof value !== "string") {
283
+ return undefined;
284
+ }
285
+
286
+ const normalized = value
287
+ .trim()
288
+ .replace(/\\/g, "/")
289
+ .replace(/^\/+/, "")
290
+ .replace(/\/+$/, "")
291
+ .split("/")
292
+ .map((segment) => segment.trim())
293
+ .filter(Boolean)
294
+ .join("/");
295
+
296
+ return normalized.length > 0 ? normalized : undefined;
297
+ }
298
+
279
299
  function extractFrontmatterScalar(raw: string, key: string): string | null {
280
300
  const frontmatterMatch = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
281
301
  if (!frontmatterMatch) {
@@ -353,6 +373,15 @@ function pickDocPrefix(frontmatter: Record<string, unknown>, raw: string): strin
353
373
  return undefined;
354
374
  }
355
375
 
376
+ function pickDocCategoryPath(frontmatter: Record<string, unknown>, raw: string): string | undefined {
377
+ const literal = normalizeCategoryPath(extractFrontmatterScalar(raw, "category_path"));
378
+ if (literal) {
379
+ return literal;
380
+ }
381
+
382
+ return normalizeCategoryPath(frontmatter.category_path);
383
+ }
384
+
356
385
  function appendRouteSuffix(route: string, suffix: string): string {
357
386
  const clean = route.replace(/^\/+/, "").replace(/\/+$/, "");
358
387
  if (!clean) {
@@ -470,6 +499,7 @@ function toCachedSourceEntry(raw: string, parsed: matter.GrayMatterFile<string>)
470
499
  draft: parsed.data.draft === true,
471
500
  title: typeof parsed.data.title === "string" && parsed.data.title.trim().length > 0 ? parsed.data.title.trim() : undefined,
472
501
  prefix: pickDocPrefix(parsed.data as Record<string, unknown>, raw),
502
+ categoryPath: pickDocCategoryPath(parsed.data as Record<string, unknown>, raw),
473
503
  date: pickDocDate(parsed.data as Record<string, unknown>, raw),
474
504
  updatedDate: pickDocUpdatedDate(parsed.data as Record<string, unknown>, raw),
475
505
  description: typeof parsed.data.description === "string" ? parsed.data.description.trim() || undefined : undefined,
@@ -500,6 +530,7 @@ function toDocRecord(
500
530
  fileName,
501
531
  title: entry.title ?? makeTitleFromFileName(fileName),
502
532
  prefix: entry.prefix,
533
+ categoryPath: entry.categoryPath ?? "",
503
534
  date: entry.date,
504
535
  updatedDate: entry.updatedDate,
505
536
  description: entry.description,
@@ -564,6 +595,12 @@ async function readPublishedDocs(options: BuildOptions, previousSources: BuildCa
564
595
  console.warn(`[publish] Skipped published doc without prefix: ${relPath}`);
565
596
  continue;
566
597
  }
598
+
599
+ if (!completeEntry.categoryPath) {
600
+ console.warn(`[publish] Skipped published doc without category_path: ${relPath}`);
601
+ continue;
602
+ }
603
+
567
604
  docs.push(toDocRecord(sourcePath, relPath, completeEntry, newThreshold));
568
605
  }
569
606
 
@@ -755,6 +792,10 @@ function compareByRecentDateThenPath(left: DocRecord, right: DocRecord): number
755
792
  return left.relNoExt.localeCompare(right.relNoExt, "ko-KR");
756
793
  }
757
794
 
795
+ function matchesPathPrefix(value: string, prefix: string): boolean {
796
+ return value === prefix || value.startsWith(`${prefix}/`);
797
+ }
798
+
758
799
  function pickHomeDoc(docs: DocRecord[]): DocRecord | null {
759
800
  const inDefaultBranch = docs.filter((doc) => doc.branch == null || doc.branch === DEFAULT_BRANCH);
760
801
  const candidates = inDefaultBranch.length > 0 ? inDefaultBranch : docs;
@@ -771,17 +812,27 @@ function buildPinnedMenuFolder(docs: DocRecord[], options: BuildOptions): Folder
771
812
  return null;
772
813
  }
773
814
 
815
+ const categoryPath = options.pinnedMenu.categoryPath;
774
816
  const sourceDir = options.pinnedMenu.sourceDir;
775
- const sourcePrefix = `${sourceDir}/`;
776
817
  const children = docs
777
- .filter((doc) => doc.relNoExt.startsWith(sourcePrefix))
818
+ .filter((doc) => {
819
+ if (categoryPath) {
820
+ return matchesPathPrefix(doc.categoryPath, categoryPath);
821
+ }
822
+ if (sourceDir) {
823
+ return matchesPathPrefix(doc.relNoExt, sourceDir);
824
+ }
825
+ return false;
826
+ })
778
827
  .sort((left, right) => left.relNoExt.localeCompare(right.relNoExt, "ko-KR"))
779
828
  .map((doc) => fileNodeFromDoc(doc));
780
829
 
830
+ const pathKey = categoryPath ? `category/${categoryPath}` : `source/${sourceDir ?? "unknown"}`;
831
+
781
832
  return {
782
833
  type: "folder",
783
834
  name: options.pinnedMenu.label,
784
- path: `__virtual__/pinned/${sourceDir}`,
835
+ path: `__virtual__/pinned/${pathKey}`,
785
836
  virtual: true,
786
837
  children,
787
838
  };
@@ -799,8 +850,7 @@ function buildTree(docs: DocRecord[], options: BuildOptions): TreeNode[] {
799
850
  folderIndex.set("", root);
800
851
 
801
852
  for (const doc of docs) {
802
- const segments = doc.relNoExt.split("/");
803
- const folders = segments.slice(0, -1);
853
+ const folders = doc.categoryPath.split("/");
804
854
 
805
855
  let currentPath = "";
806
856
  let parent = root;
@@ -861,6 +911,7 @@ function buildManifest(docs: DocRecord[], tree: TreeNode[], options: BuildOption
861
911
  route: doc.route,
862
912
  title: doc.title,
863
913
  prefix: doc.prefix,
914
+ categoryPath: doc.categoryPath,
864
915
  date: doc.date,
865
916
  updatedDate: doc.updatedDate,
866
917
  tags: doc.tags,
package/src/config.ts CHANGED
@@ -151,19 +151,38 @@ function normalizePinnedMenu(raw: unknown, errorPrefix = "[config]"): PinnedMenu
151
151
 
152
152
  const menu = raw as Record<string, unknown>;
153
153
  const sourceDirRaw = menu.sourceDir;
154
+ const categoryPathRaw = menu.categoryPath;
154
155
  const labelRaw = menu.label;
156
+ const normalizeMenuPath = (value: unknown, fieldName: "sourceDir" | "categoryPath"): string | undefined => {
157
+ if (value == null) {
158
+ return undefined;
159
+ }
160
+ if (typeof value !== "string" || value.trim().length === 0) {
161
+ throw new Error(`${errorPrefix} "pinnedMenu.${fieldName}" must be a non-empty string`);
162
+ }
155
163
 
156
- if (typeof sourceDirRaw !== "string" || sourceDirRaw.trim().length === 0) {
157
- throw new Error(`${errorPrefix} "pinnedMenu.sourceDir" must be a non-empty string`);
158
- }
164
+ const normalized = value
165
+ .trim()
166
+ .replace(/\\/g, "/")
167
+ .replace(/^\/+/, "")
168
+ .replace(/\/+$/, "")
169
+ .split("/")
170
+ .map((segment) => segment.trim())
171
+ .filter(Boolean)
172
+ .join("/");
173
+
174
+ if (!normalized) {
175
+ throw new Error(`${errorPrefix} "pinnedMenu.${fieldName}" must not be root`);
176
+ }
177
+
178
+ return normalized;
179
+ };
180
+
181
+ const normalizedSourceDir = normalizeMenuPath(sourceDirRaw, "sourceDir");
182
+ const normalizedCategoryPath = normalizeMenuPath(categoryPathRaw, "categoryPath");
159
183
 
160
- const normalizedSourceDir = sourceDirRaw
161
- .trim()
162
- .replace(/\\/g, "/")
163
- .replace(/^\/+/, "")
164
- .replace(/\/+$/, "");
165
- if (!normalizedSourceDir) {
166
- throw new Error(`${errorPrefix} "pinnedMenu.sourceDir" must not be root`);
184
+ if (!normalizedSourceDir && !normalizedCategoryPath) {
185
+ throw new Error(`${errorPrefix} "pinnedMenu" must include "sourceDir" or "categoryPath"`);
167
186
  }
168
187
 
169
188
  const label =
@@ -174,6 +193,7 @@ function normalizePinnedMenu(raw: unknown, errorPrefix = "[config]"): PinnedMenu
174
193
  return {
175
194
  label,
176
195
  sourceDir: normalizedSourceDir,
196
+ categoryPath: normalizedCategoryPath,
177
197
  };
178
198
  }
179
199
 
package/src/types.ts CHANGED
@@ -32,7 +32,8 @@ export interface BuildSeoOptions {
32
32
 
33
33
  export interface PinnedMenuOption {
34
34
  label: string;
35
- sourceDir: string;
35
+ sourceDir?: string;
36
+ categoryPath?: string;
36
37
  }
37
38
 
38
39
  export interface UserConfig {
@@ -42,7 +43,8 @@ export interface UserConfig {
42
43
  staticPaths?: string[];
43
44
  pinnedMenu?: {
44
45
  label?: string;
45
- sourceDir: string;
46
+ sourceDir?: string;
47
+ categoryPath?: string;
46
48
  };
47
49
  ui?: {
48
50
  newWithinDays?: number;
@@ -96,6 +98,7 @@ export interface DocRecord {
96
98
  fileName: string;
97
99
  title: string;
98
100
  prefix?: string;
101
+ categoryPath: string;
99
102
  date?: string;
100
103
  updatedDate?: string;
101
104
  description?: string;
@@ -156,6 +159,7 @@ export interface Manifest {
156
159
  route: string;
157
160
  title: string;
158
161
  prefix?: string;
162
+ categoryPath: string;
159
163
  contentUrl: string;
160
164
  date?: string;
161
165
  updatedDate?: string;
@@ -185,6 +189,7 @@ export interface BuildCache {
185
189
  draft: boolean;
186
190
  title?: string;
187
191
  prefix?: string;
192
+ categoryPath?: string;
188
193
  date?: string;
189
194
  updatedDate?: string;
190
195
  description?: string;