🏕️
47일차
Part 15. Next.js로 블로그 만들기
Ch 1. Sanity로 Blog 모델 만들기
Ch 1. Sanity로 Blog 모델 만들기
https://slides.com/woongjae/nextjs2021
Content is Data
Sanity.io is the unified content platform that powers better digital experiences
🔗 Sanity Project 만들고 Deploy 하기
$# nextjs2021 (상위 폴더)
$ npm install -g @sanity/cli
$ sanity login
$# my-blog-contents (하위 폴더)
$ sanity init
$ sanity start # = $ npm start # local run
$ sanity deploy # sanity run
🔗 Schema 만들기 (1) - author
// ./schemas/author.js
export default {
name: "author",
title: "Author",
type: "document",
fields: [
{
name: "name",
title: "Name",
type: "string",
validation: (Rule) => Rule.required(),
},
{ name: "role", title: "Role", type: "string" },
{
name: "image",
title: "Image",
type: "image",
options: {
hotspot: true,
},
validation: (Rule) => Rule.required(),
},
],
preview: {
select: {
title: "name",
media: "image",
},
},
};
🔗 Schema 만들기 (2) - post
// ./shemas/post.js
export default {
name: "post",
title: "Post",
type: "document",
fields: [
{
name: "title",
title: "Title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "title",
maxLength: 96,
},
validation: (Rule) => Rule.required(),
},
{
name: "subtitle",
title: "Sub Title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "author",
title: "Author",
type: "reference",
to: { type: "author" },
validation: (Rule) => Rule.required(),
},
{
name: "content",
title: "Content",
type: "blockContent",
validation: (Rule) => Rule.required(),
},
{
name: "createdAt",
title: "Created at",
type: "datetime",
validation: (Rule) => Rule.required(),
},
{
name: "thumbnail",
title: "Thumbnail",
type: "image",
options: {
hotspot: true,
},
fields: [
{
name: "alt",
type: "string",
title: "alt",
options: { isHighlighted: true },
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required(),
},
{
name: "tag",
title: "Tag",
type: "reference",
to: { type: "tag" },
validation: (Rule) => Rule.required(),
},
],
preview: {
select: {
title: "title",
author: "author.name",
media: "thumbnail",
},
prepare(selection) {
const { author } = selection;
return Object.assign({}, selection, {
subtitle: author && `by ${author}`,
});
},
},
};
- tag
// ./schemas/tag.js
export default {
name: "tag",
title: "Tag",
type: "document",
fields: [
{
name: "title",
title: "Title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "title",
maxLength: 96,
},
validation: (Rule) => Rule.required(),
},
],
preview: {
select: {
title: "title",
subtitle: "slug.current",
},
},
};
// ./shemas/schemas.js
...
// Then we give our schema to the builder and provide the result to Sanity
export default createSchema({
// We name our schema
name: "default",
// Then proceed to concatenate our document type
// to the ones provided by any plugins that are installed
types: schemaTypes.concat([
// The following are document types which will appear
// in the studio.
post,
author,
tag, // ⭐
// When added to this list, object types can be used as
// { type: 'typename' } in other document schemas
blockContent,
]),
});
🔗 Schema 만들기 (3) - home
// ./schemas/home.js
export default {
name: "home",
title: "Home",
type: "document",
fields: [
{
name: "title",
title: "Title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "mainPost",
title: "Main Post",
type: "reference",
to: { type: "post" },
validation: (Rule) => Rule.required(),
},
],
preview: {
select: {
title: "title",
subtitle: "mainPost.title",
},
},
};
// ./schemas/shemas.js
...
// Then we give our schema to the builder and provide the result to Sanity
export default createSchema({
// We name our schema
name: "default",
// Then proceed to concatenate our document type
// to the ones provided by any plugins that are installed
types: schemaTypes.concat([
// The following are document types which will appear
// in the studio.
post,
author,
tag,
home, // ⭐
// When added to this list, object types can be used as
// { type: 'typename' } in other document schemas
blockContent,
]),
});
🔗 Schema 만들기 (4) - blockContent
https://www.sanity.io/plugins/sanity-plugin-url-metadata-input
$ sanity install url-metadata-input
// ./schemas/blockContent.js
...
export default {
title: "Block Content",
name: "blockContent",
type: "array",
of: [
{
...
styles: [
{ title: "Normal", value: "normal" },
{ title: "H1", value: "h1" },
{ title: "H2", value: "h2" },
{ title: "H3", value: "h3" },
{ title: "H4", value: "h4" },
{ title: "H5", value: "h5" },
{ title: "Quote", value: "blockquote" },
],
lists: [
{ title: "Bullet", value: "bullet" },
{ title: "Numbered", value: "number" },
],
// Marks let you mark up inline text in the block editor.
marks: {
// Decorators usually describe a single property – e.g. a typographic
// preference or highlighting by editors.
decorators: [
{ title: "Strong", value: "strong" },
{ title: "Emphasis", value: "em" },
],
// Annotations can be any object structure – e.g. a link or a footnote.
annotations: [
{
title: "URL",
name: "link",
type: "object",
fields: [
{
title: "URL",
name: "href",
type: "url",
},
],
},
],
},
},
// You can add additional types here. Note that you can't use
// primitive types such as 'string' and 'number' in the same array
// as a block type.
{
type: "image",
options: { hotspot: true },
fields: [
{
name: "caption",
title: "Caption",
type: "string",
options: { isHighlighted: true },
},
{
name: "alt",
title: "alt",
type: "string",
options: { isHighlighted: true },
validation: (Rule) => Rule.required(),
},
],
},
{
type: "video",
},
{
type: "code",
},
{ type: "link" },
{ type: "imageGallery" },
],
};
// ./schemas/video.js
export default {
name: "video",
title: "Video",
type: "object",
fields: [
{ name: "caption", title: "Caption", type: "string" },
{ name: "metadata", title: "MetaData", type: "urlWithMetadata" },
],
preview: {
select: {
title: "caption",
subtitle: "metadata.url",
},
},
};
// ./schemas/code.js
export default {
name: "code",
title: "Code",
type: "object",
fields: [
{
name: "title",
title: "Title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "language",
title: "Language",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "code",
title: "Code",
type: "string",
validation: (Rule) => Rule.required(),
},
],
};
// ./schemas/link.js
export default {
name: "link",
title: "Link",
type: "object",
fields: [{ name: "metadata", title: "Metadata", type: "urlWithMetadata" }],
preview: {
select: {
title: "metadata.openGraph.title",
subtitle: "metadata.openGraph.url",
},
},
};
// ./schemas/imageGallery.js
export default {
name: "imageGallery",
title: "Image Gallery",
type: "object",
fields: [
{
name: "caption",
title: "Caption",
type: "string",
validation: (Rule) => Rule.required(),
},
{
name: "images",
title: "Images",
type: "array",
options: { layout: "gird" },
of: [
{
name: "image",
title: "Image",
type: "image",
hotspot: true,
fields: [
{
name: "alt",
title: "alt",
type: "string",
options: { isHighlighted: true },
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required().max(4),
},
],
};
// ./schemas/schema.js
...
// Then we give our schema to the builder and provide the result to Sanity
export default createSchema({
...
types: schemaTypes.concat([
// The following are document types which will appear
// in the studio.
post,
author,
tag,
home,
// When added to this list, object types can be used as
// { type: 'typename' } in other document schemas
blockContent,
video,
code,
link,
imageGallery,
]),
});
🔗 Stdio 의 input 창 커스터마이징
https://www.sanity.io/docs/custom-input-widgets
$ npm i yarn -g
$ yarn add react-ace
// ./components/CodeInput.jsx
import React, { useCallback } from "react";
import { FormField } from "@sanity/base/components";
import AceEditor from "react-ace";
import PatchEvent, { set, unset } from "@sanity/form-builder/PatchEvent";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/mode-javascript";
const CodeInput = React.forwardRef((props, ref) => {
const {
type, // Schema information
value, // Current field value
markers, // Markers including validation rules
presence, // Presence information for collaborative avatars
compareValue, // Value to check for "edited" functionality
onChange,
} = props;
const codeChange = useCallback(
(code) => {
onChange(PatchEvent.from(code ? set(code) : unset()));
},
[onChange]
);
return (
<FormField
description={type.description} // Creates description from schema
title={type.title} // Creates label from schema title
__unstable_markers={markers} // Handles all markers including validation
__unstable_presence={presence} // Handles presence avatars
compareValue={compareValue} // Handles "edited" status
>
<AceEditor
mode="javascript"
name="ace-editor-code"
width="100%"
theme="github"
style={{
boxShadow: "0 0 0 1px #cad1dc",
lineHeight: 1.6,
}}
value={value}
tabSize={2}
setOptions={{ useWorker: false }}
ref={ref}
onChange={codeChange}
/>
</FormField>
);
});
export default CodeInput;
// ./schemas/code.js
import CodeInput from "../components/CodeInput";
export default {
name: "code",
title: "Code",
type: "object",
fields: [
...
{
name: "code",
title: "Code",
type: "string",
validation: (Rule) => Rule.required(),
inputComponent: CodeInput, // ⭐
},
],
};
🔗 query 사용하기 ⭐
*[_type == 'home'][0] {
'mainPostUrl': mainPost -> slug.current
}
*[_type=='post'] {
title,
subtitle,
createdAt,
'content': content[] {
...,
...select(_type == 'imageGallery' => {'images': images[]{..., 'url': asset -> url}})
},
'slug': slug.current,
'thumbnail': {
'alt': thumbnail.alt,
'imageUrl': thumbnail.asset -> url
},
'author': author -> {
name,
role,
'image': image.asset -> url
},
'tag': tag -> {
title,
'slug': slug.current
}
}