feat: enhance routing with dedicated error pages, improve navigation handling, and add internal path checks
This commit is contained in:
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user