English | 日本語
This monorepo contains hono-decks, a toolkit for serving MDX slide decks from Hono applications and Cloudflare Workers. The CLI compiles MDX into TypeScript modules, so Workers only load generated modules at runtime.
bun add hono hono-decks
bunx hono-decks init
bunx hono-decks compileinit creates hono-decks.config.ts and src/decks.ts.
// hono-decks.config.ts
import { defineDecksConfig } from "hono-decks";
export default defineDecksConfig({
mountPath: "/decks",
build: {
root: "decks",
outDir: "src/generated",
},
});// src/decks.ts
import config from "../hono-decks.config";
import { createDecks } from "./generated/decks";
export const decks = createDecks(config);// src/index.ts
import { Hono } from "hono";
import { decks } from "./decks";
const app = new Hono();
app.get("/", (c) => c.redirect(decks.paths("welcome").viewer));
app.route(decks.mountPath, decks.router());
export default app;Add decks/welcome/deck.mdx, then open /decks/welcome to view the deck.
hono-decks.config.ts is the single source of truth for the CLI and runtime. You do not need to specify separate mount paths for compiled asset URLs and app.route().
The generated module returns a configured kit containing the operations your application needs.
decks.mountPath;
decks.source;
decks.router();
decks.context();
decks.paths("welcome");decks.paths(slug) returns the following route map.
{
viewer,
render,
print,
presentation,
presenter,
embed,
exportPdf,
exportPng,
ogImage,
assets,
}Use this path map or DeckPageMeta.paths in custom viewers and routes instead of concatenating strings.
router: {
viewer: {
controls: {
after: ({ meta }) => [
{ type: "link", href: `${meta.paths.viewer}/about`, label: "Details" },
],
},
},
}Integrate generation into the existing dev command. For Cloudflare Workers, use Wrangler's custom build configuration.
This automatically recompiles decks when they change. Use wrangler dev --live-reload in the dev script to refresh the browser as well. For HonoX or Vite applications, add the plugin to the existing Vite config.
import { honoDecks } from "hono-decks/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [honoDecks()],
});The Vite plugin triggers a full reload after a successful compile. With either integration, users only need to run the existing bun run dev command. hono-decks compile --watch remains available as a lower-level option for custom tooling.
Use hono-decks compile --config path/to/config.ts only when the config file has a custom name. Put root and outDir under build, and put mountPath at the top level of the config.
Every resolver accepts a single object argument.
import {
defineDecksConfig,
type DeckBrowserRunBinding,
} from "hono-decks";
interface AppEnv {
Bindings: {
BROWSER?: DeckBrowserRunBinding;
DECK_EXPORT_TOKEN?: string;
};
}
export default defineDecksConfig<AppEnv>({
mountPath: "/decks",
build: { root: "decks", outDir: "src/generated" },
router: {
presenter: {
enabled: ({ dev }) => dev,
viewerControl: true,
},
export: {
authorize: ({ c }) =>
c.req.header("authorization") === `Bearer ${c.env.DECK_EXPORT_TOKEN}`,
browser: ({ c }) => c.env.BROWSER,
pdf: true,
png: true,
},
},
});When dev is omitted, hono-decks infers it from the NODE_ENV set by Vite or Wrangler. With the standard setup, vite and wrangler dev run in development mode, while production builds and wrangler deploy run in production mode. An explicit value such as dev: false, or a resolver, takes precedence. Environments that cannot be identified default to production mode.
decks.router(overrides) merges nested options while preserving the config. Set export: false, embed: false, or presenter: false to disable a feature explicitly.
import type { DeckContextVariables } from "hono-decks";
import { decks } from "./decks";
const app = new Hono<{ Variables: DeckContextVariables }>();
app.get(
`${decks.mountPath}/:slug/about`,
decks.context(),
(c) => c.html(renderDetails({
deck: c.var.deck,
meta: c.var.deckMeta,
toc: c.var.deckToc,
})),
);The configured middleware shares its source, mount path, and draft/development policy with the standard router.
decks/product/
deck.mdx
theme.css
assets/
architecture.svg
components/
index.tsx
client/
index.tsx
theme.css: deck-specific stylesassets/: local assets rewritten to public paths during compilationcomponents/index.tsx: server componentscomponents/client/index.tsx: island components hydrated in the browser
Enable external iframes explicitly with router.embed, and list every allowed embedding origin in frameAncestors.
router: {
embed: {
frameAncestors: ["https://blog.example.com"],
robots: false,
},
}For PDF and PNG exports, return the Cloudflare Browser Rendering binding from browser: ({ c }) => c.env.BROWSER. Export controls appear only for requests accepted by authorize.
In the viewer, Cmd + P or Ctrl + P opens the print route and includes every slide in the print job.
When router.viewer.openGraph is enabled, the viewer uses decks.paths(slug).ogImage to emit absolute Open Graph and Twitter Card image URLs. The core package does not include an image-generation library.
router: {
viewer: { openGraph: true },
}examples/ogp provides a recipe that installs Satori and resvg only in the example, generates 1200×630 PNG files from frontmatter at build time, and serves them through Workers Static Assets. It does not require a Browser Rendering binding. build.ogpCacheFile is an external metadata cache for LinkCards inside slides and is separate from this share-image generation.
hono-decks:defineDecksConfig, configured-kit types, customization, and deck authoringhono-decks/advanced: low-level APIs for assembling raw routers, sources, and renderershono-decks/client: client-island hydrationhono-decks/node: compiler and local-filesystem adaptershono-decks/vite: Vite integration for compiling and watching decks during developmenthono-decks/cli: programmatic CLI
Most applications should use the root entry and the generated createDecks(config) function.
import { decksRouter, manifestDeckSource } from "hono-decks/advanced";
const source = manifestDeckSource(manifest);
app.route("/internal", decksRouter({ source }));Use the advanced entry only when building a custom source or pipeline.
examples/minimal: minimal standalone Workerexamples/basic: R2 assets, Browser Rendering, custom pages, and client componentsexamples/honox: mounting the router in a HonoX routeexamples/ogp: browserless build-time OGP generation with Satoridocs: documentation site and embedded demo
Every example uses the same hono-decks.config.ts contract. The example scripts update generated modules before decks:compile, typecheck, test, and deploy.
Copy-ready Worker examples use JSONC Wrangler configs, a current compatibility date, nodejs_compat, and binding types generated by wrangler types. Store secrets with wrangler secret put, not in the config's vars section.
GitHub Actions publishes hono-decks to npm from Conventional Commits merged into main. The package metadata in packages/decks/package.json is kept at the latest published version, and the matching Git tag is the semantic-release baseline. During the 0.x series, feat produces a minor release, while fix and perf produce patch releases. A BREAKING CHANGE is reserved for the Ver1 boundary and produces the first major release, 1.0.0. CI runs bun run check for pull requests. On main, the release workflow runs the same checks and bun run smoke:package:compat before semantic-release.
To start the Ver1 release, the commit message analyzed on main must retain a Conventional Commits marker such as feat!: (or feat(scope)!:) or a BREAKING CHANGE: footer. When squash-merging, do not rely on the PR body alone: confirm the final squash commit message in the merge dialog contains the marker.
The current published baseline is hono-decks@0.5.0 at v0.5.0. scripts/verify-release-baseline.mjs checks that the tag corresponding to packages/decks/package.json exists in the checked-out history before publication. When the baseline is missing, the workflow performs validation and safely skips publication. The release workflow also runs bun run smoke:package:compat against the declared Vite 6, 7, and 8 peer range.
Before merging a Ver1 release commit into main, run the following manual browser and PDF checks. After the merge, GitHub Actions automatically runs bun run check and bun run smoke:package:compat; it does not currently run these browser/PDF checks. Both commands require the agent-browser Chromium binary, and PDF preview validation also needs Poppler or macOS Quick Look. See the basic example's local smoke checks for setup details.
bun run smoke:viewport
bun run smoke:pdfThe local smoke:pdf check renders the /print surface with a developer-local browser; it does not call /export.pdf or Cloudflare Browser Run. The production export route uses the BROWSER binding. To validate that deployed or remote-bound path, set an origin and export token, then run the credentialed smoke:
export HONO_DECKS_BROWSER_RUN_ORIGIN=https://your-worker.example.com
export HONO_DECKS_BROWSER_RUN_TOKEN=your-export-token
bun run smoke:browser-runThis credentialed check requires a deployed or remote-bound Worker and is not part of generic CI or release validation until such an environment is configured.
For repeatable credentialed validation, configure the protected browser-run-smoke environment with the HONO_DECKS_BROWSER_RUN_ORIGIN environment variable and HONO_DECKS_BROWSER_RUN_TOKEN secret, restrict it to main, and trigger the Browser Run smoke workflow manually. This workflow calls the real /export.pdf route and uploads the returned PDFs; it does not install or invoke a local browser.
The command block below mirrors the checks and release command that GitHub Actions runs after the merge; do not treat bun run release as part of the pre-merge browser/PDF gate.
bun install --frozen-lockfile
bun run check
bun run smoke:package:compat
bun run releaseFor a new baseline, first publish the exact version in packages/decks/package.json and create the matching annotated tag before enabling publication. npm publish requires an npm account login and 2FA. Configure GitHub Actions as a Trusted Publisher in the npm settings for the hono-decks package:
- Organization or user:
ts-76 - Repository:
hono-decks - Workflow filename:
release.yml
Releases use GitHub OIDC and provenance, so do not store an npm token in GitHub Secrets. Keep the package version, published npm version, and matching Git tag on the same release baseline.