diff --git a/backend/app/main.py b/backend/app/main.py
index b3554940..4b7a688d 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -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",
}
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index df7b4105..fc09ba63 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -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;
+ }
}
diff --git a/docs/WOKWI_LIBS.md b/docs/WOKWI_LIBS.md
index 72eec9a3..e8ca6a5c 100644
--- a/docs/WOKWI_LIBS.md
+++ b/docs/WOKWI_LIBS.md
@@ -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';
diff --git a/frontend/index.html b/frontend/index.html
index 8866abb7..b8b6f1a1 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -239,6 +239,7 @@
@@ -270,6 +271,17 @@
Open the Editor — no installation needed.
Self-host with Docker: docker run -d -p 3080:80 ghcr.io/davidmonterocrespo24/velxio:master
+ Documentation
+ Browse the full Velxio documentation to learn how to set up, configure, and extend the emulator:
+
Frequently Asked Questions
- Is Velxio free?
diff --git a/frontend/src/pages/DocsPage.tsx b/frontend/src/pages/DocsPage.tsx
index 456c8e5f..9f4e078b 100644
--- a/frontend/src/pages/DocsPage.tsx
+++ b/frontend/src/pages/DocsPage.tsx
@@ -509,32 +509,59 @@ export const DocsPage: React.FC = () => {
// Capture the original values once on mount and restore them on unmount
useEffect(() => {
const origTitle = document.title;
- const descEl = document.querySelector('meta[name="description"]');
- const origDesc = descEl?.getAttribute('content') ?? '';
- const canonicalEl = document.querySelector('link[rel="canonical"]');
- const origCanonical = canonicalEl?.getAttribute('href') ?? '';
+
+ // Helper to capture an element and its original attribute value
+ const snap = (selector: string, attr: string): [E | null, string] => {
+ const el = document.querySelector(selector);
+ return [el, el?.getAttribute(attr) ?? ''];
+ };
+
+ const [descEl, origDesc] = snap('meta[name="description"]', 'content');
+ const [canonicalEl, origCanonical] = snap('link[rel="canonical"]', 'href');
+ const [ogTitleEl, origOgTitle] = snap('meta[property="og:title"]', 'content');
+ const [ogDescEl, origOgDesc] = snap('meta[property="og:description"]', 'content');
+ const [ogUrlEl, origOgUrl] = snap('meta[property="og:url"]', 'content');
+ const [twTitleEl, origTwTitle] = snap('meta[name="twitter:title"]', 'content');
+ const [twDescEl, origTwDesc] = snap('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('meta[name="description"]');
- if (descEl) descEl.setAttribute('content', meta.description);
+ const set = (selector: string, value: string) =>
+ document.querySelector(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('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]);