prmbr-image-mksaas/content-collections.ts
javayhu 0929d93342 chore: enhance MDX component fix copy button and localization updates
- Add __rawString__ property to MDX component for improved code handling.
- Refactor pre element to extract code content dynamically and support copy functionality.
- Update login translation in English to "Log in" for consistency.
2025-03-15 11:27:07 +08:00

342 lines
9.8 KiB
TypeScript

import { defineCollection, defineConfig } from "@content-collections/core";
import { compileMDX } from "@content-collections/mdx";
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode, { Options } from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import { codeImport } from 'remark-code-import';
import remarkGfm from 'remark-gfm';
import { createHighlighter } from 'shiki';
import path from "path";
import { LOCALES, DEFAULT_LOCALE } from "@/i18n/routing";
import { visit } from 'unist-util-visit';
/**
* Content Collections documentation
* 1. https://www.content-collections.dev/docs/quickstart/next
* 2. https://www.content-collections.dev/docs/configuration
* 3. https://www.content-collections.dev/docs/transform#join-collections
*/
/**
* Blog Author collection
*
* Authors are identified by their slug across all languages
*/
export const authors = defineCollection({
name: 'author',
directory: 'content',
include: '**/author/*.mdx',
schema: (z) => ({
slug: z.string(),
name: z.string(),
avatar: z.string(),
locale: z.enum(LOCALES as [string, ...string[]]).optional()
}),
transform: async (data, context) => {
// 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;
return {
...data,
locale,
};
}
});
/**
* Blog Category collection
*
* Categories are identified by their slug across all languages
*/
export const categories = defineCollection({
name: 'category',
directory: 'content',
include: '**/category/*.mdx',
schema: (z) => ({
slug: z.string(),
name: z.string(),
description: z.string(),
locale: z.enum(LOCALES as [string, ...string[]]).optional()
}),
transform: async (data, context) => {
// 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;
return {
...data,
locale
};
}
});
/**
* Blog Post collection
*
* 1. For a blog post at content/en/blog/first-post.mdx:
* locale: en
* slug: /blog/first-post
* slugAsParams: first-post
*
* 2. For a blog post at content/zh/blog/first-post.mdx:
* locale: zh
* slug: /blog/first-post
* slugAsParams: first-post
*/
export const posts = defineCollection({
name: 'post',
directory: 'content',
include: '**/blog/**/*.mdx',
schema: (z) => ({
title: z.string(),
description: z.string(),
image: z.string(),
date: z.string().datetime(),
published: z.boolean().default(true),
categories: z.array(z.string()),
author: z.string(),
locale: z.enum(LOCALES as [string, ...string[]]).optional()
}),
transform: async (data, context) => {
const body = await compileWithCodeCopy(context, data);
// 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;
// Find the author by matching slug
const blogAuthor = context
.documents(authors)
.find((a) => a.slug === data.author && a.locale === locale) ||
context
.documents(authors)
.find((a) => a.slug === data.author);
// Find categories by matching slug
const blogCategories = data.categories.map(categorySlug => {
// Try to find a category with matching slug and locale
const category = context
.documents(categories)
.find(c => c.slug === categorySlug && c.locale === locale) ||
context
.documents(categories)
.find(c => c.slug === categorySlug);
return category;
}).filter(Boolean); // Remove null values
// 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,
author: blogAuthor,
categories: blogCategories,
slug: `/${slugPath}`,
slugAsParams,
body: {
raw: data.content,
code: body
}
};
}
});
/**
* 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 compileWithCodeCopy(context, data);
// 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
}
};
}
});
/**
* Releases collection for changelog
*
* 1. For a release at content/en/release/v1-0-0.md:
* locale: en
* slug: /release/v1-0-0
* slugAsParams: v1-0-0
*
* 2. For a release at content/zh/release/v1-0-0.md:
* locale: zh
* slug: /release/v1-0-0
* slugAsParams: v1-0-0
*/
export const releases = defineCollection({
name: 'release',
directory: 'content',
include: '**/release/**/*.{md,mdx}',
schema: (z) => ({
title: z.string(),
description: z.string(),
date: z.string().datetime(),
version: z.string(),
published: z.boolean().default(true),
locale: z.enum(LOCALES as [string, ...string[]]).optional()
}),
transform: async (data, context) => {
const body = await compileWithCodeCopy(context, data);
// 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) =>
createHighlighter({
...options
}),
onVisitLine(node) {
// Prevent lines from collapsing in `display: grid` mode, and allow empty
// lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }];
}
},
onVisitHighlightedLine(node) {
if (!node.properties.className) {
node.properties.className = [];
}
node.properties.className.push('line--highlighted');
},
onVisitHighlightedChars(node) {
if (!node.properties.className) {
node.properties.className = [];
}
node.properties.className = ['word--highlighted'];
}
};
const compileWithCodeCopy = async (
context: any,
data: any,
options: {
remarkPlugins?: any[];
rehypePlugins?: any[];
} = {}
) => {
return await compileMDX(context, data, {
...options,
remarkPlugins: [
remarkGfm,
codeImport,
...(options.remarkPlugins || [])
],
rehypePlugins: [
rehypeSlug,
[rehypePrettyCode, prettyCodeOptions],
// add __rawString__ to pre element
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
const [codeEl] = node.children;
if (codeEl.tagName !== "code") return;
// add __rawString__ as a property that will be passed to the React component
if (!node.properties) {
node.properties = {};
}
node.properties.__rawString__ = codeEl.children?.[0]?.value;
}
});
},
rehypeAutolinkHeadings,
...(options.rehypePlugins || [])
]
});
};
export default defineConfig({
collections: [authors, categories, posts, pages, releases]
});