website

#astro#js#html#css

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

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


be00a33pyrossh 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 R2 access get `c.env` from the Hono context:
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(c.env.REPOS)
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(c.env.REPOS, issueId)
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
- "r2_buckets": [{ "binding": "REPOS", "bucket_name": "pyrossh-repos-prd" }],
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. Content and Blog Posts
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
- 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`.
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
- The deploy script syncs content to the REPOS bucket (already fixed in the previous iteration).
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
- | Package | Reason |
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
- - `packages/shared/types/src/env.d.ts` unchanged (REPOS binding still used)
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.