feat: add modified datetime in blog posts

* feat: add modified datetime in blog posts

Add modDatetime in blogSchema. Update Datetime component to accept modDatetime. Update sorting logic. Rename datetime to pubDatetime in Datetime component.

Closes: #134

* feat: add published_time & modified_time meta tags

* docs: update related blog posts to be up-to-date

* fix: remove extra spaces in breadcrumbs
This commit is contained in:
Sat Naing
2023-12-26 10:11:26 +06:30
committed by GitHub
parent 2f602fe815
commit 80e67a1dca
9 changed files with 134 additions and 50 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ breadcrumbList[0] === "posts" &&
<ul> <ul>
<li> <li>
<a href="/">Home</a> <a href="/">Home</a>
<span aria-hidden="true">&nbsp;&gt;&nbsp;</span> <span aria-hidden="true">&gt;</span>
</li> </li>
{ {
breadcrumbList.map((breadcrumb, index) => breadcrumbList.map((breadcrumb, index) =>
@@ -33,7 +33,7 @@ breadcrumbList[0] === "posts" &&
) : ( ) : (
<li> <li>
<a href={`/${breadcrumb}`}>{breadcrumb}</a> <a href={`/${breadcrumb}`}>{breadcrumb}</a>
<span aria-hidden="true">&nbsp;&gt;&nbsp;</span> <span aria-hidden="true">&gt;</span>
</li> </li>
) )
) )
+2 -2
View File
@@ -9,7 +9,7 @@ export interface Props {
} }
export default function Card({ href, frontmatter, secHeading = true }: Props) { export default function Card({ href, frontmatter, secHeading = true }: Props) {
const { title, pubDatetime, description } = frontmatter; const { title, pubDatetime, modDatetime, description } = frontmatter;
const headerProps = { const headerProps = {
style: { viewTransitionName: slugifyStr(title) }, style: { viewTransitionName: slugifyStr(title) },
@@ -28,7 +28,7 @@ export default function Card({ href, frontmatter, secHeading = true }: Props) {
<h3 {...headerProps}>{title}</h3> <h3 {...headerProps}>{title}</h3>
)} )}
</a> </a>
<Datetime datetime={pubDatetime} /> <Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
<p>{description}</p> <p>{description}</p>
</li> </li>
); );
+29 -11
View File
@@ -1,38 +1,56 @@
import { LOCALE } from "@config"; import { LOCALE } from "@config";
export interface Props { interface DatetimesProps {
datetime: string | Date; pubDatetime: string | Date;
modDatetime: string | Date | undefined;
}
interface Props extends DatetimesProps {
size?: "sm" | "lg"; size?: "sm" | "lg";
className?: string; className?: string;
} }
export default function Datetime({ datetime, size = "sm", className }: Props) { export default function Datetime({
pubDatetime,
modDatetime,
size = "sm",
className,
}: Props) {
return ( return (
<div className={`flex items-center space-x-2 opacity-80 ${className}`}> <div className={`flex items-center space-x-2 opacity-80 ${className}`}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={`${ className={`${
size === "sm" ? "scale-90" : "scale-100" size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 fill-skin-base`} } inline-block h-6 w-6 min-w-[1.375rem] fill-skin-base`}
aria-hidden="true" aria-hidden="true"
> >
<path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path> <path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path>
<path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path> <path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path>
</svg> </svg>
<span className="sr-only">Posted on:</span> {modDatetime ? (
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}> <span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime datetime={datetime} /> Updated:
</span>
) : (
<span className="sr-only">Published:</span>
)}
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
/>
</span> </span>
</div> </div>
); );
} }
const FormattedDatetime = ({ datetime }: { datetime: string | Date }) => { const FormattedDatetime = ({ pubDatetime, modDatetime }: DatetimesProps) => {
const myDatetime = new Date(datetime); const myDatetime = new Date(modDatetime ? modDatetime : pubDatetime);
const date = myDatetime.toLocaleDateString(LOCALE.langTag, { const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
year: "numeric", year: "numeric",
month: "long", month: "short",
day: "numeric", day: "numeric",
}); });
@@ -43,10 +61,10 @@ const FormattedDatetime = ({ datetime }: { datetime: string | Date }) => {
return ( return (
<> <>
{date} <time dateTime={myDatetime.toISOString()}>{date}</time>
<span aria-hidden="true"> | </span> <span aria-hidden="true"> | </span>
<span className="sr-only">&nbsp;at&nbsp;</span> <span className="sr-only">&nbsp;at&nbsp;</span>
{time} <span className="text-nowrap">{time}</span>
</> </>
); );
}; };
+5 -1
View File
@@ -1,6 +1,7 @@
--- ---
author: Sat Naing author: Sat Naing
pubDatetime: 2022-09-23T15:22:00Z pubDatetime: 2022-09-23T15:22:00Z
modDatetime: 2023-12-21T09:12:47.400Z
title: Adding new posts in AstroPaper theme title: Adding new posts in AstroPaper theme
postSlug: adding-new-posts-in-astropaper-theme postSlug: adding-new-posts-in-astropaper-theme
featured: true featured: true
@@ -23,10 +24,11 @@ Frontmatter is the main place to store some important information about the blog
Here is the list of frontmatter property for each post. Here is the list of frontmatter property for each post.
| Property | Description | Remark | | Property | Description | Remark |
| ------------------ | ------------------------------------------------------------------------------- | --------------------------------------------- | | ------------------ | ------------------------------------------------------------------------------------------- | --------------------------------------------- |
| **_title_** | Title of the post. (h1) | required<sup>\*</sup> | | **_title_** | Title of the post. (h1) | required<sup>\*</sup> |
| **_description_** | Description of the post. Used in post excerpt and site description of the post. | required<sup>\*</sup> | | **_description_** | Description of the post. Used in post excerpt and site description of the post. | required<sup>\*</sup> |
| **_pubDatetime_** | Published datetime in ISO 8601 format. | required<sup>\*</sup> | | **_pubDatetime_** | Published datetime in ISO 8601 format. | required<sup>\*</sup> |
| **_modDatetime_** | Modified datetime in ISO 8601 format. (only add this property when a blog post is modified) | optional |
| **_author_** | Author of the post. | default = SITE.author | | **_author_** | Author of the post. | default = SITE.author |
| **_postSlug_** | Slug for the post. Will automatically be slugified. | default = slugified title | | **_postSlug_** | Slug for the post. Will automatically be slugified. | default = slugified title |
| **_featured_** | Whether or not display this post in featured section of home page | default = false | | **_featured_** | Whether or not display this post in featured section of home page | default = false |
@@ -35,6 +37,8 @@ Here is the list of frontmatter property for each post.
| **_ogImage_** | OG image of the post. Useful for social media sharing and SEO. | default = SITE.ogImage or generated OG image | | **_ogImage_** | OG image of the post. Useful for social media sharing and SEO. | default = SITE.ogImage or generated OG image |
| **_canonicalURL_** | Canonical URL (absolute), in case the article already exists on other source. | default = `Astro.site` + `Astro.url.pathname` | | **_canonicalURL_** | Canonical URL (absolute), in case the article already exists on other source. | default = `Astro.site` + `Astro.url.pathname` |
> Tip! You can get ISO 8601 datetime by running `new Date().toISOString()` in the console. Make sure you remove quotes though.
Only `title`, `description` and `pubDatetime` fields in frontmatter must be specified. Only `title`, `description` and `pubDatetime` fields in frontmatter must be specified.
Title and description (excerpt) are important for search engine optimization (SEO) and thus AstroPaper encourages to include these in blog posts. Title and description (excerpt) are important for search engine optimization (SEO) and thus AstroPaper encourages to include these in blog posts.
@@ -2,6 +2,7 @@
title: How to add an estimated reading time in AstroPaper title: How to add an estimated reading time in AstroPaper
author: Sat Naing author: Sat Naing
pubDatetime: 2023-07-21T10:11:06.130Z pubDatetime: 2023-07-21T10:11:06.130Z
modDatetime: 2023-12-21T05:03:41.955Z
postSlug: how-to-add-estimated-reading-time postSlug: how-to-add-estimated-reading-time
featured: false featured: false
draft: false draft: false
@@ -157,8 +158,15 @@ export interface Props {
const { post } = Astro.props; const { post } = Astro.props;
const { title, author, description, ogImage, pubDatetime, tags, readingTime } = const {
post.data; // we can now directly access readingTime from frontmatter title,
author,
description,
ogImage,
readingTime, // we can now directly access readingTime from frontmatter
pubDatetime,
modDatetime,
tags } = post.data;
// other codes // other codes
--- ---
@@ -181,8 +189,12 @@ const getSortedPosts = async (posts: CollectionEntry<"blog">[]) => {
.filter(({ data }) => !data.draft) .filter(({ data }) => !data.draft)
.sort( .sort(
(a, b) => (a, b) =>
Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) - Math.floor(
Math.floor(new Date(a.data.pubDatetime).getTime() / 1000) new Date(b.data.modDatetime ?? b.data.pubDatetime).getTime() / 1000
) -
Math.floor(
new Date(a.data.modDatetime ?? a.data.pubDatetime).getTime() / 1000
)
); );
}; };
@@ -215,7 +227,7 @@ But in this section, I'm gonna show you how I would display `readingTime` in my
Step (1) Update `Datetime` component to display `readingTime` Step (1) Update `Datetime` component to display `readingTime`
```ts ```tsx
import { LOCALE } from "@config"; import { LOCALE } from "@config";
export interface Props { export interface Props {
@@ -234,7 +246,7 @@ export default function Datetime({
return ( return (
// other codes // other codes
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}> <span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime datetime={datetime} /> <FormattedDatetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
<span> ({readingTime})</span> {/* display reading time */} <span> ({readingTime})</span> {/* display reading time */}
</span> </span>
// other codes // other codes
@@ -248,10 +260,14 @@ file: Card.tsx
```ts ```ts
export default function Card({ href, frontmatter, secHeading = true }: Props) { export default function Card({ href, frontmatter, secHeading = true }: Props) {
const { title, pubDatetime, description, readingTime } = frontmatter; const { title, pubDatetime, modDatetime description, readingTime } = frontmatter;
return ( return (
... ...
<Datetime datetime={pubDatetime} readingTime={readingTime} /> <Datetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
readingTime={readingTime}
/>
... ...
); );
} }
@@ -264,7 +280,8 @@ file: PostDetails.tsx
<main id="main-content"> <main id="main-content">
<h1 class="post-title">{title}</h1> <h1 class="post-title">{title}</h1>
<Datetime <Datetime
datetime={pubDatetime} pubDatetime={pubDatetime}
modDatetime={modDatetime}
size="lg" size="lg"
className="my-2" className="my-2"
readingTime={readingTime} readingTime={readingTime}
+1
View File
@@ -7,6 +7,7 @@ const blog = defineCollection({
z.object({ z.object({
author: z.string().default(SITE.author), author: z.string().default(SITE.author),
pubDatetime: z.date(), pubDatetime: z.date(),
modDatetime: z.date().optional(),
title: z.string(), title: z.string(),
postSlug: z.string().optional(), postSlug: z.string().optional(),
featured: z.boolean().optional(), featured: z.boolean().optional(),
+22
View File
@@ -11,6 +11,8 @@ export interface Props {
description?: string; description?: string;
ogImage?: string; ogImage?: string;
canonicalURL?: string; canonicalURL?: string;
pubDatetime?: Date;
modDatetime?: Date;
} }
const { const {
@@ -19,6 +21,8 @@ const {
description = SITE.desc, description = SITE.desc,
ogImage = SITE.ogImage, ogImage = SITE.ogImage,
canonicalURL = new URL(Astro.url.pathname, Astro.site).href, canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
pubDatetime,
modDatetime,
} = Astro.props; } = Astro.props;
const socialImageURL = new URL( const socialImageURL = new URL(
@@ -49,6 +53,24 @@ const socialImageURL = new URL(
<meta property="og:url" content={canonicalURL} /> <meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={socialImageURL} /> <meta property="og:image" content={socialImageURL} />
<!-- Article Published/Modified time -->
{
pubDatetime && (
<meta
property="article:published_time"
content={pubDatetime.toISOString()}
/>
)
}
{
modDatetime && (
<meta
property="article:modified_time"
content={modDatetime.toISOString()}
/>
)
}
<!-- Twitter --> <!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} /> <meta property="twitter:url" content={canonicalURL} />
+27 -10
View File
@@ -13,8 +13,16 @@ export interface Props {
const { post } = Astro.props; const { post } = Astro.props;
const { title, author, description, ogImage, canonicalURL, pubDatetime, tags } = const {
post.data; title,
author,
description,
ogImage,
canonicalURL,
pubDatetime,
modDatetime,
tags,
} = post.data;
const { Content } = await post.render(); const { Content } = await post.render();
@@ -23,15 +31,19 @@ const ogUrl = new URL(
ogImageUrl ?? `/posts/${slugifyStr(title)}.png`, ogImageUrl ?? `/posts/${slugifyStr(title)}.png`,
Astro.url.origin Astro.url.origin
).href; ).href;
const layoutProps = {
title,
author,
description,
pubDatetime,
modDatetime,
canonicalURL,
ogImage: ogUrl,
};
--- ---
<Layout <Layout {...layoutProps}>
title={title}
author={author}
description={description}
ogImage={ogUrl}
canonicalURL={canonicalURL}
>
<Header /> <Header />
<div class="mx-auto flex w-full max-w-3xl justify-start px-2"> <div class="mx-auto flex w-full max-w-3xl justify-start px-2">
<button <button
@@ -47,7 +59,12 @@ const ogUrl = new URL(
</div> </div>
<main id="main-content"> <main id="main-content">
<h1 transition:name={slugifyStr(title)} class="post-title">{title}</h1> <h1 transition:name={slugifyStr(title)} class="post-title">{title}</h1>
<Datetime datetime={pubDatetime} size="lg" className="my-2" /> <Datetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
size="lg"
className="my-2"
/>
<article id="article" role="article" class="prose mx-auto mt-8 max-w-3xl"> <article id="article" role="article" class="prose mx-auto mt-8 max-w-3xl">
<Content /> <Content />
</article> </article>
+9 -4
View File
@@ -1,12 +1,17 @@
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
const getSortedPosts = (posts: CollectionEntry<"blog">[]) => const getSortedPosts = (posts: CollectionEntry<"blog">[]) => {
posts return posts
.filter(({ data }) => !data.draft) .filter(({ data }) => !data.draft)
.sort( .sort(
(a, b) => (a, b) =>
Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) - Math.floor(
Math.floor(new Date(a.data.pubDatetime).getTime() / 1000) new Date(b.data.modDatetime ?? b.data.pubDatetime).getTime() / 1000
) -
Math.floor(
new Date(a.data.modDatetime ?? a.data.pubDatetime).getTime() / 1000
)
); );
};
export default getSortedPosts; export default getSortedPosts;