feat: enhance routing with dedicated error pages, improve navigation handling, and add internal path checks

This commit is contained in:
2026-05-06 13:37:28 +02:00
parent 11c299310c
commit 575e2b4779
6 changed files with 88 additions and 47 deletions
+15 -5
View File
@@ -8,7 +8,7 @@ Sito web moderno e responsivo sviluppato con **Svelte + Vite** per presentare se
- 📱 **Design completamente responsivo** - 📱 **Design completamente responsivo**
- ⬆️ **Navbar collapsibile** con hide/show al scroll - ⬆️ **Navbar collapsibile** con hide/show al scroll
- 🎨 **Sistema di componenti riusabili** - 🎨 **Sistema di componenti riusabili**
- 🚀 **Routing client-side** con pagine di errore dedicate - 🚀 **Routing client-side** con pagine dedicate e fallback 404
-**Accessibilità** e buone pratiche web -**Accessibilità** e buone pratiche web
## 🛠️ Requisiti ## 🛠️ Requisiti
@@ -58,16 +58,19 @@ src/
## 🗺️ Routing ## 🗺️ Routing
Il routing è gestito in `src/App.svelte` tramite `window.location.pathname`. Il routing è gestito in `src/App.svelte` leggendo `window.location.pathname` e reagendo ai cambi di History API.
| Route | Componente | Descrizione | | Route | Componente | Descrizione |
|-------|-----------|-------------| |-------|-----------|-------------|
| `/` | `Hero` + `Services` | Home page | | `/` | `Hero` + `Services` | Home page |
| `/contatti`, `/contacts` | `Contacts` | Pagina contatti | | `/contatti`, `/contacts` | `Contacts` | Pagina contatti |
| `/403` | `Forbidden` | Accesso negato | | `/403` | `Forbidden` | Accesso negato |
| `/404` | `NotFound` | Pagina non trovata |
| `/500` | `ServerError` | Errore server | | `/500` | `ServerError` | Errore server |
| `/errore` | `ErrorGeneric` | Errore generico | | `/errore` | `ErrorGeneric` | Errore generico |
| `/*` | `NotFound` | Pagina non trovata (404) | | qualsiasi altra route | `NotFound` | Fallback 404 per pagine non ancora create |
Le voci della navbar puntano a route reali come `/servizi`, `/metodo`, `/progetti` e `/chi-siamo`: se una pagina non esiste ancora, il contenuto mostrato è il 404 ma l'URL resta quello richiesto.
## 🎨 Tema e Stili ## 🎨 Tema e Stili
@@ -141,19 +144,24 @@ Componente button/link riusabile con tema integrato.
- `href` - se presente, renderizza come `<a>` tag - `href` - se presente, renderizza come `<a>` tag
- `borderWidth` - spessore bordo interno (default: `'0px'`) - `borderWidth` - spessore bordo interno (default: `'0px'`)
Se `href` punta a una route interna, il click usa la navigazione client-side e aggiorna il contenuto senza ricaricare la pagina.
### Header ### Header
Navbar sticky che si nascondi al scroll verso il basso e riappare al scroll verso l'alto. Navbar sticky che si nasconde al scroll verso il basso e riappare al scroll verso l'alto.
Le voci della navbar usano route interne. Le pagine non ancora create devono restituire 404.
Modifica il contenuto in `src/components/Header/Header.svelte`. Modifica il contenuto in `src/components/Header/Header.svelte`.
### ThemeToggle ### ThemeToggle
Pulsante per switchare tra tema light e dark. Il tema viene salvato in `localStorage`. Pulsante per switchare tra tema light e dark. Il tema viene salvato in `localStorage` e come backup in cookie, così resta disponibile anche dopo operazioni che puliscono parte dello storage del browser.
## 📱 Modificare i contenuti ## 📱 Modificare i contenuti
- **Header/Navigazione:** `src/components/Header/Header.svelte` - **Header/Navigazione:** `src/components/Header/Header.svelte`
- **Routing e redirect:** `src/App.svelte`, `src/lib/navigation.js`, `src/lib/errorRouting.js`
- **Sezione hero:** `src/components/Hero/Hero.svelte` - **Sezione hero:** `src/components/Hero/Hero.svelte`
- **Servizi:** `src/components/Services/Services.svelte` - **Servizi:** `src/components/Services/Services.svelte`
- **Footer:** `src/components/Footer/Footer.svelte` - **Footer:** `src/components/Footer/Footer.svelte`
@@ -181,6 +189,8 @@ const getRoute = (path) => {
<MyPage /> <MyPage />
``` ```
Se non aggiungi la route in `getRoute`, il path continuerà a mostrare il fallback 404.
## 🚀 Build e Deploy ## 🚀 Build e Deploy
### Build di produzione ### Build di produzione
+3 -18
View File
@@ -13,8 +13,9 @@
import { initializeStoredConsent } from "./lib/cookieConsent"; import { initializeStoredConsent } from "./lib/cookieConsent";
import { applyTheme, getSavedTheme } from "./lib/theme"; import { applyTheme, getSavedTheme } from "./lib/theme";
import { getErrorPath, resolvePathname } from "./lib/errorRouting"; import { getErrorPath, resolvePathname } from "./lib/errorRouting";
import { navigateTo } from "./lib/navigation";
let pathname = typeof window !== 'undefined' ? window.location.pathname : '/' let pathname = $state(typeof window !== 'undefined' ? window.location.pathname : '/')
const getRoute = (path) => { const getRoute = (path) => {
const normalized = resolvePathname(path) const normalized = resolvePathname(path)
@@ -25,28 +26,12 @@
if (normalized === '/404') return '404' if (normalized === '/404') return '404'
if (normalized === '/500') return '500' if (normalized === '/500') return '500'
if (normalized === '/errore') return 'error' if (normalized === '/errore') return 'error'
return 'error' return '404'
} }
let currentRoute = $derived(getRoute(pathname)) let currentRoute = $derived(getRoute(pathname))
let isCenteredMain = $derived(['403', '404', '500', 'error'].includes(currentRoute)) let isCenteredMain = $derived(['403', '404', '500', 'error'].includes(currentRoute))
const navigateTo = (targetPath, replace = false) => {
const resolved = resolvePathname(targetPath)
const current = resolvePathname(window.location.pathname)
if (resolved === current) {
pathname = resolved
return
}
if (replace) {
window.history.replaceState({}, '', resolved)
} else {
window.history.pushState({}, '', resolved)
}
pathname = resolved
}
const syncRouteFromLocation = () => { const syncRouteFromLocation = () => {
const resolved = resolvePathname(window.location.pathname) const resolved = resolvePathname(window.location.pathname)
if (resolved !== window.location.pathname) { if (resolved !== window.location.pathname) {
+10
View File
@@ -1,5 +1,6 @@
<script> <script>
import './Button.css' import './Button.css'
import { isInternalPath, navigateTo } from '../../lib/navigation'
let { let {
color = 'var(--primary-color)', color = 'var(--primary-color)',
@@ -11,6 +12,14 @@
borderWidth = '0px', borderWidth = '0px',
...restProps ...restProps
} = $props() } = $props()
function handleClick(event) {
if (!href || !isInternalPath(href)) return
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
event.preventDefault()
navigateTo(href)
}
</script> </script>
{#if href} {#if href}
@@ -18,6 +27,7 @@
class="button" class="button"
style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`} style={`--button-color: ${color}; --button-text-color: ${textColor}; --button-radius: ${round}; --button-padding: ${padding}; --button-border-width: ${borderWidth};`}
{href} {href}
on:click={handleClick}
{...restProps} {...restProps}
> >
{text} {text}
+33 -22
View File
@@ -2,16 +2,21 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import './Header.css' import './Header.css'
import Button from '../Button/Button.svelte' import Button from '../Button/Button.svelte'
import { navigateTo, isInternalPath } from '../../lib/navigation'
let activeNav = 'home' let activeNav = $state(typeof window !== 'undefined' ? window.location.pathname : '/')
let isHidden = false let isHidden = $state(false)
let lastScrollY = 0 let lastScrollY = 0
const SCROLL_DELTA = 8 const SCROLL_DELTA = 8
const TOP_OFFSET = 24 const TOP_OFFSET = 24
function navigate(section) { function handleNavClick(event, path) {
activeNav = section if (!isInternalPath(path)) return
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
event.preventDefault()
navigateTo(path)
} }
function handleScroll() { function handleScroll() {
@@ -33,8 +38,14 @@
lastScrollY = window.scrollY || 0 lastScrollY = window.scrollY || 0
window.addEventListener('scroll', handleScroll, { passive: true }) window.addEventListener('scroll', handleScroll, { passive: true })
const updateActiveNav = () => {
activeNav = window.location.pathname
}
window.addEventListener('popstate', updateActiveNav)
return () => { return () => {
window.removeEventListener('scroll', handleScroll) window.removeEventListener('scroll', handleScroll)
window.removeEventListener('popstate', updateActiveNav)
} }
}) })
</script> </script>
@@ -42,46 +53,46 @@
<header class="header" class:hidden={isHidden} id="home"> <header class="header" class:hidden={isHidden} id="home">
<div class="header-container"> <div class="header-container">
<div class="logo"> <div class="logo">
<a href="/" aria-label="CIMA PROGETTI home"> <a href="/" aria-label="CIMA PROGETTI home" on:click={(e) => handleNavClick(e, '/')}>
<span class="logo-img logo-mask" aria-hidden="true"></span> <span class="logo-img logo-mask" aria-hidden="true"></span>
</a> </a>
</div> </div>
<nav class="nav"> <nav class="nav">
<a <a
href="#services" href="/servizi"
class:active={activeNav === 'services'} class:active={activeNav === '/servizi'}
on:click|preventDefault={() => navigate('services')} on:click={(e) => handleNavClick(e, '/servizi')}
aria-current={activeNav === 'services' ? 'page' : undefined} aria-current={activeNav === '/servizi' ? 'page' : undefined}
> >
Servizi Servizi
</a> </a>
<a <a
href="#method" href="/metodo"
class:active={activeNav === 'method'} class:active={activeNav === '/metodo'}
on:click|preventDefault={() => navigate('method')} on:click={(e) => handleNavClick(e, '/metodo')}
aria-current={activeNav === 'method' ? 'page' : undefined} aria-current={activeNav === '/metodo' ? 'page' : undefined}
> >
Il metodo Il metodo
</a> </a>
<a <a
href="#projects" href="/progetti"
class:active={activeNav === 'projects'} class:active={activeNav === '/progetti'}
on:click|preventDefault={() => navigate('projects')} on:click={(e) => handleNavClick(e, '/progetti')}
aria-current={activeNav === 'projects' ? 'page' : undefined} aria-current={activeNav === '/progetti' ? 'page' : undefined}
> >
Progetti Progetti
</a> </a>
<a <a
href="#us" href="/chi-siamo"
class:active={activeNav === 'us'} class:active={activeNav === '/chi-siamo'}
on:click|preventDefault={() => navigate('us')} on:click={(e) => handleNavClick(e, '/chi-siamo')}
aria-current={activeNav === 'us' ? 'page' : undefined} aria-current={activeNav === '/chi-siamo' ? 'page' : undefined}
> >
Chi Siamo Chi Siamo
</a> </a>
</nav> </nav>
<div class="header-actions"> <div class="header-actions">
<Button text="Contattaci" href="#contact" /> <Button text="Contattaci" href="/contatti" />
</div> </div>
</div> </div>
</header> </header>
+3 -2
View File
@@ -25,8 +25,9 @@ export const resolvePathname = (path) => {
const normalized = normalizePathname(path) const normalized = normalizePathname(path)
const alias = PATH_ALIASES[normalized] const alias = PATH_ALIASES[normalized]
if (alias) return alias if (alias) return alias
if (KNOWN_PATHS.has(normalized)) return normalized // Non forziamo un "/404" per preservare l'URL inserito dall'utente.
return '/404' // Sarà getRoute() in App.svelte a mappare le route non conosciute sul componente 404
return normalized
} }
export const getErrorPath = (statusOrCode) => { export const getErrorPath = (statusOrCode) => {
+24
View File
@@ -0,0 +1,24 @@
export const isInternalPath = (path) => {
if (!path) return false;
// Percorsi relativi o assoluti interni (es. /, /contatti) ma non ancore interne (#) per le quali basta l'HTML normale
if (path.startsWith('/') && !path.startsWith('//')) return true;
try {
const url = new URL(path, window.location.origin);
return url.origin === window.location.origin;
} catch {
return false;
}
};
export const navigateTo = (path, replace = false) => {
if (typeof window === 'undefined') return;
if (replace) {
window.history.replaceState({}, '', path);
} else {
window.history.pushState({}, '', path);
}
// Avvisa l'applicazione (App.svelte) che l'URL è cambiato
window.dispatchEvent(new Event('popstate'));
};