Code Monkey home page Code Monkey logo

Comments (10)

satnaing avatar satnaing commented on May 18, 2024 2

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.

  1. Install required dependencies
npm install reading-time mdast-util-to-string
  1. Create remark-reading-time.mjs file under utils 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;
  };
}
  1. 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"],
    },
  },
});
  1. 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.

  1. 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}
    />
  )
}
  1. 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.

satnaing avatar satnaing commented on May 18, 2024 2

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.

satnaing avatar satnaing commented on May 18, 2024 2

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.

satnaing avatar satnaing commented on May 18, 2024 1

Just a quick update!

I refactored the codes and move the reading time logic into a util function.

  1. create a new file called getPostsWithRT.ts under src/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;
  1. 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
  1. 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}
    />
  )
}
  1. 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.

mattppal avatar mattppal commented on May 18, 2024 1

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.

EmilLuta avatar EmilLuta commented on May 18, 2024 1

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.

dushyanth31 avatar dushyanth31 commented on May 18, 2024

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.

ferrarafer avatar ferrarafer commented on May 18, 2024

@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.

ferrarafer avatar ferrarafer commented on May 18, 2024

@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.

EmilLuta avatar EmilLuta commented on May 18, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.