(null);
const resizingRef = useRef(false);
diff --git a/frontend/src/pages/ExamplesPage.tsx b/frontend/src/pages/ExamplesPage.tsx
index 4301aec..474aaf1 100644
--- a/frontend/src/pages/ExamplesPage.tsx
+++ b/frontend/src/pages/ExamplesPage.tsx
@@ -8,6 +8,7 @@ import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ExamplesGallery } from '../components/examples/ExamplesGallery';
import { AppHeader } from '../components/layout/AppHeader';
+import { useSEO } from '../utils/useSEO';
import { useEditorStore } from '../store/useEditorStore';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { useVfsStore } from '../store/useVfsStore';
@@ -16,6 +17,13 @@ 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',
+ });
+
const navigate = useNavigate();
const { setCode } = useEditorStore();
const { setComponents, setWires, setBoardType, activeBoardId, boards, addBoard, removeBoard, setActiveBoardId } = useSimulatorStore();
diff --git a/frontend/src/pages/LandingPage.css b/frontend/src/pages/LandingPage.css
index 48bd0df..c602fb0 100644
--- a/frontend/src/pages/LandingPage.css
+++ b/frontend/src/pages/LandingPage.css
@@ -351,6 +351,15 @@
transform: scale(1.02);
}
+/* Trust line under CTA buttons */
+.hero-trust-line {
+ margin: 6px 0 0;
+ font-size: 11.5px;
+ font-weight: 400;
+ color: #48484a;
+ letter-spacing: 0.02em;
+}
+
/* Specs strip */
.hero-specs {
margin: 0;
diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx
index 622cbc2..1c8cd00 100644
--- a/frontend/src/pages/LandingPage.tsx
+++ b/frontend/src/pages/LandingPage.tsx
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/useAuthStore';
import { AppHeader } from '../components/layout/AppHeader';
+import { useSEO } from '../utils/useSEO';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import './LandingPage.css';
@@ -434,6 +435,13 @@ const UserMenu: React.FC = () => {
/* ── Component ────────────────────────────────────────── */
export const LandingPage: React.FC = () => {
+ useSEO({
+ title: 'Velxio — Free Local Arduino Emulator | AVR8 · RP2040 · 48+ Components',
+ description:
+ 'Velxio is a free, open-source Arduino emulator that runs entirely in your browser. Real AVR8 emulation at 16 MHz, 48+ electronic components, Monaco Editor, Serial Monitor, and Library Manager — no cloud, no latency.',
+ url: 'https://velxio.dev/',
+ });
+
return (
@@ -451,13 +459,14 @@ export const LandingPage: React.FC = () => {
+
No signup required · Runs 100% in your browser · Free & open-source
diff --git a/frontend/src/pages/SEOPage.css b/frontend/src/pages/SEOPage.css
new file mode 100644
index 0000000..3be2088
--- /dev/null
+++ b/frontend/src/pages/SEOPage.css
@@ -0,0 +1,231 @@
+/* ── SEOPage.css — Shared layout for keyword-targeted landing pages ── */
+
+.seo-page {
+ min-height: 100vh;
+ background: #0d0d0f;
+ color: #e6edf3;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* ── Hero ─────────────────────────────────────────────── */
+.seo-hero {
+ max-width: 820px;
+ margin: 0 auto;
+ padding: 72px 2rem 56px;
+ text-align: center;
+}
+
+.seo-hero h1 {
+ font-size: clamp(1.75rem, 4.5vw, 2.75rem);
+ font-weight: 700;
+ line-height: 1.15;
+ color: #ffffff;
+ margin: 0 0 1rem;
+ letter-spacing: -0.5px;
+}
+
+.seo-hero h1 .accent {
+ color: #007acc;
+}
+
+.seo-hero .subtitle {
+ font-size: 1.1rem;
+ color: #8b949e;
+ max-width: 600px;
+ margin: 0 auto 2rem;
+ line-height: 1.65;
+}
+
+.seo-cta-group {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+/* Reuse brand CTA style consistent with LandingPage */
+.seo-btn-primary {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 12px 26px;
+ background: #007acc;
+ color: #fff;
+ text-decoration: none;
+ font-size: 15px;
+ font-weight: 500;
+ border-radius: 9999px;
+ transition: filter 0.2s, transform 0.2s;
+ border: none;
+ cursor: pointer;
+ letter-spacing: -0.1px;
+}
+
+.seo-btn-primary:hover {
+ filter: brightness(1.14);
+ transform: scale(1.02);
+}
+
+.seo-btn-secondary {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 12px 26px;
+ background: #2c2c2e;
+ color: #e6edf3;
+ text-decoration: none;
+ font-size: 15px;
+ font-weight: 500;
+ border-radius: 9999px;
+ transition: background 0.2s, transform 0.2s;
+ border: none;
+ cursor: pointer;
+}
+
+.seo-btn-secondary:hover {
+ background: #3a3a3c;
+ transform: scale(1.02);
+}
+
+.seo-trust {
+ font-size: 0.78rem;
+ color: #484848;
+ margin-top: 1rem;
+ letter-spacing: 0.02em;
+}
+
+/* ── Section wrapper ──────────────────────────────────── */
+.seo-section {
+ max-width: 820px;
+ margin: 0 auto;
+ padding: 48px 2rem;
+ border-top: 1px solid #1c1c1e;
+}
+
+.seo-section h2 {
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: #fff;
+ margin: 0 0 0.5rem;
+ letter-spacing: -0.3px;
+}
+
+.seo-section .lead {
+ color: #8b949e;
+ font-size: 0.95rem;
+ margin: 0 0 1.75rem;
+ line-height: 1.6;
+}
+
+/* ── Feature grid ─────────────────────────────────────── */
+.seo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
+ gap: 12px;
+}
+
+.seo-card {
+ background: #121214;
+ border: 1px solid #1c1c1e;
+ border-radius: 10px;
+ padding: 1.2rem 1.25rem;
+}
+
+.seo-card h3 {
+ font-size: 0.88rem;
+ font-weight: 600;
+ color: #007acc;
+ margin: 0 0 0.35rem;
+ letter-spacing: -0.1px;
+}
+
+.seo-card p {
+ font-size: 0.83rem;
+ color: #8b949e;
+ margin: 0;
+ line-height: 1.55;
+}
+
+/* ── FAQ ──────────────────────────────────────────────── */
+.seo-faq {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.seo-faq dt {
+ font-weight: 600;
+ color: #e6edf3;
+ font-size: 0.9rem;
+ margin: 1.2rem 0 0.25rem;
+}
+
+.seo-faq dd {
+ margin: 0;
+ color: #8b949e;
+ font-size: 0.875rem;
+ line-height: 1.6;
+ padding-left: 1rem;
+ border-left: 2px solid #1c1c1e;
+}
+
+/* ── Bottom CTA ───────────────────────────────────────── */
+.seo-bottom {
+ text-align: center;
+ padding: 64px 2rem 72px;
+ border-top: 1px solid #1c1c1e;
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.seo-bottom h2 {
+ font-size: 1.55rem;
+ font-weight: 700;
+ color: #fff;
+ margin: 0 0 0.75rem;
+ letter-spacing: -0.3px;
+}
+
+.seo-bottom p {
+ color: #8b949e;
+ font-size: 0.95rem;
+ margin: 0 0 2rem;
+ line-height: 1.6;
+}
+
+/* ── Internal links ───────────────────────────────────── */
+.seo-internal-links {
+ display: flex;
+ gap: 1.25rem;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-top: 2rem;
+ font-size: 0.82rem;
+}
+
+.seo-internal-links a {
+ color: #007acc;
+ text-decoration: none;
+ transition: color 0.15s;
+}
+
+.seo-internal-links a:hover {
+ color: #3b9ae8;
+ text-decoration: underline;
+}
+
+/* ── Responsive ───────────────────────────────────────── */
+@media (max-width: 600px) {
+ .seo-hero {
+ padding: 48px 1.25rem 40px;
+ }
+
+ .seo-section {
+ padding: 36px 1.25rem;
+ }
+
+ .seo-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/frontend/src/utils/useSEO.ts b/frontend/src/utils/useSEO.ts
new file mode 100644
index 0000000..8492e2f
--- /dev/null
+++ b/frontend/src/utils/useSEO.ts
@@ -0,0 +1,88 @@
+import { useEffect, useRef } from 'react';
+
+export interface SEOMeta {
+ title: string;
+ description: string;
+ url: string;
+ ogImage?: string;
+ /** Module-level constant: injected once on mount, removed on unmount. */
+ jsonLd?: object | object[];
+}
+
+function qs(selector: string): HTMLMetaElement | null {
+ return document.querySelector(selector) as HTMLMetaElement | null;
+}
+
+/**
+ * Updates document.title, meta description, OG/Twitter tags, and canonical
+ * to reflect the current page. Restores originals on unmount.
+ *
+ * jsonLd (if provided) is injected as a