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),
|
// 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
|
// so /<locale>/<route> paths that fall through to it get the route's own
|
||||||
// head instead of the homepage's.
|
// 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');
|
const { SEO_ROUTES } = await vite.ssrLoadModule('/src/seoRoutes.ts');
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(distDir, 'seo-routes.json'),
|
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 { EditorPage } from './EditorPage';
|
||||||
import { AppHeader } from '../components/layout/AppHeader';
|
import { AppHeader } from '../components/layout/AppHeader';
|
||||||
import { useSEO } from '../utils/useSEO';
|
import { useSEO } from '../utils/useSEO';
|
||||||
|
import { starterBoard, starterTitle, starterDescription } from '../data/starters';
|
||||||
|
|
||||||
const DOMAIN = 'https://velxio.dev';
|
const DOMAIN = 'https://velxio.dev';
|
||||||
|
|
||||||
|
|
@ -49,12 +50,18 @@ export const ExampleEditorPage: React.FC = () => {
|
||||||
? exampleProjects.find((e) => e.id === exampleId)
|
? exampleProjects.find((e) => e.id === exampleId)
|
||||||
: null;
|
: 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({
|
useSEO({
|
||||||
title: example
|
title: board
|
||||||
? `${example.title} — Velxio Arduino Simulator`
|
? starterTitle(board)
|
||||||
: 'Example — Velxio',
|
: example
|
||||||
description:
|
? `${example.title} — Velxio Arduino Simulator`
|
||||||
example?.description ?? 'Arduino example running on Velxio.',
|
: 'Example — Velxio',
|
||||||
|
description: board
|
||||||
|
? starterDescription(board)
|
||||||
|
: (example?.description ?? 'Arduino example running on Velxio.'),
|
||||||
url: example
|
url: example
|
||||||
? `${DOMAIN}/example/${example.id}`
|
? `${DOMAIN}/example/${example.id}`
|
||||||
: `${DOMAIN}/examples`,
|
: `${DOMAIN}/examples`,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue