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..b9a5baf9f 100644 --- a/website/README.md +++ b/website/README.md @@ -1,13 +1,13 @@ # 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 ```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/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..3522ac9dd --- /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..a6d936df9 --- /dev/null +++ b/website/www-redirect/index.js @@ -0,0 +1,11 @@ +// 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.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 new file mode 100644 index 000000000..3c86cb9ba --- /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" }] +}