feat(seo+nav+community): public-project indexing + classroom nav + examples grid

Search-engine indexing of public projects
- Update robots.txt to also list /sitemap-projects.xml so Googlebot /
  Bingbot discover every public project's canonical /:username/:slug URL.
- Add /docs/github-sync + /classroom entries to seoRoutes.ts so the
  build-time sitemap.xml picks them up.

Navigation polish
- AppHeader gains a "For schools" link between Pricing and Download.
- LandingPage's pricing section gets a slim banner under the cards
  pointing institutional visitors to /classroom (visible discovery path,
  not just a footer link).
- Localised header.nav.classroom + landing.pricing.classroomBanner +
  landing.pricing.classroomCta across all 9 maintained locales (en/es/
  pt-br/fr/de/it/ja/ru/zh-cn).

Community examples
- New CommunityProjectsGrid component lives next to ExamplesGallery on
  /examples.  Fetches /api/projects/featured (Pro-overlay-only endpoint)
  and renders the top public projects ranked by run_count.  Quietly
  hides itself when the endpoint returns nothing or fails, so the OSS
  build still ships cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-05-29 18:11:28 +02:00
parent 0bcfbde617
commit 44f12f0e53
16 changed files with 258 additions and 20 deletions

View File

@ -5,12 +5,14 @@ Allow: /
Disallow: /login
Disallow: /register
# Dynamic user/project pages — noindex is also set via meta tag
# UUID-based duplicate of /:username/:slug — keep crawlers on the canonical
# user-friendly URL instead of the opaque /project/<uuid> alternate.
Disallow: /project/
Disallow: /admin
# Prevent crawling of API endpoints
Disallow: /api/
# Sitemap
# Sitemap (static build-time routes + dynamic public projects)
Sitemap: https://velxio.dev/sitemap.xml
Sitemap: https://velxio.dev/sitemap-projects.xml

View File

@ -0,0 +1,152 @@
/**
* CommunityProjectsGrid second section on /examples beneath the
* hardcoded official examples. Fetches `/api/projects/featured` (a
* pro-overlay-only endpoint that returns the top public projects ranked
* by run_count).
*
* Renders nothing when the fetch fails / returns empty so the OSS build
* (which has no projects route at all) degrades to its old layout
* without surfacing an error.
*/
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useLocalizedHref } from '../../i18n/useLocalizedNavigate';
type FeaturedProject = {
id: string;
name: string;
description: string | null;
slug: string;
owner_username: string;
board_type: string;
run_count: number;
compile_count: number;
};
const BOARD_LABELS: Record<string, string> = {
'arduino-uno': 'Arduino Uno',
'arduino-nano': 'Arduino Nano',
'arduino-mega': 'Arduino Mega',
'esp32': 'ESP32',
'esp32-s3': 'ESP32-S3',
'esp32-c3': 'ESP32-C3',
'raspberry-pi-pico': 'Pico',
'raspberry-pi-3': 'Pi 3',
'attiny85': 'ATtiny85',
};
export const CommunityProjectsGrid = () => {
const [projects, setProjects] = useState<FeaturedProject[] | null>(null);
const [hidden, setHidden] = useState(false);
const localize = useLocalizedHref();
useEffect(() => {
let cancelled = false;
fetch('/api/projects/featured?limit=12', { credentials: 'include' })
.then(async (r) => {
if (!r.ok) throw new Error(`status ${r.status}`);
return (await r.json()) as FeaturedProject[];
})
.then((rows) => {
if (cancelled) return;
if (!rows.length) {
setHidden(true);
return;
}
setProjects(rows);
})
.catch(() => { if (!cancelled) setHidden(true); });
return () => { cancelled = true; };
}, []);
if (hidden || !projects) return null;
return (
<section style={styles.shell}>
<div style={styles.header}>
<h2 style={styles.title}>Featured community projects</h2>
<p style={styles.sub}>
The most-run public circuits on Velxio. Open one to see how it's wired,
remix the code, and run it live.
</p>
</div>
<div style={styles.grid}>
{projects.map((p) => (
<Link
key={p.id}
to={localize(`/${p.owner_username}/${p.slug}`)}
style={styles.card}
className="velxio-community-card"
>
<div style={styles.cardName}>{p.name || 'Untitled'}</div>
<div style={styles.cardMeta}>
<span>{BOARD_LABELS[p.board_type] || p.board_type}</span>
<span>·</span>
<span>{p.run_count.toLocaleString()} runs</span>
</div>
<div style={styles.cardOwner}>by @{p.owner_username}</div>
{p.description && (
<div style={styles.cardDesc}>{p.description.slice(0, 140)}</div>
)}
</Link>
))}
</div>
</section>
);
};
const styles: Record<string, React.CSSProperties> = {
shell: {
maxWidth: 1280,
margin: '40px auto 60px',
padding: '0 24px',
color: '#ddd',
},
header: { marginBottom: 24 },
title: { fontSize: 24, fontWeight: 600, color: '#fff', margin: 0 },
sub: { color: '#aaa', fontSize: 14, margin: '6px 0 0 0', maxWidth: 640 },
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
gap: 14,
},
card: {
display: 'block',
background: '#1e1e1e',
border: '1px solid #2a2a2a',
borderRadius: 8,
padding: '14px 16px',
textDecoration: 'none',
color: 'inherit',
transition: 'border-color 0.12s, transform 0.12s',
},
cardName: {
fontSize: 15,
fontWeight: 600,
color: '#fff',
marginBottom: 6,
},
cardMeta: {
display: 'flex',
gap: 6,
alignItems: 'center',
color: '#888',
fontSize: 12,
},
cardOwner: {
color: '#4fc3f7',
fontSize: 12,
marginTop: 6,
},
cardDesc: {
color: '#aaa',
fontSize: 13,
marginTop: 8,
lineHeight: 1.45,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
},
};

View File

@ -142,6 +142,9 @@ export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave }) => {
<Link to={localize('/pricing')} className={'header-nav-link' + isActive('/pricing')}>
{t('header.nav.pricing')}
</Link>
<Link to={localize('/classroom')} className={'header-nav-link' + isActive('/classroom')}>
{t('header.nav.classroom', 'For schools')}
</Link>
<Link
to={localize('/account/desktop-install')}
className={'header-nav-link' + isActive('/account/desktop-install')}

View File

@ -13,7 +13,8 @@
"download": "Download",
"blog": "Blog",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "Für Schulen"
},
"auth": {
"signIn": "Anmelden",
@ -143,7 +144,9 @@
"f3": "GitHub-Sync + Embeds ohne Wasserzeichen",
"cta": "Pro abonnieren"
}
}
},
"classroomBanner": "Velxio im Unterricht einsetzen? Velxio for Classroom gibt jedem Schüler Pro-Zugang ab $40/Jahr.",
"classroomCta": "Klassen-Pläne ansehen →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "Download",
"blog": "Blog",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "For schools"
},
"auth": {
"signIn": "Sign in",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + watermark-free embeds",
"cta": "Subscribe to Pro"
}
}
},
"classroomBanner": "Bringing Velxio into a course? Velxio for Classroom gives every student Pro access from $40/year.",
"classroomCta": "See classroom plans →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "Descargar",
"blog": "Blog",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "Para escuelas"
},
"auth": {
"signIn": "Iniciar sesión",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + embeds sin marca",
"cta": "Suscribirse a Pro"
}
}
},
"classroomBanner": "¿Llevás Velxio a un curso? Velxio for Classroom da a cada estudiante acceso Pro desde $40/año.",
"classroomCta": "Ver planes para aula →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "Télécharger",
"blog": "Blog",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "Pour les écoles"
},
"auth": {
"signIn": "Connexion",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + intégrations sans filigrane",
"cta": "S'abonner à Pro"
}
}
},
"classroomBanner": "Vous apportez Velxio dans un cours ? Velxio for Classroom donne à chaque étudiant un accès Pro dès $40/an.",
"classroomCta": "Voir les plans classe →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "Scarica",
"blog": "Blog",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "Per le scuole"
},
"auth": {
"signIn": "Accedi",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + embed senza watermark",
"cta": "Abbonati a Pro"
}
}
},
"classroomBanner": "Stai portando Velxio in un corso? Velxio for Classroom dà a ogni studente accesso Pro da $40/anno.",
"classroomCta": "Vedi i piani per classi →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "ダウンロード",
"blog": "ブログ",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "学校向け"
},
"auth": {
"signIn": "サインイン",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + ウォーターマーク無しの埋め込み",
"cta": "Proを購読"
}
}
},
"classroomBanner": "コースでVelxioを使いますか Velxio for Classroomは年$40から学生全員にProアクセスを提供します。",
"classroomCta": "クラスプランを見る →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "Baixar",
"blog": "Blog",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "Para escolas"
},
"auth": {
"signIn": "Entrar",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + embeds sem marca d'água",
"cta": "Assinar Pro"
}
}
},
"classroomBanner": "Está trazendo o Velxio para um curso? Velxio for Classroom dá acesso Pro a cada aluno a partir de US$ 40/ano.",
"classroomCta": "Ver planos para aulas →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "Скачать",
"blog": "Блог",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "Для школ"
},
"auth": {
"signIn": "Войти",
@ -143,7 +144,9 @@
"f3": "GitHub Sync + встраивания без водяного знака",
"cta": "Подписаться на Pro"
}
}
},
"classroomBanner": "Внедряете Velxio в курс? Velxio for Classroom даёт каждому студенту доступ Pro от $40/год.",
"classroomCta": "Посмотреть планы для классов →"
}
},
"profile": {

View File

@ -13,7 +13,8 @@
"download": "下载",
"blog": "博客",
"github": "GitHub",
"discord": "Discord"
"discord": "Discord",
"classroom": "面向学校"
},
"auth": {
"signIn": "登录",
@ -143,7 +144,9 @@
"f3": "GitHub 同步 + 无水印嵌入",
"cta": "订阅 Pro"
}
}
},
"classroomBanner": "准备在课程中使用 VelxioVelxio for Classroom 为每位学生提供 Pro 访问权限,每年 $40 起。",
"classroomCta": "查看课堂方案 →"
}
},
"profile": {

View File

@ -10,6 +10,7 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ExamplesGallery } from '../components/examples/ExamplesGallery';
import { CommunityProjectsGrid } from '../components/examples/CommunityProjectsGrid';
import { AppHeader } from '../components/layout/AppHeader';
import { useLocalizedHref } from '../i18n/useLocalizedNavigate';
import { useSEO } from '../utils/useSEO';
@ -37,6 +38,7 @@ export const ExamplesPage: React.FC = () => {
>
<AppHeader />
<ExamplesGallery onLoadExample={handleLoadExample} />
<CommunityProjectsGrid />
</div>
);
};

View File

@ -1368,6 +1368,34 @@
border-color: rgba(255, 255, 255, 0.35);
}
.pricing-classroom-banner {
margin: 28px auto 0;
max-width: 920px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 14px;
padding: 14px 22px;
border-radius: 10px;
background: rgba(0, 184, 212, 0.06);
border: 1px solid rgba(0, 184, 212, 0.2);
color: #b5b5b5;
font-size: 14px;
text-align: center;
}
.pricing-classroom-banner-cta {
color: #4fc3f7;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
.pricing-classroom-banner-cta:hover {
text-decoration: underline;
}
@media (max-width: 900px) {
.pricing-grid {
grid-template-columns: 1fr;

View File

@ -1152,6 +1152,17 @@ export const LandingPage: React.FC = () => {
</Link>
</div>
</div>
<div className="pricing-classroom-banner">
<span>
{t(
'landing.pricing.classroomBanner',
'Bringing Velxio into a course? Velxio for Classroom gives every student Pro access from $40/year.',
)}
</span>
<Link to={localize('/classroom')} className="pricing-classroom-banner-cta">
{t('landing.pricing.classroomCta', 'See classroom plans →')}
</Link>
</div>
</section>
{/* Support */}

View File

@ -423,6 +423,19 @@ export const SEO_ROUTES: SeoRoute[] = [
},
},
// ── GitHub Sync docs — Phase 3 D3.5 companion
{
path: '/docs/github-sync',
priority: 0.7,
changefreq: 'monthly',
seoMeta: {
title: 'GitHub Sync — Velxio Pro docs',
description:
"Velxio Pro's GitHub Sync commits every project save (sketch.ino + velxio.json + auto-generated README) to a repo you control. Setup walkthrough, security model and FAQ.",
url: `${DOMAIN}/docs/github-sync`,
},
},
// ── Auth / admin (noindex)
{ path: '/login', noindex: true },
{ path: '/register', noindex: true },