Skip to content

Commit bc185dc

Browse files
committed
feat: implementar navegación SPA con rutas limpias y redirección 404; actualizar enlaces en el menú
1 parent d6c195b commit bc185dc

6 files changed

Lines changed: 135 additions & 17 deletions

File tree

astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import tailwindcss from '@tailwindcss/vite';
55

66
// https://astro.build/config
77
export default defineConfig({
8+
// Configuración para SPA (Single Page Application)
9+
// Esto maneja correctamente las rutas client-side
10+
output: 'static',
11+
812
image: {
913
// Configuración de optimización de imágenes para Astro 5
1014
service: {

public/_redirects

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Redirects para SPA - todas las rutas apuntan a index.html
2+
# Esto permite que /skills, /projects, etc. funcionen correctamente
3+
# El código 200 hace un "rewrite" sin cambiar la URL
4+
5+
# Rutas de secciones
6+
/skills /index.html 200
7+
/projects /index.html 200
8+
/about-me /index.html 200
9+
/experience /index.html 200
10+
/contact /index.html 200
11+
/aptitudes /index.html 200
12+
/conocimientos /index.html 200
13+
/otras-tecnologias /index.html 200
14+
15+
# Catch-all para cualquier otra ruta (fallback)
16+
/* /index.html 200

src/components/Projects.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const { projects = [] } = Astro.props as { projects: Array<Project> };
2828
<div class="my-slider flex transition-transform duration-500 ease-in-out">
2929
{
3030
projects.map((p: Project) => (
31-
<article data-id={p.id} class="carousel-item shrink-0 w-full p-4">
31+
<article data-id={p.id} class="carousel-item shrink-0 w-full pt-4">
3232
<Image
3333
src={projectImages[p.image]}
3434
alt={p.title}

src/components/navbar/Navbar.astro

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,23 @@ const { menu = [] } = Astro.props;
149149
currentSectionId = sections[0].id;
150150
}
151151

152-
// Actualizamos clases
152+
// Actualizamos clases y URL
153153
navLinks.forEach((link) => {
154154
const href = link.getAttribute("href");
155155
if (!href) return;
156156

157-
const linkId = href.substring(1); // Quitar el '#'
157+
// Extraer ID desde la URL limpia: "/" -> "home", "/skills" -> "skills"
158+
const linkId = href === "/" ? "home" : href.substring(1);
158159

159160
if (linkId === currentSectionId) {
160161
link.classList.add(...activeClasses);
161-
link.setAttribute("aria-current", "page"); // Accesibilidad
162+
link.setAttribute("aria-current", "page");
163+
164+
// Actualizar URL del navegador solo si es diferente (sin recargar)
165+
const currentPath = window.location.pathname;
166+
if (currentPath !== href) {
167+
window.history.replaceState({ section: linkId }, "", href);
168+
}
162169
} else {
163170
link.classList.remove(...activeClasses);
164171
link.removeAttribute("aria-current");
@@ -190,31 +197,39 @@ const { menu = [] } = Astro.props;
190197
};
191198

192199
/**
193-
* Configura el Smooth Scroll para enlaces internos
200+
* Configura el Smooth Scroll para enlaces internos con URLs limpias (sin #)
201+
* Usa History API para mantener URLs profesionales
194202
*/
195203
const initSmoothScroll = (): void => {
204+
// Seleccionar enlaces internos (que empiezan con / y son de la misma página)
196205
document
197-
.querySelectorAll<HTMLAnchorElement>('a[href^="#"]')
206+
.querySelectorAll<HTMLAnchorElement>('nav a.nav-link')
198207
.forEach((anchor) => {
199208
anchor.addEventListener("click", function (e) {
200209
e.preventDefault();
201210
const href = this.getAttribute("href");
202211
if (!href) return;
203212

204-
const targetId = href.substring(1);
213+
// Extraer el ID de la sección desde la URL
214+
// "/" -> "home", "/skills" -> "skills"
215+
const targetId = href === "/" ? "home" : href.substring(1);
205216
const targetElement = document.getElementById(targetId);
206217

207218
if (targetElement) {
208-
const headerOffset = 80; // Ajustar según la altura real de tu navbar
219+
const headerOffset = 80;
209220
const elementPosition = targetElement.getBoundingClientRect().top;
210221
const offsetPosition =
211222
elementPosition + window.scrollY - headerOffset;
212223

224+
// Scroll suave a la sección
213225
window.scrollTo({
214226
top: offsetPosition,
215227
behavior: "smooth",
216228
});
217229

230+
// Actualizar URL usando History API
231+
window.history.pushState({ section: targetId }, "", href);
232+
218233
// Cerrar menú móvil si está abierto
219234
const collapseTarget = document.getElementById("navbar-sticky");
220235
if (
@@ -229,6 +244,59 @@ const { menu = [] } = Astro.props;
229244
});
230245
};
231246

247+
/**
248+
* Maneja la navegación con botones atrás/adelante del navegador
249+
*/
250+
const handleBrowserNavigation = (): void => {
251+
window.addEventListener("popstate", (event) => {
252+
if (event.state && event.state.section) {
253+
const targetElement = document.getElementById(event.state.section);
254+
if (targetElement) {
255+
const headerOffset = 80;
256+
const elementPosition = targetElement.getBoundingClientRect().top;
257+
const offsetPosition =
258+
elementPosition + window.scrollY - headerOffset;
259+
260+
window.scrollTo({
261+
top: offsetPosition,
262+
behavior: "smooth",
263+
});
264+
}
265+
} else {
266+
// Si no hay state, ir al inicio
267+
window.scrollTo({ top: 0, behavior: "smooth" });
268+
}
269+
});
270+
};
271+
272+
/**
273+
* Maneja la navegación inicial cuando se carga la página con una ruta específica
274+
* Ejemplo: si cargas /skills directamente, hace scroll a esa sección
275+
*/
276+
const handleInitialNavigation = (): void => {
277+
const currentPath = window.location.pathname;
278+
279+
// Si no es la raíz, intentar scroll a la sección correspondiente
280+
if (currentPath !== '/') {
281+
const sectionId = currentPath.substring(1); // /skills -> skills
282+
const targetElement = document.getElementById(sectionId);
283+
284+
if (targetElement) {
285+
// Pequeño delay para asegurar que el DOM está listo
286+
setTimeout(() => {
287+
const headerOffset = 80;
288+
const elementPosition = targetElement.getBoundingClientRect().top;
289+
const offsetPosition = elementPosition + window.scrollY - headerOffset;
290+
291+
window.scrollTo({
292+
top: offsetPosition,
293+
behavior: 'smooth',
294+
});
295+
}, 100);
296+
}
297+
}
298+
};
299+
232300
// --- INICIALIZACIÓN MAESTRA ---
233301

234302
const initNavbar = (): void => {
@@ -237,6 +305,8 @@ const { menu = [] } = Astro.props;
237305
handleScrollSpy();
238306
initMobileMenu();
239307
initSmoothScroll();
308+
handleBrowserNavigation();
309+
handleInitialNavigation();
240310

241311
// 2. Event Listener para Scroll (Optimizado con requestAnimationFrame)
242312
let ticking = false;

src/data/menu.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,49 @@
11
[
22
{
33
"id": "home",
4-
"href": "#home",
4+
"href": "/",
55
"label": "Home"
66
},
77
{
88
"id": "about-me",
9-
"href": "#about-me",
9+
"href": "/about-me",
1010
"label": "About Me"
1111
},
1212
{
1313
"id": "skills",
14-
"href": "#skills",
14+
"href": "/skills",
1515
"label": "Skills"
1616
},
1717
{
1818
"id": "experience",
19-
"href": "#experience",
19+
"href": "/experience",
2020
"label": "Experience",
2121
"items": [
2222
{
2323
"id": "aptitudes",
2424
"label": "Aptitudes",
25-
"href": "#aptitudes",
25+
"href": "/aptitudes",
2626
"current": false
2727
},
28-
{ "id": "conocimientos", "label": "Conocimientos", "href": "#conocimientos", "current": false },
28+
{ "id": "conocimientos", "label": "Conocimientos", "href": "/conocimientos", "current": false },
2929
{
3030
"id": "projects",
3131
"label": "Projects",
32-
"href": "#projects",
32+
"href": "/projects",
3333
"current": false
3434
},
3535
{
3636
"id": "otras-tecnologias",
3737
"label": "Otras Tecnologías",
38-
"href": "#otras-tecnologias",
38+
"href": "/otras-tecnologias",
3939
"current": false,
4040
"divider": true
4141
}
4242
]
4343
},
4444
{
4545
"id": "contact",
46-
"href": "#contact",
46+
"href": "/contact",
4747
"label": "Contact",
4848
"current": false
4949
}

src/pages/404.astro

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
// Esta página se muestra cuando hay un 404
3+
// Redirige automáticamente al index manteniendo el path para client-side routing
4+
---
5+
6+
<!DOCTYPE html>
7+
<html lang="es">
8+
<head>
9+
<meta charset="UTF-8">
10+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
11+
<title>Cargando...</title>
12+
<script>
13+
// Redirección inmediata al index manteniendo el path
14+
// Esto permite que el client-side routing maneje la navegación
15+
const currentPath = window.location.pathname;
16+
const search = window.location.search;
17+
const hash = window.location.hash;
18+
19+
// Redirigir al index con el path como state
20+
window.location.replace('/' + search + hash);
21+
</script>
22+
</head>
23+
<body>
24+
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: system-ui;">
25+
<p>Cargando...</p>
26+
</div>
27+
</body>
28+
</html>

0 commit comments

Comments
 (0)