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:
parent
0bcfbde617
commit
44f12f0e53
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@
|
|||
"download": "下载",
|
||||
"blog": "博客",
|
||||
"github": "GitHub",
|
||||
"discord": "Discord"
|
||||
"discord": "Discord",
|
||||
"classroom": "面向学校"
|
||||
},
|
||||
"auth": {
|
||||
"signIn": "登录",
|
||||
|
|
@ -143,7 +144,9 @@
|
|||
"f3": "GitHub 同步 + 无水印嵌入",
|
||||
"cta": "订阅 Pro"
|
||||
}
|
||||
}
|
||||
},
|
||||
"classroomBanner": "准备在课程中使用 Velxio?Velxio for Classroom 为每位学生提供 Pro 访问权限,每年 $40 起。",
|
||||
"classroomCta": "查看课堂方案 →"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Reference in New Issue