diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e11c81136..941da3094 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -84,6 +84,10 @@ jobs: for _ in {1..15}; do if curl --max-time 2 --fail --silent http://127.0.0.1:8789/ > /dev/null; then + curl --max-time 2 --fail --silent http://127.0.0.1:8789/robots.txt | cmp public/robots.txt - + for guide_icon in build/client/assets/*.avif; do + curl --max-time 2 --fail --silent "http://127.0.0.1:8789/${guide_icon#build/client/}" | cmp "$guide_icon" - + done exit 0 fi sleep 1 diff --git a/.gitignore b/.gitignore index 001b864b5..177e198e7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ node_modules/ .next/ out/ apps/docs/out/ -apps/guide/out/ index_data/*.json apps/docs/index_data/*.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json index c94351978..f4e51e821 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -12,6 +12,7 @@ "*.swo", "*.swp", "apps/docs/app/**/*.mjs", + "apps/guide/app/**/*.jsx", "build", "data/api/**/*.json", "apps/docs/data/api/**/*.json", diff --git a/apps/guide/README.md b/apps/guide/README.md index 4dd962e6e..f536d4e86 100644 --- a/apps/guide/README.md +++ b/apps/guide/README.md @@ -16,6 +16,7 @@ contract only. Lesson copy belongs to the guide author. | `app/GuideHome.res` | Guide workspace UI | | `app/GuideCompilerBridgeHook.res` | Browser compiler and execution integration | | `styles/main.css` | Guide-only visual system and responsive behavior | +| `public/` | Guide static assets, including `robots.txt` | | `__tests__/` | Browser and unit coverage for the guide | ## Lesson Contract @@ -71,9 +72,12 @@ yarn build:guide yarn workspace @rescript-lang/guide ci:test ``` -`yarn build:guide` compiles ReScript, pre-renders `/`, and copies the client -assets to `apps/guide/out/` for Wrangler. The guide fetches available compiler -versions from `https://cdn.rescript-lang.org` during pre-rendering; the +`yarn build:guide` compiles ReScript and pre-renders `/` into +`apps/guide/build/client/`, the assets directory configured in `wrangler.toml`. +Vite serves and copies the guide's `public/` directory, including `robots.txt`. +The root route imports the shared docs favicons so Vite emits them with the +guide's other assets. The guide fetches available compiler versions from +`https://cdn.rescript-lang.org` during pre-rendering; the interactive compiler and runtime then run in the learner's browser. ## Current Capability @@ -92,7 +96,7 @@ show a stop screen rather than a usable guide. ## Production Checklist -Before launch, complete and verify the following: +Track outstanding guide launch work in this checklist: - [ ] Author, review, and run every lesson using its real expected output. - [ ] Add a content-validation test for the complete lesson set, including @@ -108,6 +112,7 @@ Before launch, complete and verify the following: compiler and checkpoint changes. - [ ] Choose mobile support or explicitly publish the desktop-only constraint with an alternate learning path. +- [ ] Define the supported browsers and initial page-load performance budget. - [ ] Split or defer the initial compiler/editor code so the initial route is within the agreed performance budget. - [ ] Replace the deprecated Vite `envFile` configuration and resolve the @@ -121,6 +126,6 @@ Before launch, complete and verify the following: ## Deployment The `deploy-guide` job in `.github/workflows/deploy.yml` builds the guide and -deploys it with Wrangler. Pushes to `master` deploy the production Worker; -non-Dependabot pull requests from this repository receive a preview Worker +deploys its `build/client/` assets with Wrangler. Pushes to `master` deploy the +production Worker; non-Dependabot pull requests from this repository receive a preview Worker version and a link in the pull request. diff --git a/apps/guide/TODO.md b/apps/guide/TODO.md deleted file mode 100644 index b4b7c2011..000000000 --- a/apps/guide/TODO.md +++ /dev/null @@ -1,73 +0,0 @@ -# Guide MVP Launch TODO - -This checklist is for making the interactive guide ready to launch. It does not -cover writing or editing lesson content. - -## 1. Define The Launch Boundary - -- [ ] Decide the public guide URL and Cloudflare Worker custom domain. -- [ ] Record that the initial release requires a desktop browser. -- [ ] Define the supported browsers and the performance budget for the initial - page load. -- [ ] Assign ownership for deployment, CDN/compiler availability, learner - support, and production incident response. - -## 2. Complete The Exercise System - -- [ ] Choose a completion strategy for exercises that cannot be checked with - one exact runtime-output line. -- [ ] Implement that strategy without allowing an incomplete lesson to unlock - the next one. -- [ ] Decide whether learners need an explicit reset action for one exercise, - all progress, or both. -- [ ] Decide how published lesson or exercise identifier changes preserve or - intentionally reset browser-local progress. - -## 3. Make Lesson Structure Safe To Publish - -- [ ] Add build-time validation that lesson IDs, exercise IDs, and positions - are unique. -- [ ] Add tests for the complete lesson collection rather than fixtures only. -- [ ] Add tests for invalid or incomplete lesson frontmatter. -- [ ] Verify every published exercise against the production compiler version - and its expected runtime output. -- [ ] Establish a human review path for lesson accuracy, progression, and - accessibility. Lesson writing remains author-owned. - -## 4. Finish The Product Surface - -- [ ] Add the production document metadata, favicon, social preview asset, and - canonical URL. -- [ ] Decide and implement robots and sitemap/discovery behavior for the guide - domain. -- [ ] Add keyboard-operable pane resizing or replace resize controls with an - accessible alternative. -- [ ] Add visible focus styles and announce compiler errors, output changes, - and checkpoint state changes to assistive technology. -- [x] Show a clear small-screen warning that the guide requires a desktop - browser. - -## 5. Reduce Delivery Risk - -- [ ] Split or defer the compiler/editor bundle to meet the agreed initial-load - budget. The current initial guide route is 1.61 MB uncompressed (463 KB - gzip). -- [ ] Replace the deprecated Vite `envFile` option. -- [ ] Resolve the guide workspace's `@types/react` peer-dependency warning. -- [ ] Decide whether compiler-version metadata should be available at build - time, at runtime, or both when the CDN is unavailable. -- [ ] Add graceful user-visible handling for compiler-CDN and runtime failures. - -## 6. Prove The Release Path - -- [ ] Ensure the pinned Playwright Chromium browser is installed in local and - CI environments, then run `yarn workspace @rescript-lang/guide ci:test`. -- [ ] Add a production-preview smoke test covering page load, compiler - availability, one successful checkpoint, hash deep link, persisted draft, - and final navigation. -- [ ] Test the Cloudflare preview Worker with the intended production domain - configuration and SPA fallback behavior. -- [ ] Define rollback steps and verify that a prior Worker version can be - restored. -- [ ] Record the final launch decision and remaining accepted risks in the - release pull request. diff --git a/apps/guide/app/root.jsx b/apps/guide/app/root.jsx index 798528bea..d08a1138e 100644 --- a/apps/guide/app/root.jsx +++ b/apps/guide/app/root.jsx @@ -3,56 +3,80 @@ import * as ReactRouter from "react-router"; import * as JsxRuntime from "react/jsx-runtime"; import MainCssurl from "../styles/main.css?url"; +import FaviconX16AvifurlnoInline from "../../docs/public/favicon/favicon-16x16.avif?url&no-inline"; +import FaviconX32AvifurlnoInline from "../../docs/public/favicon/favicon-32x32.avif?url&no-inline"; +import AppleTouchIconAvifurlnoInline from "../../docs/public/favicon/apple-touch-icon.avif?url&no-inline"; let mainCss = MainCssurl; +let appleTouchIcon = AppleTouchIconAvifurlnoInline; + +let favicon32 = FaviconX32AvifurlnoInline; + +let favicon16 = FaviconX16AvifurlnoInline; + function Root$default(props) { - return ( - - - - - - - - - - - - - - {"ReScript Guide"} - - - - - - - - ); + return + + + + + + + + + + + + + + {"ReScript Guide"} + + + + + + + + ; } let $$default = Root$default; -export { $$default as default }; +export { + $$default as default, +} /* mainCss Not a pure module */ diff --git a/apps/guide/app/root.res b/apps/guide/app/root.res index d90e70733..d817565df 100644 --- a/apps/guide/app/root.res +++ b/apps/guide/app/root.res @@ -1,6 +1,16 @@ @module("../styles/main.css?url") external mainCss: string = "default" +// Share the official icons while keeping the guide's own public directory. +@module("../../docs/public/favicon/apple-touch-icon.avif?url&no-inline") +external appleTouchIcon: string = "default" + +@module("../../docs/public/favicon/favicon-32x32.avif?url&no-inline") +external favicon32: string = "default" + +@module("../../docs/public/favicon/favicon-16x16.avif?url&no-inline") +external favicon16: string = "default" + @react.component let default = () => { @@ -13,9 +23,9 @@ let default = () => { - - - + + + {React.string("ReScript Guide")} diff --git a/apps/guide/app/root.resi b/apps/guide/app/root.resi index 47fa1ac2e..7e91a2ebb 100644 --- a/apps/guide/app/root.resi +++ b/apps/guide/app/root.resi @@ -1,2 +1,3 @@ +// Keep build asset bindings private; React Router consumes the root component. @react.component let default: unit => React.element diff --git a/apps/guide/react-router.config.mjs b/apps/guide/react-router.config.mjs index df41a8ad8..634528bc5 100644 --- a/apps/guide/react-router.config.mjs +++ b/apps/guide/react-router.config.mjs @@ -1,9 +1,4 @@ -import * as fs from "node:fs"; - export default { ssr: false, prerender: ["/"], - buildEnd: async () => { - fs.cpSync("./build/client", "./out", { recursive: true }); - }, }; diff --git a/apps/guide/vite.config.mjs b/apps/guide/vite.config.mjs index fc20d334c..b6e1594e1 100644 --- a/apps/guide/vite.config.mjs +++ b/apps/guide/vite.config.mjs @@ -42,8 +42,6 @@ export default defineConfig(({ mode }) => { return { envDir: "../..", - // Reuse the official favicon set without duplicating branding assets. - publicDir: "../docs/public/favicon", plugins: [ env({ prefix: "PUBLIC_" }), ...(isTest ? [] : [reactRouter()]),