Chapter 06: Routing and Navigation System
Haiyue
18min
Learning Objectives
- Deeply understand Astro’s file-system routing
- Implement dynamic routing and nested routing
- Build navigation components and breadcrumb systems
- Optimize page transitions and loading performance
Knowledge Points
Astro Routing System
Astro uses a file-system-based routing mechanism with the following features:
- Zero-config routing: File paths directly map to URL paths
- Static-first: Generates static pages by default, supports dynamic routing
- Nested routing: Implement route nesting through folder structure
- Flexible matching: Support dynamic parameters and catch-all routes
Routing Hierarchy Structure
🔄 正在渲染 Mermaid 图表...
Navigation Performance Optimization
- Prefetch strategy: Intelligently preload linked resources
- View transitions: Smooth page transition animations
- Route caching: Client-side route state management
- Code splitting: Split JavaScript bundles by route
File System Routing
Basic Route Structure
src/pages/
├── index.astro → /
├── about.astro → /about
├── contact.astro → /contact
├── blog/
│ ├── index.astro → /blog
│ ├── [slug].astro → /blog/:slug
│ └── category/
│ ├── index.astro → /blog/category
│ └── [name].astro → /blog/category/:name
├── products/
│ ├── [id].astro → /products/:id
│ └── [...slug].astro → /products/* (catch-all)
└── api/
├── posts.json.ts → /api/posts.json
└── users/
└── [id].json.ts → /api/users/:id.json
Dynamic Route Parameters
---
// src/pages/blog/[slug].astro
// Get route parameters via Astro.params
const { slug } = Astro.params;
// Get complete URL info via Astro.url
const url = Astro.url;
const searchParams = url.searchParams;
// Example: /blog/my-post?category=tech&featured=true
console.log(slug); // "my-post"
console.log(searchParams.get('category')); // "tech"
console.log(searchParams.get('featured')); // "true"
---
<h1>Post: {slug}</h1>
<p>Current URL: {url.pathname}</p>
Nested Route Layout
---
// src/layouts/BlogLayout.astro
export interface Props {
frontmatter?: {
title?: string;
description?: string;
};
}
const { frontmatter } = Astro.props;
---
<html>
<head>
<title>{frontmatter?.title || 'Blog'}</title>
<meta name="description" content={frontmatter?.description || ''} />
</head>
<body>
<div class="blog-layout">
<!-- Blog navigation -->
<nav class="blog-nav">
<a href="/blog">All Posts</a>
<a href="/blog/category">Categories</a>
<a href="/blog/tags">Tags</a>
<a href="/blog/archive">Archive</a>
</nav>
<!-- Main content area -->
<main class="blog-content">
<slot />
</main>
<!-- Sidebar -->
<aside class="blog-sidebar">
<div class="recent-posts">
<h3>Recent Posts</h3>
<!-- Recent posts list -->
</div>
<div class="categories">
<h3>Categories</h3>
<!-- Category list -->
</div>
</aside>
</div>
</body>
</html>
Catch-all Routes
---
// src/pages/docs/[...slug].astro
export async function getStaticPaths() {
// Generate all possible documentation paths
const docs = [
{ slug: 'getting-started' },
{ slug: 'api/introduction' },
{ slug: 'api/authentication' },
{ slug: 'guides/deployment' },
{ slug: 'guides/best-practices' },
];
return docs.map(doc => ({
params: { slug: doc.slug },
props: { doc }
}));
}
const { slug } = Astro.params;
const { doc } = Astro.props;
// slug might be a nested path like "api/introduction"
const pathSegments = slug.split('/');
const section = pathSegments[0]; // "api"
const page = pathSegments[1]; // "introduction"
---
<div class="docs-page">
<!-- Breadcrumb navigation -->
<nav class="breadcrumb">
<a href="/docs">Documentation</a>
{pathSegments.map((segment, index) => {
const path = pathSegments.slice(0, index + 1).join('/');
return (
<>
<span>/</span>
<a href={`/docs/${path}`}>{segment}</a>
</>
);
})}
</nav>
<!-- Documentation content -->
<article class="doc-content">
<h1>{doc.title}</h1>
<!-- Documentation content -->
</article>
</div>
Navigation Component Development
Smart Navigation Menu
---
// src/components/Navigation.astro
interface NavItem {
label: string;
href: string;
children?: NavItem[];
icon?: string;
}
const navItems: NavItem[] = [
{ label: 'Home', href: '/', icon: 'home' },
{
label: 'Blog',
href: '/blog',
icon: 'blog',
children: [
{ label: 'All Posts', href: '/blog' },
{ label: 'Tech Sharing', href: '/blog/category/tech' },
{ label: 'Life Notes', href: '/blog/category/life' },
]
},
{
label: 'Projects',
href: '/projects',
icon: 'projects',
children: [
{ label: 'Open Source', href: '/projects/open-source' },
{ label: 'Personal Work', href: '/projects/personal' },
]
},
{ label: 'About', href: '/about', icon: 'about' },
{ label: 'Contact', href: '/contact', icon: 'contact' },
];
// Get current page path
const currentPath = Astro.url.pathname;
// Check if it's the current page or subpage
function isActive(href: string): boolean {
if (href === '/') {
return currentPath === '/';
}
return currentPath.startsWith(href);
}
// Check if has active child
function hasActiveChild(item: NavItem): boolean {
if (!item.children) return false;
return item.children.some(child => isActive(child.href));
}
---
<nav class="main-navigation">
<div class="nav-container">
<!-- Logo -->
<div class="nav-logo">
<a href="/">
<img src="/logo.svg" alt="Website Logo" />
</a>
</div>
<!-- Navigation menu -->
<ul class="nav-menu">
{navItems.map((item) => (
<li class={`nav-item ${isActive(item.href) || hasActiveChild(item) ? 'active' : ''}`}>
<a
href={item.href}
class={`nav-link ${isActive(item.href) ? 'current' : ''}`}
>
{item.icon && <i class={`icon-${item.icon}`}></i>}
<span>{item.label}</span>
{item.children && <i class="icon-arrow-down"></i>}
</a>
<!-- Submenu -->
{item.children && (
<ul class="sub-menu">
{item.children.map((child) => (
<li class="sub-item">
<a
href={child.href}
class={`sub-link ${isActive(child.href) ? 'current' : ''}`}
>
{child.label}
</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
<!-- Mobile menu toggle -->
<button class="mobile-menu-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
</nav>
<style>
.main-navigation {
@apply bg-white shadow-sm border-b sticky top-0 z-50;
}
.nav-container {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
@apply flex items-center justify-between h-16;
}
.nav-logo img {
@apply h-8 w-auto;
}
.nav-menu {
@apply hidden md:flex space-x-8;
}
.nav-item {
@apply relative;
}
.nav-link {
@apply flex items-center space-x-1 px-3 py-2 rounded-md;
@apply text-gray-600 hover:text-gray-900 transition-colors;
}
.nav-link.current {
@apply text-blue-600 font-medium;
}
.nav-item.active > .nav-link {
@apply text-blue-600;
}
.sub-menu {
@apply absolute left-0 mt-2 w-48 bg-white shadow-lg rounded-md py-1;
@apply opacity-0 invisible transform translate-y-1 transition-all;
}
.nav-item:hover .sub-menu {
@apply opacity-100 visible translate-y-0;
}
.sub-link {
@apply block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100;
}
.sub-link.current {
@apply text-blue-600 bg-blue-50;
}
.mobile-menu-toggle {
@apply md:hidden flex flex-col justify-center items-center w-6 h-6 space-y-1;
}
.mobile-menu-toggle span {
@apply block w-6 h-0.5 bg-gray-600 transition-all;
}
</style>
Breadcrumb Navigation
---
// src/components/Breadcrumb.astro
export interface Props {
items?: Array<{ label: string; href: string }>;
}
const { items } = Astro.props;
// If items not provided, generate from current path
const breadcrumbItems = items || generateBreadcrumb(Astro.url.pathname);
function generateBreadcrumb(pathname: string) {
const segments = pathname.split('/').filter(Boolean);
const breadcrumb = [{ label: 'Home', href: '/' }];
let currentPath = '';
segments.forEach((segment, index) => {
currentPath += `/${segment}`;
// Convert path segment to readable label
const label = segment
.replace(/-/g, ' ')
.replace(/^\w/, c => c.toUpperCase());
breadcrumb.push({
label,
href: currentPath
});
});
return breadcrumb;
}
---
{breadcrumbItems.length > 1 && (
<nav class="breadcrumb" aria-label="Breadcrumb navigation">
<ol class="breadcrumb-list">
{breadcrumbItems.map((item, index) => (
<li class="breadcrumb-item">
{index === breadcrumbItems.length - 1 ? (
<span class="breadcrumb-current" aria-current="page">
{item.label}
</span>
) : (
<>
<a href={item.href} class="breadcrumb-link">
{item.label}
</a>
<span class="breadcrumb-separator" aria-hidden="true">
/
</span>
</>
)}
</li>
))}
</ol>
</nav>
)}
<style>
.breadcrumb {
@apply py-3 text-sm;
}
.breadcrumb-list {
@apply flex items-center space-x-1;
}
.breadcrumb-item {
@apply flex items-center;
}
.breadcrumb-link {
@apply text-gray-500 hover:text-gray-700 transition-colors;
}
.breadcrumb-current {
@apply text-gray-900 font-medium;
}
.breadcrumb-separator {
@apply text-gray-400 mx-2;
}
</style>
Pagination Navigation
---
// src/components/Pagination.astro
export interface Props {
currentPage: number;
totalPages: number;
baseUrl: string;
showFirstLast?: boolean;
showPrevNext?: boolean;
maxVisible?: number;
}
const {
currentPage,
totalPages,
baseUrl,
showFirstLast = true,
showPrevNext = true,
maxVisible = 5
} = Astro.props;
// Calculate visible page range
function getVisiblePages(current: number, total: number, max: number) {
if (total <= max) {
return Array.from({ length: total }, (_, i) => i + 1);
}
const half = Math.floor(max / 2);
let start = Math.max(1, current - half);
let end = Math.min(total, start + max - 1);
if (end - start + 1 < max) {
start = Math.max(1, end - max + 1);
}
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
const visiblePages = getVisiblePages(currentPage, totalPages, maxVisible);
function getPageUrl(page: number): string {
if (page === 1) {
return baseUrl;
}
return `${baseUrl}/page/${page}`;
}
---
{totalPages > 1 && (
<nav class="pagination" aria-label="Pagination navigation">
<div class="pagination-info">
Page {currentPage} of {totalPages}
</div>
<ul class="pagination-list">
<!-- First page -->
{showFirstLast && currentPage > 1 && (
<li>
<a href={getPageUrl(1)} class="pagination-link">
<span class="sr-only">First</span>
<i class="icon-first"></i>
</a>
</li>
)}
<!-- Previous page -->
{showPrevNext && currentPage > 1 && (
<li>
<a href={getPageUrl(currentPage - 1)} class="pagination-link">
<span class="sr-only">Previous</span>
<i class="icon-prev"></i>
</a>
</li>
)}
<!-- Page numbers -->
{visiblePages.map((page) => (
<li>
{page === currentPage ? (
<span class="pagination-current" aria-current="page">
{page}
</span>
) : (
<a href={getPageUrl(page)} class="pagination-link">
{page}
</a>
)}
</li>
))}
<!-- Next page -->
{showPrevNext && currentPage < totalPages && (
<li>
<a href={getPageUrl(currentPage + 1)} class="pagination-link">
<span class="sr-only">Next</span>
<i class="icon-next"></i>
</a>
</li>
)}
<!-- Last page -->
{showFirstLast && currentPage < totalPages && (
<li>
<a href={getPageUrl(totalPages)} class="pagination-link">
<span class="sr-only">Last</span>
<i class="icon-last"></i>
</a>
</li>
)}
</ul>
</nav>
)}
<style>
.pagination {
@apply flex flex-col sm:flex-row items-center justify-between gap-4 py-6;
}
.pagination-info {
@apply text-sm text-gray-600;
}
.pagination-list {
@apply flex items-center space-x-1;
}
.pagination-link {
@apply flex items-center justify-center w-10 h-10 rounded-md;
@apply text-gray-600 hover:text-gray-900 hover:bg-gray-100;
@apply transition-colors duration-200;
}
.pagination-current {
@apply flex items-center justify-center w-10 h-10 rounded-md;
@apply bg-blue-600 text-white font-medium;
}
.sr-only {
@apply sr-only;
}
</style>
View Transition Animations
Enable View Transitions
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
Custom Transition Animations
---
// src/components/PageTransition.astro
import { fade, slide } from 'astro:transitions';
---
<div transition:animate={fade({ duration: '0.3s' })}>
<slot />
</div>
<style>
/* Custom transition styles */
::view-transition-old(root) {
animation: slideOut 0.3s ease-out forwards;
}
::view-transition-new(root) {
animation: slideIn 0.3s ease-out;
}
@keyframes slideOut {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
</style>
Conditional Transitions
---
// src/pages/blog/[slug].astro
// Select different transitions based on source page
const referrer = Astro.request.headers.get('referer');
const fromBlogIndex = referrer?.includes('/blog');
const transitionName = fromBlogIndex ? 'blog-detail' : 'slide';
---
<div transition:name={transitionName}>
<article>
<!-- Article content -->
</article>
</div>
Route Performance Optimization
Smart Prefetch
---
// src/components/SmartLink.astro
export interface Props {
href: string;
prefetch?: 'hover' | 'visible' | 'load' | false;
class?: string;
}
const { href, prefetch = 'hover', class: className, ...rest } = Astro.props;
---
<a
href={href}
class={className}
data-astro-prefetch={prefetch}
{...rest}
>
<slot />
</a>
<script>
// Custom prefetch logic
document.addEventListener('DOMContentLoaded', () => {
const links = document.querySelectorAll('a[data-astro-prefetch="hover"]');
links.forEach(link => {
let timeout: number;
link.addEventListener('mouseenter', () => {
timeout = setTimeout(() => {
const href = link.getAttribute('href');
if (href && !href.startsWith('#')) {
// Prefetch page resources
fetch(href, { method: 'HEAD' });
}
}, 100);
});
link.addEventListener('mouseleave', () => {
clearTimeout(timeout);
});
});
});
</script>
Router Cache Strategy
// src/lib/router-cache.ts
interface CacheEntry {
html: string;
timestamp: number;
etag?: string;
}
class RouterCache {
private cache = new Map<string, CacheEntry>();
private maxAge = 5 * 60 * 1000; // 5 minutes
async get(url: string): Promise<string | null> {
const entry = this.cache.get(url);
if (!entry) return null;
// Check if expired
if (Date.now() - entry.timestamp > this.maxAge) {
this.cache.delete(url);
return null;
}
return entry.html;
}
set(url: string, html: string, etag?: string): void {
this.cache.set(url, {
html,
timestamp: Date.now(),
etag
});
// Limit cache size
if (this.cache.size > 50) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
clear(): void {
this.cache.clear();
}
}
export const routerCache = new RouterCache();
Key Takeaways
- File System Routing: Leverage file structure to automatically generate routes, zero-config development
- Dynamic Parameters: Support dynamic routing through
[param]syntax - Nested Layouts: Use layout components to reuse page structures
- Navigation State: Accurately determine current page state, provide good user experience
- Performance Optimization: Use prefetch and cache strategies wisely
Considerations
- Dynamic routes require pre-generating all possible paths via
getStaticPaths - View transitions only work in supported browsers, provide fallback solutions
- Balance prefetch strategy between user experience and server load
- Consider i18n and dynamic content scenarios for breadcrumbs and navigation state