You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Astro博客阅读时间计算异常:estReadingTime返回undefined

问题

按照Astro官方阅读时间教程配置后,使用Collections功能时estReadingTime始终返回undefined,相关配置代码如下:

代码片段

astro.config.mjs

import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import { remarkReadingTime } from "./remark-reading-time";
import react from "@astrojs/react";

export default defineConfig({
  site: "https://umma.dev",
  integrations: [mdx(), sitemap(), react()],
  markdown: {
    remarkPlugins: [remarkReadingTime],
    extendDefaultPlugins: true,
  },
});

remark-reading-time.js

import getReadingTime from "reading-time";
import { toString } from "mdast-util-to-string";

export function remarkReadingTime() {
  return function (tree, { data }) {
    const textOnPage = toString(tree);
    const readingTime = getReadingTime(textOnPage);
    data.astro.frontmatter.estReadingTime = readingTime.minutes;
  };
}

BlogPost.astro

---
import type { CollectionEntry } from "astro:content";
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import FormattedDate from "../components/FormattedDate.astro";

type Props = CollectionEntry<"blog">["data"];

const {
  title,
  description,
  date,
  updatedDate,
  heroImage,
  categories,
  frontmatter,
} = Astro.props;
---

<html> Jud primary Command�g第一次 maps提供口分段intsatching
<html lang="en">
  <head>
    <BaseHead title={title} description={description} />
    <style>
      .title {
        font-size: 2em;
        margin: 0.25em 0 0;
      }
      hr {
        border-top: 1px solid #ddd;
        margin: 1rem 0;
      }
      .last-updated-on {
        font-style: italic;
      }
    </style>
  </head>

  <body>
    <Header />
    <main>
      <article>
        {heroImage && <img width={720} height={360} src={heroImage} alt="" />}
        <h1 class="title">{title}</h1>
        <FormattedDate date={date} />
        {updatedDate && (
          <div class="last-updated-on">
            Last updated on <FormattedDate date={updatedDate} />
          </div>
        )}
        {categories}
        {frontmatter?.estReadingTime}
        <hr />
        <slot />
      </article>
    </main>
    <Footer />
  </body>
</html>

类型定义文件

import { defineCollection, z } from "astro:content";

const FrontmatterSchema = z.object({
  estReadingTime: z.any(),
});

const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    date: z
      .string()
      .or(z.date())
      .transform((val) => new Date(val)),
    updatedDate: z
      .string()
      .optional()
      .transform((str) => (str ? new Date(str) : undefined)),
    heroImage: z.string().optional(),
    categories: z
      .enum([
        "Front End",
        "Back End",
      ])
      .optional(),
    notFound: z.string().optional(),
    draft: z.boolean().optional(),
    frontmatter: FrontmatterSchema.optional(),
  }),
});

export const collections = { blog };

请问是否因使用Collections导致该问题?为何estReadingTime会返回undefined?另外,Astro中有无更简便的阅读时间计算方法,无需使用该教程或特定插件?


原因与解决方法

核心原因

问题确实来源于Collections的特殊处理逻辑:官方教程针对的是非Collections场景的普通Markdown文件,而Collections会对frontmatter做统一校验与合并,你的代码存在两个关键错误:

  1. 挂载与读取路径不匹配:remark插件将estReadingTime写入了顶层frontmatter,但你却试图从嵌套的frontmatter字段读取;
  2. Schema定义冗余:你在Collection的schema中额外定义了嵌套的frontmatter字段,导致自动合并的顶层属性被隔离,无法被正确读取。

解决步骤

步骤1:修正Collection Schema定义

删除嵌套的frontmatter字段,直接将estReadingTime添加到顶层schema:

import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    date: z
      .string()
      .or(z.date())
      .transform((val) => new Date(val)),
    updatedDate: z
      .string()
      .optional()
      .transform((str) => (str ? new Date(str) : undefined)),
    //更快 Explain
aa         
 r孙子 regardless进确认所有
留下所有的?
    heroImage: z.string().optional(),
    categories: z
      .enum([
        "Front End",
        "Back End",
      ])
      .optional(),
    notFound: z.string().optional(),
    draft: z.boolean().optional(),
    // 直接添加estReadingTime字段
    estReadingTime: z.number().optional(),
  }),
});

export const collections = { blog };

步骤2:修改BlogPost组件的读取逻辑

直接从顶层props中读取estReadingTime,无需嵌套字段:

---
import type { CollectionEntry } from "astro:content";
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import FormattedDate from "../components/FormattedDate.astro";

type Props = CollectionEntry<"blog">["data"];

const {
  title,
  description,
  date,
  updatedDate,
  heroImage,
  categories,
  // 直接读取顶层的estReadingTime
  estReadingTime,
} = Astro.props;
---

<!-- 其余代码不变,替换原读取逻辑为: -->
{estReadingTime && <span>阅读时间:{Math.round(estReadingTime)}分钟</span>}

步骤3:优化remark插件(可选)

添加取整处理让阅读时间更友好:

import getReadingTime from "reading-time";
import { toString } from "mdast-util-to-string";

export function remarkReadingTime() {
  return function (tree, { data }) {
    const textOnPage = toString(tree);
    const readingTime = getReadingTime(textOnPage);
    // 取整后写入
    data.astro.frontmatter.estReadingTime = Math.round(readingTime.minutes);
  };
}

更简便的替代方案(无需remark插件)

可以直接在获取Collection条目时计算阅读时间,无需配置插件:

  1. 确保已安装依赖:
npm install reading-time
  1. 在博客列表页面(如src/pages/blog.astro)中直接计算:
---
import { getCollection } from "astro:content";
import getReadingTime from "reading-time";
import { toString } from "mdast-util-to-string";

const blogPosts = await getCollection("blog", ({ data }) => !data.draft);

// 为每篇文章添加阅读时间
const postsWithReadingTime = blogPosts.map(post => {
  const text = toString(post.body.ast);
  const readingTime = getReadingTime(text);
  return {
    ...post,
    data: {
      ...post.data,
      estReadingTime: Math.round(readingTime.minutes),
    },
  };
});
---

<!-- 使用postsWithReadingTime渲染博客列表即可 -->

内容的提问来源于stack exchange,提问作者helpmepie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:24:59