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>
This commit is contained in:
parent
b8bdaf4c65
commit
c00f094d6b
|
|
@ -33,6 +33,11 @@ app = FastAPI(
|
||||||
description="Compilation and project management API",
|
description="Compilation and project management API",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
lifespan=lifespan,
|
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
|
# CORS for local development
|
||||||
|
|
@ -62,7 +67,7 @@ def root():
|
||||||
return {
|
return {
|
||||||
"message": "Arduino Emulator API",
|
"message": "Arduino Emulator API",
|
||||||
"version": "1.0.0",
|
"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 X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
# Frontend SPA routing
|
# SEO crawler files — never cache so Googlebot always gets the latest version
|
||||||
location / {
|
location = /sitemap.xml {
|
||||||
try_files $uri $uri/ /index.html;
|
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
|
# Health check endpoint
|
||||||
location /health {
|
location = /health {
|
||||||
proxy_pass http://127.0.0.1:8001/health;
|
proxy_pass http://127.0.0.1:8001/health;
|
||||||
proxy_set_header Host $host;
|
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/ {
|
location /api/ {
|
||||||
proxy_pass http://127.0.0.1:8001/api/;
|
proxy_pass http://127.0.0.1:8001/api/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
@ -32,25 +41,26 @@ server {
|
||||||
proxy_connect_timeout 75s;
|
proxy_connect_timeout 75s;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Proxy websockets/docs if needed
|
# Cache static assets with content-hash filenames (js/css/fonts/images)
|
||||||
location /docs {
|
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
|
||||||
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)$ {
|
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~* \.(png|jpg|jpeg|gif|ico|svg|webp)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
# Gzip compression
|
# Gzip compression
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_proxied any;
|
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:
|
The `AVRSimulator` (`frontend/src/simulation/AVRSimulator.ts`) uses avr8js to create:
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { CPU, avrInstruction, AVRTimer, AVRUSART, AVRADC, AVRIOPort } from 'avr8js';
|
|
||||||
```typescript
|
```typescript
|
||||||
import { CPU, avrInstruction, AVRTimer, AVRUSART, AVRADC, AVRIOPort } from 'avr8js';
|
import { CPU, avrInstruction, AVRTimer, AVRUSART, AVRADC, AVRIOPort } from 'avr8js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,7 @@
|
||||||
<nav style="margin-top:.75rem;">
|
<nav style="margin-top:.75rem;">
|
||||||
<a href="/editor" style="color:#58a6ff;margin-right:1.5rem;">Editor</a>
|
<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="/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>
|
<a href="https://github.com/davidmonterocrespo24/velxio" style="color:#58a6ff;">GitHub</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -270,6 +271,17 @@
|
||||||
<a href="/editor">Open the Editor</a> — no installation needed.<br />
|
<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>
|
Self-host with Docker: <code>docker run -d -p 3080:80 ghcr.io/davidmonterocrespo24/velxio:master</code>
|
||||||
</p>
|
</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>
|
<h3>Frequently Asked Questions</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Is Velxio free?</dt>
|
<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
|
// Capture the original <head> values once on mount and restore them on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const origTitle = document.title;
|
const origTitle = document.title;
|
||||||
const descEl = document.querySelector<HTMLMetaElement>('meta[name="description"]');
|
|
||||||
const origDesc = descEl?.getAttribute('content') ?? '';
|
// Helper to capture an element and its original attribute value
|
||||||
const canonicalEl = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
|
const snap = <E extends Element>(selector: string, attr: string): [E | null, string] => {
|
||||||
const origCanonical = canonicalEl?.getAttribute('href') ?? '';
|
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 () => {
|
return () => {
|
||||||
document.title = origTitle;
|
document.title = origTitle;
|
||||||
if (descEl) descEl.setAttribute('content', origDesc);
|
descEl?.setAttribute('content', origDesc);
|
||||||
if (canonicalEl) canonicalEl.setAttribute('href', origCanonical);
|
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();
|
document.getElementById('docs-jsonld')?.remove();
|
||||||
};
|
};
|
||||||
}, []); // runs once on mount; cleanup runs once on unmount
|
}, []); // 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,
|
// No cleanup here — the mount effect above restores defaults on unmount,
|
||||||
// and on a section change the next run of this effect immediately overwrites.
|
// and on a section change the next run of this effect immediately overwrites.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const meta = SECTION_META[activeSection];
|
const meta = SECTION_META[activeSection];
|
||||||
|
const pageUrl = `https://velxio.dev/docs/${activeSection}`;
|
||||||
|
|
||||||
document.title = meta.title;
|
document.title = meta.title;
|
||||||
|
|
||||||
const descEl = document.querySelector<HTMLMetaElement>('meta[name="description"]');
|
const set = (selector: string, value: string) =>
|
||||||
if (descEl) descEl.setAttribute('content', meta.description);
|
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"]');
|
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
|
// Inject / update JSON-LD structured data for this doc page
|
||||||
const ldId = 'docs-jsonld';
|
const ldId = 'docs-jsonld';
|
||||||
|
|
@ -547,13 +574,25 @@ export const DocsPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
ldScript.textContent = JSON.stringify({
|
ldScript.textContent = JSON.stringify({
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'TechArticle',
|
'@graph': [
|
||||||
headline: meta.title,
|
{
|
||||||
description: meta.description,
|
'@type': 'TechArticle',
|
||||||
url: `https://velxio.dev/docs/${activeSection}`,
|
headline: meta.title,
|
||||||
isPartOf: { '@type': 'WebSite', url: 'https://velxio.dev/', name: 'Velxio' },
|
description: meta.description,
|
||||||
inLanguage: 'en-US',
|
url: pageUrl,
|
||||||
author: { '@type': 'Person', name: 'David Montero Crespo', url: 'https://github.com/davidmonterocrespo24' },
|
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]);
|
}, [activeSection]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue