From ce7fe5b45f665ea783c1d7f1becd97c3e162f975 Mon Sep 17 00:00:00 2001 From: javayhu Date: Thu, 6 Mar 2025 00:48:22 +0800 Subject: [PATCH] feat: enhance blog category filtering and pagination - Add blog category filter for desktop and mobile views - Implement category-based blog post filtering - Add pagination support for blog and category pages - Update localization messages for blog categories - Reduce posts per page from 9 to 6 - Add loading states for blog pages - Create new content collections for blog categories in Chinese --- .vscode/settings.json | 3 +- content/en/blog/what-is-indiehub.mdx | 2 +- content/zh/author/indiehub.mdx | 5 + content/zh/author/mkdirs.mdx | 5 + content/zh/author/mksaas.mdx | 5 + content/zh/blog/what-is-indiehub.mdx | 211 ++++++++++++++++++ content/zh/blog/what-is-mkdirs.mdx | 211 ++++++++++++++++++ content/zh/blog/what-is-mksaas.mdx | 2 +- content/zh/category/company.mdx | 6 + content/zh/category/guide.mdx | 6 + content/zh/category/news.mdx | 6 + messages/en.json | 11 +- messages/zh.json | 13 +- .../blog/(blog)/category/[slug]/page.tsx | 96 ++++++++ .../(marketing)/blog/(blog)/layout.tsx | 22 +- .../(marketing)/blog/(blog)/loading.tsx | 5 + .../[locale]/(marketing)/blog/(blog)/page.tsx | 53 ++++- src/app/[locale]/loading.tsx | 5 + src/components/blog/blog-card.tsx | 9 +- src/components/blog/blog-category-filter.tsx | 24 ++ .../blog/blog-category-list-desktop.tsx | 66 ++++++ .../blog/blog-category-list-mobile.tsx | 77 +++++++ src/components/shared/filter-item-mobile.tsx | 33 +++ src/lib/constants.ts | 2 +- 24 files changed, 854 insertions(+), 24 deletions(-) create mode 100644 content/zh/author/indiehub.mdx create mode 100644 content/zh/author/mkdirs.mdx create mode 100644 content/zh/author/mksaas.mdx create mode 100644 content/zh/blog/what-is-indiehub.mdx create mode 100644 content/zh/blog/what-is-mkdirs.mdx create mode 100644 content/zh/category/company.mdx create mode 100644 content/zh/category/guide.mdx create mode 100644 content/zh/category/news.mdx create mode 100644 src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx create mode 100644 src/app/[locale]/(marketing)/blog/(blog)/loading.tsx create mode 100644 src/app/[locale]/loading.tsx create mode 100644 src/components/blog/blog-category-filter.tsx create mode 100644 src/components/blog/blog-category-list-desktop.tsx create mode 100644 src/components/blog/blog-category-list-mobile.tsx create mode 100644 src/components/shared/filter-item-mobile.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index b0db98e..cf37414 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "i18n-ally.localesPaths": [ "messages", "src/i18n" - ] + ], + "i18n-ally.keystyle": "nested" } \ No newline at end of file diff --git a/content/en/blog/what-is-indiehub.mdx b/content/en/blog/what-is-indiehub.mdx index 6fed952..4ac90e3 100644 --- a/content/en/blog/what-is-indiehub.mdx +++ b/content/en/blog/what-is-indiehub.mdx @@ -4,7 +4,7 @@ description: IndieHub is the best all-in-one directory for indie hackers. image: /images/blog/indiehub-og.png date: 2024-11-24T12:00:00.000Z published: true -categories: [news] +categories: [news, guide] author: indiehub --- diff --git a/content/zh/author/indiehub.mdx b/content/zh/author/indiehub.mdx new file mode 100644 index 0000000..a63325c --- /dev/null +++ b/content/zh/author/indiehub.mdx @@ -0,0 +1,5 @@ +--- +slug: indiehub +name: IndieHub +avatar: /images/avatars/indiehub.png +--- diff --git a/content/zh/author/mkdirs.mdx b/content/zh/author/mkdirs.mdx new file mode 100644 index 0000000..e5cf61f --- /dev/null +++ b/content/zh/author/mkdirs.mdx @@ -0,0 +1,5 @@ +--- +slug: mkdirs +name: Mkdirs +avatar: /images/avatars/mkdirs.png +--- diff --git a/content/zh/author/mksaas.mdx b/content/zh/author/mksaas.mdx new file mode 100644 index 0000000..77581c6 --- /dev/null +++ b/content/zh/author/mksaas.mdx @@ -0,0 +1,5 @@ +--- +slug: mksaas +name: MkSaaS +avatar: /images/avatars/mksaas.png +--- diff --git a/content/zh/blog/what-is-indiehub.mdx b/content/zh/blog/what-is-indiehub.mdx new file mode 100644 index 0000000..43224f2 --- /dev/null +++ b/content/zh/blog/what-is-indiehub.mdx @@ -0,0 +1,211 @@ +--- +title: IndieHub 是什么? +description: IndieHub是一站式的独立开发者导航站。 +image: /images/blog/indiehub-og.png +date: 2024-11-24T12:00:00.000Z +published: true +categories: [news, guide] +author: indiehub +locale: zh +--- + +到目前为止,尝试使用 Tailwind 来设计文章、文档或博客文章的样式一直是一项繁琐的任务,需要对排版有敏锐的眼光,并且需要大量复杂的自定义 CSS。 + +默认情况下,Tailwind 会删除段落、标题、列表等所有默认的浏览器样式。这对于构建应用程序 UI 非常有用,因为您花更少的时间撤销用户代理样式,但是当您真的只是尝试设置来自 CMS 中富文本编辑器或 markdown 文件的内容的样式时,这可能会令人惊讶和不直观。 + +我们实际上收到了很多关于它的投诉,人们经常问我们这样的问题: + +> 为什么 Tailwind 删除了我的 `h1` 元素上的默认样式?我如何禁用这个?你说我也会失去所有其他基本样式是什么意思? +> 我们听到了您的声音,但我们并不确信简单地禁用我们的基本样式就是您真正想要的。您不希望每次在仪表板 UI 的一部分中使用 `p` 元素时都必须删除烦人的边距。而且我怀疑您真的希望您的博客文章使用用户代理样式——您希望它们看起来很棒,而不是糟糕。 + +`@tailwindcss/typography` 插件是我们尝试给您真正想要的东西,而不会有做一些愚蠢的事情(比如禁用我们的基本样式)的任何缺点。 + +它添加了一个新的 `prose` 类,您可以将其应用于任何普通 HTML 内容块,并将其转变为一个美丽、格式良好的文档: + +```html +
+

大蒜面包配奶酪:科学告诉我们什么

+

+ 多年来,父母一直向他们的孩子宣扬吃大蒜面包配奶酪的健康益处,这种食物在我们的文化中获得了如此标志性的地位,以至于孩子们经常在万圣节装扮成温暖、奶酪味的面包。 +

+

+ 但最近的一项研究表明,这种受欢迎的开胃菜可能与全国各地出现的一系列狂犬病病例有关。 +

+
+``` + +有关如何使用该插件及其包含的功能的更多信息,[阅读文档](https://github.com/tailwindcss/typography/blob/master/README.md)。 + +--- + +## 从现在开始期待什么 + +从这里开始的是我写的一堆绝对无意义的内容,用来测试插件本身。它包括我能想到的每一个合理的排版元素,如**粗体文本**、无序列表、有序列表、代码块、块引用,_甚至斜体_。 + +涵盖所有这些用例很重要,原因如下: + +1. 我们希望一切开箱即用看起来都很好。 +2. 实际上只是第一个原因,这是插件的全部意义。 +3. 这里有第三个假装的原因,尽管一个有三个项目的列表看起来比一个有两个项目的列表更真实。 + +现在我们将尝试另一种标题样式。 + +### 排版应该很简单 + +所以这是给你的一个标题——如果我们做得正确,那应该看起来相当合理。 + +一位智者曾经告诉我关于排版的一件事是: + +> 如果你不希望你的东西看起来像垃圾,排版是非常重要的。做好它,那么它就不会糟糕。 + +默认情况下,图片在这里看起来也应该不错: + +{/* 图片 */} + +与普遍的看法相反,Lorem Ipsum 并不是简单的随机文本。它起源于公元前 45 年的一段古典拉丁文学,使其有超过 2000 年的历史。 + +现在我将向您展示一个无序列表的例子,以确保它看起来也不错: + +- 所以这是这个列表中的第一项。 +- 在这个例子中,我们保持项目简短。 +- 稍后,我们将使用更长、更复杂的列表项。 + +这就是本节的结尾。 + +## 如果我们堆叠标题怎么办? + +### 我们也应该确保这看起来不错。 + +有时候你有直接堆叠在一起的标题。在这些情况下,你通常必须取消第二个标题上的顶部边距,因为标题彼此靠得更近通常看起来比段落后面跟着标题要好。 + +### 当标题在段落之后出现时…… + +当标题在段落之后出现时,我们需要更多的空间,就像我上面已经提到的那样。现在让我们看看一个更复杂的列表会是什么样子。 + +- **我经常做这种事,列表项有标题。** + + 由于某种原因,我认为这看起来很酷,这很不幸,因为要让样式正确是相当烦人的。 + + 我在这些列表项中通常也有两到三个段落,所以困难的部分是让段落之间的间距、列表项标题和单独的列表项都有意义。老实说,这很困难,你可以提出一个强有力的论点,认为你根本不应该这样写。 + +- **由于这是一个列表,我至少需要两个项目。** + + 我已经在前面的列表项中解释了我在做什么,但是如果一个列表只有一个项目,那就不是一个列表,我们真的希望这看起来真实。这就是为什么我添加了这第二个列表项,所以我在写样式时实际上有东西可以看。 + +- **添加第三项也不是一个坏主意。** + + 我认为只使用两个项目可能已经足够了,但三个肯定不会更糟,而且由于我似乎在编造任意的东西时没有遇到麻烦,我不妨包括它。 + +在这种列表之后,我通常会有一个结束语或段落,因为直接跳到标题看起来有点奇怪。 + +## 代码默认应该看起来不错。 + +我认为大多数人如果想要设置他们的代码块的样式,会使用 [highlight.js](https://highlightjs.org/) 或 [Prism](https://prismjs.com/) 或其他东西,但是让它们开箱即用看起来_还不错_,即使没有语法高亮,也不会有害。 + +以下是撰写本文时默认的 `tailwind.config.js` 文件的样子: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +希望这对你来说看起来足够好。 + +### 嵌套列表怎么办? + +嵌套列表基本上总是看起来很糟糕,这就是为什么像 Medium 这样的编辑器甚至不让你这样做,但我猜既然你们中的一些傻瓜要这样做,我们至少要承担让它工作的负担。 + +1. **嵌套列表很少是一个好主意。** + - 你可能觉得你真的很"有组织"或者什么的,但你只是在屏幕上创建一个难以阅读的粗糙形状。 + - UI 中的嵌套导航也是一个坏主意,尽可能保持扁平。 + - 在源代码中嵌套大量文件夹也没有帮助。 +2. **既然我们需要有更多的项目,这里有另一个。** + - 我不确定我们是否会费心设置超过两级深度的样式。 + - 两级已经太多了,三级肯定是一个坏主意。 + - 如果你嵌套四级深度,你应该进监狱。 +3. **两个项目并不是真正的列表,三个项目就好了。** + - 再次请不要嵌套列表,如果你希望人们真正阅读你的内容。 + - 没有人想看这个。 + - 我很不高兴我们甚至必须费心设置这个样式。 + +Markdown 中列表最烦人的事情是,除非列表项中有多个段落,否则 `
  • ` 元素不会被赋予子 `

    ` 标签。这意味着我也必须担心设置那种烦人情况的样式。 + +- **例如,这里是另一个嵌套列表。** + + 但这次有第二段。 + + - 这些列表项不会有 `

    ` 标签 + - 因为它们每个只有一行 + +- **但在这第二个顶级列表项中,它们会有。** + + 这特别烦人,因为这段话的间距。 + + - 正如你在这里看到的,因为我添加了第二行,这个列表项现在有一个 `

    ` 标签。 + + 顺便说一下,这是我说的第二行。 + + - 最后这里有另一个列表项,所以它更像一个列表。 + +- 一个结束列表项,但没有嵌套列表,为什么不呢? + +最后一句话结束这一节。 + +## 还有其他我们需要设置样式的元素 + +我几乎忘了提到链接,比如[这个链接到 Tailwind CSS 网站](https://tailwindcss.com)。我们几乎把它们变成蓝色,但那是昨天的事了,所以我们选择了深灰色,感觉更前卫。 + +我们甚至包括了表格样式,看看: + +| 摔跤手 | 出生地 | 终结技 | +| ----------------------- | ------------- | ------------------- | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +我们还需要确保内联代码看起来不错,比如如果我想谈论 `` 元素或者告诉你关于 `@tailwindcss/typography` 的好消息。 + +### 有时我甚至在标题中使用 `code` + +尽管这可能是一个坏主意,而且历史上我一直很难让它看起来不错。不过这个_"将代码块包裹在反引号中"_的技巧效果相当不错。 + +我过去做过的另一件事是在链接中放置一个 `code` 标签,比如如果我想告诉你关于 [`tailwindcss/docs`](https://github.com/tailwindcss/docs) 仓库的事情。我不喜欢反引号下面有下划线,但为了避免它而导致的疯狂绝对不值得。 + +#### 我们还没有使用 `h4` + +但现在我们有了。请不要在你的内容中使用 `h5` 或 `h6`,Medium 只支持两个标题级别是有原因的,你们这些动物。我老实说考虑过使用 `before` 伪元素,如果你使用 `h5` 或 `h6` 就对你大喊大叫。 + +我们根本不会为它们设置样式,因为 `h4` 元素已经很小,与正文大小相同。我们应该怎么处理 `h5`,让它比正文更_小_?不,谢谢。 + +### 不过我们仍然需要考虑堆叠的标题。 + +#### 让我们确保我们也不会用 `h4` 元素搞砸这个。 + +呼,运气好的话,我们已经设置了上面这段文字的标题样式,它们看起来相当不错。 + +让我们在这里添加一个结束段落,这样事情就会以一个相当大小的文本块结束。我无法解释为什么我希望事情以这种方式结束,但我必须假设这是因为我认为如果文档末尾太靠近标题,事情会看起来奇怪或不平衡。 + +我在这里写的可能已经足够长了,但添加这最后一句话不会有害。 + +## GitHub 风格的 Markdown + +我还添加了对使用 `remark-gfm` 的 GitHub 风格 Markdown 的支持。 + +使用 `remark-gfm`,我们在 markdown 中获得了一些额外的功能。例如:自动链接文字。 + +像 www.example.com 或 https://example.com 这样的链接会自动转换为 `a` 标签。 + +这对电子邮件链接也有效:contact@example.com。 \ No newline at end of file diff --git a/content/zh/blog/what-is-mkdirs.mdx b/content/zh/blog/what-is-mkdirs.mdx new file mode 100644 index 0000000..83237cd --- /dev/null +++ b/content/zh/blog/what-is-mkdirs.mdx @@ -0,0 +1,211 @@ +--- +title: Mkdirs 是什么? +description: Mkdirs 是构建导航站的最佳代码模板。 +image: /images/blog/mkdirs-og.png +date: 2024-11-25T12:00:00.000Z +published: true +categories: [news, guide] +author: mkdirs +locale: zh +--- + +到目前为止,尝试使用 Tailwind 来设计文章、文档或博客文章的样式一直是一项繁琐的任务,需要对排版有敏锐的眼光,并且需要大量复杂的自定义 CSS。 + +默认情况下,Tailwind 会删除段落、标题、列表等所有默认的浏览器样式。这对于构建应用程序 UI 非常有用,因为您花更少的时间撤销用户代理样式,但是当您真的只是尝试设置来自 CMS 中富文本编辑器或 markdown 文件的内容的样式时,这可能会令人惊讶和不直观。 + +我们实际上收到了很多关于它的投诉,人们经常问我们这样的问题: + +> 为什么 Tailwind 删除了我的 `h1` 元素上的默认样式?我如何禁用这个?你说我也会失去所有其他基本样式是什么意思? +> 我们听到了您的声音,但我们并不确信简单地禁用我们的基本样式就是您真正想要的。您不希望每次在仪表板 UI 的一部分中使用 `p` 元素时都必须删除烦人的边距。而且我怀疑您真的希望您的博客文章使用用户代理样式——您希望它们看起来很棒,而不是糟糕。 + +`@tailwindcss/typography` 插件是我们尝试给您真正想要的东西,而不会有做一些愚蠢的事情(比如禁用我们的基本样式)的任何缺点。 + +它添加了一个新的 `prose` 类,您可以将其应用于任何普通 HTML 内容块,并将其转变为一个美丽、格式良好的文档: + +```html +

    +

    大蒜面包配奶酪:科学告诉我们什么

    +

    + 多年来,父母一直向他们的孩子宣扬吃大蒜面包配奶酪的健康益处,这种食物在我们的文化中获得了如此标志性的地位,以至于孩子们经常在万圣节装扮成温暖、奶酪味的面包。 +

    +

    + 但最近的一项研究表明,这种受欢迎的开胃菜可能与全国各地出现的一系列狂犬病病例有关。 +

    +
    +``` + +有关如何使用该插件及其包含的功能的更多信息,[阅读文档](https://github.com/tailwindcss/typography/blob/master/README.md)。 + +--- + +## 从现在开始期待什么 + +从这里开始的是我写的一堆绝对无意义的内容,用来测试插件本身。它包括我能想到的每一个合理的排版元素,如**粗体文本**、无序列表、有序列表、代码块、块引用,_甚至斜体_。 + +涵盖所有这些用例很重要,原因如下: + +1. 我们希望一切开箱即用看起来都很好。 +2. 实际上只是第一个原因,这是插件的全部意义。 +3. 这里有第三个假装的原因,尽管一个有三个项目的列表看起来比一个有两个项目的列表更真实。 + +现在我们将尝试另一种标题样式。 + +### 排版应该很简单 + +所以这是给你的一个标题——如果我们做得正确,那应该看起来相当合理。 + +一位智者曾经告诉我关于排版的一件事是: + +> 如果你不希望你的东西看起来像垃圾,排版是非常重要的。做好它,那么它就不会糟糕。 + +默认情况下,图片在这里看起来也应该不错: + +{/* 图片 */} + +与普遍的看法相反,Lorem Ipsum 并不是简单的随机文本。它起源于公元前 45 年的一段古典拉丁文学,使其有超过 2000 年的历史。 + +现在我将向您展示一个无序列表的例子,以确保它看起来也不错: + +- 所以这是这个列表中的第一项。 +- 在这个例子中,我们保持项目简短。 +- 稍后,我们将使用更长、更复杂的列表项。 + +这就是本节的结尾。 + +## 如果我们堆叠标题怎么办? + +### 我们也应该确保这看起来不错。 + +有时候你有直接堆叠在一起的标题。在这些情况下,你通常必须取消第二个标题上的顶部边距,因为标题彼此靠得更近通常看起来比段落后面跟着标题要好。 + +### 当标题在段落之后出现时…… + +当标题在段落之后出现时,我们需要更多的空间,就像我上面已经提到的那样。现在让我们看看一个更复杂的列表会是什么样子。 + +- **我经常做这种事,列表项有标题。** + + 由于某种原因,我认为这看起来很酷,这很不幸,因为要让样式正确是相当烦人的。 + + 我在这些列表项中通常也有两到三个段落,所以困难的部分是让段落之间的间距、列表项标题和单独的列表项都有意义。老实说,这很困难,你可以提出一个强有力的论点,认为你根本不应该这样写。 + +- **由于这是一个列表,我至少需要两个项目。** + + 我已经在前面的列表项中解释了我在做什么,但是如果一个列表只有一个项目,那就不是一个列表,我们真的希望这看起来真实。这就是为什么我添加了这第二个列表项,所以我在写样式时实际上有东西可以看。 + +- **添加第三项也不是一个坏主意。** + + 我认为只使用两个项目可能已经足够了,但三个肯定不会更糟,而且由于我似乎在编造任意的东西时没有遇到麻烦,我不妨包括它。 + +在这种列表之后,我通常会有一个结束语或段落,因为直接跳到标题看起来有点奇怪。 + +## 代码默认应该看起来不错。 + +我认为大多数人如果想要设置他们的代码块的样式,会使用 [highlight.js](https://highlightjs.org/) 或 [Prism](https://prismjs.com/) 或其他东西,但是让它们开箱即用看起来_还不错_,即使没有语法高亮,也不会有害。 + +以下是撰写本文时默认的 `tailwind.config.js` 文件的样子: + +```js +module.exports = { + purge: [], + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +}; +``` + +希望这对你来说看起来足够好。 + +### 嵌套列表怎么办? + +嵌套列表基本上总是看起来很糟糕,这就是为什么像 Medium 这样的编辑器甚至不让你这样做,但我猜既然你们中的一些傻瓜要这样做,我们至少要承担让它工作的负担。 + +1. **嵌套列表很少是一个好主意。** + - 你可能觉得你真的很"有组织"或者什么的,但你只是在屏幕上创建一个难以阅读的粗糙形状。 + - UI 中的嵌套导航也是一个坏主意,尽可能保持扁平。 + - 在源代码中嵌套大量文件夹也没有帮助。 +2. **既然我们需要有更多的项目,这里有另一个。** + - 我不确定我们是否会费心设置超过两级深度的样式。 + - 两级已经太多了,三级肯定是一个坏主意。 + - 如果你嵌套四级深度,你应该进监狱。 +3. **两个项目并不是真正的列表,三个项目就好了。** + - 再次请不要嵌套列表,如果你希望人们真正阅读你的内容。 + - 没有人想看这个。 + - 我很不高兴我们甚至必须费心设置这个样式。 + +Markdown 中列表最烦人的事情是,除非列表项中有多个段落,否则 `
  • ` 元素不会被赋予子 `

    ` 标签。这意味着我也必须担心设置那种烦人情况的样式。 + +- **例如,这里是另一个嵌套列表。** + + 但这次有第二段。 + + - 这些列表项不会有 `

    ` 标签 + - 因为它们每个只有一行 + +- **但在这第二个顶级列表项中,它们会有。** + + 这特别烦人,因为这段话的间距。 + + - 正如你在这里看到的,因为我添加了第二行,这个列表项现在有一个 `

    ` 标签。 + + 顺便说一下,这是我说的第二行。 + + - 最后这里有另一个列表项,所以它更像一个列表。 + +- 一个结束列表项,但没有嵌套列表,为什么不呢? + +最后一句话结束这一节。 + +## 还有其他我们需要设置样式的元素 + +我几乎忘了提到链接,比如[这个链接到 Tailwind CSS 网站](https://tailwindcss.com)。我们几乎把它们变成蓝色,但那是昨天的事了,所以我们选择了深灰色,感觉更前卫。 + +我们甚至包括了表格样式,看看: + +| 摔跤手 | 出生地 | 终结技 | +| ----------------------- | ------------- | ------------------- | +| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter | +| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner | +| Randy Savage | Sarasota, FL | Elbow Drop | +| Vader | Boulder, CO | Vader Bomb | +| Razor Ramon | Chuluota, FL | Razor's Edge | + +我们还需要确保内联代码看起来不错,比如如果我想谈论 `` 元素或者告诉你关于 `@tailwindcss/typography` 的好消息。 + +### 有时我甚至在标题中使用 `code` + +尽管这可能是一个坏主意,而且历史上我一直很难让它看起来不错。不过这个_"将代码块包裹在反引号中"_的技巧效果相当不错。 + +我过去做过的另一件事是在链接中放置一个 `code` 标签,比如如果我想告诉你关于 [`tailwindcss/docs`](https://github.com/tailwindcss/docs) 仓库的事情。我不喜欢反引号下面有下划线,但为了避免它而导致的疯狂绝对不值得。 + +#### 我们还没有使用 `h4` + +但现在我们有了。请不要在你的内容中使用 `h5` 或 `h6`,Medium 只支持两个标题级别是有原因的,你们这些动物。我老实说考虑过使用 `before` 伪元素,如果你使用 `h5` 或 `h6` 就对你大喊大叫。 + +我们根本不会为它们设置样式,因为 `h4` 元素已经很小,与正文大小相同。我们应该怎么处理 `h5`,让它比正文更_小_?不,谢谢。 + +### 不过我们仍然需要考虑堆叠的标题。 + +#### 让我们确保我们也不会用 `h4` 元素搞砸这个。 + +呼,运气好的话,我们已经设置了上面这段文字的标题样式,它们看起来相当不错。 + +让我们在这里添加一个结束段落,这样事情就会以一个相当大小的文本块结束。我无法解释为什么我希望事情以这种方式结束,但我必须假设这是因为我认为如果文档末尾太靠近标题,事情会看起来奇怪或不平衡。 + +我在这里写的可能已经足够长了,但添加这最后一句话不会有害。 + +## GitHub 风格的 Markdown + +我还添加了对使用 `remark-gfm` 的 GitHub 风格 Markdown 的支持。 + +使用 `remark-gfm`,我们在 markdown 中获得了一些额外的功能。例如:自动链接文字。 + +像 www.example.com 或 https://example.com 这样的链接会自动转换为 `a` 标签。 + +这对电子邮件链接也有效:contact@example.com。 \ No newline at end of file diff --git a/content/zh/blog/what-is-mksaas.mdx b/content/zh/blog/what-is-mksaas.mdx index 82ae335..bb5d011 100644 --- a/content/zh/blog/what-is-mksaas.mdx +++ b/content/zh/blog/what-is-mksaas.mdx @@ -1,6 +1,6 @@ --- title: MkSaaS 是什么? -description: MkSaaS 是构建 AI SaaS 网站的最佳样板。 +description: MkSaaS 是构建 AI SaaS 网站的最佳代码模板。 image: /images/blog/mksaas-og.png date: 2024-11-26T12:00:00.000Z published: true diff --git a/content/zh/category/company.mdx b/content/zh/category/company.mdx new file mode 100644 index 0000000..c2fd020 --- /dev/null +++ b/content/zh/category/company.mdx @@ -0,0 +1,6 @@ +--- +slug: company +name: 公司 +description: 公司新闻 +locale: zh +--- diff --git a/content/zh/category/guide.mdx b/content/zh/category/guide.mdx new file mode 100644 index 0000000..41d370b --- /dev/null +++ b/content/zh/category/guide.mdx @@ -0,0 +1,6 @@ +--- +slug: guide +name: 指南 +description: 使用指南 +locale: zh +--- diff --git a/content/zh/category/news.mdx b/content/zh/category/news.mdx new file mode 100644 index 0000000..d9ff69b --- /dev/null +++ b/content/zh/category/news.mdx @@ -0,0 +1,6 @@ +--- +slug: news +name: 新闻 +description: 最新新闻 +locale: zh +--- diff --git a/messages/en.json b/messages/en.json index 61146f8..1d10fa6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -57,5 +57,14 @@ "backToLogin": "Back to login", "checkEmail": "Please check your email inbox" } + }, + "BlogPage": { + "title": "Blog", + "categories": { + "all": "All", + "news": "News", + "guide": "Guide", + "company": "Company" + } } -} +} \ No newline at end of file diff --git a/messages/zh.json b/messages/zh.json index d9ce456..5206f47 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -57,5 +57,14 @@ "backToLogin": "返回登录", "checkEmail": "请检查您的邮箱" } - } -} + }, + "BlogPage": { + "title": "博客", + "categories": { + "all": "全部", + "news": "新闻", + "guide": "指南", + "company": "公司" + } + } +} \ No newline at end of file diff --git a/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx new file mode 100644 index 0000000..faa1b6d --- /dev/null +++ b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx @@ -0,0 +1,96 @@ +import BlogGrid from "@/components/blog/blog-grid"; +import EmptyGrid from "@/components/shared/empty-grid"; +import CustomPagination from "@/components/shared/pagination"; +import { siteConfig } from "@/config/site"; +import { POSTS_PER_PAGE } from "@/lib/constants"; +import { constructMetadata } from "@/lib/metadata"; +import type { Metadata } from "next"; +import { allCategories, allPosts } from "content-collections"; + +export async function generateMetadata({ + params, +}: { + params: { slug: string }; +}): Promise { + const category = allCategories.find( + (category) => category.slug === params.slug + ); + + if (!category) { + console.warn( + `generateMetadata, category not found for slug: ${params.slug}`, + ); + return; + } + + const ogImageUrl = new URL(`${siteConfig.url}/api/og`); + ogImageUrl.searchParams.append("title", category.name); + ogImageUrl.searchParams.append("description", category.description || ""); + ogImageUrl.searchParams.append("type", "Blog Category"); + + return constructMetadata({ + title: `${category.name}`, + description: category.description, + canonicalUrl: `${siteConfig.url}/blog/category/${params.slug}`, + // image: ogImageUrl.toString(), + }); +} + +export default async function BlogCategoryPage({ + params, + searchParams, +}: { + params: { slug: string; locale: string }; + searchParams?: { [key: string]: string | string[] | undefined }; +}) { + const { page } = searchParams as { [key: string]: string }; + const currentPage = page ? Number(page) : 1; + const startIndex = (currentPage - 1) * POSTS_PER_PAGE; + const endIndex = startIndex + POSTS_PER_PAGE; + + // Filter posts by category and locale + const filteredPosts = allPosts.filter( + (post) => + post.published && + post.locale === params.locale && + post.categories.some(category => category.slug === params.slug) + ); + + // Sort posts by date (newest first) + const sortedPosts = [...filteredPosts].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + + // Paginate posts + const paginatedPosts = sortedPosts.slice(startIndex, endIndex); + const totalCount = filteredPosts.length; + const totalPages = Math.ceil(totalCount / POSTS_PER_PAGE); + + console.log( + "BlogCategoryPage, totalCount", + totalCount, + ", totalPages", + totalPages, + ); + + return ( +

    + {/* when no posts are found */} + {paginatedPosts.length === 0 && } + + {/* when posts are found */} + {paginatedPosts.length > 0 && ( +
    + + +
    + +
    +
    + )} +
    + ); +} diff --git a/src/app/[locale]/(marketing)/blog/(blog)/layout.tsx b/src/app/[locale]/(marketing)/blog/(blog)/layout.tsx index c07c4ba..8b9f68d 100644 --- a/src/app/[locale]/(marketing)/blog/(blog)/layout.tsx +++ b/src/app/[locale]/(marketing)/blog/(blog)/layout.tsx @@ -1,10 +1,26 @@ +import { BlogCategoryFilter } from '@/components/blog/blog-category-filter'; import Container from '@/components/container'; import { HeaderSection } from '@/components/shared/header-section'; import { PropsWithChildren } from 'react'; +import { allCategories } from 'content-collections'; -export default async function BlogListLayout({ +interface BlogListLayoutProps extends PropsWithChildren { + params: { + locale: string; + }; +} + +export default async function BlogListLayout({ children, -}: { children: React.ReactNode }) { + params +}: BlogListLayoutProps) { + const { locale } = params; + + // Filter categories by locale + const categoryList = allCategories.filter( + category => category.locale === locale + ); + return (
    @@ -14,7 +30,7 @@ export default async function BlogListLayout({ subtitle="Read our latest blog posts about MkSaaS" /> - {/* */} +
    diff --git a/src/app/[locale]/(marketing)/blog/(blog)/loading.tsx b/src/app/[locale]/(marketing)/blog/(blog)/loading.tsx new file mode 100644 index 0000000..029f260 --- /dev/null +++ b/src/app/[locale]/(marketing)/blog/(blog)/loading.tsx @@ -0,0 +1,5 @@ +import { BlogGridSkeleton } from "@/components/blog/blog-grid"; + +export default function Loading() { + return ; +} diff --git a/src/app/[locale]/(marketing)/blog/(blog)/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/page.tsx index a49291f..f86b23f 100644 --- a/src/app/[locale]/(marketing)/blog/(blog)/page.tsx +++ b/src/app/[locale]/(marketing)/blog/(blog)/page.tsx @@ -1,8 +1,9 @@ import { allPosts } from 'content-collections'; import { Metadata } from 'next'; import BlogGrid from '@/components/blog/blog-grid'; -import Container from '@/components/container'; -import { Separator } from '@/components/ui/separator'; +import EmptyGrid from '@/components/shared/empty-grid'; +import CustomPagination from '@/components/shared/pagination'; +import { POSTS_PER_PAGE } from '@/lib/constants'; export async function generateMetadata(): Promise { return { @@ -12,13 +13,18 @@ export async function generateMetadata(): Promise { } interface BlogPageProps { - params: Promise<{ + params: { locale: string; - }>; + }; + searchParams?: { [key: string]: string | string[] | undefined }; } -export default async function BlogPage({ params }: BlogPageProps) { - const { locale } = await params; +export default async function BlogPage({ params, searchParams }: BlogPageProps) { + const { locale } = params; + const { page } = searchParams as { [key: string]: string }; + const currentPage = page ? Number(page) : 1; + const startIndex = (currentPage - 1) * POSTS_PER_PAGE; + const endIndex = startIndex + POSTS_PER_PAGE; // Filter posts by locale const localePosts = allPosts.filter( @@ -26,16 +32,45 @@ export default async function BlogPage({ params }: BlogPageProps) { ); // If no posts found for the current locale, show all published posts - const posts = localePosts.length > 0 + const filteredPosts = localePosts.length > 0 ? localePosts : allPosts.filter((post) => post.published); // Sort posts by date (newest first) - const sortedPosts = [...posts].sort( + const sortedPosts = [...filteredPosts].sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); + + // Paginate posts + const paginatedPosts = sortedPosts.slice(startIndex, endIndex); + const totalCount = filteredPosts.length; + const totalPages = Math.ceil(totalCount / POSTS_PER_PAGE); + + console.log( + "BlogPage, totalCount", + totalCount, + ", totalPages", + totalPages, + ); return ( - +
    + {/* when no posts are found */} + {paginatedPosts.length === 0 && } + + {/* when posts are found */} + {paginatedPosts.length > 0 && ( +
    + + +
    + +
    +
    + )} +
    ); } \ No newline at end of file diff --git a/src/app/[locale]/loading.tsx b/src/app/[locale]/loading.tsx new file mode 100644 index 0000000..d730bb7 --- /dev/null +++ b/src/app/[locale]/loading.tsx @@ -0,0 +1,5 @@ +import { Loader2Icon } from "lucide-react"; + +export default function Loading() { + return ; +} diff --git a/src/components/blog/blog-card.tsx b/src/components/blog/blog-card.tsx index 949eabb..1c1b361 100644 --- a/src/components/blog/blog-card.tsx +++ b/src/components/blog/blog-card.tsx @@ -3,7 +3,7 @@ import { getBaseUrl } from "@/lib/urls/get-base-url"; import { getLocaleDate } from "@/lib/utils"; import { Post } from "content-collections"; import Image from "next/image"; -import Link from "next/link"; +import { Link } from "@/i18n/navigation"; interface BlogCardProps { post: Post; @@ -12,7 +12,6 @@ interface BlogCardProps { export default function BlogCard({ post }: BlogCardProps) { const publishDate = post.date; const date = getLocaleDate(publishDate); - // const postUrlPrefix = "/blog"; const postUrlPrefix = getBaseUrl(); const postUrl = `${postUrlPrefix}${post.slug}`; @@ -20,7 +19,7 @@ export default function BlogCard({ post }: BlogCardProps) {
    {/* Image container - fixed aspect ratio */}
    - + {post.image && (
    {/* Post title */}

    - + {post.description && (

    - {post.description} + {post.description}

    )}

    diff --git a/src/components/blog/blog-category-filter.tsx b/src/components/blog/blog-category-filter.tsx new file mode 100644 index 0000000..5229eaf --- /dev/null +++ b/src/components/blog/blog-category-filter.tsx @@ -0,0 +1,24 @@ +import Container from "@/components/container"; +import { Category } from "content-collections"; +import { BlogCategoryListDesktop } from "./blog-category-list-desktop"; +import { BlogCategoryListMobile } from "./blog-category-list-mobile"; + +interface BlogCategoryFilterProps { + categoryList: Category[]; +} + +export function BlogCategoryFilter({ categoryList }: BlogCategoryFilterProps) { + return ( +
    + {/* Desktop View, has Container */} + + + + + {/* Mobile View, no Container */} +
    + +
    +
    + ); +} diff --git a/src/components/blog/blog-category-list-desktop.tsx b/src/components/blog/blog-category-list-desktop.tsx new file mode 100644 index 0000000..8f118b7 --- /dev/null +++ b/src/components/blog/blog-category-list-desktop.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { cn } from "@/lib/utils"; +import { Category } from "content-collections"; +import { Link } from "@/i18n/navigation"; +import { useParams } from "next/navigation"; +import { useTranslations } from "next-intl"; + +export type BlogCategoryListDesktopProps = { + categoryList: Category[]; +}; + +export function BlogCategoryListDesktop({ + categoryList, +}: BlogCategoryListDesktopProps) { + const { slug } = useParams() as { slug?: string }; + const t = useTranslations("BlogPage.categories"); + + return ( +
    + {/* Desktop View */} +
    + + + +

    {t("BlogPage.categories.all")}

    + +
    + + {categoryList.map((category) => ( + + +

    {category.name}

    + +
    + ))} +
    +
    +
    + ); +} diff --git a/src/components/blog/blog-category-list-mobile.tsx b/src/components/blog/blog-category-list-mobile.tsx new file mode 100644 index 0000000..39a2c8e --- /dev/null +++ b/src/components/blog/blog-category-list-mobile.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { Category } from "content-collections"; +import { LayoutListIcon } from "lucide-react"; +import { useParams } from "next/navigation"; +import { useState } from "react"; +import { Drawer } from "vaul"; +import FilterItemMobile from "@/components/shared/filter-item-mobile"; + +export type BlogCategoryListMobileProps = { + categoryList: Category[]; +}; + +export function BlogCategoryListMobile({ + categoryList, +}: BlogCategoryListMobileProps) { + const [open, setOpen] = useState(false); + const { slug } = useParams() as { slug?: string }; + const selectedCategory = categoryList.find( + (category) => category.slug === slug, + ); + + const closeDrawer = () => { + setOpen(false); + }; + + return ( + + setOpen(true)} + className="flex items-center w-full p-3 border-y text-foreground/90" + > +
    +
    + + Category +
    + + {selectedCategory?.name ? `${selectedCategory?.name}` : "All"} + +
    +
    + + + + Category +
    +
    +
    + +
      + + + {categoryList.map((item) => ( + + ))} +
    + + + + + ); +} diff --git a/src/components/shared/filter-item-mobile.tsx b/src/components/shared/filter-item-mobile.tsx new file mode 100644 index 0000000..61b4b28 --- /dev/null +++ b/src/components/shared/filter-item-mobile.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Link } from "@/i18n/navigation"; + +interface FilterItemMobileProps { + title: string; + href: string; + active?: boolean; + clickAction?: () => void; +} + +export default function FilterItemMobile({ + title, + href, + active, + clickAction, +}: FilterItemMobileProps) { + return ( +
  • + + {title} + +
  • + ); +} \ No newline at end of file diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 24c5781..86dd757 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1 +1 @@ -export const POSTS_PER_PAGE = 9; +export const POSTS_PER_PAGE = 6;