website
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 +4 -3
- scripts/apply-cloudflare-rules.ts +154 -0
- wrangler.jsonc +0 -12
- wrangler.toml +46 -0
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
|
-
"
|
|
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"
|