diff --git a/.gitignore b/.gitignore
index bec3335..6ecd25f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,3 +94,6 @@ test/esp32-emulator/**/*.elf
test/esp32-emulator/**/*.map
test/esp32-emulator/out_*/
.claude/settings.json
+
+# Google Cloud service account credentials
+velxio-ba3355a41944.json
diff --git a/frontend/index.html b/frontend/index.html
index 360e559..fee8a5c 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -236,15 +236,6 @@
ILI9341 TFT display simulation
I2C, SPI, USART, ADC, PWM support
Multi-board canvas: mix Arduino + ESP32 + Raspberry Pi in one simulation
-
- arduino-cli compilation backend — produces real .hex / .uf2 files
- Serial Monitor with auto baud-rate detection and send
- Library Manager for Arduino libraries
- Multi-file workspace (.ino, .h, .cpp)
- Wire system with orthogonal routing
- ILI9341 TFT display simulation
- I2C, SPI, USART, ADC, PWM support
- Docker standalone image — deploy anywhere with one command
Supported Boards
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
index 25208a3..1ee8500 100644
--- a/frontend/public/robots.txt
+++ b/frontend/public/robots.txt
@@ -1,5 +1,16 @@
User-agent: *
Allow: /
+# Auth pages — no useful content for search engines
+Disallow: /login
+Disallow: /register
+
+# Dynamic user/project pages — noindex is also set via meta tag
+Disallow: /project/
+Disallow: /admin
+
+# Prevent crawling of API endpoints
+Disallow: /api/
+
# Sitemap
Sitemap: https://velxio.dev/sitemap.xml
diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml
index 6b6229f..443a154 100644
--- a/frontend/public/sitemap.xml
+++ b/frontend/public/sitemap.xml
@@ -2,105 +2,128 @@
+
https://velxio.dev/
- 2026-03-06
+ 2026-03-23
weekly
1.0
https://velxio.dev/editor
- 2026-03-06
+ 2026-03-23
weekly
0.9
+
+ https://velxio.dev/examples
+ 2026-03-23
+ weekly
+ 0.8
+
+
+
+
+ https://velxio.dev/docs
+ 2026-03-23
+ monthly
+ 0.8
+
+
https://velxio.dev/docs/intro
- 2026-03-11
+ 2026-03-23
monthly
0.8
https://velxio.dev/docs/getting-started
- 2026-03-11
+ 2026-03-23
monthly
0.8
https://velxio.dev/docs/emulator
- 2026-03-11
- monthly
- 0.7
-
-
-
- https://velxio.dev/docs/riscv-emulation
- 2026-03-15
+ 2026-03-23
monthly
0.7
https://velxio.dev/docs/esp32-emulation
- 2026-03-15
+ 2026-03-23
+ monthly
+ 0.7
+
+
+
+ https://velxio.dev/docs/riscv-emulation
+ 2026-03-23
+ monthly
+ 0.7
+
+
+
+ https://velxio.dev/docs/rp2040-emulation
+ 2026-03-23
+ monthly
+ 0.7
+
+
+
+ https://velxio.dev/docs/raspberry-pi3-emulation
+ 2026-03-23
monthly
0.7
https://velxio.dev/docs/components
- 2026-03-11
+ 2026-03-23
monthly
0.7
-
- https://velxio.dev/docs/roadmap
- 2026-03-11
- monthly
- 0.6
-
-
https://velxio.dev/docs/architecture
- 2026-03-11
+ 2026-03-23
monthly
0.7
https://velxio.dev/docs/wokwi-libs
- 2026-03-11
+ 2026-03-23
monthly
0.7
https://velxio.dev/docs/mcp
- 2026-03-11
+ 2026-03-23
monthly
0.7
https://velxio.dev/docs/setup
- 2026-03-11
+ 2026-03-23
monthly
0.6
- https://velxio.dev/examples
+ https://velxio.dev/docs/roadmap
2026-03-23
monthly
- 0.8
+ 0.6
-
+
https://velxio.dev/arduino-simulator
2026-03-23
@@ -129,6 +152,6 @@
0.85
-
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 13d76fa..1788313 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -283,32 +283,36 @@ body {
.mobile-tab-bar {
display: flex;
flex-shrink: 0;
- height: 54px;
- background: #252526;
- border-top: 1px solid #007acc;
+ height: 44px;
+ background: #1e1e1e;
+ border-bottom: 2px solid #007acc;
z-index: 50;
+ /* Sits between header and panels */
}
.mobile-tab-btn {
flex: 1;
display: flex;
- flex-direction: column;
+ flex-direction: row;
align-items: center;
justify-content: center;
- gap: 3px;
+ gap: 6px;
background: transparent;
border: none;
+ border-bottom: 2px solid transparent;
color: #7a7a7a;
- font-size: 11px;
- font-weight: 500;
+ font-size: 13px;
+ font-weight: 600;
cursor: pointer;
- transition: color 0.15s, background 0.15s;
- padding: 6px 0;
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
+ padding: 8px 0;
+ margin-bottom: -2px;
}
.mobile-tab-btn--active {
color: #007acc;
- background: rgba(0, 122, 204, 0.08);
+ border-bottom-color: #007acc;
+ background: rgba(0, 122, 204, 0.1);
}
.mobile-tab-btn:active {
diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx
index 7f08822..f572192 100644
--- a/frontend/src/pages/AdminPage.tsx
+++ b/frontend/src/pages/AdminPage.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/useAuthStore';
+import { useSEO } from '../utils/useSEO';
import {
getAdminSetupStatus,
createFirstAdmin,
@@ -517,6 +518,13 @@ function AdminDashboard() {
type AdminPageState = 'loading' | 'setup' | 'not-admin' | 'dashboard';
export const AdminPage: React.FC = () => {
+ useSEO({
+ title: 'Admin — Velxio',
+ description: 'Velxio administration panel.',
+ url: 'https://velxio.dev/admin',
+ noindex: true,
+ });
+
const user = useAuthStore((s) => s.user);
const [pageState, setPageState] = useState('loading');
diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx
index 99d68b1..7543bc4 100644
--- a/frontend/src/pages/EditorPage.tsx
+++ b/frontend/src/pages/EditorPage.tsx
@@ -112,8 +112,8 @@ export const EditorPage: React.FC = () => {
const [explorerOpen, setExplorerOpen] = useState(true);
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
- // Default to 'circuit' on mobile — the visual simulation is the primary content
- const [mobileView, setMobileView] = useState<'code' | 'circuit'>('circuit');
+ // Default to 'code' on mobile — show the editor so users can write/view code
+ const [mobileView, setMobileView] = useState<'code' | 'circuit'>('code');
const user = useAuthStore((s) => s.user);
const handleSaveClick = useCallback(() => {
@@ -233,6 +233,34 @@ export const EditorPage: React.FC = () => {
+ {/* ── Mobile tab bar (top, above panels) ── */}
+ {isMobile && (
+
+ )}
+
{/* ── Editor side ── */}
{
- {/* ── Mobile tab bar ── */}
- {isMobile && (
-
- )}
-
{saveModalOpen &&
setSaveModalOpen(false)} />}
{loginPromptOpen && setLoginPromptOpen(false)} />}
{showStarBanner && }
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx
index 2b35d5b..2a7a5d4 100644
--- a/frontend/src/pages/LoginPage.tsx
+++ b/frontend/src/pages/LoginPage.tsx
@@ -2,8 +2,15 @@ import { useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { login, initiateGoogleLogin } from '../services/authService';
import { useAuthStore } from '../store/useAuthStore';
+import { useSEO } from '../utils/useSEO';
export const LoginPage: React.FC = () => {
+ useSEO({
+ title: 'Sign In — Velxio',
+ description: 'Sign in to your Velxio account to save projects and access your Arduino simulations.',
+ url: 'https://velxio.dev/login',
+ noindex: true,
+ });
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const setUser = useAuthStore((s) => s.setUser);
diff --git a/frontend/src/pages/ProjectByIdPage.tsx b/frontend/src/pages/ProjectByIdPage.tsx
index c5a7f15..5c32cc4 100644
--- a/frontend/src/pages/ProjectByIdPage.tsx
+++ b/frontend/src/pages/ProjectByIdPage.tsx
@@ -4,9 +4,16 @@ import { getProjectById } from '../services/projectService';
import { useEditorStore } from '../store/useEditorStore';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { useProjectStore } from '../store/useProjectStore';
+import { useSEO } from '../utils/useSEO';
import { EditorPage } from './EditorPage';
export const ProjectByIdPage: React.FC = () => {
+ useSEO({
+ title: 'Project — Velxio Arduino Emulator',
+ description: 'View and simulate this Arduino project on Velxio — free, open-source multi-board emulator.',
+ url: 'https://velxio.dev/editor',
+ noindex: true,
+ });
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const loadFiles = useEditorStore((s) => s.loadFiles);
diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx
index 05d861e..906143f 100644
--- a/frontend/src/pages/RegisterPage.tsx
+++ b/frontend/src/pages/RegisterPage.tsx
@@ -3,8 +3,15 @@ import { Link, useNavigate } from 'react-router-dom';
import { register, initiateGoogleLogin } from '../services/authService';
import { useAuthStore } from '../store/useAuthStore';
import { RESERVED_USERNAMES } from '../utils/reservedUsernames';
+import { useSEO } from '../utils/useSEO';
export const RegisterPage: React.FC = () => {
+ useSEO({
+ title: 'Create Account — Velxio',
+ description: 'Create a free Velxio account to save your Arduino projects and share simulations.',
+ url: 'https://velxio.dev/register',
+ noindex: true,
+ });
const navigate = useNavigate();
const setUser = useAuthStore((s) => s.setUser);
const [username, setUsername] = useState('');
diff --git a/frontend/src/pages/UserProfilePage.tsx b/frontend/src/pages/UserProfilePage.tsx
index 0fc3c16..b77bb0f 100644
--- a/frontend/src/pages/UserProfilePage.tsx
+++ b/frontend/src/pages/UserProfilePage.tsx
@@ -3,10 +3,18 @@ import { Link, useParams } from 'react-router-dom';
import { getUserProjects, type ProjectResponse } from '../services/projectService';
import { useAuthStore } from '../store/useAuthStore';
import { AppHeader } from '../components/layout/AppHeader';
+import { useSEO } from '../utils/useSEO';
import './UserProfilePage.css';
export const UserProfilePage: React.FC = () => {
const { username } = useParams<{ username: string }>();
+
+ useSEO({
+ title: `${username ?? 'User'} — Velxio Profile`,
+ description: `View Arduino and ESP32 projects by ${username ?? 'this user'} on Velxio.`,
+ url: `https://velxio.dev/${username ?? ''}`,
+ noindex: true,
+ });
const user = useAuthStore((s) => s.user);
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
diff --git a/frontend/src/utils/useSEO.ts b/frontend/src/utils/useSEO.ts
index 88ffea1..ff30407 100644
--- a/frontend/src/utils/useSEO.ts
+++ b/frontend/src/utils/useSEO.ts
@@ -7,6 +7,8 @@ export interface SEOMeta {
ogImage?: string;
/** Module-level constant: injected once on mount, removed on unmount. */
jsonLd?: object | object[];
+ /** If true, sets robots meta to "noindex, nofollow" to prevent indexing. */
+ noindex?: boolean;
}
function qs(selector: string): HTMLMetaElement | null {
@@ -21,12 +23,13 @@ function qs(selector: string): HTMLMetaElement | null {
* once on mount and removed on unmount. Pass a module-level constant to avoid
* unnecessary re-injection.
*/
-export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
+export function useSEO({ title, description, url, ogImage, jsonLd, noindex }: SEOMeta) {
const scriptRef = useRef(null);
useEffect(() => {
const origTitle = document.title;
const descEl = qs('meta[name="description"]');
+ const robotsEl = qs('meta[name="robots"]');
const ogTitleEl = qs('meta[property="og:title"]');
const ogDescEl = qs('meta[property="og:description"]');
const ogUrlEl = qs('meta[property="og:url"]');
@@ -39,6 +42,7 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
const set = (el: HTMLMetaElement | null, v: string) => el?.setAttribute('content', v);
const origDesc = get(descEl);
+ const origRobots = get(robotsEl);
const origOgTitle = get(ogTitleEl);
const origOgDesc = get(ogDescEl);
const origOgUrl = get(ogUrlEl);
@@ -61,6 +65,9 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
// Apply
document.title = title;
set(descEl, description);
+ if (noindex) {
+ set(robotsEl, 'noindex, nofollow');
+ }
set(ogTitleEl, title);
set(ogDescEl, description);
set(ogUrlEl, url);
@@ -82,6 +89,7 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
return () => {
document.title = origTitle;
set(descEl, origDesc);
+ if (noindex) set(robotsEl, origRobots);
set(ogTitleEl, origOgTitle);
set(ogDescEl, origOgDesc);
set(ogUrlEl, origOgUrl);
@@ -99,5 +107,5 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [title, description, url, ogImage]);
+ }, [title, description, url, ogImage, noindex]);
}