-
Notifications
You must be signed in to change notification settings - Fork 423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to add an Estimated Reading Time to Astro Blog? #74
Comments
In order to add reading time to AstroPaper, we have to tweak PostDetails a little bit since dynamic frontmatter injection and content collection API are not compatible I guess. (correct me if I'm wrong) So, you can add
npm install reading-time mdast-util-to-string
// file: src/utils/remark-reading-time.mjs
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.readingTime = readingTime.text;
};
}
// file: astro.config.mjs
import { defineConfig } from "astro/config";
// other imports
import { remarkReadingTime } from "./src/utils/remark-reading-time.mjs"; // make sure your relative path is correct
// https://astro.build/config
export default defineConfig({
site: SITE.website,
integrations: [
// other integrations
],
markdown: {
remarkPlugins: [
remarkToc,
remarkReadingTime, // 👈🏻 our plugin
[
remarkCollapse,
{
test: "Table of contents",
},
],
],
// other config
},
vite: {
optimizeDeps: {
exclude: ["@resvg/resvg-js"],
},
},
});
// file: src/content/_schemas.ts
import { z } from "astro:content";
export const blogSchema = z
.object({
author: z.string().optional(),
pubDatetime: z.date(),
readingTime: z.string().optional(), // 👈🏻 optional readingTime frontmatter
title: z.string(),
postSlug: z.string().optional(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
tags: z.array(z.string()).default(["others"]),
ogImage: z.string().optional(),
description: z.string(),
})
.strict();
export type BlogFrontmatter = z.infer<typeof blogSchema>; So far so good. Now it's time for a tricky part.
// file: src/pages/posts/[slug].astro
---
import { CollectionEntry, getCollection } from "astro:content";
import Posts from "@layouts/Posts.astro";
import PostDetails from "@layouts/PostDetails.astro";
import getSortedPosts from "@utils/getSortedPosts";
import getPageNumbers from "@utils/getPageNumbers";
import slugify from "@utils/slugify";
import { SITE } from "@config";
import type { BlogFrontmatter } from "@content/_schemas"; // 👈🏻 import frontmatter type
export interface Props {
post: CollectionEntry<"blog">;
frontmatter: BlogFrontmatter; // 👈🏻 specify frontmatter type in Props
}
export async function getStaticPaths() {
const mapFrontmatter = new Map();
// Get all posts using glob. This is to get the updated frontmatter
const globPosts = await Astro.glob<BlogFrontmatter>(
"../../content/blog/*.md"
);
// Then, set those frontmatter value in a JS Map with key value pair
// (post-slug, frontmatter)
globPosts.map(({ frontmatter }) => {
mapFrontmatter.set(slugify(frontmatter), frontmatter);
});
const posts = await getCollection("blog", ({ data }) => !data.draft);
const postResult = posts.map(post => ({
params: { slug: slugify(post.data) },
props: { post, frontmatter: mapFrontmatter.get(slugify(post.data)) }, // add extra frontmatter props
}));
const pagePaths = getPageNumbers(posts.length).map(pageNum => ({
params: { slug: String(pageNum) },
}));
return [...postResult, ...pagePaths];
}
const { slug } = Astro.params;
const { post, frontmatter } = Astro.props; // restructure frontmatter property
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
const totalPages = getPageNumbers(sortedPosts.length);
const currentPage =
slug && !isNaN(Number(slug)) && totalPages.includes(Number(slug))
? Number(slug)
: 0;
const lastPost = currentPage * SITE.postPerPage;
const startPost = lastPost - SITE.postPerPage;
const paginatedPosts = sortedPosts.slice(startPost, lastPost);
---
{
post ? (
<PostDetails post={post} frontmatter={frontmatter} /> // add frontmatter as prop to PostDetails component
) : (
<Posts
posts={paginatedPosts}
pageNum={currentPage}
totalPages={totalPages.length}
/>
)
}
// file: src/layouts/PostDetails
---
// other imports
import type { BlogFrontmatter } from "@content/_schemas"; // 👈🏻 import frontmatter type
export interface Props {
post: CollectionEntry<"blog">;
frontmatter: BlogFrontmatter; // 👈🏻 specify frontmatter type in Props
}
const { post, frontmatter } = Astro.props; // restructure frontmatter from props
// others
---
<Layout ...>
<p>{frontmatter.readingTime}</p> <!-- Show readingTime anywhere you want -->
</Layout> If you want to see the code, I've pushed a new branch and you can check that out if you want. Sorry for my late reply. |
Hey, thank you very much @satnaing. If you don't mind may I know, how to use it on the .md file? |
@satnaing how do you put the reading time on each post inside the list of posts like in |
Just a quick update! I refactored the codes and move the
// file: getPostsWithRT.ts
import type { BlogFrontmatter } from "@content/_schemas";
import type { MarkdownInstance } from "astro";
import slugify from "./slugify";
import type { CollectionEntry } from "astro:content";
export const getReadingTime = async () => {
// Get all posts using glob. This is to get the updated frontmatter
const globPosts = import.meta.glob<MarkdownInstance<BlogFrontmatter>>(
"../content/blog/*.md"
);
// Then, set those frontmatter value in a JS Map with key value pair
const mapFrontmatter = new Map();
const globPostsValues = Object.values(globPosts);
await Promise.all(
globPostsValues.map(async globPost => {
const { frontmatter } = await globPost();
mapFrontmatter.set(slugify(frontmatter), frontmatter.readingTime);
})
);
return mapFrontmatter;
};
const getPostsWithRT = async (posts: CollectionEntry<"blog">[]) => {
const mapFrontmatter = await getReadingTime();
return posts.map(post => {
post.data.readingTime = mapFrontmatter.get(slugify(post.data));
return post;
});
};
export default getPostsWithRT;
// file: utils/getSortedPosts
import type { CollectionEntry } from "astro:content";
import getPostsWithRT from "./getPostsWithRT";
const getSortedPosts = async (posts: CollectionEntry<"blog">[]) => { // make sure that this func must be async
const postsWithRT = await getPostsWithRT(posts); // add reading time
return postsWithRT
.filter(({ data }) => !data.draft)
.sort(
(a, b) =>
Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) -
Math.floor(new Date(a.data.pubDatetime).getTime() / 1000)
);
};
export default getSortedPosts; If you update this, make sure you update all files that use
All you have to do is like this const sortedPosts = getSortedPosts(posts); // old code
const sortedPosts = await getSortedPosts(posts); // new code
// file: [slug].astro
---
import { CollectionEntry, getCollection } from "astro:content";
...
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.draft);
const postsWithRT = await getPostsWithRT(posts); // replace reading time logic with this func
const postResult = postsWithRT.map(post => ({
params: { slug: slugify(post.data) },
props: { post },
}));
const pagePaths = getPageNumbers(posts.length).map(pageNum => ({
params: { slug: String(pageNum) },
}));
return [...postResult, ...pagePaths];
}
const { slug } = Astro.params;
const { post } = Astro.props; // remove frontmatter from this
const posts = await getCollection("blog");
const sortedPosts = await getSortedPosts(posts); // make sure to await getSortedPosts
....
---
{
post ? (
<PostDetails post={post} /> // remove frontmatter prop
) : (
<Posts
posts={paginatedPosts}
pageNum={currentPage}
totalPages={totalPages.length}
/>
)
}
file: src/layouts/PostDetails.astro
---
import Layout from "@layouts/Layout.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Tag from "@components/Tag.astro";
import Datetime from "@components/Datetime";
import type { CollectionEntry } from "astro:content";
import { slugifyStr } from "@utils/slugify";
export interface Props {
post: CollectionEntry<"blog">;
}
const { post } = Astro.props;
const { title, author, description, ogImage, pubDatetime, tags, readingTime } =
post.data; // we can now directly access readingTime from frontmatter
....
--- Now you can access Optional!!! import { LOCALE } from "@config";
export interface Props {
datetime: string | Date;
size?: "sm" | "lg";
className?: string;
readingTime?: string;
}
export default function Datetime({
datetime,
size = "sm",
className,
readingTime, // new prop
}: Props) {
return (
...
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime datetime={datetime} />
<span> ({readingTime})</span> {/* display reading time */}
</span>
...
);
) Then, pass readingTime props from its parent component export default function Card({ href, frontmatter, secHeading = true }: Props) {
const { title, pubDatetime, description, readingTime } = frontmatter;
return (
...
<Datetime datetime={pubDatetime} readingTime={readingTime} />
...
);
} @ferrarafer hopefully this solves your issue. |
Hello @dushyanth31 Another simple approach is that you can specify ---
author: Sat Naing
pubDatetime: 2023-01-30T15:57:52.737Z
title: AstroPaper 2.0
postSlug: astro-paper-2
featured: true
ogImage: https://user-images.githubusercontent.com/53733092/215771435-25408246-2309-4f8b-a781-1f3d93bdf0ec.png
tags:
- release
description: AstroPaper with the enhancements of Astro v2. Type-safe markdown contents, bug fixes and better dev experience etc.
readingTime: 2 min read
---
.... |
@satnaing thanks for the update man! I resolved it during the weekend but I will take a look to this implementation during the week, probably better. Thanks a lot! |
Wow this is so awesome! Thank you! 🤯 One small piece you didn't explicitly state, but tripped me up for a bit: in addition to passing
|
Hello everyone, Here's the link to that blog post. Let me know if you still have some problems. |
Hi there, sorry to open this thread again, but seems current version is not working as intended. If I follow the guide, everything breaks at step 3:
breaks the entire website with:
If it helps, I'm running on MacOS. Let me know if you'd also like me to open a new ticket or if I can assist further. Thanks a lot in advance! |
Figured out that the problem was related with to the lack of server restart. As soon as I restarted it, all's good. Works as intended, please disregard the comment. Leaving message above for anyone who stumbles on the same issues. |
I just wanted to read-time to blog posts, I have already tried to add read-Time docs given by the Astro itself but ended in vain.
Kindly help me figure out how to implement this.
Thank you.
I needed an step-by-step process on how to do this.
The text was updated successfully, but these errors were encountered: