Merge pull request #57 from davidmonterocrespo24/website
feat: update SEO handling across various pages, enhance mobile tab na…pull/74/head
commit
fab2c8d604
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 — 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 — deploy anywhere with one command</li>
|
||||
</ul>
|
||||
<h3>Supported Boards</h3>
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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></> 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} />}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue