seo: starter examples are "New <board> project" pages

data/starters.ts maps eight free boards to the gallery example that opens
the editor with the board and a working blink (blink-led, nano-blink,
mega-blink, esp32-blink-led, esp32s3-blink-led, c3-blink, pico-blink,
attiny85-blink). ExampleEditorPage gives those ids an action-shaped
title/description ("New ESP32 project — free online ESP32 simulator")
while the gallery keeps the sketch title; the prerender writes
dist/starters.json so the prod app shell can serve the same head in the
raw HTML. This is the Wokwi "New ESP32 Project" shape: a URL of its own,
canonical of its own, that lands inside the product with the board loaded
— a candidate for brand sitelinks with usage intent.
This commit is contained in:
David Montero 2026-08-18 17:38:13 +02:00
parent 6f19cf9068
commit e1fc833f61
3 changed files with 79 additions and 5 deletions

View File

@ -108,6 +108,20 @@ try {
// Route metadata for the prod app shell (title/description per SEO route),
// so /<locale>/<route> paths that fall through to it get the route's own
// head instead of the homepage's.
// Starter examples ("New <board> project" pages) for the prod app shell.
const starters = await vite.ssrLoadModule('/src/data/starters.ts');
writeFileSync(
join(distDir, 'starters.json'),
JSON.stringify(
starters.STARTER_EXAMPLES.map((st) => ({
id: st.id, board: st.board,
title: starters.starterTitle(st.board),
description: starters.starterDescription(st.board),
})),
),
'utf-8',
);
const { SEO_ROUTES } = await vite.ssrLoadModule('/src/seoRoutes.ts');
writeFileSync(
join(distDir, 'seo-routes.json'),

View File

@ -0,0 +1,53 @@
/**
* Starter examples — the "New <board> project" entry points.
*
* Each maps a board to the gallery example that opens the editor with that
* board and a minimal working sketch (a blink), so /example/<id> is a
* one-click "new project" for the board, the way Wokwi's "New ESP32
* Project" pages work. Used by:
* - pages/ExampleEditorPage.tsx: action-shaped <title>/description for
* these ids ("New ESP32 project — ...") so they can surface as brand
* sitelinks with usage intent (the gallery keeps the example's own title)
* - scripts/prerender-seo.mjs: writes dist/starters.json for the prod
* app shell, which serves the same head in the raw HTML
* - the velxio.dev landing (pro overlay): the "Start a new project" tiles
*
* Free boards only — a starter that lands on a paywall is not a starter.
*/
export interface StarterExample {
/** Gallery example id (=> /example/<id>). */
id: string;
/** Board name as shown in the tile and the title. */
board: string;
}
export const STARTER_EXAMPLES: readonly StarterExample[] = [
{ id: 'blink-led', board: 'Arduino Uno' },
{ id: 'nano-blink', board: 'Arduino Nano' },
{ id: 'mega-blink', board: 'Arduino Mega' },
{ id: 'esp32-blink-led', board: 'ESP32' },
{ id: 'esp32s3-blink-led', board: 'ESP32-S3' },
{ id: 'c3-blink', board: 'ESP32-C3' },
{ id: 'pico-blink', board: 'Raspberry Pi Pico' },
{ id: 'attiny85-blink', board: 'ATtiny85' },
];
/** Board name when `id` is a starter example, else undefined. */
export function starterBoard(id: string | undefined): string | undefined {
if (!id) return undefined;
return STARTER_EXAMPLES.find((s) => s.id === id)?.board;
}
/** <title> for a starter's editor page. */
export function starterTitle(board: string): string {
return `New ${board} project — free online ${board} simulator | Velxio`;
}
/** Meta description for a starter's editor page. */
export function starterDescription(board: string): string {
return (
`Start a new ${board} project in your browser: the editor opens with a ${board} ` +
`and a working blink sketch — edit the code, wire components and run the ` +
`simulation. Free, no install, no account needed.`
);
}

View File

@ -29,6 +29,7 @@ import { loadExample, type LibraryInstallProgress } from '../utils/loadExample';
import { EditorPage } from './EditorPage';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { starterBoard, starterTitle, starterDescription } from '../data/starters';
const DOMAIN = 'https://velxio.dev';
@ -49,12 +50,18 @@ export const ExampleEditorPage: React.FC = () => {
? exampleProjects.find((e) => e.id === exampleId)
: null;
// Starter examples are the "New <board> project" entry points: their
// head says the action, not the sketch (the gallery keeps the sketch title).
const board = starterBoard(example?.id);
useSEO({
title: example
title: board
? starterTitle(board)
: example
? `${example.title} — Velxio Arduino Simulator`
: 'Example — Velxio',
description:
example?.description ?? 'Arduino example running on Velxio.',
description: board
? starterDescription(board)
: (example?.description ?? 'Arduino example running on Velxio.'),
url: example
? `${DOMAIN}/example/${example.id}`
: `${DOMAIN}/examples`,