yufan.me/velite.config.ts

275 lines
8.7 KiB
TypeScript
Raw Normal View History

2024-06-14 02:13:47 +08:00
import { join } from 'node:path';
import rehypePrettyCode from 'rehype-pretty-code';
import { defineCollection, defineConfig, s } from 'velite';
import { nextImage } from '@/components/mdx/remark-plugins';
// Custom Field Types
const mdx = s.mdx({
gfm: true,
removeComments: true,
copyLinkedFiles: false,
remarkPlugins: [nextImage],
rehypePlugins: [
[
rehypePrettyCode,
{
theme: 'solarized-light',
},
],
],
});
const assertRoot = join(process.cwd(), '/public');
// Option Types
const options = defineCollection({
name: 'Options',
pattern: 'options/index.yml',
single: true,
schema: s.object({
title: s.string().max(99),
website: s.string().url(),
description: s.string().max(999),
keywords: s.array(s.string()),
author: s.object({ name: s.string(), email: s.string().email(), url: s.string().url() }),
navigation: s.array(s.object({ text: s.string(), link: s.string(), target: s.string().optional() })),
socials: s.array(
s.object({
name: s.string(),
icon: s.string(),
type: s.enum(['link', 'qrcode']),
title: s.string().optional(),
link: s.string().url(),
}),
),
settings: s.object({
initialYear: s.number().max(2024),
icpNo: s.string().optional(),
locale: s.string().optional().default('zh-CN'),
timeZone: s.string().optional().default('Asia/Shanghai'),
timeFormat: s.string().optional().default('yyyy-MM-dd HH:mm:ss'),
post: s.object({
sort: s.enum(['asc', 'desc']),
feature: s.array(s.string()).optional(),
category: s.array(s.string()).optional(),
}),
pagination: s.object({
posts: s.number().optional().default(5),
category: s.number().optional().default(7),
tags: s.number().optional().default(7),
search: s.number().optional().default(7),
}),
feed: s.object({
full: s.boolean().optional().default(true),
size: s.number().optional().default(20),
}),
sidebar: s.object({
search: s.boolean().default(false),
post: s.number().default(6),
comment: s.number().default(0),
tag: s.number().default(20),
}),
comments: s.object({
server: s.string().url().readonly(),
admins: s.array(s.number()).readonly(),
}),
}),
}),
});
// Category Types
const categories = defineCollection({
name: 'Category',
pattern: 'categories/*.yml',
schema: s
.object({
name: s.string().max(20),
slug: s.slug('category'),
cover: s.image({ absoluteRoot: assertRoot }),
description: s.string().max(999).optional(),
count: s.number().default(0),
})
.transform((data) => ({ ...data, permalink: `/cats/${data.slug}` })),
});
// Friend Types
const friends = defineCollection({
name: 'Friend',
pattern: 'options/friends.yml',
schema: s
.object({
website: s.string().max(40),
description: s.string().optional(),
homepage: s.string().url(),
poster: s.image({ absoluteRoot: assertRoot }),
favicon: s.string().optional(),
})
.transform((data: any) => {
if (data.favicon === undefined) {
data.favicon = `${data.homepage}/favicon.ico`;
}
return data;
}),
});
// Tags Types
const tags = defineCollection({
name: 'Tag',
pattern: 'tags/index.yml',
schema: s
.object({
name: s.string().max(20),
slug: s.slug('tag'),
description: s.string().max(999).optional(),
count: s.number().default(0),
})
.transform((data) => ({ ...data, permalink: `/tags/${data.slug}` })),
});
// Post Types
const posts = defineCollection({
name: 'Post',
pattern: 'posts/**/*.mdx',
schema: s
.object({
title: s.string().max(99),
slug: s.slug('posts', ['admin', 'login']),
date: s.isodate(),
updated: s.isodate().optional(),
comments: s.boolean().optional().default(true),
tags: s.array(s.string()).optional().default([]),
category: s.string(),
summary: s.string().optional().default(''),
cover: s.image({ absoluteRoot: assertRoot }).optional().default('/images/default-cover.jpg'),
published: s.boolean().optional().default(true),
excerpt: s.excerpt(),
meta: s.metadata(),
toc: s.toc(),
raw: s.raw(),
content: mdx,
})
.transform((data) => ({ ...data, permalink: `/posts/${data.slug}` })),
});
// Page Types
const pages = defineCollection({
name: 'Page',
pattern: 'pages/*.mdx',
schema: s
.object({
title: s.string().max(99),
slug: s.slug('global', ['admin', 'login', 'cats', 'page', 'tags']),
date: s.isodate(),
updated: s.isodate().optional(),
comments: s.boolean().optional().default(true),
cover: s.image({ absoluteRoot: assertRoot }).optional().default('/images/default-cover.jpg'),
published: s.boolean().optional().default(true),
friend: s.boolean().optional().default(false),
excerpt: s.excerpt(),
meta: s.metadata(),
toc: s.toc(),
raw: s.raw(),
content: mdx,
})
.transform((data) => ({ ...data, permalink: `/${data.slug}` })),
});
// Define all the available content formats.
export default defineConfig({
strict: true,
root: 'content',
output: {
data: '.velite',
clean: true,
assets: 'public',
},
collections: {
options: options,
categories: categories,
tags: tags,
posts: posts,
pages: pages,
friends: friends,
},
markdown: { rehypePlugins: [[rehypePrettyCode, { theme: 'solarized-light' }]] },
prepare: async (collections: any) => {
const { categories, tags, posts, options, pages } = collections;
// Some service provider, like vercel will set this environment automatically.
// But you need to pay more attention on the SaaS platforms that didn't implicit set this for you.
// For instance, Zeabur.
const publishedPosts: any[] = posts.filter((post: any) => process.env.NODE_ENV !== 'production' || post.published);
const publishedPages: any[] = pages.filter((page: any) => process.env.NODE_ENV !== 'production' || page.published);
// Find missing categories, tags from posts.
const missingCategories: string[] = publishedPosts
.map((post: any) => post.category)
.filter((c: any) => !categories.find((i: any) => i.name === c));
if (missingCategories.length > 0) {
console.error('The bellowing categories has not been configured:');
console.error(missingCategories);
}
const missingTags: string[] = publishedPosts
.flatMap((post: any) => post.tags)
.filter((tag) => !tags.find((t: any) => t.name === tag));
if (missingTags.length > 0) {
console.error('The bellowing tags has not been configured:');
console.error(missingTags);
}
// Find the missing covers for posts.
const missingCovers = publishedPosts
.filter((post: any) => post.cover.src === '/images/default-cover.jpg')
.map((post: any) => ({ title: post.title, slug: post.slug }));
if (process.env.NODE_ENV !== 'production' && missingCovers.length > 0) {
console.warn(`The following ${missingCovers.length} posts don't have a cover.`);
console.warn(missingCovers);
}
// Count tags and categories.
categories.forEach((i: any) => {
i.count = posts.filter((j: any) => j.category === i.name).length;
});
tags.forEach((i: any) => {
i.count = posts.filter((j: any) => j.tags.includes(i.name)).length;
});
// Validate feature posts option.
const featurePosts: string[] = options.settings.post.feature ?? [];
const slugs = publishedPosts.map((post: any) => post.slug as string);
const invalidFeaturePosts = featurePosts.filter((slug) => !slugs.includes(slug));
if (invalidFeaturePosts.length > 0) {
console.error('The bellowing feature posts are invalid');
console.error(invalidFeaturePosts);
}
// Validate pinned categories.
const pinnedCategories: string[] = options.settings.post.category ?? [];
const invalidPinnedCategories = pinnedCategories.filter((c) => categories.find((e: any) => e.name === c));
if (invalidPinnedCategories.length > 0) {
console.error('The bellowing pinned categories are invalid');
console.error(invalidPinnedCategories);
}
// Sort the posts.
const sortedPosts = publishedPosts.sort((left: any, right: any) => {
const a = new Date(left.date).getTime();
const b = new Date(right.date).getTime();
return options.settings.post.sort === 'asc' ? a - b : b - a;
});
// Update the result collections
Object.assign(collections, {
posts: sortedPosts,
pages: publishedPages,
options: options,
tags: tags,
categories: categories,
});
},
});