Chapter 05: Data Fetching and Content Management

Haiyue
18min
Learning Objectives
  • Master Astro’s data fetching patterns (getStaticPaths, Astro.props)
  • Integrate CMS and API data sources
  • Implement Markdown and MDX content processing
  • Learn to use Content Collections

Knowledge Points

Astro Data Fetching Mechanism

Astro provides multiple data fetching methods to suit different application scenarios:

  • Build-time data fetching: Fetch data during the build phase to generate static pages
  • getStaticPaths: Pre-fetch data for dynamic routes
  • Content Collections: Type-safe content management system
  • API integration: Seamless connection with external data sources

Data Flow Architecture

🔄 正在渲染 Mermaid 图表...

Content Management Strategies

  • Local content: Markdown/MDX file management
  • Headless CMS: Strapi, Contentful, Sanity, etc.
  • Database integration: PostgreSQL, MongoDB, etc.
  • API aggregation: Multi-source data integration

Static Path Generation

getStaticPaths Basic Usage

---
// src/pages/blog/[slug].astro

export async function getStaticPaths() {
  // Fetch blog post data
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json());

  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post } = Astro.props;
---

<html>
<head>
  <title>{post.title}</title>
  <meta name="description" content={post.excerpt} />
</head>
<body>
  <article>
    <h1>{post.title}</h1>
    <time>{new Date(post.publishDate).toLocaleDateString()}</time>
    <div set:html={post.content} />
  </article>
</body>
</html>

Pagination Data Processing

---
// src/pages/blog/page/[page].astro

export async function getStaticPaths({ paginate }) {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json());

  // Use built-in pagination feature
  return paginate(posts, { pageSize: 6 });
}

const { page } = Astro.props;
---

<div class="blog-container">
  <div class="posts-grid">
    {page.data.map((post) => (
      <article class="post-card">
        <h2>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
        </h2>
        <p>{post.excerpt}</p>
        <time>{new Date(post.publishDate).toLocaleDateString()}</time>
      </article>
    ))}
  </div>

  <!-- Pagination navigation -->
  <nav class="pagination">
    {page.url.prev && (
      <a href={page.url.prev} class="pagination-link">← Previous</a>
    )}

    <span class="pagination-info">
      Page {page.currentPage} of {page.lastPage}
    </span>

    {page.url.next && (
      <a href={page.url.next} class="pagination-link">Next →</a>
    )}
  </nav>
</div>

Nested Dynamic Routes

---
// src/pages/category/[category]/[slug].astro

export async function getStaticPaths() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json());

  return posts.map((post) => ({
    params: {
      category: post.category.slug,
      slug: post.slug
    },
    props: {
      post,
      category: post.category
    }
  }));
}

const { post, category } = Astro.props;
---

<div class="post-container">
  <!-- Breadcrumb navigation -->
  <nav class="breadcrumb">
    <a href="/">Home</a>
    <span>/</span>
    <a href={`/category/${category.slug}`}>{category.name}</a>
    <span>/</span>
    <span>{post.title}</span>
  </nav>

  <article class="post-content">
    <header>
      <h1>{post.title}</h1>
      <div class="post-meta">
        <span class="category">
          <a href={`/category/${category.slug}`}>{category.name}</a>
        </span>
        <time>{new Date(post.publishDate).toLocaleDateString()}</time>
      </div>
    </header>

    <div class="content" set:html={post.content} />
  </article>
</div>

Content Collections

Configuring Content Collections

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.date(),
    author: z.string(),
    tags: z.array(z.string()),
    featured: z.boolean().default(false),
    draft: z.boolean().default(false),
    heroImage: z.string().optional(),
  }),
});

const projectCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    technology: z.array(z.string()),
    github: z.string().url(),
    demo: z.string().url().optional(),
    featured: z.boolean().default(false),
    completedDate: z.date(),
  }),
});

export const collections = {
  'blog': blogCollection,
  'projects': projectCollection,
};

Using Content Collections

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BlogCard from '../../components/BlogCard.astro';

// Get all blog posts
const allPosts = await getCollection('blog');

// Filter and sort
const publishedPosts = allPosts
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf());

// Get featured posts
const featuredPosts = publishedPosts.filter(post => post.data.featured);
---

<div class="blog-index">
  <!-- Featured posts section -->
  {featuredPosts.length > 0 && (
    <section class="featured-posts">
      <h2>Featured Posts</h2>
      <div class="featured-grid">
        {featuredPosts.slice(0, 3).map((post) => (
          <BlogCard post={post} featured={true} />
        ))}
      </div>
    </section>
  )}

  <!-- Latest posts -->
  <section class="recent-posts">
    <h2>Latest Posts</h2>
    <div class="posts-grid">
      {publishedPosts.map((post) => (
        <BlogCard post={post} />
      ))}
    </div>
  </section>
</div>

Dynamic Blog Pages

---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');

  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}

const post = Astro.props;
const { Content } = await post.render();
---

<BlogLayout frontmatter={post.data}>
  <article class="prose prose-lg max-w-4xl mx-auto">
    <header class="post-header">
      <h1>{post.data.title}</h1>

      <div class="post-meta">
        <time>{post.data.publishDate.toLocaleDateString('en-US')}</time>
        <span>Author: {post.data.author}</span>
      </div>

      {post.data.heroImage && (
        <img
          src={post.data.heroImage}
          alt={post.data.title}
          class="hero-image"
        />
      )}

      <p class="post-description">{post.data.description}</p>

      <!-- Tags -->
      <div class="post-tags">
        {post.data.tags.map((tag) => (
          <a href={`/tags/${tag}`} class="tag">#{tag}</a>
        ))}
      </div>
    </header>

    <!-- Article content -->
    <div class="post-content">
      <Content />
    </div>
  </article>
</BlogLayout>

External Data Source Integration

REST API Integration

// src/lib/api.ts

interface Post {
  id: number;
  title: string;
  content: string;
  slug: string;
  publishDate: string;
  author: {
    name: string;
    avatar: string;
  };
  categories: string[];
}

export class BlogAPI {
  private baseURL = 'https://api.yourblog.com';

  async getPosts(page = 1, limit = 10): Promise<Post[]> {
    const response = await fetch(
      `${this.baseURL}/posts?page=${page}&limit=${limit}`
    );

    if (!response.ok) {
      throw new Error(`Failed to fetch posts: ${response.statusText}`);
    }

    return response.json();
  }

  async getPost(slug: string): Promise<Post> {
    const response = await fetch(`${this.baseURL}/posts/${slug}`);

    if (!response.ok) {
      throw new Error(`Failed to fetch post: ${response.statusText}`);
    }

    return response.json();
  }

  async getCategories(): Promise<string[]> {
    const response = await fetch(`${this.baseURL}/categories`);
    return response.json();
  }
}

export const blogAPI = new BlogAPI();

GraphQL Integration

// src/lib/graphql.ts

interface GraphQLResponse<T> {
  data: T;
  errors?: Array<{ message: string }>;
}

export class GraphQLClient {
  constructor(private endpoint: string) {}

  async query<T>(query: string, variables?: Record<string, any>): Promise<T> {
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query,
        variables,
      }),
    });

    const result: GraphQLResponse<T> = await response.json();

    if (result.errors) {
      throw new Error(result.errors[0].message);
    }

    return result.data;
  }
}

// Usage example
const client = new GraphQLClient('https://api.contentful.com/graphql');

export async function fetchBlogPosts() {
  const query = `
    query GetBlogPosts($limit: Int) {
      blogPostCollection(limit: $limit) {
        items {
          title
          slug
          content
          publishDate
          author {
            name
            bio
          }
          featuredImage {
            url
            description
          }
        }
      }
    }
  `;

  return client.query(query, { limit: 10 });
}

CMS Integration Example

---
// src/pages/cms-blog/[slug].astro
import { fetchBlogPosts, fetchBlogPost } from '../../lib/contentful';

export async function getStaticPaths() {
  const posts = await fetchBlogPosts();

  return posts.items.map((post) => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post } = Astro.props;
---

<html>
<head>
  <title>{post.title}</title>
  <meta name="description" content={post.excerpt} />

  <!-- SEO metadata -->
  <meta property="og:title" content={post.title} />
  <meta property="og:description" content={post.excerpt} />
  <meta property="og:image" content={post.featuredImage?.url} />
  <meta property="og:type" content="article" />
</head>

<body>
  <article class="cms-post">
    <header>
      <h1>{post.title}</h1>

      {post.featuredImage && (
        <img
          src={post.featuredImage.url}
          alt={post.featuredImage.description}
          class="featured-image"
        />
      )}

      <div class="post-meta">
        <time>{new Date(post.publishDate).toLocaleDateString()}</time>
        <span>Author: {post.author.name}</span>
      </div>
    </header>

    <!-- Use rich text renderer -->
    <div class="content">
      <RichTextRenderer content={post.content} />
    </div>
  </article>
</body>
</html>

MDX Integration and Advanced Usage

MDX Configuration

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import rehypePrettyCode from 'rehype-pretty-code';

export default defineConfig({
  integrations: [
    mdx({
      remarkPlugins: [remarkGfm, remarkMath],
      rehypePlugins: [
        rehypeKatex,
        [rehypePrettyCode, {
          theme: 'github-dark',
          keepBackground: false,
        }]
      ],
    }),
  ],
});

Custom MDX Components


// src/components/mdx/CodeBlock.astro
export interface Props {
  title?: string;
  language: string;
  code: string;
  showLineNumbers?: boolean;
}

const { title, language, code, showLineNumbers = false } = Astro.props;

<div class="code-block">
  {title && (
    <div class="code-title">
      <span class="language-label">{language}</span>
      <span class="title">{title}</span>
    </div>
  )}

  <pre class="code-content" class:list={[
    showLineNumbers && 'line-numbers'
  ]}>
    <code class={`language-${language}`} set:html={code} />
  </pre>
</div>
---
// src/components/mdx/Callout.astro
export interface Props {
  type: 'info' | 'warning' | 'error' | 'success';
  title?: string;
}

const { type, title } = Astro.props;

const icons = {
  info: '💡',
  warning: '⚠️',
  error: '❌',
  success: '✅'
};
---

<div class={`callout callout-${type}`}>
  {title && (
    <div class="callout-title">
      <span class="callout-icon">{icons[type]}</span>
      <span>{title}</span>
    </div>
  )}
  <div class="callout-content">
    <slot></slot>
  </div>
</div>

<style>
.callout {
  @apply p-4 rounded-lg border-l-4 my-4;
}

.callout-info {
  @apply bg-blue-50 border-blue-400 text-blue-800;
}

.callout-warning {
  @apply bg-yellow-50 border-yellow-400 text-yellow-800;
}

.callout-error {
  @apply bg-red-50 border-red-400 text-red-800;
}

.callout-success {
  @apply bg-green-50 border-green-400 text-green-800;
}

.callout-title {
  @apply font-semibold flex items-center gap-2 mb-2;
}
</style>

MDX Page Example

---
# src/content/blog/advanced-astro-patterns.mdx
title: "Advanced Astro Patterns and Best Practices"
description: "Deep dive into advanced Astro usage and architectural patterns"
publishDate: 2024-01-15
author: "Developer"
tags: ["Astro", "Architecture", "Best Practices"]
---

import { Callout } from '../../components/mdx/Callout.astro';
import { CodeBlock } from '../../components/mdx/CodeBlock.astro';

# Advanced Astro Patterns and Best Practices

<Callout type="info" title="Learning Objectives">
This article introduces some advanced Astro usage patterns to help you build more complex applications.
</Callout>

## Component Islands Architecture

Astro's islands architecture is one of its core advantages:

```astro
---
// Only components that need interactivity are hydrated
---

<div class="page-layout">
  <!-- Static content, not sent to client -->
  <header>
    <h1>My Website</h1>
  </header>

  <!-- Interactive components, hydrated on demand -->
  <SearchWidget client:load />
  <UserProfile client:visible />
  <Newsletter client:idle />
</div>
Use the `client:load` directive sparingly, prioritize `client:visible` or `client:idle`.

Data Fetching Best Practices

1. Caching Strategy

<CodeBlock
  title="Cache Optimization Example"
  language="typescript"
  code={`
// src/lib/cache.ts
const cache = new Map();

export async function fetchWithCache(url: string, ttl = 300000) {
  const cached = cache.get(url);

  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data;
  }

  const data = await fetch(url).then(res => res.json());
  cache.set(url, { data, timestamp: Date.now() });

  return data;
}
  `}
  showLineNumbers={true}
/>

2. Error Handling

export async function safeApiCall<T>(
  apiCall: () => Promise<T>,
  fallback: T
): Promise<T> {
  try {
    return await apiCall();
  } catch (error) {
    console.error('API call failed:', error);
    return fallback;
  }
}
This pattern ensures your website works even when APIs fail. ```

Data Validation and Type Safety

Zod Integration

// src/lib/validation.ts
import { z } from 'zod';

// API response validation
export const PostSchema = z.object({
  id: z.number(),
  title: z.string().min(1),
  content: z.string(),
  publishDate: z.string().datetime(),
  author: z.object({
    name: z.string(),
    email: z.string().email(),
  }),
  tags: z.array(z.string()),
  featured: z.boolean().default(false),
});

export type Post = z.infer<typeof PostSchema>;

// Validation function
export function validatePost(data: unknown): Post {
  return PostSchema.parse(data);
}

// Safe API call
export async function fetchValidatedPosts(): Promise<Post[]> {
  const response = await fetch('/api/posts');
  const data = await response.json();

  return z.array(PostSchema).parse(data);
}

Environment Variable Validation

// src/env.ts
import { z } from 'zod';

const envSchema = z.object({
  PUBLIC_SITE_URL: z.string().url(),
  CMS_API_KEY: z.string().min(1),
  DATABASE_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});

export const env = envSchema.parse(process.env);
Key Takeaways
  1. Type Safety: Use TypeScript and Zod to ensure data structure correctness
  2. Caching Strategy: Use caching wisely to reduce API call frequency
  3. Error Handling: Provide fallback solutions to ensure website availability
  4. Content Collections: Leverage the type-safe content management system
  5. Performance Optimization: Only hydrate on the client when necessary
Considerations
  • Data fetching in getStaticPaths happens at build time, not runtime
  • Large amounts of data may impact build time, consider using incremental builds
  • Don’t expose sensitive information in client-side code
  • Regularly verify the availability and data format of external APIs