内容渲染 #
一、基本渲染 #
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
---

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