website

#astro#js#html#css

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

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


150c6c8pyrossh 2026-07-08T19:23:08+05:30
docs: use plain Hono + html/css helpers, no HonoX or JSX
docs/superpowers/specs/2026-07-08-migrate-to-honox-design.md CHANGED
@@ -1,75 +1,38 @@
1
- # pyrossh.dev — Migrate to HonoX (single app)
1
+ # pyrossh.dev — Rewrite (Hono + html/css helpers)
2
2
 
3
- **Goal:** Replace the current monorepo of ~18 Workers + dev-router + assets worker + 4 shared packages with a single HonoX application. No monorepo, no shared packages just one app with local imports.
3
+ **Goal:** Replace the current monorepo of ~22 npm packages with a single Hono application using `hono/html` for templating and `hono/css` for styling. No JSX, no HonoX, no monorepo, no shared packages. Storage via s3mini (S3-compatible). HTMX for interactivity.
4
4
 
5
5
  ## Motivation
6
6
 
7
- Current architecture has 22+ npm packages for a personal website. This is over-engineered:
7
+ Current architecture has 22+ packages for a personal website. Over-engineered.
8
-
9
- - 18 individual worker packages, each with its own config
10
- - 4 shared packages (config, types, core, ui) linked via pnpm workspaces
11
- - Dev-router that imports all workers for local development
12
- - Assets worker proxying to R2 bucket
13
- - Manual route pattern matching
14
-
15
- A single HonoX app with file-based routing, hono/jsx for templating, hono/css for styling, s3mini for storage, and htmx for interactivity eliminates all of this.
16
8
 
17
9
  ## Project Structure
18
10
 
19
11
  ```
20
12
  pyrossh.dev/
21
- app/
13
+ src/
22
- global.d.ts # HonoX type declarations
23
- server.ts # createApp() entry
24
- client.ts # HonoX client entry
14
+ index.ts # Hono app, all routes defined here
25
- config.ts # SITE_TITLE, REPOS list, TOOLS list, NAV_ITEMS, SITE_URL
15
+ config.ts # SITE_TITLE, REPOS, TOOLS, NAV_ITEMS, SITE_URL
26
- types.ts # RuntimePost, RuntimeRepo, Commit, FileEntry, GitBugIssue, etc.
16
+ types.ts # RuntimePost, RuntimeRepo, Commit, etc.
27
- s3.ts # S3mini singleton from env vars
17
+ s3.ts # S3mini singleton
28
- components/ # UI components
18
+ components/ # UI components (functions returning html``)
29
- header.tsx
19
+ header.ts
30
- footer.tsx
20
+ footer.ts
31
- repo-layout.tsx
21
+ repo-layout.ts
32
- file-layout.tsx
22
+ file-layout.ts
33
- commit-entry.tsx
23
+ commit-entry.ts
34
- issue-card.tsx
24
+ issue-card.ts
35
- file-tree.tsx
25
+ file-tree.ts
36
- formatted-date.tsx
26
+ formatted-date.ts
37
- lib/ # Business logic (ported from @pyrossh/core)
27
+ lib/ # Business logic
38
28
  markdown.ts
39
- content.ts # getPosts, getPost — reads from S3
29
+ content.ts # getPosts, getPost
40
- gitReader.ts # getCommits, getFiles, getFileContentData, etc.
30
+ gitReader.ts # getCommits, getFiles, etc.
41
- gitBug.ts # GitBug issue CRUD
31
+ gitBug.ts # GitBug issue CRUD
42
- files.ts # File tree builder, icon resolver
32
+ files.ts # File tree, icons
43
- repoContent.ts # getRepoReadme
33
+ repoContent.ts # getRepoReadme
44
- routes/
45
- _renderer.tsx # HonoX jsxRenderer with layout
46
- _404.tsx # Not found
47
- _error.tsx # Error page
48
- index.tsx # Home (/)
49
- cv.tsx # CV (/cv)
50
- posts/
51
- index.tsx # Blog list (/posts)
52
- [id].tsx # Blog post (/posts/:id)
53
- repos/
54
- [id].tsx # Repo README (/repos/:id)
55
- [id]/
56
- commits.tsx
57
- commits/[hash].tsx
58
- files.tsx
59
- files/[...path].tsx
60
- files/[...path]/history.tsx
61
- files/[...path]/blame.tsx
62
- issues.tsx
63
- issues/[issueId].tsx
64
- only-bible-app/
65
- index.tsx
66
- privacy-policy.tsx
67
- terms-and-conditions.tsx
68
- robots.txt.ts
69
- rss.xml.tsx
70
- assets/ # CSS, images, PDFs, icons (served by wrangler)
34
+ assets/ # Static files (CSS, images, PDFs, icons)
71
- content/ # Blog posts (synced to S3)
35
+ content/ # Blog posts (synced to S3)
72
- vite.config.ts
73
36
  wrangler.jsonc
74
37
  tsconfig.json
75
38
  package.json
@@ -77,90 +40,136 @@ pyrossh.dev/
77
40
 
78
41
  ## Architecture
79
42
 
80
- ### Renderer (`app/routes/_renderer.tsx`)
43
+ ### App Entry (`src/index.ts`)
81
-
44
+
82
- Standard HonoX `jsxRenderer` renders the HTML document layout. Uses hono/jsx:
45
+ All routes defined in a single file. Plain Hono, no HonoX.
83
-
46
+
84
- ```tsx
47
+ ```typescript
48
+ import { Hono } from 'hono'
85
- import { jsxRenderer } from 'hono/jsx-renderer'
49
+ import { html } from 'hono/html'
86
- import { Script } from 'honox/server'
50
+ import { css } from 'hono/css'
87
- import { Header } from '../components/header'
51
+ import { Header } from './components/header'
88
- import { Footer } from '../components/footer'
52
+ import { Footer } from './components/footer'
89
- import { SITE_TITLE, SITE_DESCRIPTION, SITE_URL } from '../config'
53
+ import { SITE_TITLE } from './config'
90
-
91
- export default jsxRenderer(({ children, title, description, css }) => {
54
+
92
- const url = new URL(c.req.url)
55
+ const app = new Hono()
56
+
93
- return (
57
+ // Layout wrapper
58
+ const Layout = (props: { title: string; description?: string; css?: string; children: any }) => html`
94
- <html lang="en">
59
+ <html lang="en">
95
- <head>
60
+ <head>
96
- <meta charset="utf-8" />
61
+ <meta charset="utf-8" />
97
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
62
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
98
- <link rel="icon" type="image/svg+xml" href="/assets/icons/icon.svg" />
63
+ <link rel="icon" type="image/svg+xml" href="/assets/icons/icon.svg" />
99
- <link rel="canonical" href={`${SITE_URL}${url.pathname}`} />
100
- <link rel="stylesheet" href="/assets/css/shared.css" />
64
+ <link rel="stylesheet" href="/assets/css/shared.css" />
101
- {css && <link rel="stylesheet" href={css} />}
65
+ ${props.css ? html`<link rel="stylesheet" href="${props.css}" />` : ''}
102
- <title>{title ?? SITE_TITLE}</title>
66
+ <title>${props.title}</title>
103
- <meta name="description" content={description ?? SITE_DESCRIPTION} />
67
+ <meta name="description" content="${props.description ?? ''}" />
104
- <meta property="og:title" content={title ?? SITE_TITLE} />
105
- <meta property="og:description" content={description ?? SITE_DESCRIPTION} />
106
- <meta property="og:image" content="/assets/icons/icon.svg" />
107
- <Script src="/app/client.ts" />
68
+ <script src="https://unpkg.com/[email protected]"></script>
108
- </head>
69
+ </head>
109
- <body>
70
+ <body>
110
- <Header currentPath={url.pathname} />
71
+ ${Header({ currentPath: '' })}
111
- <div class="wrapper"><main>{children}</main></div>
72
+ <div class="wrapper"><main>${props.children}</main></div>
112
- <Footer />
73
+ ${Footer()}
113
- <script src="https://unpkg.com/[email protected]"></script>
114
- </body>
74
+ </body>
115
- </html>
75
+ </html>
116
- )
76
+ `
77
+
78
+ // Routes
79
+ app.get('/', (c) => c.html(Layout({
80
+ title: SITE_TITLE,
81
+ description: '...',
82
+ children: html`<h1>Hello!</h1>`,
83
+ css: '/assets/css/workers/home.css',
84
+ })))
85
+
86
+ app.get('/posts', async (c) => {
87
+ const posts = await getPosts(getS3())
88
+ return c.html(Layout({
89
+ title: 'Posts',
90
+ children: html`
91
+ <h1>Posts</h1>
92
+ ${posts.map(post => html`
93
+ <a href="/posts/${post.id}">${post.data.title}</a>
94
+ `)}
95
+ `,
96
+ }))
117
97
  })
98
+
99
+ app.get('/posts/:id', async (c) => {
100
+ const posts = await getPosts(getS3())
101
+ const post = posts.find(p => p.id === c.req.param('id'))
102
+ if (!post) return c.notFound()
103
+ return c.html(Layout({
104
+ title: post.data.title,
105
+ children: html`
106
+ <article>
107
+ <h1>${post.data.title}</h1>
108
+ <time>${post.data.pubDate.toLocaleDateString()}</time>
109
+ <hr />
110
+ ${post.html}
111
+ </article>
112
+ `,
113
+ }))
114
+ })
115
+
116
+ // More routes: /cv, /repos/:id, /repos/:id/commits, etc.
117
+ // HTMX handlers: app.post('/repos/:id/issues', handler)
118
+
119
+ export default app
118
120
  ```
119
121
 
120
- All CSS (shared, per-page) is served as static files from `assets/css/` via wrangler's `assets.directory`. hono/css is available for scoped inline styles if needed later.
122
+ `hono/html` tagged templates auto-escape values. Arrays (from `.map()`) are joined automatically. Components are functions returning `html` template results.
121
123
 
122
- ### Routes
124
+ For non-trivial route files, the Layout can be extracted, or routes can be split into separate files and mounted.
123
125
 
124
- Each route handler imports from local `../lib/` and `../components/`:
126
+ ### Components
125
127
 
126
- ```tsx
127
- // app/routes/posts/index.tsx
128
- import { createRoute } from 'honox/factory'
129
- import { getPosts } from '../lib/content'
130
- import { getS3 } from '../s3'
128
+ Components are functions returning `html` results:
131
129
 
130
+ ```typescript
131
+ // src/components/header.ts
132
+ import { html } from 'hono/html'
133
+ import { NAV_ITEMS } from '../config'
134
+
132
- export default createRoute(async (c) => {
135
+ export const Header = (props: { currentPath: string }) => html`
133
- const posts = await getPosts(getS3())
134
- return c.render(
136
+ <header>
135
- <div>
137
+ <nav>
138
+ <a href="/" class="logo">木 pyrossh</a>
136
- <h1>Posts</h1>
139
+ <div class="links">
137
- {posts.map(post => (
140
+ ${NAV_ITEMS.map(item => html`
141
+ <a href="${item.href}" class="${props.currentPath?.startsWith(item.href) ? 'active' : ''}">
142
+ ${item.label}
138
- <div>
143
+ </a>
139
- <a href={`/posts/${post.id}`}>{post.data.title}</a>
140
- <time>{post.data.pubDate.toLocaleDateString()}</time>
144
+ `)}
141
- </div>
145
+ </div>
142
- ))}
143
- </div>,
146
+ </nav>
144
- { title: 'Posts', css: '/assets/css/workers/posts-index.css' }
147
+ </header>
145
- )
148
+ `
146
- })
147
149
  ```
148
150
 
149
- HTMX handlers (issue CRUD) export POST/PUT alongside default GET:
151
+ ### CSS
150
152
 
153
+ Per-route CSS is linked from `assets/css/workers/*.css` via the `css` prop on Layout (same as current approach). `hono/css` is available for scoped inline styles when needed:
154
+
151
- ```tsx
155
+ ```typescript
152
- export default createRoute(async (c) => { /* GET */ })
156
+ import { css } from 'hono/css'
157
+
158
+ const myClass = css`
159
+ color: red;
153
- export const POST = createRoute(async (c) => { /* form handler */ })
160
+ &:hover { color: blue; }
161
+ `
154
162
  ```
155
163
 
156
164
  ### S3 Storage
157
165
 
166
+ Same as before — s3mini singleton from env vars:
167
+
158
168
  ```typescript
159
- // app/s3.ts
169
+ // src/s3.ts
160
170
  import { S3mini } from 's3mini'
161
171
 
162
172
  let _s3: S3mini | null = null
163
-
164
173
  export const getS3 = () => {
165
174
  if (!_s3) {
166
175
  _s3 = new S3mini({
@@ -174,15 +183,17 @@ export const getS3 = () => {
174
183
  }
175
184
  ```
176
185
 
177
- All business logic functions (`getPosts`, `getCommits`, `getFiles`, `getRepoReadme`, etc.) take `S3mini` as the first parameter instead of `R2Bucket`.
178
-
179
- ### Static Assets
186
+ ### HTMX Interactivity
180
187
 
181
- Served by wrangler's `assets.directory` no worker involved.
188
+ HTMX handlers use `app.post()`, `app.put()` alongside the GET handlers:
182
-
183
- ### CSS
184
189
 
185
- CSS files remain as static files in `assets/css/` (shared.css, workers/*.css). Keeps full control over styles without inline CSS bloat. hono/css is available as an option for scoped component styles.
190
+ ```typescript
191
+ app.post('/repos/:repoId/issues', async (c) => {
192
+ const form = await c.req.parseBody()
193
+ const issue = await createGitBugIssue(getS3(), c.req.param('repoId'), form)
194
+ return c.html(html`<div>Issue created</div>`)
195
+ })
196
+ ```
186
197
 
187
198
  ## Dependencies
188
199
 
@@ -190,7 +201,6 @@ CSS files remain as static files in `assets/css/` (shared.css, workers/*.css). K
190
201
  {
191
202
  "dependencies": {
192
203
  "hono": "^4.x",
193
- "honox": "^0.1.x",
194
204
  "s3mini": "^0.9.x",
195
205
  "gray-matter": "^4.0.3",
196
206
  "unified": "^11.0.0",
@@ -203,60 +213,46 @@ CSS files remain as static files in `assets/css/` (shared.css, workers/*.css). K
203
213
  "diff2html": "^3.4.52"
204
214
  },
205
215
  "devDependencies": {
206
- "vite": "^6.x",
216
+ "wrangler": "^4.x",
207
- "@hono/vite-build": "^1.x",
208
- "@hono/vite-dev-server": "^1.x",
209
217
  "@cloudflare/workers-types": "^5.x",
210
218
  "typescript": "^6.x"
211
219
  }
212
220
  }
213
221
  ```
214
222
 
215
- No monorepo, no pnpm workspaces, no workspace dependencies. Just `npm install` or `bun install`.
223
+ No honox, no vite, no typed-htmx, no @hono/vite-build/dev-server. `hono/html`, `hono/css`, and `hono/jsx` are built into `hono`.
216
224
 
217
225
  ## What Gets Removed
218
226
 
219
- | Item | Files |
227
+ | Item | Replaced by |
220
- |------|-------|
228
+ |------|-------------|
221
- | `packages/` directory | 18 workers + dev-router + 4 shared packages (~22 packages) |
229
+ | 18 worker packages + 4 shared packages | `src/` with local imports |
222
- | Root `wrangler.toml` | Replaced by `wrangler.jsonc` |
223
- | `worker-configuration.d.ts` | No R2 bindings needed |
224
- | `.env` (old) | R2 access keys moved to S3 env vars |
225
- | `pnpm-workspace.yaml` | No workspaces |
230
+ | pnpm workspaces | Single package.json |
231
+ | typed-htmx | `hono/html` (built in) |
232
+ | HonoX / Vite / dev-server | Plain Hono + wrangler dev |
233
+ | R2 bucket binding (`env.REPOS`) | s3mini from env vars |
234
+ | Assets worker + R2 proxy | wrangler `assets.directory` |
226
- | `pnpm-lock.yaml` | Use npm or bun lockfile |
235
+ | Dev-router | Single app entry point |
236
+ | `_renderer.tsx`, `global.d.ts`, `client.ts`, `server.ts` | Single `src/index.ts` |
227
- | `tsconfig.base.json` | Single tsconfig |
237
+ | JSX config in tsconfig | Standard TypeScript |
228
- | `scripts/deploy-all.sh` | Single wrangler deploy |
229
- | `alchemy.run.ts` / `.alchemy/` | Infrastructure-as-code not needed |
230
238
 
231
239
  ## Environment Variables
232
240
 
233
- | Variable | Description |
234
- |----------|-------------|
235
- | `S3_ACCESS_KEY_ID` | R2 API token access key |
236
- | `S3_SECRET_ACCESS_KEY` | R2 API token secret key |
241
+ `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_ENDPOINT`, `S3_REGION` (optional).
237
- | `S3_ENDPOINT` | S3 endpoint URL (`https://<account>.r2.cloudflarestorage.com/<bucket>`) |
238
- | `S3_REGION` | Defaults to `auto` |
239
242
 
240
243
  ## Deployment
241
244
 
242
245
  ```bash
243
246
  bun install
244
- bunx vite build --mode client && bunx vite build
245
247
  bunx wrangler deploy
246
248
  bunx wrangler secret put S3_ACCESS_KEY_ID
247
249
  bunx wrangler secret put S3_SECRET_ACCESS_KEY
248
250
  ```
249
251
 
250
- ## Summary of Simplifications
252
+ No build step. Wrangler bundles the TypeScript directly.
251
253
 
252
- | Before | After |
254
+ ## Dev
255
+
253
- |--------|-------|
256
+ ```bash
254
- | 22 npm packages | 1 app |
255
- | pnpm workspaces | single package.json |
257
+ bunx wrangler dev src/index.ts
256
- | 4 shared packages with tsconfigs each | flat imports (`../lib/`, `../components/`) |
257
- | R2 bucket binding (`env.REPOS`) | S3mini from env vars |
258
- | typed-htmx JSX (extra dep) | hono/jsx (built in) |
259
- | Private worker packages with individual routes | HonoX file-based routing |
260
- | Assets worker + R2 proxy | wrangler `assets.directory` |
261
- | Dev-router imports 18 workers | `vite dev` with HonoX |
258
+ ```
262
- | Separate deploy script per worker | one `wrangler deploy` |