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:
parent
6f19cf9068
commit
e1fc833f61
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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.`
|
||||
);
|
||||
}
|
||||
|
|
@ -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`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue