mirror of
https://github.com/ChenQihan666/MyBlog-Next.git
synced 2026-08-14 11:29:33 +08:00
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:
@@ -16,7 +16,7 @@ breadcrumbList[0] === "posts" &&
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<span aria-hidden="true"> > </span>
|
<span aria-hidden="true">></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"> > </span>
|
<span aria-hidden="true">></span>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -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"> at </span>
|
<span className="sr-only"> at </span>
|
||||||
{time}
|
<span className="text-nowrap">{time}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user