From 10898c6867d312d31a6e395eaa364dbd24af1be0 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Wed, 14 Oct 2026 12:49:12 +0200 Subject: [PATCH 1/2] ci(website): deploy the site to Cloudflare Workers instead of GitHub Pages docs.yml now uploads a plain artifact and deploys it with wrangler from a `website` environment that only main may deploy from. The Worker serves static assets only: each locale gets its own 404 page, and content-hashed assets are cached as immutable. A separate Worker (website/www-redirect, deployed by hand) sends www to the apex, as GitHub Pages did. Both Workers sit on zone routes rather than custom domains, which Cloudflare refuses over existing DNS records. The switch was proxying the apex A records and the www CNAME: no record deleted, no downtime, and turning the proxy off goes back to GitHub Pages. --- .github/workflows/build.yml | 2 +- .github/workflows/docs.yml | 48 +++++++++++-------- .../engineering/ci-workflows.md | 4 +- website/.gitignore | 2 + website/README.md | 2 +- website/docusaurus.config.ts | 18 +++---- website/i18n/TRANSLATING.md | 2 +- website/static/_headers | 7 +++ website/wrangler.jsonc | 21 ++++++++ website/www-redirect/index.js | 10 ++++ website/www-redirect/wrangler.jsonc | 11 +++++ 11 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 website/static/_headers create mode 100644 website/wrangler.jsonc create mode 100644 website/www-redirect/index.js create mode 100644 website/www-redirect/wrangler.jsonc diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4fa82600b..3c23c78a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1100,7 +1100,7 @@ jobs: # Only a stable release changes what /releases/latest resolves to, so a # pre-release would rebuild the site to byte-identical output. # - # Dispatched against main on purpose: the github-pages environment only + # Dispatched against main on purpose: the website environment only # permits `main` to deploy, so docs.yml's old `on: release` trigger ran # with a tag ref and failed its deploy every time. See docs.yml. if: ${{ steps.release.outputs.is_prerelease == 'false' }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8565f14e9..3575c0249 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,8 +18,8 @@ on: # # That rebuild is a `workflow_dispatch` fired by build.yml once the release is # published, NOT an `on: release` trigger. A release event runs with - # github.ref = refs/tags/vX.Y.Z, and the github-pages environment only allows - # `main` to deploy, so the deploy job failed on every stable release (it never + # github.ref = refs/tags/vX.Y.Z, and the deploy environment only allows `main` + # to deploy, so the deploy job failed on every stable release (it never # surfaced earlier because pre-releases skipped the build entirely). Dispatching # against main both satisfies that policy and publishes main's docs rather than # the release branch's older snapshot. @@ -119,33 +119,41 @@ jobs: exit 1 fi - name: Upload artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: + name: website-build path: website/build - # v4 stopped bundling dotfiles by default. Nothing in website/build - # is hidden today — the deployed artifact from 7c4827b9 was checked - # and contains no dot-entries at all, so this changes nothing now - # (Docusaurus only writes a `.nojekyll` on its own `deploy` command, - # not in a plain `build`). It is set so that stays true by accident - # rather than by omission: a `.well-known/` for domain verification - # or security.txt is the realistic case, and under the v4+ default it - # would vanish from the deploy silently. `.git` and `.github` are - # excluded regardless of this flag. + retention-days: 7 + # Off by default since v4. Nothing in website/build is hidden today, + # but a `.well-known/` for domain verification or security.txt is the + # realistic case, and it would vanish from the deploy silently. include-hidden-files: true + # The site is a Cloudflare Worker with static assets only (website/wrangler.jsonc). + # The token lives in the `website` environment, which only `main` may deploy + # from, so neither a PR nor a dispatch on another branch can read it. deploy: - name: Deploy to GitHub Pages + name: Deploy to Cloudflare runs-on: ubuntu-latest needs: build if: >- (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - permissions: - pages: write - id-token: write + name: website + url: https://getopenscreen.com steps: - - id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + sparse-checkout: website/wrangler.jsonc + sparse-checkout-cone-mode: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: website-build + path: website/build + - name: Deploy + working-directory: website + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: npx --yes wrangler@4.145.0 deploy diff --git a/technical-documentation/engineering/ci-workflows.md b/technical-documentation/engineering/ci-workflows.md index 2f1b61c7a..cc3ecea65 100644 --- a/technical-documentation/engineering/ci-workflows.md +++ b/technical-documentation/engineering/ci-workflows.md @@ -13,7 +13,7 @@ flowchart TD CI --> BuildSmoke[Vite build] CI --> Semantic[semantic PR title] Docs["docs.yml
website changes or dispatch"] --> DocsBuild[website build artifact] - DocsBuild --> DocsDeploy[GitHub Pages deploy on main] + DocsBuild --> DocsDeploy[Cloudflare deploy on main] end subgraph T2["Tier 2 — native and release builds"] @@ -138,4 +138,4 @@ Each workflow needs variables or credentials, and where it checks for them decid Release platform artifacts have 30-day Actions retention. `publish-release` copies the NSIS installer, two architecture-specific DMGs when present, and Linux packages into the GitHub release. It intentionally does not download the independently uploaded Store artifact. A stable published release then fans out to Homebrew, WinGet, and AUR. -Diagnostic artifacts remain Actions-only for 14 days. Whisper archives remain Actions-only for 30 days and are used as a binary-refresh output rather than being downloaded by the release workflow. The website build artifact flows only into GitHub Pages deployment. +Diagnostic artifacts remain Actions-only for 14 days. Whisper archives remain Actions-only for 30 days and are used as a binary-refresh output rather than being downloaded by the release workflow. The website build artifact flows only into the Cloudflare deployment, which reads its token from the `website` environment, restricted to `main`. diff --git a/website/.gitignore b/website/.gitignore index 01c29630f..98f0dd340 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -5,6 +5,8 @@ node_modules/ build/ .docusaurus/ .cache-loader/ +# wrangler dev's local state +.wrangler/ # Generated files .bundle-stats diff --git a/website/README.md b/website/README.md index 3d6f41ac8..2f9f5e181 100644 --- a/website/README.md +++ b/website/README.md @@ -1,6 +1,6 @@ # OpenScreen docs site -Docusaurus 3 site. Lives in `website/` inside the monorepo. Deployed to GitHub Pages via `.github/workflows/docs.yml`. +Docusaurus 3 site. Lives in `website/` inside the monorepo. Deployed to Cloudflare (a Worker serving static assets, `wrangler.jsonc`) via `.github/workflows/docs.yml`. `www-redirect/` is a separate Worker that sends www.getopenscreen.com to the apex; it is deployed by hand. ## Develop diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 3ba10bfca..ee792e6d8 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -333,13 +333,13 @@ export default async function createConfig(): Promise { tagline: "A free, open-source screen recorder and editor.", favicon: "img/logo-icon.png", - // Pages serves this from the custom domain's root, not from - // getopenscreen.github.io/openscreen/, so baseUrl has to be "/" — a project - // baseUrl would prefix every asset URL with a path the server has nothing at. + // Served from the custom domain's root, so baseUrl has to be "/": a + // project baseUrl would prefix every asset URL with a path the server has + // nothing at. url: SITE_URL, baseUrl: "/", - // Every page is emitted as /index.html, and GitHub Pages 301s the + // Every page is emitted as /index.html, and Cloudflare redirects the // extensionless form to the trailing-slash one. Leaving this unset makes // Docusaurus advertise the pre-redirect URL in both // and sitemap.xml, so every indexed URL costs a crawler an extra hop to a @@ -439,11 +439,13 @@ export default async function createConfig(): Promise { // for a renamed tag: a translated build has no blog, so that copy // would be the only thing under /fr/blog/, a page nothing links to, // redirecting a French URL to an English one. /fr/discord/ would be - // a second, unlinked copy of the invite redirect, and crawlers read - // llms.txt and robots.txt at the root only. The rest (img, video) - // stays: pages resolve images through the locale's baseUrl. + // a second, unlinked copy of the invite redirect, crawlers read + // llms.txt and robots.txt at the root only, and Cloudflare reads + // _headers there only, serving any other copy as a public file. The + // rest (img, video) stays: pages resolve images through the locale's + // baseUrl. if (LOCALE !== "en") { - for (const entry of [BLOG_PATH, "discord", "llms.txt", "robots.txt"]) { + for (const entry of [BLOG_PATH, "discord", "llms.txt", "robots.txt", "_headers"]) { await rm(path.join(outDir, entry), { recursive: true, force: true }); } } diff --git a/website/i18n/TRANSLATING.md b/website/i18n/TRANSLATING.md index 009904e3d..1920df10d 100644 --- a/website/i18n/TRANSLATING.md +++ b/website/i18n/TRANSLATING.md @@ -6,7 +6,7 @@ The site is translated into `fr`, `es`, `pt-BR`, `ja`, `zh-CN`, `zh-TW` and `de` ## What is translated, and what is not -- **Translated:** everything but the blog: the landing page, `/download/`, the docs, the comparison, platform and feature pages (`src/pages/**/*.mdx`), the navbar, the docs sidebar, the footer and the theme (404 page, pagination, "last updated"…). The translated 404 page only shows after a click inside the site: GitHub Pages answers every missing URL, `/fr/…` included, with the English one. +- **Translated:** everything but the blog: the landing page, `/download/`, the docs, the comparison, platform and feature pages (`src/pages/**/*.mdx`), the navbar, the docs sidebar, the footer and the theme (404 page, pagination, "last updated"…). A missing URL under `//` gets that locale's 404 page. - **English only:** the blog, a dated development journal. A translated build does not contain it, and links to it from translated pages go to the English blog (see below). - **The build enforces it:** a locale missing any doc or `.mdx` page fails `npm run build`, because Docusaurus would otherwise publish the English file under `//` marked as your language. diff --git a/website/static/_headers b/website/static/_headers new file mode 100644 index 000000000..a796a253b --- /dev/null +++ b/website/static/_headers @@ -0,0 +1,7 @@ +# Read by Cloudflare at the root of the deployed build (website/wrangler.jsonc). +# Docusaurus puts a content hash in the name of every file under assets/, so a +# cached copy can never go stale. +/assets/* + Cache-Control: public, max-age=31536000, immutable +/:locale/assets/* + Cache-Control: public, max-age=31536000, immutable diff --git a/website/wrangler.jsonc b/website/wrangler.jsonc new file mode 100644 index 000000000..294ef1296 --- /dev/null +++ b/website/wrangler.jsonc @@ -0,0 +1,21 @@ +// Cloudflare Workers config for getopenscreen.com, deployed by .github/workflows/docs.yml. +// Assets only, no Worker script: Cloudflare serves static assets without counting +// them against the Workers request quota. +{ + "name": "getopenscreen-website", + // An identifier, not a credential: the API token in CI is the secret. + "account_id": "49f0ef8f15d646deb80a7e7d675a4910", + "compatibility_date": "2026-10-01", + "assets": { + "directory": "./build", + // The nearest 404.html, so /fr/missing/ gets the French page, not the English one. + "not_found_handling": "404-page", + }, + // A route on the proxied apex, not a custom domain: Cloudflare will not attach a + // custom domain over existing DNS records (error 100117). With a route, the move off + // GitHub Pages was only switching those records to proxied, with no gap. + "routes": [{ "pattern": "getopenscreen.com/*", "zone_name": "getopenscreen.com" }], + // The site has one address; a workers.dev copy would be a duplicate of it. + "workers_dev": false, + "preview_urls": false, +} diff --git a/website/www-redirect/index.js b/website/www-redirect/index.js new file mode 100644 index 000000000..e094f2b6e --- /dev/null +++ b/website/www-redirect/index.js @@ -0,0 +1,10 @@ +// www.getopenscreen.com only: every request goes to the same path on the apex, as +// GitHub Pages did. Deployed by hand (`npx wrangler deploy` in this folder); it has no +// reason to change with the site. +export default { + fetch(request) { + const url = new URL(request.url); + url.hostname = "getopenscreen.com"; + return Response.redirect(url.toString(), 301); + }, +}; diff --git a/website/www-redirect/wrangler.jsonc b/website/www-redirect/wrangler.jsonc new file mode 100644 index 000000000..0d72cd703 --- /dev/null +++ b/website/www-redirect/wrangler.jsonc @@ -0,0 +1,11 @@ +// Redirects www to the apex (index.js). The site itself is ../wrangler.jsonc. +{ + "name": "getopenscreen-www-redirect", + "account_id": "49f0ef8f15d646deb80a7e7d675a4910", + "main": "index.js", + "compatibility_date": "2026-10-01", + "workers_dev": false, + "preview_urls": false, + // A route, like the site's: the www record only has to be proxied. + "routes": [{ "pattern": "www.getopenscreen.com/*", "zone_name": "getopenscreen.com" }], +} From d2f3ff12c9bf3b5e8202e042bab001bf39972c6c Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Wed, 14 Oct 2026 14:41:25 +0200 Subject: [PATCH 2/2] fix(website): format the wrangler configs, redirect www straight to https, refresh README notes --- website/README.md | 6 ++---- website/wrangler.jsonc | 4 ++-- website/www-redirect/index.js | 1 + website/www-redirect/wrangler.jsonc | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/website/README.md b/website/README.md index 2f9f5e181..b9a5baf9f 100644 --- a/website/README.md +++ b/website/README.md @@ -7,7 +7,7 @@ Docusaurus 3 site. Lives in `website/` inside the monorepo. Deployed to Cloudfla ```sh cd website npm install -npm run dev # http://localhost:3000/openscreen/ +npm run dev # http://localhost:3000/ ``` ## Build @@ -25,6 +25,4 @@ npm run typecheck ## Notes -- Site URL: -- The base URL is `/openscreen/` because the org-level Pages site hosts multiple projects. -- Docs migration from `docs/` (root) → `website/docs/` happens in a follow-up PR. For now only the intro page is published. \ No newline at end of file +- Site URL: , served from the domain root (`baseUrl: "/"`). diff --git a/website/wrangler.jsonc b/website/wrangler.jsonc index 294ef1296..3522ac9dd 100644 --- a/website/wrangler.jsonc +++ b/website/wrangler.jsonc @@ -9,7 +9,7 @@ "assets": { "directory": "./build", // The nearest 404.html, so /fr/missing/ gets the French page, not the English one. - "not_found_handling": "404-page", + "not_found_handling": "404-page" }, // A route on the proxied apex, not a custom domain: Cloudflare will not attach a // custom domain over existing DNS records (error 100117). With a route, the move off @@ -17,5 +17,5 @@ "routes": [{ "pattern": "getopenscreen.com/*", "zone_name": "getopenscreen.com" }], // The site has one address; a workers.dev copy would be a duplicate of it. "workers_dev": false, - "preview_urls": false, + "preview_urls": false } diff --git a/website/www-redirect/index.js b/website/www-redirect/index.js index e094f2b6e..a6d936df9 100644 --- a/website/www-redirect/index.js +++ b/website/www-redirect/index.js @@ -4,6 +4,7 @@ export default { fetch(request) { const url = new URL(request.url); + url.protocol = "https:"; url.hostname = "getopenscreen.com"; return Response.redirect(url.toString(), 301); }, diff --git a/website/www-redirect/wrangler.jsonc b/website/www-redirect/wrangler.jsonc index 0d72cd703..3c86cb9ba 100644 --- a/website/www-redirect/wrangler.jsonc +++ b/website/www-redirect/wrangler.jsonc @@ -7,5 +7,5 @@ "workers_dev": false, "preview_urls": false, // A route, like the site's: the www record only has to be proxied. - "routes": [{ "pattern": "www.getopenscreen.com/*", "zone_name": "getopenscreen.com" }], + "routes": [{ "pattern": "www.getopenscreen.com/*", "zone_name": "getopenscreen.com" }] }