@pagenary/publisher 2026.6.13 → 2026.6.15

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 (151) hide show
  1. package/README.md +37 -3
  2. package/bin/pagenary.mjs +6 -0
  3. package/examples/blog-demo/.public/images/hero-1.svg +11 -0
  4. package/examples/blog-demo/.public/images/hero-2.svg +11 -0
  5. package/examples/blog-demo/.public/images/hero-3.svg +11 -0
  6. package/examples/blog-demo/config.json +28 -0
  7. package/examples/blog-demo/posts/accessible-by-default.md +84 -0
  8. package/examples/blog-demo/posts/designing-for-living-scroll.md +98 -0
  9. package/examples/blog-demo/posts/launching-pagenary-blogs.md +99 -0
  10. package/examples/managed-hosting-billing-cancel.json +4 -0
  11. package/examples/managed-hosting-billing-pro.json +6 -0
  12. package/examples/managed-hosting-domain-failed.json +6 -0
  13. package/examples/managed-hosting-domain-verified.json +5 -0
  14. package/examples/managed-hosting-gitea.yml +118 -0
  15. package/examples/managed-hosting-onboarding-pro.json +16 -0
  16. package/examples/managed-hosting-repo-connected.json +12 -0
  17. package/examples/managed-hosting-repo-failed.json +5 -0
  18. package/examples/managed-hosting-site-created.json +17 -0
  19. package/examples/managed-hosting-site-removed.json +5 -0
  20. package/examples/managed-hosting-webhook-installed.json +5 -0
  21. package/examples/managed-hosting-worker-failed.json +9 -0
  22. package/examples/managed-hosting-worker-publish.json +8 -0
  23. package/examples/managed-hosting.tenants.json +45 -0
  24. package/examples/page-effects/.public/images/aurora.svg +20 -0
  25. package/examples/page-effects/.public/images/ridge.svg +15 -0
  26. package/examples/page-effects/config.json +12 -0
  27. package/examples/page-effects/content/disclosure.md +40 -0
  28. package/examples/page-effects/content/index.md +123 -0
  29. package/examples/page-effects/content/parallax-and-sticky.md +76 -0
  30. package/examples/page-effects/content/scroll-snap.md +29 -0
  31. package/examples/page-effects/content/scrollytelling.md +35 -0
  32. package/examples/page-effects/manifest.json +35 -0
  33. package/examples/recipes.tenants.json +70 -0
  34. package/package.json +9 -3
  35. package/scripts/build-tenants.js +1293 -46
  36. package/scripts/check-accessibility-linter.js +123 -0
  37. package/scripts/check-accessibility-report.js +60 -0
  38. package/scripts/check-accessibility.js +191 -0
  39. package/scripts/check-media-renderers.js +86 -0
  40. package/scripts/check-narration.js +132 -0
  41. package/scripts/check-reading-metadata.js +92 -0
  42. package/scripts/lib/accessibility-linter.js +406 -0
  43. package/scripts/lib/accessibility-report.js +172 -0
  44. package/scripts/lib/collections-generator.js +11 -3
  45. package/scripts/lib/frontmatter.js +263 -20
  46. package/scripts/lib/managed-hosting.js +2025 -0
  47. package/scripts/managed-hosting.js +427 -0
  48. package/scripts/smoke-accessibility.mjs +460 -0
  49. package/scripts/smoke-browser.mjs +51 -4
  50. package/site/accessibility-report.json +80 -0
  51. package/site/accessibility-report.md +37 -0
  52. package/site/app.54e1ad90b733.js +1 -0
  53. package/site/app.js +1 -1
  54. package/site/assets/images/pipeline.e57f0dbfd05a.svg +33 -0
  55. package/site/assets/images/pipeline.svg +33 -0
  56. package/site/index.html +16 -13
  57. package/site/lib/blog-index.00956c25bdd1.js +1 -0
  58. package/site/lib/blog-index.js +1 -0
  59. package/site/lib/categories.af208377f073.js +1 -0
  60. package/site/lib/docs-map.6d60a86c4dae.js +1 -0
  61. package/site/lib/docs-map.js +1 -1
  62. package/site/lib/export.2db2c0bd974c.js +1 -0
  63. package/site/lib/export.js +1 -1
  64. package/site/lib/fortemi-corpus.6a5e3cc1c69e.js +1 -0
  65. package/site/lib/fortemi-corpus.js +1 -1
  66. package/site/lib/manifest-utils.5d2b43ebceab.js +1 -0
  67. package/site/lib/page-effects.2131b53bea6b.js +1 -0
  68. package/site/lib/page-effects.js +1 -0
  69. package/site/lib/router.f9d1cfba022d.js +1 -0
  70. package/site/lib/search.c52bcae8afda.js +1 -0
  71. package/site/lib/search.js +1 -1
  72. package/site/llms.txt +12 -1
  73. package/site/manifest.95224f06782d.js +209 -0
  74. package/site/manifest.js +80 -12
  75. package/site/media-init.16fde41d8850.js +1 -0
  76. package/site/media-init.js +1 -0
  77. package/site/mermaid-init.f25ee3b6ec1e.js +1 -0
  78. package/site/pages/accessible-authoring.html +245 -0
  79. package/site/pages/api.html +25 -4
  80. package/site/pages/architecture.html +11 -4
  81. package/site/pages/blog-layout.html +186 -0
  82. package/site/pages/deployment.html +26 -12
  83. package/site/pages/developer-guide.html +3 -2
  84. package/site/pages/extending.html +2 -2
  85. package/site/pages/overview.html +189 -0
  86. package/site/pages/page-effects.html +265 -0
  87. package/site/pages/publishing.html +258 -0
  88. package/site/pages/quickstart.html +2 -2
  89. package/site/pages/search-and-data.html +159 -0
  90. package/site/pages/seo-strategy.html +2 -2
  91. package/site/pages/showcase-gallery.html +184 -0
  92. package/site/pages/showcase-story.html +130 -0
  93. package/site/pages/tenant-config.html +133 -12
  94. package/site/pages/theming-recipes.html +56 -6
  95. package/site/pages/welcome.html +3 -2
  96. package/site/robots.txt +1 -1
  97. package/site/search-index/manifest.json +62 -25
  98. package/site/search-index/metadata.json +1548 -331
  99. package/site/search-index/part-0000.64586040c481.json +2772 -0
  100. package/site/search-index/part-0000.json +1560 -335
  101. package/site/sections/accessible-authoring.c7e5f50f256c.js +3 -0
  102. package/site/sections/accessible-authoring.js +3 -0
  103. package/site/sections/api.cd952afb3bf1.js +3 -0
  104. package/site/sections/api.js +1 -1
  105. package/site/sections/architecture.c98e76504bce.js +3 -0
  106. package/site/sections/architecture.js +1 -1
  107. package/site/sections/blog-layout.f34ad33cdde0.js +3 -0
  108. package/site/sections/blog-layout.js +3 -0
  109. package/site/sections/deployment.13d21771dd4c.js +3 -0
  110. package/site/sections/deployment.js +1 -1
  111. package/site/sections/developer-guide.0e330658f394.js +3 -0
  112. package/site/sections/developer-guide.js +1 -1
  113. package/site/sections/extending.7ae6dac6af72.js +3 -0
  114. package/site/sections/overview.5a483987fb9d.js +3 -0
  115. package/site/sections/overview.js +3 -0
  116. package/site/sections/page-effects.a38e5a7715d4.js +3 -0
  117. package/site/sections/page-effects.js +3 -0
  118. package/site/sections/publishing.32bf1d55b285.js +3 -0
  119. package/site/sections/publishing.js +3 -0
  120. package/site/sections/quickstart.fda6fa38d58d.js +3 -0
  121. package/site/sections/search-and-data.f929f5fdaac9.js +3 -0
  122. package/site/sections/search-and-data.js +3 -0
  123. package/site/sections/section-templates.ccdf4cf67f7b.js +1 -0
  124. package/site/sections/seo-strategy.8958aca48673.js +3 -0
  125. package/site/sections/showcase-gallery.cd729f94752d.js +3 -0
  126. package/site/sections/showcase-gallery.js +3 -0
  127. package/site/sections/showcase-story.79311d1302c8.js +3 -0
  128. package/site/sections/showcase-story.js +3 -0
  129. package/site/sections/tenant-config.552c0d8c6d2b.js +3 -0
  130. package/site/sections/tenant-config.js +1 -1
  131. package/site/sections/theming-recipes.5fa819c1767c.js +3 -0
  132. package/site/sections/theming-recipes.js +1 -1
  133. package/site/sections/welcome.8f10c9c4c4f0.js +3 -0
  134. package/site/sections/welcome.js +1 -1
  135. package/site/seo.90687a1d3d78.js +1 -0
  136. package/site/sitemap.xml +59 -11
  137. package/site/styles.bdb30ba34de5.css +3525 -0
  138. package/site/styles.css +1037 -94
  139. package/site/syntax-highlight.9d51f36b24da.js +1 -0
  140. package/site/vendor/fortemi-aiwg-index.5fb180ec6a20.js +1 -0
  141. package/src/app.js +329 -109
  142. package/src/index.html +12 -9
  143. package/src/lib/blog-index.js +113 -0
  144. package/src/lib/docs-map.js +224 -30
  145. package/src/lib/export.js +22 -15
  146. package/src/lib/fortemi-corpus.js +1 -1
  147. package/src/lib/page-effects.js +644 -0
  148. package/src/manifest.js +13 -0
  149. package/src/media-init.js +28 -0
  150. package/src/styles.css +1037 -94
  151. package/tenants.schema.json +253 -0
package/README.md CHANGED
@@ -31,7 +31,7 @@ npx pagenary serve # serve on http://localhost:5173
31
31
 
32
32
  ## What It Is
33
33
 
34
- The publisher takes a catalog of shared section templates plus per-tenant content and configuration and produces a self-contained documentation bundle for each tenant. Each bundle is a static single-page app — hash-based routing (`#/page-id`), no server-side rendering, no runtime dependencies — that you build once and host anywhere that serves files. A per-tenant `<base>` resolves asset and module URLs to the tenant root, so the same bundle serves correctly at a domain root *or* under a subpath mount. Tenants share the template catalog but keep isolated content, branding, navigation, and domains — each with ranked client-side search and SEO-ready output — so one repository can publish a dozen distinct sites.
34
+ The publisher takes a catalog of shared section templates plus per-tenant content and configuration and produces a self-contained documentation bundle for each tenant. Each bundle is a static single-page app — hash-based routing (`#page-id`), no server-side rendering, no runtime dependencies — that you build once and host anywhere that serves files. A per-tenant `<base>` resolves asset and module URLs to the tenant root, so the same bundle serves correctly at a domain root *or* under a subpath mount. Tenants share the template catalog but keep isolated content, branding, navigation, and domains — each with ranked client-side search and SEO-ready output — so one repository can publish a dozen distinct sites.
35
35
 
36
36
  ---
37
37
 
@@ -47,8 +47,10 @@ npx pagenary build:tenants my-docs # build your tenant (see Tenant Registry be
47
47
  npx pagenary serve # preview on http://localhost:5173
48
48
  ```
49
49
 
50
- Commands: `build`, `build:tenants [id]`, `tenants:list`, `serve` (run
51
- `npx pagenary --help`). The package also ships a compiled reference site under `site/` — the Pagenary docs, built by Pagenary itself.
50
+ Commands: `build`, `build:tenants [id]`, `tenants:list`,
51
+ `managed-hosting`, `serve` (run `npx pagenary --help`). The package also
52
+ ships a compiled reference site under `site/` — the Pagenary docs, built by
53
+ Pagenary itself.
52
54
 
53
55
  **Building from source** (contributors / modifying Pagenary):
54
56
 
@@ -265,6 +267,7 @@ npx pagenary build # build the default bundle to dist/
265
267
  npx pagenary build:tenants # build all enabled tenants
266
268
  npx pagenary build:tenants my-tenant # build a specific tenant
267
269
  npx pagenary tenants:list # list configured tenants
270
+ npx pagenary managed-hosting plans # inspect public hosting entitlements
268
271
  npx pagenary serve # serve dist/ on localhost:5173
269
272
  ```
270
273
 
@@ -352,6 +355,36 @@ Use a non-privileged port: `DOCS_TOOLKIT_PORT=5173 npm run caddy:up`
352
355
 
353
356
  ---
354
357
 
358
+ ## Managed Hosting MVP
359
+
360
+ Pagenary can be operated as a concierge managed-hosting service before the
361
+ self-serve control panel exists. The public package includes the static
362
+ publisher, plan/entitlement contract, routing generator, and build-worker
363
+ examples; Stripe secrets, OAuth apps, customer records, and the control panel
364
+ belong in the private hosting/control-plane repository.
365
+
366
+ ```bash
367
+ npm run managed-hosting -- plans
368
+ npm run managed-hosting -- onboarding-intake examples/managed-hosting.tenants.json examples/managed-hosting-onboarding-pro.json
369
+ npm run managed-hosting -- account-usage examples/managed-hosting.tenants.json
370
+ npm run managed-hosting -- dashboard-state examples/managed-hosting.tenants.json --account-id acme
371
+ npm run managed-hosting -- validate examples/managed-hosting.tenants.json
372
+ npm run managed-hosting -- caddy examples/managed-hosting.tenants.json
373
+ npm run managed-hosting -- billing-action examples/managed-hosting.tenants.json acme
374
+ npm run managed-hosting -- site-event examples/managed-hosting.tenants.json examples/managed-hosting-site-created.json
375
+ npm run managed-hosting -- domain-event examples/managed-hosting.tenants.json acme examples/managed-hosting-domain-verified.json
376
+ npm run managed-hosting -- repo-event examples/managed-hosting.tenants.json acme examples/managed-hosting-repo-connected.json
377
+ npm run managed-hosting -- rollback-plan examples/managed-hosting.tenants.json acme
378
+ npm run managed-hosting -- deploy-manifest examples/managed-hosting.tenants.json acme
379
+ npm run managed-hosting -- artifact-index examples/managed-hosting.tenants.json acme
380
+ ```
381
+
382
+ See [Managed Hosting MVP](docs/MANAGED-HOSTING.md) for the concierge flow,
383
+ plan gates, Caddy routing, worker example, post-sync support packet, worker
384
+ status events, and private control-panel boundary.
385
+
386
+ ---
387
+
355
388
  ## Repository Layout
356
389
 
357
390
  ```
@@ -384,6 +417,7 @@ The full documentation site is published at **[docs.pagenary.com](https://docs.p
384
417
  - [Getting Started](docs/GETTING-STARTED.md) — **start here**: zero to a published site with the npm package
385
418
  - [Quick Start Guide](docs/QUICKSTART.md) — step-by-step tenant creation
386
419
  - [Publish with GitHub/Gitea Actions](docs/PUBLISHING.md) — make any docs repo Pagenary-ready: copy-paste CI workflows + auto-discovery
420
+ - [Managed Hosting MVP](docs/MANAGED-HOSTING.md) — concierge hosting, plan gates, Caddy routing, and worker examples
387
421
  - [Tenant Configuration](docs/TENANT-CONFIG.md) — all config options (branding, theme, SEO)
388
422
  - [Theming Recipes](docs/THEMING-RECIPES.md) — copy-paste recipes for colors, fonts, and nav positions, with screenshots
389
423
  - [Architecture](docs/ARCHITECTURE.md) — system design
package/bin/pagenary.mjs CHANGED
@@ -37,6 +37,11 @@ const COMMANDS = {
37
37
  script: 'serve.js',
38
38
  baseArgs: [],
39
39
  summary: 'Serve the built output over HTTP.'
40
+ },
41
+ 'managed-hosting': {
42
+ script: 'managed-hosting.js',
43
+ baseArgs: [],
44
+ summary: 'Validate concierge hosting plans, routing, and onboarding records.'
40
45
  }
41
46
  };
42
47
 
@@ -67,6 +72,7 @@ function printHelp() {
67
72
  ' pagenary build:tenants pagenary # build one tenant',
68
73
  ' pagenary tenants:list',
69
74
  ' pagenary serve',
75
+ ' pagenary managed-hosting plans',
70
76
  '',
71
77
  'Any extra options are passed through to the underlying script, e.g.',
72
78
  ' pagenary build:tenants --incremental',
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600" role="img" aria-label="Abstract gradient">
2
+ <defs>
3
+ <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#0ea5e9"/>
5
+ <stop offset="1" stop-color="#6366f1"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="1200" height="600" fill="url(#g1)"/>
9
+ <circle cx="320" cy="200" r="180" fill="#ffffff" opacity="0.10"/>
10
+ <circle cx="900" cy="440" r="240" fill="#ffffff" opacity="0.08"/>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600" role="img" aria-label="Abstract gradient">
2
+ <defs>
3
+ <linearGradient id="g2" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#f59e0b"/>
5
+ <stop offset="1" stop-color="#ec4899"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="1200" height="600" fill="url(#g2)"/>
9
+ <rect x="120" y="120" width="360" height="360" rx="40" fill="#ffffff" opacity="0.10"/>
10
+ <rect x="760" y="240" width="300" height="300" rx="40" fill="#ffffff" opacity="0.08"/>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600" role="img" aria-label="Abstract gradient">
2
+ <defs>
3
+ <linearGradient id="g3" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#10b981"/>
5
+ <stop offset="1" stop-color="#0ea5e9"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="1200" height="600" fill="url(#g3)"/>
9
+ <path d="M0 460 Q 300 360 600 460 T 1200 460 V600 H0 Z" fill="#ffffff" opacity="0.10"/>
10
+ <path d="M0 520 Q 300 430 600 520 T 1200 520 V600 H0 Z" fill="#ffffff" opacity="0.08"/>
11
+ </svg>
@@ -0,0 +1,28 @@
1
+ {
2
+ "title": "Fieldnotes — the Pagenary blog demo",
3
+ "brandMark": "Fieldnotes",
4
+ "brandSub": "Blog",
5
+ "tagline": "A blog-layout demo built with Pagenary.",
6
+ "description": "Demonstrates the Pagenary blog layout: a chronological index, post pages with hero images, bylines, and tags — driven by the collections engine.",
7
+ "accentColor": "#0ea5e9",
8
+ "layout": "blog",
9
+ "blog": {
10
+ "sidebar": "hidden",
11
+ "indexTitle": "Latest posts",
12
+ "livingScroll": true
13
+ },
14
+ "collections": [
15
+ {
16
+ "path": "posts",
17
+ "route": "/posts",
18
+ "title": "Posts",
19
+ "manifest": true,
20
+ "feed": true,
21
+ "sortBy": "date",
22
+ "order": "desc",
23
+ "showDate": true,
24
+ "showSummary": true,
25
+ "showReadingTime": true
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: Accessible by default
3
+ date: 2026-04-30
4
+ author: Pagenary Team
5
+ summary: Semantic landmarks, real links and buttons, keyboard order, and progressive enhancement are not add-ons in Pagenary — they are the baseline the blog layout is built on.
6
+ tags: [accessibility]
7
+ hero: assets/images/hero-3.svg
8
+ ---
9
+
10
+ # Accessible by default
11
+
12
+ A blog that looks modern but traps keyboard users, or hides its text behind an
13
+ animation that never finishes, is not modern — it is broken with nicer paint. The
14
+ Pagenary blog layout starts from the opposite premise: the accessible version
15
+ *is* the version. Everything else is layered on top of a page that already works
16
+ for everyone.
17
+
18
+ ## The baseline
19
+
20
+ Before any styling or motion, a post page is built from real, meaningful HTML:
21
+
22
+ - Real `<article>`, `<time>`, `<a>`, and `<nav>` elements — not click handlers
23
+ bolted onto `<div>`s.
24
+ - One `<h1>` per page, with headings in order, so the document outline makes
25
+ sense to a screen reader and to a search crawler.
26
+ - Semantic landmarks and a working skip link, so keyboard and assistive-tech
27
+ users can jump straight to the content.
28
+ - Focus order that follows reading order — you tab through the page the way you
29
+ read it.
30
+ - Text and background colors drawn from theme tokens that are checked for
31
+ contrast, in light and dark alike.
32
+
33
+ If you turned off the stylesheet entirely, you would still have a coherent,
34
+ navigable document. That is the test.
35
+
36
+ ## Progressive enhancement, not graceful degradation
37
+
38
+ The order of operations matters. We do not build a flashy page and then try to
39
+ claw back accessibility for the readers it excluded. We build the readable page
40
+ first and *enhance* it for readers who can take the enhancement.
41
+
42
+ Concretely: the reveal-on-scroll base state is hidden only when scripting is
43
+ present (`html.has-js`) and the reader allows motion. So the no-JavaScript reader
44
+ and the reduced-motion reader never have content hidden from them — there is
45
+ nothing to "recover", because they were never excluded in the first place.
46
+
47
+ > Accessibility is not a feature you add at the end. It is the shape of the thing
48
+ > you started with.
49
+
50
+ ## Motion is a preference, and we honor it
51
+
52
+ Every animation in the blog — reveal-on-scroll, hero parallax, the works — is
53
+ wrapped in `@media (prefers-reduced-motion: no-preference)`. When the reader has
54
+ told their operating system they want less motion, the page obliges completely:
55
+ no fades, no parallax, no movement. The content is simply present.
56
+
57
+ This is not a fallback we tolerate; it is a first-class path we test. A reduced-
58
+ motion reader should get a blog that feels intentional and calm, not a stripped
59
+ one that feels broken.
60
+
61
+ ## Navigation you can actually operate
62
+
63
+ The post navigation at the foot of every page is a labelled `<nav>` of real
64
+ links. You can reach it by keyboard, you can tell where each link goes from its
65
+ text alone, and screen readers announce it as a navigation region. The same is
66
+ true of the command palette, the theme picker, and the sidebar: real controls,
67
+ real focus management, real labels.
68
+
69
+ ## Why bother being strict
70
+
71
+ Two reasons, and only one of them is ethics.
72
+
73
+ The first is that the readers excluded by inaccessible design are real and many —
74
+ keyboard-only users, screen-reader users, people with vestibular conditions for
75
+ whom unbidden motion is genuinely unpleasant. A docs or blog tool that quietly
76
+ locks them out is failing at its one job: communication.
77
+
78
+ The second is that accessible markup is *better engineering*. Semantic HTML is
79
+ more robust, more searchable, easier to style, and easier to maintain than a pile
80
+ of `<div>`s wearing ARIA as a costume. Doing it right is not a tax on the modern,
81
+ dynamic feel — it is the foundation that lets the dynamic layer be added safely.
82
+
83
+ Accessible and dynamic were never in tension. One is just the floor the other
84
+ stands on.
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: Designing for living scroll
3
+ date: 2026-05-22
4
+ author: Pagenary Team
5
+ summary: A blog should feel alive as you read it — content arriving as it enters view, a sense of progress, motion that flows. Here is how we think about it, accessibly.
6
+ tags: [design, motion, accessibility]
7
+ hero: assets/images/hero-2.svg
8
+ ---
9
+
10
+ # Designing for living scroll
11
+
12
+ "Living scroll" is the feeling that a page is responding to you as you move
13
+ through it. Sections settle into place as they enter the viewport. A slim bar at
14
+ the top tracks how far you have read. Nothing snaps; everything flows. Done well,
15
+ you stop noticing the mechanics and simply feel that the page is *with* you.
16
+
17
+ This post is itself an example. As you scroll, each block you are reading arrived
18
+ a moment ago, and the bar along the top has been creeping rightward the whole
19
+ time. If you have reduced motion turned on, none of that happened — and you have
20
+ not missed a single word. That is the whole design philosophy in one sentence.
21
+
22
+ ## The reader sets the terms
23
+
24
+ Motion is an enhancement, never a requirement. We hold three rules, in order:
25
+
26
+ 1. **The words come first.** With JavaScript disabled, the full article is on the
27
+ page — nothing is hidden waiting for a script to run. The reveal only ever
28
+ *delays* paint for readers who can run it; it never gates content.
29
+ 2. **Reduced motion wins.** Every animation lives inside
30
+ `@media (prefers-reduced-motion: no-preference)`. If the reader has asked the
31
+ operating system for less motion, the content is simply there, fully visible,
32
+ with no transitions at all.
33
+ 3. **It degrades to plain.** Old browser, flaky connection, script error — any
34
+ failure leaves a complete, readable blog behind, because the dynamic layer is
35
+ added on top of a page that already works.
36
+
37
+ If a feature cannot honor all three, it does not ship.
38
+
39
+ ## How the reveal actually works
40
+
41
+ The trick is where the hidden state lives. It would be easy — and wrong — to hide
42
+ every block in JavaScript and reveal it on scroll, because then a reader with no
43
+ JavaScript sees nothing. Instead the base "hidden" state is **CSS**, scoped under
44
+ three conditions at once: the page has opted in, scripting is present, and the
45
+ reader allows motion.
46
+
47
+ ```css
48
+ html.has-js body[data-blog-living-scroll] .doc-content > * {
49
+ opacity: 0;
50
+ transform: translateY(14px);
51
+ }
52
+ ```
53
+
54
+ The `html.has-js` class is only added when scripts run, so a no-JavaScript page
55
+ never matches this rule and shows everything. The media query handles reduced
56
+ motion. Only when all three line up does a block start hidden — and then an
57
+ `IntersectionObserver` adds a `revealed` class as it scrolls into view.
58
+
59
+ Blocks already on screen when the page loads reveal at once, as a gentle
60
+ entrance. Everything below the fold arrives as you reach it. There is no list of
61
+ hard-coded timings to maintain; the viewport is the timeline.
62
+
63
+ ## A sense of progress
64
+
65
+ The reading-progress bar is deliberately the quietest element on the page: three
66
+ pixels tall, the accent color, pinned to the top, and marked `aria-hidden`
67
+ because it carries no information a screen reader needs — it mirrors the scrollbar
68
+ the reader already has. It exists for the same reason a book's thickness in your
69
+ hand does: a glanceable sense of *how much is left*.
70
+
71
+ ## Motion that flows, not motion that shows off
72
+
73
+ It is tempting to reach for parallax on everything, snap-scrolling between
74
+ full-screen panels, and text that types itself out. Each of those can be lovely
75
+ and each can be exhausting. Our test for any effect is simple:
76
+
77
+ > Does it help the reader move through the writing, or does it ask the reader to
78
+ > watch it perform?
79
+
80
+ Reveal-on-scroll passes — it paces the page to your scrolling. A hero parallax
81
+ passes in small doses — it adds depth without demanding attention. A full-screen
82
+ typewriter on the article body fails — it slows the one thing the reader came to
83
+ do. So we ship the first two as defaults, keep the rest opt-in, and gate all of
84
+ it on the reader's stated preference.
85
+
86
+ ## Turning it on
87
+
88
+ Living scroll is one key:
89
+
90
+ ```json
91
+ { "blog": { "livingScroll": true } }
92
+ ```
93
+
94
+ That sets two body hooks at build time — one for the reveal, one for the progress
95
+ bar — and the runtime does the rest, per render, tearing the observers down when
96
+ you navigate away. Leave it off and the blog is calm and static. Turn it on and
97
+ the blog feels alive — for every reader who wants it, and invisibly out of the
98
+ way for every reader who doesn't.
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: Launching the Pagenary blog layout
3
+ date: 2026-06-10
4
+ author: Pagenary Team
5
+ summary: Pagenary is no longer just a knowledge-base tool — the new blog layout turns a folder of dated Markdown into a chronological, hero-led publication.
6
+ tags: [announcement, layout]
7
+ hero: assets/images/hero-1.svg
8
+ ---
9
+
10
+ # Launching the Pagenary blog layout
11
+
12
+ For a long time every Pagenary site shared one silhouette: a header, a left
13
+ sidebar, and a reading column tuned for reference material. That shape is right
14
+ for documentation — you want the whole tree in front of you, and you jump around
15
+ more than you read top to bottom. It is the wrong shape for writing that is meant
16
+ to be *read*: a changelog, a field journal, a release blog, an essay.
17
+
18
+ So we added a second layout family. Set `layout: "blog"` and the same Markdown,
19
+ the same collections engine, and the same build produce a chronological index of
20
+ post cards and reading-first post pages — hero image, byline, tags, and a
21
+ comfortable measure. Nothing about your content changes. Only the silhouette does.
22
+
23
+ ## What you get
24
+
25
+ A blog tenant is still just a folder of files, but the build now understands a
26
+ few new conventions:
27
+
28
+ - **A chronological index.** The landing page becomes a card grid built from your
29
+ collection's `index.json`, newest first. Each card carries the hero thumbnail,
30
+ title, date, author, reading time, tags, and excerpt.
31
+ - **Reading-first post pages.** The hero banner renders above the title, the
32
+ byline (`date · By author · N min read`) sits below it, and tag chips follow
33
+ the summary. The reading column holds a comfortable line length.
34
+ - **A feed.** Turn on `feed: true` for a collection and the build emits
35
+ `feed.xml` alongside the index — RSS readers get your posts for free.
36
+ - **Two silhouettes.** `blog.sidebar: "hidden"` gives a single centered column;
37
+ `blog.sidebar: "rail"` keeps a slim posts rail on the trailing edge.
38
+
39
+ None of this is a new content type. A post is a Markdown file with frontmatter,
40
+ exactly like a docs page — it just lives in a collection folder and carries a
41
+ `date`.
42
+
43
+ ## Quick start
44
+
45
+ Declare the layout and a collection in `config.json`:
46
+
47
+ ```json
48
+ {
49
+ "title": "Fieldnotes",
50
+ "layout": "blog",
51
+ "blog": { "sidebar": "hidden", "indexTitle": "Latest posts", "livingScroll": true },
52
+ "collections": [
53
+ { "path": "posts", "route": "/posts", "manifest": true, "feed": true,
54
+ "sortBy": "date", "order": "desc", "showDate": true, "showSummary": true }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ Then write one Markdown file per post under `posts/`, each with a `title`,
60
+ `date`, `summary`, and optional `tags` and `hero`. Build with
61
+ `npm run build:examples` and open `dist/blog-demo/`. That is the entire workflow —
62
+ there is no admin panel, no database, and no per-post wiring.
63
+
64
+ ## Moving between posts
65
+
66
+ With the sidebar hidden there is nowhere to put a full navigation tree, so each
67
+ post ends with a small, persistent control: previous post, back to the index,
68
+ next post. It is a real `<nav>` of links, scoped to the collection, and it is
69
+ keyboard-navigable. Readers never reach the end of a post and find themselves
70
+ stranded.
71
+
72
+ ## It is still a static site
73
+
74
+ The blog layout inherits everything that makes a Pagenary docs site cheap and
75
+ durable. The output is plain static files: deploy them to any free static host or
76
+ CDN. Ranked search still works across your posts. The SEO pipeline still
77
+ prerenders a snapshot of every page, so crawlers and no-JavaScript readers get
78
+ the full article, not an empty shell.
79
+
80
+ > A blog should not cost more to run than a folder of text files. With Pagenary,
81
+ > it doesn't — it *is* a folder of text files.
82
+
83
+ ## Make it yours
84
+
85
+ Because a blog themes exactly like a docs site, the same `theme` presets, accent
86
+ and surface colors, and fonts all apply. Want a dark journal, a warm serif
87
+ review, or a bright editorial palette? That is a few keys in `config.json`, not a
88
+ fork. The theme gallery ships several ready-made blog looks to start from.
89
+
90
+ ## Where this is going
91
+
92
+ This first release is deliberately small: the layout, the index, the post page,
93
+ a feed, and post navigation. From here the interesting work is in how a post
94
+ *reads* — content that arrives as you scroll, a sense of progress, motion that
95
+ flows but never gets in the way. The next post digs into that, and into why every
96
+ bit of it stays optional and accessible.
97
+
98
+ If you keep your writing in a git repo today, you are already most of the way to
99
+ a blog. Point Pagenary at it, set `layout: "blog"`, and publish.
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "subscription.canceled",
3
+ "effectiveAt": "2026-06-22T00:00:00.000Z"
4
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "subscription.active",
3
+ "plan": "pro",
4
+ "paymentStatus": "active",
5
+ "effectiveAt": "2026-06-22T00:00:00.000Z"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "custom_domain.failed",
3
+ "domain": "docs.acme.com",
4
+ "reason": "DNS record did not resolve to acme.pagenary.app",
5
+ "effectiveAt": "2026-06-22T00:05:00.000Z"
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "custom_domain.verified",
3
+ "domain": "docs.acme.com",
4
+ "effectiveAt": "2026-06-22T00:00:00.000Z"
5
+ }
@@ -0,0 +1,118 @@
1
+ # Example managed-hosting build worker for a single concierge tenant.
2
+ # Copy into the private hosting/control-plane repo and replace the tenant id,
3
+ # deploy target, and secrets there. Keep payment/OAuth/customer secrets out of
4
+ # this public repository.
5
+
6
+ name: Managed Hosting Tenant Build
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ workflow_dispatch:
12
+
13
+ concurrency:
14
+ group: managed-hosting-${{ vars.PAGENARY_TENANT_ID }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ build-and-deploy:
19
+ runs-on: ubuntu-latest
20
+ container: node:20@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5
21
+ timeout-minutes: 10
22
+ defaults:
23
+ run:
24
+ shell: bash
25
+
26
+ steps:
27
+ - name: Checkout hosting registry
28
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
29
+
30
+ - name: Install dependencies
31
+ run: npm ci
32
+
33
+ - name: Validate public hosting contract
34
+ working-directory: apps/publisher
35
+ run: node scripts/managed-hosting.js validate examples/managed-hosting.tenants.json
36
+
37
+ - name: Build tenant bundle
38
+ working-directory: apps/publisher
39
+ env:
40
+ GIT_TERMINAL_PROMPT: "0"
41
+ GIT_SSH_COMMAND: "ssh -i ${{ secrets.CUSTOMER_DEPLOY_KEY }} -o StrictHostKeyChecking=accept-new"
42
+ run: node scripts/build-tenants.js "${{ vars.PAGENARY_TENANT_ID }}" --registry examples/managed-hosting.tenants.json
43
+
44
+ - name: Verify build output
45
+ env:
46
+ TENANT_ID: ${{ vars.PAGENARY_TENANT_ID }}
47
+ run: |
48
+ set -euo pipefail
49
+ OUT="apps/publisher/dist/${TENANT_ID}"
50
+ test -f "${OUT}/index.html"
51
+ test -d "${OUT}/sections"
52
+ test -d "${OUT}/search-index"
53
+
54
+ - name: Write public deployment status
55
+ working-directory: apps/publisher
56
+ run: |
57
+ node scripts/managed-hosting.js status examples/managed-hosting.tenants.json "${{ vars.PAGENARY_TENANT_ID }}" \
58
+ --commit "${{ gitea.sha }}" \
59
+ --log-url "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
60
+ --verified-domain "${{ vars.PAGENARY_VERIFIED_DOMAIN }}" \
61
+ > "dist/${{ vars.PAGENARY_TENANT_ID }}/pagenary-hosting-status.json"
62
+
63
+ - name: Gate launch readiness
64
+ working-directory: apps/publisher
65
+ run: |
66
+ node scripts/managed-hosting.js readiness examples/managed-hosting.tenants.json "${{ vars.PAGENARY_TENANT_ID }}" \
67
+ --commit "${{ gitea.sha }}" \
68
+ --log-url "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
69
+ --verified-domain "${{ vars.PAGENARY_VERIFIED_DOMAIN }}" \
70
+ > "dist/${{ vars.PAGENARY_TENANT_ID }}/pagenary-hosting-readiness.json"
71
+
72
+ - name: Write publish plan
73
+ working-directory: apps/publisher
74
+ run: |
75
+ node scripts/managed-hosting.js publish-plan examples/managed-hosting.tenants.json "${{ vars.PAGENARY_TENANT_ID }}" \
76
+ --commit "${{ gitea.sha }}" \
77
+ --log-url "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
78
+ --verified-domain "${{ vars.PAGENARY_VERIFIED_DOMAIN }}" \
79
+ --deploy-root "${{ secrets.PAGENARY_DEPLOY_ROOT }}" \
80
+ > "dist/${{ vars.PAGENARY_TENANT_ID }}/pagenary-hosting-publish-plan.json"
81
+
82
+ - name: Sync static output
83
+ env:
84
+ TENANT_ID: ${{ vars.PAGENARY_TENANT_ID }}
85
+ DEPLOY_ROOT: ${{ secrets.PAGENARY_DEPLOY_ROOT }}
86
+ run: |
87
+ set -euo pipefail
88
+ rsync -av --delete "apps/publisher/dist/${TENANT_ID}/" "${DEPLOY_ROOT%/}/${TENANT_ID}/"
89
+
90
+ - name: Verify published output
91
+ working-directory: apps/publisher
92
+ run: |
93
+ node scripts/managed-hosting.js publish-check examples/managed-hosting.tenants.json "${{ vars.PAGENARY_TENANT_ID }}" \
94
+ --commit "${{ gitea.sha }}" \
95
+ --log-url "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
96
+ --verified-domain "${{ vars.PAGENARY_VERIFIED_DOMAIN }}" \
97
+ --deploy-root "${{ secrets.PAGENARY_DEPLOY_ROOT }}" \
98
+ > "dist/${{ vars.PAGENARY_TENANT_ID }}/pagenary-hosting-publish-result.json"
99
+
100
+ - name: Write deploy/CDN manifest
101
+ working-directory: apps/publisher
102
+ run: |
103
+ node scripts/managed-hosting.js deploy-manifest examples/managed-hosting.tenants.json "${{ vars.PAGENARY_TENANT_ID }}" \
104
+ --commit "${{ gitea.sha }}" \
105
+ --log-url "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
106
+ --verified-domain "${{ vars.PAGENARY_VERIFIED_DOMAIN }}" \
107
+ --deploy-root "${{ secrets.PAGENARY_DEPLOY_ROOT }}" \
108
+ > "dist/${{ vars.PAGENARY_TENANT_ID }}/pagenary-hosting-deploy-manifest.json"
109
+
110
+ - name: Write managed-hosting artifact index
111
+ working-directory: apps/publisher
112
+ run: |
113
+ node scripts/managed-hosting.js artifact-index examples/managed-hosting.tenants.json "${{ vars.PAGENARY_TENANT_ID }}" \
114
+ --commit "${{ gitea.sha }}" \
115
+ --log-url "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
116
+ --verified-domain "${{ vars.PAGENARY_VERIFIED_DOMAIN }}" \
117
+ --deploy-root "${{ secrets.PAGENARY_DEPLOY_ROOT }}" \
118
+ > "dist/${{ vars.PAGENARY_TENANT_ID }}/pagenary-hosting-artifact-index.json"
@@ -0,0 +1,16 @@
1
+ {
2
+ "tenantId": "beta",
3
+ "accountId": "beta",
4
+ "plan": "pro",
5
+ "subdomain": "beta",
6
+ "siteCount": 1,
7
+ "paymentStatus": "manual-pending",
8
+ "privateRepo": true,
9
+ "domains": ["docs.beta.example.com"],
10
+ "source": {
11
+ "type": "git",
12
+ "url": "ssh://git@git.example.com/beta/docs.git",
13
+ "ref": "main",
14
+ "path": "docs"
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "type": "repository.connected",
3
+ "privateRepo": true,
4
+ "credentialRef": "secret:CUSTOMER_DEPLOY_KEY/acme",
5
+ "source": {
6
+ "type": "git",
7
+ "url": "ssh://git@git.example.com/acme/docs.git",
8
+ "ref": "main",
9
+ "path": "docs"
10
+ },
11
+ "effectiveAt": "2026-06-22T00:00:00.000Z"
12
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "repository.failed",
3
+ "reason": "provider rejected webhook setup",
4
+ "effectiveAt": "2026-06-22T00:10:00.000Z"
5
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "type": "site.created",
3
+ "effectiveAt": "2026-06-22T00:00:00.000Z",
4
+ "tenant": {
5
+ "tenantId": "acme-api",
6
+ "accountId": "acme",
7
+ "plan": "pro",
8
+ "paymentStatus": "active",
9
+ "subdomain": "acme-api",
10
+ "source": {
11
+ "type": "git",
12
+ "url": "ssh://git@git.example.com/acme/api-docs.git",
13
+ "ref": "main"
14
+ },
15
+ "webhookSecretRef": "secret:WEBHOOK_SECRET/acme-api"
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "site.removed",
3
+ "effectiveAt": "2026-06-22T00:00:00.000Z",
4
+ "tenantId": "acme-api"
5
+ }