Sanity CMS 是一个无头CMS,它是一个与表现层或客户端脱钩的内容创作后端系统。它通过API将存储的内容作为数据公开,以便在网站、移动应用和物联网等不同平台上消费,它与WordPress等传统CMS截然不同。
Sanity CMS采取了结构化的内容创作方法,提供了简洁的功能,通过其图像管道管理图像,通过管理文字,以及设计。它还提供了Sanity Studio ,一个用React构建的功能齐全、可定制、可扩展的编辑器。
说人话就是你不要编写后端代码,也不需要写管理后台,CMS,只需要专注于用户端的编写,后端的数据只需要使用sanity即可
1npx create-next-app@latest
2
3// 安装时,会看到以下提示
4What is your project named? my-app
5Would you like to use TypeScript? No / Yes
6Would you like to use ESLint? No / Yes
7Would you like to use Tailwind CSS? No / Yes
8Would you like to use `src/` directory? No / Yes
9Would you like to use App Router? (recommended) No / Yes
10Would you like to customize the default import alias (@/*)? No / Yes
11What import alias would you like configured? @/*
需要在 https://www.sanity.io/ 注册自己的账号,再回到项目中执行以下命令:
1npm create sanity@latest -- --template clean --create-project "project-name" --dataset database-name
在新版的sanity网站中注册时就会让你创建studio,而上面的命令会帮你创建一个新的studio和新的数据库,过程中会提示让你登录。
执行完上面的方法就会看到sanity自动帮你在项目中生成了一个文件夹,这时输入项目路径 /studio
就能看到sanity的工作台,也就是CMS
注意,这里首次进入studio会提示登录,需要登录刚才命令登录的同一个账号
上图是已经有了数据也就是blog
和tag type,
而首次打开是没有的,那么该怎么操作呢?就需要进入刚刚自动生成的sanity文件夹中 sanity/schema.ts
1import { type SchemaTypeDefinition } from 'sanity'
2import { blogType } from './schemas/blogType'
3import { tagType } from './schemas/tagType'
4
5export const schema: { types: SchemaTypeDefinition[] } = {
6 types: [blogType,tagType],
7}
1import { defineType, defineField, defineArrayMember } from "sanity";
2
3export const blogType = defineType({
4 name: "blog",
5 type: "document",
6 title: "Blog",
7 fields: [
8 defineField({
9 name: "title",
10 type: "string",
11 title: "Blog Title",
12 validation: (Rule) => Rule.required().max(50).error("最多不超过50个字"),
13 }),
14
15 defineField({
16 name: "slug",
17 type: "slug",
18 title: "Slug",
19 options: {
20 source: "title",
21 },
22 validation: (Rule) => Rule.required(),
23 }),
24 defineField({
25 name: "publishDate",
26 type: "datetime",
27 title: "Blog Publish Date",
28 initialValue: () => new Date().toISOString(),
29 }),
30 defineField({
31 name: "description",
32 type: "text",
33 title: "Blog Description",
34 validation: (Rule) => Rule.max(200).error("最多不超过200个字").required(),
35 }),
36 defineField({
37 name: "content",
38 type: "array",
39 title: "Blog Content",
40 of: [
41 { type: "block" },
42 {
43 type: "image",
44 fields: [{ name: "alt", type: "text", title: "Alt" }],
45 },
46 {
47 type: 'code',
48 options:{
49 language: 'tsx',
50 }
51 }
52 ],
53 validation: (Rule) => Rule.required(),
54 }),
55 defineField({
56 name: "tags",
57 type: "array",
58 title: "Tags",
59 of: [
60 {
61 type: "reference",
62 to: [{ type: "tagType" }],
63 },
64 ],
65 }),
66 ],
67});
68
1import { defineType } from "sanity";
2
3export const tagType = defineType({
4 name: "tagType",
5 type: "document",
6 title: "Tag Type",
7 fields: [
8 {
9 name: "name",
10 type: "string",
11 title: "Tag Name",
12 },
13 {
14 name: "slug",
15 type: "slug",
16 title: "Slug",
17 options: {
18 source: "name",
19 maxLength: 50,
20 },
21 },
22 ],
23});
24
需要自己创建对应的schemas type
并且导入到schema.ts
文件中即可
其中有一点需要注意:在blogType中如下代码如果需要使用插入代码块的功能和高亮文字/文字颜色设置,则需要配置type:code,
而sanity本身还不支持直接添加type为code的类型,所以需要下载插件:
npm i @sanity/code-input,
npm i sanity-plugin-simpler-color-input
并且在sanity.config.ts
中配置下载的相关插件
1defineField({
2 name: "content",
3 type: "array",
4 title: "Blog Content",
5 of: [
6 {
7 type: "block",
8 marks: {
9 annotations: [
10 {
11 type: "textColor",
12 },
13 {
14 type: "highlightColor",
15 },
16 ],
17 },
18 },
19 {
20 type: "image",
21 fields: [{ name: "alt", type: "text", title: "Alt" }],
22 },
23 {
24 type: "code",
25 options: {
26 language: "tsx",
27 },
28 },
29 ],
30 validation: (Rule) => Rule.required(),
31 }),
1const PortableComponents = {
2 types: {
3 image: ({ value }: any) => {
4 return (
5 <Image src={urlForImage(value)} alt="Blog" width={700} height={700} />
6 );
7 },
8 // for support insert code block
9 code: ({ value }: any) => {
10 return <CodeBlock value={value} />;
11 },
12 },
13 // for support text color and highlight
14 marks: {
15 textColor: ({ children, value }: any) => (
16 <span style={{ color: value.value }}>{children}</span>
17 ),
18 highlightColor: ({ children, value }: any) => (
19 <span style={{ background: value.value }}>{children}</span>
20 ),
21 },
22};
使文字能够支持添加超链接以及内部链接功能
1defineField({
2 name: "content",
3 type: "array",
4 title: "Blog Content",
5 of: [
6 {
7 type: "block",
8 marks: {
9 annotations: [
10 {
11 name: "link", //外部链接
12 type: "object",
13 title: "External link",
14 fields: [
15 {
16 name: "href",
17 type: "url",
18 title: "URL",
19 },
20 {
21 title: "Open in new tab",
22 name: "blank",
23 description: "是否在新窗口打开?",
24 type: "boolean",
25 },
26 ],
27 },
28 {
29 name: "internalLink", //内部链接
30 type: "object",
31 title: "Internal link",
32 fields: [
33 {
34 name: "reference",
35 type: "reference",
36 title: "Reference",
37 to: [
38 { type: "blog" },
39 // other types you may want to link to
40 ],
41 },
42 ],
43 },
44 ],
45 },
46 },
47
48 ],
49 validation: (Rule) => Rule.required(),
50 }),
.env
文件中配置1NEXT_PUBLIC_SANITY_PROJECT_ID=""
2NEXT_PUBLIC_SANITY_DATASET=""
npm i @portabletext/react,
portabletext可以支持sanity返回的type为block的内容1import {PortableText} from '@portabletext/react'
2
3<PortableText
4 value={blog.content}
5 components={PortableComponents}
6/>Ï
1import SyntaxHighlighter from "react-syntax-highlighter";
2import { tomorrowNightBright } from "react-syntax-highlighter/dist/esm/styles/hljs";
3import { urlForImage } from "../../sanity/lib/image";
4import Image from "next/image";
5
6interface CodeBlockProps {
7 value: {
8 code: string;
9 language: string;
10 };
11}
12
13// for support code block
14const CodeBlock = ({ value }: CodeBlockProps) => {
15 const { code, language } = value;
16 return (
17 <SyntaxHighlighter
18 showLineNumbers={true}
19 showInlineLineNumbers={true}
20 language={language}
21 style={tomorrowNightBright}
22 customStyle={{
23 padding: "1em",
24 marginBottom: "2em",
25 }}
26 >
27 {code}
28 </SyntaxHighlighter>
29 );
30};
31
32const PortableComponents = {
33 types: {
34 // for support image
35 image: ({ value }: any) => {
36 return (
37 <Image src={urlForImage(value)} alt="Blog" width={700} height={700} />
38 );
39 },
40 // for support code block
41 code: ({ value }: any) => {
42 return <CodeBlock value={value} />;
43 },
44 },
45 // for support text color and highlight
46 marks: {
47 textColor: ({ children, value }: any) => (
48 <span style={{ color: value.value }}>{children}</span>
49 ),
50 highlightColor: ({ children, value }: any) => (
51 <span style={{ background: value.value }}>{children}</span>
52 ),
53 },
54};
55
56export default PortableComponents;
57
1export const getBlogDetailBySlug = async (slug: string): Promise<BlogFace> => {
2
3 try {
4 const sql = `*[_type == "blog" && slug.current == "${slug}"][0]{
5 title,
6 slug,
7 publishDate,
8 description,
9 content,
10 tags[]->{_id,slug,name}
11 }`;
12 const result = await client.fetch(sql);
13 return result;
14 } catch (error) {
15 console.error("Failed to fetch blog data:", error);
16 throw error;
17 }
18};
19