iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0

一直在想要不要寫這個主題,因為 Visual Editing 這個需要介紹的技術實在太多了,從 Sanity 本身的設定到 Next.js 前端專案的設定,是一個需要相對廣泛理解才能使用的技術。

Visual Editing 可以在 Sanity 編輯頁面的同時,即時看到編輯後網頁的樣子:

https://ithelp.ithome.com.tw/upload/images/20241011/20101989YYPKPs7rIy.jpg

https://ithelp.ithome.com.tw/upload/images/20241011/20101989WuDmleQbP7.jpg


Sanity 與 Next.js 連動 Presentation

Sanity 設定

一開始設定,在 Sanity 的 sanity.config.ts 引入 sanity/presentation 的 presentationTool,並且設定 previewUrl

// ...
import {presentationTool} from 'sanity/presentation'

export default defineConfig({
	// ...
  plugins: [
	  // ...
    presentationTool({
      previewUrl: 'http://localhost:3000',
    }),
  ],
	// ...
})

因為我的 Next.js 專案跟 Sanity 專案是的專案,這邊就設定為 Next.js 的服務位置。

Next.js 設定

設定完後再到 Next.js 專案內設定:

.
├── app
│   ├── layout.tsx // 前端頁面
│   └── sanity
│       ├── lib
│       │   ├── client.ts // Sanity 連線模組
// ...

Next.js 的話主要是在 createClient 的設定, apiVersion 可以設定今天的日子,Sanity 會自動從當日開始找最新的版本。

然後再設定 stegaenabledtruestudioUrl 為 Sanity 服務的 http://localhost:3333

import { createClient } from "next-sanity";
import { dataset, projectId } from "../env";
import imageUrlBuilder from "@sanity/image-url";
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";

export const client = createClient({
  projectId,
  dataset,
  useCdn: true,
  apiVersion: "2024-10-05", // 選擇最新的
  stega: {
    enabled: true, // 全站開啟
    studioUrl: "http://localhost:3333", // Sanity 的服務位置
  },
});

// ...

到這邊還沒設定完,還要在前端頁面引入 next-sanity<VisualEditing />

// ...

import { VisualEditing } from "next-sanity";

// ...

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <Navbar />
        {children}
        <VisualEditing /> {/* 引入 VisualEditing 設定 */}
      </body>
    </html>
  );
}

設定到這邊初步的連動就可以算是設定好了,就會有上面的看到的連動畫面了。
但現在還有幾個問題:

https://ithelp.ithome.com.tw/upload/images/20241012/20101989uk3LHguqLP.png

https://ithelp.ithome.com.tw/upload/images/20241012/201019897HuOUaxsKu.jpg

首頁中的 Logo 圖片跟日期沒有 click-to-edit 功能,

click-to-edit 是 Sanity 對可編輯的標籤的稱呼
大多數的字串欄位都可以直接推斷出來
type 是 image, date 或是從 plugin 來的 markdown 就無法直接推斷出來。

在引入 stega 跟 <VisualEditing /> 的同時, Sanity 會根據 client.fetch() 內的查詢語法跟引入的 html 標籤自行推斷 click-to-edit 欄位的連動。

但是像說 image 在使用的時候不僅包裹了一層 <SanityImage>,還用了 @sanity/image-url 處理過 url 結構,對於這種經過複雜處理的 Sanity 通常無法直接推斷出對應的 Sanity 欄位。

講自行推斷其實不精準,而是由查詢語法 Content Source Maps 判斷出來的

這時候要怎麼辦?


Visual Editing Overlays

這時候就可以透過手動 Overlays 的動作來完成 click-to-edit 的建立。

因為使用的是 Next.js 專案 ,可以直接從 next-sanity 中引入 createDataAttribute,再用 createDataAttribute 定義物件 id, type,取得 attr() 方法,再由 attr() 方法建立出 click-to-editContent Source Maps ( CSM ),傳到任何一個標籤的 data-sanity 屬性中:

// ./app/[slug]/page.tsx

import { client } from "@/app/sanity/lib/client";
import { createDataAttribute } from "next-sanity";

export default async function Post({ params }: { params: { slug: string } }) {
  const { slug } = params;

  const post = await client.fetch(BLOG_POSTS_BY_SLUG_QUERY, {
    slug: decodeURI(slug),
  });
  
  // ...

  const attr = createDataAttribute({
    id: post._id,
    type: post._type,
  });

  return (
    <div className="post max-w-screen-md p-5">
      <div className="flex text-xs">
        <div
          data-sanity={attr("publishedAt")}  // <- 指定 publishedAt 欄位為 click-to-edit
          className="flex items-center px-2 py-1 border-primary-300 text-primary-300 border rounded-3xl"
        >
          <FaCalculator className="mr-1" />
          <span>{post.publishedAt}</span>
        </div>
        {/*  */}
      </div>
      {/* ... */}
    </div>
  );
}

可以看到在這邊使用了 createDataAttribute() 方法傳入了 idtype 取得了 attr,並且傳入 "publishedAt" 作為參數,這樣在 Sanity 的 Presentation 頁面就會連結到對應的欄位了。

可是還有一個問題,那就是在 Next.js 的 Visual Editing 模式下的網址會有錯誤:

https://ithelp.ithome.com.tw/upload/images/20241012/20101989kL8uwRI2wC.jpg

這問題就出在 createDataAttribute() 預設的 url 是跟著 Next.js 專案本身的 url,可是應該要是當初在 ./app/sanity/lib/client.ts 中設定的 http://localhost:3333 才對。

createDataAttribute() 也確實提供了 baseUrl 參數讓我們設定:

const attr = createDataAttribute({
  baseUrl: studioUrl,
  id: post._id,
  type: post._type,
});

但是總不想要每次用到 createDataAttribute() 的時候都重複設定 baseUrl,
這時候就可以使用到 createDataAttribute() 建立後的 combine() 方法了。

先回到 Next.js 專案中:

.
├── README.md
├── app
│   ├── [slug]
│   │   └── page.tsx // <- 文章內容
│   ├── components
│   │   ├── PostPreview.tsx // <- 首頁文章預覽
// ...
│   └── sanity
│       ├── lib
│       │   ├── client.ts // <- Sanity 連線設定檔
// ...

./app/sanity/lib/client.ts 中從 next-sanity 再引入 createDataAttribute 並且設定他的 baseUrl,並且命名為 baseDataAttribute

import { createClient, createDataAttribute } from "next-sanity";
// ...

export const client = createClient({
  projectId,
  dataset,
  useCdn: true,
  apiVersion: "2024-10-05",
  stega: {
    enabled: true,
    studioUrl: "http://localhost:3333",
  },
});

// 設定 baseUrl,並且 export 出去
export const baseDataAttribute = createDataAttribute({
  baseUrl: "http://localhost:3333",
});

再到 ./app/[slug]/page.tsx

// ./app/[slug]/page.tsx

import { client, baseDataAttribute } from "@/app/sanity/lib/client";
//...

export default async function Post({ params }: { params: { slug: string } }) {
  const { slug } = params;

  const post = await client.fetch(BLOG_POSTS_BY_SLUG_QUERY, {
    slug: decodeURI(slug),
  });
  
  // ...

	// 使用 combine 連結 baseUrl 資訊,再傳入 id 與 type
  const attr = baseDataAttribute.combine({
    id: post._id,
    type: post._type,
  });

  return (
    <div className="post max-w-screen-md p-5">
      <div className="flex text-xs">
        <div
          data-sanity={attr("publishedAt")} // <- 指定 publishedAt 欄位為 click-to-edit
          className="flex items-center px-2 py-1 border-primary-300 text-primary-300 border rounded-3xl"
        >
          <FaCalculator className="mr-1" />
          <span>{post.publishedAt}</span>
        </div>
        {/*  */}
      </div>
      {/* ... */}
    </div>
  );
}

這樣就不用每次重複定義 baseUrl 了。

首頁的文章預覽卡片也可以這樣改:

"use client";
import Link from "next/link";
import { baseDataAttribute } from "@/app/sanity/lib/client";
import type { BlogPost } from "@/app/sanity/types";
import NProgress from "nprogress";

export default function PostPreview({ post }: { post: BlogPost }) {
  const attr = baseDataAttribute.combine({
    id: post._id,
    type: post._type,
  });
  return (
    <li className="py-8 border-b border-b-neutral-800">
      <h2 className="text-3xl tracking-wider font-bold text-neutral-200">
        <Link href={`/${post.slug.current}`}>{post.title}</Link>
      </h2>
      <div className="text-base font-bold text-neutral-200 mt-5">
        {post.tags?.map((tag) => (
          <span
            className="px-3 first:pl-0 border-r border-r-neutral-200"
            key={tag}
          >
            {tag}
          </span>
        ))}
        <span className="px-3" data-sanity={attr("publishedAt")}>
          {post.publishedAt}
        </span>
      </div>
      <h3 className="text-lg font-light mt-5">{post.subtitle}</h3>
      <div className="mt-5">
        <Link
          className="inline-block border-2 border-neutral-200 text-neutral-200 px-3 py-2 text-sm font-bold rounded uppercase"
          href={`/${post.slug.current}`}
          onClick={() => NProgress.start()}
        >
          Read More
        </Link>
      </div>
    </li>
  );
}

本篇到這邊告一段落,Sanity Visual Editing 的功能可以說是只套用上 35% - 40% 而已。

想想現在顯而易見可優化的問題有:

  • 現在是整個 Next.js 的環境都跑出來了 click-to-edit 的框框,應該只有在 Sanity 的 Visual Editing 的視窗才有 click-to-edit 的視窗才對。
  • 就算在 Sanity Visual Editing 視窗更新了,也沒有及時響應到畫面,甚至是要刪除快取才會得到新的畫面。

等待這些問題處理完後才可以說是完全導入了 Sanity Visual Editing


上一篇
Day 26 - Next.js + Sanity Webhook
下一篇
Day 28 - Next.js Draft Mode With Sanity Visual Editing
系列文
用 Sanity 跟 Nextjs 重寫個人部落格30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言