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
parent
b8bdaf4c65
commit
c00f094d6b
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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> — 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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
'@type': 'TechArticle',
|
||||
headline: meta.title,
|
||||
description: meta.description,
|
||||
url: `https://velxio.dev/docs/${activeSection}`,
|
||||
isPartOf: { '@type': 'WebSite', url: 'https://velxio.dev/', name: 'Velxio' },
|
||||
inLanguage: 'en-US',
|
||||
author: { '@type': 'Person', name: 'David Montero Crespo', url: 'https://github.com/davidmonterocrespo24' },
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'TechArticle',
|
||||
headline: meta.title,
|
||||
description: meta.description,
|
||||
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]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue