Fix nginx /docs routing conflict, move FastAPI Swagger to /api/docs, complete SEO meta tags

Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>
pull/25/head
copilot-swe-agent[bot] 2026-03-11 18:55:18 +00:00
parent b8bdaf4c65
commit c00f094d6b
5 changed files with 101 additions and 37 deletions

View File

@ -33,6 +33,11 @@ app = FastAPI(
description="Compilation and project management API",
version="1.0.0",
lifespan=lifespan,
# Moved from /docs to /api/docs so the frontend /docs/* documentation
# routes are served by the React SPA without any nginx conflict.
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
)
# CORS for local development
@ -62,7 +67,7 @@ def root():
return {
"message": "Arduino Emulator API",
"version": "1.0.0",
"docs": "/docs",
"docs": "/api/docs",
}

View File

@ -10,18 +10,27 @@ server {
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Frontend SPA routing
location / {
try_files $uri $uri/ /index.html;
# SEO crawler files never cache so Googlebot always gets the latest version
location = /sitemap.xml {
try_files $uri =404;
add_header Cache-Control "no-cache, must-revalidate";
add_header X-Content-Type-Options "nosniff" always;
}
location = /robots.txt {
try_files $uri =404;
add_header Cache-Control "no-cache, must-revalidate";
}
# Health check endpoint
location /health {
location = /health {
proxy_pass http://127.0.0.1:8001/health;
proxy_set_header Host $host;
}
# Proxy /api requests to localhost backend
# Proxy /api/* requests to the FastAPI backend.
# FastAPI Swagger UI is at /api/docs (moved from /docs to avoid
# conflicting with the frontend /docs/* documentation routes).
location /api/ {
proxy_pass http://127.0.0.1:8001/api/;
proxy_set_header Host $host;
@ -32,25 +41,26 @@ server {
proxy_connect_timeout 75s;
}
# Proxy websockets/docs if needed
location /docs {
proxy_pass http://127.0.0.1:8001/docs;
}
location /openapi.json {
proxy_pass http://127.0.0.1:8001/openapi.json;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# Cache static assets with content-hash filenames (js/css/fonts/images)
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~* \.(png|jpg|jpeg|gif|ico|svg|webp)$ {
expires 30d;
add_header Cache-Control "public";
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/rss+xml;
# Frontend SPA routing must be last so specific locations above take precedence
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@ -174,8 +174,6 @@ pause
The `AVRSimulator` (`frontend/src/simulation/AVRSimulator.ts`) uses avr8js to create:
```typescript
import { CPU, avrInstruction, AVRTimer, AVRUSART, AVRADC, AVRIOPort } from 'avr8js';
```typescript
import { CPU, avrInstruction, AVRTimer, AVRUSART, AVRADC, AVRIOPort } from 'avr8js';

View File

@ -239,6 +239,7 @@
<nav style="margin-top:.75rem;">
<a href="/editor" style="color:#58a6ff;margin-right:1.5rem;">Editor</a>
<a href="/examples" style="color:#58a6ff;margin-right:1.5rem;">Examples</a>
<a href="/docs/intro" style="color:#58a6ff;margin-right:1.5rem;">Documentation</a>
<a href="https://github.com/davidmonterocrespo24/velxio" style="color:#58a6ff;">GitHub</a>
</nav>
</header>
@ -270,6 +271,17 @@
<a href="/editor">Open the Editor</a> &mdash; no installation needed.<br />
Self-host with Docker: <code>docker run -d -p 3080:80 ghcr.io/davidmonterocrespo24/velxio:master</code>
</p>
<h3>Documentation</h3>
<p>Browse the full Velxio documentation to learn how to set up, configure, and extend the emulator:</p>
<nav>
<ul>
<li><a href="/docs/intro">Introduction — What is Velxio and why use it?</a></li>
<li><a href="/docs/getting-started">Getting Started — Hosted editor, Docker, and manual setup</a></li>
<li><a href="/docs/emulator">Emulator Architecture — AVR8 and RP2040 CPU emulation internals</a></li>
<li><a href="/docs/components">Components Reference — All 48+ interactive electronic components</a></li>
<li><a href="/docs/roadmap">Roadmap — Implemented, in-progress, and planned features</a></li>
</ul>
</nav>
<h3>Frequently Asked Questions</h3>
<dl>
<dt>Is Velxio free?</dt>

View File

@ -509,32 +509,59 @@ export const DocsPage: React.FC = () => {
// Capture the original <head> values once on mount and restore them on unmount
useEffect(() => {
const origTitle = document.title;
const descEl = document.querySelector<HTMLMetaElement>('meta[name="description"]');
const origDesc = descEl?.getAttribute('content') ?? '';
const canonicalEl = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
const origCanonical = canonicalEl?.getAttribute('href') ?? '';
// Helper to capture an element and its original attribute value
const snap = <E extends Element>(selector: string, attr: string): [E | null, string] => {
const el = document.querySelector<E>(selector);
return [el, el?.getAttribute(attr) ?? ''];
};
const [descEl, origDesc] = snap<HTMLMetaElement>('meta[name="description"]', 'content');
const [canonicalEl, origCanonical] = snap<HTMLLinkElement>('link[rel="canonical"]', 'href');
const [ogTitleEl, origOgTitle] = snap<HTMLMetaElement>('meta[property="og:title"]', 'content');
const [ogDescEl, origOgDesc] = snap<HTMLMetaElement>('meta[property="og:description"]', 'content');
const [ogUrlEl, origOgUrl] = snap<HTMLMetaElement>('meta[property="og:url"]', 'content');
const [twTitleEl, origTwTitle] = snap<HTMLMetaElement>('meta[name="twitter:title"]', 'content');
const [twDescEl, origTwDesc] = snap<HTMLMetaElement>('meta[name="twitter:description"]', 'content');
return () => {
document.title = origTitle;
if (descEl) descEl.setAttribute('content', origDesc);
if (canonicalEl) canonicalEl.setAttribute('href', origCanonical);
descEl?.setAttribute('content', origDesc);
canonicalEl?.setAttribute('href', origCanonical);
ogTitleEl?.setAttribute('content', origOgTitle);
ogDescEl?.setAttribute('content', origOgDesc);
ogUrlEl?.setAttribute('content', origOgUrl);
twTitleEl?.setAttribute('content', origTwTitle);
twDescEl?.setAttribute('content', origTwDesc);
document.getElementById('docs-jsonld')?.remove();
};
}, []); // runs once on mount; cleanup runs once on unmount
// Update document title, meta description, canonical, and JSON-LD per section.
// Update all head metadata + JSON-LD per section.
// No cleanup here — the mount effect above restores defaults on unmount,
// and on a section change the next run of this effect immediately overwrites.
useEffect(() => {
const meta = SECTION_META[activeSection];
const pageUrl = `https://velxio.dev/docs/${activeSection}`;
document.title = meta.title;
const descEl = document.querySelector<HTMLMetaElement>('meta[name="description"]');
if (descEl) descEl.setAttribute('content', meta.description);
const set = (selector: string, value: string) =>
document.querySelector<HTMLMetaElement>(selector)?.setAttribute('content', value);
set('meta[name="description"]', meta.description);
set('meta[property="og:title"]', meta.title);
set('meta[property="og:description"]', meta.description);
set('meta[property="og:url"]', pageUrl);
set('meta[name="twitter:title"]', meta.title);
set('meta[name="twitter:description"]', meta.description);
const canonicalEl = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (canonicalEl) canonicalEl.setAttribute('href', `https://velxio.dev/docs/${activeSection}`);
if (canonicalEl) canonicalEl.setAttribute('href', pageUrl);
// Build the breadcrumb section label for JSON-LD
const activeNav = NAV_ITEMS.find((i) => i.id === activeSection);
const sectionLabel = activeNav?.label ?? activeSection;
// Inject / update JSON-LD structured data for this doc page
const ldId = 'docs-jsonld';
@ -547,13 +574,25 @@ export const DocsPage: React.FC = () => {
}
ldScript.textContent = JSON.stringify({
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'TechArticle',
headline: meta.title,
description: meta.description,
url: `https://velxio.dev/docs/${activeSection}`,
url: pageUrl,
isPartOf: { '@type': 'WebSite', url: 'https://velxio.dev/', name: 'Velxio' },
inLanguage: 'en-US',
author: { '@type': 'Person', name: 'David Montero Crespo', url: 'https://github.com/davidmonterocrespo24' },
},
{
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://velxio.dev/' },
{ '@type': 'ListItem', position: 2, name: 'Documentation', item: 'https://velxio.dev/docs/intro' },
{ '@type': 'ListItem', position: 3, name: sectionLabel, item: pageUrl },
],
},
],
});
}, [activeSection]);