diff --git a/frontend/scripts/prerender-seo.mjs b/frontend/scripts/prerender-seo.mjs index d510c295..1490b787 100644 --- a/frontend/scripts/prerender-seo.mjs +++ b/frontend/scripts/prerender-seo.mjs @@ -108,6 +108,20 @@ try { // Route metadata for the prod app shell (title/description per SEO route), // so // paths that fall through to it get the route's own // head instead of the homepage's. + // Starter examples ("New 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'), diff --git a/frontend/src/data/starters.ts b/frontend/src/data/starters.ts new file mode 100644 index 00000000..2ee6f265 --- /dev/null +++ b/frontend/src/data/starters.ts @@ -0,0 +1,53 @@ +/** + * Starter examples — the "New 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/ 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 /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.` + ); +} diff --git a/frontend/src/pages/ExampleEditorPage.tsx b/frontend/src/pages/ExampleEditorPage.tsx index dd234ab5..fceade0e 100644 --- a/frontend/src/pages/ExampleEditorPage.tsx +++ b/frontend/src/pages/ExampleEditorPage.tsx @@ -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 - ? `${example.title} — Velxio Arduino Simulator` - : 'Example — Velxio', - description: - example?.description ?? 'Arduino example running on Velxio.', + title: board + ? starterTitle(board) + : example + ? `${example.title} — Velxio Arduino Simulator` + : 'Example — Velxio', + description: board + ? starterDescription(board) + : (example?.description ?? 'Arduino example running on Velxio.'), url: example ? `${DOMAIN}/example/${example.id}` : `${DOMAIN}/examples`,