hono-decks 0.2.2 → 0.3.1
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.ja.md +2 -0
- package/README.md +2 -0
- package/package.json +9 -4
- package/skills/hono-decks/SKILL.md +170 -0
- package/skills/hono-decks/references/authoring-mdx.md +200 -0
- package/skills/hono-decks/references/browser-export.md +84 -0
- package/skills/hono-decks/references/custom-sources.md +95 -0
- package/skills/hono-decks/references/getting-started.md +123 -0
- package/skills/hono-decks/references/integrating.md +146 -0
- package/skills/hono-decks/references/ogp-generation.md +85 -0
- package/skills/hono-decks/references/r2-assets.md +77 -0
- package/skills/hono-decks/references/styling.md +151 -0
package/README.ja.md
CHANGED
|
@@ -12,6 +12,8 @@ bunx hono-decks init
|
|
|
12
12
|
bunx hono-decks compile
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
AI agent を使う場合は `npx @tanstack/intent@latest install` を実行してください。公開パッケージには、agent が `node_modules` から検出できるバージョン付きの hono-decks Skill が含まれます。
|
|
16
|
+
|
|
15
17
|
`init` は次の2ファイルを作ります。既存ファイルは上書きしません。
|
|
16
18
|
|
|
17
19
|
- `hono-decks.config.ts`: CLI と runtime が共有する唯一の設定
|
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ bunx hono-decks init
|
|
|
14
14
|
bunx hono-decks compile
|
|
15
15
|
~~~
|
|
16
16
|
|
|
17
|
+
If you use an AI agent, run `npx @tanstack/intent@latest install`. The published package includes a versioned hono-decks skill that the agent can discover from `node_modules`.
|
|
18
|
+
|
|
17
19
|
<code>init</code> creates two files without overwriting existing files:
|
|
18
20
|
|
|
19
21
|
- <code>hono-decks.config.ts</code>: the shared configuration for the CLI and runtime
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-decks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Slide decks for Hono and Cloudflare Workers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
7
7
|
"hono",
|
|
8
8
|
"mdx",
|
|
9
|
-
"slides"
|
|
9
|
+
"slides",
|
|
10
|
+
"tanstack-intent"
|
|
10
11
|
],
|
|
11
12
|
"homepage": "https://github.com/ts-76/hono-decks#readme",
|
|
12
13
|
"bugs": {
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
27
28
|
"README.md",
|
|
28
|
-
"README.ja.md"
|
|
29
|
+
"README.ja.md",
|
|
30
|
+
"skills"
|
|
29
31
|
],
|
|
30
32
|
"type": "module",
|
|
31
33
|
"sideEffects": false,
|
|
@@ -62,9 +64,11 @@
|
|
|
62
64
|
"build": "tsdown",
|
|
63
65
|
"prepack": "bun run build",
|
|
64
66
|
"decks:compile": "bun run build && node dist/bin.js compile",
|
|
67
|
+
"skills:validate": "intent validate skills",
|
|
68
|
+
"skills:stale": "intent stale .",
|
|
65
69
|
"typecheck": "bun run build && tsc --noEmit && tsc --noEmit -p tsconfig.worker.json",
|
|
66
70
|
"test": "bun run build && vp test run",
|
|
67
|
-
"check": "bun run typecheck && bun run test"
|
|
71
|
+
"check": "bun run typecheck && bun run test && bun run skills:validate"
|
|
68
72
|
},
|
|
69
73
|
"dependencies": {
|
|
70
74
|
"@mdx-js/mdx": "^3.1.1",
|
|
@@ -77,6 +81,7 @@
|
|
|
77
81
|
"unified": "^11.0.5"
|
|
78
82
|
},
|
|
79
83
|
"devDependencies": {
|
|
84
|
+
"@tanstack/intent": "^0.3.6",
|
|
80
85
|
"hono": "^4.12.30",
|
|
81
86
|
"vite": "catalog:",
|
|
82
87
|
"vite-plus": "catalog:",
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hono-decks
|
|
3
|
+
description: >
|
|
4
|
+
Create, style, integrate, and troubleshoot hono-decks presentations in Hono and Cloudflare Workers. Load for installation, deck.mdx and frontmatter authoring, Fire reveals, theme.css, generated createDecks modules, Hono routing, Vite or Wrangler builds, R2 assets, PDF or PNG export, OGP generation, embeds, or custom DeckSource pipelines.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# hono-decks
|
|
8
|
+
|
|
9
|
+
Use this skill as a router. Inspect the application and the request, then read only the relevant reference files before editing. Combine routes when a request spans topics.
|
|
10
|
+
|
|
11
|
+
## Route the request
|
|
12
|
+
|
|
13
|
+
| User intent | Read |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Install, initialize, first deck, compile failure | [Getting started](references/getting-started.md) |
|
|
16
|
+
| Write or fix `deck.mdx`, frontmatter, Markdown, MDX, Fire, embeds, components, notes | [Authoring MDX](references/authoring-mdx.md) |
|
|
17
|
+
| Create or improve `theme.css`, layout, typography, visual quality, overflow | [Styling](references/styling.md) |
|
|
18
|
+
| Add to an existing Hono, HonoX, Vite, Wrangler, or Worker app; configure routes, presenter, or external embed | [Integrating](references/integrating.md) |
|
|
19
|
+
| Serve deck images or other local assets from R2 | [R2 assets](references/r2-assets.md) plus [Integrating](references/integrating.md) |
|
|
20
|
+
| Generate PDF or PNG with Cloudflare Browser Rendering | [Browser export](references/browser-export.md) plus [Integrating](references/integrating.md) |
|
|
21
|
+
| Generate OGP/social images | [OGP generation](references/ogp-generation.md) |
|
|
22
|
+
| Use a manifest, database, remote store, custom `DeckSource`, or low-level router | [Custom sources](references/custom-sources.md) |
|
|
23
|
+
|
|
24
|
+
If the request is simply “make a deck,” read Authoring MDX and Styling. If it also asks to put the deck in an application, read Integrating. Do not load the advanced references unless the request or inspected code needs them.
|
|
25
|
+
|
|
26
|
+
## Non-negotiable architecture
|
|
27
|
+
|
|
28
|
+
1. Compile MDX with Node.js or Bun at build time.
|
|
29
|
+
2. Import generated TypeScript modules in the Worker runtime.
|
|
30
|
+
3. Keep `hono-decks.config.ts` as the single mount-path and build configuration.
|
|
31
|
+
4. Mount with `app.route(decks.mountPath, decks.router())` and obtain URLs from `decks.paths(slug)` or `meta.paths`.
|
|
32
|
+
5. Edit source decks, deck-local components, assets, and themes. Never hand-edit the generated output directory.
|
|
33
|
+
|
|
34
|
+
## Authoring and design defaults
|
|
35
|
+
|
|
36
|
+
- hono-decks is not Slidev. Never generate `v-click`, Slidev frontmatter, or UnoCSS utility classes unless the application independently installed and configured those features.
|
|
37
|
+
- Use supported `:::fire` blocks or block-level custom components with `fire`; reveals follow source order.
|
|
38
|
+
- Design for the fixed 1920 × 1080, 16:9 canvas. Responsive slide layouts are not required.
|
|
39
|
+
- Keep body text presentation-sized (the base root size is 32px), preserve strong contrast, and keep every slide inside the canvas.
|
|
40
|
+
- Prefer one idea per slide. Split dense content instead of shrinking text or relying on hidden overflow.
|
|
41
|
+
- Preserve print behavior and respect `prefers-reduced-motion` for custom animation.
|
|
42
|
+
|
|
43
|
+
## Common mistakes
|
|
44
|
+
|
|
45
|
+
### Editing generated modules
|
|
46
|
+
|
|
47
|
+
Wrong:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/generated/decks.ts
|
|
51
|
+
// Hand-edit the generated manifest or routes.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Correct: edit `decks/product/deck.mdx`, `theme.css`, assets, or components, then run `bunx hono-decks compile`.
|
|
55
|
+
|
|
56
|
+
### Compiling inside the Worker
|
|
57
|
+
|
|
58
|
+
Wrong:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { compileDecks } from "hono-decks/node";
|
|
62
|
+
|
|
63
|
+
app.get("/decks", async (c) => c.json(await compileDecks({ cwd: "." })));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Correct:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { decks } from "./decks";
|
|
70
|
+
|
|
71
|
+
app.route(decks.mountPath, decks.router());
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Duplicating route strings
|
|
75
|
+
|
|
76
|
+
Wrong:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
app.route("/slides", decks.router());
|
|
80
|
+
const presenter = `/decks/${slug}/presenter`;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Correct:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
app.route(decks.mountPath, decks.router());
|
|
87
|
+
const presenter = decks.paths(slug).presenter;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Passing executable values to client islands
|
|
91
|
+
|
|
92
|
+
Wrong:
|
|
93
|
+
|
|
94
|
+
```mdx
|
|
95
|
+
<Counter onChange={() => save()} startedAt={new Date()} />
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Correct:
|
|
99
|
+
|
|
100
|
+
```mdx
|
|
101
|
+
<Counter label="Votes" initial={3} />
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Keep event handlers, dates, class instances, and component state inside `components/client/index.tsx`; island props must be JSON-serializable.
|
|
105
|
+
|
|
106
|
+
### Using unsupported Fire forms
|
|
107
|
+
|
|
108
|
+
Wrong:
|
|
109
|
+
|
|
110
|
+
```mdx
|
|
111
|
+
<p fire="fade-up">Reveal me</p>
|
|
112
|
+
<Fire order={2}>Later</Fire>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Correct:
|
|
116
|
+
|
|
117
|
+
```mdx
|
|
118
|
+
:::fire{effect="fade-up"}
|
|
119
|
+
Reveal me
|
|
120
|
+
:::
|
|
121
|
+
|
|
122
|
+
<Chart fire="scale" at="+1" />
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Treating iframe embedding as CORS
|
|
126
|
+
|
|
127
|
+
Wrong: add `Access-Control-Allow-Origin` and leave `router.embed` without a parent policy.
|
|
128
|
+
|
|
129
|
+
Correct:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
router: {
|
|
133
|
+
embed: { frameAncestors: ["https://blog.example.com"] },
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Iframe navigation uses CSP `frame-ancestors`, not CORS.
|
|
138
|
+
|
|
139
|
+
### Publishing unprotected browser exports
|
|
140
|
+
|
|
141
|
+
Wrong:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
export: {
|
|
145
|
+
browser: ({ c }) => c.env.BROWSER,
|
|
146
|
+
pdf: true,
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Correct: add an `authorize` resolver backed by a Wrangler secret. Export authorization defaults to allowed when the resolver is absent.
|
|
151
|
+
|
|
152
|
+
### Generating Slidev or UnoCSS syntax
|
|
153
|
+
|
|
154
|
+
Wrong:
|
|
155
|
+
|
|
156
|
+
```mdx
|
|
157
|
+
<div v-click class="grid grid-cols-2 gap-4">Content</div>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Correct: use `:::fire` and define project-specific classes explicitly in `theme.css`.
|
|
161
|
+
|
|
162
|
+
## Verification
|
|
163
|
+
|
|
164
|
+
After changes:
|
|
165
|
+
|
|
166
|
+
1. Run `hono-decks compile` through the application's package manager.
|
|
167
|
+
2. Run the application's typecheck and tests.
|
|
168
|
+
3. Open the viewer and check every slide at 16:9 for readable text and overflow.
|
|
169
|
+
4. Check presentation, presenter, print, embed, export, or OGP routes that the change affects.
|
|
170
|
+
5. Verify production policies for drafts, presenter access, CSP `frame-ancestors`, and export authorization.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Authoring MDX
|
|
2
|
+
|
|
3
|
+
Use this route when writing or correcting a `deck.mdx` file. Start with the basic syntax, then load the advanced sections only when the requested deck needs them.
|
|
4
|
+
|
|
5
|
+
## Basic deck structure
|
|
6
|
+
|
|
7
|
+
The first frontmatter block describes the deck. Each following `---` boundary starts a slide; a slide may immediately include its own frontmatter.
|
|
8
|
+
|
|
9
|
+
```mdx
|
|
10
|
+
---
|
|
11
|
+
title: Product update
|
|
12
|
+
description: What changed and why it matters
|
|
13
|
+
author: Product team
|
|
14
|
+
date: 2026-07-17
|
|
15
|
+
tags: [product, engineering]
|
|
16
|
+
theme: default
|
|
17
|
+
transition: fade
|
|
18
|
+
transitionDuration: 300ms
|
|
19
|
+
transitionEasing: ease-out
|
|
20
|
+
draft: false
|
|
21
|
+
presenter: true
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
title: Opening
|
|
26
|
+
layout: cover
|
|
27
|
+
class: opening-slide
|
|
28
|
+
background: "#0b1020"
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Product update
|
|
32
|
+
|
|
33
|
+
One clear sentence that frames the talk.
|
|
34
|
+
|
|
35
|
+
{/* Introduce the customer problem before the feature. */}
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
title: Outcomes
|
|
39
|
+
layout: default
|
|
40
|
+
transition: slide-left
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Outcomes
|
|
44
|
+
|
|
45
|
+
- Faster setup
|
|
46
|
+
- Fewer production surprises
|
|
47
|
+
- Clearer ownership
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Deck frontmatter supports `title`, `description`, `author`, `date`, `tags`, `theme`, `transition`, `transitionDuration`, `transitionEasing`, `assets`, `draft`, and `presenter`.
|
|
51
|
+
|
|
52
|
+
Slide frontmatter supports `title`, `layout`, `class`, `notes`, `background`, `transition`, `transitionDuration`, and `transitionEasing`. Unknown keys are preserved in `meta` and produce a compile warning.
|
|
53
|
+
|
|
54
|
+
Supported transitions are `none`, `fade`, `fade-out`, `slide-left`, `slide-right`, `slide-up`, `slide-down`, and `view-transition`.
|
|
55
|
+
|
|
56
|
+
## Markdown and MDX basics
|
|
57
|
+
|
|
58
|
+
Use headings, paragraphs, emphasis, links, images, blockquotes, ordered and unordered lists, task lists, tables, inline code, and fenced code. GFM is enabled.
|
|
59
|
+
|
|
60
|
+
~~~mdx
|
|
61
|
+
## Release checklist
|
|
62
|
+
|
|
63
|
+
- [x] Compile the deck
|
|
64
|
+
- [ ] Verify the print route
|
|
65
|
+
|
|
66
|
+
| Surface | Path |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Viewer | `/decks/product` |
|
|
69
|
+
| Presenter | `/decks/product/presenter` |
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
app.route(decks.mountPath, decks.router())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
~~~
|
|
77
|
+
|
|
78
|
+
Local image paths are rewritten during compilation. Put deck-local files under `assets/` and use relative paths.
|
|
79
|
+
|
|
80
|
+
MDX exports, expressions, and JSX are supported:
|
|
81
|
+
|
|
82
|
+
```mdx
|
|
83
|
+
export const release = "2026.07"
|
|
84
|
+
export const metrics = ["42% faster", "3 fewer steps"]
|
|
85
|
+
|
|
86
|
+
## Release {release}
|
|
87
|
+
|
|
88
|
+
{metrics.map((metric) => <Badge label={metric} />)}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Speaker notes
|
|
92
|
+
|
|
93
|
+
Use the slide `notes` frontmatter field or MDX comments. Comments are removed from visible slide output and collected as presenter notes.
|
|
94
|
+
|
|
95
|
+
```mdx
|
|
96
|
+
---
|
|
97
|
+
notes: |
|
|
98
|
+
Pause after the headline.
|
|
99
|
+
Explain the build/runtime boundary.
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Compile once, serve everywhere
|
|
103
|
+
|
|
104
|
+
{/* Open the presenter route before the live demo. */}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Built-in components
|
|
108
|
+
|
|
109
|
+
- `<Hero>`: `title`, `subtitle` or `description`, `eyebrow`, `image` or `src`, and `featured`
|
|
110
|
+
- `<CodeBlock>`: `lang`, `filename` or `title`, and `highlight`
|
|
111
|
+
- `<EmbedFrame>`: iframe with sandbox, fallback, and print behavior
|
|
112
|
+
- `<SocialEmbed>`, `<TweetEmbed>`, and `<LinkCard>`: social and link surfaces
|
|
113
|
+
- `<Fire>`: explicit reveal wrapper; prefer the Markdown directive for ordinary prose
|
|
114
|
+
|
|
115
|
+
```mdx
|
|
116
|
+
<Hero
|
|
117
|
+
eyebrow="Architecture"
|
|
118
|
+
title="Build-time MDX"
|
|
119
|
+
subtitle="Generated modules run on the Worker"
|
|
120
|
+
image="./assets/pipeline.svg"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<CodeBlock lang="ts" filename="src/index.ts" highlight="4">
|
|
124
|
+
import { Hono } from "hono";
|
|
125
|
+
import { decks } from "./decks";
|
|
126
|
+
|
|
127
|
+
const app = new Hono();
|
|
128
|
+
app.route(decks.mountPath, decks.router());
|
|
129
|
+
</CodeBlock>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Fire reveals
|
|
133
|
+
|
|
134
|
+
Markdown content uses a container directive:
|
|
135
|
+
|
|
136
|
+
```mdx
|
|
137
|
+
:::fire{effect="fade-up"}
|
|
138
|
+
This appears on the next reveal step.
|
|
139
|
+
:::
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Lists can reveal items in source order:
|
|
143
|
+
|
|
144
|
+
```mdx
|
|
145
|
+
:::fire{each="item" depth="2" every="1"}
|
|
146
|
+
- First point
|
|
147
|
+
- Nested detail
|
|
148
|
+
- Second point
|
|
149
|
+
:::
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Block-level custom components may use `fire` and `at`:
|
|
153
|
+
|
|
154
|
+
```mdx
|
|
155
|
+
<MetricCard fire="scale" at="+1" label="Activation" value="42%" />
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`at` accepts a non-negative integer or a relative value such as `+1`. Do not use `$fire`, `order`, `v-click`, or `fire` on HTML elements or inline custom components.
|
|
159
|
+
|
|
160
|
+
## Embeds and link cards
|
|
161
|
+
|
|
162
|
+
```mdx
|
|
163
|
+
@[youtube](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
|
|
164
|
+
|
|
165
|
+
@[x](https://x.com/honojs/status/1659577874821836801)
|
|
166
|
+
|
|
167
|
+
@[card](https://hono.dev/)
|
|
168
|
+
|
|
169
|
+
@[embed](https://example.com/embed/status)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`@[card]` metadata is resolved at compile time when available. `build.ogpCacheFile` stores that external metadata cache; it is not the deck's social share image.
|
|
173
|
+
|
|
174
|
+
## Deck-local components
|
|
175
|
+
|
|
176
|
+
Use this structure:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
decks/product/
|
|
180
|
+
deck.mdx
|
|
181
|
+
theme.css
|
|
182
|
+
assets/
|
|
183
|
+
diagram.svg
|
|
184
|
+
components/
|
|
185
|
+
index.tsx
|
|
186
|
+
client/
|
|
187
|
+
index.tsx
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`components/index.tsx` contains server-rendered Hono JSX components. Mark a component as client-enabled in the server registry and export its browser implementation with the same name from `components/client/index.tsx`.
|
|
191
|
+
|
|
192
|
+
Client props may contain only strings, finite numbers, booleans, `null`, arrays, and plain objects composed from those values. Keep callbacks and interactive state in the client component.
|
|
193
|
+
|
|
194
|
+
## Authoring quality check
|
|
195
|
+
|
|
196
|
+
- Use a short title and one primary idea per slide.
|
|
197
|
+
- Keep visible body copy sparse enough for a 32px base size.
|
|
198
|
+
- Split long lists, large tables, and multi-screen code into multiple slides.
|
|
199
|
+
- Add useful alt text to images and titles to embeds.
|
|
200
|
+
- Compile after syntax changes and resolve all warnings instead of hiding them.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# PDF and PNG browser export
|
|
2
|
+
|
|
3
|
+
Use this route only when the request explicitly asks for server-side PDF or PNG generation with Cloudflare Browser Rendering.
|
|
4
|
+
|
|
5
|
+
Browser export renders the deck print route. The export endpoint is a billable production capability, so always configure authorization.
|
|
6
|
+
|
|
7
|
+
## Configure bindings and secrets
|
|
8
|
+
|
|
9
|
+
Declare the Browser Rendering binding in the application's Wrangler config and store the token as a secret:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
wrangler secret put DECK_EXPORT_TOKEN
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use the binding shape generated by the application's current Wrangler version. Regenerate types with `wrangler types`.
|
|
16
|
+
|
|
17
|
+
## Configure export
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// hono-decks.config.ts
|
|
21
|
+
import {
|
|
22
|
+
defineDecksConfig,
|
|
23
|
+
type DeckBrowserRunBinding,
|
|
24
|
+
} from "hono-decks";
|
|
25
|
+
|
|
26
|
+
interface Env {
|
|
27
|
+
Bindings: {
|
|
28
|
+
BROWSER?: DeckBrowserRunBinding;
|
|
29
|
+
DECK_EXPORT_TOKEN?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default defineDecksConfig<Env>({
|
|
34
|
+
mountPath: "/decks",
|
|
35
|
+
build: {
|
|
36
|
+
root: "decks",
|
|
37
|
+
outDir: "src/generated",
|
|
38
|
+
},
|
|
39
|
+
router: {
|
|
40
|
+
export: {
|
|
41
|
+
authorize: ({ c }) => {
|
|
42
|
+
const token = c.env.DECK_EXPORT_TOKEN;
|
|
43
|
+
const authorization = c.req.header("authorization");
|
|
44
|
+
const provided = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
|
|
45
|
+
return typeof token === "string" && token.length > 0 && provided === token;
|
|
46
|
+
},
|
|
47
|
+
browser: ({ c }) => c.env.BROWSER,
|
|
48
|
+
pdf: {
|
|
49
|
+
filename: (deck) => `${deck.slug}-slides`,
|
|
50
|
+
},
|
|
51
|
+
png: {
|
|
52
|
+
filename: (deck) => `${deck.slug}-slides`,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The endpoints are available from `decks.paths(slug).exportPdf` and `decks.paths(slug).exportPng`. Export controls are shown only when that request is authorized.
|
|
60
|
+
|
|
61
|
+
## Request an export
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl \
|
|
65
|
+
-H "Authorization: Bearer $DECK_EXPORT_TOKEN" \
|
|
66
|
+
-o product.pdf \
|
|
67
|
+
https://slides.example.com/decks/product/export.pdf
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use an environment variable supplied by the caller; never commit a token or put it in Wrangler `vars`.
|
|
71
|
+
|
|
72
|
+
## CSP and network checks
|
|
73
|
+
|
|
74
|
+
Browser Rendering must be able to load the absolute print URL and every font, image, stylesheet, and embedded fallback it needs. Review CSP and authentication middleware on the print route. Interactive iframes are replaced by print fallbacks, so provide useful fallback links or posters.
|
|
75
|
+
|
|
76
|
+
## Verification
|
|
77
|
+
|
|
78
|
+
- no token, empty token, and incorrect token return 403
|
|
79
|
+
- missing Browser binding returns 503 without exposing internals
|
|
80
|
+
- authorized PDF has `application/pdf` and a safe attachment filename
|
|
81
|
+
- authorized PNG has `image/png` and a safe attachment filename
|
|
82
|
+
- draft decks remain unavailable outside development
|
|
83
|
+
- every slide appears in print order with backgrounds and local assets
|
|
84
|
+
- exported pages contain no clipped content; fix the deck rather than increasing the browser viewport to hide overflow
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Custom sources and low-level APIs
|
|
2
|
+
|
|
3
|
+
Use this route only when the request needs an existing manifest, database, remote object store, custom `DeckSource`, or a router pipeline that the generated `createDecks(config)` kit cannot express.
|
|
4
|
+
|
|
5
|
+
For normal file-based decks, stay with `hono-decks init`, generated modules, and `createDecks(config)`.
|
|
6
|
+
|
|
7
|
+
## Custom DeckSource
|
|
8
|
+
|
|
9
|
+
A source lists deck summaries, returns compiled decks, and optionally serves assets:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import type {
|
|
13
|
+
CompiledDeck,
|
|
14
|
+
DeckEntry,
|
|
15
|
+
DeckSource,
|
|
16
|
+
} from "hono-decks";
|
|
17
|
+
|
|
18
|
+
export function databaseDeckSource(input: {
|
|
19
|
+
list(): Promise<DeckEntry[]>;
|
|
20
|
+
find(slug: string): Promise<CompiledDeck | null>;
|
|
21
|
+
}): DeckSource {
|
|
22
|
+
return {
|
|
23
|
+
async listDecks() {
|
|
24
|
+
return input.list();
|
|
25
|
+
},
|
|
26
|
+
async getCompiledDeck(_c, slug) {
|
|
27
|
+
return input.find(slug);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Compiled data must satisfy the public `CompiledDeck` model. Do not send raw MDX to the Worker and invoke the Node compiler per request.
|
|
34
|
+
|
|
35
|
+
## Decorate generated data
|
|
36
|
+
|
|
37
|
+
If only asset lookup, visibility, metadata, or caching changes, prefer `config.source(source)` over replacing the entire generated flow:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { defineDecksConfig, type DeckSource } from "hono-decks";
|
|
41
|
+
|
|
42
|
+
export default defineDecksConfig({
|
|
43
|
+
mountPath: "/decks",
|
|
44
|
+
source(source: DeckSource): DeckSource {
|
|
45
|
+
return {
|
|
46
|
+
async listDecks(c) {
|
|
47
|
+
return (await source.listDecks(c)).filter((deck) => !deck.draft);
|
|
48
|
+
},
|
|
49
|
+
getCompiledDeck: (c, slug) => source.getCompiledDeck(c, slug),
|
|
50
|
+
getAsset: (c, slug, assetPath) => source.getAsset?.(c, slug, assetPath) ?? Promise.resolve(null),
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Assemble a low-level router
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Hono } from "hono";
|
|
60
|
+
import {
|
|
61
|
+
decksRouter,
|
|
62
|
+
manifestDeckSource,
|
|
63
|
+
type DeckManifest,
|
|
64
|
+
} from "hono-decks/advanced";
|
|
65
|
+
|
|
66
|
+
export function createInternalSlides(manifest: DeckManifest) {
|
|
67
|
+
const app = new Hono();
|
|
68
|
+
const source = manifestDeckSource(manifest);
|
|
69
|
+
|
|
70
|
+
app.route(
|
|
71
|
+
"/internal-slides",
|
|
72
|
+
decksRouter({
|
|
73
|
+
source,
|
|
74
|
+
dev: false,
|
|
75
|
+
presenter: false,
|
|
76
|
+
embed: false,
|
|
77
|
+
export: false,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return app;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
At this level the application owns the mount path, option merging, source policy, draft behavior, and security. Use `createDeckPaths(mountPath, slug)` whenever low-level code needs a route map.
|
|
86
|
+
|
|
87
|
+
## Boundary checks
|
|
88
|
+
|
|
89
|
+
- compile or validate raw MDX before deployment, outside the Worker
|
|
90
|
+
- return 404 for missing or unauthorized decks without leaking their existence
|
|
91
|
+
- keep asset paths normalized and prevent traversal into unrelated objects
|
|
92
|
+
- keep `listDecks` and `getCompiledDeck` visibility policies consistent
|
|
93
|
+
- preserve JSON-serializable client-island props in custom compiled data
|
|
94
|
+
- explicitly configure presenter, embed CSP, and export authorization when enabling those surfaces
|
|
95
|
+
- add contract tests for every custom source method and route surface
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Use this route for installation, initialization, a first deck, or a failed initial compile.
|
|
4
|
+
|
|
5
|
+
## Inspect before changing
|
|
6
|
+
|
|
7
|
+
Identify the package manager, application entry point, Hono environment type, build tool, Wrangler config format, and existing Vite plugins. Preserve the application's established structure and scripts.
|
|
8
|
+
|
|
9
|
+
## Install and initialize
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add hono hono-decks
|
|
13
|
+
bunx hono-decks init
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`init` creates these files without overwriting existing files:
|
|
17
|
+
|
|
18
|
+
- `hono-decks.config.ts`: shared build and runtime configuration
|
|
19
|
+
- `src/decks.ts`: editable facade around generated modules
|
|
20
|
+
|
|
21
|
+
Use the equivalent `npm`, `pnpm`, or Yarn commands when the repository does not use Bun.
|
|
22
|
+
|
|
23
|
+
## Configure once
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// hono-decks.config.ts
|
|
27
|
+
import { defineDecksConfig } from "hono-decks";
|
|
28
|
+
|
|
29
|
+
export default defineDecksConfig({
|
|
30
|
+
mountPath: "/decks",
|
|
31
|
+
build: {
|
|
32
|
+
root: "decks",
|
|
33
|
+
outDir: "src/generated",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// src/decks.ts
|
|
40
|
+
import config from "../hono-decks.config";
|
|
41
|
+
import { createDecks } from "./generated/decks";
|
|
42
|
+
|
|
43
|
+
export const decks = createDecks(config);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Create `decks/welcome/deck.mdx`:
|
|
47
|
+
|
|
48
|
+
```mdx
|
|
49
|
+
---
|
|
50
|
+
title: Welcome
|
|
51
|
+
description: My first hono-decks presentation
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
layout: cover
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Welcome
|
|
59
|
+
|
|
60
|
+
MDX slides served by Hono.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
layout: default
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Build-time compilation
|
|
67
|
+
|
|
68
|
+
- Author in `decks/`
|
|
69
|
+
- Compile to `src/generated/`
|
|
70
|
+
- Serve generated modules from Hono
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Compile and mount it:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bunx hono-decks compile
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/index.ts
|
|
81
|
+
import { Hono } from "hono";
|
|
82
|
+
import { decks } from "./decks";
|
|
83
|
+
|
|
84
|
+
const app = new Hono();
|
|
85
|
+
|
|
86
|
+
app.get("/", (c) => c.redirect(decks.paths("welcome").viewer));
|
|
87
|
+
app.route(decks.mountPath, decks.router());
|
|
88
|
+
|
|
89
|
+
export default app;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Add compilation to development
|
|
93
|
+
|
|
94
|
+
For a Wrangler Worker, use the existing JSONC config:
|
|
95
|
+
|
|
96
|
+
```jsonc
|
|
97
|
+
{
|
|
98
|
+
"build": {
|
|
99
|
+
"command": "hono-decks compile",
|
|
100
|
+
"watch_dir": ["decks"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For Vite or HonoX, add the plugin to the existing plugin array:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { honoDecks } from "hono-decks/vite";
|
|
109
|
+
import { defineConfig } from "vite";
|
|
110
|
+
|
|
111
|
+
export default defineConfig({
|
|
112
|
+
plugins: [honoDecks()],
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Diagnose setup failures
|
|
117
|
+
|
|
118
|
+
- Missing config: run `hono-decks init` or create a default-exported `hono-decks.config.ts`.
|
|
119
|
+
- Missing generated import: run `hono-decks compile` before typechecking the application.
|
|
120
|
+
- Deck not discovered: use `decks/<slug>/deck.mdx` or a supported single-file deck in the configured root.
|
|
121
|
+
- Broken routes or assets: remove duplicated strings and use `decks.mountPath` and `decks.paths(slug)`.
|
|
122
|
+
- Worker bundling Node modules: import `hono-decks/node` only from build scripts, never from Worker runtime code.
|
|
123
|
+
- Lost edits after compile: move changes out of `src/generated` and into source decks or configuration.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Integrating with an application
|
|
2
|
+
|
|
3
|
+
Use this route when adding hono-decks to an existing Hono, HonoX, Vite, Wrangler, or Cloudflare Workers application, or when configuring application routes, presenter access, or external embedding.
|
|
4
|
+
|
|
5
|
+
## Preserve the build/runtime boundary
|
|
6
|
+
|
|
7
|
+
The compiler and `hono-decks/node` use Node filesystem tooling. Run them in a build command. Worker runtime code imports the generated facade and root package only.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/decks.ts
|
|
11
|
+
import config from "../hono-decks.config";
|
|
12
|
+
import { createDecks } from "./generated/decks";
|
|
13
|
+
|
|
14
|
+
export const decks = createDecks(config);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// src/index.ts
|
|
19
|
+
import { Hono } from "hono";
|
|
20
|
+
import { decks } from "./decks";
|
|
21
|
+
|
|
22
|
+
const app = new Hono();
|
|
23
|
+
|
|
24
|
+
app.route(decks.mountPath, decks.router());
|
|
25
|
+
|
|
26
|
+
export default app;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use the configured kit
|
|
30
|
+
|
|
31
|
+
- `decks.mountPath`: normalized path for `app.route`
|
|
32
|
+
- `decks.source`: configured `DeckSource`
|
|
33
|
+
- `decks.router(overrides?)`: built-in viewer, renderer, presenter, embed, export, and asset routes
|
|
34
|
+
- `decks.context(overrides?)`: middleware for application-owned deck routes
|
|
35
|
+
- `decks.paths(slug)`: viewer, render, print, presentation, presenter, embed, PDF, PNG, OGP, and asset paths
|
|
36
|
+
|
|
37
|
+
Never rebuild these paths with string concatenation.
|
|
38
|
+
|
|
39
|
+
## Existing Wrangler application
|
|
40
|
+
|
|
41
|
+
Merge this into the existing `wrangler.jsonc`:
|
|
42
|
+
|
|
43
|
+
```jsonc
|
|
44
|
+
{
|
|
45
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
46
|
+
"main": "src/index.ts",
|
|
47
|
+
"compatibility_date": "2026-07-14",
|
|
48
|
+
"build": {
|
|
49
|
+
"command": "hono-decks compile",
|
|
50
|
+
"watch_dir": ["decks"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Keep other bindings, assets, routes, and compatibility flags intact. Run `wrangler types` after changing bindings.
|
|
56
|
+
|
|
57
|
+
## Existing Vite or HonoX application
|
|
58
|
+
|
|
59
|
+
Add the plugin to the existing config rather than replacing other plugins:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { honoDecks } from "hono-decks/vite";
|
|
63
|
+
import { defineConfig } from "vite";
|
|
64
|
+
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
plugins: [honoDecks()],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For HonoX, mount the router from a HonoX route module while keeping the generated facade outside the route tree. Follow the application's established catch-all route convention.
|
|
71
|
+
|
|
72
|
+
## Application-owned routes
|
|
73
|
+
|
|
74
|
+
Use `decks.context()` to share deck lookup, draft policy, paths, and viewer configuration:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { Hono } from "hono";
|
|
78
|
+
import type { DeckContextVariables } from "hono-decks";
|
|
79
|
+
import { decks } from "./decks";
|
|
80
|
+
|
|
81
|
+
const app = new Hono<{ Variables: DeckContextVariables }>();
|
|
82
|
+
|
|
83
|
+
app.get(
|
|
84
|
+
`${decks.mountPath}/:slug/about`,
|
|
85
|
+
decks.context(),
|
|
86
|
+
(c) => c.json({
|
|
87
|
+
title: c.var.deckMeta.title,
|
|
88
|
+
viewer: c.var.deckMeta.paths.viewer,
|
|
89
|
+
slides: c.var.deckToc,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
app.route(decks.mountPath, decks.router());
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Register more specific application routes before the mounted deck router.
|
|
97
|
+
|
|
98
|
+
## Presenter policy
|
|
99
|
+
|
|
100
|
+
Presenter view can contain notes and previews. Enable it intentionally:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
router: {
|
|
104
|
+
presenter: {
|
|
105
|
+
enabled: ({ dev }) => dev,
|
|
106
|
+
viewerControl: true,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For production, replace the development condition with application authentication or a trusted binding policy.
|
|
112
|
+
|
|
113
|
+
## External iframe embedding
|
|
114
|
+
|
|
115
|
+
Enable the embed route and explicitly list allowed parent origins:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
router: {
|
|
119
|
+
embed: {
|
|
120
|
+
frameAncestors: ["https://blog.example.com"],
|
|
121
|
+
robots: false,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<iframe
|
|
128
|
+
src="https://slides.example.com/decks/product/embed"
|
|
129
|
+
title="Product deck"
|
|
130
|
+
allow="fullscreen"
|
|
131
|
+
></iframe>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The response writes CSP `frame-ancestors` and removes conflicting `X-Frame-Options`. Invalid origins do not widen access. Do not use CORS headers to solve iframe permission.
|
|
135
|
+
|
|
136
|
+
Review any application-wide CSP middleware after mounting: it must preserve the embed response's `frame-ancestors` directive and allow the scripts, styles, images, frames, and fonts actually used by the deck.
|
|
137
|
+
|
|
138
|
+
## Integration verification
|
|
139
|
+
|
|
140
|
+
- Start the normal application dev command and confirm MDX changes recompile.
|
|
141
|
+
- Verify direct navigation and application links use `decks.paths`.
|
|
142
|
+
- Confirm draft decks are unavailable outside development.
|
|
143
|
+
- Test application-owned routes before the deck router catch-all.
|
|
144
|
+
- Check presenter notes are not exposed by an unintended production policy.
|
|
145
|
+
- Test the external embed from an allowed and a disallowed parent origin.
|
|
146
|
+
- Run the deployment dry run so Worker-only bundling errors are caught before publish.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# OGP and social image generation
|
|
2
|
+
|
|
3
|
+
Use this route when the request explicitly asks to generate Open Graph or social sharing images.
|
|
4
|
+
|
|
5
|
+
hono-decks exposes OGP paths and metadata but intentionally does not bundle an image renderer. Generate 1200 × 630 PNGs at build time and serve them from Workers Static Assets or another application-owned asset pipeline.
|
|
6
|
+
|
|
7
|
+
## Enable viewer metadata
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// hono-decks.config.ts
|
|
11
|
+
import { defineDecksConfig } from "hono-decks";
|
|
12
|
+
|
|
13
|
+
export default defineDecksConfig({
|
|
14
|
+
mountPath: "/decks",
|
|
15
|
+
build: {
|
|
16
|
+
root: "decks",
|
|
17
|
+
outDir: "src/generated",
|
|
18
|
+
},
|
|
19
|
+
router: {
|
|
20
|
+
viewer: {
|
|
21
|
+
openGraph: true,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The default image URL is `decks.paths(slug).ogImage`. The viewer turns it into an absolute Open Graph and Twitter Card URL from the request origin.
|
|
28
|
+
|
|
29
|
+
## Build-time pipeline
|
|
30
|
+
|
|
31
|
+
Use `compileDecks()` from `hono-decks/node`, then generate one image for each non-draft manifest entry:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
35
|
+
import { dirname, join } from "node:path";
|
|
36
|
+
import { createDeckPaths } from "hono-decks";
|
|
37
|
+
import { compileDecks } from "hono-decks/node";
|
|
38
|
+
import config from "../hono-decks.config";
|
|
39
|
+
import { renderOgpCard } from "./ogp-card";
|
|
40
|
+
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
const manifest = await compileDecks({
|
|
43
|
+
cwd,
|
|
44
|
+
root: config.build?.root ?? "decks",
|
|
45
|
+
out: config.build?.outDir ?? "src/generated",
|
|
46
|
+
mountPath: config.mountPath,
|
|
47
|
+
ogpCacheFile: config.build?.ogpCacheFile,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const deck of manifest.decks) {
|
|
51
|
+
if (deck.meta.draft) continue;
|
|
52
|
+
const paths = createDeckPaths(config.mountPath, deck.slug);
|
|
53
|
+
const png = await renderOgpCard({
|
|
54
|
+
title: deck.meta.title ?? deck.slug,
|
|
55
|
+
description: deck.meta.description,
|
|
56
|
+
author: deck.meta.author,
|
|
57
|
+
path: paths.viewer,
|
|
58
|
+
});
|
|
59
|
+
const output = join(cwd, "public", paths.ogImage.replace(/^\//, ""));
|
|
60
|
+
await mkdir(dirname(output), { recursive: true });
|
|
61
|
+
await writeFile(output, png);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Implement `renderOgpCard` with application-selected tooling such as Satori plus resvg. Keep those dependencies in the application, not the hono-decks runtime bundle.
|
|
66
|
+
|
|
67
|
+
Satori accepts TTF, OTF, and WOFF fonts, but not WOFF2. Bundle fonts that cover every language used in deck metadata; builds should not depend on fetching remote fonts.
|
|
68
|
+
|
|
69
|
+
## Separate concepts
|
|
70
|
+
|
|
71
|
+
- `router.viewer.openGraph`: emits social metadata for the viewer
|
|
72
|
+
- `decks.paths(slug).ogImage`: canonical image output/serving path
|
|
73
|
+
- `build.ogpCacheFile`: caches external page metadata for `@[card](...)`; it does not generate the deck share image
|
|
74
|
+
|
|
75
|
+
## Verification
|
|
76
|
+
|
|
77
|
+
- generate only non-draft decks
|
|
78
|
+
- output exactly 1200 × 630 PNG files
|
|
79
|
+
- avoid rewriting unchanged bytes so local watchers do not loop
|
|
80
|
+
- verify the viewer emits absolute `og:image` and Twitter Card metadata
|
|
81
|
+
- request the generated image path from the deployed asset setup
|
|
82
|
+
- use an `imagePath` resolver when images live on a separate CDN
|
|
83
|
+
- ensure titles, descriptions, and fonts fit languages with wider glyphs
|
|
84
|
+
|
|
85
|
+
The repository's `examples/ogp` directory is the canonical complete recipe.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# R2-backed assets
|
|
2
|
+
|
|
3
|
+
Use this route only when the request explicitly asks to serve deck-local images or other assets from Cloudflare R2.
|
|
4
|
+
|
|
5
|
+
Compilation still discovers and rewrites local asset URLs. `withR2Assets` decorates the generated source so requests prefer R2 and fall back to the original embedded asset source.
|
|
6
|
+
|
|
7
|
+
## Configure a binding
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
{
|
|
11
|
+
"r2_buckets": [
|
|
12
|
+
{
|
|
13
|
+
"binding": "DECK_ASSETS",
|
|
14
|
+
"bucket_name": "production-deck-assets"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Generate binding types with `wrangler types`.
|
|
21
|
+
|
|
22
|
+
## Decorate the source
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// hono-decks.config.ts
|
|
26
|
+
import {
|
|
27
|
+
defineDecksConfig,
|
|
28
|
+
withR2Assets,
|
|
29
|
+
type DeckSource,
|
|
30
|
+
type R2BucketLike,
|
|
31
|
+
} from "hono-decks";
|
|
32
|
+
|
|
33
|
+
interface Env {
|
|
34
|
+
Bindings: {
|
|
35
|
+
DECK_ASSETS?: R2BucketLike;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default defineDecksConfig<Env>({
|
|
40
|
+
mountPath: "/decks",
|
|
41
|
+
build: {
|
|
42
|
+
root: "decks",
|
|
43
|
+
outDir: "src/generated",
|
|
44
|
+
},
|
|
45
|
+
source(source: DeckSource<Env>) {
|
|
46
|
+
return withR2Assets(source, {
|
|
47
|
+
bucket: (c) => c.env.DECK_ASSETS,
|
|
48
|
+
keyPrefix: "slides",
|
|
49
|
+
cacheControl: "public, max-age=31536000, immutable",
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
By default the R2 key comes from the compiled asset's source path, optionally prefixed by `keyPrefix`. If the upload pipeline uses a different layout, provide a deterministic key resolver:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
source(source: DeckSource<Env>) {
|
|
59
|
+
return withR2Assets(source, {
|
|
60
|
+
bucket: (c) => c.env.DECK_ASSETS,
|
|
61
|
+
key: ({ slug, assetPath }) => `decks/${slug}/${assetPath}`,
|
|
62
|
+
cacheControl: ({ asset }) =>
|
|
63
|
+
asset.publicPath.includes("/versioned/")
|
|
64
|
+
? "public, max-age=31536000, immutable"
|
|
65
|
+
: "public, max-age=300",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Operational requirements
|
|
71
|
+
|
|
72
|
+
- Upload the same paths that the key resolver produces; `withR2Assets` only reads objects.
|
|
73
|
+
- Set correct R2 HTTP metadata, especially `contentType`. The decorator falls back to the compiled asset type when needed.
|
|
74
|
+
- Use immutable caching only for versioned or content-addressed keys.
|
|
75
|
+
- Keep the generated fallback if local development must work without an R2 binding.
|
|
76
|
+
- Test a present object, missing object, and missing binding. Missing R2 data should fall back to the underlying source.
|
|
77
|
+
- Do not replace slide image URLs with arbitrary public R2 URLs unless bypassing hono-decks asset routes is an explicit requirement.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Styling
|
|
2
|
+
|
|
3
|
+
Use this route for deck-local `theme.css`, layout decisions, typography, visual hierarchy, animation, and overflow correction.
|
|
4
|
+
|
|
5
|
+
## Canvas contract
|
|
6
|
+
|
|
7
|
+
hono-decks renders a fixed 1920 × 1080 design canvas with a 16:9 aspect ratio and scales it into the viewer. Do not build responsive slide breakpoints. Style the canvas itself, then verify the scaled result and print route.
|
|
8
|
+
|
|
9
|
+
The base root font size is 32px. Treat that as body copy, not a value to shrink globally. A practical hierarchy is:
|
|
10
|
+
|
|
11
|
+
- body: `1rem` (32px)
|
|
12
|
+
- supporting text: no smaller than `.75rem` (24px)
|
|
13
|
+
- slide title: `1.75rem` to `2.5rem`
|
|
14
|
+
- cover title: `3rem` to `5rem`
|
|
15
|
+
- line height: about `1.15` for headings and `1.35` to `1.5` for body text
|
|
16
|
+
|
|
17
|
+
## Start with deck-scoped CSS
|
|
18
|
+
|
|
19
|
+
Create `decks/product/theme.css`:
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
:root {
|
|
23
|
+
--hono-decks-color: #f7f8fb;
|
|
24
|
+
--hono-decks-muted-color: #c3c8d4;
|
|
25
|
+
--hono-decks-accent-color: #8bd3ff;
|
|
26
|
+
--hono-decks-border-color: rgba(255, 255, 255, 0.2);
|
|
27
|
+
--hono-decks-card-background: rgba(12, 18, 34, 0.82);
|
|
28
|
+
--hono-decks-inline-code-background: rgba(12, 18, 34, 0.72);
|
|
29
|
+
--hono-decks-code-background: #0a1020;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.layout-cover,
|
|
33
|
+
.layout-default,
|
|
34
|
+
.layout-statement,
|
|
35
|
+
.layout-comparison {
|
|
36
|
+
background: #080d1a;
|
|
37
|
+
color: var(--hono-decks-color);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.slide h1,
|
|
41
|
+
.slide h2 {
|
|
42
|
+
margin: 0 0 0.65em;
|
|
43
|
+
max-width: 18ch;
|
|
44
|
+
line-height: 1.08;
|
|
45
|
+
letter-spacing: -0.025em;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.slide p,
|
|
49
|
+
.slide li {
|
|
50
|
+
line-height: 1.42;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.comparison-grid {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
56
|
+
gap: 1.5rem;
|
|
57
|
+
align-items: stretch;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.comparison-card {
|
|
61
|
+
min-width: 0;
|
|
62
|
+
border: 1px solid var(--hono-decks-border-color);
|
|
63
|
+
border-radius: 20px;
|
|
64
|
+
background: var(--hono-decks-card-background);
|
|
65
|
+
padding: 1.25rem;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use explicit semantic classes in MDX:
|
|
70
|
+
|
|
71
|
+
```mdx
|
|
72
|
+
---
|
|
73
|
+
layout: comparison
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Build and runtime
|
|
77
|
+
|
|
78
|
+
<div class="comparison-grid">
|
|
79
|
+
<section class="comparison-card">
|
|
80
|
+
<h3>Build</h3>
|
|
81
|
+
<p>Compile MDX with Node.js or Bun.</p>
|
|
82
|
+
</section>
|
|
83
|
+
<section class="comparison-card">
|
|
84
|
+
<h3>Runtime</h3>
|
|
85
|
+
<p>Serve generated modules from Hono.</p>
|
|
86
|
+
</section>
|
|
87
|
+
</div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Do not assume UnoCSS or Tailwind utility classes exist.
|
|
91
|
+
|
|
92
|
+
## Readability and density rules
|
|
93
|
+
|
|
94
|
+
- Prefer 20–40 visible words for a typical slide; use more only when the layout is deliberately text-led.
|
|
95
|
+
- Keep lists to about 3–6 short items. Split a long sequence across slides.
|
|
96
|
+
- Use at most two primary columns. More columns usually force unreadable type.
|
|
97
|
+
- Keep code examples focused on the lines needed to explain the idea. Use `CodeBlock` or fenced code, both of which scroll instead of enlarging the canvas.
|
|
98
|
+
- Use images and diagrams at a size that can be interpreted from the back of a room; remove decorative detail before reducing text.
|
|
99
|
+
- Maintain strong foreground/background contrast and never communicate state by color alone.
|
|
100
|
+
|
|
101
|
+
## Prevent overflow
|
|
102
|
+
|
|
103
|
+
The slide intentionally uses `overflow: hidden`. Hidden content is a failure, not an invitation to add scrolling.
|
|
104
|
+
|
|
105
|
+
When content overflows, fix it in this order:
|
|
106
|
+
|
|
107
|
+
1. Remove repetition and shorten prose.
|
|
108
|
+
2. Split the content into two slides.
|
|
109
|
+
3. Replace a dense table with a focused comparison or chart.
|
|
110
|
+
4. Reduce gaps or decorative padding modestly.
|
|
111
|
+
5. Reduce a local type size only when it remains at least 24px.
|
|
112
|
+
|
|
113
|
+
Avoid global transforms, `zoom`, negative margins that pull content outside the safe area, and font sizes chosen only to make one crowded slide fit.
|
|
114
|
+
|
|
115
|
+
## Motion
|
|
116
|
+
|
|
117
|
+
Built-in slide transitions and Fire reveals already respect `prefers-reduced-motion`. Custom animation must do the same:
|
|
118
|
+
|
|
119
|
+
```css
|
|
120
|
+
.signal-dot {
|
|
121
|
+
animation: pulse 2.4s ease-in-out infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes pulse {
|
|
125
|
+
50% {
|
|
126
|
+
transform: scale(1.08);
|
|
127
|
+
opacity: 0.72;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@media (prefers-reduced-motion: reduce) {
|
|
132
|
+
.signal-dot {
|
|
133
|
+
animation: none;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Animation should clarify sequence or change. Do not animate every object or make critical information depend on motion.
|
|
139
|
+
|
|
140
|
+
## Visual QA
|
|
141
|
+
|
|
142
|
+
Inspect every slide in the viewer and check:
|
|
143
|
+
|
|
144
|
+
- no clipped headings, lists, code, images, or footers
|
|
145
|
+
- body text remains readable when the 1920 × 1080 canvas is scaled down
|
|
146
|
+
- titles and key claims dominate the hierarchy
|
|
147
|
+
- repeated layouts align consistently
|
|
148
|
+
- images retain aspect ratio and meaningful content is not cropped
|
|
149
|
+
- Fire states remain understandable at each reveal step
|
|
150
|
+
- print preview contains every slide and does not depend on live embeds
|
|
151
|
+
- custom motion has a reduced-motion fallback
|