Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7e95b5a
Upgrade Vite 8, Vitest 4.1, React 19.2, ReScript 12.2, React Router 7…
jderochervlk Apr 5, 2026
3d84fe2
feat: improve Algolia search results
jderochervlk Apr 6, 2026
6945d0f
sorting and formatting
jderochervlk Apr 6, 2026
73b141a
remove MDN links
jderochervlk Apr 6, 2026
e2280ec
improve visuals
jderochervlk Apr 6, 2026
19cdff5
Merge branch 'master' of github.com:rescript-lang/rescript-lang.org i…
jderochervlk Apr 6, 2026
aa24517
remove test file
jderochervlk Apr 6, 2026
e6cc448
remove unused css
jderochervlk Apr 6, 2026
fe97256
pr feedback
jderochervlk Apr 6, 2026
e033ccc
add back icons
jderochervlk Apr 6, 2026
05bcd74
add unit tests
jderochervlk Apr 6, 2026
9016a08
Update DocSearch styles for footer and command keys
jderochervlk Apr 9, 2026
e29a26e
Remove Escape key handler from Search component
jderochervlk Apr 9, 2026
6480ca3
[codex] finish Algolia env split and search fallback (#1273)
jderochervlk Apr 25, 2026
41fc3e7
Merge master into vlk/fix-algolia
jderochervlk Apr 25, 2026
910b0f8
restore lazy components
jderochervlk Apr 25, 2026
f1d7fbc
simplify tests
jderochervlk Apr 25, 2026
25de384
fix: normalize docs search urls
jderochervlk Apr 25, 2026
47a4ad6
refactor: move algolia env check to rescript
jderochervlk Apr 25, 2026
1f3b47e
fix: switch Algolia indexing to DocSearch crawler
jderochervlk Apr 27, 2026
f6287fa
fix: restore runtime Algolia env mapping
jderochervlk Apr 27, 2026
97edadf
fix: align DocSearch index and sitemap output
jderochervlk Apr 27, 2026
2a0d9ca
fix: use public Algolia deploy secrets
jderochervlk Apr 27, 2026
ccc9d1f
fix: pass Algolia secrets to deploy build
jderochervlk Apr 27, 2026
f7c8227
fix: handle crawler search hits without url_without_anchor
jderochervlk Apr 27, 2026
e7c911e
fix: run Cypress against preview deploy
jderochervlk Apr 27, 2026
cb64fde
fix: restore crawler search highlights
jderochervlk Apr 27, 2026
d66c312
fix: pass Cypress preview URL within e2e job
jderochervlk Apr 27, 2026
3e0ac90
fix: tolerate search hits without snippet metadata
jderochervlk Apr 27, 2026
4ef439e
fix: isolate DocSearch render errors
jderochervlk Apr 27, 2026
c59e1c5
fix: separate Belt search result groups
jderochervlk Apr 27, 2026
bf9ee3f
fix: add DocSearch heading markers
jderochervlk Apr 27, 2026
465e2b0
fix: add trailing slashes to sitemap URLs
jderochervlk Apr 27, 2026
0cdb18a
fix: route search hits through React Router
jderochervlk Apr 27, 2026
cefe2f5
Merge remote-tracking branch 'origin/master' into codex/pr-1231-docse…
jderochervlk Apr 28, 2026
438c8e6
Merge remote-tracking branch 'origin/master' into codex/pr-1231-merge…
jderochervlk Apr 28, 2026
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
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VITE_VERSION_LATEST="v11.0.0"
VITE_VERSION_NEXT="v12.0.0"
VITE_VERSION_LATEST="v12.0.0"
VITE_VERSION_NEXT="v13.0.0"
24 changes: 21 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ jobs:
contents: read
deployments: write
pull-requests: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- uses: actions/checkout@v6.0.2
- name: Setup Node.js environment
Expand Down Expand Up @@ -54,6 +52,9 @@ jobs:
run: yarn build
env:
VITE_DEPLOYMENT_URL: ${{ env.VITE_DEPLOYMENT_URL }}
VITE_ALGOLIA_APP_ID: ${{ secrets.VITE_ALGOLIA_APP_ID }}
VITE_ALGOLIA_INDEX_NAME: ${{ secrets.VITE_ALGOLIA_INDEX_NAME }}
VITE_ALGOLIA_SEARCH_API_KEY: ${{ secrets.VITE_ALGOLIA_SEARCH_API_KEY }}
- name: Deploy
if: ${{ github.actor != 'dependabot[bot]' }}
id: deploy
Expand Down Expand Up @@ -98,10 +99,27 @@ jobs:
run: yarn install
- name: Build ReScript
run: yarn build:res
- name: Set Cypress base URL
id: cypress-base-url
shell: bash
run: |
RAW_BRANCH="${{ github.head_ref || github.ref_name }}"

if [[ "$RAW_BRANCH" == "master" ]]; then
CYPRESS_BASE_URL="https://rescript-lang.org"
else
SAFE_BRANCH="${RAW_BRANCH//\//-}"

SAFE_BRANCH=$(echo "$SAFE_BRANCH" | tr '[:upper:]' '[:lower:]')

CYPRESS_BASE_URL="https://${SAFE_BRANCH}.rescript-lang.pages.dev"
fi

echo "url=$CYPRESS_BASE_URL" >> "$GITHUB_OUTPUT"
- name: Cypress E2E tests
uses: cypress-io/github-action@v7
with:
install: false
working-directory: apps/docs
browser: chrome
config: baseUrl=${{ needs.deploy.outputs.deployment-url }}
config: baseUrl=${{ steps.cypress-base-url.outputs.url }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ apps/docs/scripts/gendocs.mjs
apps/docs/scripts/generate_*.mjs
apps/docs/scripts/gendocs.jsx
apps/docs/scripts/generate_*.jsx
apps/docs/scripts/LogAlgoliaEnvStatus.jsx

# Generated via generate-llms script
apps/docs/public/llms/manual/**/llm*.txt
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,42 @@ yarn dev

`yarn dev` prepares generated assets, starts the ReScript watcher, runs the React Router/Vite dev server, and serves the built client through Wrangler Pages.

## Search and DocSearch

Search is powered by Algolia DocSearch. The DocSearch crawler owns indexing and index settings; site builds and deployments do not upload records, replace indexes, or use an Algolia admin/write key.

The frontend only needs public DocSearch runtime variables:

```sh
VITE_ALGOLIA_APP_ID="..."
VITE_ALGOLIA_INDEX_NAME="..."
VITE_ALGOLIA_SEARCH_API_KEY="..."
```

The GitHub deploy workflow passes the public GitHub secrets named `VITE_ALGOLIA_APP_ID`, `VITE_ALGOLIA_INDEX_NAME`, and `VITE_ALGOLIA_SEARCH_API_KEY` to the `yarn build` step. `VITE_ALGOLIA_INDEX_NAME` is the full runtime index name; the workflow does not add `prod_` or `dev_` prefixes. Builds and deployments should not configure or export Algolia admin/write keys.

DocSearch crawl quality comes from the generated HTML. Searchable page bodies use `DocSearch-content`, each crawlable section provides a hidden `DocSearch-lvl0` marker such as `Manual`, `API`, `React`, `Syntax Lookup`, `Community`, or `Blog`, and headings own unique `id` attributes for section links.

The crawler configuration should use selectors shaped like this:

```js
recordProps: {
lvl0: {
selectors: ".DocSearch-lvl0",
defaultValue: "Documentation",
},
lvl1: [".DocSearch-content h1", "main h1", "h1", "head > title"],
lvl2: [".DocSearch-content h2", "main h2", "h2"],
lvl3: [".DocSearch-content h3", "main h3", "h3"],
lvl4: [".DocSearch-content h4", "main h4", "h4"],
lvl5: [".DocSearch-content h5", "main h5", "h5"],
lvl6: [".DocSearch-content h6", "main h6", "h6"],
content: [".DocSearch-content p, .DocSearch-content li"],
}
```

Production crawler start URLs, ranking, and crawler schedules live in the Algolia dashboard. The build generates `sitemap.xml` from the prerendered HTML pages so the crawler can use the deployed sitemap.

## Project Structure Overview

- `app/`: React Router app shell, layouts, route definitions, and route modules
Expand Down
27 changes: 27 additions & 0 deletions apps/docs/__tests__/AlgoliaConfig_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
open Vitest

test("publicConfigFrom returns config when all public vars are present", async () => {
let result = AlgoliaConfig.publicConfigFrom(
~appId=Some("app_123"),
~indexName=Some("rescript_lang"),
~searchApiKey=Some("search_123"),
)

let expected: AlgoliaConfig.publicConfig = {
appId: "app_123",
indexName: "rescript_lang",
searchApiKey: "search_123",
}

expect(result)->toEqual(Some(expected))
})

test("publicConfigFrom reports missing public vars in declaration order", async () => {
let result = AlgoliaConfig.missingPublicVars(
~appId=None,
~indexName=Some("rescript_lang"),
~searchApiKey=None,
)

expect(result)->toEqual(["VITE_ALGOLIA_APP_ID", "VITE_ALGOLIA_SEARCH_API_KEY"])
})
19 changes: 19 additions & 0 deletions apps/docs/__tests__/AlgoliaEnvStatus_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
open Vitest

test("reports missing public vars in declaration order", async () => {
let env = Dict.fromArray([
("VITE_ALGOLIA_APP_ID", ""),
("VITE_ALGOLIA_INDEX_NAME", "rescript_lang"),
])

expect(AlgoliaEnvStatus.getMissingPublicAlgoliaVars(~env))->toEqual([
"VITE_ALGOLIA_APP_ID",
"VITE_ALGOLIA_SEARCH_API_KEY",
])
})

test("formats the disabled search warning", async () => {
expect(AlgoliaEnvStatus.formatDisabledMessage(["VITE_ALGOLIA_APP_ID"]))->toBe(
"Algolia search disabled: missing VITE_ALGOLIA_APP_ID",
)
})
26 changes: 26 additions & 0 deletions apps/docs/__tests__/BlogArticle_.test.res
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
open ReactRouter
open Vitest

@get external textContent: WebAPI.DOMAPI.element => string = "textContent"

let mockAuthor: BlogFrontmatter.author = {
username: "test-author",
fullname: "Test Author",
Expand Down Expand Up @@ -46,6 +48,30 @@ test("desktop blog article renders header, author, date, and body", async () =>
await element(wrapper)->toMatchScreenshot("desktop-blog-article")
})

test("blog article marks body content for DocSearch crawling", async () => {
await viewport(1440, 900)

let _screen = await render(
<BrowserRouter>
<BlogArticle frontmatter=mockFrontmatter isArchived=false path="/blog/test-article">
<p> {React.string("This is the blog post body content for testing.")} </p>
</BlogArticle>
</BrowserRouter>,
)

switch document->WebAPI.Document.querySelector("article.DocSearch-content") {
| Value(_) => ()
| Null => failwith("expected blog article body to be marked as DocSearch content")
}

let lvl0 = switch document->WebAPI.Document.querySelector("article .DocSearch-lvl0") {
| Value(element) => element
| Null => failwith("expected blog article to render a DocSearch lvl0 marker")
}

expect(lvl0->textContent)->toBe("Blog")
})

test("desktop archived blog article shows warning banner", async () => {
await viewport(1440, 900)

Expand Down
37 changes: 37 additions & 0 deletions apps/docs/__tests__/DocsLayout_.test.res
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
open ReactRouter
open Vitest

@get external textContent: WebAPI.DOMAPI.element => string = "textContent"

let mockCategories: array<SidebarLayout.Sidebar.Category.t> = [
{
name: "Overview",
Expand Down Expand Up @@ -57,6 +59,41 @@ test("desktop docs layout shows sidebar with categories", async () => {
await element(wrapper)->toMatchScreenshot("desktop-docs-layout")
})

test("docs layout marks the textual content for DocSearch crawling", async () => {
await viewport(1440, 900)

let screen = await render(
<MemoryRouter initialEntries=["/docs/manual/introduction"]>
<DocsLayout categories=mockCategories activeToc=mockToc docSearchLvl0="Manual">
<div> {React.string("This is the documentation content.")} </div>
</DocsLayout>
</MemoryRouter>,
)

let _mainContent = await screen->getByTestId("side-layout-children")

let mainContent = switch document->WebAPI.Document.querySelector(
"[data-testid='side-layout-children']",
) {
| Value(element) => element
| Null => failwith("expected docs layout main content")
}

let className = switch mainContent->WebAPI.Element.getAttribute("class") {
| Value(value) => value
| Null => ""
}

expect(className->String.includes("DocSearch-content"))->toBe(true)

let lvl0 = switch document->WebAPI.Document.querySelector(".DocSearch-lvl0") {
| Value(element) => element
| Null => failwith("expected docs layout to render a DocSearch lvl0 marker")
}

expect(lvl0->textContent)->toBe("Manual")
})

test("desktop docs layout shows table of contents entries", async () => {
await viewport(1440, 900)

Expand Down
33 changes: 33 additions & 0 deletions apps/docs/__tests__/DocsOverview_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ test("desktop docs overview shows ecosystem links", async () => {
await element(wrapper)->toMatchScreenshot("desktop-docs-overview-ecosystem")
})

test("docs overview uses unversioned docs links", async () => {
await viewport(1440, 900)

let _screen = await render(
<MemoryRouter initialEntries=["/docs"]>
<div dataTestId="docs-overview-wrapper">
<DocsOverview.default />
</div>
</MemoryRouter>,
)

let overviewLink = switch document->WebAPI.Document.querySelector(
"a[href='/docs/manual/introduction']",
) {
| Value(link) => link
| Null => failwith("expected docs overview to link to the unversioned manual introduction")
}
await element(overviewLink)->toBeVisible

let genTypeLink = switch document->WebAPI.Document.querySelector(
"a[href='/docs/manual/typescript-integration']",
) {
| Value(link) => link
| Null => failwith("expected docs overview to link to the unversioned GenType docs page")
}
await element(genTypeLink)->toBeVisible

switch document->WebAPI.Document.querySelector("a[href*='/docs/manual/v']") {
| Value(_) => failwith("expected docs overview to avoid versioned manual links")
| Null => ()
}
})

test("mobile docs overview", async () => {
await viewport(600, 1200)

Expand Down
63 changes: 63 additions & 0 deletions apps/docs/__tests__/MarkdownComponents_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,69 @@ test("renders headings h1 through h5", async () => {
await element(wrapper)->toMatchScreenshot("markdown-headings")
})

test("h1 keeps the generated markdown id for DocSearch", async () => {
await viewport(1440, 900)

let _screen = await render(
<div>
<Markdown.H1 id="heading-level-1"> {React.string("Heading Level 1")} </Markdown.H1>
</div>,
)

switch document->WebAPI.Document.querySelector("h1#heading-level-1") {
| Value(_) => ()
| Null => failwith("expected markdown h1 to keep the generated id")
}
})

test("markdown headings expose explicit DocSearch hierarchy markers", async () => {
await viewport(1440, 900)

let _screen = await render(
<div>
<Markdown.H1 id="heading-level-1"> {React.string("Heading Level 1")} </Markdown.H1>
<Markdown.H2 id="heading-level-2"> {React.string("Heading Level 2")} </Markdown.H2>
<Markdown.H3 id="heading-level-3"> {React.string("Heading Level 3")} </Markdown.H3>
<Markdown.H4 id="heading-level-4"> {React.string("Heading Level 4")} </Markdown.H4>
<Markdown.H5 id="heading-level-5"> {React.string("Heading Level 5")} </Markdown.H5>
</div>,
)

switch document->WebAPI.Document.querySelector("h1.DocSearch-lvl1#heading-level-1") {
| Value(_) => ()
| Null => failwith("expected h1 to expose DocSearch lvl1")
}
switch document->WebAPI.Document.querySelector("h2.DocSearch-lvl2#heading-level-2") {
| Value(_) => ()
| Null => failwith("expected h2 to expose DocSearch lvl2")
}
switch document->WebAPI.Document.querySelector("h3.DocSearch-lvl3#heading-level-3") {
| Value(_) => ()
| Null => failwith("expected h3 to expose DocSearch lvl3")
}
switch document->WebAPI.Document.querySelector("h4.DocSearch-lvl4#heading-level-4") {
| Value(_) => ()
| Null => failwith("expected h4 to expose DocSearch lvl4")
}
switch document->WebAPI.Document.querySelector("h5.DocSearch-lvl5#heading-level-5") {
| Value(_) => ()
| Null => failwith("expected h5 to expose DocSearch lvl5")
}
})

test("heading anchor links do not duplicate heading ids", async () => {
await viewport(1440, 900)

let _screen = await render(
<div>
<Markdown.H2 id="duplicate-check"> {React.string("Duplicate Check")} </Markdown.H2>
</div>,
)

let matches = document->WebAPI.Document.querySelectorAll("[id='duplicate-check']")
expect(matches.length)->toBe(1)
})

test("renders paragraph, strong, and intro", async () => {
await viewport(1440, 900)

Expand Down
19 changes: 19 additions & 0 deletions apps/docs/__tests__/Meta_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
open Vitest

let getMetaContent = name => {
switch document->WebAPI.Document.querySelector(`meta[name='${name}']`) {
| Value(element) =>
switch element->WebAPI.Element.getAttribute("content") {
| Value(content) => content
| Null => failwith(`expected ${name} meta tag to have content`)
}
| Null => failwith(`expected ${name} meta tag`)
}
}

test("renders DocSearch crawler meta tags", async () => {
let _screen = await render(<Meta />)

expect(getMetaContent("docsearch:language"))->toBe("en")
expect(getMetaContent("docsearch:version"))->toBe("v12,latest")
})
Loading
Loading