for-humanity 0.13.1 → 0.14.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.14.0 — 2026-10-19
6
+
7
+ **Breaking change:** Configuration now belongs in the project root. CLI paths identify a project, not a Markdown folder. Move published documents into `content/public`, local-only documents into `content/private`, and the site home into `content/README.md`; use `content.root`, `content.public` and `content.private` to customize these paths. Existing flat document folders are not automatically published. See the [migration guide](https://for-humanity.fyi/migration/). Node.js 22.12.0 or later is still required.
8
+
9
+ - Focus public documentation on installation, configuration, authoring and deployment. Keep internal plans, release operations, development history and fictional project studies in local-only content.
10
+ - Show both document collections in `dev`; build and preview only the public site in `content/dist`.
11
+ - Preserve public document URLs and reserve `/private/` for local internal pages. Exclude internal pages, navigation data, sitemap entries and their assets from public builds.
12
+ - Reject public references to internal Markdown, images and snippets, including raw HTML and symlink targets.
13
+ - Move this repository's documentation to `content` and its configuration to the project root.
14
+ - Include a reusable `template/` starter with public/private folders and document migration from flat Markdown projects.
15
+ - Reject content-root symlinks into build output, Git metadata and installed dependencies.
16
+
5
17
  ## 0.13.1 — 2026-10-16
6
18
 
7
19
  **Upgrade:** CLI commands, configuration and Node.js requirements remain unchanged. Navigation scroll positions saved by older versions may reset once after upgrading.
package/README.md CHANGED
@@ -13,122 +13,97 @@ Write Markdown. Keep the context. Share a static site.
13
13
  [![MIT](https://img.shields.io/badge/license-MIT-1111aa?style=flat-square)](https://github.com/minu-ha/for-humanity/blob/main/LICENSE)
14
14
  [![npm](https://img.shields.io/npm/v/for-humanity?style=flat-square)](https://www.npmjs.com/package/for-humanity)
15
15
  ![Node 22.12.0+](https://img.shields.io/badge/node-22.12.0%2B-444444?style=flat-square)
16
- ![Hono JSX](https://img.shields.io/badge/Hono-JSX-444444?style=flat-square)
17
16
  ![Prototype](https://img.shields.io/badge/status-prototype-9a6400?style=flat-square)
18
17
 
19
- [Quick start](#quick-start) · [Guide](https://github.com/minu-ha/for-humanity/blob/main/templates/document/writing.md) · [API](https://github.com/minu-ha/for-humanity/blob/main/templates/document/api.md) · [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) · [Korean](https://github.com/minu-ha/for-humanity/blob/main/language/README.ko.md)
18
+ [Quick start](#quick-start) · [Guide](https://github.com/minu-ha/for-humanity/blob/main/content/public/writing.md) · [Reference](https://github.com/minu-ha/for-humanity/blob/main/content/public/api.md) · [Deployment](https://github.com/minu-ha/for-humanity/blob/main/content/public/deployment.md) · [Korean](https://github.com/minu-ha/for-humanity/blob/main/language/README.ko.md)
20
19
 
21
20
  </div>
22
21
 
23
- ## A little structure. Room to read.
22
+ ## Write, organize, publish
24
23
 
25
- - **Find your place.** Pages grouped by purpose, a reading order, a tree of contents and the current reading position.
26
- - **Keep the detail.** Notes for context, native disclosures for implementation details, and links that open folded sections.
27
- - **Show the connections.** Tables, highlighted code, official Mermaid diagrams including flowcharts, sequences and directory trees, status badges and color swatches.
28
- - **Share the result.** Static HTML, self-hosted fonts and a small browser script. Browser interactions stay separate from Markdown rendering.
29
- - **Read your way.** System-matched light and dark themes, keyboard navigation and a layout for narrow screens.
24
+ Write Markdown with code examples, tables, images and Mermaid diagrams. Organize pages into groups and nested navigation. Use notes for context and collapsible details for longer explanations.
25
+
26
+ The result is a static site with bundled fonts, light and dark themes, keyboard navigation and a layout that adapts to narrow screens.
30
27
 
31
28
  ## Quick start
32
29
 
33
- Node.js 22.12.0 or later and pnpm. Install the CLI in your document project:
30
+ You need Node.js 22.12.0 or later and pnpm. In a project with a `package.json`, install the package:
34
31
 
35
32
  ```sh
36
- pnpm add -D --save-exact for-humanity@0.13.1
37
- mkdir docs
33
+ pnpm add -D --save-exact for-humanity@0.14.0
38
34
  ```
39
35
 
40
- Create `docs/README.md` for the home page, then start the development server:
36
+ Copy `for-humanity.config.ts` and the `content` folder from `node_modules/for-humanity/template` into your project. If you already have documentation, follow [Migration](https://github.com/minu-ha/for-humanity/blob/main/content/public/migration.md) before moving files.
41
37
 
42
- ```sh
43
- pnpm exec for-humanity dev docs
44
- ```
38
+ Edit `content/README.md` for your home page:
45
39
 
46
- Open [localhost:4321](http://localhost:4321). Build and preview your static site:
40
+ ```markdown
41
+ # Project notes
47
42
 
48
- ```sh
49
- pnpm exec for-humanity build docs # Generate docs/dist
50
- pnpm exec for-humanity preview docs
43
+ Welcome to the project documentation.
51
44
  ```
52
45
 
53
- If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
54
-
55
- Version `0.13.1` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
56
- Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
57
- Set an optional public root `url` in the site configuration to generate `robots.txt` and `sitemap.xml`; projects without a URL emit neither file. See [Crawling](https://github.com/minu-ha/for-humanity/blob/main/templates/document/crawling.md).
58
-
59
- ### Develop the kit
60
-
61
- Version 0.13.0 requires Node.js 22.12.0 or later. Repository development uses one Vite server with Hono, source TSX reloads and CSS HMR.
62
-
63
- To work on the CLI or read this library's guides and examples locally, run the project from source:
46
+ Start the local server:
64
47
 
65
48
  ```sh
66
- git clone https://github.com/minu-ha/for-humanity.git
67
- cd for-humanity
68
- pnpm install
69
- pnpm dev
49
+ pnpm exec for-humanity dev
70
50
  ```
71
51
 
72
- Open [localhost:4321](http://localhost:4321) to read this library's usage guides, API reference and fictional examples.
52
+ Open [localhost:4321](http://localhost:4321). Edit the Markdown file and save to update the page. Follow [Commands](https://github.com/minu-ha/for-humanity/blob/main/content/public/commands.md) to add your first document.
53
+
54
+ Build and inspect the site before deployment:
73
55
 
74
56
  ```sh
75
- pnpm build # Generate templates/document/dist
76
- pnpm preview # Preview the static site
77
- pnpm build:pages # Add Cloudflare Pages headers and a 404 page
57
+ pnpm exec for-humanity build
58
+ pnpm exec for-humanity preview
78
59
  ```
79
60
 
80
- Changes pushed to `main` update the documentation site. A stable version tag such as `v0.13.1` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
81
- The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
61
+ Deploy the generated `content/dist` directory and add it to `.gitignore`. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/content/public/deployment.md) for hosting settings.
62
+
63
+ Keep published documents in `content/public` and local notes in `content/private`. The local server shows both; builds include only public documents and the home page. Folder names are configurable.
82
64
 
83
65
  ## Make it yours
84
66
 
85
- A document folder is enough to get started. Without a site configuration, the sidebar shows **for humanity**. The pixel face appears in the browser tab, and follows the mouse on desktop when the site sets `cursorFace: true`.
86
- The folder's `README.md` becomes the home page at `/`, with no frontmatter required. Click the site name to return home.
87
- This repository generates [the documentation home](https://github.com/minu-ha/for-humanity/blob/main/templates/document/README.md) from its root README.md and [the site changelog](https://github.com/minu-ha/for-humanity/blob/main/templates/document/changelog.md) from CHANGELOG.md. Edit those source files, then run `pnpm sync:docs`; `pnpm dev` and `pnpm build` also sync them on startup.
88
- Set your own name in `for-humanity.config.ts`, using the exported `SiteConfig` type for editor completion and static checking:
67
+ Set the site name and the order of navigation groups:
89
68
 
90
69
  ```ts
91
70
  import type {SiteConfig} from "for-humanity";
92
71
 
93
72
  export default {
94
73
  title: "Project notes",
95
- description: "Decisions, working notes and the details behind them.",
96
- navigation: ["Getting started", "Guide", "Reference", "Examples"],
74
+ description: "A guide to using the project.",
75
+ navigation: ["Getting started", "Guide", "Reference"],
97
76
  } satisfies SiteConfig;
98
77
  ```
99
78
 
100
- Existing `for-humanity.config.mjs` files remain supported. When migrating to TypeScript, replace the old file with the `.ts` file.
101
- Use one configuration file per document folder. The CLI validates values at startup; run `tsc --noEmit` in your project for static type checking.
79
+ Save this as `for-humanity.config.ts` in the project root, alongside `package.json`. Existing `.mjs` configuration is also supported; keep only one configuration file.
102
80
 
103
- See [Commands](https://github.com/minu-ha/for-humanity/blob/main/templates/document/commands.md) for using another document folder, and [Settings](https://github.com/minu-ha/for-humanity/blob/main/templates/document/settings.md) for configuration.
104
- Each document other than the root README chooses a `group` and optional `order`. Pages stay independent; groups do not need parent documents or nested folders. Document names may share the same first letter.
105
- Use `group: Guide` for one group or `group: [Projects, Example project, Research]` for nested navigation. Group paths do not change document URLs.
106
- Use `parent: writing` to place a document below another document in the same group. File-relative references such as `parent: ./readme` also work. Parent pages remain clickable and each page keeps its own URL; see [Navigation](https://github.com/minu-ha/for-humanity/blob/main/templates/document/navigation.md).
81
+ Each document declares a title, description and navigation group in frontmatter. See [Writing](https://github.com/minu-ha/for-humanity/blob/main/content/public/writing.md) for a complete example and [Settings](https://github.com/minu-ha/for-humanity/blob/main/content/public/settings.md) for available options.
107
82
 
108
- ## Write with context
83
+ ## Keep useful detail
109
84
 
110
85
  ```markdown
111
- :::note[Scope]
112
- What this document covers and what still needs a decision.
86
+ :::note[Before you start]
87
+ Check the requirements before following the steps.
113
88
  :::
114
89
 
115
- :::details[Implementation]
116
- The contract, code or reasoning behind the visible result.
90
+ :::details[More information]
91
+ Add examples or explanations readers can open when needed.
117
92
  :::
118
93
  ```
119
94
 
120
- [Parts](https://github.com/minu-ha/for-humanity/blob/main/templates/document/parts.md) pairs source examples with their rendered results. The [Design example](https://github.com/minu-ha/for-humanity/blob/main/templates/document/blueprint.md) uses a fictional reading-list app to show a screen flow, decisions, open questions and illustrative API contracts.
95
+ [Parts](https://github.com/minu-ha/for-humanity/blob/main/content/public/parts.md) shows the syntax alongside its rendered output.
121
96
 
122
97
  ## Documentation
123
98
 
124
- | Group | Read |
125
- | ----- | ---- |
126
- | Getting started | [Commands](https://github.com/minu-ha/for-humanity/blob/main/templates/document/commands.md) · [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) |
127
- | Guide | [Writing](https://github.com/minu-ha/for-humanity/blob/main/templates/document/writing.md) · [Parts](https://github.com/minu-ha/for-humanity/blob/main/templates/document/parts.md) · [Settings](https://github.com/minu-ha/for-humanity/blob/main/templates/document/settings.md) |
128
- | Reference | [API](https://github.com/minu-ha/for-humanity/blob/main/templates/document/api.md) |
129
- | Releases | [Official releases](https://github.com/minu-ha/for-humanity/blob/main/templates/document/releases.md) · [Changelog](https://github.com/minu-ha/for-humanity/blob/main/templates/document/changelog.md) |
130
- | Examples | [Fictional design example](https://github.com/minu-ha/for-humanity/blob/main/templates/document/blueprint.md) |
131
- | Development | [Architecture](https://github.com/minu-ha/for-humanity/blob/main/templates/document/architecture.md) · [Design](https://github.com/minu-ha/for-humanity/blob/main/templates/document/design.md) · [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md) · [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) |
99
+ | Task | Guide |
100
+ | --- | --- |
101
+ | Install and run | [Commands](https://github.com/minu-ha/for-humanity/blob/main/content/public/commands.md) |
102
+ | Write and organize pages | [Writing](https://github.com/minu-ha/for-humanity/blob/main/content/public/writing.md) · [Navigation](https://github.com/minu-ha/for-humanity/blob/main/content/public/navigation.md) |
103
+ | Add rich content | [Parts](https://github.com/minu-ha/for-humanity/blob/main/content/public/parts.md) · [Diagrams](https://github.com/minu-ha/for-humanity/blob/main/content/public/diagrams.md) · [Images](https://github.com/minu-ha/for-humanity/blob/main/content/public/images.md) |
104
+ | Configure the site | [Settings](https://github.com/minu-ha/for-humanity/blob/main/content/public/settings.md) · [Reference](https://github.com/minu-ha/for-humanity/blob/main/content/public/api.md) |
105
+ | Publish | [Deployment](https://github.com/minu-ha/for-humanity/blob/main/content/public/deployment.md) · [Crawling](https://github.com/minu-ha/for-humanity/blob/main/content/public/crawling.md) |
106
+ | Update | [Releases](https://github.com/minu-ha/for-humanity/blob/main/content/public/releases.md) · [Migration](https://github.com/minu-ha/for-humanity/blob/main/content/public/migration.md) |
132
107
 
133
108
  ## License
134
109
 
package/dist/cli.js CHANGED
@@ -5,11 +5,11 @@ import { defineCommand, runMain } from "citty";
5
5
  import { z } from "zod/mini";
6
6
  //#region src/entry/cli/_constant/cli.ts
7
7
  /**
8
- * 문서 폴더 생략 시 현재 폴더
8
+ * 프로젝트 폴더 생략 시 현재 폴더
9
9
  */
10
10
  var cli_docs_args = { dir: {
11
11
  type: "positional",
12
- description: "문서 폴더",
12
+ description: "프로젝트 폴더",
13
13
  default: "."
14
14
  } };
15
15
  //#endregion
@@ -50,7 +50,7 @@ var main = defineCommand({
50
50
  * 도움말 조회와 분리한 사이트 실행
51
51
  */
52
52
  run: async (context) => {
53
- await (await import("./run-site-command-cqCjpIlc.js")).runSiteCommand({
53
+ await (await import("./run-site-command-DPG5RNIN.js")).runSiteCommand({
54
54
  command: "dev",
55
55
  args: context.args
56
56
  });
@@ -59,14 +59,14 @@ var main = defineCommand({
59
59
  build: defineCommand({
60
60
  meta: {
61
61
  name: "build",
62
- description: "문서 폴더의 dist에 정적 사이트 생성"
62
+ description: "content/dist에 공개 문서 사이트 생성"
63
63
  },
64
64
  args: cli_docs_args,
65
65
  /**
66
66
  * 도움말 조회와 분리한 사이트 실행
67
67
  */
68
68
  run: async (context) => {
69
- await (await import("./run-site-command-cqCjpIlc.js")).runSiteCommand({
69
+ await (await import("./run-site-command-DPG5RNIN.js")).runSiteCommand({
70
70
  command: "build",
71
71
  args: context.args
72
72
  });
@@ -82,7 +82,7 @@ var main = defineCommand({
82
82
  * 도움말 조회와 분리한 사이트 실행
83
83
  */
84
84
  run: async (context) => {
85
- await (await import("./run-site-command-cqCjpIlc.js")).runSiteCommand({
85
+ await (await import("./run-site-command-DPG5RNIN.js")).runSiteCommand({
86
86
  command: "preview",
87
87
  args: context.args
88
88
  });
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- export { t as siteConfigSchema } from "./site-config-bllORGpm.js";
1
+ export { t as siteConfigSchema } from "./site-config-FmStUQ2W.js";
@@ -18,3 +18,11 @@ export declare const site_port_min = 1;
18
18
  * 포트 번호 상한. TCP
19
19
  */
20
20
  export declare const site_port_max = 65535;
21
+ /**
22
+ * 기본 문서 루트와 공개 범위 폴더
23
+ */
24
+ export declare const content_default: {
25
+ root: string;
26
+ public: string;
27
+ private: string;
28
+ };
@@ -106,6 +106,23 @@ var asset_media_extensions = /* @__PURE__ */ new Set([
106
106
  ".zip"
107
107
  ]);
108
108
  //#endregion
109
+ //#region src/util/file/assert-content-path.ts
110
+ /**
111
+ * 파일 경로와 symlink 대상의 공개 범위 검사
112
+ */
113
+ var assertContentPath = (params) => {
114
+ for (const path of [resolve(params.path), ...existsSync(params.path) ? [realpathSync(params.path)] : []]) {
115
+ const location = relative(params.scope.root, path);
116
+ const internal = relative(params.scope.privateRoot, path);
117
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location) || location.split(sep).some((part) => [
118
+ "dist",
119
+ ".git",
120
+ "node_modules"
121
+ ].includes(part))) throw new Error(`문서 범위 밖의 참조: ${params.path}`);
122
+ if (!params.scope.includePrivate && internal !== ".." && !internal.startsWith(`..${sep}`) && !isAbsolute(internal)) throw new Error(`공개 문서에서 private 자료를 참조할 수 없습니다: ${params.path}`);
123
+ }
124
+ };
125
+ //#endregion
109
126
  //#region src/util/file/to-asset-path.ts
110
127
  /**
111
128
  * 파일명에 내용 지문 추가. 같은 내용은 같은 URL, 변경한 내용은 새 캐시 키 사용
@@ -137,6 +154,14 @@ var rehypeAssets = (options) => (tree, file) => {
137
154
  return;
138
155
  }
139
156
  const target = resolve(dirname(file.path), path);
157
+ if (options.scope !== void 0) try {
158
+ assertContentPath({
159
+ scope: options.scope,
160
+ path: target
161
+ });
162
+ } catch (error) {
163
+ file.fail(error instanceof Error ? error.message : String(error), node);
164
+ }
140
165
  const location = relative(options.root, target);
141
166
  if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_asset}: 문서 폴더 밖 · ${url}`, node);
142
167
  if (!existsSync(target)) file.fail(`${copy_error_asset}: 파일이 없다 · ${url}`, node);
@@ -152,6 +177,69 @@ var rehypeAssets = (options) => (tree, file) => {
152
177
  });
153
178
  };
154
179
  //#endregion
180
+ //#region src/util/file/to-content-id.ts
181
+ /**
182
+ * 폴더 이름과 독립적인 공개 URL, 내부 문서의 private 접두사
183
+ */
184
+ var toContentId = (params) => {
185
+ if (relative(params.scope.root, params.path).toLowerCase() === "readme.md") return "";
186
+ for (const [root, prefix] of [[params.scope.publicRoot, ""], [params.scope.privateRoot, "private/"]]) {
187
+ const location = relative(root, params.path);
188
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) continue;
189
+ const id = location.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase();
190
+ if (prefix === "" && id.split("/")[0] === "private") throw new Error(`private URL은 내부 문서 전용입니다: ${params.path}`);
191
+ return `${prefix}${id}`;
192
+ }
193
+ throw new Error(`공개 또는 내부 문서 폴더에 없는 Markdown 참조: ${params.path}`);
194
+ };
195
+ //#endregion
196
+ //#region src/app/_function/create-processor/_rehype-content-links.ts
197
+ /**
198
+ * 원시 HTML의 내부 참조 차단과 Markdown 링크의 사이트 URL 변환
199
+ */
200
+ var rehypeContentLinks = (options) => (tree, file) => {
201
+ const scope = options.scope;
202
+ if (scope === void 0) return;
203
+ visit(tree, "element", (node) => {
204
+ for (const property of [
205
+ "href",
206
+ "src",
207
+ "poster",
208
+ "data",
209
+ "srcSet"
210
+ ]) {
211
+ const value = node.properties[property];
212
+ if (typeof value !== "string") continue;
213
+ const urls = property === "srcSet" ? value.split(",").map((entry) => entry.trim().split(/\s+/)[0]) : [value];
214
+ for (const url of urls) {
215
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(url)) continue;
216
+ const path = decodeURIComponent(url.split(/[?#]/)[0]);
217
+ try {
218
+ if (path.startsWith("/")) {
219
+ const pathname = new URL(path, "https://content.invalid").pathname.toLowerCase();
220
+ if (!scope.includePrivate && (pathname === "/private" || pathname.startsWith("/private/"))) throw new Error(`공개 문서에서 private URL을 참조할 수 없습니다: ${url}`);
221
+ continue;
222
+ }
223
+ const target = resolve(dirname(file.path), path);
224
+ assertContentPath({
225
+ scope,
226
+ path: target
227
+ });
228
+ if (property === "href" && /\.mdx?$/i.test(path)) {
229
+ const id = toContentId({
230
+ scope,
231
+ path: target
232
+ });
233
+ node.properties[property] = `${id === "" ? "/" : `/${id}/`}${url.slice(url.split(/[?#]/)[0].length)}`;
234
+ }
235
+ } catch (error) {
236
+ file.fail(error instanceof Error ? error.message : String(error), node);
237
+ }
238
+ }
239
+ }
240
+ });
241
+ };
242
+ //#endregion
155
243
  //#region src/app/_function/create-processor/_rehype-heading-ids.ts
156
244
  /**
157
245
  * 제목 id와 목차 수집. 같은 제목은 -1, -2 접미사
@@ -259,6 +347,14 @@ var remarkSnippets = (options) => (tree, file) => {
259
347
  if (match === null) return;
260
348
  const [, path = "", region, meta] = match;
261
349
  const target = resolve(dirname(file.path), path);
350
+ if (options.scope !== void 0) try {
351
+ assertContentPath({
352
+ scope: options.scope,
353
+ path: target
354
+ });
355
+ } catch (error) {
356
+ file.fail(error instanceof Error ? error.message : String(error), node);
357
+ }
262
358
  const location = relative(options.root, target);
263
359
  if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
264
360
  if (!existsSync(target)) file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
@@ -513,6 +609,24 @@ var remarkLinks = (options) => (tree, file) => {
513
609
  if (node.type !== "link" && node.type !== "definition") return;
514
610
  const match = /^(?![a-z]+:|\/|#)([^#?]+)(\.mdx?)([?#].*|)$/i.exec(node.url);
515
611
  if (match) {
612
+ if (options.scope !== void 0) {
613
+ const target = resolve(dirname(file.path), decodeURIComponent(`${match[1]}${match[2]}`));
614
+ try {
615
+ assertContentPath({
616
+ scope: options.scope,
617
+ path: target
618
+ });
619
+ const id = toContentId({
620
+ scope: options.scope,
621
+ path: target
622
+ });
623
+ if (!existsSync(target)) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
624
+ node.url = `${id === "" ? "/" : `/${id}/`}${match[3]}`;
625
+ } catch (error) {
626
+ file.fail(error instanceof Error ? error.message : String(error), node);
627
+ }
628
+ return;
629
+ }
516
630
  if (!existsSync(resolve(dirname(file.path), `${match[1]}${match[2]}`))) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
517
631
  const target = posix.join(here, `${match[1]}${match[2]}`).toLowerCase();
518
632
  node.url = target === "readme.md" ? `/${match[3]}` : `/${target.slice(0, -match[2].length)}/${match[3]}`;
@@ -630,18 +744,25 @@ var remarkUnknownDirectives = () => (tree, file) => {
630
744
  */
631
745
  var createProcessor = (options) => {
632
746
  const files = options.files ?? /* @__PURE__ */ new Map();
633
- return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
747
+ return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, {
748
+ root: options.root,
749
+ scope: options.scope
750
+ }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
634
751
  assets: options.mermaid,
635
752
  files
636
- }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
753
+ }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, {
754
+ root: options.root,
755
+ scope: options.scope
756
+ }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
637
757
  theme: createCssVariablesTheme({
638
758
  name: "for-humanity",
639
759
  variablePrefix: "--app-code-"
640
760
  }),
641
761
  transformers: shikiTransformers
642
- }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, {
762
+ }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeContentLinks, { scope: options.scope }).use(rehypeAssets, {
643
763
  root: options.root,
644
- files
764
+ files,
765
+ scope: options.scope
645
766
  }).use(rehypeStringify, { allowDangerousHtml: true });
646
767
  };
647
768
  //#endregion
@@ -660,6 +781,32 @@ var loadMermaidAssets = (kitRoot) => {
660
781
  };
661
782
  };
662
783
  //#endregion
784
+ //#region src/app/_function/read-content-scope.ts
785
+ /**
786
+ * 공개 폴더 필수, 내부 폴더 선택, 범위 폴더의 symlink 별칭 금지
787
+ */
788
+ var readContentScope = (params) => {
789
+ if (!existsSync(params.root) || !statSync(params.root).isDirectory()) throw new Error(`content 문서 루트가 없습니다: ${params.root}`);
790
+ const root = realpathSync(params.root);
791
+ const publicRoot = join(root, params.content.public);
792
+ const privateRoot = join(root, params.content.private);
793
+ for (const path of [publicRoot, privateRoot]) {
794
+ if (!existsSync(path)) {
795
+ if (path === privateRoot) continue;
796
+ throw new Error(`공개 문서 폴더가 없습니다: ${path}. content/public 구조 또는 content 설정을 확인하세요`);
797
+ }
798
+ const actual = realpathSync(path);
799
+ const location = relative(root, actual);
800
+ if (!statSync(path).isDirectory() || actual !== path || location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) throw new Error(`문서 폴더는 content 내부의 실제 폴더여야 합니다: ${path}`);
801
+ }
802
+ return {
803
+ root,
804
+ publicRoot,
805
+ privateRoot,
806
+ includePrivate: params.includePrivate
807
+ };
808
+ };
809
+ //#endregion
663
810
  //#region src/constant/doc.ts
664
811
  /**
665
812
  * 기존 frontmatter type의 호환 값. 모두 같은 Markdown 렌더링
@@ -708,15 +855,23 @@ var docDataSchema = z.object({
708
855
  //#endregion
709
856
  //#region src/app/_function/read-docs.ts
710
857
  /**
711
- * Markdown 수집과 렌더링. 루트 README는 홈, AGENTS, CLAUDE는 작성 지침으로 분리
858
+ * 공개 범위의 Markdown 수집과 렌더링, 범위 미지정 시 독립 Markdown 처리
712
859
  *
713
- * - 문서 목록에서 루트 안내 파일, node_modules, dist 제외
860
+ * - 사이트 실행에서 공개·내부 문서만 수집, content 루트 안내 파일 제외
714
861
  * - 머리말, 문서 식별자 중복, 플러그인 오류 시 실패
715
862
  */
716
863
  var readDocs = async (options) => {
717
- const paths = (await readdir(options.root, { recursive: true })).filter((path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`));
864
+ const roots = options.scope === void 0 ? [options.root] : [options.scope.publicRoot, ...options.scope.includePrivate && existsSync(options.scope.privateRoot) ? [options.scope.privateRoot] : []];
865
+ const paths = (await Promise.all(roots.map(async (root) => (await readdir(root, { recursive: true })).map((path) => relative(options.root, join(root, path)))))).flat().filter((path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`));
718
866
  const docs = await Promise.all(paths.map(async (path) => {
719
- const id = path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase();
867
+ if (options.scope !== void 0) assertContentPath({
868
+ scope: options.scope,
869
+ path: join(options.root, path)
870
+ });
871
+ const id = options.scope === void 0 ? path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase() : toContentId({
872
+ scope: options.scope,
873
+ path: join(options.root, path)
874
+ });
720
875
  const segments = id.split("/");
721
876
  if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") || /[#?%*:\\]/.test(id) || /[\p{Cc}]/u.test(id) || options.reserved?.includes(segments[0]) === true || segments[0] === "_fh" || segments[0] === "/favicon.svg".slice(1)) throw new Error(`${copy_error_doc_id}: ${path}`);
722
877
  const source = await readFile(join(options.root, path), "utf8");
@@ -728,9 +883,14 @@ var readDocs = async (options) => {
728
883
  const frontmatter = tree.children[0];
729
884
  const parsed = docDataSchema.safeParse(frontmatter?.type === "yaml" ? parse(frontmatter.value) : {});
730
885
  if (!parsed.success) throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
886
+ const parentPath = parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? resolve(dirname(file.path), parsed.data.parent) : void 0;
887
+ const parent = parsed.data.parent !== void 0 && parentPath !== void 0 ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent;
731
888
  const data = {
732
889
  ...parsed.data,
733
- parent: parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent
890
+ parent: options.scope !== void 0 && parentPath !== void 0 ? toContentId({
891
+ scope: options.scope,
892
+ path: parentPath
893
+ }) : parent
734
894
  };
735
895
  const fh = {
736
896
  frontmatter: data,
@@ -783,6 +943,10 @@ var readHome = async (options) => {
783
943
  if (paths.length > 1) throw new Error(`${copy_error_home_clash}: ${paths.join(", ")}`);
784
944
  if (paths.length === 0) return;
785
945
  const path = join(options.root, paths[0]);
946
+ if (options.scope !== void 0) assertContentPath({
947
+ scope: options.scope,
948
+ path
949
+ });
786
950
  const fh = {
787
951
  headings: [],
788
952
  sections: []
@@ -2285,18 +2449,26 @@ var loadSite = async (params) => {
2285
2449
  ...brand.files
2286
2450
  ]);
2287
2451
  if (params.stylePaths === void 0) files.set(stylePaths[0], join(params.kitRoot, "dist/cli.css"));
2452
+ const scope = readContentScope({
2453
+ root: params.root,
2454
+ content: params.site.content,
2455
+ includePrivate: params.includePrivate === true
2456
+ });
2288
2457
  const processor = createProcessor({
2289
2458
  site: params.site,
2290
- root: params.root,
2459
+ root: scope.root,
2291
2460
  files,
2292
- mermaid: loadMermaidAssets(params.kitRoot)
2461
+ mermaid: loadMermaidAssets(params.kitRoot),
2462
+ scope
2293
2463
  });
2294
2464
  const [docs, home] = await Promise.all([readDocs({
2295
- root: params.root,
2465
+ root: scope.root,
2466
+ scope,
2296
2467
  processor,
2297
2468
  reserved: params.site.url === void 0 ? void 0 : crawling_reserved_roots
2298
2469
  }), readHome({
2299
- root: params.root,
2470
+ root: scope.root,
2471
+ scope,
2300
2472
  processor
2301
2473
  })]);
2302
2474
  const store = {
@@ -1,18 +1,18 @@
1
1
  import { d as copy_error_missing_build, n as copy_error_config } from "./copy-C1uzvEnr.js";
2
2
  import { t as logger } from "./logger-Dv1i9xSV.js";
3
- import { n as site_config_absent, t as siteConfigSchema } from "./site-config-bllORGpm.js";
4
- import { n as font_cache_control, r as asset_font_dir, t as loadSite } from "./load-site-B-u5-jv0.js";
3
+ import { n as site_config_absent, t as siteConfigSchema } from "./site-config-FmStUQ2W.js";
4
+ import { n as font_cache_control, r as asset_font_dir, t as loadSite } from "./load-site-CBJGMNfH.js";
5
5
  import { n as config_file_names, t as toErrorMessage } from "./to-error-message-pF6_7wGK.js";
6
6
  import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
7
7
  import { z } from "zod";
8
- import { existsSync } from "node:fs";
9
- import { dirname, join, relative, resolve } from "node:path";
8
+ import { existsSync, realpathSync } from "node:fs";
9
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
10
  import { toSSG } from "hono/ssg";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { createJiti } from "jiti";
13
13
  //#region src/app/_function/read-site-config.ts
14
14
  /**
15
- * 문서 루트의 설정 조회, TypeScript 실행과 스키마 검증
15
+ * 프로젝트 루트의 설정 조회, TypeScript 실행과 스키마 검증
16
16
  *
17
17
  * - 중복 파일 거부, 실제 적용되는 설정의 모호성 방지
18
18
  * - Node의 기본 TypeScript 지원 여부와 무관하게 같은 설정 실행
@@ -31,6 +31,29 @@ var readSiteConfig = async (root) => {
31
31
  return parsed.data.default;
32
32
  };
33
33
  //#endregion
34
+ //#region src/app/_function/read-project.ts
35
+ /**
36
+ * 프로젝트 설정과 문서 루트 해소, 외부·시스템 폴더 symlink 거부
37
+ */
38
+ var readProject = async (directory) => {
39
+ const projectRoot = realpathSync(resolve(directory));
40
+ const site = await readSiteConfig(projectRoot);
41
+ const root = resolve(projectRoot, site.content.root);
42
+ if (existsSync(root)) {
43
+ const location = relative(projectRoot, realpathSync(root));
44
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) throw new Error(`content 루트는 프로젝트 내부여야 합니다: ${root}`);
45
+ if (location.split(sep).some((part) => [
46
+ "dist",
47
+ ".git",
48
+ "node_modules"
49
+ ].includes(part.toLowerCase()))) throw new Error(`content 루트는 dist, .git, node_modules 내부일 수 없습니다: ${root}`);
50
+ }
51
+ return {
52
+ root,
53
+ site
54
+ };
55
+ };
56
+ //#endregion
34
57
  //#region src/entry/preview/to-preview-config.ts
35
58
  /**
36
59
  * 빌드된 사이트 전용 Vite 설정. 소비 프로젝트의 설정과 환경 파일 자동 로드 제외
@@ -75,31 +98,31 @@ var toPreviewConfig = (params) => {
75
98
  */
76
99
  var runSiteCommand = async (params) => {
77
100
  try {
78
- if (params.args._.length > 1) throw new Error("문서 폴더는 하나만 지정하세요");
101
+ if (params.args._.length > 1) throw new Error("프로젝트 폴더는 하나만 지정하세요");
79
102
  const kitRoot = fileURLToPath(new URL("../", import.meta.url));
80
- const docsRoot = resolve(params.args.dir);
81
- const outDir = join(docsRoot, "dist");
82
- const siteConfig = await readSiteConfig(docsRoot);
103
+ const project = await readProject(params.args.dir);
104
+ const outDir = join(project.root, "dist");
83
105
  if (params.command === "preview") {
84
106
  (await (await import("vite")).preview(toPreviewConfig({
85
- root: docsRoot,
86
- port: siteConfig.port
107
+ root: project.root,
108
+ port: project.site.port
87
109
  }))).printUrls();
88
110
  return;
89
111
  }
90
112
  if (params.command === "dev") {
91
113
  await (await import("./start-dev-server-x7Ja5uwv.js")).startDevServer({
92
- root: docsRoot,
114
+ root: project.root,
93
115
  kitRoot,
94
- site: siteConfig,
95
- source: false
116
+ site: project.site,
117
+ source: false,
118
+ includePrivate: true
96
119
  });
97
120
  return;
98
121
  }
99
122
  const snapshot = await loadSite({
100
- root: docsRoot,
123
+ root: project.root,
101
124
  kitRoot,
102
- site: siteConfig
125
+ site: project.site
103
126
  });
104
127
  await rm(outDir, {
105
128
  recursive: true,
@@ -18,6 +18,14 @@ var site_port_default = 4321;
18
18
  * 포트 번호 상한. TCP
19
19
  */
20
20
  var site_port_max = 65535;
21
+ /**
22
+ * 기본 문서 루트와 공개 범위 폴더
23
+ */
24
+ var content_default = {
25
+ root: "content",
26
+ public: "public",
27
+ private: "private"
28
+ };
21
29
  //#endregion
22
30
  //#region src/constant/status.ts
23
31
  /**
@@ -39,11 +47,47 @@ var status_default_phrases = [{
39
47
  kind: status_kind.unverified
40
48
  }];
41
49
  //#endregion
50
+ //#region src/type/content-config.ts
51
+ /**
52
+ * 프로젝트 내부의 상대 경로, 출력 폴더와 시스템 폴더 제외
53
+ */
54
+ var contentPathSchema = z.string().trim().min(1).refine((value) => !/^[\\/]|[:?#%*\p{Cc}]/u.test(value) && !value.split(/[\\/]/).some((part) => [
55
+ "..",
56
+ "dist",
57
+ "node_modules",
58
+ ".git"
59
+ ].includes(part.toLowerCase())), "프로젝트 내부 상대 경로만 사용하세요. dist, node_modules, .git 경로는 사용할 수 없습니다").transform((value) => value.replaceAll("\\", "/").split("/").filter((part) => part !== "" && part !== ".").join("/"));
60
+ /**
61
+ * 공개와 내부 문서 폴더의 상대 경로 계약
62
+ */
63
+ var contentConfigSchema = z.object({
64
+ /**
65
+ * 프로젝트 루트 기준 문서 폴더, 점 하나는 프로젝트 루트
66
+ */
67
+ root: contentPathSchema.transform((value) => value === "" ? "." : value).default(content_default.root),
68
+ /**
69
+ * 문서 루트 아래 공개 문서 폴더
70
+ */
71
+ public: contentPathSchema.refine((value) => value !== "", "공개 문서는 별도 하위 폴더가 필요합니다").default(content_default.public),
72
+ /**
73
+ * 문서 루트 아래 로컬 전용 문서 폴더
74
+ */
75
+ private: contentPathSchema.refine((value) => value !== "", "내부 문서는 별도 하위 폴더가 필요합니다").default(content_default.private)
76
+ }).refine((value) => {
77
+ const published = value.public.toLowerCase();
78
+ const internal = value.private.toLowerCase();
79
+ return published !== internal && !published.startsWith(`${internal}/`) && !internal.startsWith(`${published}/`);
80
+ }, "공개 문서와 내부 문서 폴더는 같거나 중첩될 수 없습니다");
81
+ //#endregion
42
82
  //#region src/type/site-config.ts
43
83
  /**
44
84
  * 사이트 설정의 입력 검증과 기본값 적용
45
85
  */
46
86
  var siteConfigSchema = z.object({
87
+ /**
88
+ * 프로젝트 기준 문서 루트와 공개 범위
89
+ */
90
+ content: contentConfigSchema.default(content_default),
47
91
  /**
48
92
  * 사이드바 이름과 탭 제목
49
93
  */
package/dist/site.js CHANGED
@@ -1 +1 @@
1
- export { t as loadSite } from "./load-site-B-u5-jv0.js";
1
+ export { t as loadSite } from "./load-site-CBJGMNfH.js";
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 공개와 내부 문서 폴더의 상대 경로 계약
4
+ */
5
+ export declare const contentConfigSchema: z.ZodObject<{
6
+ root: z.ZodDefault<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>>;
7
+ public: z.ZodDefault<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
8
+ private: z.ZodDefault<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
9
+ }, z.core.$strip>;
@@ -3,6 +3,11 @@ import { z } from "zod";
3
3
  * 사이트 설정의 입력 검증과 기본값 적용
4
4
  */
5
5
  export declare const siteConfigSchema: z.ZodObject<{
6
+ content: z.ZodDefault<z.ZodObject<{
7
+ root: z.ZodDefault<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>>;
8
+ public: z.ZodDefault<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
9
+ private: z.ZodDefault<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
10
+ }, z.core.$strip>>;
6
11
  title: z.ZodDefault<z.ZodString>;
7
12
  description: z.ZodOptional<z.ZodString>;
8
13
  url: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodURL, z.ZodTransform<URL, string>>, z.ZodTransform<string, URL>>>;
@@ -26,6 +31,10 @@ export type ResolvedSiteConfig = z.infer<typeof siteConfigSchema>;
26
31
  * 작성자가 지정하는 사이트 설정, 기본값 적용 전 입력
27
32
  */
28
33
  export interface SiteConfig {
34
+ /**
35
+ * 기본 content/public, content/private 구조의 경로 변경
36
+ */
37
+ content?: z.input<typeof siteConfigSchema>["content"];
29
38
  /**
30
39
  * 사이드바 이름과 탭 제목, 생략 시 for humanity
31
40
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "for-humanity",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Documents written for humans to read: a docs kit with a sidebar, parts, sections and Mermaid diagrams, built on Hono JSX",
5
5
  "keywords": [
6
6
  "docs",
@@ -27,6 +27,7 @@
27
27
  "files": [
28
28
  "dist",
29
29
  "src/asset",
30
+ "template",
30
31
  "CHANGELOG.md"
31
32
  ],
32
33
  "engines": {
@@ -37,9 +38,9 @@
37
38
  "sync:readme": "tsx util/sync-readme.ts",
38
39
  "sync:docs": "pnpm sync:readme && tsx util/sync-changelog.ts",
39
40
  "dev": "pnpm sync:docs && pnpm build:kit && tsx src/entry/dev/dev.ts",
40
- "build": "pnpm sync:docs && pnpm build:kit && node dist/cli.js build templates/document",
41
+ "build": "pnpm sync:docs && pnpm build:kit && node dist/cli.js build .",
41
42
  "build:pages": "pnpm build && tsx util/prepare-pages.ts",
42
- "preview": "node dist/cli.js preview templates/document",
43
+ "preview": "node dist/cli.js preview .",
43
44
  "test": "pnpm build:kit && vitest run",
44
45
  "test:watch": "pnpm build:kit && vitest",
45
46
  "test:browser": "pnpm build:kit && playwright test",
@@ -0,0 +1,5 @@
1
+ # Project notes
2
+
3
+ 이 프로젝트의 소개와 문서 안내.
4
+
5
+ [시작하기](public/getting-started.md)
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: Getting started
3
+ label: 문서 작성과 공개 범위
4
+ group: Guide
5
+ order: 10
6
+ ---
7
+
8
+ # Getting started
9
+
10
+ `content/public`은 공개 문서, `content/private`는 로컬 전용 문서입니다.
11
+ 공용 이미지와 첨부는 `content/asset`, 내부 자료는 `content/private/asset`에 보관합니다.
12
+
13
+ 프로젝트 루트에서 실행합니다.
14
+
15
+ ```sh
16
+ pnpm exec for-humanity dev
17
+ pnpm exec for-humanity build
18
+ pnpm exec for-humanity preview
19
+ ```
20
+
21
+ `dev`는 전체 문서, `build`와 `preview`는 공개 결과를 제공합니다.
22
+ 배포할 폴더는 `content/dist`입니다. `content` 전체를 업로드하지 않습니다.
23
+ `private`는 사이트 배포 제외이며 Git 저장소 접근 제어가 아닙니다.
24
+
25
+ 설정은 프로젝트 루트의 `for-humanity.config.ts`, 홈은 `content/README.md`입니다.
26
+ 공개 문서의 URL에는 `public`이 붙지 않습니다. 이 문서는 `/getting-started/`에서 확인할 수 있습니다.
@@ -0,0 +1,8 @@
1
+ import type {SiteConfig} from "for-humanity";
2
+
3
+ export default {
4
+ title: "Project notes",
5
+
6
+ // 프로젝트 기준 문서 루트, 문서 루트 기준 공개와 내부 폴더
7
+ content: {root: "content", public: "public", private: "private"},
8
+ } satisfies SiteConfig;