website

#astro#js#html#css

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

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


be9362f — pyrossh 2026-06-21T19:38:38+05:30
changes

Generated files not shown: bun.lock

package.json CHANGED
@@ -11,13 +11,13 @@
11
11
  "build": "astro build",
12
12
  "build:resume:pdf": "typst compile src/assets/pdfs/resume.typ -f pdf src/assets/pdfs/resume.pdf",
13
13
  "build:resume:svg": "typst compile src/assets/pdfs/resume.typ -f svg src/assets/pdfs/resume.svg",
14
- "deploy": "alchemy deploy --stage prd"
14
+ "cloudflare:rules": "bun run scripts/apply-cloudflare-rules.ts",
15
+ "deploy": "bun run build && wrangler deploy --config wrangler.toml --env=\"\""
15
16
  },
16
17
  "dependencies": {
17
18
  "@appzic/astro-reset-css": "^1.2.0",
18
19
  "@astrojs/node": "^10.1.1",
19
20
  "@aws-sdk/client-s3": "^3.1053.0",
20
- "alchemy": "^0.93.7",
21
21
  "astro": "6.3.1",
22
22
  "astro-color-scheme": "^1.1.6",
23
23
  "astro-expressive-code": "^0.42.0",
@@ -43,6 +43,7 @@
43
43
  "@playwright/test": "^1.52.0",
44
44
  "@types/bun": "^1.3.2",
45
45
  "diff2html": "^3.4.52",
46
- "vscode-icons": "https://github.com/vscode-icons/vscode-icons.git"
46
+ "vscode-icons": "https://github.com/vscode-icons/vscode-icons.git",
47
+ "wrangler": "^4.94.0"
47
48
  }
48
49
  }
scripts/apply-cloudflare-rules.ts ADDED
@@ -0,0 +1,154 @@
1
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
2
+ const zoneName = process.env.CLOUDFLARE_ZONE_NAME ?? "pyrossh.dev";
3
+ const rulesetName = process.env.CLOUDFLARE_REWRITE_RULESET_NAME ?? "rewrite-rules-prd";
4
+ const apiBase = "https://api.cloudflare.com/client/v4";
5
+
6
+ if (!apiToken) {
7
+ throw new Error("Missing CLOUDFLARE_API_TOKEN.");
8
+ }
9
+
10
+ type CloudflareResponse<T> = {
11
+ success: boolean;
12
+ result: T;
13
+ errors: Array<{ code: number; message: string }>;
14
+ };
15
+
16
+ type Zone = {
17
+ id: string;
18
+ name: string;
19
+ };
20
+
21
+ type Ruleset = {
22
+ id: string;
23
+ name: string;
24
+ kind: string;
25
+ phase: string;
26
+ rules: Rule[];
27
+ };
28
+
29
+ type Rule = {
30
+ description?: string;
31
+ expression: string;
32
+ action: string;
33
+ action_parameters?: unknown;
34
+ enabled?: boolean;
35
+ };
36
+
37
+ const cloudflare = async <T>(
38
+ path: string,
39
+ init?: RequestInit & { allowNotFound?: boolean },
40
+ ) => {
41
+ const response = await fetch(`${apiBase}${path}`, {
42
+ method: init?.method,
43
+ body: init?.body,
44
+ headers: {
45
+ authorization: `Bearer ${apiToken}`,
46
+ "content-type": "application/json",
47
+ ...init?.headers,
48
+ },
49
+ });
50
+ const payload = (await response.json()) as CloudflareResponse<T>;
51
+
52
+ if (init?.allowNotFound && response.status === 404) {
53
+ return undefined;
54
+ }
55
+
56
+ if (!response.ok || !payload.success) {
57
+ const message = payload.errors
58
+ ?.map((error) => `${error.code}: ${error.message}`)
59
+ .join("; ");
60
+ throw new Error(message || `Cloudflare API request failed: ${response.status}`);
61
+ }
62
+
63
+ return payload.result;
64
+ };
65
+
66
+ const getZoneId = async () => {
67
+ if (process.env.CLOUDFLARE_ZONE_ID) {
68
+ return process.env.CLOUDFLARE_ZONE_ID;
69
+ }
70
+
71
+ const zones = await cloudflare<Zone[]>(
72
+ `/zones?name=${encodeURIComponent(zoneName)}`,
73
+ );
74
+ const zone = zones.find((item) => item.name === zoneName);
75
+
76
+ if (!zone) {
77
+ throw new Error(`Cloudflare zone not found: ${zoneName}`);
78
+ }
79
+
80
+ return zone.id;
81
+ };
82
+
83
+ const makeRewriteRules = (host: string): Rule[] => [
84
+ {
85
+ description: "Rewrite trailing slash to index.html",
86
+ expression: `(ends_with(http.request.uri.path, "/") and http.host eq "${host}")`,
87
+ action: "rewrite",
88
+ action_parameters: {
89
+ uri: {
90
+ path: {
91
+ expression: `concat(http.request.uri.path, "index.html")`,
92
+ },
93
+ },
94
+ },
95
+ },
96
+ {
97
+ description: "Rewrite non-file paths to path/index.html",
98
+ expression: `(not ends_with(http.request.uri.path, "/") and not http.request.uri.path contains "." and http.host eq "${host}")`,
99
+ action: "rewrite",
100
+ action_parameters: {
101
+ uri: {
102
+ path: {
103
+ expression: `concat(http.request.uri.path, "/index.html")`,
104
+ },
105
+ },
106
+ },
107
+ },
108
+ {
109
+ description: "Rewrite /files/ paths with extension to append /index.html",
110
+ expression: `(http.request.uri.path contains "/files/" and http.request.uri.path contains "." and not ends_with(http.request.uri.path, "/") and http.host eq "${host}")`,
111
+ action: "rewrite",
112
+ action_parameters: {
113
+ uri: {
114
+ path: {
115
+ expression: `concat(http.request.uri.path, "/index.html")`,
116
+ },
117
+ },
118
+ },
119
+ },
120
+ ];
121
+
122
+ const rewriteRules = makeRewriteRules(zoneName);
123
+ const rewriteRuleDescriptions = new Set(
124
+ rewriteRules.map((rule) => rule.description),
125
+ );
126
+
127
+ const makeRulesetBody = (rules: Rule[], existingRuleset?: Ruleset) => ({
128
+ name: rulesetName,
129
+ kind: existingRuleset?.kind ?? "zone",
130
+ phase: existingRuleset?.phase ?? "http_request_transform",
131
+ rules,
132
+ });
133
+
134
+ const zoneId = await getZoneId();
135
+ const entrypointPath = `/zones/${zoneId}/rulesets/phases/http_request_transform/entrypoint`;
136
+ const existingRuleset = await cloudflare<Ruleset>(entrypointPath, {
137
+ allowNotFound: true,
138
+ });
139
+ const existingRules = existingRuleset?.rules ?? [];
140
+ const nextRules = [
141
+ ...existingRules.filter(
142
+ (rule) => !rewriteRuleDescriptions.has(rule.description),
143
+ ),
144
+ ...rewriteRules,
145
+ ];
146
+
147
+ const ruleset = await cloudflare<Ruleset>(entrypointPath, {
148
+ method: "PUT",
149
+ body: JSON.stringify(makeRulesetBody(nextRules, existingRuleset)),
150
+ });
151
+
152
+ console.log(
153
+ `${existingRuleset ? "Updated" : "Created"} Cloudflare ruleset ${ruleset.name} (${ruleset.id}).`,
154
+ );
wrangler.jsonc DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "$schema": "node_modules/wrangler/config-schema.json",
3
- "name": "pyrossh-website",
4
- "compatibility_date": "2026-05-15",
5
- "r2_buckets": [
6
- {
7
- "binding": "REPOS",
8
- "bucket_name": "pyrossh-repos-prd",
9
- "preview_bucket_name": "pyrossh-repos-dev"
10
- }
11
- ]
12
- }
wrangler.toml ADDED
@@ -0,0 +1,46 @@
1
+ name = "pyrossh-website"
2
+ main = "dist/server/entry.mjs"
3
+ compatibility_date = "2026-05-15"
4
+ compatibility_flags = ["nodejs_compat"]
5
+ workers_dev = false
6
+
7
+ # Build locally before deploying:
8
+ # bun run build
9
+ #
10
+ # Alchemy also provisioned Cloudflare zone settings, R2 bucket custom domains,
11
+ # and transform rules. Wrangler does not manage those account/zone resources
12
+ # from wrangler.toml; keep them configured in the Cloudflare dashboard or via
13
+ # a separate Cloudflare API/Terraform workflow.
14
+
15
+ [[routes]]
16
+ pattern = "pyrossh.dev"
17
+ custom_domain = true
18
+
19
+ [assets]
20
+ directory = "./dist/client"
21
+ binding = "ASSETS"
22
+ not_found_handling = "none"
23
+ run_worker_first = true
24
+
25
+ [observability]
26
+ enabled = true
27
+
28
+ [vars]
29
+ REPOS_R2_BUCKET = "pyrossh-repos-prd"
30
+
31
+ [[r2_buckets]]
32
+ binding = "REPOS"
33
+ bucket_name = "pyrossh-repos-prd"
34
+ preview_bucket_name = "pyrossh-repos-dev"
35
+
36
+ [env.dev]
37
+ name = "pyrossh-website-dev"
38
+ workers_dev = true
39
+
40
+ [env.dev.vars]
41
+ REPOS_R2_BUCKET = "pyrossh-repos-dev"
42
+
43
+ [[env.dev.r2_buckets]]
44
+ binding = "REPOS"
45
+ bucket_name = "pyrossh-repos-dev"
46
+ preview_bucket_name = "pyrossh-repos-dev"