feat: enhance build scripts and add SEO prerendering; improve touch interactions for wire creation

pull/72/head
David Montero Crespo 2026-03-25 00:08:46 -03:00
parent effd1ee765
commit 005f50262f
21 changed files with 771 additions and 195 deletions

View File

@ -10,8 +10,8 @@
"generate:og-image": "node ../scripts/generate-og-image.mjs",
"generate:sitemap": "node scripts/generate-sitemap.mjs",
"dev": "npm run generate:metadata && vite",
"build": "npm run generate:metadata && npm run generate:sitemap && tsc -b && vite build",
"build:docker": "npm run generate:sitemap && vite build",
"build": "npm run generate:metadata && npm run generate:sitemap && tsc -b && vite build && node scripts/prerender-seo.mjs",
"build:docker": "npm run generate:sitemap && vite build && node scripts/prerender-seo.mjs",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest run",

View File

@ -4,163 +4,163 @@
<url>
<loc>https://velxio.dev/</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://velxio.dev/editor</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/examples</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/intro</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/getting-started</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/emulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/esp32-emulation</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/riscv-emulation</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/rp2040-emulation</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/raspberry-pi3-emulation</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/components</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/architecture</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/wokwi-libs</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/mcp</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/setup</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/docs/roadmap</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/arduino-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/arduino-emulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/atmega328p-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/arduino-mega-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/esp32-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/esp32-s3-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/esp32-c3-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/raspberry-pi-pico-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/raspberry-pi-simulator</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://velxio.dev/v2</loc>
<lastmod>2026-03-23</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>

View File

@ -29,8 +29,9 @@ const arrayStr = match[1]
.replace(/\/\/.*$/gm, '') // remove line comments
.replace(/\/\*[\s\S]*?\*\//g, ''); // remove block comments
// Use Function constructor to evaluate the JS array literal
const routes = new Function(`return [${arrayStr}]`)();
// Use Function constructor to evaluate the JS array literal.
// Inject DOMAIN so template literals like `${DOMAIN}/path` resolve correctly.
const routes = new Function('DOMAIN', `return [${arrayStr}]`)(DOMAIN);
const indexable = routes.filter((r) => !r.noindex);

View File

@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* prerender-seo.mjs Generates route-specific HTML for SEO-important pages.
*
* How it works:
* 1. Starts a Vite dev server in SSR mode (no HTTP server, just the transform pipeline)
* 2. Loads src/entry-server.tsx through Vite handles TSX, CSS, SVG, path aliases
* 3. For each route with seoMeta in seoRoutes.ts:
* a. Renders the React component to HTML via renderToString
* b. Replaces <title>, <meta>, OG/Twitter tags, canonical URL from seoMeta
* c. Injects the rendered HTML into #root-seo
* 4. Writes to dist/{route}/index.html
*
* nginx's try_files ($uri/) serves these automatically.
*
* Run after `vite build`: node scripts/prerender-seo.mjs
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createServer } from 'vite';
const __dirname = dirname(fileURLToPath(import.meta.url));
const distDir = join(__dirname, '..', 'dist');
// Check that dist exists (vite build must have run first)
if (!existsSync(join(distDir, 'index.html'))) {
console.error('❌ dist/index.html not found. Run `vite build` first.');
process.exit(1);
}
const baseHtml = readFileSync(join(distDir, 'index.html'), 'utf-8');
const DOMAIN = 'https://velxio.dev';
// ── Mock browser globals for SSR ────────────────────────────────────────────
// Zustand's persist middleware and some components access these at import time.
if (typeof globalThis.localStorage === 'undefined') {
globalThis.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => null,
};
}
if (typeof globalThis.window === 'undefined') {
globalThis.window = globalThis;
}
if (typeof globalThis.navigator === 'undefined') {
globalThis.navigator = { userAgent: 'prerender-seo' };
}
if (typeof globalThis.matchMedia === 'undefined') {
globalThis.matchMedia = () => ({ matches: false, addListener: () => {}, removeListener: () => {} });
}
// ── Start Vite in SSR mode ──────────────────────────────────────────────────
console.log('🔧 Starting Vite SSR transform pipeline...');
const vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
logLevel: 'warn',
});
let generated = 0;
try {
// Load entry-server.tsx through Vite's transform pipeline
const { getPrerenderedRoutes, render } = await vite.ssrLoadModule('/src/entry-server.tsx');
const routes = getPrerenderedRoutes();
console.log(`📄 Prerendering ${routes.length} SEO pages...\n`);
for (const route of routes) {
const { seoMeta } = route;
if (!seoMeta) continue;
// Render the React component to HTML
let bodyHtml = render(route.path);
// Build the page HTML
let html = baseHtml;
// Replace <title>
html = html.replace(/<title>[^<]*<\/title>/, `<title>${seoMeta.title}</title>`);
// Replace meta description
html = html.replace(
/<meta name="description" content="[^"]*"/,
`<meta name="description" content="${seoMeta.description}"`
);
// Add/replace canonical URL
const canonicalTag = `<link rel="canonical" href="${seoMeta.url}" />`;
if (html.includes('<link rel="canonical"')) {
html = html.replace(/<link rel="canonical"[^>]*\/>/, canonicalTag);
} else {
html = html.replace('</head>', ` ${canonicalTag}\n </head>`);
}
// Replace OG tags
html = html.replace(
/<meta property="og:title" content="[^"]*"/,
`<meta property="og:title" content="${seoMeta.title}"`
);
html = html.replace(
/<meta property="og:description" content="[^"]*"/,
`<meta property="og:description" content="${seoMeta.description}"`
);
html = html.replace(
/<meta property="og:url" content="[^"]*"/,
`<meta property="og:url" content="${seoMeta.url}"`
);
// Replace Twitter tags
html = html.replace(
/<meta name="twitter:title" content="[^"]*"/,
`<meta name="twitter:title" content="${seoMeta.title}"`
);
html = html.replace(
/<meta name="twitter:description" content="[^"]*"/,
`<meta name="twitter:description" content="${seoMeta.description}"`
);
// Replace #root-seo content with SSR-rendered body (or fallback to title+description)
const seoBody = bodyHtml
? bodyHtml
: `<h1>${seoMeta.title.split(' | ')[0]}</h1><p>${seoMeta.description}</p>`;
html = html.replace(
/<div id="root-seo"[^>]*>[\s\S]*?<\/div>\s*<script/,
`<div id="root-seo" aria-hidden="true">${seoBody}</div>\n <script`
);
// Write to dist/{path}/index.html
const routePath = route.path === '/' ? '' : route.path.slice(1);
if (routePath) {
const dir = join(distDir, routePath);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'index.html'), html, 'utf-8');
} else {
// Root — update dist/index.html in place
writeFileSync(join(distDir, 'index.html'), html, 'utf-8');
}
generated++;
const ssrStatus = bodyHtml ? '✓' : '⚠ (meta only)';
console.log(` ${ssrStatus} ${route.path}`);
}
// Also ensure root index.html has a canonical tag
let rootHtml = readFileSync(join(distDir, 'index.html'), 'utf-8');
const rootCanonical = `<link rel="canonical" href="${DOMAIN}/" />`;
if (!rootHtml.includes('<link rel="canonical"')) {
rootHtml = rootHtml.replace('</head>', ` ${rootCanonical}\n </head>`);
writeFileSync(join(distDir, 'index.html'), rootHtml, 'utf-8');
}
} finally {
await vite.close();
}
console.log(`\n✅ Prerendered ${generated} SEO pages with SSR content`);

View File

@ -74,17 +74,22 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
return (
<div
key={`${pin.name}-${index}`}
data-pin-overlay="true"
onClick={(e) => {
e.stopPropagation();
onPinClick(componentId, pin.name, componentX + wrapperOffsetX + pinX, componentY + wrapperOffsetY + pinY);
}}
onTouchEnd={(e) => {
e.stopPropagation();
onPinClick(componentId, pin.name, componentX + wrapperOffsetX + pinX, componentY + wrapperOffsetY + pinY);
}}
style={{
position: 'absolute',
left: `${pinX - 4}px`,
top: `${pinY - 4}px`,
width: '8px',
height: '8px',
borderRadius: '2px',
left: `${pinX - 6}px`,
top: `${pinY - 6}px`,
width: '12px',
height: '12px',
borderRadius: '3px',
backgroundColor: 'rgba(0, 200, 255, 0.8)',
border: '1.5px solid white',
cursor: 'crosshair',

View File

@ -241,3 +241,94 @@
text-align: center;
color: #aaa;
}
/* ── Wire creation mode banner ──────────────────── */
.wire-mode-banner {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
background: rgba(0, 122, 204, 0.92);
color: #fff;
padding: 8px 16px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
white-space: nowrap;
pointer-events: auto;
}
.wire-mode-banner button {
background: rgba(255, 255, 255, 0.2);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 4px;
padding: 4px 14px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.15s;
}
.wire-mode-banner button:hover {
background: rgba(255, 255, 255, 0.35);
}
/* ── Mobile-friendly adjustments ────────────────── */
@media (max-width: 768px) {
.canvas-header {
height: 42px;
padding: 0 6px;
}
.canvas-header-left,
.canvas-header-right {
gap: 4px;
}
.canvas-serial-btn {
padding: 0 6px;
font-size: 0; /* hide text, show icon only */
}
.canvas-serial-btn svg {
width: 18px;
height: 18px;
}
.add-component-btn {
padding: 0 8px;
font-size: 0; /* hide text, show icon only */
}
.add-component-btn svg {
width: 18px;
height: 18px;
}
.board-selector {
font-size: 11px;
max-width: 90px;
padding: 4px 20px 4px 6px;
}
.zoom-controls {
display: none; /* Use pinch-to-zoom on mobile */
}
.component-count {
display: none;
}
.wire-mode-banner {
font-size: 12px;
padding: 6px 12px;
gap: 8px;
bottom: 8px;
}
}

View File

@ -170,6 +170,15 @@ export const SimulatorCanvas = () => {
const pinchStartMidRef = useRef({ x: 0, y: 0 });
const pinchStartPanRef = useRef({ x: 0, y: 0 });
// Refs for touch-based wire creation, selection, and interactive passthrough
const wireInProgressRef = useRef(wireInProgress);
wireInProgressRef.current = wireInProgress;
const selectedWireIdRef = useRef(selectedWireId);
selectedWireIdRef.current = selectedWireId;
const touchPassthroughRef = useRef(false);
const touchOnPinRef = useRef(false);
const lastTapTimeRef = useRef(0);
// Convert viewport coords to world (canvas) coords
const toWorld = useCallback((screenX: number, screenY: number) => {
const rect = canvasRef.current?.getBoundingClientRect();
@ -223,18 +232,24 @@ export const SimulatorCanvas = () => {
}, []);
// Attach touch listeners as non-passive so preventDefault() works, enabling
// single-finger pan, single-finger component drag, and two-finger pinch-to-zoom.
// single-finger pan, component drag, wire creation/selection, and two-finger pinch-to-zoom.
useEffect(() => {
const el = canvasRef.current;
if (!el) return;
const onTouchStart = (e: TouchEvent) => {
e.preventDefault(); // Prevent browser scroll / mouse-event synthesis
pinchStartDistRef.current = 0; // Reset pinch state on each new gesture
// Reset per-gesture flags
touchOnPinRef.current = false;
touchPassthroughRef.current = false;
pinchStartDistRef.current = 0;
if (e.touches.length === 2) {
// ── Two-finger pinch: cancel any active drag/pan and prepare zoom ──
e.preventDefault();
// Cancel wire in progress on two-finger gesture
if (wireInProgressRef.current) {
useSimulatorStore.getState().cancelWireCreation();
}
// Cancel any active drag/pan and prepare zoom
isPanningRef.current = false;
touchDraggedComponentIdRef.current = null;
@ -254,16 +269,47 @@ export const SimulatorCanvas = () => {
if (e.touches.length !== 1) return;
const touch = e.touches[0];
touchClickStartTimeRef.current = Date.now();
touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };
// Identify what element was touched
const target = document.elementFromPoint(touch.clientX, touch.clientY);
// ── 1. Pin overlay → let pin's onTouchEnd React handler call handlePinClick ──
if (target?.closest('[data-pin-overlay]')) {
e.preventDefault();
touchOnPinRef.current = true;
return;
}
// ── 2. Interactive web component during simulation → let browser synthesize mouse events ──
// (potentiometer knobs, button presses, etc. need mousedown/mouseup synthesis)
// touch-action:none on .canvas-content already prevents browser scroll/zoom.
if (runningRef.current) {
const webComp = target?.closest('.web-component-container');
if (webComp) {
touchPassthroughRef.current = true;
// Don't preventDefault → browser synthesizes mouse events for the component
return;
}
}
e.preventDefault();
touchClickStartTimeRef.current = Date.now();
touchClickStartPosRef.current = { x: touch.clientX, y: touch.clientY };
// ── 3. Wire in progress → track for waypoint, update preview ──
if (wireInProgressRef.current) {
const world = toWorld(touch.clientX, touch.clientY);
useSimulatorStore.getState().updateWireInProgress(world.x, world.y);
// Don't start pan/drag — let touchmove update wire preview, touchend add waypoint
return;
}
// ── 4. Component detection ──
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
if (componentWrapper) {
// ── Single finger on a component: track for click/drag ──
const componentId = componentWrapper.getAttribute('data-component-id');
if (componentId) {
const component = componentsRef.current.find((c) => c.id === componentId);
@ -278,16 +324,29 @@ export const SimulatorCanvas = () => {
}
}
} else if (boardOverlay && !runningRef.current) {
// ── Single finger on the board overlay: start board drag ──
const board = boardPositionRef.current;
const world = toWorld(touch.clientX, touch.clientY);
touchDraggedComponentIdRef.current = '__board__';
touchDragOffsetRef.current = {
x: world.x - board.x,
y: world.y - board.y,
};
// ── 5. Board overlay: use multi-board path ──
const boardId = boardOverlay.getAttribute('data-board-id');
const storeBoards = useSimulatorStore.getState().boards;
const boardInstance = boardId ? storeBoards.find(b => b.id === boardId) : null;
if (boardInstance) {
const world = toWorld(touch.clientX, touch.clientY);
touchDraggedComponentIdRef.current = `__board__:${boardId}`;
touchDragOffsetRef.current = {
x: world.x - boardInstance.x,
y: world.y - boardInstance.y,
};
} else {
// Fallback to legacy single board
const board = boardPositionRef.current;
const world = toWorld(touch.clientX, touch.clientY);
touchDraggedComponentIdRef.current = '__board__';
touchDragOffsetRef.current = {
x: world.x - board.x,
y: world.y - board.y,
};
}
} else {
// ── Single finger on empty canvas: start pan ──
// ── 6. Empty canvas → start pan ──
isPanningRef.current = true;
panStartRef.current = {
mouseX: touch.clientX,
@ -299,6 +358,11 @@ export const SimulatorCanvas = () => {
};
const onTouchMove = (e: TouchEvent) => {
// Let interactive components handle their own touch (potentiometer drag, etc.)
if (touchPassthroughRef.current) return;
// Pin touch: no move processing needed
if (touchOnPinRef.current) { e.preventDefault(); return; }
e.preventDefault();
if (e.touches.length === 2 && pinchStartDistRef.current > 0) {
@ -331,6 +395,13 @@ export const SimulatorCanvas = () => {
if (e.touches.length !== 1) return;
const touch = e.touches[0];
// ── Wire preview: update position as finger moves ──
if (wireInProgressRef.current && !isPanningRef.current && !touchDraggedComponentIdRef.current) {
const world = toWorld(touch.clientX, touch.clientY);
useSimulatorStore.getState().updateWireInProgress(world.x, world.y);
return;
}
if (isPanningRef.current) {
// ── Single finger pan ──
const dx = touch.clientX - panStartRef.current.mouseX;
@ -369,6 +440,18 @@ export const SimulatorCanvas = () => {
};
const onTouchEnd = (e: TouchEvent) => {
// Let interactive components handle their own touch
if (touchPassthroughRef.current) {
touchPassthroughRef.current = false;
return;
}
// Pin touch: let pin's onTouchEnd React handler deal with it
if (touchOnPinRef.current) {
touchOnPinRef.current = false;
e.preventDefault();
return;
}
e.preventDefault();
// ── Finish pinch zoom: commit values to React state ──
@ -381,33 +464,49 @@ export const SimulatorCanvas = () => {
if (e.touches.length > 0) return; // Still fingers on screen
// ── Finish panning ──
let wasPanning = false;
if (isPanningRef.current) {
isPanningRef.current = false;
setPan({ ...panRef.current });
wasPanning = true;
// Don't return — fall through so short taps can select wires
}
const changed = e.changedTouches[0];
if (!changed) return;
const elapsed = Date.now() - touchClickStartTimeRef.current;
const dx = changed.clientX - touchClickStartPosRef.current.x;
const dy = changed.clientY - touchClickStartPosRef.current.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const isShortTap = dist < 10 && elapsed < 400;
// If we actually panned (moved significantly), don't process as tap
if (wasPanning && !isShortTap) return;
// ── Finish component/board drag ──
if (touchDraggedComponentIdRef.current) {
const elapsed = Date.now() - touchClickStartTimeRef.current;
const dx = changed ? changed.clientX - touchClickStartPosRef.current.x : 0;
const dy = changed ? changed.clientY - touchClickStartPosRef.current.y : 0;
const dist = Math.sqrt(dx * dx + dy * dy);
const touchId = touchDraggedComponentIdRef.current;
// Short tap with minimal movement → open property dialog or sensor panel
if (dist < 5 && elapsed < 300 && touchDraggedComponentIdRef.current !== '__board__') {
const component = componentsRef.current.find(
(c) => c.id === touchDraggedComponentIdRef.current
);
if (component) {
if (runningRef.current && SENSOR_CONTROLS[component.metadataId] !== undefined) {
setSensorControlComponentId(touchDraggedComponentIdRef.current);
setSensorControlMetadataId(component.metadataId);
} else {
setPropertyDialogComponentId(touchDraggedComponentIdRef.current);
setPropertyDialogPosition({ x: component.x, y: component.y });
setShowPropertyDialog(true);
if (isShortTap) {
if (touchId.startsWith('__board__:')) {
// Short tap on board → make it the active board
const boardId = touchId.slice('__board__:'.length);
useSimulatorStore.getState().setActiveBoardId(boardId);
} else if (touchId !== '__board__') {
// Short tap on component → open property dialog or sensor panel
const component = componentsRef.current.find(
(c) => c.id === touchId
);
if (component) {
if (runningRef.current && SENSOR_CONTROLS[component.metadataId] !== undefined) {
setSensorControlComponentId(touchId);
setSensorControlMetadataId(component.metadataId);
} else {
setPropertyDialogComponentId(touchId);
setPropertyDialogPosition({ x: component.x, y: component.y });
setShowPropertyDialog(true);
}
}
}
}
@ -417,13 +516,36 @@ export const SimulatorCanvas = () => {
return;
}
// ── Short tap on empty canvas: deselect ──
if (changed) {
const elapsed = Date.now() - touchClickStartTimeRef.current;
const dx = changed.clientX - touchClickStartPosRef.current.x;
const dy = changed.clientY - touchClickStartPosRef.current.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 5 && elapsed < 300) {
// ── Wire in progress: short tap adds waypoint ──
if (wireInProgressRef.current) {
if (isShortTap) {
const world = toWorld(changed.clientX, changed.clientY);
useSimulatorStore.getState().addWireWaypoint(world.x, world.y);
}
return;
}
// ── Short tap on empty canvas: wire selection + double-tap wire deletion ──
if (isShortTap) {
const now = Date.now();
const world = toWorld(changed.clientX, changed.clientY);
const threshold = 8 / zoomRef.current;
const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
// Double-tap → delete wire
const timeSinceLastTap = now - lastTapTimeRef.current;
if (timeSinceLastTap < 350 && wire) {
useSimulatorStore.getState().removeWire(wire.id);
lastTapTimeRef.current = 0;
return;
}
lastTapTimeRef.current = now;
if (wire) {
const curr = selectedWireIdRef.current;
useSimulatorStore.getState().setSelectedWire(curr === wire.id ? null : wire.id);
} else {
useSimulatorStore.getState().setSelectedWire(null);
setSelectedComponentId(null);
}
}
@ -1224,6 +1346,14 @@ export const SimulatorCanvas = () => {
{/* Components using wokwi-elements */}
<div className="components-area">{registryLoaded && components.map(renderComponent)}</div>
</div>
{/* Wire creation mode banner — visible on both desktop and mobile */}
{wireInProgress && (
<div className="wire-mode-banner">
<span>Tap a pin to connect tap canvas for waypoints</span>
<button onClick={() => cancelWireCreation()}>Cancel</button>
</div>
)}
</div>
</div>

View File

@ -0,0 +1,84 @@
/**
* SSR entry point for prerendering SEO pages at build time.
*
* Used by scripts/prerender-seo.mjs via Vite's ssrLoadModule.
* Renders each page component to an HTML string so the prerender script
* can inject it into the static dist/index.html per route.
*/
import React from 'react';
import { renderToString } from 'react-dom/server';
import { MemoryRouter } from 'react-router-dom';
import { SEO_ROUTES } from './seoRoutes';
// ── SEO page components ─────────────────────────────────────────────────────
import { LandingPage } from './pages/LandingPage';
import { ExamplesPage } from './pages/ExamplesPage';
import { ArduinoSimulatorPage } from './pages/ArduinoSimulatorPage';
import { ArduinoEmulatorPage } from './pages/ArduinoEmulatorPage';
import { AtmegaSimulatorPage } from './pages/AtmegaSimulatorPage';
import { ArduinoMegaSimulatorPage } from './pages/ArduinoMegaSimulatorPage';
import { Esp32SimulatorPage } from './pages/Esp32SimulatorPage';
import { Esp32S3SimulatorPage } from './pages/Esp32S3SimulatorPage';
import { Esp32C3SimulatorPage } from './pages/Esp32C3SimulatorPage';
import { RaspberryPiPicoSimulatorPage } from './pages/RaspberryPiPicoSimulatorPage';
import { RaspberryPiSimulatorPage } from './pages/RaspberryPiSimulatorPage';
import { Velxio2Page } from './pages/Velxio2Page';
import { DocsPage } from './pages/DocsPage';
// Map route paths to their React component
const ROUTE_COMPONENTS: Record<string, React.FC> = {
'/': LandingPage,
'/examples': ExamplesPage,
'/arduino-simulator': ArduinoSimulatorPage,
'/arduino-emulator': ArduinoEmulatorPage,
'/atmega328p-simulator': AtmegaSimulatorPage,
'/arduino-mega-simulator': ArduinoMegaSimulatorPage,
'/esp32-simulator': Esp32SimulatorPage,
'/esp32-s3-simulator': Esp32S3SimulatorPage,
'/esp32-c3-simulator': Esp32C3SimulatorPage,
'/raspberry-pi-pico-simulator': RaspberryPiPicoSimulatorPage,
'/raspberry-pi-simulator': RaspberryPiSimulatorPage,
'/v2': Velxio2Page,
// Docs sections — all use DocsPage with different URL params
'/docs': DocsPage,
'/docs/intro': DocsPage,
'/docs/getting-started': DocsPage,
'/docs/emulator': DocsPage,
'/docs/esp32-emulation': DocsPage,
'/docs/riscv-emulation': DocsPage,
'/docs/rp2040-emulation': DocsPage,
'/docs/raspberry-pi3-emulation': DocsPage,
'/docs/components': DocsPage,
'/docs/architecture': DocsPage,
'/docs/wokwi-libs': DocsPage,
'/docs/mcp': DocsPage,
'/docs/setup': DocsPage,
'/docs/roadmap': DocsPage,
};
/**
* Returns all routes that have both seoMeta and a renderable component.
*/
export function getPrerenderedRoutes() {
return SEO_ROUTES.filter(r => r.seoMeta && ROUTE_COMPONENTS[r.path]);
}
/**
* Render a route's page component to an HTML string.
*/
export function render(path: string): string {
const Component = ROUTE_COMPONENTS[path];
if (!Component) return '';
try {
return renderToString(
<MemoryRouter initialEntries={[path]}>
<Component />
</MemoryRouter>
);
} catch (err) {
console.warn(` ⚠ SSR render failed for ${path}:`, (err as Error).message);
return '';
}
}

View File

@ -7,8 +7,11 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import './SEOPage.css';
const META = getSeoMeta('/arduino-emulator')!;
const FAQ_ITEMS = [
{
q: 'What is an Arduino emulator?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const ArduinoEmulatorPage: React.FC = () => {
useSEO({
title: 'Arduino Emulator — Real AVR8 & RP2040 Emulation, Free | Velxio',
description:
'Free, open-source Arduino emulator with cycle-accurate AVR8 emulation at 16 MHz. Emulate Arduino Uno, Nano, Mega and Raspberry Pi Pico in your browser — no cloud, no install.',
url: 'https://velxio.dev/arduino-emulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,8 +7,11 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import './SEOPage.css';
const META = getSeoMeta('/arduino-mega-simulator')!;
const FAQ_ITEMS = [
{
q: 'What is the Arduino Mega 2560?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const ArduinoMegaSimulatorPage: React.FC = () => {
useSEO({
title: 'Arduino Mega 2560 Simulator — Free Online AVR8 Emulator | Velxio',
description:
'Simulate Arduino Mega 2560 (ATmega2560) code for free in your browser. 256 KB flash, 54 digital pins, 16 analog inputs, 4 serial ports — full AVR8 emulation with 48+ components.',
url: 'https://velxio.dev/arduino-mega-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,8 +7,11 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import './SEOPage.css';
const META = getSeoMeta('/arduino-simulator')!;
const FAQ_ITEMS = [
{
q: 'Is this Arduino simulator free?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const ArduinoSimulatorPage: React.FC = () => {
useSEO({
title: 'Free Online Arduino Simulator — Run Sketches in Your Browser | Velxio',
description:
'A free online Arduino simulator with real AVR8 emulation. Write and simulate Arduino code with LEDs, sensors, and 48+ components — no install, no account, instant results.',
url: 'https://velxio.dev/arduino-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,8 +7,11 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import './SEOPage.css';
const META = getSeoMeta('/atmega328p-simulator')!;
const FAQ_ITEMS = [
{
q: 'What is the ATmega328P?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const AtmegaSimulatorPage: React.FC = () => {
useSEO({
title: 'ATmega328P Simulator — Free Browser-Based AVR8 Emulation | Velxio',
description:
'Simulate ATmega328P code in your browser. Full AVR8 emulation at 16 MHz — PORTB, PORTC, PORTD, Timer0/1/2, ADC, USART — with 48+ interactive components. Free & open-source.',
url: 'https://velxio.dev/atmega328p-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,9 +7,12 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import esp32C3SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-c3-devkitm-1/board.svg?url';
import './SEOPage.css';
const META = getSeoMeta('/esp32-c3-simulator')!;
const FAQ_ITEMS = [
{
q: 'What is the ESP32-C3?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const Esp32C3SimulatorPage: React.FC = () => {
useSEO({
title: 'Free ESP32-C3 & RISC-V Simulator — Browser-Native Emulation | Velxio',
description:
'Simulate ESP32-C3 RISC-V code directly in your browser — no backend needed. RV32IMC at 160 MHz, 48+ components, Serial Monitor. Also supports CH32V003. Free and open-source.',
url: 'https://velxio.dev/esp32-c3-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,9 +7,12 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import esp32S3SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-s3-devkitc-1/board.svg?url';
import './SEOPage.css';
const META = getSeoMeta('/esp32-s3-simulator')!;
const FAQ_ITEMS = [
{
q: 'What is the ESP32-S3?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const Esp32S3SimulatorPage: React.FC = () => {
useSEO({
title: 'Free ESP32-S3 Simulator — Xtensa LX7 Emulation Online | Velxio',
description:
'Simulate ESP32-S3 code for free. Real Xtensa LX7 dual-core emulation at 240 MHz via QEMU — DevKitC, XIAO ESP32-S3, Arduino Nano ESP32. 48+ components, no install.',
url: 'https://velxio.dev/esp32-s3-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,9 +7,12 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import esp32SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-devkit-v1/board.svg?url';
import './SEOPage.css';
const META = getSeoMeta('/esp32-simulator')!;
const FAQ_ITEMS = [
{
q: 'Is this ESP32 simulator free?',
@ -67,13 +70,7 @@ const JSON_LD: object[] = [
];
export const Esp32SimulatorPage: React.FC = () => {
useSEO({
title: 'Free ESP32 Simulator Online — Xtensa LX6 Emulation | Velxio',
description:
'Simulate ESP32 code in your browser for free. Real Xtensa LX6 emulation at 240 MHz via QEMU — ESP32 DevKit, ESP32-S3, ESP32-CAM. 48+ components, Serial Monitor, no install.',
url: 'https://velxio.dev/esp32-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -9,6 +9,7 @@ import { useNavigate } from 'react-router-dom';
import { ExamplesGallery } from '../components/examples/ExamplesGallery';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import { useEditorStore } from '../store/useEditorStore';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { useVfsStore } from '../store/useVfsStore';
@ -18,12 +19,7 @@ import type { ExampleProject } from '../data/examples';
import type { BoardKind } from '../types/board';
export const ExamplesPage: React.FC = () => {
useSEO({
title: 'Arduino Simulator Examples — Run 18+ Sketches Instantly | Velxio',
description:
'Explore 18+ interactive Arduino examples with LEDs, sensors, displays, and games. Runs entirely in your browser — free, no install, no account required.',
url: 'https://velxio.dev/examples',
});
useSEO(getSeoMeta('/examples')!);
const navigate = useNavigate();
const { setCode } = useEditorStore();

View File

@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/useAuthStore';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import './LandingPage.css';
@ -436,10 +437,7 @@ const UserMenu: React.FC = () => {
/* ── Component ────────────────────────────────────────── */
export const LandingPage: React.FC = () => {
useSEO({
title: 'Velxio — Free Multi-Board Emulator | Arduino · ESP32 · RP2040 · RISC-V · Raspberry Pi',
description:
'Velxio is a free, open-source multi-board emulator. 19 boards across 5 CPU architectures: Arduino Uno/Mega/ATtiny (AVR8), ESP32/ESP32-S3 (Xtensa QEMU), ESP32-C3/CH32V003 (RISC-V), Raspberry Pi Pico (RP2040), Raspberry Pi 3 (Linux). 48+ components, no cloud.',
url: 'https://velxio.dev/',
...getSeoMeta('/')!,
jsonLd: {
'@context': 'https://schema.org',
'@type': 'FAQPage',

View File

@ -7,9 +7,12 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import piPicoSvgUrl from '../../../wokwi-libs/wokwi-boards/boards/pi-pico/board.svg?url';
import './SEOPage.css';
const META = getSeoMeta('/raspberry-pi-pico-simulator')!;
const FAQ_ITEMS = [
{
q: 'Is this Raspberry Pi Pico simulator free?',
@ -37,12 +40,11 @@ const JSON_LD: object[] = [
{
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Velxio — Free Raspberry Pi Pico & RP2040 Simulator',
name: META.title.split(' | ')[0],
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any (browser-based)',
description:
'Free online Raspberry Pi Pico simulator with real RP2040 ARM Cortex-M0+ emulation at 133 MHz. Simulate Arduino code for Pico and Pico W with 48+ components — no hardware needed.',
url: 'https://velxio.dev/raspberry-pi-pico-simulator',
description: META.description,
url: META.url,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
author: { '@type': 'Person', name: 'David Montero Crespo' },
},
@ -60,19 +62,13 @@ const JSON_LD: object[] = [
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Velxio', item: 'https://velxio.dev/' },
{ '@type': 'ListItem', position: 2, name: 'Raspberry Pi Pico Simulator', item: 'https://velxio.dev/raspberry-pi-pico-simulator' },
{ '@type': 'ListItem', position: 2, name: 'Raspberry Pi Pico Simulator', item: META.url },
],
},
];
export const RaspberryPiPicoSimulatorPage: React.FC = () => {
useSEO({
title: 'Free Raspberry Pi Pico Simulator — RP2040 ARM Cortex-M0+ Emulation | Velxio',
description:
'Simulate Raspberry Pi Pico and Pico W code for free. Real RP2040 ARM Cortex-M0+ emulation at 133 MHz via rp2040js. 48+ components, Serial Monitor, Arduino-Pico core. No install.',
url: 'https://velxio.dev/raspberry-pi-pico-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,9 +7,12 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import './SEOPage.css';
const META = getSeoMeta('/raspberry-pi-simulator')!;
const FAQ_ITEMS = [
{
q: 'Can I simulate a Raspberry Pi 3 in my browser?',
@ -66,13 +69,7 @@ const JSON_LD: object[] = [
];
export const RaspberryPiSimulatorPage: React.FC = () => {
useSEO({
title: 'Free Raspberry Pi 3 Simulator — Full Linux Emulation in Your Browser | Velxio',
description:
'Simulate Raspberry Pi 3 for free. Full ARM Cortex-A53 Linux emulation via QEMU — run Python, bash, RPi.GPIO in your browser. No Raspberry Pi hardware needed.',
url: 'https://velxio.dev/raspberry-pi-simulator',
jsonLd: JSON_LD,
});
useSEO({ ...META, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -7,6 +7,7 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import './SEOPage.css';
import './Velxio2Page.css';
@ -243,13 +244,7 @@ const CHANGE_SECTIONS = [
];
export const Velxio2Page: React.FC = () => {
useSEO({
title: 'Velxio 2.0 — Multi-Board Embedded Simulator | ESP32, Raspberry Pi, Arduino, RISC-V',
description:
'Velxio 2.0 is here. Simulate Arduino, ESP32, Raspberry Pi Pico, and Raspberry Pi 3 in your browser. 19 boards, 68+ examples, realistic sensor simulation. Free and open-source.',
url: 'https://velxio.dev/v2',
jsonLd: JSON_LD,
});
useSEO({ ...getSeoMeta('/v2')!, jsonLd: JSON_LD });
return (
<div className="seo-page">

View File

@ -1,12 +1,22 @@
/**
* Single source of truth for all public, indexable routes.
* Single source of truth for all public, indexable routes and their SEO metadata.
* Used by:
* 1. scripts/generate-sitemap.ts builds sitemap.xml at build time
* 2. Any component that needs the canonical URL list
* 1. scripts/generate-sitemap.mjs builds sitemap.xml at build time
* 2. scripts/prerender-seo.mjs generates prerendered HTML per route
* 3. Page components (via getSeoMeta) useSEO() hook
*
* Routes with `noindex: true` are excluded from the sitemap.
* Routes with `seoMeta` get prerendered HTML at build time.
*/
const DOMAIN = 'https://velxio.dev';
export interface SeoMeta {
title: string;
description: string;
url: string;
}
export interface SeoRoute {
path: string;
/** 0.0 1.0 (default 0.5) */
@ -14,46 +24,175 @@ export interface SeoRoute {
changefreq?: 'daily' | 'weekly' | 'monthly' | 'yearly';
/** If true, excluded from sitemap */
noindex?: boolean;
/** SEO metadata — if present, this route gets a prerendered HTML page at build time. */
seoMeta?: SeoMeta;
}
/** Look up the SEO metadata for a given path. */
export function getSeoMeta(path: string): SeoMeta | undefined {
return SEO_ROUTES.find(r => r.path === path)?.seoMeta;
}
export const SEO_ROUTES: SeoRoute[] = [
// ── Main pages
{ path: '/', priority: 1.0, changefreq: 'weekly' },
{ path: '/editor', priority: 0.9, changefreq: 'weekly' },
{ path: '/examples', priority: 0.8, changefreq: 'weekly' },
{
path: '/',
priority: 1.0,
changefreq: 'weekly',
seoMeta: {
title: 'Velxio — Free Multi-Board Emulator | Arduino · ESP32 · RP2040 · RISC-V · Raspberry Pi',
description: 'Velxio is a free, open-source multi-board emulator. 19 boards across 5 CPU architectures: Arduino Uno/Mega/ATtiny (AVR8), ESP32/ESP32-S3 (Xtensa QEMU), ESP32-C3/CH32V003 (RISC-V), Raspberry Pi Pico (RP2040), Raspberry Pi 3 (Linux). 48+ components, no cloud.',
url: `${DOMAIN}/`,
},
},
{ path: '/editor', priority: 0.9, changefreq: 'weekly' },
{
path: '/examples',
priority: 0.8,
changefreq: 'weekly',
seoMeta: {
title: 'Arduino Simulator Examples — Run 18+ Sketches Instantly | Velxio',
description: 'Explore 18+ interactive Arduino examples with LEDs, sensors, displays, and games. Runs entirely in your browser — free, no install, no account required.',
url: `${DOMAIN}/examples`,
},
},
// ── Documentation
{ path: '/docs', priority: 0.8, changefreq: 'monthly' },
{ path: '/docs/intro', priority: 0.8, changefreq: 'monthly' },
{ path: '/docs/getting-started', priority: 0.8, changefreq: 'monthly' },
{ path: '/docs/emulator', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/esp32-emulation', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/riscv-emulation', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/rp2040-emulation', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/raspberry-pi3-emulation', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/components', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/architecture', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/wokwi-libs', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/mcp', priority: 0.7, changefreq: 'monthly' },
{ path: '/docs/setup', priority: 0.6, changefreq: 'monthly' },
{ path: '/docs/roadmap', priority: 0.6, changefreq: 'monthly' },
{ path: '/docs', priority: 0.8, changefreq: 'monthly',
seoMeta: { title: 'Introduction | Velxio Documentation', description: 'Learn about Velxio, the free open-source Arduino emulator with real AVR8 and RP2040 CPU emulation and 48+ interactive electronic components.', url: `${DOMAIN}/docs` } },
{ path: '/docs/intro', priority: 0.8, changefreq: 'monthly',
seoMeta: { title: 'Introduction | Velxio Documentation', description: 'Learn about Velxio, the free open-source Arduino emulator with real AVR8 and RP2040 CPU emulation and 48+ interactive electronic components.', url: `${DOMAIN}/docs/intro` } },
{ path: '/docs/getting-started', priority: 0.8, changefreq: 'monthly',
seoMeta: { title: 'Getting Started | Velxio Documentation', description: 'Get started with Velxio: use the hosted editor, self-host with Docker, or set up a local development environment. Simulate your first Arduino sketch in minutes.', url: `${DOMAIN}/docs/getting-started` } },
{ path: '/docs/emulator', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'Emulator Architecture | Velxio Documentation', description: 'How Velxio emulates AVR8 (ATmega328p), RP2040, and RISC-V (ESP32-C3) CPUs. Covers execution loops, peripherals, and pin mapping for all supported boards.', url: `${DOMAIN}/docs/emulator` } },
{ path: '/docs/esp32-emulation', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'ESP32 Emulation (Xtensa) | Velxio Documentation', description: 'QEMU-based emulation for ESP32 and ESP32-S3 (Xtensa LX6/LX7). Covers the lcgamboa fork, libqemu-xtensa, GPIO, WiFi, I2C, SPI, RMT/NeoPixel, and LEDC/PWM.', url: `${DOMAIN}/docs/esp32-emulation` } },
{ path: '/docs/riscv-emulation', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'RISC-V Emulation (ESP32-C3) | Velxio Documentation', description: 'Browser-side RV32IMC emulator for ESP32-C3, XIAO ESP32-C3, and C3 SuperMini. Covers memory map, GPIO, UART0, the ESP32 image parser, RV32IMC ISA, and test suite.', url: `${DOMAIN}/docs/riscv-emulation` } },
{ path: '/docs/rp2040-emulation', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'RP2040 Emulation (Raspberry Pi Pico) | Velxio Documentation', description: 'How Velxio emulates the Raspberry Pi Pico and Pico W using rp2040js: ARM Cortex-M0+ at 133 MHz, GPIO, UART, ADC, I2C, SPI, PWM and WFI optimization.', url: `${DOMAIN}/docs/rp2040-emulation` } },
{ path: '/docs/raspberry-pi3-emulation', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'Raspberry Pi 3 Emulation (QEMU) | Velxio Documentation', description: 'How Velxio emulates a full Raspberry Pi 3B using QEMU raspi3b: real Raspberry Pi OS, Python + RPi.GPIO shim, dual-channel UART, VFS, and multi-board serial bridge.', url: `${DOMAIN}/docs/raspberry-pi3-emulation` } },
{ path: '/docs/components', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'Components Reference | Velxio Documentation', description: 'Full reference for all 48+ interactive electronic components in Velxio: LEDs, displays, sensors, buttons, potentiometers, and more. Includes wiring and property details.', url: `${DOMAIN}/docs/components` } },
{ path: '/docs/architecture', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'Project Architecture | Velxio Documentation', description: 'Detailed overview of the Velxio system architecture: frontend, backend, AVR8 emulation pipeline, data flows, Zustand stores, and wire system.', url: `${DOMAIN}/docs/architecture` } },
{ path: '/docs/wokwi-libs', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'Wokwi Libraries | Velxio Documentation', description: 'How Velxio integrates the official Wokwi open-source libraries: avr8js, wokwi-elements, and rp2040js. Covers configuration, updates, and the 48 available components.', url: `${DOMAIN}/docs/wokwi-libs` } },
{ path: '/docs/mcp', priority: 0.7, changefreq: 'monthly',
seoMeta: { title: 'MCP Server | Velxio Documentation', description: 'Velxio MCP Server reference: integrate AI agents (Claude, Cursor) with Velxio via Model Context Protocol. Covers tools, transports, circuit format, and example walkthroughs.', url: `${DOMAIN}/docs/mcp` } },
{ path: '/docs/setup', priority: 0.6, changefreq: 'monthly',
seoMeta: { title: 'Project Status | Velxio Documentation', description: 'Complete status of all implemented Velxio features: AVR emulation, component system, wire system, code editor, example projects, and next steps.', url: `${DOMAIN}/docs/setup` } },
{ path: '/docs/roadmap', priority: 0.6, changefreq: 'monthly',
seoMeta: { title: 'Roadmap | Velxio Documentation', description: "Velxio's feature roadmap: what's implemented, what's in progress, and what's planned for future releases.", url: `${DOMAIN}/docs/roadmap` } },
// ── SEO keyword landing pages
{ path: '/arduino-simulator', priority: 0.9, changefreq: 'monthly' },
{ path: '/arduino-emulator', priority: 0.9, changefreq: 'monthly' },
{ path: '/atmega328p-simulator', priority: 0.85, changefreq: 'monthly' },
{ path: '/arduino-mega-simulator', priority: 0.85, changefreq: 'monthly' },
{ path: '/esp32-simulator', priority: 0.9, changefreq: 'monthly' },
{ path: '/esp32-s3-simulator', priority: 0.85, changefreq: 'monthly' },
{ path: '/esp32-c3-simulator', priority: 0.85, changefreq: 'monthly' },
{ path: '/raspberry-pi-pico-simulator', priority: 0.9, changefreq: 'monthly' },
{ path: '/raspberry-pi-simulator', priority: 0.85, changefreq: 'monthly' },
{
path: '/arduino-simulator',
priority: 0.9,
changefreq: 'monthly',
seoMeta: {
title: 'Free Online Arduino Simulator — Run Sketches in Your Browser | Velxio',
description: 'A free online Arduino simulator with real AVR8 emulation. Write and simulate Arduino code with LEDs, sensors, and 48+ components — no install, no account, instant results.',
url: `${DOMAIN}/arduino-simulator`,
},
},
{
path: '/arduino-emulator',
priority: 0.9,
changefreq: 'monthly',
seoMeta: {
title: 'Arduino Emulator — Real AVR8 & RP2040 Emulation, Free | Velxio',
description: 'Free, open-source Arduino emulator with cycle-accurate AVR8 emulation at 16 MHz. Emulate Arduino Uno, Nano, Mega and Raspberry Pi Pico in your browser — no cloud, no install.',
url: `${DOMAIN}/arduino-emulator`,
},
},
{
path: '/atmega328p-simulator',
priority: 0.85,
changefreq: 'monthly',
seoMeta: {
title: 'ATmega328P Simulator — Free Browser-Based AVR8 Emulation | Velxio',
description: 'Simulate ATmega328P code in your browser. Full AVR8 emulation at 16 MHz — PORTB, PORTC, PORTD, Timer0/1/2, ADC, USART — with 48+ interactive components. Free & open-source.',
url: `${DOMAIN}/atmega328p-simulator`,
},
},
{
path: '/arduino-mega-simulator',
priority: 0.85,
changefreq: 'monthly',
seoMeta: {
title: 'Arduino Mega 2560 Simulator — Free Online AVR8 Emulator | Velxio',
description: 'Simulate Arduino Mega 2560 (ATmega2560) code for free in your browser. 256 KB flash, 54 digital pins, 16 analog inputs, 4 serial ports — full AVR8 emulation with 48+ components.',
url: `${DOMAIN}/arduino-mega-simulator`,
},
},
{
path: '/esp32-simulator',
priority: 0.9,
changefreq: 'monthly',
seoMeta: {
title: 'Free ESP32 Simulator Online — Xtensa LX6 Emulation | Velxio',
description: 'Simulate ESP32 code in your browser for free. Real Xtensa LX6 emulation at 240 MHz via QEMU — ESP32 DevKit, ESP32-S3, ESP32-CAM. 48+ components, Serial Monitor, no install.',
url: `${DOMAIN}/esp32-simulator`,
},
},
{
path: '/esp32-s3-simulator',
priority: 0.85,
changefreq: 'monthly',
seoMeta: {
title: 'Free ESP32-S3 Simulator — Xtensa LX7 Emulation Online | Velxio',
description: 'Simulate ESP32-S3 code for free. Real Xtensa LX7 dual-core emulation at 240 MHz via QEMU — DevKitC, XIAO ESP32-S3, Arduino Nano ESP32. 48+ components, no install.',
url: `${DOMAIN}/esp32-s3-simulator`,
},
},
{
path: '/esp32-c3-simulator',
priority: 0.85,
changefreq: 'monthly',
seoMeta: {
title: 'Free ESP32-C3 & RISC-V Simulator — Browser-Native Emulation | Velxio',
description: 'Simulate ESP32-C3 RISC-V code directly in your browser — no backend needed. RV32IMC at 160 MHz, 48+ components, Serial Monitor. Also supports CH32V003. Free and open-source.',
url: `${DOMAIN}/esp32-c3-simulator`,
},
},
{
path: '/raspberry-pi-pico-simulator',
priority: 0.9,
changefreq: 'monthly',
seoMeta: {
title: 'Free Raspberry Pi Pico Simulator — RP2040 ARM Cortex-M0+ Emulation | Velxio',
description: 'Simulate Raspberry Pi Pico and Pico W code for free. Real RP2040 ARM Cortex-M0+ emulation at 133 MHz via rp2040js. 48+ components, Serial Monitor, Arduino-Pico core. No install.',
url: `${DOMAIN}/raspberry-pi-pico-simulator`,
},
},
{
path: '/raspberry-pi-simulator',
priority: 0.85,
changefreq: 'monthly',
seoMeta: {
title: 'Free Raspberry Pi 3 Simulator — Full Linux Emulation in Your Browser | Velxio',
description: 'Simulate Raspberry Pi 3 for free. Full ARM Cortex-A53 Linux emulation via QEMU — run Python, bash, RPi.GPIO in your browser. No Raspberry Pi hardware needed.',
url: `${DOMAIN}/raspberry-pi-simulator`,
},
},
// ── Release pages
{ path: '/v2', priority: 0.9, changefreq: 'monthly' },
{
path: '/v2',
priority: 0.9,
changefreq: 'monthly',
seoMeta: {
title: 'Velxio 2.0 — Multi-Board Embedded Simulator | ESP32, Raspberry Pi, Arduino, RISC-V',
description: 'Velxio 2.0 is here. Simulate Arduino, ESP32, Raspberry Pi Pico, and Raspberry Pi 3 in your browser. 19 boards, 68+ examples, realistic sensor simulation. Free and open-source.',
url: `${DOMAIN}/v2`,
},
},
// ── Auth / admin (noindex)
{ path: '/login', noindex: true },
{ path: '/register', noindex: true },
{ path: '/admin', noindex: true },
{ path: '/login', noindex: true },
{ path: '/register', noindex: true },
{ path: '/admin', noindex: true },
];