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
7 changes: 5 additions & 2 deletions www/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ Same layout as the previous `xo.run` site:
bindings when the frontend or `std/**/*.echo` changes so Pages can ship the
playground without a Rust toolchain.

SPA deep links use the same `public/404.html` → `/?/path` bounce and
`index.html` restore script as before. Custom domain: `public/CNAME` → `xo.run`.
Content routes (Documents, Packages, Spec, Install, First program, Book, and
the rest of the docs tree) are written as `dist/<path>/index.html` so those
URLs are real pages. Unknown paths still use the `public/404.html` → `/?/path`
bounce and `index.html` restore script. Custom domain: `public/CNAME` →
`xo.run`. Wasm bindings stay in `public/echo-wasm/`.

## Search

Expand Down
17 changes: 16 additions & 1 deletion www/SITE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ maturity.
## Primary nav

The logo is the only Home control. Book stays at `/book` and in the footer.
Catalog, footer, and nav destinations are real HTML files at those paths.

| Item | Path | Notes |
| ----------------- | ----------- | --------------------------------------------- |
Expand All @@ -161,6 +162,10 @@ The home page is a language-docs front door. Copy and links live in
3. First-class links: Documents (`/docs`), Packages (`/docs/std`), Spec (`/e26`)
4. Footer

Each of those links, plus Install, First program, Book, and Try, is a real
page (`path/index.html`) with that page title and body. A destination
without a document is removed from the catalog or footer.

Homepage trust stays factual. Rust, LLVM, the public edition, and the
machine-checked suite are implementation facts.

Expand All @@ -169,7 +174,7 @@ machine-checked suite are implementation facts.
`/docs` is a short catalog. Groups: Start (install, first program, project),
Language (the Echo 2026 form pages), Packages (std + API index), Spec
(Echo 2026 + Language Spec). Each entry has a title, one-line description,
and a working path.
and a working path that the build writes as HTML.

## Echo 2026 section

Expand All @@ -183,6 +188,16 @@ and a working path.

Cross-links: Reference ↔ Spec ↔ suite pages keep the triangle explicit.

## Static pages

`npm run build` writes `index.html` for every content route: the homepage,
`/install`, `/try`, and each page in `docsPages` (Documents, Packages, Spec,
Book, First program, and the rest of the Reference / std / suite pages).
Each file keeps the SPA shell and a noscript body from the same modules the
React app renders. Unknown paths still use the `404.html` bounce.

Wasm bindings stay in `www/public/echo-wasm/`.

## Playground

`/try` runs the shared compiler frontend in WebAssembly (`just wasm`). It
Expand Down
1 change: 1 addition & 0 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Echo Programming Language</title>
<meta name="description" content="Echo is a compiled language." />
<script>
(function () {
var redirect = window.location.search;
Expand Down
2 changes: 1 addition & 1 deletion www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "npm run sync:tree-sitter && tsc -b && vite build",
"build": "npm run sync:tree-sitter && tsc -b && vite build && node scripts/verify-static-dist.mjs",
"build:semantic": "DOCS_EMBEDDINGS=true npm run build",
"format": "oxfmt --check .",
"lint": "oxlint . --ignore-pattern public/echo-wasm",
Expand Down
69 changes: 58 additions & 11 deletions www/scripts/verify-docs-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* - Documents hub catalog groups
* - every language-feature catalog entry is a real page with a summary
* and at least one Echo code block
* - catalog, footer, and nav destinations have static HTML snapshots
*
* Loads src/docs/site.ts, src/docs/content.ts, and src/lib/current-release.ts
* through Vite SSR. Also checks that /install, README, and docs/install.md
Expand Down Expand Up @@ -36,9 +37,12 @@ try {
languageFeatureEntries,
primaryNav,
primaryNavItemIsActive,
publicChromePaths,
renderStaticHomeAndHub,
} = site;
const { docsPageByPath } = content;
const staticHtml = await server.ssrLoadModule("/src/docs/static-html.ts");
const { distFileForPath, staticPageByPath } = staticHtml;

const failures = [];

Expand Down Expand Up @@ -160,22 +164,27 @@ try {

const repoRoot = path.resolve(root, "..");
const installPage = readFileSync(path.join(root, "src/install.tsx"), "utf8");
const installContent = readFileSync(path.join(root, "src/docs/install-content.ts"), "utf8");
const installSources = `${installPage}\n${installContent}`;
const readme = readFileSync(path.join(repoRoot, "README.md"), "utf8");
const installDoc = readFileSync(path.join(repoRoot, "docs/install.md"), "utf8");
const installSh = readFileSync(path.join(repoRoot, "scripts/install.sh"), "utf8");
if (
!installPage.includes("./lib/current-release") ||
!installPage.includes("currentPrereleaseTag")
!installSources.includes("current-release") ||
!installSources.includes("currentPrereleaseTag")
) {
fail("src/install.tsx must render the current prerelease from current-release.ts");
fail("install page must render the current prerelease from current-release.ts");
}
if (!installPage.includes("from-release") || !installPage.includes("currentPrereleaseTag")) {
if (
!installSources.includes("from-release") ||
!installSources.includes("currentPrereleaseTag")
) {
fail("install page must show from-release and how to pin the current tag");
}
if (/windows-x86_64/.test(installPage)) {
fail("src/install.tsx must not claim a Windows tarball");
if (/windows-x86_64/.test(installSources)) {
fail("install page must not claim a Windows tarball");
}
if (!/prerelease/i.test(installPage) || !/prerelease/i.test(readme)) {
if (!/prerelease/i.test(installSources) || !/prerelease/i.test(readme)) {
fail("install page and README must say published builds are prereleases");
}
for (const [label, text] of [
Expand All @@ -196,11 +205,11 @@ try {
fail(`${label} must not present /releases/latest as working`);
}
}
if (/latest GitHub release/i.test(installPage)) {
fail("src/install.tsx must not present a GitHub latest release");
if (/latest GitHub release/i.test(installSources)) {
fail("install page must not present a GitHub latest release");
}
if (/releases\/latest/.test(installPage) && !/404/.test(installPage)) {
fail("src/install.tsx must not present /releases/latest as working");
if (/releases\/latest/.test(installSources) && !/404/.test(installSources)) {
fail("install page must not present /releases/latest as working");
}
if (!installSh.includes("releases?per_page=")) {
fail("install.sh from-release must list published releases, including prereleases");
Expand All @@ -224,6 +233,43 @@ try {
}
}

const pages = staticPageByPath();
const requiredChrome = [
["/docs", "Documents"],
["/docs/std", "Standard library"],
["/e26", "Echo 2026"],
["/install", "Install Echo"],
["/docs/first-program", "First program"],
["/book", "Introduction"],
];
for (const [path, heading] of requiredChrome) {
const page = pages.get(path);
if (!page) {
fail(`${path}: missing static page for catalog/footer link`);
continue;
}
if (!page.body.includes(`<h1>${heading}</h1>`)) {
fail(`${path}: static page must include <h1>${heading}</h1>`);
}
if (!page.title.trim() || !page.description.trim()) {
fail(`${path}: static page needs a title and description`);
}
if (distFileForPath(path) !== `${path.slice(1)}/index.html`) {
fail(`${path}: expected dist file ${path.slice(1)}/index.html`);
}
}

for (const path of publicChromePaths()) {
const page = pages.get(path);
if (!page) {
fail(`${path}: catalog/nav/footer link has no static page`);
continue;
}
if (!page.body.includes("<h1>")) {
fail(`${path}: static page is missing an h1`);
}
}

if (failures.length) {
console.error(JSON.stringify({ ok: false, failures }, null, 2));
process.exitCode = 1;
Expand All @@ -236,6 +282,7 @@ try {
nav: primaryNav.map((item) => item.label),
languagePages: languageFeatureEntries.map((entry) => entry.to),
hubGroups: docsHubCatalog.map((group) => group.title),
chromePaths: publicChromePaths(),
},
null,
2,
Expand Down
19 changes: 17 additions & 2 deletions www/scripts/verify-prose.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ try {

const { docsPages, docsPageByPath } = content;
const { stdModules } = ref;
const { homePage, docsHubCatalog } = site;
const { homePage, docsHubCatalog, tryPage } = site;
const install = await server.ssrLoadModule("/src/docs/install-content.ts");
const { installPage, inlineText } = install;

const failures = [];
const prose = [];
Expand All @@ -150,6 +152,13 @@ try {

prose.push({ where: "home definition", text: homePage.definition });
prose.push({ where: "home lead", text: homePage.lead });
prose.push({ where: "try lead", text: tryPage.lead });
prose.push({ where: "install lead", text: inlineText(installPage.lead) });
for (const section of installPage.sections) {
for (const paragraph of section.paragraphs) {
prose.push({ where: `install ${section.title}`, text: inlineText(paragraph) });
}
}
for (const link of homePage.links) {
prose.push({ where: `home link ${link.title}`, text: link.description });
}
Expand Down Expand Up @@ -189,7 +198,13 @@ try {
}

// High-visibility TSX chrome: em dashes + slogan templates inside string literals
const tsxFiles = ["src/app.tsx", "src/install.tsx", "src/router.tsx", "src/docs/site.ts"];
const tsxFiles = [
"src/app.tsx",
"src/install.tsx",
"src/router.tsx",
"src/docs/site.ts",
"src/docs/install-content.ts",
];
const stringLitRe = /"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/g;
for (const rel of tsxFiles) {
const src = readFileSync(path.join(root, rel), "utf8");
Expand Down
79 changes: 79 additions & 0 deletions www/scripts/verify-static-dist.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* After vite build, every catalog / footer / nav destination must exist as a
* real HTML file with that page's title and body. SPA-only copies of index.html
* are not enough.
*/
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createServer } from "vite";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const dist = path.join(root, "dist");

const server = await createServer({
root,
logLevel: "error",
server: { middlewareMode: true },
appType: "custom",
});

try {
const site = await server.ssrLoadModule("/src/docs/site.ts");
const staticHtml = await server.ssrLoadModule("/src/docs/static-html.ts");
const { publicChromePaths } = site;
const { distFileForPath, staticPageByPath } = staticHtml;
const pages = staticPageByPath();
const failures = [];

function fail(message) {
failures.push(message);
}

if (!existsSync(path.join(dist, "index.html"))) {
fail("dist/index.html is missing; run this script after vite build");
}

for (const route of publicChromePaths()) {
const page = pages.get(route);
if (!page) {
fail(`${route}: renderer has no static page`);
continue;
}

const file = path.join(dist, distFileForPath(route));
if (!existsSync(file)) {
fail(`${route}: missing ${path.relative(root, file)}`);
continue;
}

const html = readFileSync(file, "utf8");
if (!html.includes(`<title>${page.title}</title>`)) {
fail(`${route}: ${path.relative(root, file)} is missing title ${page.title}`);
}
if (!html.includes(page.body)) {
fail(`${route}: ${path.relative(root, file)} is missing the page body`);
}
if (route !== "/" && html.includes(`<h1>${site.homePage.definition}</h1>`)) {
fail(`${route}: ${path.relative(root, file)} still has the homepage snapshot`);
}
}

if (failures.length) {
console.error(JSON.stringify({ ok: false, failures }, null, 2));
process.exitCode = 1;
} else {
console.log(
JSON.stringify(
{
ok: true,
files: publicChromePaths().map((route) => distFileForPath(route)),
},
null,
2,
),
);
}
} finally {
await server.close();
}
2 changes: 1 addition & 1 deletion www/scripts/verify-try.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ try {
}
}

const trySource = readFileSync(trySourcePath, "utf8");
const trySource = `${readFileSync(trySourcePath, "utf8")}\n${site.tryPage.lead}`;
if (/\bxo run\b/.test(trySource)) {
fail("/try must not describe itself as xo run");
}
Expand Down
Loading
Loading