内容渲染 #

一、基本渲染 #

1.1 使用 render 函数 #

astro
---
import { getEntry, render } from 'astro:content';

const post = await getEntry('blog', 'hello-world');
const { Content } = await render(post);
---

<article>
  <h1>{post.data.title}</h1>
  <p>{post.data.description}</p>
  
  <Content />
</article>

1.2 在动态路由中使用 #

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

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 render(post);
---

<article>
  <header>
    <h1>{post.data.title}</h1>
    <time datetime={post.data.date.toISOString()}>
      {post.data.date.toLocaleDateString('zh-CN')}
    </time>
  </header>
  
  <Content />
</article>

二、渲染信息 #

2.1 获取标题列表 #

astro
---
import { getEntry, render } from 'astro:content';

const post = await getEntry('blog', 'hello-world');
const { Content, headings } = await render(post);
---

<div class="layout">
  <aside class="toc">
    <h2>目录</h2>
    <ul>
      {headings.map(heading => (
        <li class={`toc-${heading.depth}`}>
          <a href={`#${heading.slug}`}>{heading.text}</a>
        </li>
      ))}
    </ul>
  </aside>
  
  <article>
    <Content />
  </article>
</div>

2.2 标题结构 #

typescript
interface Heading {
  depth: 1 | 2 | 3 | 4 | 5 | 6;
  slug: string;
  text: string;
}

2.3 嵌套目录 #

astro
---
function buildToc(headings) {
  const toc = [];
  const stack = [{ depth: 0, children: toc }];
  
  for (const heading of headings) {
    while (stack[stack.length - 1].depth >= heading.depth) {
      stack.pop();
    }
    
    const item = { ...heading, children: [] };
    stack[stack.length - 1].children.push(item);
    stack.push(item);
  }
  
  return toc;
}

const { Content, headings } = await render(post);
const toc = buildToc(headings);
---

<nav class="toc">
  <TocList items={toc} />
</nav>

三、自定义组件 #

3.1 覆盖默认组件 #

创建自定义组件来覆盖 Markdown 元素的默认渲染:

astro
---
// src/components/Prose.astro
const { Content } = Astro.props;
---

<div class="prose">
  <Content 
    components={{
      h1: Heading1,
      h2: Heading2,
      a: CustomLink,
      code: InlineCode,
      pre: CodeBlock,
    }}
  />
</div>

3.2 自定义标题组件 #

astro
---
// src/components/Heading.astro
interface Props {
  level: number;
}

const { level, ...props } = Astro.props;
const Tag = `h${level}` as const;
---

<Tag {...props} class="heading">
  <slot />
</Tag>

<style>
  .heading {
    scroll-margin-top: 80px;
  }
</style>

3.3 自定义链接组件 #

astro
---
// src/components/CustomLink.astro
interface Props {
  href: string;
}

const { href, ...props } = Astro.props;
const isExternal = href.startsWith('http');
---

<a 
  href={href}
  target={isExternal ? '_blank' : undefined}
  rel={isExternal ? 'noopener noreferrer' : undefined}
  {...props}
>
  <slot />
  {isExternal && <span class="external-icon">↗</span>}
</a>

3.4 使用自定义组件 #

astro
---
import { getEntry, render } from 'astro:content';
import CustomLink from '../components/CustomLink.astro';

const post = await getEntry('blog', 'hello-world');
const { Content } = await render(post);
---

<article>
  <Content 
    components={{
      a: CustomLink,
    }}
  />
</article>

四、代码高亮 #

4.1 配置代码高亮 #

javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  markdown: {
    syntaxHighlight: 'shiki',
    shikiConfig: {
      theme: 'github-dark',
      wrap: true,
    },
  },
});

4.2 自定义代码块 #

astro
---
// src/components/CodeBlock.astro
interface Props {
  lang?: string;
  code?: string;
}

const { lang, code } = Astro.props;
---

<div class="code-block">
  {lang && <span class="language">{lang}</span>}
  <pre><code>{code}</code></pre>
  <button class="copy-btn">复制</button>
</div>

<script>
  document.querySelectorAll('.copy-btn').forEach(btn => {
    btn.addEventListener('click', async () => {
      const code = btn.previousElementSibling?.textContent || '';
      await navigator.clipboard.writeText(code);
      btn.textContent = '已复制!';
      setTimeout(() => btn.textContent = '复制', 2000);
    });
  });
</script>

4.3 代码块标题 #

使用代码块元信息:

markdown
```javascript title="example.js"
const greeting = "Hello World";
console.log(greeting);
```

五、图片处理 #

5.1 相对路径图片 #

在 Markdown 中使用相对路径:

markdown
---
title: Astro
cover: ./images/cover.jpg
---

![截图](./images/screenshot.png)

5.2 图片优化 #

astro
---
import { getEntry, render } from 'astro:content';
import { Image } from 'astro:assets';

const post = await getEntry('blog', 'hello-world');
const { Content } = await render(post);
---

<article>
  {post.data.cover && (
    <Image 
      src={post.data.cover}
      alt={post.data.coverAlt || post.data.title}
      width={800}
      height={400}
      loading="eager"
    />
  )}
  
  <Content 
    components={{
      img: OptimizedImage,
    }}
  />
</article>

5.3 自定义图片组件 #

astro
---
// src/components/OptimizedImage.astro
import { Image } from 'astro:assets';

interface Props {
  src: ImageMetadata | string;
  alt: string;
}

const { src, alt, ...props } = Astro.props;
---

<Image 
  src={src}
  alt={alt}
  loading="lazy"
  {...props}
/>

六、MDX 支持 #

6.1 安装 MDX 集成 #

bash
npm install @astrojs/mdx

6.2 配置 MDX #

javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [mdx()],
});

6.3 使用 MDX #

mdx
---
title: Astro
date: 2024-01-15
---

import Chart from '../components/Chart.astro';

# MDX 文章

这是 MDX 内容,可以使用组件:

<Chart data={[1, 2, 3, 4, 5]} />

6.4 MDX 配置 #

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

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
  }),
});

export const collections = { blog };

七、渲染钩子 #

7.1 自定义渲染 #

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

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
  }),
  // 自定义渲染选项
});

7.2 Markdown 插件 #

javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';

export default defineConfig({
  markdown: {
    remarkPlugins: [
      [remarkToc, { tight: true }],
    ],
    rehypePlugins: [
      rehypeSlug,
      [rehypeAutolinkHeadings, { behavior: 'wrap' }],
    ],
  },
});

八、完整示例 #

8.1 博客文章页面 #

astro
---
// src/pages/blog/[slug].astro
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/BlogLayout.astro';
import TableOfContents from '../../components/TableOfContents.astro';
import AuthorCard from '../../components/AuthorCard.astro';
import RelatedPosts from '../../components/RelatedPosts.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, headings } = await render(post);

const allPosts = await getCollection('blog');
const relatedPosts = allPosts
  .filter(p => 
    p.slug !== post.slug &&
    !p.data.draft &&
    p.data.tags.some(tag => post.data.tags.includes(tag))
  )
  .slice(0, 3);
---

<Layout 
  title={post.data.title}
  description={post.data.description}
  image={post.data.cover}
>
  <article class="blog-post">
    <header class="post-header">
      <h1>{post.data.title}</h1>
      <p class="description">{post.data.description}</p>
      
      <div class="meta">
        <time datetime={post.data.date.toISOString()}>
          {post.data.date.toLocaleDateString('zh-CN')}
        </time>
        
        {post.data.updated && (
          <span class="updated">
            更新于 {post.data.updated.toLocaleDateString('zh-CN')}
          </span>
        )}
        
        <div class="tags">
          {post.data.tags.map(tag => (
            <span class="tag">{tag}</span>
          ))}
        </div>
      </div>
    </header>
    
    <div class="post-content">
      <aside class="toc">
        <TableOfContents headings={headings} />
      </aside>
      
      <div class="content">
        <Content />
      </div>
    </div>
    
    <footer class="post-footer">
      <AuthorCard author={post.data.author} />
      <RelatedPosts posts={relatedPosts} />
    </footer>
  </article>
</Layout>

<style>
  .blog-post {
    max-width: 800px;
    margin: 0 auto;
  }
  
  .post-header {
    margin-bottom: 2rem;
  }
  
  .post-content {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 2rem;
  }
  
  @media (max-width: 768px) {
    .post-content {
      grid-template-columns: 1fr;
    }
    
    .toc {
      display: none;
    }
  }
</style>

8.2 目录组件 #

astro
---
// src/components/TableOfContents.astro
interface Heading {
  depth: number;
  slug: string;
  text: string;
}

interface Props {
  headings: Heading[];
}

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

<nav class="toc">
  <h3>目录</h3>
  <ul>
    {headings.map(heading => (
      <li class={`toc-item toc-depth-${heading.depth}`}>
        <a href={`#${heading.slug}`}>{heading.text}</a>
      </li>
    ))}
  </ul>
</nav>

<style>
  .toc {
    position: sticky;
    top: 2rem;
  }
  
  .toc h3 {
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #6b7280;
    margin-bottom: 1rem;
  }
  
  .toc ul {
    list-style: none;
    padding: 0;
  }
  
  .toc-item {
    margin-bottom: 0.5rem;
  }
  
  .toc-item a {
    font-size: 0.875rem;
    color: #4b5563;
    text-decoration: none;
  }
  
  .toc-item a:hover {
    color: #2563eb;
  }
  
  .toc-depth-3 {
    padding-left: 1rem;
  }
</style>

九、总结 #

内容渲染核心要点:

text
┌─────────────────────────────────────────────────────┐
│                 内容渲染要点                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  🎨 render()     渲染 Markdown 内容                │
│                                                     │
│  📋 headings     获取标题列表                       │
│                                                     │
│  🧩 组件覆盖     自定义渲染组件                     │
│                                                     │
│  💻 代码高亮     Shiki 语法高亮                     │
│                                                     │
│  🖼️ 图片处理    优化和响应式图片                   │
│                                                     │
│  📝 MDX         支持 JSX 组件                      │
│                                                     │
└─────────────────────────────────────────────────────┘

下一步,让我们学习 CSS样式,掌握 Astro 的样式处理!

最后更新:2026-03-28