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/**/*.map
|
||||||
test/esp32-emulator/out_*/
|
test/esp32-emulator/out_*/
|
||||||
.claude/settings.json
|
.claude/settings.json
|
||||||
|
|
||||||
|
# Google Cloud service account credentials
|
||||||
|
velxio-ba3355a41944.json
|
||||||
|
|
|
||||||
|
|
@ -236,15 +236,6 @@
|
||||||
<li>ILI9341 TFT display simulation</li>
|
<li>ILI9341 TFT display simulation</li>
|
||||||
<li>I2C, SPI, USART, ADC, PWM support</li>
|
<li>I2C, SPI, USART, ADC, PWM support</li>
|
||||||
<li>Multi-board canvas: mix Arduino + ESP32 + Raspberry Pi in one simulation</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>
|
</ul>
|
||||||
<h3>Supported Boards</h3>
|
<h3>Supported Boards</h3>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
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
|
||||||
Sitemap: https://velxio.dev/sitemap.xml
|
Sitemap: https://velxio.dev/sitemap.xml
|
||||||
|
|
|
||||||
|
|
@ -2,105 +2,128 @@
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<!-- ── Main pages ─────────────────────────────────── -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/</loc>
|
<loc>https://velxio.dev/</loc>
|
||||||
<lastmod>2026-03-06</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/editor</loc>
|
<loc>https://velxio.dev/editor</loc>
|
||||||
<lastmod>2026-03-06</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/intro</loc>
|
<loc>https://velxio.dev/docs/intro</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/getting-started</loc>
|
<loc>https://velxio.dev/docs/getting-started</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/emulator</loc>
|
<loc>https://velxio.dev/docs/emulator</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/riscv-emulation</loc>
|
|
||||||
<lastmod>2026-03-15</lastmod>
|
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/esp32-emulation</loc>
|
<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>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/components</loc>
|
<loc>https://velxio.dev/docs/components</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
|
||||||
<loc>https://velxio.dev/docs/roadmap</loc>
|
|
||||||
<lastmod>2026-03-11</lastmod>
|
|
||||||
<changefreq>monthly</changefreq>
|
|
||||||
<priority>0.6</priority>
|
|
||||||
</url>
|
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/architecture</loc>
|
<loc>https://velxio.dev/docs/architecture</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/wokwi-libs</loc>
|
<loc>https://velxio.dev/docs/wokwi-libs</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/mcp</loc>
|
<loc>https://velxio.dev/docs/mcp</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/docs/setup</loc>
|
<loc>https://velxio.dev/docs/setup</loc>
|
||||||
<lastmod>2026-03-11</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/examples</loc>
|
<loc>https://velxio.dev/docs/roadmap</loc>
|
||||||
<lastmod>2026-03-23</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<!-- SEO keyword landing pages -->
|
<!-- ── SEO keyword landing pages ──────────────────── -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://velxio.dev/arduino-simulator</loc>
|
<loc>https://velxio.dev/arduino-simulator</loc>
|
||||||
<lastmod>2026-03-23</lastmod>
|
<lastmod>2026-03-23</lastmod>
|
||||||
|
|
@ -129,6 +152,6 @@
|
||||||
<priority>0.85</priority>
|
<priority>0.85</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<!-- Auth pages intentionally excluded from sitemap (not for indexing) -->
|
<!-- Auth/admin/project pages intentionally excluded (noindex) -->
|
||||||
|
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|
|
||||||
|
|
@ -283,32 +283,36 @@ body {
|
||||||
.mobile-tab-bar {
|
.mobile-tab-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 54px;
|
height: 44px;
|
||||||
background: #252526;
|
background: #1e1e1e;
|
||||||
border-top: 1px solid #007acc;
|
border-bottom: 2px solid #007acc;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
|
/* Sits between header and panels */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-tab-btn {
|
.mobile-tab-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 3px;
|
gap: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
color: #7a7a7a;
|
color: #7a7a7a;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s, background 0.15s;
|
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||||
padding: 6px 0;
|
padding: 8px 0;
|
||||||
|
margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-tab-btn--active {
|
.mobile-tab-btn--active {
|
||||||
color: #007acc;
|
color: #007acc;
|
||||||
background: rgba(0, 122, 204, 0.08);
|
border-bottom-color: #007acc;
|
||||||
|
background: rgba(0, 122, 204, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-tab-btn:active {
|
.mobile-tab-btn:active {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
|
import { useSEO } from '../utils/useSEO';
|
||||||
import {
|
import {
|
||||||
getAdminSetupStatus,
|
getAdminSetupStatus,
|
||||||
createFirstAdmin,
|
createFirstAdmin,
|
||||||
|
|
@ -517,6 +518,13 @@ function AdminDashboard() {
|
||||||
type AdminPageState = 'loading' | 'setup' | 'not-admin' | 'dashboard';
|
type AdminPageState = 'loading' | 'setup' | 'not-admin' | 'dashboard';
|
||||||
|
|
||||||
export const AdminPage: React.FC = () => {
|
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 user = useAuthStore((s) => s.user);
|
||||||
const [pageState, setPageState] = useState<AdminPageState>('loading');
|
const [pageState, setPageState] = useState<AdminPageState>('loading');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,8 @@ export const EditorPage: React.FC = () => {
|
||||||
const [explorerOpen, setExplorerOpen] = useState(true);
|
const [explorerOpen, setExplorerOpen] = useState(true);
|
||||||
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
|
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
|
||||||
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
|
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
|
||||||
// Default to 'circuit' on mobile — the visual simulation is the primary content
|
// Default to 'code' on mobile — show the editor so users can write/view code
|
||||||
const [mobileView, setMobileView] = useState<'code' | 'circuit'>('circuit');
|
const [mobileView, setMobileView] = useState<'code' | 'circuit'>('code');
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
|
|
||||||
const handleSaveClick = useCallback(() => {
|
const handleSaveClick = useCallback(() => {
|
||||||
|
|
@ -233,6 +233,34 @@ export const EditorPage: React.FC = () => {
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<AppHeader />
|
<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}>
|
<div className="app-container" ref={containerRef}>
|
||||||
{/* ── Editor side ── */}
|
{/* ── Editor side ── */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -359,34 +387,6 @@ export const EditorPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</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)} />}
|
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
|
||||||
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
|
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
|
||||||
{showStarBanner && <GitHubStarBanner onClose={handleDismissStarBanner} />}
|
{showStarBanner && <GitHubStarBanner onClose={handleDismissStarBanner} />}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,15 @@ import { useState } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { login, initiateGoogleLogin } from '../services/authService';
|
import { login, initiateGoogleLogin } from '../services/authService';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
|
import { useSEO } from '../utils/useSEO';
|
||||||
|
|
||||||
export const LoginPage: React.FC = () => {
|
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 navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const setUser = useAuthStore((s) => s.setUser);
|
const setUser = useAuthStore((s) => s.setUser);
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,16 @@ import { getProjectById } from '../services/projectService';
|
||||||
import { useEditorStore } from '../store/useEditorStore';
|
import { useEditorStore } from '../store/useEditorStore';
|
||||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||||
import { useProjectStore } from '../store/useProjectStore';
|
import { useProjectStore } from '../store/useProjectStore';
|
||||||
|
import { useSEO } from '../utils/useSEO';
|
||||||
import { EditorPage } from './EditorPage';
|
import { EditorPage } from './EditorPage';
|
||||||
|
|
||||||
export const ProjectByIdPage: React.FC = () => {
|
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 { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const loadFiles = useEditorStore((s) => s.loadFiles);
|
const loadFiles = useEditorStore((s) => s.loadFiles);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,15 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { register, initiateGoogleLogin } from '../services/authService';
|
import { register, initiateGoogleLogin } from '../services/authService';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
import { RESERVED_USERNAMES } from '../utils/reservedUsernames';
|
import { RESERVED_USERNAMES } from '../utils/reservedUsernames';
|
||||||
|
import { useSEO } from '../utils/useSEO';
|
||||||
|
|
||||||
export const RegisterPage: React.FC = () => {
|
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 navigate = useNavigate();
|
||||||
const setUser = useAuthStore((s) => s.setUser);
|
const setUser = useAuthStore((s) => s.setUser);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,18 @@ import { Link, useParams } from 'react-router-dom';
|
||||||
import { getUserProjects, type ProjectResponse } from '../services/projectService';
|
import { getUserProjects, type ProjectResponse } from '../services/projectService';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
import { AppHeader } from '../components/layout/AppHeader';
|
import { AppHeader } from '../components/layout/AppHeader';
|
||||||
|
import { useSEO } from '../utils/useSEO';
|
||||||
import './UserProfilePage.css';
|
import './UserProfilePage.css';
|
||||||
|
|
||||||
export const UserProfilePage: React.FC = () => {
|
export const UserProfilePage: React.FC = () => {
|
||||||
const { username } = useParams<{ username: string }>();
|
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 user = useAuthStore((s) => s.user);
|
||||||
const [projects, setProjects] = useState<ProjectResponse[]>([]);
|
const [projects, setProjects] = useState<ProjectResponse[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ export interface SEOMeta {
|
||||||
ogImage?: string;
|
ogImage?: string;
|
||||||
/** Module-level constant: injected once on mount, removed on unmount. */
|
/** Module-level constant: injected once on mount, removed on unmount. */
|
||||||
jsonLd?: object | object[];
|
jsonLd?: object | object[];
|
||||||
|
/** If true, sets robots meta to "noindex, nofollow" to prevent indexing. */
|
||||||
|
noindex?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function qs(selector: string): HTMLMetaElement | null {
|
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
|
* once on mount and removed on unmount. Pass a module-level constant to avoid
|
||||||
* unnecessary re-injection.
|
* 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);
|
const scriptRef = useRef<HTMLScriptElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const origTitle = document.title;
|
const origTitle = document.title;
|
||||||
const descEl = qs('meta[name="description"]');
|
const descEl = qs('meta[name="description"]');
|
||||||
|
const robotsEl = qs('meta[name="robots"]');
|
||||||
const ogTitleEl = qs('meta[property="og:title"]');
|
const ogTitleEl = qs('meta[property="og:title"]');
|
||||||
const ogDescEl = qs('meta[property="og:description"]');
|
const ogDescEl = qs('meta[property="og:description"]');
|
||||||
const ogUrlEl = qs('meta[property="og:url"]');
|
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 set = (el: HTMLMetaElement | null, v: string) => el?.setAttribute('content', v);
|
||||||
|
|
||||||
const origDesc = get(descEl);
|
const origDesc = get(descEl);
|
||||||
|
const origRobots = get(robotsEl);
|
||||||
const origOgTitle = get(ogTitleEl);
|
const origOgTitle = get(ogTitleEl);
|
||||||
const origOgDesc = get(ogDescEl);
|
const origOgDesc = get(ogDescEl);
|
||||||
const origOgUrl = get(ogUrlEl);
|
const origOgUrl = get(ogUrlEl);
|
||||||
|
|
@ -61,6 +65,9 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
|
||||||
// Apply
|
// Apply
|
||||||
document.title = title;
|
document.title = title;
|
||||||
set(descEl, description);
|
set(descEl, description);
|
||||||
|
if (noindex) {
|
||||||
|
set(robotsEl, 'noindex, nofollow');
|
||||||
|
}
|
||||||
set(ogTitleEl, title);
|
set(ogTitleEl, title);
|
||||||
set(ogDescEl, description);
|
set(ogDescEl, description);
|
||||||
set(ogUrlEl, url);
|
set(ogUrlEl, url);
|
||||||
|
|
@ -82,6 +89,7 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
|
||||||
return () => {
|
return () => {
|
||||||
document.title = origTitle;
|
document.title = origTitle;
|
||||||
set(descEl, origDesc);
|
set(descEl, origDesc);
|
||||||
|
if (noindex) set(robotsEl, origRobots);
|
||||||
set(ogTitleEl, origOgTitle);
|
set(ogTitleEl, origOgTitle);
|
||||||
set(ogDescEl, origOgDesc);
|
set(ogDescEl, origOgDesc);
|
||||||
set(ogUrlEl, origOgUrl);
|
set(ogUrlEl, origOgUrl);
|
||||||
|
|
@ -99,5 +107,5 @@ export function useSEO({ title, description, url, ogImage, jsonLd }: SEOMeta) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [title, description, url, ogImage]);
|
}, [title, description, url, ogImage, noindex]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue