Open10

Nuxt3(SSG) + prismic + bunで無料ブログ作成(ミニマム実装編)

yunayuna

Nuxt3(SSG) + prismic + bunで無料ブログ作成の知識編はこちらにまとめています
https://zenn.dev/myuna/scraps/96c1b76e7f54f7

ここからは、新規でブログ構築するまでのメモです。

※現時点で、SSG(bun run generateで固めてデプロイした環境)でreview機能の動作がうまくいってません。
 うまく動いている方いらっしゃいましたら、「自分はいけてます」だけでもコメントいただけると嬉しいです。

yunayuna

ブログページを作成してみる

ここから、ブログのコンテンツを扱えるページを実装していきます。

手順

slice machine(ローカルGUI)上で
① Slice(ブログページを構成する部品の最小構成)の作成
② Page(ブログページの構成)の作成

prismic管理画面(GUI)で、
③ document(ブログの実際の記事・コンテンツデータ)の作成

を行い、
④ Nuxtのコンポーネント、ページを実装
⑤ SSGに固めてデプロイ

という流れです。

prismic利用の方針

Webサイトによっては、
ほとんどのサイト上のコンテンツをprismicで制作することもできそうですが、
今回はミニマムにprismicを使うという方針で、「ブログコンテンツ部分のみ」利用します。

理由は、CMS依存を小さくし、今後CMS基盤を変更するときの負荷を小さくするためです。

ざっくりした構成

ブログパーツは

・タイトル
・コンテンツ

の2つのみ。

コンテンツの中身は、画像やテキスト、様々な組み合わせでパーツを選択できるようにします。

yunayuna

Sliceの作成

ブログコンテンツに使いそうな、以下を作成。

Image(サイズで分けられるように、2バリエーション作成)
Hero(タイトル)
Text(各種装飾などができる、リッチテキスト)
Quote(引用に使う)

Image

Default variation

※フリーサイズで、アップ画像の全体を表示させる

※3000px × 1000px でサイズ指定する

Text

1フィールド、装飾は全部許可のシンプルな構成

Quote

Textと同じリッチテキストフィールド(h2,B,I装飾のみ許可)と、引用元用のテキストフィールド



yunayuna

Pageを作成

ブログの汎用ページ用に、blog_postというラベルで、1つだけPageを作成します。

Static Zoneには、必須のUIDの他に、Titleフィールド(リッチテキスト)を追加。
Slice Zoneには、作成した全てのSliceを登録しておき、必要に応じて部品として使えるようにしておく。

yunayuna

document (実データ)を作成

作成したsliceをPushで本番に反映させた後、
実データをセットしていきます。

タグ

テストタグ というタグを追加

Static Zoneのデータ

UID: first_sample
Title: テストタイトル

Slice Zoneのデータ

Imageと、Textの2つのSlice構成

yunayuna

Nuxtのページ作成

Sliceを作成したタイミングで、基本的なファイル群は、
自動的にNuxtのソースコードとして生成されます。

他のフレームワークを選択している場合、それぞれに応じたファイルが生成されているはずなので(未確認です)、
それぞれの環境に置き換えてみてください。

nuxt.config.tsの設定

modulesで、prismicモジュールの利用を宣言し、
prismicで、細かい設定をしてきます。

endpoint:prismicのリポジトリ名
preview:サンプルのデフォルト値のまま(今回未使用)
clientConfig.routes: ここに、prismicを使うパス(ブログポスト)を指定します。

nuxt.config.ts
export default defineNuxtConfig({
    modules: [
        "@nuxtjs/prismic",
    ],
    prismic: {
        endpoint: 'sampleproject',
        preview: '/api/preview',
        clientConfig: {
            routes: [
              //topics blog
              {
                type: "blog_post",
                path: "/topics/blog/:uid",
              },
            ],
          },
    },

ページを実装

Nuxtはディレクトリ構造がそのままURLのパスになります。
ブログ記事に指定したいパスに、ページファイルを作成します。
[uid].vueとすることで、uid部分を変数で取得できます。

具体的には、
http://localhost:3000/topics/blog/first_sample
とすると、
first_sampleという文字列を、ページ内で変数として取得できるということです。
今回は、この部分がprismicのdocumentで指定した、各ページのUIDの相当するようにしています。

uidを取得して、prismicのデータ(document)を呼び、
SliceZoneコンポーネントを使って、そのまま展開しています。

src/pages/topics/blog/[uid].vue
<script setup>
import { usePrismic } from "@prismicio/vue";
import { components } from "~/slices";
// const { client } = usePrismic();

const prismic = usePrismic();
const route = useRoute()

const { data: page } = useAsyncData(route.params.uid, () =>
    prismic.client.getByUID('blog_post', route.params.uid)
);

useHead({
    title: prismic.asText(page.value?.data.title)
})

</script>

<template>
    <SliceZone wrapper="main" :slices="page?.data.slices ?? []" :components="components" />
</template>

この状態で、http://localhost:3000/topics/blog/first_sampleを表示すると、
以下のように表示されます。
documentで1つずつ作成したImage,TextそれぞれのSliceに対応した部分は表示できていそうですが、
画像や文字列が取得できません。

各sliceのコンポーネントを実装

sliceをGUIで作成すると、それに応じたコンポーネントファイルは自動的に生成されるものの、
デフォルトでは、以下のように中身が実装されていません。
ここから、表示したい内容に編集していきます。

初期状態

上記の画面の通り、

slices/Text/index.vue
<script setup lang="ts">
import { type Content } from "@prismicio/client";

// The array passed to `getSliceComponentProps` is purely optional.
// Consider it as a visual hint for you when templating your slice.
defineProps(
  getSliceComponentProps<Content.TextSlice>([
    "slice",
    "index",
    "slices",
    "context",
  ]),
);
</script>

<template>
  <section
    :data-slice-type="slice.slice_type"
    :data-slice-variation="slice.variation"
  >
    Placeholder component for text (variation: {{ slice.variation }}) Slices
  </section>
</template>

表示させるときは、slice machineのGUIを見ながら実装すると分かりやすいです。
例えばTextの部分であれば、
GUI画面で、Show code snippets?部分をONにすると、
記述すべきNuxtのコードが表示されます。

snippets部分はこちら↓

<PrismicRichText :field="slice.primary.text" />

編集後

snippetsに置き換えてみると、、、

slices/Text/index.vue
<script setup lang="ts">
import { type Content, type HTMLRichTextMapSerializer } from '@prismicio/client'

// The array passed to \`getSliceComponentProps\` is purely optional.
// Consider it as a visual hint for you when templating your slice.
defineProps(
  getSliceComponentProps<Content.TextSlice>([
    "slice",
    "index",
    "slices",
    "context",
  ]),
);


</script>

<template>
  <section>
    <PrismicRichText :field="slice.primary.text" />
  </section>
</template>

テキスト部分が表示されました。

同様に、Imageも修正してみます。

初期状態

slices/Image/index.vue
<script setup lang="ts">
import { type Content } from "@prismicio/client";

// The array passed to `getSliceComponentProps` is purely optional.
// Consider it as a visual hint for you when templating your slice.
defineProps(
  getSliceComponentProps<Content.ImageSlice>([
    "slice",
    "index",
    "slices",
    "context",
  ]),
);
</script>

<template>
  <section
    :data-slice-type="slice.slice_type"
    :data-slice-variation="slice.variation"
  >
    Placeholder component for image (variation: {{ slice.variation }}) Slices
  </section>
</template>

編集後

slices/Image/index.vue
<script setup lang="ts">
import { type Content } from "@prismicio/client";

// The array passed to `getSliceComponentProps` is purely optional.
// Consider it as a visual hint for you when templating your slice.
defineProps(
  getSliceComponentProps<Content.ImageSlice>([
    "slice",
    "index",
    "slices",
    "context",
  ]),
);
</script>

<template>
  <PrismicImage :field="slice.primary.image" />
</template>

画像も表示されました。

prismicは"headless" CMSなので、デザイン、cssは自分で実装する必要が有ります。
逆に言えば、どんなデザイン・環境でも柔軟にカスタマイズできて、
データ部分のみを管理できるのが、headlessの良い所です。

yunayuna

Nuxt3 SSGによるファイル出力

NuxtでSSG用の静的ファイル出力は、

bun run generate

で出力できるが、ブログ記事のような動的なページについては、
事前にルーティングを指定しなければいけません。

※こちらの記事がとても参考になりました
https://zenn.dev/kon_karin/articles/0e514dea329044
※公式のissue
https://github.com/nuxt/nuxt/issues/13949

ブログのpathは、
https://xxxxx/topics/blog/[uid].vue
で、[uid]の部分はprismicで作成したdocumentのUIDになるため、
過去作成した全てのファイルを取得する必要があります。

routingの動的設定

prismicから、ブログページのUIDを含むパスを取得する関数を用意する

nuxt.config.ts
async function getAllBlogRoutes(): Promise<string[]> {
    const client = prismic.createClient(PRISMIC_REPOSITORY);
    const pages = await client.getAllByType("blog_post");
    
    return pages.map((page) => {
        return `/topics/blog/${page.uid}`;
    });
    // return [];
}

hookとして、ルーティング情報を事前に追加する

nuxt.config.ts
export default defineNuxtConfig({
    hooks: {
        // fetch async routes to prerender
        'prerender:routes': async (ctx: any) =>{
            const routes = await getAllBlogRoutes();
            if (routes && routes.length) {
            routes.forEach((route) => {
                ctx.routes.add(route);
            });
            }
        },
    },

これで、対象の静的ファイルが出力されるようになります。

$ bun run generate
・・・省略・・・

ℹ Initializing prerenderer                    
ℹ Prerendering 8 initial routes with crawler           
  ├─ /200.html (83ms)                       
  ├─ /404.html (86ms)                        
  ├─ / (157ms)                                  
  ├─ /api/preview (154ms)                   
  ├─ /api/preview/_payload.json?302213b5-2cf0-4cf5-b14c-c012295fd514 (3ms) (skipped)
  ├─ /api/preview/_payload.json (3ms)      
  ├─ /slice-simulator (184ms)           
  ├─ /slice-simulator/_payload.json?302213b5-2cf0-4cf5-b14c-c012295fd514 (0ms) (skipped) 
  ├─ /slice-simulator/_payload.json (1ms)      
  ├─ /topics/blog/first_sample (1226ms)    
  ├─ /topics/blog/first_sample/_payload.json?302213b5-2cf0-4cf5-b14c-c012295fd514 (2ms) (skipped)                    
  ├─ /topics/blog/first_sample/_payload.json (2ms)    
yunayuna

prismicプレビューの利用(現在、動作エラー中)

ブログ記事を投稿するときは、公開前にページの表示を確認する必要が有ります。
prismicでもプレビュー機能が用意されているので、利用するための設定を行います

公式

https://prismic.io/docs/preview#how-previews-work

公式 Nuxt3でのプレビュー実装

https://prismic.io/docs/nuxt-3-setup#open-your-page-builder-to-configure-previews
https://www.youtube.com/watch?v=q0g9vu10ANI

プレビュー設定画面で、サイト名、applicationのドメイン(https://<domain名>)、
必要であればプレビューのrouteパス(デフォルトはpreview)を入力し、保存します。

今回は、nuxt.config.tsで以下のようにpreview pathを指定しているので、これに合わせます

    prismic: {
        endpoint: 'xxxxx',
        preview: '/api/preview',


Nuxtでプレビューを設定

上記、prismicのpreview設定画面で、前画面にscriptの設置が必要と記載があるので、
nuxt.config.tsにコピペ追記して対応

nuxt.config.ts
app: {
  head: {
    script: [
      {
        src: 'https://static.cdn.prismic.io/prismic.js?new=true&repo=xxxxx',
        async: true,
        defer: true,
      }
    ]
  }
}

yunayuna

(previewがSSGで使えない問題、未解決です。分かる方いらっしゃったらコメントください!)

SSGだと、preview動作しないかも・・・?
https://community.prismic.io/t/nuxt-3-prismic-preview-not-working-with-ssg/13116

I just got my problem solved by using the Route Resolver :)

link resolver、Router Resolverで解決したとの報告が。

https://github.com/nuxt-modules/prismic/issues/191

link resolver, router resolverとは?何を設定すれば良い?
https://prismic.nuxtjs.org/guides/advanced/link-resolver/

https://prismic.io/docs/route-resolver

chatGPTにも聞いてみた

  1. Link Resolver: PrismicにおけるLink Resolverは、APIから返されるドキュメントのIDを実際のURLに変換する機能を持つ関数です。つまり、それはあなたのアプリケーションのルーティングシステムとPrismicのコンテンツをリンクする方法です。例えば、ブログ投稿のドキュメントがある場合、Link ResolverはそのドキュメントIDを"/blog/[投稿のID]"といった形式のURLに変換します。
  1. Router Resolver: Nuxt.jsにおけるRouter Resolverは、URLのパスからそれがどのページコンポーネントに対応するかを決定する機能を持つ関数です。Nuxt.jsでは、"pages"ディレクトリ内の.vueファイルの構造に基づいて自動的にルーティングが生成されますが、カスタムのルーティングロジックを制御するためにRouter Resolverを使用することができます。

両者は似ていますが、Link ResolverはAPIからのレスポンスをURLに変換する役割を持ち、一方、Router ResolverはURLを適切なページコンポーネントにリダイレクトする役割を持ちます。

簡単に言うと、prismicのdocumentについて、
そのページのURLがどうなるかを紐付ける仕組みのこと。

この設定は、nuxt.config.ts内で定義しているが、

nuxt.config.ts
        prismic: {
            routes: [
              //topics blog
              {
                type: "blog_post",
                path: "/topics/blog/:uid",
              },
            ],
        }

linkResolver.jsファイルを配置することでも指定することができ、
linkResolverがある場合は、ここで返り値が返ってきたらそのパスを、
nullが返ってきたら、nuxt.config.ts内のパスを取得する、という優先度の設定になっているらしい。

app/prismic/linkResolver.js
export default (doc) => {
	if (doc.type === "blog_post") {
		return `/topics/blog/${doc.uid}`;
	}
	return null;
};

linkResolverでの動作確認

上記linkResolver.jsがあり、なしどちらもNGだった。

previewのURLをローカルに設定

ローカルに設定して、ローカルでアプリを稼働しておくと、
ちゃんとプレビューが起動して、Publish前のdocumentが確認できた。

上記issueで議論されている、SSGでpreviewが閲覧できない原因と別の原因っぽいが、
原因わからないのでローカルで作業することで運用回避し、保留とする。