website

#astro#js#html#css

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

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


1c2fa8epyrossh 2026-08-11T18:46:18+05:30
fix README screenshots
eleventy.config.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
4
4
  import pluginRss from "@11ty/eleventy-plugin-rss";
5
5
  import { fileIcon, folderIcon, iconSpriteDefs } from "./src/_helpers/fileIcons.js";
6
+ import { discoverLocalRepos, findReadmeImagePaths } from "./src/_helpers/gitRepos.js";
6
7
 
7
8
  // theme-switcher (src/components/theme-switcher.js) used to be a Web
8
9
  // Component (via @elenajs/core + an @elenajs/ssr build-time transform
@@ -56,6 +57,20 @@ export default function (eleventyConfig) {
56
57
  "node_modules/htmx.org/dist/htmx.min.js": "assets/vendor/htmx.min.js",
57
58
  });
58
59
 
60
+ // README images referenced by a repo-relative path (e.g. `shots/shot1.png`)
61
+ // only render on GitHub because GitHub serves the file itself. Copying
62
+ // just the handful of files each README actually references to a `raw/`
63
+ // route keeps them real, loadable URLs without inlining every screenshot
64
+ // as base64 into the README's HTML (see resolveReadmeImages in
65
+ // gitRepos.js, which points <img> at these same paths).
66
+ for (const repo of discoverLocalRepos()) {
67
+ for (const relPath of findReadmeImagePaths(repo.readme, repo.dir)) {
68
+ eleventyConfig.addPassthroughCopy({
69
+ [path.join(repo.dir, relPath)]: `repos/${repo.id}/raw/${relPath}`,
70
+ });
71
+ }
72
+ }
73
+
59
74
  eleventyConfig.addPlugin(syntaxHighlight);
60
75
  eleventyConfig.addPlugin(pluginRss);
61
76
 
infra/terraform.tfstate CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 4,
3
3
  "terraform_version": "1.13.3",
4
- "serial": 58,
4
+ "serial": 65,
5
5
  "lineage": "6cb48eaa-b069-089b-fce3-e277626defab",
6
6
  "outputs": {
7
7
  "bucket_name": {
@@ -155,12 +155,12 @@
155
155
  {
156
156
  "schema_version": 0,
157
157
  "attributes": {
158
- "id": "08bd1d67-88db-0cb1-5d58-db6264f7fe13",
158
+ "id": "68b15cd2-01d4-a423-1a46-a72b890a2181",
159
159
  "input": null,
160
160
  "output": null,
161
161
  "triggers_replace": {
162
162
  "value": [
163
- "2026-08-07T14:09:27Z"
163
+ "2026-08-09T10:20:29Z"
164
164
  ],
165
165
  "type": [
166
166
  "tuple",
@@ -184,12 +184,12 @@
184
184
  {
185
185
  "schema_version": 0,
186
186
  "attributes": {
187
- "id": "3ec957d7-b52d-b06a-a7ff-92284b8ad15a",
187
+ "id": "2965dfb2-b48d-0edd-bec4-a60d0143c24b",
188
188
  "input": null,
189
189
  "output": null,
190
190
  "triggers_replace": {
191
191
  "value": [
192
- "2026-08-07T14:10:17Z"
192
+ "2026-08-09T10:23:13Z"
193
193
  ],
194
194
  "type": [
195
195
  "tuple",
@@ -218,12 +218,12 @@
218
218
  {
219
219
  "schema_version": 0,
220
220
  "attributes": {
221
- "id": "fc1bdc6f-6098-7963-e883-53a937260a47",
221
+ "id": "7b4cd3c4-6c8b-a4b7-b350-5404beca57c9",
222
222
  "input": null,
223
223
  "output": null,
224
224
  "triggers_replace": {
225
225
  "value": [
226
- "2026-08-07T14:09:40Z"
226
+ "2026-08-09T10:20:46Z"
227
227
  ],
228
228
  "type": [
229
229
  "tuple",
infra/terraform.tfstate.backup CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 4,
3
3
  "terraform_version": "1.13.3",
4
- "serial": 50,
4
+ "serial": 58,
5
5
  "lineage": "6cb48eaa-b069-089b-fce3-e277626defab",
6
6
  "outputs": {
7
7
  "bucket_name": {
@@ -18,6 +18,28 @@
18
18
  }
19
19
  },
20
20
  "resources": [
21
+ {
22
+ "mode": "managed",
23
+ "type": "cloudflare_r2_bucket",
24
+ "name": "apps",
25
+ "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]",
26
+ "instances": [
27
+ {
28
+ "schema_version": 500,
29
+ "attributes": {
30
+ "account_id": "dfdb8ca4e419990852eb214c5b50bd5e",
31
+ "creation_date": "2026-08-07T14:09:28.899Z",
32
+ "id": "apps",
33
+ "jurisdiction": "default",
34
+ "location": "APAC",
35
+ "name": "apps",
36
+ "storage_class": "Standard"
37
+ },
38
+ "sensitive_attributes": [],
39
+ "identity_schema_version": 0
40
+ }
41
+ ]
42
+ },
21
43
  {
22
44
  "mode": "managed",
23
45
  "type": "cloudflare_r2_bucket",
@@ -133,12 +155,12 @@
133
155
  {
134
156
  "schema_version": 0,
135
157
  "attributes": {
136
- "id": "a10e65da-2d65-7e3c-88fe-83072d314b09",
158
+ "id": "08bd1d67-88db-0cb1-5d58-db6264f7fe13",
137
159
  "input": null,
138
160
  "output": null,
139
161
  "triggers_replace": {
140
162
  "value": [
141
- "2026-08-07T13:50:14Z"
163
+ "2026-08-07T14:09:27Z"
142
164
  ],
143
165
  "type": [
144
166
  "tuple",
@@ -162,12 +184,12 @@
162
184
  {
163
185
  "schema_version": 0,
164
186
  "attributes": {
165
- "id": "e32197a8-fca2-44ca-a347-721abe2b4525",
187
+ "id": "3ec957d7-b52d-b06a-a7ff-92284b8ad15a",
166
188
  "input": null,
167
189
  "output": null,
168
190
  "triggers_replace": {
169
191
  "value": [
170
- "2026-08-07T13:50:51Z"
192
+ "2026-08-07T14:10:17Z"
171
193
  ],
172
194
  "type": [
173
195
  "tuple",
@@ -196,12 +218,12 @@
196
218
  {
197
219
  "schema_version": 0,
198
220
  "attributes": {
199
- "id": "1ac6f77a-618a-653c-6420-1f3e5547695e",
221
+ "id": "fc1bdc6f-6098-7963-e883-53a937260a47",
200
222
  "input": null,
201
223
  "output": null,
202
224
  "triggers_replace": {
203
225
  "value": [
204
- "2026-08-07T13:50:28Z"
226
+ "2026-08-07T14:09:40Z"
205
227
  ],
206
228
  "type": [
207
229
  "tuple",
package.json CHANGED
@@ -25,4 +25,4 @@
25
25
  "@iconify-json/vscode-icons": "^1.2.68",
26
26
  "playwright": "^1.62.1"
27
27
  }
28
- }
28
+ }
src/_data/localRepos.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import MarkdownIt from "markdown-it";
2
- import { discoverLocalRepos } from "../_helpers/gitRepos.js";
2
+ import { discoverLocalRepos, resolveReadmeImages } from "../_helpers/gitRepos.js";
3
3
  import { highlightFenceCode } from "../_helpers/highlightCode.js";
4
4
 
5
5
  const md = new MarkdownIt({ html: true, linkify: true, highlight: highlightFenceCode });
@@ -8,6 +8,6 @@ export default async () => {
8
8
  const repos = discoverLocalRepos();
9
9
  return repos.map((repo) => ({
10
10
  ...repo,
11
- readmeHtml: repo.readme ? md.render(repo.readme) : undefined,
11
+ readmeHtml: repo.readme ? resolveReadmeImages(md.render(repo.readme), repo.id, repo.dir) : undefined,
12
12
  }));
13
13
  };
src/_data/repoFiles.js CHANGED
@@ -10,17 +10,13 @@ import {
10
10
  getFileHistory,
11
11
  getHeadSha,
12
12
  isDirty,
13
+ toDataUri,
13
14
  } from "../_helpers/gitRepos.js";
14
15
  import { highlightCode } from "../_helpers/highlightCode.js";
15
16
  import { mapPool } from "../_helpers/pool.js";
16
17
 
17
18
  const CONCURRENCY = 8;
18
19
 
19
- const toDataUri = (buf, ext) => {
20
- const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
21
- return `data:${mime};base64,${buf.toString("base64")}`;
22
- };
23
-
24
20
  const renderFile = (repoDir, file) => {
25
21
  const full = path.join(repoDir, file.path);
26
22
  if (BINARY_EXTENSIONS.has(file.ext)) return { kind: "binary" };
src/_helpers/gitRepos.js CHANGED
@@ -274,6 +274,51 @@ const readReadme = (repoDir) => {
274
274
  return undefined;
275
275
  };
276
276
 
277
+ export const toDataUri = (buf, ext) => {
278
+ const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
279
+ return `data:${mime};base64,${buf.toString("base64")}`;
280
+ };
281
+
282
+ const isAbsoluteSrc = (src) => /^([a-z][a-z0-9+.-]*:)?\/\//i.test(src) || src.startsWith("data:");
283
+
284
+ const IMG_SRC_PATTERNS = [
285
+ /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g,
286
+ /<img\b[^>]*?\bsrc\s*=\s*"([^"]*)"/gi,
287
+ ];
288
+
289
+ const isLocalRepoImage = (repoDir, src) => {
290
+ if (!src || isAbsoluteSrc(src)) return false;
291
+ const full = path.resolve(repoDir, src);
292
+ if (!(full + path.sep).startsWith(path.resolve(repoDir) + path.sep)) return false;
293
+ const ext = path.extname(src).slice(1).toLowerCase();
294
+ return IMAGE_EXTENSIONS.has(ext) && fs.existsSync(full);
295
+ };
296
+
297
+ // READMEs commonly reference screenshots with a path relative to the repo
298
+ // root (`shots/shot1.png`) rather than a URL — that only renders on GitHub
299
+ // because GitHub serves the file itself. The paths this finds get passed to
300
+ // eleventy.config.js, which copies just those files to a `raw/` route
301
+ // alongside the repo, so resolveReadmeImages can point the README at a real
302
+ // URL instead of inlining the bytes as a data URI (which would bloat every
303
+ // page load of the README with base64).
304
+ export const findReadmeImagePaths = (readme, repoDir) => {
305
+ if (!readme) return [];
306
+ const found = new Set();
307
+ for (const re of IMG_SRC_PATTERNS) {
308
+ for (const match of readme.matchAll(re)) {
309
+ const src = match[1];
310
+ if (isLocalRepoImage(repoDir, src)) found.add(path.posix.normalize(src));
311
+ }
312
+ }
313
+ return [...found];
314
+ };
315
+
316
+ export const resolveReadmeImages = (html, repoId, repoDir) =>
317
+ html.replace(/(<img\b[^>]*?\bsrc\s*=\s*")([^"]*)(")/gi, (match, pre, src, post) => {
318
+ if (!isLocalRepoImage(repoDir, src)) return match;
319
+ return `${pre}/repos/${repoId}/raw/${path.posix.normalize(src)}${post}`;
320
+ });
321
+
277
322
  const readCargoVersion = (repoDir) => {
278
323
  const p = path.join(repoDir, "Cargo.toml");
279
324
  if (!fs.existsSync(p)) return undefined;