@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.
- package/README.md +37 -3
- package/bin/pagenary.mjs +6 -0
- package/examples/blog-demo/.public/images/hero-1.svg +11 -0
- package/examples/blog-demo/.public/images/hero-2.svg +11 -0
- package/examples/blog-demo/.public/images/hero-3.svg +11 -0
- package/examples/blog-demo/config.json +28 -0
- package/examples/blog-demo/posts/accessible-by-default.md +84 -0
- package/examples/blog-demo/posts/designing-for-living-scroll.md +98 -0
- package/examples/blog-demo/posts/launching-pagenary-blogs.md +99 -0
- package/examples/managed-hosting-billing-cancel.json +4 -0
- package/examples/managed-hosting-billing-pro.json +6 -0
- package/examples/managed-hosting-domain-failed.json +6 -0
- package/examples/managed-hosting-domain-verified.json +5 -0
- package/examples/managed-hosting-gitea.yml +118 -0
- package/examples/managed-hosting-onboarding-pro.json +16 -0
- package/examples/managed-hosting-repo-connected.json +12 -0
- package/examples/managed-hosting-repo-failed.json +5 -0
- package/examples/managed-hosting-site-created.json +17 -0
- package/examples/managed-hosting-site-removed.json +5 -0
- package/examples/managed-hosting-webhook-installed.json +5 -0
- package/examples/managed-hosting-worker-failed.json +9 -0
- package/examples/managed-hosting-worker-publish.json +8 -0
- package/examples/managed-hosting.tenants.json +45 -0
- package/examples/page-effects/.public/images/aurora.svg +20 -0
- package/examples/page-effects/.public/images/ridge.svg +15 -0
- package/examples/page-effects/config.json +12 -0
- package/examples/page-effects/content/disclosure.md +40 -0
- package/examples/page-effects/content/index.md +123 -0
- package/examples/page-effects/content/parallax-and-sticky.md +76 -0
- package/examples/page-effects/content/scroll-snap.md +29 -0
- package/examples/page-effects/content/scrollytelling.md +35 -0
- package/examples/page-effects/manifest.json +35 -0
- package/examples/recipes.tenants.json +70 -0
- package/package.json +9 -3
- package/scripts/build-tenants.js +1293 -46
- package/scripts/check-accessibility-linter.js +123 -0
- package/scripts/check-accessibility-report.js +60 -0
- package/scripts/check-accessibility.js +191 -0
- package/scripts/check-media-renderers.js +86 -0
- package/scripts/check-narration.js +132 -0
- package/scripts/check-reading-metadata.js +92 -0
- package/scripts/lib/accessibility-linter.js +406 -0
- package/scripts/lib/accessibility-report.js +172 -0
- package/scripts/lib/collections-generator.js +11 -3
- package/scripts/lib/frontmatter.js +263 -20
- package/scripts/lib/managed-hosting.js +2025 -0
- package/scripts/managed-hosting.js +427 -0
- package/scripts/smoke-accessibility.mjs +460 -0
- package/scripts/smoke-browser.mjs +51 -4
- package/site/accessibility-report.json +80 -0
- package/site/accessibility-report.md +37 -0
- package/site/app.54e1ad90b733.js +1 -0
- package/site/app.js +1 -1
- package/site/assets/images/pipeline.e57f0dbfd05a.svg +33 -0
- package/site/assets/images/pipeline.svg +33 -0
- package/site/index.html +16 -13
- package/site/lib/blog-index.00956c25bdd1.js +1 -0
- package/site/lib/blog-index.js +1 -0
- package/site/lib/categories.af208377f073.js +1 -0
- package/site/lib/docs-map.6d60a86c4dae.js +1 -0
- package/site/lib/docs-map.js +1 -1
- package/site/lib/export.2db2c0bd974c.js +1 -0
- package/site/lib/export.js +1 -1
- package/site/lib/fortemi-corpus.6a5e3cc1c69e.js +1 -0
- package/site/lib/fortemi-corpus.js +1 -1
- package/site/lib/manifest-utils.5d2b43ebceab.js +1 -0
- package/site/lib/page-effects.2131b53bea6b.js +1 -0
- package/site/lib/page-effects.js +1 -0
- package/site/lib/router.f9d1cfba022d.js +1 -0
- package/site/lib/search.c52bcae8afda.js +1 -0
- package/site/lib/search.js +1 -1
- package/site/llms.txt +12 -1
- package/site/manifest.95224f06782d.js +209 -0
- package/site/manifest.js +80 -12
- package/site/media-init.16fde41d8850.js +1 -0
- package/site/media-init.js +1 -0
- package/site/mermaid-init.f25ee3b6ec1e.js +1 -0
- package/site/pages/accessible-authoring.html +245 -0
- package/site/pages/api.html +25 -4
- package/site/pages/architecture.html +11 -4
- package/site/pages/blog-layout.html +186 -0
- package/site/pages/deployment.html +26 -12
- package/site/pages/developer-guide.html +3 -2
- package/site/pages/extending.html +2 -2
- package/site/pages/overview.html +189 -0
- package/site/pages/page-effects.html +265 -0
- package/site/pages/publishing.html +258 -0
- package/site/pages/quickstart.html +2 -2
- package/site/pages/search-and-data.html +159 -0
- package/site/pages/seo-strategy.html +2 -2
- package/site/pages/showcase-gallery.html +184 -0
- package/site/pages/showcase-story.html +130 -0
- package/site/pages/tenant-config.html +133 -12
- package/site/pages/theming-recipes.html +56 -6
- package/site/pages/welcome.html +3 -2
- package/site/robots.txt +1 -1
- package/site/search-index/manifest.json +62 -25
- package/site/search-index/metadata.json +1548 -331
- package/site/search-index/part-0000.64586040c481.json +2772 -0
- package/site/search-index/part-0000.json +1560 -335
- package/site/sections/accessible-authoring.c7e5f50f256c.js +3 -0
- package/site/sections/accessible-authoring.js +3 -0
- package/site/sections/api.cd952afb3bf1.js +3 -0
- package/site/sections/api.js +1 -1
- package/site/sections/architecture.c98e76504bce.js +3 -0
- package/site/sections/architecture.js +1 -1
- package/site/sections/blog-layout.f34ad33cdde0.js +3 -0
- package/site/sections/blog-layout.js +3 -0
- package/site/sections/deployment.13d21771dd4c.js +3 -0
- package/site/sections/deployment.js +1 -1
- package/site/sections/developer-guide.0e330658f394.js +3 -0
- package/site/sections/developer-guide.js +1 -1
- package/site/sections/extending.7ae6dac6af72.js +3 -0
- package/site/sections/overview.5a483987fb9d.js +3 -0
- package/site/sections/overview.js +3 -0
- package/site/sections/page-effects.a38e5a7715d4.js +3 -0
- package/site/sections/page-effects.js +3 -0
- package/site/sections/publishing.32bf1d55b285.js +3 -0
- package/site/sections/publishing.js +3 -0
- package/site/sections/quickstart.fda6fa38d58d.js +3 -0
- package/site/sections/search-and-data.f929f5fdaac9.js +3 -0
- package/site/sections/search-and-data.js +3 -0
- package/site/sections/section-templates.ccdf4cf67f7b.js +1 -0
- package/site/sections/seo-strategy.8958aca48673.js +3 -0
- package/site/sections/showcase-gallery.cd729f94752d.js +3 -0
- package/site/sections/showcase-gallery.js +3 -0
- package/site/sections/showcase-story.79311d1302c8.js +3 -0
- package/site/sections/showcase-story.js +3 -0
- package/site/sections/tenant-config.552c0d8c6d2b.js +3 -0
- package/site/sections/tenant-config.js +1 -1
- package/site/sections/theming-recipes.5fa819c1767c.js +3 -0
- package/site/sections/theming-recipes.js +1 -1
- package/site/sections/welcome.8f10c9c4c4f0.js +3 -0
- package/site/sections/welcome.js +1 -1
- package/site/seo.90687a1d3d78.js +1 -0
- package/site/sitemap.xml +59 -11
- package/site/styles.bdb30ba34de5.css +3525 -0
- package/site/styles.css +1037 -94
- package/site/syntax-highlight.9d51f36b24da.js +1 -0
- package/site/vendor/fortemi-aiwg-index.5fb180ec6a20.js +1 -0
- package/src/app.js +329 -109
- package/src/index.html +12 -9
- package/src/lib/blog-index.js +113 -0
- package/src/lib/docs-map.js +224 -30
- package/src/lib/export.js +22 -15
- package/src/lib/fortemi-corpus.js +1 -1
- package/src/lib/page-effects.js +644 -0
- package/src/manifest.js +13 -0
- package/src/media-init.js +28 -0
- package/src/styles.css +1037 -94
- 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 (
|
|
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`,
|
|
51
|
-
`npx pagenary --help`). The package also
|
|
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,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,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
|
+
}
|