Merge pull request #57 from davidmonterocrespo24/website

feat: update SEO handling across various pages, enhance mobile tab na…
pull/74/head
David Montero Crespo 2026-03-23 17:22:51 -03:00 committed by GitHub
commit fab2c8d604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 157 additions and 80 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -236,15 +236,6 @@
<li>ILI9341 TFT display simulation</li>
<li>I2C, SPI, USART, ADC, PWM support</li>
<li>Multi-board canvas: mix Arduino + ESP32 + Raspberry Pi in one simulation</li>
</ul>
<li>arduino-cli compilation backend &mdash; produces real .hex / .uf2 files</li>
<li>Serial Monitor with auto baud-rate detection and send</li>
<li>Library Manager for Arduino libraries</li>
<li>Multi-file workspace (.ino, .h, .cpp)</li>
<li>Wire system with orthogonal routing</li>
<li>ILI9341 TFT display simulation</li>
<li>I2C, SPI, USART, ADC, PWM support</li>
<li>Docker standalone image &mdash; deploy anywhere with one command</li>
</ul>
<h3>Supported Boards</h3>
<ul>

View File

@ -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

View File

@ -2,105 +2,128 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- ── Main pages ─────────────────────────────────── -->
<url>
<loc>https://velxio.dev/</loc>
<lastmod>2026-03-06</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://velxio.dev/editor</loc>
<lastmod>2026-03-06</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/examples</loc>
<lastmod>2026-03-23</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- ── Documentation ──────────────────────────────── -->
<url>
<loc>https://velxio.dev/docs</loc>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/intro</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/getting-started</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://velxio.dev/docs/emulator</loc>
<lastmod>2026-03-11</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/riscv-emulation</loc>
<lastmod>2026-03-15</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/esp32-emulation</loc>
<lastmod>2026-03-15</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/riscv-emulation</loc>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/rp2040-emulation</loc>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/raspberry-pi3-emulation</loc>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/components</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/roadmap</loc>
<lastmod>2026-03-11</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/docs/architecture</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/wokwi-libs</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/mcp</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/docs/setup</loc>
<lastmod>2026-03-11</lastmod>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://velxio.dev/examples</loc>
<loc>https://velxio.dev/docs/roadmap</loc>
<lastmod>2026-03-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<priority>0.6</priority>
</url>
<!-- SEO keyword landing pages -->
<!-- ── SEO keyword landing pages ──────────────────── -->
<url>
<loc>https://velxio.dev/arduino-simulator</loc>
<lastmod>2026-03-23</lastmod>
@ -129,6 +152,6 @@
<priority>0.85</priority>
</url>
<!-- Auth pages intentionally excluded from sitemap (not for indexing) -->
<!-- Auth/admin/project pages intentionally excluded (noindex) -->
</urlset>

View File

@ -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 {

View File

@ -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<AdminPageState>('loading');

View File

@ -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 = () => {
<div className="app">
<AppHeader />
{/* ── Mobile tab bar (top, above panels) ── */}
{isMobile && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('code')}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
<span>&lt;/&gt; Code</span>
</button>
<button
className={`mobile-tab-btn${mobileView === 'circuit' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('circuit')}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2" />
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
<line x1="12" y1="12" x2="12" y2="16" />
<line x1="10" y1="14" x2="14" y2="14" />
</svg>
<span>Circuit</span>
</button>
</nav>
)}
<div className="app-container" ref={containerRef}>
{/* ── Editor side ── */}
<div
@ -359,34 +387,6 @@ export const EditorPage: React.FC = () => {
</div>
</div>
{/* ── Mobile tab bar ── */}
{isMobile && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('code')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
<span>Code</span>
</button>
<button
className={`mobile-tab-btn${mobileView === 'circuit' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('circuit')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2" />
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
<line x1="12" y1="12" x2="12" y2="16" />
<line x1="10" y1="14" x2="14" y2="14" />
</svg>
<span>Circuit</span>
</button>
</nav>
)}
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
{showStarBanner && <GitHubStarBanner onClose={handleDismissStarBanner} />}

View File

@ -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);

View File

@ -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);

View File

@ -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('');

View File

@ -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<ProjectResponse[]>([]);
const [loading, setLoading] = useState(true);

View File

@ -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<HTMLScriptElement | null>(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]);
}