Comments (10)
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 reading time
to AstroPaper by following these steps.
- Install required dependencies
npm install reading-time mdast-util-to-string
- Create
remark-reading-time.mjs
file underutils
directory
// 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;
};
}
- Add the plugin to
astro.config.mjs
// 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"],
},
},
});
- Add
readingTime
to blog schema
// 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.
- modify
src/pages/posts/[slug].astro
as the following
// 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}
/>
)
}
- Then, show that
frontmatter.readingTime
inside PostDetails page
// 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.
Moreover, do let me know if you have any other good suggestions.
Sorry for my late reply.
Hope this helps. Thanks.
from astro-paper.
Hey, thank you very much @satnaing. If you don't mind may I know, how to use it on the .md file?
Hello @dushyanth31
Since this is just a markdown file, I don't think we can use JavaScript or TypeScript directly. If you want to do so, you can use mdx file format for that specific purpose.
And I think it's better to add readingTime
inside parent layout components like PostDetails.astro
. In this way, you don't have to specify readingTime
in each article.
Another simple approach is that you can specify readingTime
manually in the markdown frontmatter.
For example,
file: astro-paper-2.md
---
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
---
....
from astro-paper.
Hello everyone,
I'm gonna push a commit that closes this issue.
In that commit, I rearranged all the steps. Hope it helps.
Here's the link to that blog post.
Let me know if you still have some problems.
from astro-paper.
Just a quick update!
I refactored the codes and move the reading time
logic into a util function.
- create a new file called
getPostsWithRT.ts
undersrc/utils
directory.
// 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;
- Update
getSortedPosts
func if you want to include estimated reading time in places other than post details.
// 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 getSortedPosts
. (simply add await in front of getSortedPosts)
Those files are
- src/pages/index.astro
- src/pages/posts/index.astro
- src/pages/rss.xml.ts
- src/pages/posts/[slug].astro
All you have to do is like this
const sortedPosts = getSortedPosts(posts); // old code
const sortedPosts = await getSortedPosts(posts); // new code
- Refactor
getStaticPaths
of/src/pages/posts/[slug].astro
as the following
// 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}
/>
)
}
- refactor
PostDetails.astro
like this
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 readingTime
in posts and post details
Optional!!!
Update Datetime
component to display readingTime
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
eg: Card.tsx
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.
I've also updated the feat/post-reading-time branch.
from astro-paper.
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 readingTime
to Card.tsx
, you also have to do so in PostDetails.astro
to have the reading time displayed on the post itself 🙂
<Datetime datetime={pubDatetime} readingTime={readingTime} size="lg" className="my-2" />
from astro-paper.
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.
from astro-paper.
Hey, thank you very much @satnaing. If you don't mind may I know, how to use it on the .md file?
from astro-paper.
@satnaing how do you put the reading time on each post inside the list of posts like in posts
path (index.astro)?
from astro-paper.
@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!
from astro-paper.
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:
remarkReadingTime, // 👈🏻 our plugin
breaks the entire website with:
TypeError: Failed to parse Markdown file "/path_to_website/src/content/blog/adding-new-post.md":
Function.prototype.toString requires that 'this' be a Function
at Proxy.toString (<anonymous>)
at eval (/path_to_website/src/utils/remark-reading-time.mjs:10:46)
at wrapped (file:///path_to_website/node_modules/trough/index.js:115:27)
at next (file:///path_to_website/node_modules/trough/index.js:65:23)
at done (file:///path_to_website/node_modules/trough/index.js:148:7)
at then (file:///path_to_website/node_modules/trough/index.js:158:5)
at wrapped (file:///path_to_website/node_modules/trough/index.js:136:9)
at next (file:///path_to_website/node_modules/trough/index.js:65:23)
at done (file:///path_to_website/node_modules/trough/index.js:148:7)
at then (file:///path_to_website/node_modules/trough/index.js:158:5)
at wrapped (file:///path_to_website/node_modules/trough/index.js:136:9)
at next (file:///path_to_website/node_modules/trough/index.js:65:23)
at done (file:///path_to_website/node_modules/trough/index.js:148:7)
at then (file:///path_to_website/node_modules/trough/index.js:158:5)
at wrapped (file:///path_to_website/node_modules/trough/index.js:136:9)
at next (file:///path_to_website/node_modules/trough/index.js:65:23)
at Object.run (file:///path_to_website/node_modules/trough/index.js:36:5)
at executor (file:///path_to_website/node_modules/unified/lib/index.js:321:20)
at Function.run (file:///path_to_website/node_modules/unified/lib/index.js:312:5)
at executor (file:///path_to_website/node_modules/unified/lib/index.js:393:17)
at new Promise (<anonymous>)
at Function.process (file:///path_to_website/node_modules/unified/lib/index.js:380:14)
at renderMarkdown (file:///path_to_website/node_modules/@astrojs/markdown-remark/dist/index.js:98:26)
at async Context.load (file:///path_to_website/node_modules/astro/dist/vite-plugin-markdown/index.js:62:30)
at async Object.load (file:///path_to_website/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:42892:32)
at async loadAndTransform (file:///path_to_website/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:53318:24)
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!
from astro-paper.
Related Issues (20)
- toggle-theme.js HOT 1
- posts description question HOT 2
- Build failing when using this template with SolidJS HOT 1
- Following installation steps doesn't create Astro with template HOT 2
- Any chance of documentation on adding additional pages to menu? HOT 1
- Vercel Web Analytics don't work
- Publishing to Netlify: latest post doesn't show up. HOT 3
- Post Readtime Blog Guide leads to errors in getPostsWithRT.ts HOT 2
- Invalid Date HOT 3
- Any chance of guidance for basic CSS modifications? HOT 2
- react version conflict HOT 3
- Categories Need to add categories feature in menu HOT 4
- Cms need to connect to cms HOT 2
- Google search console how to add astro paper to it HOT 6
- Blog header should support blog deployed at <root.url/base> in addition to <root.url> HOT 7
- Hijri Date
- [CouldNotTransformImage] Could not transform image `/_astro/AstroPaper-v3.uaW8qzSG.png`. See the stack trace for more information. HOT 1
- TOC for blog posts
- Author Pages Support HOT 2
- Mermaid diagrams HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from astro-paper.