对于许多开发者来说,拥有一个属于自己的博客是分享知识、记录成长的最佳方式。比如说我在掘金上发的文章,刀乐都让掘金赚了,搭建一个博客有助于提升我们程序员的影响力。
今天,我们将介绍一个解决方案:使用 Sanity.io 这个 Headless CMS,结合前端框架 SolidStart,来构建一个属于你自己的博客。
先看看Demo
什么是 Sanity.io?
Sanity.io 是一个现代化的、可定制的 Headless 内容管理系统(CMS)。就是说它提供可视化管理端来管理我们数据库的东西。这意味着你可以使用任何你喜欢的前端技术(如 Next.js, React, Vue 等)来构建你的网站或应用,并通过 API 从 Sanity 获取内容。

Sanity 的主要优势包括:
- 高度灵活的内容建模:你可以通过简单的 JavaScript 对象来定义你想要的内容结构(Schema),完全掌控你的数据模型,如下我定义了一个作者表
js
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'author',
title: 'Author',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Name',
type: 'string',
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
maxLength: 96,
},
}),
defineField({
name: 'image',
title: 'Image',
type: 'image',
options: {
hotspot: true,
},
}),
defineField({
name: 'bio',
title: 'Bio',
type: 'array',
of: [
{
title: 'Block',
type: 'block',
styles: [{title: 'Normal', value: 'normal'}],
lists: [],
},
],
}),
],
preview: {
select: {
title: 'name',
media: 'image',
},
},
})
- 优秀的编辑体验:Sanity Studio 是一个基于 React 的可视化编辑器,你可以对其进行深度定制,以满足你的编辑需求。
- 强大的查询语言 (GROQ) :Sanity 弄了一种新的查询语言 GROQ,让你可以精确、高效地获取所需数据,如下是通过id查询一篇文章的信息
js
export async function getPostBySlug(slug: string): Promise<Post | null> {
const query = `*[_type == "post" && slug.current == $slug][0] {
_id,
title,
slug,
excerpt,
body,
publishedAt,
mainImage,
"categories": categories[]->{title},
"tags": tags[]->{title, color},
"author": author->{
name,
image,
bio
}
}`;
const params = { slug };
return await client.fetch(query, params);
}
- 慷慨的免费套餐:每月提供 100GB 的流量和 1M 的 API CDN 请求,对于个人博客和小型项目来说完全足够。
- 丰富的生态:sanity官方和社区提供了大量的编辑器插件,比如有markdown,流媒体上传,还有国际化插件。
我的博客实现
技术选型:
- 内容后端: Sanity.io (Headless CMS)
- 核心框架: SolidStart (服务端渲染)
- UI/样式: Tailwind CSS & daisyUI
项目亮点:
-
高性能架构: 采用 SolidStart 框架,充分利用其出色的服务端渲染(SSR)能力,为博客提供极致的加载速度和流畅的交互体验。
-
灵活的双模内容策略:
- Portable Text (
blockContent
): 它基于 JSON 的结构化特性,允许在文章中嵌入复杂的自定义组件,实现了内容与表现的彻底解耦。 - Markdown (
markdownContent
): 保留对传统 Markdown 的支持,专为技术文章设计。此举旨在简化向外部技术论坛(如掘金、GitHub)的同步发布流程,优化了我的工作流。
- Portable Text (
-
高效的前端实现:
- UI 层面,daisyUI 作为 Tailwind CSS 的组件库,在保证开发灵活性的同时,快速构建出风格统一且富有趣味性的界面。
- 前端使用
markdown-it
库高效解析 Markdown 文本,并结合 Tailwind CSS 进行精准的样式渲染。
js
import MarkdownIt from 'markdown-it';
import highlightjs from 'markdown-it-highlightjs';
const getLineHighlightClass = (line) => {
const keywordMap = {
'error': 'bg-error text-error-content',
'warning': 'bg-warning text-warning-content',
'success': 'bg-success text-success-content',
'installed': 'bg-success text-success-content',
'info': 'bg-info text-info-content',
'note': 'bg-info text-info-content',
};
const lowerLine = line.toLowerCase();
for (const keyword in keywordMap) {
if (lowerLine.includes(keyword)) {
return ` class="${keywordMap[keyword]}"`;
}
}
return '';
};
const customFenceRenderer = (md) => (tokens, idx) => {
const token = tokens[idx];
const code = token.content.trim();
const language = token.info || '';
const lines = code.split('\n');
const languageBadge = language ? `<div class="badge badge-sm badge-neutral absolute right-2 top-2">${language}</div>` : '';
const codeLines = lines.map((line, index) =>
`<pre data-prefix="${index + 1}"${getLineHighlightClass(line)}><code>${md.utils.escapeHtml(line)}</code></pre>`
).join('');
return `<div class="mockup-code w-full">${languageBadge}${codeLines}</div>`;
};
const enhanceRule = (md, ruleName, modifier) => {
const defaultRender = md.renderer.rules[ruleName] || ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules[ruleName] = (tokens, idx, options, env, self) => {
modifier(tokens[idx]);
return defaultRender(tokens, idx, options, env, self);
};
};
const md = new MarkdownIt({
html: true,
breaks: true,
linkify: true,
typographer: true,
}).use(highlightjs, { inline: true });
md.renderer.rules.fence = customFenceRenderer(md);
md.renderer.rules.blockquote_open = () => '<div class="alert my-4"><div><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg><span>';
md.renderer.rules.blockquote_close = () => '</span></div></div>';
md.renderer.rules.table_open = () => '<div class="overflow-x-auto my-4"><table class="table table-zebra w-full">';
md.renderer.rules.table_close = () => '</table></div>';
enhanceRule(md, 'link_open', (token) => {
token.attrSet('class', 'link link-primary');
if (token.attrGet('href')?.startsWith('http')) {
token.attrSet('target', '_blank');
token.attrSet('rel', 'noopener noreferrer');
}
});
enhanceRule(md, 'image', (token) => {
token.attrSet('class', 'rounded-lg shadow-lg max-w-full h-auto my-4');
});
export function renderMarkdown(content) {
return md.render(content || '');
}