website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
be00a33
— pyrossh
2026-07-08T19:07:38+05:30
docs: add S3mini storage layer, replace R2 binding with portable S3 API
docs/superpowers/specs/2026-07-08-migrate-to-honox-typed-htmx-design.md
CHANGED
|
@@ -177,15 +177,15 @@ export default createRoute((c) => {
|
|
|
177
177
|
})
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
Handlers that need
|
|
180
|
+
Handlers that need S3 storage use `getS3()` — no `c.env` dependency:
|
|
181
181
|
|
|
182
182
|
```typescript
|
|
183
183
|
// app/routes/posts/index.tsx
|
|
184
184
|
import { createRoute } from 'honox/factory'
|
|
185
|
-
import { getPosts } from '@pyrossh/core'
|
|
185
|
+
import { getPosts, getS3 } from '@pyrossh/core'
|
|
186
186
|
|
|
187
187
|
export default createRoute(async (c) => {
|
|
188
|
-
const posts = await getPosts(
|
|
188
|
+
const posts = await getPosts(getS3())
|
|
189
189
|
return c.render(
|
|
190
190
|
<div>
|
|
191
191
|
<h1>Posts</h1>
|
|
@@ -203,12 +203,12 @@ Workers that handle HTMX form submissions (e.g., issues CRUD) export named handl
|
|
|
203
203
|
```typescript
|
|
204
204
|
// app/routes/repos/[id]/issues/[issueId].tsx
|
|
205
205
|
import { createRoute } from 'honox/factory'
|
|
206
|
-
import { getGitBugIssue, addGitBugIssueComment } from '@pyrossh/core'
|
|
206
|
+
import { getGitBugIssue, addGitBugIssueComment, getS3 } from '@pyrossh/core'
|
|
207
207
|
|
|
208
208
|
// GET: Render page
|
|
209
209
|
export default createRoute(async (c) => {
|
|
210
210
|
const issueId = c.req.param('issueId')
|
|
211
|
-
const issue = await getGitBugIssue(
|
|
211
|
+
const issue = await getGitBugIssue(getS3(), issueId)
|
|
212
212
|
return c.render(
|
|
213
213
|
<div>{/* issue detail */}</div>,
|
|
214
214
|
{ title: `Issue #${issue.id}`, css: '/assets/css/workers/issues-detail.css' }
|
|
@@ -235,18 +235,112 @@ Assets are served via Cloudflare Workers' built-in `assets.directory` feature, n
|
|
|
235
235
|
"compatibility_date": "2026-07-07",
|
|
236
236
|
"compatibility_flags": ["nodejs_compat"],
|
|
237
237
|
"assets": { "directory": "assets" },
|
|
238
|
+
"vars": {
|
|
238
|
-
|
|
239
|
+
"S3_ENDPOINT": "https://...r2.cloudflarestorage.com/pyrossh-repos-prd",
|
|
240
|
+
"S3_REGION": "auto",
|
|
241
|
+
"S3_BUCKET": "pyrossh-repos-prd"
|
|
242
|
+
},
|
|
239
243
|
"routes": [{ "pattern": "pyrossh.dev", "custom_domain": true }]
|
|
240
244
|
}
|
|
241
245
|
```
|
|
242
246
|
|
|
247
|
+
No R2 bucket binding — storage is accessed via S3-compatible API. `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` are set as worker secrets.
|
|
248
|
+
|
|
243
249
|
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
250
|
|
|
245
|
-
### 4.
|
|
251
|
+
### 4. S3 Storage Layer (s3mini)
|
|
252
|
+
|
|
253
|
+
All storage operations switch from Cloudflare R2 bindings (`env.REPOS`) to a portable S3 API using `s3mini`. Zero dependencies, works on Workers (no `nodejs_compat` needed), Bun, and Node.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// packages/shared/core/src/s3.ts
|
|
257
|
+
import { S3mini } from 's3mini'
|
|
258
|
+
|
|
259
|
+
let _s3: S3mini | null = null
|
|
260
|
+
|
|
261
|
+
export const getS3 = () => {
|
|
262
|
+
if (!_s3) {
|
|
263
|
+
_s3 = new S3mini({
|
|
264
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
|
265
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
|
266
|
+
endpoint: process.env.S3_ENDPOINT!,
|
|
267
|
+
region: process.env.S3_REGION ?? 'auto',
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
return _s3
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The S3 client is initialized once (singleton) from environment variables. On Cloudflare Workers, `process.env` resolves from `vars` and secrets automatically. On Bun/Node, it reads from `.env` or runtime env.
|
|
246
275
|
|
|
247
|
-
|
|
276
|
+
### 4a. Core Function Changes
|
|
248
277
|
|
|
278
|
+
All `packages/shared/core/` functions change from `R2Bucket` to `S3mini`:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Before (Cloudflare-specific R2 binding)
|
|
249
|
-
|
|
282
|
+
export const getPosts = async (bucket: R2Bucket): Promise<RuntimePost[]> => {
|
|
283
|
+
const objects = await bucket.list({ prefix: CONTENT_PREFIX });
|
|
284
|
+
// ...
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// After (portable S3 API)
|
|
288
|
+
import { S3mini } from 's3mini'
|
|
289
|
+
|
|
290
|
+
const CONTENT_PREFIX = 'content/'
|
|
291
|
+
|
|
292
|
+
export const getPosts = async (s3: S3mini): Promise<RuntimePost[]> => {
|
|
293
|
+
const objects = await s3.listObjects('/', CONTENT_PREFIX);
|
|
294
|
+
if (!objects) return [];
|
|
295
|
+
const posts: RuntimePost[] = [];
|
|
296
|
+
for (const obj of objects) {
|
|
297
|
+
if (!obj.Key.endsWith('.md') && !obj.Key.endsWith('.mdx')) continue;
|
|
298
|
+
const data = await s3.getObject(obj.Key);
|
|
299
|
+
if (!data) continue;
|
|
300
|
+
const parsed = matter(data);
|
|
301
|
+
// ...
|
|
302
|
+
}
|
|
303
|
+
return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### 4b. Route Handler Changes
|
|
308
|
+
|
|
309
|
+
Route handlers use the S3 client directly instead of `c.env.REPOS`:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// app/routes/posts/index.tsx
|
|
313
|
+
import { createRoute } from 'honox/factory'
|
|
314
|
+
import { getPosts } from '@pyrossh/core'
|
|
315
|
+
import { getS3 } from '@pyrossh/core/s3'
|
|
316
|
+
|
|
317
|
+
export default createRoute(async (c) => {
|
|
318
|
+
const posts = await getPosts(getS3())
|
|
319
|
+
return c.render(
|
|
320
|
+
<div>
|
|
321
|
+
<h1>Posts</h1>
|
|
322
|
+
{posts.map(post => (
|
|
323
|
+
<a href={`/posts/${post.id}`}>{post.data.title}</a>
|
|
324
|
+
))}
|
|
325
|
+
</div>,
|
|
326
|
+
{ title: 'Posts', css: '/assets/css/workers/posts-index.css' }
|
|
327
|
+
)
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
All functions that previously took `bucket: R2Bucket` now take `s3: S3mini`:
|
|
332
|
+
- `getPosts(s3)` instead of `getPosts(bucket)`
|
|
333
|
+
- `getPost(s3, postId)` instead of `getPost(bucket, postId)`
|
|
334
|
+
- `getRepoReadme(s3, repoId)` instead of `getRepoReadme(bucket, repoId)`
|
|
335
|
+
- `getCommits(s3, repoId)` instead of `getCommits(bucket, repoId)`
|
|
336
|
+
- `getFiles(s3, repoId)` instead of `getFiles(bucket, repoId)`
|
|
337
|
+
- `getFileContentData(s3, repoId, path)` instead of `getFileContentData(bucket, ...)`
|
|
338
|
+
- `getFileHistory(s3, repoId, path)` instead of `getFileHistory(bucket, ...)`
|
|
339
|
+
- `getGitBugIssues(s3, repoId)` instead of `getGitBugIssues(bucket, repoId)`
|
|
340
|
+
- `getGitBugIssue(s3, issueId)` instead of `getGitBugIssue(bucket, issueId)`
|
|
341
|
+
- `createGitBugIssue(s3, repoId, data)` instead of `createGitBugIssue(bucket, ...)`
|
|
342
|
+
- `addGitBugIssueComment(s3, issueId, data)` instead of `addGitBugIssueComment(bucket, ...)`
|
|
343
|
+
- `setGitBugIssueState(s3, issueId, state)` instead of `setGitBugIssueState(bucket, ...)`
|
|
250
344
|
|
|
251
345
|
### 5. HTMX Interactivity
|
|
252
346
|
|
|
@@ -301,41 +395,34 @@ The existing `@pyrossh/ui` Layout component accepts a `request: Request` prop. I
|
|
|
301
395
|
|
|
302
396
|
## What Gets Removed
|
|
303
397
|
|
|
304
|
-
|
|
|
398
|
+
| Item | Reason |
|
|
305
|
-
|------
|
|
399
|
+
|------|--------|
|
|
400
|
+
| `packages/workers/` (all 18 + dev-router) | Replaced by `app/routes/` |
|
|
306
401
|
| `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
402
|
| Root `wrangler.toml` | Replaced by HonoX's `wrangler.jsonc` |
|
|
403
|
+
| `REPOS` R2 bucket binding | Replaced by S3mini client |
|
|
404
|
+
| `packages/shared/types/src/env.d.ts` | No longer needed — no R2 binding |
|
|
405
|
+
| `worker-configuration.d.ts` | Replaced by HonoX types |
|
|
406
|
+
| `WEBSITE_BUCKET` R2 binding (recent addition) | No longer needed |
|
|
329
407
|
|
|
330
|
-
## What Stays Unchanged
|
|
408
|
+
## What Stays Unchanged (mostly)
|
|
331
409
|
|
|
332
410
|
- `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
411
|
- `packages/shared/ui/` — UI components (Layout, Header, Footer, RepoLayout, etc.)
|
|
336
412
|
- `assets/` directory — static files
|
|
337
413
|
- `content/` directory — blog posts
|
|
414
|
+
|
|
415
|
+
## What Changes in packages/shared/
|
|
416
|
+
|
|
417
|
+
| Package | Change |
|
|
418
|
+
|---------|--------|
|
|
419
|
+
| `packages/shared/types/` | Remove `Env` interface — no more R2 binding. Add `S3mini` type imports where needed |
|
|
420
|
+
| `packages/shared/core/` | All functions change from `(bucket: R2Bucket)` to `(s3: S3mini)`. Add `src/s3.ts` singleton factory |
|
|
421
|
+
| `packages/shared/core/package.json` | Add `s3mini` dependency |
|
|
422
|
+
| `packages/shared/core/src/gitBug.ts` | Replace `R2Bucket` with `S3mini`, use `s3.listObjects/getObject/putObject/deleteObject` |
|
|
338
|
-
|
|
423
|
+
| `packages/shared/core/src/gitReader.ts` | Replace `R2Bucket` with `S3mini` |
|
|
424
|
+
| `packages/shared/core/src/content.ts` | Replace `R2Bucket` with `S3mini`, use `s3.listObjects/getObject` |
|
|
425
|
+
| `packages/shared/core/src/repoContent.ts` | Replace `R2Bucket` with `S3mini`, use `s3.getObject` |
|
|
339
426
|
|
|
340
427
|
## Workspace Cleanup
|
|
341
428
|
|
|
@@ -351,16 +438,29 @@ The existing `@pyrossh/ui` Layout component accepts a `request: Request` prop. I
|
|
|
351
438
|
{
|
|
352
439
|
"dependencies": {
|
|
353
440
|
"hono": "^4.x",
|
|
354
|
-
"honox": "^0.1.x"
|
|
441
|
+
"honox": "^0.1.x",
|
|
442
|
+
"s3mini": "^0.9.x"
|
|
355
443
|
},
|
|
356
444
|
"devDependencies": {
|
|
357
445
|
"vite": "^6.x",
|
|
358
446
|
"@hono/vite-build": "^1.x",
|
|
359
|
-
"@hono/vite-dev-server": "^1.x"
|
|
447
|
+
"@hono/vite-dev-server": "^1.x",
|
|
448
|
+
"@cloudflare/workers-types": "^5.x"
|
|
360
449
|
}
|
|
361
450
|
}
|
|
362
451
|
```
|
|
363
452
|
|
|
453
|
+
## Environment Variables
|
|
454
|
+
|
|
455
|
+
| Variable | Required | Description |
|
|
456
|
+
|----------|----------|-------------|
|
|
457
|
+
| `S3_ACCESS_KEY_ID` | Yes | R2 API token access key |
|
|
458
|
+
| `S3_SECRET_ACCESS_KEY` | Yes | R2 API token secret key |
|
|
459
|
+
| `S3_ENDPOINT` | Yes | S3-compatible endpoint URL (e.g., `https://<account>.r2.cloudflarestorage.com/<bucket>`) |
|
|
460
|
+
| `S3_REGION` | No | Defaults to `auto` for R2 |
|
|
461
|
+
|
|
462
|
+
On Cloudflare Workers, `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` are set as secrets (`wrangler secret put`). `S3_ENDPOINT` is set in `vars` in `wrangler.jsonc`. On Bun/Node, all are set in `.env` or runtime environment.
|
|
463
|
+
|
|
364
464
|
## Deployment
|
|
365
465
|
|
|
366
466
|
Build command:
|
|
@@ -368,4 +468,10 @@ Build command:
|
|
|
368
468
|
vite build --mode client && vite build && wrangler deploy
|
|
369
469
|
```
|
|
370
470
|
|
|
471
|
+
Set secrets:
|
|
472
|
+
```bash
|
|
473
|
+
wrangler secret put S3_ACCESS_KEY_ID
|
|
474
|
+
wrangler secret put S3_SECRET_ACCESS_KEY
|
|
475
|
+
```
|
|
476
|
+
|
|
371
|
-
Single worker deployed to `pyrossh.dev`. The `assets.directory` config in `wrangler.jsonc` handles static files.
|
|
477
|
+
Single worker deployed to `pyrossh.dev`. The `assets.directory` config in `wrangler.jsonc` handles static files. The S3 client connects to R2 via S3-compatible API — no R2 bucket binding needed.
|