markupress 0.0.1 → 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +365 -2
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +48 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/content.d.ts +59 -0
  8. package/dist/content.d.ts.map +1 -0
  9. package/dist/content.js +382 -0
  10. package/dist/content.js.map +1 -0
  11. package/dist/controllers/code.d.ts +7 -0
  12. package/dist/controllers/code.d.ts.map +1 -0
  13. package/dist/controllers/code.js +31 -0
  14. package/dist/controllers/code.js.map +1 -0
  15. package/dist/controllers/outline.d.ts +8 -0
  16. package/dist/controllers/outline.d.ts.map +1 -0
  17. package/dist/controllers/outline.js +45 -0
  18. package/dist/controllers/outline.js.map +1 -0
  19. package/dist/controllers/shell.d.ts +9 -0
  20. package/dist/controllers/shell.d.ts.map +1 -0
  21. package/dist/controllers/shell.js +41 -0
  22. package/dist/controllers/shell.js.map +1 -0
  23. package/dist/index.d.ts +10 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +6 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/markdown.d.ts +24 -0
  28. package/dist/markdown.d.ts.map +1 -0
  29. package/dist/markdown.js +54 -0
  30. package/dist/markdown.js.map +1 -0
  31. package/dist/navigation.d.ts +51 -0
  32. package/dist/navigation.d.ts.map +1 -0
  33. package/dist/navigation.js +71 -0
  34. package/dist/navigation.js.map +1 -0
  35. package/dist/site.d.ts +75 -0
  36. package/dist/site.d.ts.map +1 -0
  37. package/dist/site.js +169 -0
  38. package/dist/site.js.map +1 -0
  39. package/dist/styles.d.ts +6 -0
  40. package/dist/styles.d.ts.map +1 -0
  41. package/dist/styles.js +49 -0
  42. package/dist/styles.js.map +1 -0
  43. package/dist/theme.d.ts +3 -0
  44. package/dist/theme.d.ts.map +1 -0
  45. package/dist/theme.js +5 -0
  46. package/dist/theme.js.map +1 -0
  47. package/dist/versions.d.ts +19 -0
  48. package/dist/versions.d.ts.map +1 -0
  49. package/dist/versions.js +130 -0
  50. package/dist/versions.js.map +1 -0
  51. package/package.json +70 -6
  52. package/styles/base.css +4 -0
  53. package/styles/prose.css +14 -0
  54. package/theme/outline.html +11 -0
  55. package/theme/shell.html +66 -0
  56. package/theme/theme.css +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 threadlabs-studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,368 @@
1
1
  # Markupress
2
2
 
3
- A forthcoming documentation publishing tool built on HTMLKit, from Threadlabs Studio.
3
+ Build a versioned Markdown documentation site with native HTML Next components, HTMLKit routing and a Looma theme. Markupress provides `build`, `dev`, `preview` and `snapshot` commands. Production output is a portable static directory; controllers enhance the rendered HTML in the browser.
4
4
 
5
- Version 0.0.1 reserves the package name. It contains no implementation or public API.
5
+ The npm package `markupress@0.0.1` only reserves the name. The implementation below is being prepared for the first product release.
6
+
7
+ ## Try the source build
8
+
9
+ Use Node 22.22.2 or later in Node 22, or Node 24.15 or later in Node 24, and pnpm through Corepack:
10
+
11
+ ```sh
12
+ corepack pnpm install --frozen-lockfile
13
+ corepack pnpm build
14
+ node dist/cli.js dev --root examples/site
15
+ node dist/cli.js build --root examples/site
16
+ node dist/cli.js preview --root examples/site
17
+ ```
18
+
19
+ The example serves at `/manual/`. Open a version-specific link directly to verify that a reload preserves the selected version. Markupress is a thin layer over HTMLKit: it translates Markdown and manages versions, and HTMLKit routes, orders, renders, watches and builds the pages. The dev server reloads open pages after any edit, addition or deletion. `build` writes a static site, which is the only production output for now.
20
+
21
+ ## Author a site
22
+
23
+ `@nextwebwg/html-next` and `@threadlabs/looma` are peer dependencies, so a site shares one copy of each with its own components. npm installs them with Markupress. Pages link Looma's components by package name, so a pnpm site adds both as its own dependencies:
24
+
25
+ ```sh
26
+ pnpm add markupress @nextwebwg/html-next @threadlabs/looma
27
+ ```
28
+
29
+ Create `docs/01.index.md` and optionally `markupress.config.json`:
30
+
31
+ ```json
32
+ {
33
+ "title": "Project documentation",
34
+ "contentDir": "docs",
35
+ "base": "/manual/",
36
+ "outDir": "site"
37
+ }
38
+ ```
39
+
40
+ Every version needs an `index.md` at its root; a numeric prefix such as `01.index.md` is allowed. `base` starts and ends with `/`. `layout` names a layout component that replaces the Markupress shell (see [Layouts](#layouts)). Configuration is JSON. The programmatic `buildSite`, `devSite` and `previewSite` APIs accept the same options plus `root`, `host` and `port`; `siteOptions` returns the HTMLKit options they use.
41
+
42
+ The `markupress` command and these APIs also load the site's `htmlkit.config.ts` (or `.js`) with HTMLKit's `loadConfig`, and add `markupress()` after its plugins, so HTMLKit plugins and options compose with Markupress:
43
+
44
+ ```ts
45
+ // htmlkit.config.ts
46
+ import { defineConfig } from '@nextwebwg/htmlkit';
47
+ import { sitemap } from './plugins/sitemap.js';
48
+
49
+ export default defineConfig({ plugins: [sitemap()], redirects: { '/start/': '/guide/install/' } });
50
+ ```
51
+
52
+ `base` and `outDir` from `markupress.config.json` or the command line win over the HTMLKit config's; without either, they are `/` and `site`. `markupress()` includes the `markdown()` plugin, so the HTMLKit config lists neither.
53
+
54
+ The same pieces work as HTMLKit plugins. `markupress(options)` adds versioned documentation with this theme. `markdown()` alone makes any HTMLKit site's `.md` files into pages, with no versions or theme:
55
+
56
+ ```ts
57
+ import { defineConfig } from '@nextwebwg/htmlkit';
58
+ import { markdown } from 'markupress';
59
+
60
+ export default defineConfig({ plugins: [markdown()] });
61
+ ```
62
+
63
+ ```markdown
64
+ ---
65
+ title: Installation
66
+ description: Install the project and run your first example.
67
+ sidebarLabel: Install
68
+ id: install
69
+ aliases: [/getting-started/]
70
+ ---
71
+ # Installation
72
+
73
+ [Next steps](03.usage.md#usage)
74
+
75
+ <link rel="component" href="../../components/counter.html">
76
+
77
+ <demo-counter></demo-counter>
78
+ ```
79
+
80
+ `01.guide/02.install.md` becomes `/v/current/guide/install/`. Numeric prefixes order HTMLKit navigation and disappear from URLs. The unversioned `/guide/install/` alias selects the configured default version. Changing `02.` to `10.` changes ordering without changing the path. Duplicate paths or document IDs fail with both source filenames.
81
+
82
+ Markdown text and fenced examples are literal, including braces and component syntax. Raw HTML outside fences can declare or use native components. Resource links remain relative to the original Markdown file. A component declared in Markdown is hoisted alongside the generated page carrier; its expressions retain HTML Next semantics. Explicit `<title>`, `<meta>` and non-component `<link>` elements also become page metadata.
83
+
84
+ ### Front matter
85
+
86
+ Front matter is a YAML mapping. Each field becomes HTMLKit page metadata or page data, and an unknown or mistyped field fails the build with the Markdown file's path, the field and the expected type.
87
+
88
+ | Field | Type | Effect |
89
+ | --- | --- | --- |
90
+ | `title` | string | The document `<title>`, the default navigation label, and `page.title`. Defaults to the first heading, then the file name. |
91
+ | `description` | string | `<meta name="description">`. |
92
+ | `sidebarLabel` | string | The navigation, breadcrumb and pager label (HTMLKit `hk:label`). |
93
+ | `sidebarHidden` | boolean | `true` leaves the page out of navigation and keeps its route (HTMLKit `hk:navigation`). |
94
+ | `sidebarPosition` | whole number | The page's place among its folder's pages, in place of its file's number prefix (HTMLKit `hk:order`). On a folder's index page it places the whole folder, and on the version home it is an error. |
95
+ | `id` | string | The document identity across versions, independent of the file name and route. |
96
+ | `aliases` | list of route paths | Further URLs serving the page, relative to its version (HTMLKit `hk:alias`). |
97
+ | `layout` | string or `false` | An HTMLKit layout, `app/layouts/<name>.html`, in place of the site's layout for this page (HTMLKit `hk:layout`). `false` renders the page with no layout. |
98
+ | `toc` | `false` or `[min, max]` | The heading levels in the page's outline, `[2, 3]` by default. `false` gives an empty outline. |
99
+ | `head` | mapping | `meta` (name to content), `og` (Open Graph property without `og:`, to content) and `canonical` (a URL), rendered into the document head. |
100
+ | `editLink` | URL or `false` | The page's "Edit this page" URL, as `page.editLink` for the layout. `false` asks the layout to show none. |
101
+
102
+ ```yaml
103
+ ---
104
+ title: Installation
105
+ toc: [2, 4]
106
+ editLink: https://github.com/org/repo/edit/main/docs/01.guide/02.install.md
107
+ head:
108
+ meta: { keywords: "install, setup" }
109
+ og: { image: https://example.com/social.png, type: article }
110
+ canonical: https://example.com/guide/install/
111
+ ---
112
+ ```
113
+
114
+ Number prefixes order pages, and `sidebarPosition` takes the place of a file's prefix: `sidebarPosition: 2` on `install.md` orders it as `02.install.md` would. Pages with the same number sort by name.
115
+
116
+ Heading IDs are deterministic (`installation`, then `installation-1` for a duplicate). `## Setup {#install}` sets the ID to `install` and removes the marker from the heading; a duplicate still gets a suffix. A custom ID wins: a generated ID skips every custom ID and every ID already used, taking the next free suffix instead. Relative `.md` links resolve to the corresponding version-specific document, retaining fragments and queries; missing documents fail at build time. Relative assets are copied, and image paths starting with `/` resolve from that edition's public directory under the deployment base. Remote links remain unchanged. `sidebarHidden: true` hides a document from navigation while keeping its route available. Aliases do not add duplicate sidebar entries.
117
+
118
+ Containers and GitHub alerts render [Looma's shared blocks](https://github.com/threadlabs-studio/looma/blob/main/apps/docs/docs/shared-blocks.md), so a block looks the same in Markupress, Looma's components and LoomaEditor. The container name is matched without regard to case, and the rest of the opening line is its title:
119
+
120
+ ````markdown
121
+ ::: warning Before you upgrade
122
+ Back up your data first.
123
+ :::
124
+
125
+ > [!TIP]
126
+ > Alerts take the tones NOTE, TIP, IMPORTANT, WARNING and CAUTION.
127
+
128
+ ::: details Show the output {open}
129
+ Starts expanded.
130
+ :::
131
+
132
+ ::: code-group
133
+ ```sh title="pnpm"
134
+ pnpm add markupress
135
+ ```
136
+
137
+ ```sh title="npm"
138
+ npm install markupress
139
+ ```
140
+ :::
141
+
142
+ ::: tabs
143
+ == Examples
144
+ Usage examples.
145
+
146
+ == API
147
+ The reference.
148
+ :::
149
+ ````
150
+
151
+ | Markdown | Renders |
152
+ | --- | --- |
153
+ | `::: note`, `tip`, `info`, `important`, `warning`, `danger` or `caution`, with an optional title | `<ui-callout tone="…" heading="Title">`. `caution` is `danger`, and `note` is the muted aside. |
154
+ | `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` alone on a quote's first line | An untitled `<ui-callout>`: NOTE is `note` and CAUTION is `danger`. |
155
+ | `::: details Title`, with `{open}` to start expanded | Native `<details><summary>Title</summary>`, which opens without JavaScript. The title defaults to "Details". |
156
+ | `::: tabs` with `== Label` panels | `<ui-tabs group="tabs">`, one `<section aria-label="Label">` per panel. |
157
+ | `::: code-group` with a fence per panel | `<ui-tabs group="code-group">`, each panel labelled by its fence's `title`. `== Label` panels also work. |
158
+
159
+ Tabs in one group keep one selection across the page, matched by label, and remember it for the next visit: choosing "npm" in one code group switches every code group. `::: tabs name` or `::: code-group name` sets the group. Without JavaScript every panel shows. Any other container name stays text. To nest containers, give the outer one more colons (`::::` around `:::`). Titles and labels are literal text.
160
+
161
+ A page links the Looma components its blocks use by package name, as the theme links `ui-button`, so `@threadlabs/looma` resolves from the site. The Markupress theme spaces the blocks and styles the Markdown inside them.
162
+
163
+ Fenced code is highlighted at build time with [Shiki](https://shiki.style), so pages load no highlighting JavaScript. Any language Shiki bundles, such as `html`, `css`, `less`, `js`, `ts`, `tsx`, `jsx`, `json`, `bash`, `vue`, `svelte`, `md` or `yaml`, is loaded the first time a fence uses it. Unknown languages stay plain text. A `title` in the fence's info string adds a caption:
164
+
165
+ ````markdown
166
+ ```ts title="app/pages/index.ts"
167
+ export const count = 1;
168
+ ```
169
+ ````
170
+
171
+ Each code block renders as `<figure class="code">`, with the title as its `<figcaption>`.
172
+
173
+ The fence's info string also highlights and numbers lines, at build time with no client JavaScript:
174
+
175
+ ````markdown
176
+ ```ts {1,3-4} :line-numbers
177
+ const a = 1;
178
+ const b = 2; // [!code highlight]
179
+ const c = 3;
180
+ const d = 4;
181
+ ```
182
+ ````
183
+
184
+ - `{1,3-4}` highlights lines 1, 3 and 4. VitePress's spelling, with no space (```` ```ts{1} ````), also works.
185
+ - A `// [!code highlight]` comment at the end of a line highlights that line, and the comment is removed. `#`, `--`, `/* */` and `<!-- -->` comments work too. `[!code highlight:3]` covers three lines, and a marker alone on its line applies to the lines after it.
186
+ - `:line-numbers` numbers the lines. The `lineNumbers: true` option, in `markupress.config.json`, `markupress()` or `markdown()`, numbers every block, and `:no-line-numbers` opts a block out.
187
+ - These features apply to languages Shiki highlights and to plain text (`text`, `txt`, `plain` or `plaintext`). A fence with an unknown language or no language stays escaped text.
188
+
189
+ Every code block has Copy and Wrap buttons. Copy puts the code on the clipboard, without line numbers or highlight comments. Wrap toggles line wrapping. They are native buttons, reached with Tab, and a page with code loads a small controller (`markupress/controllers/code.js`) that handles them through one delegated listener. The buttons stay hidden until that controller runs, so without JavaScript a code block is only its code. The theme shows them while the block is hovered or holds focus, and always on touch screens.
190
+
191
+ Highlighting uses the `github-light-default` and `github-dark-default` themes. Each token's color is `light-dark(light, dark)`, so it follows the page's `color-scheme`. The Markupress theme sets the color scheme from its dark-theme button or the reader's system preference through Looma's `[data-theme]` themes, so code switches with the rest of the page. On another HTMLKit site, set `color-scheme: light dark` to follow the system, or set `color-scheme` on `[data-theme]` as Looma does. Each token also carries `--shiki-light` and `--shiki-dark`, so a site can switch with any selector instead, for example `.dark .shiki span { color: var(--shiki-dark) !important; }`. Browsers without `light-dark()` show unhighlighted text. The site's stylesheet owns the code block's background; the Markupress theme uses Looma's sunken surface (`--ui-surface-sunken`). There, comments measure 3.98:1 in the light theme and 4.42:1 in the dark theme, and light-theme function names 4.42:1, below the 4.5:1 the other token colors reach.
192
+
193
+ ## TypeScript controllers
194
+
195
+ Maintain controllers in strict ESM TypeScript, with `.js` references in HTML:
196
+
197
+ ```html
198
+ <template component="demo-counter" controller="./counter.js">
199
+ <defs><state name="count" type="number" value="0"></state></defs>
200
+ <div><button>Increment</button><output>{$count}</output></div>
201
+ </template>
202
+ ```
203
+
204
+ ```ts
205
+ // counter.ts
206
+ import type { ComponentHost } from '@nextwebwg/html-next/runtime';
207
+
208
+ export default function counter(host: ComponentHost): void {
209
+ host.on('connect', () => {
210
+ const button = host.root.querySelector('button')!;
211
+ const click = () => { host.state.count = Number(host.state.count) + 1; };
212
+ button.addEventListener('click', click);
213
+ return () => button.removeEventListener('click', click);
214
+ });
215
+ }
216
+ ```
217
+
218
+ HTMLKit resolves `counter.js` to `counter.ts` in development and bundles its JavaScript for production. Vite transpiles; it does not type-check. Install TypeScript as a development dependency when importing HTML Next's controller types, and run your project's strict TypeScript check separately. Markupress maintains its controllers with TypeScript 7. Checked JSDoc is an authoring option for your own controllers.
219
+
220
+ ## Versions
221
+
222
+ Record active docs without overwriting an existing edition:
223
+
224
+ ```sh
225
+ markupress snapshot v1
226
+ markupress build
227
+ ```
228
+
229
+ The command validates a build and writes `versioned_docs/v1/`, then atomically appends its descriptor to `versioned_docs/versions.json`. It mirrors Markdown, public assets and project-local component, controller, stylesheet and asset dependencies, preserving relative imports. Existing snapshots are never replaced. Keep those files in version control. Package imports continue to use the project's installed dependencies and lockfile; snapshots freeze authored sources rather than vendoring npm packages.
230
+
231
+ Use relative project-local imports for snapshot content. Absolute filesystem imports and remote resources retain their external ownership. If a snapshot process crashes, check that it has stopped before removing its `versioned_docs/.snapshot-lock` directory and recovering any incomplete destination.
232
+
233
+ For explicit versions, configure their order and labels:
234
+
235
+ ```json
236
+ {
237
+ "versions": [
238
+ { "id": "current", "label": "Next", "directory": "docs" },
239
+ { "id": "v1", "label": "Version 1", "directory": "versioned_docs/v1/_source/docs", "publicDir": "versioned_docs/v1/_source/public" }
240
+ ],
241
+ "defaultVersion": "v1"
242
+ }
243
+ ```
244
+
245
+ Version IDs are URL slugs; they need not follow SemVer. A document's `id` identifies it across versions independently of its filename, route and generated component name. The version selector links to that identity in the other version, even if its route changed. If it is absent, the selector explains the fallback and links to that version's home. Selection lives in the URL and survives reloads.
246
+
247
+ ## Theme and migration inputs
248
+
249
+ The default theme imports Looma's button and badge resources and CSS exports selectively. Its shell has a skip link, a top navigation bar, a grouped sidebar per section, native version links, an "on this page" outline, a footer and a saved light/dark preference, which an inline head script applies before first paint. It uses the platform's existing runtime.
250
+
251
+ Styles follow the component boundaries. Looma's tokens, its light and dark themes and the document defaults (`styles/base.css`) are page-wide, delivered through HTMLKit's `css` option. Each Markdown page component imports the documentation typography (`styles/prose.css`), so it styles that page and stops at any component the page embeds. The shell's own `<style>` styles the header, sidebar, columns and footer. `theme/theme.css` is page-wide too, because HTMLKit's built-in breadcrumbs and pager have no styles of their own and no component's scoped style reaches them; it also picks the logo for the theme in use. With `markdown()` alone, `markdown({ styles: [file] })` makes every Markdown page component import the given stylesheet files.
252
+
253
+ ### Navigation, logo and footer
254
+
255
+ `markupress.config.json` (or `markupress()`) configures the shell around the pages:
256
+
257
+ ```json
258
+ {
259
+ "title": "Looma",
260
+ "logo": { "light": "/img/mark.svg", "dark": "/img/mark-dark.svg" },
261
+ "favicon": "/favicon.svg",
262
+ "versionBadge": "v0.28",
263
+ "nav": [
264
+ { "label": "Get started", "href": "/" },
265
+ { "label": "Components", "href": "/components/" },
266
+ { "label": "Resources", "items": [{ "label": "HTML Next", "href": "https://nextwebwg.org/declarative-components/" }] }
267
+ ],
268
+ "social": [{ "label": "GitHub", "href": "https://github.com/threadlabs-studio/looma", "icon": "/img/github.svg" }],
269
+ "sidebar": {
270
+ "/": ["/", { "label": "Foundations", "items": ["/architecture/", "/tokens/"] }],
271
+ "/editor/": ["/editor/", { "label": "Tables", "href": "/editor/#tables" }]
272
+ },
273
+ "footer": {
274
+ "columns": [{ "title": "Docs", "items": [{ "label": "Tokens", "href": "/tokens/" }] }],
275
+ "copyright": "Copyright 2026 Threadlabs Studio contributors."
276
+ }
277
+ }
278
+ ```
279
+
280
+ | Option | Does |
281
+ | --- | --- |
282
+ | `logo` | An image before the title: one path, or `{ "light", "dark" }`, one per theme. Paths starting with `/` are files in `public/`. |
283
+ | `favicon` | The site icon, a `<link rel="icon">` on every page. |
284
+ | `versionBadge` | The badge text beside the title. Without it, a site with several versions shows the selected version's label. |
285
+ | `nav` | Top navigation links, and dropdowns (`{ "label", "items" }`), which are native `<details>`. A link to the page being read, or to a section holding it, is marked current. |
286
+ | `social` | Links at the end of the top bar, each shown as its `icon` image, labelled by `label`, or as its label. |
287
+ | `sidebar` | Explicit sidebars by section path. |
288
+ | `footer` | Link columns (`{ "title", "items" }`) and a `copyright` line. |
289
+
290
+ An `href` starting with `/` is a page path in the version being read, so `/components/` opens that version's components. Other links stay as written.
291
+
292
+ **Sidebars.** Each top-level folder of a version is a section with its own sidebar, generated from the folder's pages; a page at the version's root shows every page. A `sidebar` entry replaces the generated sidebar of the section at its path. Its entries are page paths, which take the page's label, links (`{ "label", "href" }`), and groups (`{ "label", "items", "collapsed" }`). A path such as `/editor/` that names a page also makes that page a section. A page path that names no page fails the build.
293
+
294
+ **Groups.** A generated sidebar is HTMLKit's navigation tree, rendered by `<hk-nav>`: each folder below the section is a labelled group, a native `<details>` that collapses without JavaScript, nested as deep as the folders go. A folder with an index page is that page's entry, and its summary links to the page. A group starts open, unless its folder's metadata collapses it, and a group holding the current page always starts open. A `_category.yml` in the folder sets the group's metadata; Docusaurus's `_category_.json` and `_category_.yml` work too:
295
+
296
+ ```yaml
297
+ label: Forms # default: the index page's sidebarLabel, then its title, then the folder name
298
+ position: 3 # orders the folder among its siblings like a 03. folder prefix
299
+ collapsed: true # starts closed, except on the pages inside it
300
+ ```
301
+
302
+ `label` and `position` become the folder's index page's `sidebarLabel` and `sidebarPosition` (HTMLKit `hk:label` and `hk:order`), which its own front matter overrides, so the label also names the index page in breadcrumbs and the pager. A folder with a `_category` file needs an index page, and the build fails without one. `position` is a whole number. Navigation, breadcrumbs and the previous/next links follow the same order.
303
+
304
+ **Outline.** A page with headings in its `toc` range (h2 and h3 by default) shows them in an "On this page" outline beside the content on wide screens. The links are plain fragment links. A small controller (`markupress/controllers/outline.js`, also exported) marks the heading being read with `aria-current="location"`, using an IntersectionObserver; without JavaScript only that marking is missing.
305
+
306
+ **Narrow screens.** Below 769px the sidebar is a drawer: a native popover, closed until the Navigation button opens it, and it opens without JavaScript. Escape or a tap outside closes it, and following a link in it closes it.
307
+
308
+ **Not found.** A `404.md` in the default version's content directory is the site's not-found page (HTMLKit's not-found page, R7). It renders in the shell for every unmatched path, including those under `/v/<id>/`, with status 404 in `dev` and `preview`, and `build` writes it to `404.html`. It is no page of its version, so it has no URL of its own and no sidebar entry.
309
+
310
+ ### Layouts
311
+
312
+ `"layout": "theme/layout.html"` in `markupress.config.json`, or `markupress({ layout })`, replaces the Markupress shell with a site's own HTML Next layout for every documentation page. The layout renders `<slot name="page">` where the page goes, and receives Markupress's loader data as props. It receives each prop it declares with `<prop>`, so it declares only the data it uses:
313
+
314
+ ```html
315
+ <template component="site-layout"><defs>
316
+ <prop name="navigation" type="list(object({ href: string, label: string, current: string, ... }))" required>Links</prop>
317
+ <prop name="outline" type="list(object({ href: string, label: string, depth: number, ... }))" required>On this page</prop>
318
+ <prop name="page" type="object({ title: string, editLink?: string, ... })" required>Page metadata</prop></defs>
319
+ <div class="site">
320
+ <nav><a $each="item of $navigation" from:href="$item.href" from:aria-current="$item.current">{$item.label}</a></nav>
321
+ <main><slot name="page"></slot></main>
322
+ <aside><a $each="heading of $outline" from:href="$heading.href">{$heading.label}</a></aside>
323
+ </div>
324
+ </template>
325
+ ```
326
+
327
+ | Prop | Value |
328
+ | --- | --- |
329
+ | `title` | The site title (`title` option, default "Documentation"). |
330
+ | `home` | The site's base URL. |
331
+ | `navigation` | HTMLKit navigation items for the selected version: `href`, `label`, `current` (`"page"` or `"false"`), `depth` and `pageName`. |
332
+ | `sidebar` | The page's section sidebar, in `<hk-nav>`'s tree shape: entries with `label`, `current`, `open`, `depth` and `children`, and an `href` on a page or link. A folder with an index page has both `href` and `children`; a configured group has no `href`. Pass it to `<hk-nav from:items="$sidebar">`. |
333
+ | `nav`, `social`, `footer` | The resolved `nav`, `social` and `footer` options: links have `label`, `href` and `current`, a dropdown has `items`, and a social link has its `icon` URL. |
334
+ | `logo`, `favicon`, `badge` | The logo's `light` and `dark` URLs, the icon URL (both `null` when unset), and the badge text. |
335
+ | `crumbs` | Navigation items from the version home down to this page. |
336
+ | `previous`, `next` | The neighbouring navigation items, absent at either end. |
337
+ | `versions` | Version links: `href`, `label`, `current` and `note`, which explains a fallback to that version's home. |
338
+ | `outline` | The page's headings in its `toc` range: `id`, `href` (`#id`), `label` and `depth`. |
339
+ | `page` | The page's front matter, plus its resolved `title`, `label` and document `id`. |
340
+
341
+ A folder's `_layout.html` inside a version's content wraps that folder's pages inside the site layout, as in any HTMLKit site. Its `_layout.server.ts` loader reads all of the data above from `parent.markupress`:
342
+
343
+ ```ts
344
+ export function load({ parent }) { return { props: { section: parent.markupress.page.title } }; }
345
+ ```
346
+
347
+ A layout receives exactly the props its component declares, which HTMLKit gives the loader as `propNames`. Declare navigation items with an open object type, which ends in `...`, so items keep passing as HTMLKit adds fields such as a route group's `group`.
348
+
349
+ HTMLKit plugins compose with Markupress through the site's `htmlkit.config.ts`, which the `markupress` command loads (see [Author a site](#author-a-site)).
350
+
351
+ Existing Markdown, HTML/CSS/Less/JavaScript/TypeScript examples, local assets, heading anchors and route aliases are migration inputs. The installed consumer fixture checks those under a nested base. A live Jess/Less documentation migration and visual/content parity comparison remain separate work.
352
+
353
+ ## Verification and contribution
354
+
355
+ ```sh
356
+ corepack pnpm verify:inner
357
+ corepack pnpm verify:pr
358
+ corepack pnpm exec playwright install chromium
359
+ MARKUPRESS_BROWSER_TEST=1 corepack pnpm exec vitest run tests/package.test.ts --maxWorkers=1
360
+ ```
361
+
362
+ The package contract packs and installs the actual package, runs its CLI, checks frozen imports and checks controller types. The browser proof exercises production and development controllers, keyboard version selection, reloads, mobile navigation and an axe accessibility audit. CI runs that browser proof separately.
363
+
364
+ Foundation pins TypeScript, Vitest, Oxlint and pnpm. [AGENTS.md](AGENTS.md) is the contributor contract; [threadlabs.config.json](threadlabs.config.json) records ownership. The [implementation plan](docs/plans/2026-10-04-1830-feat-markupress-platform-plan.md) defines product boundaries. Publishing uses the protected GitHub release workflow and npm environment.
365
+
366
+ ## License
367
+
368
+ [MIT](LICENSE), copyright Threadlabs Studio contributors.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { buildSite, devSite, previewSite } from './site.js';
6
+ import { snapshotVersion } from './versions.js';
7
+ async function main() {
8
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
9
+ root: { type: 'string' }, base: { type: 'string' }, port: { type: 'string' }, help: { type: 'boolean', short: 'h' },
10
+ } });
11
+ if (values.help) {
12
+ process.stdout.write('Usage: markupress build|dev|preview [--root directory] [--base /docs/] [--port 3000]\n markupress snapshot <version-id> [--root directory]\nConfiguration: markupress.config.json, and htmlkit.config.ts for HTMLKit options and plugins\n');
13
+ return;
14
+ }
15
+ const root = resolve(values.root ?? process.cwd());
16
+ let config = {};
17
+ try {
18
+ config = JSON.parse(await readFile(resolve(root, 'markupress.config.json'), 'utf8'));
19
+ }
20
+ catch (error) {
21
+ if (error.code !== 'ENOENT')
22
+ throw error;
23
+ }
24
+ const options = { ...config, root, ...(values.base === undefined ? {} : { base: values.base }),
25
+ ...(values.port === undefined ? {} : { port: Number(values.port) }) };
26
+ const command = positionals[0] ?? 'dev';
27
+ if (command === 'snapshot') {
28
+ if (!positionals[1])
29
+ throw new Error('snapshot requires a version id.');
30
+ const version = await snapshotVersion(positionals[1], options);
31
+ process.stdout.write(`Recorded ${version.id} in ${version.directory}\n`);
32
+ return;
33
+ }
34
+ if (command === 'build') {
35
+ const built = await buildSite(options);
36
+ process.stdout.write(`Generated ${built.routes.length} pages in ${built.outDir}\n`);
37
+ return;
38
+ }
39
+ if (command !== 'dev' && command !== 'preview')
40
+ throw new Error(`Unknown command: ${command}. Use build, dev, or preview.`);
41
+ const server = await (command === 'dev' ? devSite(options) : previewSite(options));
42
+ process.stdout.write(`Markupress ${command}: ${server.url}\n`);
43
+ const close = () => { void server.close().then(() => process.exit(0)); };
44
+ process.once('SIGINT', close);
45
+ process.once('SIGTERM', close);
46
+ }
47
+ void main().catch(error => { process.stderr.write(`${String(error)}\n`); process.exitCode = 1; });
48
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ export interface MarkdownOptions {
2
+ readonly file: string;
3
+ readonly pageName: string;
4
+ /** Resolve authored document links and assets from their original source. */
5
+ readonly resolveLink?: (href: string, kind: 'link' | 'asset') => string;
6
+ /** Stylesheet files the page component imports, so their rules style this page's own region. */
7
+ readonly styles?: readonly string[];
8
+ /** Number the lines of highlighted code blocks unless a fence says `:no-line-numbers`. */
9
+ readonly lineNumbers?: boolean;
10
+ /** Front matter the page's own fields override, such as its folder's `_category` label and position on an index page. */
11
+ readonly frontmatter?: FrontMatter;
12
+ }
13
+ /** A page's validated front matter. Unknown or mistyped fields fail with the Markdown file's name. */
14
+ export interface FrontMatter {
15
+ readonly title?: string;
16
+ readonly description?: string;
17
+ readonly sidebarLabel?: string;
18
+ readonly sidebarHidden?: boolean;
19
+ /** The page's place among its folder's pages, as a number prefix gives it; on an index page, the folder's place (HTMLKit `hk:order`). */
20
+ readonly sidebarPosition?: number;
21
+ readonly id?: string;
22
+ readonly aliases?: readonly string[];
23
+ /** An HTMLKit layout name (app/layouts/<name>.html) in place of the site's layout, or false for none. */
24
+ readonly layout?: string | false;
25
+ /** The heading levels the page's outline lists, such as [2, 3], or false for no outline. */
26
+ readonly toc?: false | readonly [number, number];
27
+ readonly head?: {
28
+ /** `<meta name content>` by name. */
29
+ readonly meta?: Readonly<Record<string, string | number>>;
30
+ /** Open Graph `<meta property="og:…" content>` by property name without its `og:` prefix. */
31
+ readonly og?: Readonly<Record<string, string | number>>;
32
+ readonly canonical?: string;
33
+ };
34
+ /** This page's "Edit this page" URL, or false to show none. */
35
+ readonly editLink?: string | false;
36
+ }
37
+ /** A heading the page's outline lists. */
38
+ export interface OutlineItem {
39
+ readonly id: string;
40
+ /** The heading's fragment link, `#id`. */
41
+ readonly href: string;
42
+ readonly label: string;
43
+ readonly depth: number;
44
+ }
45
+ export interface CompiledMarkdown {
46
+ readonly resource: string;
47
+ readonly title: string;
48
+ readonly label: string;
49
+ readonly id?: string;
50
+ readonly aliases: readonly string[];
51
+ readonly sidebarHidden: boolean;
52
+ readonly frontmatter: FrontMatter;
53
+ /** The headings in the page's `toc` range, [2, 3] by default, in document order. */
54
+ readonly outline: readonly OutlineItem[];
55
+ }
56
+ export declare function escapeHTML(value: string): string;
57
+ export declare function literal(value: string): string;
58
+ export declare function compileMarkdown(source: string, options: MarkdownOptions): Promise<CompiledMarkdown>;
59
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,KAAK,MAAM,CAAC;IACxE,gGAAgG;IAChG,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,yHAAyH;IACzH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;CACpC;AACD,sGAAsG;AACtG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,yIAAyI;IACzI,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,yGAAyG;IACzG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACjC,4FAA4F;IAC5F,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,qCAAqC;QACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;QAC1D,6FAA6F;QAC7F,QAAQ,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;QACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACpC;AACD,0CAA0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AACD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AACD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAA8D;AA0G5G,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsNzG"}