Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ============================================================
# Win11 Gaming Toolkit — landing site
# ============================================================
# Publishes site/ to the Cloudflare Pages project `tweakeazy`.
#
# Separate from ci.yml on purpose: that gate is PowerShell
# (PSScriptAnalyzer + Pester) and has nothing to say about HTML. This
# one only runs when site/ actually changed, so toolkit commits do not
# redeploy the page.
#
# There is no build step. site/ is hand-written HTML/CSS/JS and is
# uploaded as-is, which is also why site/_headers sits at the root of
# that folder — Pages reads _headers from the top of whatever directory
# it is handed.
# ============================================================
name: deploy-site

on:
push:
branches: [main]
paths: ['site/**', '.github/workflows/deploy-site.yml']
pull_request:
paths: ['site/**', '.github/workflows/deploy-site.yml']
workflow_dispatch:

jobs:
site:
runs-on: ubuntu-latest
# Job level so the deploy step can branch on whether the token
# exists: the `secrets` context is not usable in a step `if:`, but
# `env` is.
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- uses: actions/checkout@v4

# Nothing compiles here, so the only thing worth proving before a
# deploy is that the files the page asks for are actually present.
# A missing stylesheet is a silent 404 in production, not a build
# error.
- name: Check every local asset the page references exists
run: |
missing=0
# Read line by line rather than looping over an unquoted
# expansion: word splitting is a bash-ism, and this should
# behave the same when a dev runs it in zsh locally.
while IFS= read -r r; do
[ -n "$r" ] || continue
path="site/${r%%\?*}"
if [ -f "$path" ]; then
echo " ok $r"
else
echo "::error::index.html references $r but $path does not exist"
missing=1
fi
done <<EOF
$(grep -ohE '(href|src)="[^"]+"' site/index.html \
| sed -E 's/.*="([^"]+)".*/\1/' \
| grep -vE '^(https?:|//|#|mailto:|data:)')
EOF
[ "$missing" -eq 0 ] && echo "all referenced assets present"
exit $missing

# Only a push to main, never a pull request: a PR publishing to
# production would put unreviewed work on the live site. Forks get
# no token, so this cannot fire from them even if the ref check
# were wrong.
#
# `--branch=main` is what marks the deployment as production.
# Without it Pages files every upload as a preview, which
# succeeds, prints a URL, and never moves the live site.
- name: Deploy to Cloudflare Pages
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != ''
run: npx --yes wrangler@4 pages deploy site --project-name=tweakeazy --branch=main

# A push to main that cannot deploy should say so rather than
# report a green run that shipped nothing. Not a failure: the
# check above was fine, and failing here would break the repo for
# anyone forking it without the secrets.
- name: Say so when the deploy was skipped
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && (env.CLOUDFLARE_API_TOKEN == '' || env.CLOUDFLARE_ACCOUNT_ID == '')
run: |
missing=""
[ -z "$CLOUDFLARE_API_TOKEN" ] && missing="$missing CLOUDFLARE_API_TOKEN"
[ -z "$CLOUDFLARE_ACCOUNT_ID" ] && missing="$missing CLOUDFLARE_ACCOUNT_ID"
echo "::warning::Built but not published — missing repository secret(s):$missing"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ coverage.xml
# overrides; don't ship with the repo).
.claude/
.cursorrules

# Wrangler's local cache for site/ deploys. Holds an account-scoped
# credential blob — never commit it.
.wrangler/
40 changes: 40 additions & 0 deletions site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# site/

The toolkit's landing page. Live at
[tweakeazy.pages.dev](https://tweakeazy.pages.dev).

Hand-written HTML, CSS and JS — no build step, no dependencies. What is in
this folder is exactly what gets served.

## Deploying

A push to `main` that touches `site/**` publishes it. Toolkit-only commits do
not redeploy the page.

To publish by hand:

```bash
npx wrangler@4 pages deploy site --project-name=tweakeazy --branch=main
```

`--branch=main` is what makes it a production deploy. Leave it off and Pages
files the upload as a preview, which succeeds, prints a URL, and never moves
the live site.

## `_headers`

Cache and security headers live in `_headers`, at the root of this folder
because that is where Pages reads it from in whatever directory it is handed.

**Every matching rule is applied and concatenated.** Two rules setting
`Cache-Control` on the same path produce one header with two `max-age`s, and
the browser takes the first — so keep each rule's path set disjoint rather
than relying on a later rule to override an earlier one.

## History

This page was previously hosted on Netlify and lived in no repository. It was
recovered from the deployed files in August 2026 when the site moved to
Cloudflare Pages, then brought in here so it has a source of truth. The
original host config was not retrievable, so `_headers` was reconstructed from
the response headers the live site was actually serving.
22 changes: 22 additions & 0 deletions site/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Win11 Gaming Toolkit — response headers, ported from the Netlify config.
#
# The original netlify.toml is not retrievable through the API (it returns
# metadata, not the body), so these were reconstructed from the headers the
# live Netlify site actually served: /assets/* immutable, everything else on
# the platform default. Verified by reading responses for .css, .js, .png and
# .svg before writing this.
#
# Rules must not overlap. Cloudflare applies every match and concatenates
# them, so two rules setting Cache-Control on one path produce a header with
# two max-ages.

/assets/*
Cache-Control: public, max-age=31536000, immutable

# Added, not ported: Netlify served no security headers here beyond its
# automatic HSTS. These cost nothing on a static page and match what the other
# Spiral sites send.
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
14 changes: 14 additions & 0 deletions site/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/hero-workstation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading