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
2 changes: 2 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ jobs:
run: yarn
- name: Build ReScript
run: yarn build:res
- name: Build docs
run: yarn build:vite
- name: Validate Docs Wrangler dev server
working-directory: apps/docs
run: |
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

This is the official documentation website for the [ReScript](https://rescript-lang.org) programming language. It is a **fully pre-rendered static site** (no server-side rendering at runtime) built with **ReScript v12 + React 19 + React Router v8 + Vite 8 + Tailwind CSS v4**, deployed to **Cloudflare Pages**.
This is the official documentation website for the [ReScript](https://rescript-lang.org) programming language. It is primarily pre-rendered at build time, with runtime server-side rendering for `/try` through Cloudflare Pages Functions. It is built with **ReScript v12 + React 19 + React Router v8 + Vite 8 + Tailwind CSS v4**, deployed to **Cloudflare Pages**.

## System Requirements

Expand Down Expand Up @@ -99,7 +99,8 @@ The project uses several patterns for JavaScript interop. Follow the existing co
## ReScript React

- This project uses **React 19** and **React Router v8** (framework mode).
- The site is **pre-rendered** (`ssr: false`), so loaders have access to the filesystem during build. Loaders do **not** run on a server after the build.
- The site uses hybrid rendering: most routes are pre-rendered during the build, while `/try` is server-rendered by Cloudflare Pages Functions at runtime.
- Loaders for pre-rendered routes can access the filesystem during the build. Runtime SSR loaders, including shared loader code reachable from `/try`, execute in Cloudflare and must not depend on Node-only filesystem APIs or other build-time-only operations.
- Route modules live in `app/routes/` and export a `loader` and a `default` component.
- Route modules **require** both a `.res` and a `.resi` (interface) file for Vite HMR to work.
- Only a single React component can be exposed from a module's JS output.
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/__tests__/Sitemap_.test.res
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
open Vitest

test("renders sorted unique sitemap URLs with a normalized base URL", async () => {
let xml = Sitemap.render(
let xml = Sitemap.renderWithRuntimePaths(
~baseUrl="https://preview.example.com/",
["/docs/manual/introduction", "blog", "/", "/docs/manual/introduction"],
)
Expand All @@ -17,6 +17,9 @@ test("renders sorted unique sitemap URLs with a normalized base URL", async () =
<url>
<loc>https://preview.example.com/docs/manual/introduction/</loc>
</url>
<url>
<loc>https://preview.example.com/try/</loc>
</url>
</urlset>
`)
})
55 changes: 55 additions & 0 deletions apps/docs/app/entry.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from "react";
import { renderToReadableStream } from "react-dom/server";
import { isbot } from "isbot";
import { ServerRouter } from "react-router";

export const streamTimeout = 5_000;

export default async function handleRequest(
request,
responseStatusCode,
responseHeaders,
routerContext,
) {
responseHeaders.set("Content-Type", "text/html");

if (request.method.toUpperCase() === "HEAD") {
return new Response(null, {
status: responseStatusCode,
Comment thread
jderochervlk marked this conversation as resolved.
headers: responseHeaders,
});
}

let shellRendered = false;
let userAgent = request.headers.get("user-agent");

let body = await renderToReadableStream(
React.createElement(ServerRouter, {
context: routerContext,
url: request.url,
}),
{
signal: AbortSignal.timeout(streamTimeout + 1_000),
onError(error) {
responseStatusCode = 500;
if (shellRendered) {
console.error(error);
}
},
},
);

shellRendered = true;

if (
((userAgent && isbot(userAgent)) || routerContext.isSpaMode) &&
body.allReady
) {
await body.allReady;
}

return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
1 change: 1 addition & 0 deletions apps/docs/functions/try.data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { onRequest } from "../server/react-router-handler.js";
1 change: 1 addition & 0 deletions apps/docs/functions/try.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { onRequest } from "../server/react-router-handler.js";
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"convert-images": "auto-convert-images",
"dev:res": "rescript watch",
"dev:vite": "react-router dev --host",
"dev:wrangler": "yarn wrangler pages dev build/client",
"dev:wrangler": "yarn build:vite && yarn wrangler pages dev build/client",
"dev": "yarn prepare && yarn dev:res & yarn dev:vite & yarn dev:wrangler",
"format": "cd ../.. && oxfmt && cd apps/docs && rescript format",
"prepare": "yarn build:res && yarn build:scripts && yarn check:algolia-public-env && yarn build:update-index",
Expand Down
11 changes: 9 additions & 2 deletions apps/docs/react-router.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import * as fs from "node:fs";
const { stdlibPaths } = await import("./app/DocsRoutes.jsx");

export default {
ssr: false,
ssr: true,
Comment thread
jderochervlk marked this conversation as resolved.
routeDiscovery: { mode: "initial" },

prerender: {
// Restore os.availableParallelism() after https://github.com/remix-run/react-router/issues/15255 is fixed.
concurrency: 1,
async paths({ getStaticPaths }) {
return [...(await getStaticPaths()), ...stdlibPaths];
return [
...(await getStaticPaths()).filter(
(path) => path !== "/try" && path !== "try",
),
Comment thread
jderochervlk marked this conversation as resolved.
...stdlibPaths,
];
},
},
buildEnd: async () => {
fs.rmSync("./out", { recursive: true, force: true });
fs.cpSync("./build/client", "./out", { recursive: true });
},
};
2 changes: 1 addition & 1 deletion apps/docs/scripts/generate_sitemap.res
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ if !Node.Fs.existsSync(sourceDir) {
}

let baseUrl = Node.Process.env->Dict.get("VITE_DEPLOYMENT_URL")->Option.getOr("")
let sitemap = sourceDir->collectPagePaths("")->Sitemap.render(~baseUrl)
let sitemap = sourceDir->collectPagePaths("")->Sitemap.renderWithRuntimePaths(~baseUrl)

outputDirs->Array.forEach(outputDir => {
if Node.Fs.existsSync(outputDir) {
Expand Down
12 changes: 12 additions & 0 deletions apps/docs/server/react-router-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createRequestHandler, RouterContextProvider } from "react-router";

const mode = globalThis.process?.env?.NODE_ENV ?? "production";

const handleReactRouterRequest = createRequestHandler(
() => import("../build/server/index.js"),
Comment thread
jderochervlk marked this conversation as resolved.
mode,
);

export function onRequest(context) {
return handleReactRouterRequest(context.request, new RouterContextProvider());
}
4 changes: 4 additions & 0 deletions apps/docs/src/common/Sitemap.res
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
let defaultBaseUrl = "https://rescript-lang.org"
let runtimePaths = ["/try"]

let normalizeBaseUrl = baseUrl => {
let trimmed = baseUrl->String.trim
Expand Down Expand Up @@ -79,3 +80,6 @@ ${urls}
</urlset>
`
}

let renderWithRuntimePaths = (~baseUrl, staticPaths) =>
[...staticPaths, ...runtimePaths]->render(~baseUrl)
3 changes: 3 additions & 0 deletions apps/docs/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
name = "rescript-lang-org"
compatibility_date = "2026-09-10"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = "out"
Loading