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
45 changes: 28 additions & 17 deletions 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.
Security lives at `/security` and in the footer About group.
Catalog, footer, and nav destinations are real HTML files at those paths.

| Item | Path | Notes |
Expand All @@ -145,6 +146,7 @@ Catalog, footer, and nav destinations are real HTML files at those paths.
| Echo 2026 | `/e26` | Edition + Spec TOC + suite |
| Try | `/try` | In-browser check + playground run (wasm host) |
| **Install** (CTA) | `/install` | Solid button; get `xo` |
| Security | `/security` | Vulnerability mailbox + `SECURITY.md` |

## Docs left rail

Expand All @@ -162,13 +164,22 @@ 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, 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.
Each of those links, plus Install, First program, Book, Try, Privacy, Terms,
and Security, 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.

## Security

`/security` is the public reporting page. It points to
`security@modoterra.xyz` and the repository `SECURITY.md`. The footer About
group links there. Public mail uses `@modoterra.xyz` only. Discord stays
omitted from the footer until there is a real invite.

Copy and URLs live in `src/docs/site.ts` (`securityContact`, `footerLinkGroups`).

## Documents hub

`/docs` is a short catalog. Groups: Start (install, first program, project),
Expand All @@ -191,11 +202,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`, `/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.
`/install`, `/try`, `/privacy`, `/terms`, `/security`, 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 @@ -211,11 +222,11 @@ Compile and native run stay on `xo` (LLVM).

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`) |
| 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`), Security (`/security`) |

Public project mail on the site is `@modoterra.xyz` only (`hello@`, `security@`,
`oss@`). Do not publish `@modoterra.com`.
Expand All @@ -226,10 +237,10 @@ They are not a consumer-app policy.
## Discovery

`/sitemap.xml` lists the public catalog on `https://xo.run`: home, Install,
Try, catalog and footer routes, and every shipped docs, Book, Echo 2026, and
std page. `/robots.txt` allows crawlers and points at that sitemap. Privacy
and Terms are listed only when those pages exist. Do not list the GitHub
Pages host.
Try, Security, catalog and footer routes, and every shipped docs, Book,
Echo 2026, and std page. `/robots.txt` allows crawlers and points at that
sitemap. Privacy and Terms are listed only when those pages exist. Do not
list the GitHub Pages host.

## Out of scope (later)

Expand Down
28 changes: 28 additions & 0 deletions www/scripts/verify-docs-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* - footer About links Privacy and Terms; Discord stays hidden
* - legal pages use @modoterra.xyz mail only
* - Documents hub catalog groups
* - security mailbox, SECURITY.md, and footer /security pointer
* - 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
Expand Down Expand Up @@ -45,6 +46,7 @@ try {
publicChromePaths,
publicMailAddresses,
renderStaticHomeAndHub,
securityContact,
termsPage,
} = site;
const { docsPageByPath } = content;
Expand Down Expand Up @@ -95,6 +97,9 @@ try {
if (footerByLabel.get("Terms") !== "/terms") {
fail(`footer Terms should be /terms, got ${footerByLabel.get("Terms")}`);
}
if (footerByLabel.get("Security") !== securityContact.path) {
fail(`footer Security should be ${securityContact.path}, got ${footerByLabel.get("Security")}`);
}
if (footerByLabel.get("Modoterra") !== "https://modoterra.xyz") {
fail("footer About must keep the Modoterra company link");
}
Expand Down Expand Up @@ -135,6 +140,25 @@ try {
}
}

if (securityContact.email !== "security@modoterra.xyz") {
fail(`security mailbox must be security@modoterra.xyz, got ${securityContact.email}`);
}
if (/@modoterra\.com\b/.test(securityContact.email)) {
fail("public mail must use @modoterra.xyz, never @modoterra.com");
}
if (securityContact.mailto !== `mailto:${securityContact.email}`) {
fail("securityContact.mailto must match the public mailbox");
}
if (securityContact.path !== "/security") {
fail("securityContact.path must be /security");
}
if (!securityContact.policyUrl.includes("SECURITY.md")) {
fail("securityContact.policyUrl must point at SECURITY.md");
}
if (!securityContact.policyUrl.includes("github.com/modoterra/echo")) {
fail("securityContact.policyUrl must point at this repository");
}

if (!primaryNavItemIsActive("/docs", "/docs/leaders")) {
fail("Documents nav should be active on /docs/leaders");
}
Expand Down Expand Up @@ -282,6 +306,9 @@ try {
"/e26",
"/docs/leaders",
"/install",
securityContact.email,
securityContact.mailto,
"SECURITY.md",
]) {
if (!snapshot.includes(needle)) {
fail(`renderStaticHomeAndHub missing ${needle}`);
Expand All @@ -294,6 +321,7 @@ try {
["/docs/std", "Standard library"],
["/e26", "Echo 2026"],
["/install", "Install Echo"],
["/security", "Security"],
["/docs/first-program", "First program"],
["/book", "Introduction"],
["/privacy", "Privacy"],
Expand Down
1 change: 1 addition & 0 deletions www/scripts/verify-prose.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ try {
"src/app.tsx",
"src/install.tsx",
"src/legal.tsx",
"src/security.tsx",
"src/router.tsx",
"src/docs/site.ts",
"src/docs/install-content.ts",
Expand Down
22 changes: 22 additions & 0 deletions www/src/docs/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ export const primaryNav: SiteNavItem[] = [

export const installCta: SiteNavItem = { label: "Install", to: "/install" };

/**
* Public vulnerability reporting. Mailbox is @modoterra.xyz only.
* Discord stays omitted from the footer until there is a public invite.
*/
export const securityContact = {
email: "security@modoterra.xyz",
mailto: "mailto:security@modoterra.xyz",
path: "/security",
policyUrl: "https://github.com/modoterra/echo/blob/main/SECURITY.md",
} as const;

export const securityPage = {
title: "Security",
lead: "Report vulnerabilities by email. Do not open a public GitHub issue for a security report.",
};

export type FooterLink = {
label: string;
href: string;
Expand Down Expand Up @@ -96,6 +112,7 @@ export const footerLinkGroups: FooterLinkGroup[] = [
},
{ label: "Privacy", href: "/privacy" },
{ label: "Terms", href: "/terms" },
{ label: "Security", href: securityContact.path },
],
},
];
Expand Down Expand Up @@ -512,6 +529,11 @@ export function renderStaticHomeAndHub(): string {
`<pre>${escapeHtml(homePage.sample)}</pre>`,
`<nav aria-label="Language surfaces"><ul>${surfaceLinks}</ul></nav>`,
catalog,
`<aside>`,
`<h2>Security</h2>`,
`<p>Report vulnerabilities to <a href="${escapeHtml(securityContact.mailto)}">${escapeHtml(securityContact.email)}</a>.`,
` Policy: <a href="${escapeHtml(securityContact.policyUrl)}">SECURITY.md</a>.</p>`,
`</aside>`,
`</main>`,
].join("");
}
19 changes: 19 additions & 0 deletions www/src/docs/static-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
homePage,
legalPages,
renderStaticHomeAndHub,
securityContact,
securityPage,
tryPage,
type LegalPageContent,
} from "./site";
Expand Down Expand Up @@ -148,6 +150,17 @@ export function renderStaticTry(): string {
].join("");
}

export function renderStaticSecurity(): string {
return [
`<main>`,
`<h1>${escapeHtml(securityPage.title)}</h1>`,
`<p>${escapeHtml(securityPage.lead)}</p>`,
`<p>Email <a href="${escapeHtml(securityContact.mailto)}">${escapeHtml(securityContact.email)}</a> with a description of the issue and its impact, steps to reproduce or a proof of concept if available, and affected versions, commits, or platforms if known.</p>`,
`<p>The repository policy is <a href="${escapeHtml(securityContact.policyUrl)}">SECURITY.md</a>.</p>`,
`</main>`,
].join("");
}

function documentTitle(pageTitle: string): string {
return `${pageTitle} · Echo`;
}
Expand All @@ -172,6 +185,12 @@ export function staticPages(): StaticPage[] {
description: tryPage.lead,
body: renderStaticTry(),
},
{
path: securityContact.path,
title: documentTitle(securityPage.title),
description: securityPage.lead,
body: renderStaticSecurity(),
},
];

for (const page of legalPages) {
Expand Down
12 changes: 10 additions & 2 deletions www/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { EchoCode } from "./components/echo-code";
import { Logo } from "./components/logo";
import { InstallPage } from "./install";
import { LegalPage } from "./legal";
import { SecurityPage } from "./security";
import { TryPage } from "./try";
import {
docsPageByPath,
Expand Down Expand Up @@ -342,8 +343,8 @@ function SiteFooter() {
<a
className="text-sm text-slate-500 transition hover:text-slate-950"
href={link.href}
rel="noreferrer"
target="_blank"
rel={link.href.startsWith("mailto:") ? undefined : "noreferrer"}
target={link.href.startsWith("mailto:") ? undefined : "_blank"}
>
{link.label}
</a>
Expand Down Expand Up @@ -951,6 +952,12 @@ const installRoute = createRoute({
component: InstallPage,
});

const securityRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/security",
component: SecurityPage,
});

const tryRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/try",
Expand Down Expand Up @@ -1047,6 +1054,7 @@ const e26Children = [
const routeTree = rootRoute.addChildren([
indexRoute,
installRoute,
securityRoute,
tryRoute,
privacyRoute,
termsRoute,
Expand Down
75 changes: 75 additions & 0 deletions www/src/security.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { securityContact } from "./docs/site";

/**
* Public vulnerability reporting. Same mailbox and policy file as SECURITY.md.
*/
export function SecurityPage() {
return (
<main className="min-h-screen bg-white px-6 pb-24 pt-32 text-slate-950 sm:pt-36">
<div className="mx-auto w-full max-w-3xl">
<h1 className="text-3xl font-semibold tracking-normal text-slate-950 sm:text-4xl">
Security
</h1>
<p className="mt-4 text-pretty text-lg leading-8 text-slate-600">
Report vulnerabilities by email. Do not open a public GitHub issue for a security report.
</p>

<section className="mt-14">
<h2 className="text-xl font-semibold tracking-normal text-slate-950 sm:text-2xl">
Mailbox
</h2>
<p className="mt-4 text-pretty text-base leading-7 text-slate-600 sm:text-lg sm:leading-8">
Email{" "}
<a
className="font-semibold text-slate-800 underline-offset-4 hover:underline"
href={securityContact.mailto}
>
{securityContact.email}
</a>{" "}
with a description of the issue and its impact, steps to reproduce or a proof of concept
if available, and affected versions, commits, or platforms if known.
</p>
</section>

<section className="mt-14">
<h2 className="text-xl font-semibold tracking-normal text-slate-950 sm:text-2xl">
Policy
</h2>
<p className="mt-4 text-pretty text-base leading-7 text-slate-600 sm:text-lg sm:leading-8">
The repository policy is{" "}
<a
className="font-semibold text-slate-800 underline-offset-4 hover:underline"
href={securityContact.policyUrl}
rel="noreferrer"
target="_blank"
>
SECURITY.md
</a>
. We acknowledge receipt when we can and work with you on coordinated disclosure. Give a
reasonable window to investigate and ship a fix before public discussion.
</p>
</section>

<section className="mt-14">
<h2 className="text-xl font-semibold tracking-normal text-slate-950 sm:text-2xl">
Other bugs
</h2>
<p className="mt-4 text-pretty text-base leading-7 text-slate-600 sm:text-lg sm:leading-8">
Non-security bugs belong on{" "}
<a
className="font-semibold text-slate-800 underline-offset-4 hover:underline"
href="https://github.com/modoterra/echo/issues"
rel="noreferrer"
target="_blank"
>
GitHub issues
</a>
.
</p>
</section>
</div>
</main>
);
}

export default SecurityPage;
Loading