feat: add legal pages with content collections and localization support

- Implement pages collection in content-collections.ts for legal documents
- Add cookie policy, privacy policy, and terms of service pages for English and Chinese locales
- Create custom page layout and component for rendering legal documents
- Add metadata generation and custom page fetching logic
- Update not-found page to use LocaleLink for internationalization
- Enhance blog post page with prose styling
- Modify all posts button variant
This commit is contained in:
javayhu 2025-03-09 12:22:53 +08:00
parent 76748cf0b2
commit 9bd85d7b6f
16 changed files with 654 additions and 5 deletions

View File

@ -165,6 +165,73 @@ export const posts = defineCollection({
}
});
/**
* Pages collection for policy pages like privacy-policy, terms-of-service, etc.
*
* 1. For a page at content/en/pages/privacy-policy.md:
* locale: en
* slug: /pages/privacy-policy
* slugAsParams: privacy-policy
*
* 2. For a page at content/zh/pages/privacy-policy.md:
* locale: zh
* slug: /pages/privacy-policy
* slugAsParams: privacy-policy
*/
export const pages = defineCollection({
name: 'page',
directory: 'content',
include: '**/pages/**/*.{md,mdx}',
schema: (z) => ({
title: z.string(),
description: z.string(),
date: z.string().datetime(),
published: z.boolean().default(true),
locale: z.enum(LOCALES as [string, ...string[]]).optional()
}),
transform: async (data, context) => {
const body = await compileMDX(context, data, {
remarkPlugins: [
remarkGfm,
codeImport
],
rehypePlugins: [
rehypeSlug,
rehypeAutolinkHeadings,
[rehypePrettyCode, prettyCodeOptions]
]
});
// Determine the locale from the file path or use the provided locale
const pathParts = data._meta.path.split(path.sep);
const localeFromPath = LOCALES.includes(pathParts[0]) ? pathParts[0] : null;
const locale = data.locale || localeFromPath || DEFAULT_LOCALE;
// Create a slug without the locale in the path
let slugPath = data._meta.path;
if (localeFromPath) {
// Remove the locale from the path for the slug
const pathWithoutLocale = pathParts.slice(1).join(path.sep);
slugPath = pathWithoutLocale;
}
// Create slugAsParams without the locale
const slugParamsParts = slugPath.split(path.sep).slice(1);
const slugAsParams = slugParamsParts.join('/');
return {
...data,
locale,
slug: `/${slugPath}`,
slugAsParams,
body: {
raw: data.content,
code: body
}
};
}
});
const prettyCodeOptions: Options = {
theme: 'github-dark',
getHighlighter: (options) =>
@ -193,5 +260,5 @@ const prettyCodeOptions: Options = {
};
export default defineConfig({
collections: [authors, categories, posts]
collections: [authors, categories, posts, pages]
});

View File

@ -0,0 +1,63 @@
---
title: Cookie Policy
description: How we use cookies and similar technologies on our website
date: 2025-03-10T00:00:00.000Z
published: true
locale: en
---
## Introduction
This Cookie Policy explains how we use cookies and similar technologies on our website. By using our website, you consent to the use of cookies as described in this policy.
## What Are Cookies
Cookies are small text files that are stored on your device when you visit a website. They are widely used to make websites work more efficiently and provide information to the website owners.
## How We Use Cookies
We use cookies for the following purposes:
- **Essential Cookies**: These cookies are necessary for the website to function properly and cannot be switched off in our systems.
- **Performance Cookies**: These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site.
- **Functional Cookies**: These cookies enable the website to provide enhanced functionality and personalization.
- **Targeting Cookies**: These cookies may be set through our site by our advertising partners to build a profile of your interests.
## Types of Cookies We Use
### Essential Cookies
- Session cookies for maintaining user sessions
- Security cookies for protecting against fraud and abuse
### Performance Cookies
- Analytics cookies to understand how visitors interact with our website
- Load balancing cookies to distribute traffic to servers
### Functional Cookies
- Preference cookies to remember your settings and choices
- Language cookies to remember your language preference
### Targeting Cookies
- Advertising cookies to deliver relevant advertisements
- Social media cookies to enable sharing content on social platforms
## Managing Cookies
Most web browsers allow you to control cookies through their settings. You can:
- Delete cookies from your device
- Block cookies by activating the setting on your browser that allows you to refuse all or some cookies
- Set your browser to notify you when you receive a cookie
Please note that if you choose to block or delete cookies, you may not be able to access certain areas or features of our website.
## Changes to This Cookie Policy
We may update our Cookie Policy from time to time. We will notify you of any changes by posting the new Cookie Policy on this page.
## Contact Us
If you have any questions about this Cookie Policy, please contact us:
- By email: support@example.com
- By visiting our website: [Contact Page](https://example.com/contact)

View File

@ -0,0 +1,49 @@
---
title: Privacy Policy
description: Our commitment to protecting your privacy and personal data
date: 2025-03-10T00:00:00.000Z
published: true
locale: en
---
## Introduction
Welcome to our Privacy Policy. This document explains how we collect, use, and protect your personal information when you use our services.
## Information We Collect
We may collect the following types of information:
- **Personal Information**: Name, email address, and contact details you provide when registering or contacting us.
- **Usage Data**: Information about how you interact with our website, including pages visited and time spent.
- **Device Information**: Details about the device you use to access our services, such as IP address, browser type, and operating system.
## How We Use Your Information
We use your information for the following purposes:
- To provide and maintain our services
- To notify you about changes to our services
- To provide customer support
- To gather analysis or valuable information to improve our services
- To monitor the usage of our services
- To detect, prevent, and address technical issues
## Data Security
We implement appropriate security measures to protect your personal information from unauthorized access, alteration, disclosure, or destruction.
## Third-Party Services
We may employ third-party companies and individuals to facilitate our services, provide services on our behalf, or assist us in analyzing how our services are used.
## Changes to This Privacy Policy
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
## Contact Us
If you have any questions about this Privacy Policy, please contact us:
- By email: support@example.com
- By visiting our website: [Contact Page](https://example.com/contact)

View File

@ -0,0 +1,55 @@
---
title: Terms of Service
description: The terms and conditions governing the use of our services
date: 2025-03-10T00:00:00.000Z
published: true
locale: en
---
## Introduction
These Terms of Service ("Terms") govern your use of our website and services. By accessing or using our services, you agree to be bound by these Terms.
## Use of Services
Our services are provided "as is" and "as available" without warranties of any kind, either express or implied. We do not guarantee that our services will be uninterrupted, secure, or error-free.
## User Accounts
When you create an account with us, you must provide accurate and complete information. You are responsible for safeguarding your account and for all activities that occur under your account.
## Intellectual Property
Our website and its original content, features, and functionality are owned by us and are protected by international copyright, trademark, and other intellectual property laws.
## User Content
You retain all rights to any content you submit, post, or display on or through our services. By submitting content, you grant us a worldwide, non-exclusive, royalty-free license to use, reproduce, modify, and distribute your content.
## Prohibited Activities
You agree not to:
- Use our services in any way that violates any applicable law or regulation
- Engage in any conduct that restricts or inhibits anyone's use or enjoyment of our services
- Attempt to gain unauthorized access to our servers or networks
- Use our services to distribute malware or other harmful code
## Termination
We may terminate or suspend your account and access to our services immediately, without prior notice or liability, for any reason.
## Limitation of Liability
In no event shall we be liable for any indirect, incidental, special, consequential, or punitive damages resulting from your use of or inability to use our services.
## Changes to Terms
We reserve the right to modify these Terms at any time. If we make changes, we will provide notice by posting the updated Terms on this page.
## Contact Us
If you have any questions about these Terms, please contact us:
- By email: support@example.com
- By visiting our website: [Contact Page](https://example.com/contact)

View File

@ -0,0 +1,63 @@
---
title: Cookie 政策
description: 我们如何在网站上使用 Cookie 和类似技术
date: 2025-03-10T00:00:00.000Z
published: true
locale: zh
---
## 引言
本 Cookie 政策解释了我们如何在网站上使用 Cookie 和类似技术。通过使用我们的网站,您同意按照本政策中的描述使用 Cookie。
## 什么是 Cookie
Cookie 是在您访问网站时存储在您设备上的小型文本文件。它们被广泛用于使网站更高效地运行并向网站所有者提供信息。
## 我们如何使用 Cookie
我们将 Cookie 用于以下目的:
- **必要 Cookie**:这些 Cookie 对于网站正常运行是必需的,不能在我们的系统中关闭。
- **性能 Cookie**:这些 Cookie 允许我们计算访问量和流量来源,以便我们可以测量和改进网站的性能。
- **功能性 Cookie**:这些 Cookie 使网站能够提供增强的功能和个性化。
- **定向 Cookie**:这些 Cookie 可能通过我们的网站由我们的广告合作伙伴设置,以建立您兴趣的个人资料。
## 我们使用的 Cookie 类型
### 必要 Cookie
- 用于维护用户会话的会话 Cookie
- 用于防止欺诈和滥用的安全 Cookie
### 性能 Cookie
- 用于了解访问者如何与我们的网站互动的分析 Cookie
- 用于将流量分配到服务器的负载平衡 Cookie
### 功能性 Cookie
- 用于记住您的设置和选择的偏好 Cookie
- 用于记住您的语言偏好的语言 Cookie
### 定向 Cookie
- 用于提供相关广告的广告 Cookie
- 用于在社交平台上启用内容共享的社交媒体 Cookie
## 管理 Cookie
大多数网络浏览器允许您通过其设置控制 Cookie。您可以
- 从您的设备删除 Cookie
- 通过激活浏览器中允许您拒绝所有或某些 Cookie 的设置来阻止 Cookie
- 设置您的浏览器在收到 Cookie 时通知您
请注意,如果您选择阻止或删除 Cookie您可能无法访问我们网站的某些区域或功能。
## 本 Cookie 政策的变更
我们可能会不时更新我们的 Cookie 政策。我们将通过在此页面上发布新的 Cookie 政策来通知您任何变更。
## 联系我们
如果您对本 Cookie 政策有任何疑问,请联系我们:
- 通过电子邮件support@example.com
- 通过访问我们的网站:[联系页面](https://example.com/contact)

View File

@ -0,0 +1,49 @@
---
title: 隐私政策
description: 我们致力于保护您的隐私和个人数据
date: 2025-03-10T00:00:00.000Z
published: true
locale: zh
---
## 引言
欢迎阅读我们的隐私政策。本文档说明了我们在您使用我们的服务时如何收集、使用和保护您的个人信息。
## 我们收集的信息
我们可能会收集以下类型的信息:
- **个人信息**:您在注册或联系我们时提供的姓名、电子邮件地址和联系方式。
- **使用数据**:有关您如何与我们的网站互动的信息,包括访问的页面和花费的时间。
- **设备信息**有关您用于访问我们服务的设备的详细信息如IP地址、浏览器类型和操作系统。
## 我们如何使用您的信息
我们将您的信息用于以下目的:
- 提供和维护我们的服务
- 通知您有关我们服务的变更
- 提供客户支持
- 收集分析或有价值的信息以改进我们的服务
- 监控我们服务的使用情况
- 检测、预防和解决技术问题
## 数据安全
我们实施适当的安全措施,以保护您的个人信息免受未经授权的访问、更改、披露或销毁。
## 第三方服务
我们可能会雇用第三方公司和个人来促进我们的服务,代表我们提供服务,或协助我们分析我们的服务是如何被使用的。
## 本隐私政策的变更
我们可能会不时更新我们的隐私政策。我们将通过在此页面上发布新的隐私政策来通知您任何变更。
## 联系我们
如果您对本隐私政策有任何疑问,请联系我们:
- 通过电子邮件support@example.com
- 通过访问我们的网站:[联系页面](https://example.com/contact)

View File

@ -0,0 +1,55 @@
---
title: 服务条款
description: 管理我们服务使用的条款和条件
date: 2025-03-10T00:00:00.000Z
published: true
locale: zh
---
## 引言
这些服务条款("条款")规范您对我们网站和服务的使用。通过访问或使用我们的服务,您同意受这些条款的约束。
## 服务使用
我们的服务按"原样"和"可用性"提供,不提供任何形式的明示或暗示保证。我们不保证我们的服务不会中断、安全或无错误。
## 用户账户
当您在我们这里创建账户时,您必须提供准确和完整的信息。您负责保护您的账户安全,并对您账户下发生的所有活动负责。
## 知识产权
我们的网站及其原创内容、功能和功能性归我们所有,并受国际版权、商标和其他知识产权法律的保护。
## 用户内容
您保留您提交、发布或通过我们的服务展示的任何内容的所有权利。通过提交内容,您授予我们全球范围内的非独占、免版税的许可,以使用、复制、修改和分发您的内容。
## 禁止活动
您同意不会:
- 以违反任何适用法律或法规的方式使用我们的服务
- 从事任何限制或阻碍任何人使用或享受我们服务的行为
- 尝试未经授权访问我们的服务器或网络
- 使用我们的服务分发恶意软件或其他有害代码
## 终止
我们可以立即终止或暂停您的账户和对我们服务的访问,无需事先通知或承担责任,无论出于何种原因。
## 责任限制
在任何情况下,我们均不对因您使用或无法使用我们的服务而导致的任何间接、偶然、特殊、后果性或惩罚性损害承担责任。
## 条款变更
我们保留随时修改这些条款的权利。如果我们做出更改,我们将通过在此页面上发布更新的条款提供通知。
## 联系我们
如果您对这些条款有任何疑问,请联系我们:
- 通过电子邮件support@example.com
- 通过访问我们的网站:[联系页面](https://example.com/contact)

View File

@ -0,0 +1,58 @@
import { CustomPage } from '@/components/page/custom-page';
import { getCustomPage } from '@/lib/page/get-custom-page';
import { getBaseUrl } from '@/lib/urls/get-base-url';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import '@/styles/mdx.css';
export async function generateMetadata(
props: NextPageProps
): Promise<Metadata> {
const params = await props.params;
if (!params) {
return {};
}
const locale = params.locale as string;
const page = await getCustomPage('cookie-policy', locale);
if (!page) {
return {};
}
return {
title: page.title,
description: page.description,
openGraph: {
title: page.title,
description: page.description,
type: 'article',
url: `${getBaseUrl()}/cookie-policy`
}
};
}
export default async function CookiePolicyPage(props: NextPageProps) {
const params = await props.params;
if (!params) {
notFound();
}
const locale = params.locale as string;
const page = await getCustomPage('cookie-policy', locale);
if (!page) {
notFound();
}
return (
<CustomPage
title={page.title}
description={page.description}
date={page.date}
content={page.body.code}
/>
);
}

View File

@ -0,0 +1,12 @@
import Container from '@/components/container';
import { PropsWithChildren } from 'react';
export default function LegalLayout({ children }: PropsWithChildren) {
return (
<Container className="py-8 px-4">
<div className="mx-auto">
{children}
</div>
</Container>
);
}

View File

@ -0,0 +1,58 @@
import { CustomPage } from '@/components/page/custom-page';
import { getCustomPage } from '@/lib/page/get-custom-page';
import { getBaseUrl } from '@/lib/urls/get-base-url';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import '@/styles/mdx.css';
export async function generateMetadata(
props: NextPageProps
): Promise<Metadata> {
const params = await props.params;
if (!params) {
return {};
}
const locale = params.locale as string;
const page = await getCustomPage('privacy-policy', locale);
if (!page) {
return {};
}
return {
title: page.title,
description: page.description,
openGraph: {
title: page.title,
description: page.description,
type: 'article',
url: `${getBaseUrl()}/privacy-policy`
}
};
}
export default async function PrivacyPolicyPage(props: NextPageProps) {
const params = await props.params;
if (!params) {
notFound();
}
const locale = params.locale as string;
const page = await getCustomPage('privacy-policy', locale);
if (!page) {
notFound();
}
return (
<CustomPage
title={page.title}
description={page.description}
date={page.date}
content={page.body.code}
/>
);
}

View File

@ -0,0 +1,58 @@
import { CustomPage } from '@/components/page/custom-page';
import { getCustomPage } from '@/lib/page/get-custom-page';
import { getBaseUrl } from '@/lib/urls/get-base-url';
import type { NextPageProps } from '@/types/next-page-props';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import '@/styles/mdx.css';
export async function generateMetadata(
props: NextPageProps
): Promise<Metadata> {
const params = await props.params;
if (!params) {
return {};
}
const locale = params.locale as string;
const page = await getCustomPage('terms-of-service', locale);
if (!page) {
return {};
}
return {
title: page.title,
description: page.description,
openGraph: {
title: page.title,
description: page.description,
type: 'article',
url: `${getBaseUrl()}/terms-of-service`
}
};
}
export default async function TermsOfServicePage(props: NextPageProps) {
const params = await props.params;
if (!params) {
notFound();
}
const locale = params.locale as string;
const page = await getCustomPage('terms-of-service', locale);
if (!page) {
notFound();
}
return (
<CustomPage
title={page.title}
description={page.description}
date={page.date}
content={page.body.code}
/>
);
}

View File

@ -136,7 +136,7 @@ export default async function BlogPostPage(props: NextPageProps) {
</div>
{/* blog post content */}
<div className="mt-4">
<div className="prose prose-gray dark:prose-invert max-w-none">
<Mdx code={post.body.code} />
</div>

View File

@ -1,7 +1,7 @@
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { LocaleLink } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
import Link from "next/link";
/**
* Note that `app/[locale]/[...rest]/page.tsx`
@ -24,7 +24,7 @@ export default function NotFound() {
</p>
<Button asChild size="lg" variant="default">
<Link href="/">{t('backToHome')}</Link>
<LocaleLink href="/">{t('backToHome')}</LocaleLink>
</Button>
</div>
);

View File

@ -10,7 +10,7 @@ export default function AllPostsButton() {
return (
<Button
size="lg"
variant="outline"
variant="default"
className="inline-flex items-center gap-2 group"
asChild
>

View File

@ -0,0 +1,35 @@
import { Mdx } from '@/components/shared/mdx-component';
import { getLocaleDate } from '@/lib/utils';
import { CalendarIcon } from 'lucide-react';
interface CustomPageProps {
title: string;
description: string;
date: string;
content: any; // MDX content
}
export function CustomPage({ title, description, date, content }: CustomPageProps) {
const formattedDate = getLocaleDate(date);
return (
<div className="py-8">
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
<p className="text-lg text-muted-foreground">{description}</p>
<div className="flex items-center gap-2">
<CalendarIcon className="size-4 text-muted-foreground" />
<p className="text-sm text-muted-foreground">{formattedDate}</p>
</div>
</div>
{/* Content */}
<div className="prose prose-gray dark:prose-invert max-w-none">
<Mdx code={content} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
import { allPages } from 'content-collections';
/**
* Gets a custom page from the content collection
* @param type The type of custom page to get (e.g., 'privacy-policy', 'terms-of-service')
* @param locale The locale to get the page for
* @returns The custom page or undefined if not found
*/
export async function getCustomPage(type: string, locale: string) {
// Find page with matching slug and locale
const page = allPages.find(
(page) =>
page.slugAsParams === `${type}` &&
page.locale === locale
);
if (!page) {
// If no page found with the current locale, try to find one with any locale
const defaultPage = allPages.find(
(page) => page.slugAsParams === `${type}`
);
return defaultPage;
}
return page;
}