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]);