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
34 changes: 26 additions & 8 deletions www/SITE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ 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.
Each of those links, plus Install, First program, Book, Try, Privacy, and
Terms, 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 @@ -191,10 +191,11 @@ 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.
`/install`, `/try`, `/privacy`, `/terms`, 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/`.

Expand All @@ -206,6 +207,22 @@ playground run then executes the checked MIR and captures `io.print`.
Filesystem, net, process, and tasks fail with a playground-host error.
Compile and native run stay on `xo` (LLVM).

## Footer

Learn, Community, and About. Copy lives in `src/docs/site.ts` (`footerLinkGroups`).

| Group | Links |
| --------- | --------------------------------------------------------------------------- |
| Learn | Install, Try Echo, First program, Documents, Book, Echo 2026 |
| Community | GitHub. Omit Discord until a public invite URL exists. |
| About | Modoterra (`https://modoterra.xyz`), Privacy (`/privacy`), Terms (`/terms`) |

Public project mail on the site is `@modoterra.xyz` only (`hello@`, `security@`,
`oss@`). Do not publish `@modoterra.com`.

Privacy and Terms are short compiler/OSS pages for this documentation site.
They are not a consumer-app policy.

## Discovery

`/sitemap.xml` lists the public catalog on `https://xo.run`: home, Install,
Expand All @@ -216,4 +233,5 @@ Pages host.

## Out of scope (later)

Richer download tabs, `/e26` URL rename to `/echo-2026`.
Richer download tabs, `/e26` URL rename to `/echo-2026`. Discord footer link
when a real invite exists.
60 changes: 60 additions & 0 deletions www/scripts/verify-docs-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Verifies the shipped docs-first site model:
* - homepage definition, leader-bearing sample, docs/std/spec links
* - primary nav labels and paths
* - footer About links Privacy and Terms; Discord stays hidden
* - legal pages use @modoterra.xyz mail only
* - Documents hub catalog groups
* - every language-feature catalog entry is a real page with a summary
* and at least one Echo code block
Expand Down Expand Up @@ -32,13 +34,18 @@ try {

const {
docsHubCatalog,
footerLinkGroups,
homePage,
installCta,
languageFeatureEntries,
legalPages,
primaryNav,
primaryNavItemIsActive,
privacyPage,
publicChromePaths,
publicMailAddresses,
renderStaticHomeAndHub,
termsPage,
} = site;
const { docsPageByPath } = content;
const staticHtml = await server.ssrLoadModule("/src/docs/static-html.ts");
Expand Down Expand Up @@ -80,6 +87,54 @@ try {
fail("installCta must be Install → /install");
}

const footerLinks = footerLinkGroups.flatMap((group) => group.links);
const footerByLabel = new Map(footerLinks.map((link) => [link.label, link.href]));
if (footerByLabel.get("Privacy") !== "/privacy") {
fail(`footer Privacy should be /privacy, got ${footerByLabel.get("Privacy")}`);
}
if (footerByLabel.get("Terms") !== "/terms") {
fail(`footer Terms should be /terms, got ${footerByLabel.get("Terms")}`);
}
if (footerByLabel.get("Modoterra") !== "https://modoterra.xyz") {
fail("footer About must keep the Modoterra company link");
}
if (footerByLabel.has("Discord") || footerLinks.some((link) => /discord/i.test(link.label))) {
fail("footer must hide Discord until there is a public invite");
}

const expectedMail = ["hello@modoterra.xyz", "security@modoterra.xyz", "oss@modoterra.xyz"];
if (JSON.stringify([...publicMailAddresses]) !== JSON.stringify(expectedMail)) {
fail(`publicMailAddresses must be ${expectedMail.join(", ")}`);
}

if (privacyPage.path !== "/privacy" || termsPage.path !== "/terms") {
fail("legal pages must live at /privacy and /terms");
}
if (legalPages.length !== 2) {
fail("legalPages should be Privacy and Terms only");
}

const legalText = legalPages
.flatMap((page) => [page.summary, ...page.sections.flatMap((section) => section.paragraphs)])
.join("\n");
for (const address of expectedMail) {
if (!legalText.includes(address)) {
fail(`legal pages missing ${address}`);
}
}
if (/@modoterra\.com\b/.test(legalText)) {
fail("legal pages must not publish @modoterra.com");
}
if (legalText.includes("github.io")) {
fail("legal pages must not list github.io");
}
const publishedMail = legalText.match(/[a-z0-9._%+-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi) ?? [];
for (const address of publishedMail) {
if (!address.endsWith("@modoterra.xyz")) {
fail(`legal pages published non-xyz mail: ${address}`);
}
}

if (!primaryNavItemIsActive("/docs", "/docs/leaders")) {
fail("Documents nav should be active on /docs/leaders");
}
Expand Down Expand Up @@ -241,6 +296,8 @@ try {
["/install", "Install Echo"],
["/docs/first-program", "First program"],
["/book", "Introduction"],
["/privacy", "Privacy"],
["/terms", "Terms"],
];
for (const [path, heading] of requiredChrome) {
const page = pages.get(path);
Expand Down Expand Up @@ -280,6 +337,9 @@ try {
ok: true,
definition: homePage.definition,
nav: primaryNav.map((item) => item.label),
footerAbout: footerLinkGroups
.find((group) => group.title === "About")
?.links.map((link) => link.label),
languagePages: languageFeatureEntries.map((entry) => entry.to),
hubGroups: docsHubCatalog.map((group) => group.title),
chromePaths: publicChromePaths(),
Expand Down
11 changes: 10 additions & 1 deletion www/scripts/verify-prose.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ try {

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

Expand Down Expand Up @@ -168,6 +168,14 @@ try {
prose.push({ where: `hub ${group.title} ${entry.title}`, text: entry.description });
}
}
for (const page of legalPages) {
prose.push({ where: `${page.path} summary`, text: page.summary });
for (const section of page.sections) {
for (const paragraph of section.paragraphs) {
prose.push({ where: `${page.path}#${section.title}`, text: paragraph });
}
}
}

for (const item of prose) {
checkPatterns(item.where, item.text, BAN_PATTERNS, failures);
Expand Down Expand Up @@ -201,6 +209,7 @@ try {
const tsxFiles = [
"src/app.tsx",
"src/install.tsx",
"src/legal.tsx",
"src/router.tsx",
"src/docs/site.ts",
"src/docs/install-content.ts",
Expand Down
134 changes: 130 additions & 4 deletions www/src/docs/site.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Public site chrome: homepage, primary nav, Documents hub catalog, and the
* discovery files (`/sitemap.xml`, `/robots.txt`). Pages and tests load this
* module; do not duplicate the lists in UI or fixtures.
* Public site chrome: homepage, primary nav, footer, legal pages, Documents
* hub catalog, and the discovery files (`/sitemap.xml`, `/robots.txt`). Pages
* and tests load this module; do not duplicate the lists in UI or fixtures.
*/

/** Live public host. Do not emit github.io URLs in discovery files. */
Expand Down Expand Up @@ -62,6 +62,10 @@ export type FooterLinkGroup = {
links: FooterLink[];
};

/**
* Footer chrome. Discord stays omitted until there is a public invite URL.
* Public project mail on linked pages is @modoterra.xyz only.
*/
export const footerLinkGroups: FooterLinkGroup[] = [
{
title: "Learn",
Expand All @@ -81,7 +85,6 @@ export const footerLinkGroups: FooterLinkGroup[] = [
label: "GitHub",
href: "https://github.com/modoterra/echo",
},
{ label: "Discord", href: "#", disabled: true },
],
},
{
Expand All @@ -91,6 +94,8 @@ export const footerLinkGroups: FooterLinkGroup[] = [
label: "Modoterra",
href: "https://modoterra.xyz",
},
{ label: "Privacy", href: "/privacy" },
{ label: "Terms", href: "/terms" },
],
},
];
Expand Down Expand Up @@ -130,6 +135,127 @@ export function publicChromePaths(): string[] {
return [...paths].sort();
}

/** Public addresses already used in-repo. Keep the site on this domain. */
export const publicMailAddresses = [
"hello@modoterra.xyz",
"security@modoterra.xyz",
"oss@modoterra.xyz",
] as const;

export type LegalSection = {
title: string;
paragraphs: string[];
};

export type LegalPageContent = {
path: string;
title: string;
summary: string;
sections: LegalSection[];
};

export const privacyPage: LegalPageContent = {
path: "/privacy",
title: "Privacy",
summary:
"This page describes how the Echo project site and related project mail handle information. Echo is an open-source compiled language. xo.run is its public documentation site.",
sections: [
{
title: "The site",
paragraphs: [
"xo.run publishes language documentation, the Echo 2026 spec, install instructions, and an in-browser checker. Modoterra Corporation maintains the site. These pages have no sign-in.",
],
},
{
title: "Hosting logs",
paragraphs: [
"The site is a static site served from a CDN. The host may record ordinary request data such as IP address, user agent, requested URL, and time. This site does not run a first-party analytics product.",
],
},
{
title: "Mail",
paragraphs: [
"If you write to hello@modoterra.xyz, security@modoterra.xyz, or oss@modoterra.xyz, we receive the address and message you send so we can reply.",
],
},
{
title: "GitHub",
paragraphs: [
"Issues, pull requests, and other activity on the public Echo repository are public on GitHub and follow GitHub's terms.",
],
},
{
title: "Try Echo",
paragraphs: [
"The Try page runs the compiler frontend in your browser. Source you type there stays in that browser session. Playground source is not uploaded to a Modoterra server.",
],
},
{
title: "The compiler",
paragraphs: [
"xo is a local toolchain. It reads source on the machine where you run it. This site is not a hosted compile service.",
],
},
{
title: "Contact",
paragraphs: [
"Questions about this page go to hello@modoterra.xyz. Report security issues to security@modoterra.xyz.",
],
},
],
};

export const termsPage: LegalPageContent = {
path: "/terms",
title: "Terms",
summary:
"These terms cover the xo.run website and the Echo software published by Modoterra Corporation.",
sections: [
{
title: "Software",
paragraphs: [
"Echo is released under the MIT License. The license text lives in the project LICENSE file. You may use, copy, modify, and distribute the software under that license.",
],
},
{
title: "Website",
paragraphs: [
"The pages on xo.run describe Echo and how to use xo. We may change or remove pages as the language changes. The site is provided as is.",
],
},
{
title: "Warranty",
paragraphs: [
"The software and this site come with no warranty. The MIT License states the full disclaimer.",
],
},
{
title: "Contributions",
paragraphs: [
"Contributions to the public repository are governed by the Contributor License Agreement in CLA.md. Submitting a contribution accepts that agreement.",
],
},
{
title: "Conduct",
paragraphs: [
"Project participation follows CODE_OF_CONDUCT.md. Report conduct issues to oss@modoterra.xyz.",
],
},
{
title: "Security",
paragraphs: [
"Report vulnerabilities to security@modoterra.xyz. Use a private report for security issues.",
],
},
{
title: "Contact",
paragraphs: ["Other questions go to hello@modoterra.xyz."],
},
],
};

export const legalPages: readonly LegalPageContent[] = [privacyPage, termsPage];

/**
* Documents stays active on language and guide pages. Packages owns /docs/std.
*/
Expand Down
Loading
Loading