feat: enhance build scripts and add SEO prerendering; improve touch interactions for wire creation
parent
effd1ee765
commit
005f50262f
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue