1# gcp-static-site
2
3A static website served from a public Cloud Storage bucket. The GCP
4counterpart of [`aws-static-site`](../aws-static-site).
5
6```
7site/ ──GCP.Storage.Files──> gs://…/index.html, styles.css, docs/index.html, 404.html
8 │ allUsers: roles/storage.objectViewer
9 v
10 https://storage.googleapis.com/<bucket>/index.html
11```
12
13- `GCP.Storage.Bucket("Site")` — uniform bucket-level access, website
14 config `mainPageSuffix: "index.html"` and `notFoundPage: "404.html"`,
15 and `forceDestroy: true` so destroy empties it.
16- `GCP.IAM.Member("PublicRead")` — grants `allUsers`
17 `roles/storage.objectViewer` on the bucket's own IAM policy. Anyone can
18 read (and list) the objects; nothing else in the project is exposed.
19- `GCP.Storage.Files("SiteFiles")` — one `GCP.Storage.Object` per file
20 under `site/`, keyed by its relative path, with `Content-Type` inferred
21 from the extension (`text/html; charset=utf-8`, `text/css; charset=utf-8`,
22 `image/png`, …). HTML is served `Cache-Control: no-cache`; everything
23 else `public, max-age=300`.
24
25There are no Functions, bindings, or event sources — nothing runs
26server-side.
27
28## How redeploys work
29
30Each `Object` compares the MD5 Cloud Storage reports with the MD5 of the
31local file (plus its `Content-Type` and `Cache-Control`). A redeploy
32uploads only files whose bytes or headers changed, and deletes the objects
33of files you removed from `site/`.
34
35## Links on path-style URLs
36
37On `https://storage.googleapis.com/<bucket>/…` the bucket name is part of
38the path, so the pages link to each other with relative URLs
39(`styles.css`, `docs/index.html`). Root-relative links (`/styles.css`)
40only work once the site is served from its own domain.
41
42## Custom domain and HTTPS
43
44The website config (`mainPageSuffix`, `notFoundPage`) only takes effect
45when the bucket is served from a domain: a `/docs/` → `docs/index.html`
46rewrite and the `404.html` body are not applied to path-style URLs, which
47answer a missing object with Cloud Storage's own 404. Serving the site on
48a custom domain over HTTPS requires a global external Application Load
49Balancer with a backend bucket, a managed certificate, and a DNS record —
50out of scope for this example.
51
52## Deploy
53
54Credentials come from your alchemy profile: run `alchemy profile` once and pick GCP (*Service account JSON* for a key file, or *Stored* for an access token or key kept in `~/.alchemy/credentials`, plus a default region), then deploy with `--profile <name>`. No Docker needed.
55
56```sh
57pnpm deploy --profile <name>
58```
59
60```sh
61curl -i "$url" # index.html
62curl -i "https://storage.googleapis.com/$bucketName/styles.css"
63```
64
65The project must allow public buckets: the
66`constraints/storage.publicAccessPrevention` and
67`constraints/iam.allowedPolicyMemberDomains` organization policies both
68block `allUsers` grants.
69
70## Test
71
72```sh
73ALCHEMY_PROFILE=<name> bun test
74```
75
76Deploys the stack, fetches `index.html`, `styles.css`, `docs/index.html`
77and `404.html` anonymously and checks each body, `Content-Type` and
78`Cache-Control`, checks a missing path is a 404, reads the bucket's
79website config and IAM policy back from the Cloud Storage API, then
80destroys the stack and checks the bucket is gone.
81
82## Destroy
83
84```sh
85pnpm destroy --profile <name>
86```