📖
StrapiのRich text (Block)を呼び出す
目的
Strapiの詳細ページで記事のコンテンツを表示する際に詰まったので、共有できたらと思い記事を書きました!
ベースのサンプルなので、スタイリングなどはしていないです。
少しでも参考になったら嬉しいです🙌🏼
Strapiとは
- オープンソースのHeadless CMS
- カスタマイズ性が高いのが特徴
- 自社サーバーでセルフホスティングできれば無料
- SaaSのcloudサービスもある
Rich text (Block)とは
- Rich textは、見出しタグ、テキスト、リンク、画像、リストなど様々な要素が入ってくる
- Rich text (Markdown)もあるが、一般のCMS更新担当者はRich text(Block)の方が直感的かと思われる
- 🚨: 外部リンクの設定はできないので、フロント側で実装する必要がある
Strapiでデータを取得する
/utils/getStrapiURL.ts
export function getStrapiURL() {
return process.env.NEXT_PUBLIC_STRAPI_URL ?? "http://localhost:1337";
}
/data/baseUrl.ts
import { getStrapiURL } from "@/utils/getStrapiURL";
export const baseUrl: string = getStrapiURL();
/data/fetchData.ts
import { flattenAttributes } from "@/utils/flattenAttributes";
export const fetchData = async (url: string) => {
// MEMO: Tokenを利用したデータフェッチが必要なら、この関数内で実装する
try {
const response = await fetch(url);
const data = await response.json();
return flattenAttributes(data);
} catch (error) {
console.error("Error fetching data:", error);
return null;
}
};
/data/loaders.ts
interface Condition {
$containsi?: string;
}
interface FAQStringFilter {
title?: Condition;
body?: Condition;
}
interface Filters {
$or: FAQStringFilter[];
loginOnly?: { $ne: boolean };
}
export interface FAQ {
id: number;
title: string;
loginOnly: boolean;
faq_category: FAQCategory;
}
export interface FAQCategory {
label: string;
url: string;
faqs: {
data: FAQ[];
};
}
export interface FAQDetail extends FAQ {
body: BlocksContent;
}
export const getFaqById = async (id: string): Promise<FAQDetail | null> => {
const res = await fetchData(`${baseUrl}/api/faqs/${id}`);
if (!res) return null;
return res;
};
取得データサンプル
/app/faq/[id]/page.tsx
import { getFaqById } from "@/data/loaders";
import { type BlocksContent } from "@strapi/blocks-react-renderer";
import { Content } from "./_components/Content";
import Link from "next/link";
type Props = {
params: {
id: string;
};
};
export default async function Page({ params }: Props) {
const { id } = params;
const data = await getFaqById(id);
if (!data) return null;
const content: BlocksContent = data.body;
return (
<div>
<h2>{data.title}</h2>
<Content content={content} />
<Link href="/faq">一覧に戻る</Link>
</div>
);
}
データのサンプルは👇のようになります
const data = [
{ type: "heading", children: [{ type: "text", text: "Heading1" }], level: 1 },
{ type: "heading", children: [{ type: "text", text: "Heading2" }], level: 2 },
{ type: "heading", children: [{ type: "text", text: "Heading3" }], level: 3 },
{ type: "heading", children: [{ type: "text", text: "Heading4" }], level: 4 },
{ type: "heading", children: [{ type: "text", text: "Heading5" }], level: 5 },
{ type: "heading", children: [{ type: "text", text: "Heading6" }], level: 6 },
{ type: "paragraph", children: [{ type: "text", text: "text" }] },
{
type: "paragraph",
children: [{ type: "text", text: "text bold", bold: true }],
},
{
type: "paragraph",
children: [{ type: "text", text: "text underline", underline: true }],
},
{
type: "paragraph",
children: [
{ type: "text", text: "text strikethrough", strikethrough: true },
],
},
{
type: "paragraph",
children: [{ type: "text", text: "text italic", italic: true }],
},
{
type: "paragraph",
children: [{ type: "text", text: "text inline code", code: true }],
},
{
type: "paragraph",
children: [
{ text: "", type: "text" },
{
type: "link",
url: "https://www.google.com/",
children: [{ text: "text link", type: "text" }],
},
{ text: "", type: "text" },
],
},
{
type: "paragraph",
children: [
{ text: "", type: "text" },
{
type: "link",
url: "https://www.google.com/",
children: [{ text: "text link", type: "text" }],
},
{ text: "", type: "text" },
],
},
{ type: "quote", children: [{ type: "text", text: "this is Quote sample" }] },
{
type: "paragraph",
children: [{ type: "text", text: "external text link is enable setting?" }],
},
{
type: "list",
format: "unordered",
children: [
{ type: "list-item", children: [{ type: "text", text: "list1" }] },
{ type: "list-item", children: [{ type: "text", text: "list2" }] },
{ type: "list-item", children: [{ type: "text", text: "list3" }] },
],
},
{
type: "list",
format: "ordered",
children: [
{ type: "list-item", children: [{ type: "text", text: "order list1" }] },
{ type: "list-item", children: [{ type: "text", text: "order list2" }] },
{ type: "list-item", children: [{ type: "text", text: "order list3" }] },
],
},
{ type: "paragraph", children: [{ type: "text", text: "this is image" }] },
{
type: "image",
image: {
name: "ergonofis-tdnYk4qOGhc-unsplash.jpg",
alternativeText: "ergonofis-tdnYk4qOGhc-unsplash.jpg",
url: "http://localhost:1337/uploads/ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390.jpg",
caption: null,
width: 3000,
height: 4500,
formats: {
thumbnail: {
name: "thumbnail_ergonofis-tdnYk4qOGhc-unsplash.jpg",
hash: "thumbnail_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390",
ext: ".jpg",
mime: "image/jpeg",
path: null,
width: 104,
height: 156,
size: 5.19,
sizeInBytes: 5185,
url: "/uploads/thumbnail_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390.jpg",
},
large: {
name: "large_ergonofis-tdnYk4qOGhc-unsplash.jpg",
hash: "large_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390",
ext: ".jpg",
mime: "image/jpeg",
path: null,
width: 667,
height: 1000,
size: 109.49,
sizeInBytes: 109492,
url: "/uploads/large_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390.jpg",
},
small: {
name: "small_ergonofis-tdnYk4qOGhc-unsplash.jpg",
hash: "small_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390",
ext: ".jpg",
mime: "image/jpeg",
path: null,
width: 334,
height: 500,
size: 34.74,
sizeInBytes: 34736,
url: "/uploads/small_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390.jpg",
},
medium: {
name: "medium_ergonofis-tdnYk4qOGhc-unsplash.jpg",
hash: "medium_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390",
ext: ".jpg",
mime: "image/jpeg",
path: null,
width: 500,
height: 750,
size: 68.26,
sizeInBytes: 68261,
url: "/uploads/medium_ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390.jpg",
},
},
hash: "ergonofis_tdn_Yk4q_O_Ghc_unsplash_2ceeeb3390",
ext: ".jpg",
mime: "image/jpeg",
size: 1824.56,
previewUrl: null,
provider: "local",
provider_metadata: null,
createdAt: "2024-06-24T00:02:12.730Z",
updatedAt: "2024-06-24T00:02:12.730Z",
},
children: [{ type: "text", text: "" }],
},
];
データ型
BlocksContent
に格納されている
import { type BlocksContent } from "@strapi/blocks-react-renderer";
利用方法
blocks-react-renderer
から BlocksRenderer
をインポートしてカスタマイズする
カスタマイズのサンプル
ポイント
-
BlocksRenderer
はCSRなので 'use client'をつける必要がある -
<ul></ul>
や<ol></ol>
の取り回しに癖がある - linkについて、外部リンクかどうかはStrapi側に関心がない。つまり、外部リンクにするかどうかは実装側で決定する必要がある
- 外部リンクを実装するなら、自身のドメインと比較=>違う場合は
target="_blank"
を指定。という手法が取られているようです
/app/faq/[id]/_components/Content.tsx
"use client";
import {
type BlocksContent,
BlocksRenderer,
} from "@strapi/blocks-react-renderer";
import Image from "next/image";
import Link from "next/link";
export const Content = ({ content }: { content: BlocksContent }) => {
return (
<BlocksRenderer
content={content}
blocks={{
paragraph: ({ children }) => (
<p className="" style={{}}>
{children}
</p>
),
heading: ({ children, level }) => {
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
return <h1>{children}</h1>;
}
},
link: ({ children, url }) => <Link href={url}>{children}</Link>,
image: ({ image }) => {
return (
<Image
src={image.url}
width={image.width}
height={image.height}
alt={image.alternativeText || ""}
/>
);
},
list: (props) => {
if (props.format === "ordered") {
return <ol>{props.children}</ol>;
}
return <ul>{props.children}</ul>;
},
"list-item": (props) => <li>{props.children}</li>,
}}
/>
);
};
個人的感想
🙆♂️Good
-
@strapi/blocks-react-renderer
で型を共有してくれているのが助かる - 受け取ったデータをカスタマイズするコンポーネントが提供されているのがいい
- カスタマイズする
BlocksRenderer
コンポーネントはMUIライクにカスタマイズできるので、人によっては認知負荷が低い
🙅♂️Bad
- 外部リンクの指定がStrapi側から実装できないのが痛い。要件によってはCMSの選定から外れると思った
(自分が知らないだけで、外部リンクの設定ができる?)
以上となります!
まだStrapiの初心者ですので、ご意見やアドバイスなどあったらコメントにて教えていただけると
とても嬉しいです!❤️
Discussion