website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


c852bcapyrossh 2026-07-08T18:13:50+05:30
docs: add HonoX + typed-htmx migration design spec
docs/superpowers/specs/2026-07-08-migrate-to-honox-typed-htmx-design.md ADDED
@@ -0,0 +1,371 @@
1
+ # pyrossh.dev — Migrate to HonoX + typed-htmx
2
+
3
+ **Goal:** Replace the current architecture of ~18 individual Cloudflare Workers + dev-router + assets worker with a single HonoX application using typed-htmx (via BYOR) and HTMX for interactivity. Keep the existing `packages/shared/` library packages unchanged.
4
+
5
+ ## Motivation
6
+
7
+ Current architecture is over-engineered for the site's complexity:
8
+
9
+ - 18 separate worker packages, each with its own `wrangler.jsonc`, `package.json`, and deployment
10
+ - A dev-router that imports all 18 workers for local development
11
+ - An assets worker that proxies non-CSS requests to an R2 bucket
12
+ - Manual route pattern matching (`/^\/posts\/(.+)$/`) instead of standard routing
13
+
14
+ A single HonoX app with file-based routing eliminates all of this while keeping the same typed-htmx components and HTMX interactivity.
15
+
16
+ ## Architecture
17
+
18
+ ### Before (current)
19
+
20
+ ```
21
+ packages/workers/
22
+ assets/ → CSS + R2 proxy worker
23
+ home/ → /
24
+ cv/ → /cv
25
+ posts/index/ → /posts
26
+ posts/detail/ → /posts/:id
27
+ repos/readme/ → /repos/:id
28
+ repos/commits/index/ → /repos/:id/commits
29
+ repos/commits/detail/ → /repos/:id/commits/:hash
30
+ repos/files/index/ → /repos/:id/files
31
+ repos/files/detail/ → /repos/:id/files/*
32
+ repos/files/history/ → /repos/:id/files/*/history
33
+ repos/files/blame/ → /repos/:id/files/*/blame
34
+ repos/issues/index/ → /repos/:id/issues
35
+ repos/issues/detail/ → /repos/:id/issues/:issueId
36
+ only-bible-app/index/ → /only-bible-app
37
+ only-bible-app/privacy/ → /only-bible-app/privacy-policy
38
+ only-bible-app/terms/ → /only-bible-app/terms-and-conditions
39
+ robots/ → /robots.txt
40
+ rss/ → /rss.xml
41
+ not-found/ → 404
42
+ server-error/ → 500
43
+ dev-router/ → imports all above for wrangler dev
44
+ ```
45
+
46
+ Each worker is its own npm package (`@pyrossh/home`, `@pyrossh/cv`, etc.), bundled and deployed separately. Each has its own `wrangler.jsonc` with individual route patterns and Cloudflare deployment.
47
+
48
+ ### After
49
+
50
+ ```
51
+ pyrossh.dev/
52
+ app/
53
+ global.d.ts # HonoX type declarations
54
+ server.ts # createApp() entry point
55
+ client.ts # Client entry (for HonoX, minimal)
56
+ routes/
57
+ _renderer.ts # typed-htmx renderer bridge
58
+ _404.tsx # Not found
59
+ _error.tsx # Error page
60
+ index.tsx # Home (/)
61
+ cv.tsx # CV (/cv)
62
+ posts/index.tsx # Blog list (/posts)
63
+ posts/[id].tsx # Blog post (/posts/:id)
64
+ repos/[id].tsx # Repo README (/repos/:id)
65
+ repos/[id]/commits.tsx # Commits list
66
+ repos/[id]/commits/[hash].tsx # Commit detail
67
+ repos/[id]/files.tsx # File tree
68
+ repos/[id]/files/[...path].tsx # File view
69
+ repos/[id]/files/[...path]/history.tsx # File history
70
+ repos/[id]/files/[...path]/blame.tsx # File blame
71
+ repos/[id]/issues.tsx # Issues list
72
+ repos/[id]/issues/[issueId].tsx # Issue detail
73
+ only-bible-app/index.tsx
74
+ only-bible-app/privacy-policy.tsx
75
+ only-bible-app/terms-and-conditions.tsx
76
+ robots.txt.ts # robots.txt
77
+ rss.xml.tsx # RSS feed
78
+ assets/ # Static files (CSS, images, PDFs, icons)
79
+ content/ # Blog posts (Markdown)
80
+ packages/shared/ # Config, Types, Core, UI — unchanged
81
+ vite.config.ts
82
+ wrangler.jsonc
83
+ package.json
84
+ tsconfig.json
85
+ ```
86
+
87
+ ## Key Components
88
+
89
+ ### 1. Renderer Bridge (`_renderer.ts`)
90
+
91
+ HonoX uses `c.render(content, head)` for page rendering. The renderer middleware wraps content in the site layout. Since we use typed-htmx (not hono/jsx), the renderer bridges the gap:
92
+
93
+ ```typescript
94
+ // app/routes/_renderer.ts
95
+ import { createRoute } from 'honox/factory'
96
+ import { Layout } from '@pyrossh/ui'
97
+
98
+ export default createRoute((c, next) => {
99
+ c.setRender((
100
+ content: string,
101
+ head?: { title?: string; description?: string; css?: string }
102
+ ) => {
103
+ const html = Layout({
104
+ title: head?.title ?? 'pyrossh',
105
+ description: head?.description,
106
+ css: head?.css,
107
+ request: c.req.raw,
108
+ children: content,
109
+ })
110
+ return c.html(html)
111
+ })
112
+ return next()
113
+ })
114
+ ```
115
+
116
+ The route handler renders its body content via typed-htmx JSX and passes it to `c.render()` with metadata. The renderer wraps it in the full HTML document via the existing `Layout` component from `@pyrossh/ui`.
117
+
118
+ Note: typed-htmx JSX renders children as raw HTML (not escaped), so the string content from the route handler is safely embedded in the layout.
119
+
120
+ ### 1a. TypeScript Configuration
121
+
122
+ The `tsconfig.json` uses typed-htmx as the JSX import source (same as current workers):
123
+
124
+ ```json
125
+ {
126
+ "compilerOptions": {
127
+ "jsx": "react-jsx",
128
+ "jsxImportSource": "typed-htmx/typed-html",
129
+ "moduleResolution": "bundler",
130
+ "target": "ESNext",
131
+ "module": "ESNext",
132
+ "strict": true
133
+ },
134
+ "include": ["app"]
135
+ }
136
+ ```
137
+
138
+ The HonoX framework files (`server.ts`, `vite.config.ts`) don't use JSX, so no conflict arises.
139
+
140
+ ### 1b. HonoX Type Declarations (`global.d.ts`)
141
+
142
+ The `global.d.ts` declares the `ContextRenderer` type so that `c.render()` accepts our typed-htmx content:
143
+
144
+ ```typescript
145
+ import {} from 'hono'
146
+
147
+ type Head = {
148
+ title?: string
149
+ description?: string
150
+ css?: string
151
+ }
152
+
153
+ declare module 'hono' {
154
+ interface ContextRenderer {
155
+ (content: string | Promise<string>, head?: Head): Response | Promise<Response>
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### 2. Route Handler Pattern
161
+
162
+ Each route handler follows a consistent pattern:
163
+
164
+ ```typescript
165
+ // app/routes/index.tsx
166
+ import { createRoute } from 'honox/factory'
167
+ import { REPOS, TOOLS, SITE_TITLE } from '@pyrossh/config'
168
+
169
+ export default createRoute((c) => {
170
+ return c.render(
171
+ <div class="pageContainer">
172
+ <h1 class="title">Hello!</h1>
173
+ {/* page body content, same as current home worker */}
174
+ </div>,
175
+ { title: SITE_TITLE, css: '/assets/css/workers/home.css' }
176
+ )
177
+ })
178
+ ```
179
+
180
+ Handlers that need R2 access get `c.env` from the Hono context:
181
+
182
+ ```typescript
183
+ // app/routes/posts/index.tsx
184
+ import { createRoute } from 'honox/factory'
185
+ import { getPosts } from '@pyrossh/core'
186
+
187
+ export default createRoute(async (c) => {
188
+ const posts = await getPosts(c.env.REPOS)
189
+ return c.render(
190
+ <div>
191
+ <h1>Posts</h1>
192
+ {/* ... */}
193
+ </div>,
194
+ { title: 'Posts', css: '/assets/css/workers/posts-index.css' }
195
+ )
196
+ })
197
+ ```
198
+
199
+ ### 2a. HTMX Form Handlers (POST/PUT)
200
+
201
+ Workers that handle HTMX form submissions (e.g., issues CRUD) export named handlers alongside the default GET handler:
202
+
203
+ ```typescript
204
+ // app/routes/repos/[id]/issues/[issueId].tsx
205
+ import { createRoute } from 'honox/factory'
206
+ import { getGitBugIssue, addGitBugIssueComment } from '@pyrossh/core'
207
+
208
+ // GET: Render page
209
+ export default createRoute(async (c) => {
210
+ const issueId = c.req.param('issueId')
211
+ const issue = await getGitBugIssue(c.env.REPOS, issueId)
212
+ return c.render(
213
+ <div>{/* issue detail */}</div>,
214
+ { title: `Issue #${issue.id}`, css: '/assets/css/workers/issues-detail.css' }
215
+ )
216
+ })
217
+
218
+ // POST: HTMX form action (add comment)
219
+ export const POST = createRoute(async (c) => {
220
+ const formData = await c.req.parseBody()
221
+ // handle comment creation
222
+ return c.html(/* htmx fragment */)
223
+ })
224
+ ```
225
+
226
+ ### 3. Static Assets
227
+
228
+ Assets are served via Cloudflare Workers' built-in `assets.directory` feature, not through a worker:
229
+
230
+ ```json
231
+ // wrangler.jsonc
232
+ {
233
+ "name": "pyrossh-website",
234
+ "main": "dist/index.js",
235
+ "compatibility_date": "2026-07-07",
236
+ "compatibility_flags": ["nodejs_compat"],
237
+ "assets": { "directory": "assets" },
238
+ "r2_buckets": [{ "binding": "REPOS", "bucket_name": "pyrossh-repos-prd" }],
239
+ "routes": [{ "pattern": "pyrossh.dev", "custom_domain": true }]
240
+ }
241
+ ```
242
+
243
+ This eliminates the entire assets worker and R2 proxy for static files. In development, Vite dev server or wrangler dev serves assets from the directory automatically.
244
+
245
+ ### 4. Content and Blog Posts
246
+
247
+ Blog content is still read from the `REPOS` R2 bucket (unchanged). `packages/shared/core/src/content.ts` uses `env.REPOS` which is available via `c.env.REPOS`.
248
+
249
+ The deploy script syncs content to the REPOS bucket (already fixed in the previous iteration).
250
+
251
+ ### 5. HTMX Interactivity
252
+
253
+ HTMX is loaded from CDN in the Layout component (unchanged). Interactive features (tab navigation, issue CRUD, file tree) continue to work with HTMX as before.
254
+
255
+ HonoX islands are not used. The `app/client.ts` file is kept minimal — just `createClient()` for HonoX compatibility, but no island components:
256
+
257
+ ```typescript
258
+ // app/client.ts
259
+ import { createClient } from 'honox/client'
260
+ createClient()
261
+ ```
262
+
263
+ ### 5a. HonoX Server Entry (`app/server.ts`)
264
+
265
+ The server entry initializes the HonoX app:
266
+
267
+ ```typescript
268
+ // app/server.ts
269
+ import { createApp } from 'honox/server'
270
+
271
+ const app = createApp()
272
+
273
+ export default app
274
+ ```
275
+
276
+ ### 5b. Vite Configuration (`vite.config.ts`)
277
+
278
+ Vite config uses the HonoX plugin with Cloudflare Workers adapter:
279
+
280
+ ```typescript
281
+ // vite.config.ts
282
+ import honox from 'honox/vite'
283
+ import build from '@hono/vite-build/cloudflare-workers'
284
+ import adapter from '@hono/vite-dev-server/cloudflare'
285
+ import { defineConfig } from 'vite'
286
+
287
+ export default defineConfig({
288
+ plugins: [
289
+ honox({ devServer: { adapter } }),
290
+ build(),
291
+ ],
292
+ })
293
+ ```
294
+
295
+ ### 6. Request Context
296
+
297
+ The existing `@pyrossh/ui` Layout component accepts a `request: Request` prop. In the renderer bridge, `c.req.raw` provides the original request. This is used for:
298
+ - Canonical URL generation
299
+ - Current navigation path highlighting
300
+ - Theme toggle
301
+
302
+ ## What Gets Removed
303
+
304
+ | Package | Reason |
305
+ |---------|--------|
306
+ | `packages/workers/assets/` | Replaced by wrangler `assets.directory` |
307
+ | `packages/workers/home/` | Moved to `app/routes/index.tsx` |
308
+ | `packages/workers/cv/` | Moved to `app/routes/cv.tsx` |
309
+ | `packages/workers/posts/index/` | Moved to `app/routes/posts/index.tsx` |
310
+ | `packages/workers/posts/detail/` | Moved to `app/routes/posts/[id].tsx` |
311
+ | `packages/workers/repos/readme/` | Moved to `app/routes/repos/[id].tsx` |
312
+ | `packages/workers/repos/commits/index/` | Moved to `app/routes/repos/[id]/commits.tsx` |
313
+ | `packages/workers/repos/commits/detail/` | Moved to `app/routes/repos/[id]/commits/[hash].tsx` |
314
+ | `packages/workers/repos/files/index/` | Moved to `app/routes/repos/[id]/files.tsx` |
315
+ | `packages/workers/repos/files/detail/` | Moved to `app/routes/repos/[id]/files/[...path].tsx` |
316
+ | `packages/workers/repos/files/history/` | Moved to `app/routes/repos/[id]/files/[...path]/history.tsx` |
317
+ | `packages/workers/repos/files/blame/` | Moved to `app/routes/repos/[id]/files/[...path]/blame.tsx` |
318
+ | `packages/workers/repos/issues/index/` | Moved to `app/routes/repos/[id]/issues.tsx` |
319
+ | `packages/workers/repos/issues/detail/` | Moved to `app/routes/repos/[id]/issues/[issueId].tsx` |
320
+ | `packages/workers/only-bible-app/index/` | Moved to `app/routes/only-bible-app/index.tsx` |
321
+ | `packages/workers/only-bible-app/privacy/` | Moved to `app/routes/only-bible-app/privacy-policy.tsx` |
322
+ | `packages/workers/only-bible-app/terms/` | Moved to `app/routes/only-bible-app/terms-and-conditions.tsx` |
323
+ | `packages/workers/robots/` | Moved to `app/routes/robots.txt.ts` |
324
+ | `packages/workers/rss/` | Moved to `app/routes/rss.xml.tsx` |
325
+ | `packages/workers/not-found/` | Replaced by `app/routes/_404.tsx` |
326
+ | `packages/workers/server-error/` | Replaced by `app/routes/_error.tsx` |
327
+ | `packages/workers/dev-router/` | Eliminated — HonoX handles routing |
328
+ | Root `wrangler.toml` | Replaced by HonoX's `wrangler.jsonc` |
329
+
330
+ ## What Stays Unchanged
331
+
332
+ - `packages/shared/config/` — site constants, repo list, tools list
333
+ - `packages/shared/types/` — TypeScript type definitions
334
+ - `packages/shared/core/` — business logic (markdown, git reader, content, git bug, files)
335
+ - `packages/shared/ui/` — UI components (Layout, Header, Footer, RepoLayout, etc.)
336
+ - `assets/` directory — static files
337
+ - `content/` directory — blog posts
338
+ - `packages/shared/types/src/env.d.ts` — unchanged (REPOS binding still used)
339
+
340
+ ## Workspace Cleanup
341
+
342
+ - `packages/workers/` directory is removed entirely
343
+ - `pnpm-workspace.yaml` updated to remove `'packages/workers/**'` (or keep with no matches)
344
+ - Root `package.json` scripts updated: `dev` becomes `vite dev`, `build` becomes `vite build --mode client && vite build`, `deploy` remains `wrangler deploy`
345
+ - `@pyrossh/dev-router` package is removed (no longer needed)
346
+ - The dev-router's `dependencies` list all 18 workers — those are removed from workspace resolution
347
+
348
+ ## New Dependencies
349
+
350
+ ```json
351
+ {
352
+ "dependencies": {
353
+ "hono": "^4.x",
354
+ "honox": "^0.1.x"
355
+ },
356
+ "devDependencies": {
357
+ "vite": "^6.x",
358
+ "@hono/vite-build": "^1.x",
359
+ "@hono/vite-dev-server": "^1.x"
360
+ }
361
+ }
362
+ ```
363
+
364
+ ## Deployment
365
+
366
+ Build command:
367
+ ```bash
368
+ vite build --mode client && vite build && wrangler deploy
369
+ ```
370
+
371
+ Single worker deployed to `pyrossh.dev`. The `assets.directory` config in `wrangler.jsonc` handles static files.