diff --git a/source.config.ts b/source.config.ts index 4b1b2e4..7917344 100644 --- a/source.config.ts +++ b/source.config.ts @@ -90,14 +90,11 @@ export const category = defineCollections({ export const blog = defineCollections({ type: 'doc', dir: 'content/blog', - schema: (ctx) => { - // console.log('ctx', ctx); // {source, path} - return frontmatterSchema.extend({ - image: z.string(), - date: z.string().date(), - published: z.boolean().default(true), - categories: z.array(z.string()), - author: z.string(), - }); - }, + schema: frontmatterSchema.extend({ + image: z.string(), + date: z.string().date(), + published: z.boolean().default(true), + categories: z.array(z.string()), + author: z.string(), + }), }); diff --git a/src/app/[locale]/(marketing)/blog/(blog)/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/page.tsx index aaf6394..3aae748 100644 --- a/src/app/[locale]/(marketing)/blog/(blog)/page.tsx +++ b/src/app/[locale]/(marketing)/blog/(blog)/page.tsx @@ -1,6 +1,7 @@ import BlogGridWithPagination from '@/components/blog/blog-grid-with-pagination'; +import { websiteConfig } from '@/config/website'; import { LOCALES } from '@/i18n/routing'; -import { getPaginatedBlogPosts } from '@/lib/blog/data'; +import { blogSource } from '@/lib/docs/source'; import { constructMetadata } from '@/lib/metadata'; import { getUrlWithLocale } from '@/lib/urls/urls'; import type { Locale } from 'next-intl'; @@ -14,11 +15,11 @@ export async function generateMetadata({ params }: BlogPageProps) { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Metadata' }); const pt = await getTranslations({ locale, namespace: 'BlogPage' }); - const canonicalPath = '/blog'; + return constructMetadata({ title: `${pt('title')} | ${t('title')}`, description: pt('description'), - canonicalUrl: getUrlWithLocale(canonicalPath, locale), + canonicalUrl: getUrlWithLocale('/blog', locale), }); } @@ -30,14 +31,23 @@ interface BlogPageProps { export default async function BlogPage({ params }: BlogPageProps) { const { locale } = await params; - const currentPage = 1; - const { paginatedPosts, totalPages } = getPaginatedBlogPosts({ - locale, - page: currentPage, + const localePosts = blogSource.getPages(locale); + const publishedPosts = localePosts.filter((post) => post.data.published); + const sortedPosts = publishedPosts.sort((a, b) => { + return new Date(b.data.date).getTime() - new Date(a.data.date).getTime(); }); + const currentPage = 1; + const blogPageSize = websiteConfig.blog.paginationSize; + const paginatedLocalePosts = sortedPosts.slice( + (currentPage - 1) * blogPageSize, + currentPage * blogPageSize + ); + const totalPages = Math.ceil(sortedPosts.length / blogPageSize); + return ( diff --git a/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx b/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx index 83233c0..ee40d9a 100644 --- a/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx +++ b/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx @@ -1,16 +1,19 @@ import AllPostsButton from '@/components/blog/all-posts-button'; import BlogGrid from '@/components/blog/blog-grid'; -import { BlogToc } from '@/components/blog/blog-toc'; +import { getMDXComponents } from '@/components/custom/mdx-components'; import { NewsletterCard } from '@/components/newsletter/newsletter-card'; -import { CustomMDXContent } from '@/components/shared/custom-mdx-content'; import { websiteConfig } from '@/config/website'; import { LocaleLink } from '@/i18n/navigation'; -import { LOCALES } from '@/i18n/routing'; -import { getTableOfContents } from '@/lib/blog/toc'; +import { + type BlogType, + authorSource, + blogSource, + categorySource, +} from '@/lib/docs/source'; import { formatDate } from '@/lib/formatter'; import { constructMetadata } from '@/lib/metadata'; import { getUrlWithLocale } from '@/lib/urls/urls'; -import { type Post, allPosts } from 'content-collections'; +import { InlineTOC } from 'fumadocs-ui/components/inline-toc'; import { CalendarIcon, ClockIcon, FileTextIcon } from 'lucide-react'; import type { Metadata } from 'next'; import type { Locale } from 'next-intl'; @@ -20,49 +23,15 @@ import { notFound } from 'next/navigation'; import '@/styles/mdx.css'; -/** - * Gets the blog post from the params - * @param slug - The slug of the blog post - * @param locale - The locale of the blog post - * @returns The blog post - * - * How it works: - * /[locale]/blog/first-post: - * params.slug = ["first-post"] - * slug becomes "first-post" after join('/') - * Matches post where slugAsParams === "first-post" AND locale === params.locale - */ -async function getBlogPostFromParams(locale: Locale, slug: string) { - // console.log('getBlogPostFromParams', locale, slug); - // Find post with matching slug and locale - const post = allPosts.find( - (post) => - (post.slugAsParams === slug || - (!slug && post.slugAsParams === 'index')) && - post.locale === locale - ); - - if (!post) { - // If no post found with the current locale, try to find one with the default locale - const defaultPost = allPosts.find( - (post) => - post.slugAsParams === slug || (!slug && post.slugAsParams === 'index') - ); - - return defaultPost; - } - - return post; -} - /** * get related posts, random pick from all posts with same locale, different slug, * max size is websiteConfig.blog.relatedPostsSize */ -async function getRelatedPosts(post: Post) { - const relatedPosts = allPosts - .filter((p) => p.locale === post.locale) - .filter((p) => p.slugAsParams !== post.slugAsParams) +async function getRelatedPosts(post: BlogType) { + const relatedPosts = blogSource + .getPages(post.locale) + .filter((p) => p.data.published) + .filter((p) => p.slugs.join('/') !== post.slugs.join('/')) .sort(() => Math.random() - 0.5) .slice(0, websiteConfig.blog.relatedPostsSize); @@ -70,20 +39,22 @@ async function getRelatedPosts(post: Post) { } export function generateStaticParams() { - return LOCALES.flatMap((locale) => { - const posts = allPosts.filter((post) => post.locale === locale); - return posts.map((post) => ({ - locale, - slug: [post.slugAsParams], - })); - }); + return blogSource + .getPages() + .filter((post) => post.data.published) + .flatMap((post) => { + return { + locale: post.locale, + slug: post.slugs, + }; + }); } export async function generateMetadata({ params, }: BlogPostPageProps): Promise { const { locale, slug } = await params; - const post = await getBlogPostFromParams(locale, slug.join('/')); + const post = blogSource.getPage(slug, locale); if (!post) { notFound(); } @@ -91,10 +62,10 @@ export async function generateMetadata({ const t = await getTranslations({ locale, namespace: 'Metadata' }); return constructMetadata({ - title: `${post.title} | ${t('title')}`, - description: post.description, - canonicalUrl: getUrlWithLocale(post.slug, locale), - image: post.image, + title: `${post.data.title} | ${t('title')}`, + description: post.data.description, + canonicalUrl: getUrlWithLocale(`/blog/${slug}`, locale), + image: post.data.image, }); } @@ -107,14 +78,27 @@ interface BlogPostPageProps { export default async function BlogPostPage(props: BlogPostPageProps) { const { locale, slug } = await props.params; - const post = await getBlogPostFromParams(locale, slug.join('/')); + const post = blogSource.getPage(slug, locale); if (!post) { notFound(); } - const publishDate = post.date; - const date = formatDate(new Date(publishDate)); - const toc = await getTableOfContents(post.content); + const { date, title, description, image, author, categories } = post.data; + const publishDate = formatDate(new Date(date)); + // const toc = await getTableOfContents(post.data.body); + // console.log('post.data.toc', post.data.toc); + + // NOTICE: we can not call post.data.content here in Cloudflare Worker environment + // const wordCount = post.data.content.split(/\s+/).length; + // const wordsPerMinute = 200; // average reading speed: 200 words per minute + // const estimatedTime = Math.max(Math.ceil(wordCount / wordsPerMinute), 1); + + const blogAuthor = authorSource.getPage([author], locale); + const blogCategories = categorySource + .getPages(locale) + .filter((category) => categories.includes(category.slugs[0] ?? '')); + + const MDX = post.data.body; // getTranslations may cause error DYNAMIC_SERVER_USAGE, so we set dynamic to force-static const t = await getTranslations('BlogPage'); @@ -132,11 +116,11 @@ export default async function BlogPostPage(props: BlogPostPageProps) {
{/* blog post image */}
- {post.image && ( + {image && ( {post.title + {blogAuthor && ( +
+
+ {blogAuthor.data.avatar && ( + {`avatar + )} +
+ + {blogAuthor.data.name} + +
+ )} +
- {date} + {publishDate}
-
+ {/*
- {t('readTime', { minutes: post.estimatedTime })} + {t('readTime', { minutes: estimatedTime })} -
+
*/}
{/* blog post title */} -

{post.title}

+

{title}

{/* blog post description */} -

{post.description}

+

{description}

{/* blog post content */} {/* in order to make the mdx.css work, we need to add the className prose to the div */} {/* https://github.com/tailwindlabs/tailwindcss-typography */}
- +
@@ -183,36 +185,38 @@ export default async function BlogPostPage(props: BlogPostPageProps) {
{/* author info */} -
-

{t('author')}

-
-
- {post.author?.avatar && ( - {`avatar - )} + {/* {blogAuthor && ( +
+

{t('author')}

+
+
+ {blogAuthor.data.avatar && ( + {`avatar + )} +
+ {blogAuthor.data.name}
- {post.author?.name}
-
+ )} */} {/* categories */}

{t('categories')}

    - {post.categories?.filter(Boolean).map( + {blogCategories.map( (category) => category && ( -
  • +
  • - {category.name} + {category.data.name}
  • ) @@ -220,13 +224,20 @@ export default async function BlogPostPage(props: BlogPostPageProps) {
- {/* table of contents */} + {/* table of contents, https://fumadocs.dev/docs/ui/components/inline-toc */}

{t('tableOfContents')}

- + {post.data.toc && ( + + )}
@@ -243,7 +254,7 @@ export default async function BlogPostPage(props: BlogPostPageProps) {
- +
)} diff --git a/src/components/blog/blog-card.tsx b/src/components/blog/blog-card.tsx index 4c7d583..dd3efe9 100644 --- a/src/components/blog/blog-card.tsx +++ b/src/components/blog/blog-card.tsx @@ -18,12 +18,8 @@ export default function BlogCard({ locale, post }: BlogCardProps) { .getPages(locale) .filter((category) => categories.includes(category.slugs[0] ?? '')); - // Extract the slug parts for the Link component - const slugParts = post.slugs[0].split('/'); - // console.log('BlogCard, slugParts', slugParts); - return ( - +
{/* Image container - fixed aspect ratio */}
diff --git a/src/components/blog/blog-grid.tsx b/src/components/blog/blog-grid.tsx index 3944e89..81dc69e 100644 --- a/src/components/blog/blog-grid.tsx +++ b/src/components/blog/blog-grid.tsx @@ -14,7 +14,7 @@ export default function BlogGrid({ locale, posts }: BlogGridProps) { {posts?.length > 0 && (
{posts.map((post) => ( - + ))}
)} diff --git a/src/lib/docs/source.ts b/src/lib/docs/source.ts index f265948..c798c0c 100644 --- a/src/lib/docs/source.ts +++ b/src/lib/docs/source.ts @@ -82,6 +82,12 @@ export const blogSource = loader({ baseUrl: '/blog', i18n: docsI18nConfig, source: createMDXSource(blog), + transformers: [ + (page) => { + // console.log('page', page); + return page; + }, + ], }); export type ChangelogType = InferPageType;