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
353 changes: 290 additions & 63 deletions README.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions web/e2e/docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ test("the how-it-works embed runs on the single-cycle core", async ({ page }) =>
await expect(embed.getByText(/Halted at cycle \d+ — PASS/)).toBeVisible();
});

test("the landing page carries the social card, canonical link and structured data", async ({ page, request }) => {
await page.goto("/");
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute("content", /\/og-image\.png$/);
await expect(page.locator('meta[name="twitter:card"]')).toHaveAttribute("content", "summary_large_image");
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute("href", /tiny-cpu\.vercel\.app\/$/);
const jsonLd = await page.locator('script[type="application/ld+json"]').textContent();
expect(JSON.parse(jsonLd ?? "{}")["@type"]).toBe("SoftwareSourceCode");
const image = await request.get("/og-image.png");
expect(image.ok()).toBeTruthy();
const sitemap = await request.get("/sitemap.xml");
expect(sitemap.ok()).toBeTruthy();
expect(await sitemap.text()).toContain("/docs/verification/");
});

test("docs pages have no horizontal scroll at phone width", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/docs/verification");
Expand Down
10 changes: 9 additions & 1 deletion web/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions web/public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions web/public/og-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions web/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://tiny-cpu.vercel.app/sitemap.xml
10 changes: 10 additions & 0 deletions web/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "TinyCPU",
"short_name": "TinyCPU",
"description": "A real RV32I RISC-V CPU in SystemVerilog, running live in your browser.",
"start_url": "/",
"display": "standalone",
"background_color": "#0d1117",
"theme_color": "#0d1117",
"icons": [{ "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml" }]
}
120 changes: 120 additions & 0 deletions web/scripts/make_logo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Writes the TinyCPU logo marks as SVG: the square mark (logo.svg), the favicon, and the social card art.

python3 web/scripts/make_logo.py

The mark is a chip with pins on four sides. Inside it, three instructions move through five pipeline stages, each one
a step to the right of the one above: the overlap a pipeline gives you. The stage colours are the same in the app.
"""

from pathlib import Path

OUT = Path(__file__).resolve().parents[1] / "public"
BG = "#0d1117"
BODY = "#161b22"
EDGE = "#30363d"
PIN = "#58a6ff"
STAGES = ["#58a6ff", "#7ee787", "#d2a8ff", "#ffa657", "#ff7b72"] # IF ID EX MEM WB


def pipeline_cells(x0: float, y0: float, span: float, rows: int = 3, cols_total: int = 7) -> str:
"""Staircase of stage cells: row r holds stages 0..4 starting at column r."""
cell_w = span / cols_total
cell_h = span / rows
pad = cell_w * 0.12
parts = []
for r in range(rows):
for s in range(5):
x = x0 + (r + s) * cell_w + pad
y = y0 + r * cell_h + cell_h * 0.12
w = cell_w - 2 * pad
h = cell_h * 0.76
parts.append(
f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" rx="{w * 0.18:.1f}" '
f'fill="{STAGES[s]}" opacity="{0.95 - 0.12 * r:.2f}"/>'
)
return "\n ".join(parts)


def pins() -> str:
parts = []
xs = [158, 212, 256, 300, 354]
for x in xs:
parts.append(f'<path d="M{x} 58V112M{x} 400V454"/>')
for y in xs:
parts.append(f'<path d="M58 {y}H112M400 {y}H454"/>')
return f'<g stroke="{PIN}" stroke-width="13" stroke-linecap="round" opacity="0.85">{"".join(parts)}</g>'


def mark(size: int = 512, rounded: bool = True) -> str:
bg_rx = 96 if rounded else 0
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="{size}" height="{size}">
<rect width="512" height="512" rx="{bg_rx}" fill="{BG}"/>
{pins()}
<rect x="112" y="112" width="288" height="288" rx="26" fill="{BODY}" stroke="{EDGE}" stroke-width="6"/>
<rect x="136" y="136" width="240" height="240" rx="14" fill="none" stroke="{PIN}" stroke-opacity="0.35" stroke-width="3"/>
{pipeline_cells(150, 150, 212)}
</svg>
"""


def favicon() -> str:
# Simpler at 32 px: a chip with one bright diagonal of stages.
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="{BG}"/>
<rect x="8" y="8" width="16" height="16" rx="2.5" fill="{BODY}" stroke="{PIN}" stroke-width="1.6"/>
<rect x="10.5" y="10.5" width="4" height="4" rx="0.8" fill="{STAGES[0]}"/>
<rect x="15" y="12.5" width="4" height="4" rx="0.8" fill="{STAGES[1]}"/>
<rect x="19.5" y="14.5" width="4" height="4" rx="0.8" fill="{STAGES[2]}"/>
<rect x="10.5" y="17.5" width="4" height="4" rx="0.8" fill="{STAGES[3]}" opacity="0.7"/>
<rect x="15" y="19.5" width="4" height="4" rx="0.8" fill="{STAGES[4]}" opacity="0.7"/>
</svg>
"""


def social() -> str:
"""The 1200x630 social card (Open Graph and Twitter), rendered to PNG by render_social.sh."""
chips = ["RV32I", "SystemVerilog", "WebAssembly", "Verilator"]
chip_svg = []
x = 0
for c in chips:
w = 28 + len(c) * 10
chip_svg.append(
f'<g transform="translate({x:.0f} 0)"><rect width="{w:.0f}" height="46" rx="23" fill="{BODY}" '
f'stroke="{EDGE}" stroke-width="2"/><text x="{w / 2:.0f}" y="30" text-anchor="middle" '
f'font-size="18" fill="#c9d1d9" font-family="Inter, DejaVu Sans, sans-serif">{c}</text></g>'
)
x += w + 12
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
<rect width="1200" height="630" fill="{BG}"/>
<defs>
<radialGradient id="glow" cx="0.25" cy="0.5" r="0.6">
<stop offset="0" stop-color="#58a6ff" stop-opacity="0.18"/>
<stop offset="1" stop-color="#58a6ff" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="1200" height="630" fill="url(#glow)"/>
<g transform="translate(70 130) scale(0.82)">
<rect width="512" height="512" rx="96" fill="{BG}" stroke="{EDGE}" stroke-width="4"/>
{pins()}
<rect x="112" y="112" width="288" height="288" rx="26" fill="{BODY}" stroke="{EDGE}" stroke-width="6"/>
<rect x="136" y="136" width="240" height="240" rx="14" fill="none" stroke="{PIN}" stroke-opacity="0.35" stroke-width="3"/>
{pipeline_cells(150, 150, 212)}
</g>
<text x="520" y="250" font-size="104" font-weight="700" fill="#e6edf3" font-family="Inter, DejaVu Sans, sans-serif">TinyCPU</text>
<text x="524" y="318" font-size="28" fill="#8b949e" font-family="Inter, DejaVu Sans, sans-serif">A real RISC-V CPU, running in your browser</text>
<g transform="translate(524 370)">{"".join(chip_svg)}</g>
<text x="524" y="520" font-size="24" fill="#58a6ff" font-family="Inter, DejaVu Sans, sans-serif">tiny-cpu.vercel.app</text>
</svg>
"""


def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "logo.svg").write_text(mark(512))
(OUT / "favicon.svg").write_text(favicon())
(OUT / "og-image.svg").write_text(social())
print("wrote logo.svg, favicon.svg and og-image.svg in", OUT)


if __name__ == "__main__":
main()
11 changes: 11 additions & 0 deletions web/scripts/render_social.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Renders web/public/og-image.svg to og-image.png (1200x630) with the headless Chromium that the browser tests use.
# Needs `pnpm exec playwright install chromium` once (PLAYWRIGHT_BROWSERS_PATH as in CLAUDE.md, if you use one).
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PUBLIC="$HERE/../public"
BIN="$(ls -d "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"/chromium_headless_shell-*/chrome-linux*/headless_shell 2>/dev/null | head -n 1)"
[ -n "$BIN" ] || { echo "headless_shell not found: install Chromium with pnpm exec playwright install chromium" >&2; exit 1; }
"$BIN" --no-sandbox --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \
--window-size=1200,630 --screenshot="$PUBLIC/og-image.png" "file://$PUBLIC/og-image.svg" >/dev/null 2>&1
echo "wrote $PUBLIC/og-image.png"
2 changes: 1 addition & 1 deletion web/src/layouts/DocsLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const pages = [
<Layout title={`${title} · TinyCPU`}>
<header class="border-b border-[var(--line)]">
<nav class="mx-auto flex max-w-5xl flex-wrap items-center gap-x-5 gap-y-2 px-4 py-3 text-sm" aria-label="Site">
<a href="/" class="font-bold text-[var(--fg)]">TinyCPU</a>
<a href="/" class="flex items-center gap-2 font-bold text-[var(--fg)]"><img src="/logo.svg" alt="" width="26" height="26" />TinyCPU</a>
{
pages.map((p) => (
<a href={p.href} class="text-[var(--muted)] hover:text-[var(--fg)]" aria-current={p.href === current ? "page" : undefined}>
Expand Down
30 changes: 29 additions & 1 deletion web/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import "@/styles/global.css";
interface Props {
title: string;
description?: string;
/** JSON-LD for this page (schema.org). The home page describes the project as a SoftwareSourceCode. */
structuredData?: Record<string, unknown>;
}

const {
title,
description = "A real RV32I RISC-V CPU in SystemVerilog, compiled to WebAssembly and running live in your browser.",
description = "TinyCPU is a real RV32I RISC-V CPU in SystemVerilog with a single-cycle core and a 5-stage pipeline, compiled to WebAssembly and running live in your browser.",
structuredData,
} = Astro.props;

const canonical = new URL(Astro.url.pathname, Astro.site);
const image = new URL("/og-image.png", Astro.site);
---

<!doctype html>
Expand All @@ -30,6 +36,28 @@ const {
})();
</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="canonical" href={canonical} />
<meta name="theme-color" content="#0d1117" />
<meta name="author" content="tinycomputers" />
<meta name="robots" content="index, follow" />

<!-- Open Graph and Twitter cards: the social preview when the link is shared. -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="TinyCPU" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={image} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="TinyCPU: a RISC-V CPU running in your browser" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{structuredData && <script type="application/ld+json" set:html={JSON.stringify(structuredData)} />}

<title>{title}</title>
</head>
<body>
Expand Down
20 changes: 18 additions & 2 deletions web/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@ import { MiniRun } from "@/components/MiniRun";
import fib from "@/lib/presets/fib.s?raw";
---

<Layout title="TinyCPU - a RISC-V CPU that runs in your browser">
<Layout
title="TinyCPU: a RISC-V CPU in SystemVerilog, running in your browser"
description="TinyCPU is an open-source RV32I RISC-V processor in SystemVerilog, with a single-cycle core and a 5-stage pipeline with forwarding and hazard handling. It compiles to WebAssembly so you can step through every clock cycle in your browser."
structuredData={{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
name: "TinyCPU",
description: "A real, synthesizable RV32I RISC-V CPU in SystemVerilog (single-cycle and 5-stage pipelined), compiled to WebAssembly and running live in the browser.",
url: "https://tiny-cpu.vercel.app",
codeRepository: "https://github.com/tinycomputers/tiny-cpu",
programmingLanguage: ["SystemVerilog", "TypeScript", "Python", "C++"],
runtimePlatform: "WebAssembly",
license: "https://opensource.org/licenses/MIT",
keywords: ["RISC-V", "RV32I", "CPU", "SystemVerilog", "pipeline", "computer architecture", "WebAssembly", "FPGA", "education"],
author: { "@type": "Organization", name: "tinycomputers", url: "https://github.com/tinycomputers" },
}}
>
<header class="border-b border-[var(--line)]">
<nav class="mx-auto flex max-w-5xl items-center gap-5 px-4 py-3 text-sm" aria-label="Site">
<a href="/" class="font-bold text-[var(--fg)]">TinyCPU</a>
<a href="/" class="flex items-center gap-2 font-bold text-[var(--fg)]"><img src="/logo.svg" alt="" width="26" height="26" />TinyCPU</a>
<a href="/docs/how-it-works" class="text-[var(--muted)] hover:text-[var(--fg)]">Docs</a>
<a href="/docs/verification" class="text-[var(--muted)] hover:text-[var(--fg)]">Verification</a>
<a href="/app" class="ml-auto rounded-md bg-[var(--accent)] px-3 py-1 font-semibold text-[var(--bg)]">Open the CPU</a>
Expand Down
Loading
Loading