Open27

Prismic CMS 使う

ログインしたらダッシュボードページが表示される

https://prismic.io/dashboard

Prismic はサービスとかをリポジトリという単位で作成して扱う。
サブスクリプションやユーザ管理もこの単位で行う。

この辺についてのドキュメントは以下

https://prismic.io/docs/core-concepts#account-management

ダッシュボードで緑色の Create Repository ボタンをクリックするとリポジトリを作ることができる。

リポジトリの情報を入力し、プランを選択する。

ダッシュボードにリポジトリが表示された

リポジトリを選択すると、最初にデフォルト言語を聞かれる。
これはコンテンツの多言語化の文脈における言語。

ここからは、よくある Headless CMS と同様に型のモデリングを行う。
この辺のドキュメントは以下。

https://prismic.io/docs/core-concepts#content-modeling

ここではブログ記事を作ろうとしてみる。
Create custom type をクリックする。

2つの種類のコンテンツがある。
1つは、プログ記事などの複数コンテンツが存在しうるページに対応するもの。
もう1つは、トップページなどの単一コンテンツしか存在し得ないページに対応するもの。

いずれかを選択して、コンテンツの名前を入力する。 (ここでは BlogPost にしている)

コンテンツは、右側の基本型が表示されたリストから、左側の型情報の定義リストにドラッグドロップして作成する。
型にはapi key やプレースホルダ、型特有の設定を行う。

作成できたら、save ボタンを押して保存する。

サイドバーにある、”Documents” を選択して、コンテンツを作成する。
コンテンツ作成については主に以下のドキュメント。

https://prismic.io/docs/core-concepts#creating-content

3つタブがあるが、これはコンテンツのリリースに関係する。

鉛筆のボタンをクリックすると、どのコンテンツを作るか聞かれる。

コンテンツを作るページでは好きに値を入力する。
入力が終わったら保存する。

保存後、save ボタンが publish ボタンに変わる。
publish することで変更が公開する
publish しようとするとそのタイミングが聞かれる。

作ったコンテンツは一覧に表示される。

publish するとヘッダーの色が緑色になる。

Slices ・プレビュー・フロントエンドフレームワークとの統合は後回し。
とりあえず API 叩きたい。
(フロントエンドフレームワークとの統合はどのサービスに対してもそうだけど嫌いだから調べないかもしれない。)

API 通信をするにはトークンを生成する必要がある。

まず API の制限範囲を選択できる。

SSG でコンテンツを生成することを想定しているので、その範疇では Private API で十分そう。
Settings の API & Security で設定できる。

また、同じページでトークンも生成できる。

トークンで取得できるコンテンツの範囲ごとにそれぞれ生成できる。

master は publish されているコンテンツだけ取得でき、master + release は publish 前のコンテンツが取れる感じ。

GraphQL API を叩く。

ドキュメントは以下。

https://prismic.io/docs/technologies/intro-to-the-graphql-api

Header を2つ指定する必要がある。

  • Prismic-Ref: コンテンツのバージョン
  • Authorization: さっき作ったトークン

Prismic-Ref の値は Rest API じゃないと取れない。

$ . .env
$ curl \
    -G \
    -d "access_token=${PRISMIC_ACCESS_TOKEN}" \
    https://${PRISMIC_REPOSITORY_NAME}.cdn.prismic.io/api/v2 | jq ".refs"
[
  {
    "id": "master",
    "ref": "YduBkxIAACsAYgHD",
    "label": "Master",
    "isMasterRef": true
  }
]

.env の中身はこんなかんじ

PRISMIC_ACCESS_TOKEN=<TOKEN>
PRISMIC_REPOSITORY_NAME=<REPOSITORY NAME>

ここでは、master + release のトークンを使っているけど、publish していないコンテンツがないから一個だけ。

適当にコンテンツを変更して release で publish するようにしてみる。

もう一回取り直す。

$ . .env
$ curl \
    -G \
    -d "access_token=${PRISMIC_ACCESS_TOKEN}" \
    https://${PRISMIC_REPOSITORY_NAME}.cdn.prismic.io/api/v2 | jq ".refs"
[
  {
    "id": "master",
    "ref": "YduBkxIAACsAYgHD",
    "label": "Master",
    "isMasterRef": true
  },
  {
    "id": "YeJrhxEAACgA1CIF",
    "ref": "YeJrhxEAABIA1CIH~YduBkxIAACsAYgHD",
    "label": "test"
  }
]

トークンを master に変更してAPIを叩き直してみる。

$ . .env
$ curl \
    -G \
    -d "access_token=${PRISMIC_ACCESS_TOKEN}" \
    https://${PRISMIC_REPOSITORY_NAME}.cdn.prismic.io/api/v2 | jq ".refs"
[
  {
    "id": "master",
    "ref": "YduBkxIAACsAYgHD",
    "label": "Master",
    "isMasterRef": true
  }
]

master ref だけになる。
なので、本番環境のビルドには master トークンを使って、検証環境では master + release トークンを使って、特定バージョンのコンテンツを見るとかになりそう。

とりあえず、 ref が手に入るので、 GraphQL も叩いてみる。

$ . .env
$ query=`cat << EOS
{
    allBlogposts {
        totalCount
        edges {
            node {
                _meta {
                  id
                  uid
                }
                hero
                title
                description
                contents
            }
        }
    }
}
EOS
`;
$ query=`echo ${query} | sed -e "s/[\n]\+//g"`
$ curl \
  -G \
  -H "Prismic-Ref:YduBkxIAACsAYgHD" \
  -H "Authorization:Token ${PRISMIC_ACCESS_TOKEN}" \
  --data-urlencode query="${query}" \
  https://${PRISMIC_REPOSITORY_NAME}.cdn.prismic.io/graphql | jq
{
  "data": {
    "allBlogposts": {
      "totalCount": 1,
      "edges": [
        {
          "node": {
            "_meta": {
              "id": "YduAuRIAAC8AYf3t",
              "uid": "test"
            },
            "hero": {
              "dimensions": {
                "width": 2048,
                "height": 1536
              },
              "alt": "test",
              "copyright": null,
              "url": "https://images.prismic.io/slicemachine-blank/26d81419-4d65-46b8-853e-8ea902e160c1_groovy.png?auto=compress,format"
            },
            "title": [
              {
                "type": "heading1",
                "text": "テスト",
                "spans": []
              }
            ],
            "description": [
              {
                "type": "paragraph",
                "text": "テストコンテンツ",
                "spans": []
              }
            ],
            "contents": [
              {
                "type": "paragraph",
                "text": "これはテストコンテンツ。",
                "spans": []
              },
              {
                "type": "o-list-item",
                "text": "リッチテキスト",
                "spans": []
              },
              {
                "type": "o-list-item",
                "text": "だから",
                "spans": []
              },
              {
                "type": "o-list-item",
                "text": "いくつかの",
                "spans": []
              },
              {
                "type": "o-list-item",
                "text": "装飾文字が使える",
                "spans": []
              }
            ]
          }
        }
      ]
    }
  }
}

また、GraphQL Explorer は https://<リポジトリ名>.prismic.io/graphql で閲覧できる。

次はWeb アプリケーションの中で使うようにする方法。

SSG する想定。どのフロントエンドライブラリ使うかケンカしないために astro 使う。

$ npm init astro

Welcome to Astro! (create-astro v0.7.0)
If you encounter a problem, visit https://github.com/withastro/astro/issues to search or file a new issue.

> Prepare for liftoff.
> Gathering mission details...
✔ Which app template would you like to use? › Minimal
> Copying project files...
✔ Done!

Next steps:
  1: npm install (or pnpm install, yarn, etc)
  2: git init && git add -A && git commit -m "Initial commit" (optional step)
  3: npm run dev (or pnpm, yarn, etc)

To close the dev server, hit Ctrl-C

Stuck? Visit us at https://astro.build/chat

拡張機能追加するので、プロジェクトの推奨に追加。

.vscode/extentions.json
{
  "recommendations": [
    "astro-build.astro-vscode",
    "graphql.vscode-graphql"
  ]
}

GraphQL schema の取得がややこしいので、codegen するのに工夫がいる。
一応 custom fetch を書いている人がいるけど、用途に応じて振る舞い変えたくなりそうなので自分で書く予定。

https://www.npmjs.com/package/codegen-prismic-fetch

依存パッケージ入れる

$ npm i graphql
$ npm i -D @graphql-codegen/cli @graphql-codegen/typescript codegen-prismic-fetch

設定ファイル作る。

schema:
  - https://${PRISMIC_REPOSITORY_NAME}.prismic.io/graphql:
      customFetch: codegen-prismic-fetch
extensions:
  codegen:
    generates:
      ./types/graphql.ts:
        plugins:
          - typescript

npm scripts に以下を追加

    "generate": "graphql-codegen --config graphql.config.yml -r dotenv/config"

以下で型を生成する

$ npm run generate

生成されたのは以下のような感じ

生成された型
types/graphql.ts
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  /** DateTime */
  DateTime: any;
  /** Raw JSON value */
  Json: any;
  /** The `Long` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1. */
  Long: any;
};

export type Blogpost = _Document & _Linkable & {
  __typename?: 'Blogpost';
  _linkType?: Maybe<Scalars['String']>;
  _meta: Meta;
  contents?: Maybe<Scalars['Json']>;
  description?: Maybe<Scalars['Json']>;
  hero?: Maybe<Scalars['Json']>;
  title?: Maybe<Scalars['Json']>;
};

/** A connection to a list of items. */
export type BlogpostConnectionConnection = {
  __typename?: 'BlogpostConnectionConnection';
  /** A list of edges. */
  edges?: Maybe<Array<Maybe<BlogpostConnectionEdge>>>;
  /** Information to aid in pagination. */
  pageInfo: PageInfo;
  totalCount: Scalars['Long'];
};

/** An edge in a connection. */
export type BlogpostConnectionEdge = {
  __typename?: 'BlogpostConnectionEdge';
  /** A cursor for use in pagination. */
  cursor: Scalars['String'];
  /** The item at the end of the edge. */
  node: Blogpost;
};

export type Meta = {
  __typename?: 'Meta';
  /** Alternate languages the document. */
  alternateLanguages: Array<RelatedDocument>;
  /** The first publication date of the document. */
  firstPublicationDate?: Maybe<Scalars['DateTime']>;
  /** The id of the document. */
  id: Scalars['String'];
  /** The language of the document. */
  lang: Scalars['String'];
  /** The last publication date of the document. */
  lastPublicationDate?: Maybe<Scalars['DateTime']>;
  /** The tags of the document. */
  tags: Array<Scalars['String']>;
  /** The type of the document. */
  type: Scalars['String'];
  /** The uid of the document. */
  uid?: Maybe<Scalars['String']>;
};

/** Information about pagination in a connection. */
export type PageInfo = {
  __typename?: 'PageInfo';
  /** When paginating forwards, the cursor to continue. */
  endCursor?: Maybe<Scalars['String']>;
  /** When paginating forwards, are there more items? */
  hasNextPage: Scalars['Boolean'];
  /** When paginating backwards, are there more items? */
  hasPreviousPage: Scalars['Boolean'];
  /** When paginating backwards, the cursor to continue. */
  startCursor?: Maybe<Scalars['String']>;
};

export type Query = {
  __typename?: 'Query';
  _allDocuments: _DocumentConnection;
  allBlogposts: BlogpostConnectionConnection;
  allToppages: ToppageConnectionConnection;
  blogpost?: Maybe<Blogpost>;
};


export type Query_AllDocumentsArgs = {
  after?: InputMaybe<Scalars['String']>;
  before?: InputMaybe<Scalars['String']>;
  first?: InputMaybe<Scalars['Int']>;
  firstPublicationDate?: InputMaybe<Scalars['DateTime']>;
  firstPublicationDate_after?: InputMaybe<Scalars['DateTime']>;
  firstPublicationDate_before?: InputMaybe<Scalars['DateTime']>;
  fulltext?: InputMaybe<Scalars['String']>;
  id?: InputMaybe<Scalars['String']>;
  id_in?: InputMaybe<Array<Scalars['String']>>;
  lang?: InputMaybe<Scalars['String']>;
  last?: InputMaybe<Scalars['Int']>;
  lastPublicationDate?: InputMaybe<Scalars['DateTime']>;
  lastPublicationDate_after?: InputMaybe<Scalars['DateTime']>;
  lastPublicationDate_before?: InputMaybe<Scalars['DateTime']>;
  similar?: InputMaybe<Similar>;
  sortBy?: InputMaybe<SortDocumentsBy>;
  tags?: InputMaybe<Array<Scalars['String']>>;
  tags_in?: InputMaybe<Array<Scalars['String']>>;
  type?: InputMaybe<Scalars['String']>;
  type_in?: InputMaybe<Array<Scalars['String']>>;
};


export type QueryAllBlogpostsArgs = {
  after?: InputMaybe<Scalars['String']>;
  before?: InputMaybe<Scalars['String']>;
  first?: InputMaybe<Scalars['Int']>;
  firstPublicationDate?: InputMaybe<Scalars['DateTime']>;
  firstPublicationDate_after?: InputMaybe<Scalars['DateTime']>;
  firstPublicationDate_before?: InputMaybe<Scalars['DateTime']>;
  fulltext?: InputMaybe<Scalars['String']>;
  id?: InputMaybe<Scalars['String']>;
  id_in?: InputMaybe<Array<Scalars['String']>>;
  lang?: InputMaybe<Scalars['String']>;
  last?: InputMaybe<Scalars['Int']>;
  lastPublicationDate?: InputMaybe<Scalars['DateTime']>;
  lastPublicationDate_after?: InputMaybe<Scalars['DateTime']>;
  lastPublicationDate_before?: InputMaybe<Scalars['DateTime']>;
  similar?: InputMaybe<Similar>;
  sortBy?: InputMaybe<SortBlogposty>;
  tags?: InputMaybe<Array<Scalars['String']>>;
  tags_in?: InputMaybe<Array<Scalars['String']>>;
  uid?: InputMaybe<Scalars['String']>;
  uid_in?: InputMaybe<Array<Scalars['String']>>;
  where?: InputMaybe<WhereBlogpost>;
};


export type QueryAllToppagesArgs = {
  after?: InputMaybe<Scalars['String']>;
  before?: InputMaybe<Scalars['String']>;
  first?: InputMaybe<Scalars['Int']>;
  firstPublicationDate?: InputMaybe<Scalars['DateTime']>;
  firstPublicationDate_after?: InputMaybe<Scalars['DateTime']>;
  firstPublicationDate_before?: InputMaybe<Scalars['DateTime']>;
  fulltext?: InputMaybe<Scalars['String']>;
  id?: InputMaybe<Scalars['String']>;
  id_in?: InputMaybe<Array<Scalars['String']>>;
  lang?: InputMaybe<Scalars['String']>;
  last?: InputMaybe<Scalars['Int']>;
  lastPublicationDate?: InputMaybe<Scalars['DateTime']>;
  lastPublicationDate_after?: InputMaybe<Scalars['DateTime']>;
  lastPublicationDate_before?: InputMaybe<Scalars['DateTime']>;
  similar?: InputMaybe<Similar>;
  sortBy?: InputMaybe<SortToppagey>;
  tags?: InputMaybe<Array<Scalars['String']>>;
  tags_in?: InputMaybe<Array<Scalars['String']>>;
  uid?: InputMaybe<Scalars['String']>;
  uid_in?: InputMaybe<Array<Scalars['String']>>;
  where?: InputMaybe<WhereToppage>;
};


export type QueryBlogpostArgs = {
  lang: Scalars['String'];
  uid: Scalars['String'];
};

export type RelatedDocument = {
  __typename?: 'RelatedDocument';
  /** The id of the document. */
  id: Scalars['String'];
  /** The language of the document. */
  lang: Scalars['String'];
  /** The type of the document. */
  type: Scalars['String'];
  /** The uid of the document. */
  uid?: Maybe<Scalars['String']>;
};

export enum SortBlogposty {
  ContentsAsc = 'contents_ASC',
  ContentsDesc = 'contents_DESC',
  DescriptionAsc = 'description_ASC',
  DescriptionDesc = 'description_DESC',
  MetaFirstPublicationDateAsc = 'meta_firstPublicationDate_ASC',
  MetaFirstPublicationDateDesc = 'meta_firstPublicationDate_DESC',
  MetaLastPublicationDateAsc = 'meta_lastPublicationDate_ASC',
  MetaLastPublicationDateDesc = 'meta_lastPublicationDate_DESC',
  TitleAsc = 'title_ASC',
  TitleDesc = 'title_DESC'
}

export enum SortDocumentsBy {
  MetaFirstPublicationDateAsc = 'meta_firstPublicationDate_ASC',
  MetaFirstPublicationDateDesc = 'meta_firstPublicationDate_DESC',
  MetaLastPublicationDateAsc = 'meta_lastPublicationDate_ASC',
  MetaLastPublicationDateDesc = 'meta_lastPublicationDate_DESC'
}

export enum SortToppagey {
  MetaFirstPublicationDateAsc = 'meta_firstPublicationDate_ASC',
  MetaFirstPublicationDateDesc = 'meta_firstPublicationDate_DESC',
  MetaLastPublicationDateAsc = 'meta_lastPublicationDate_ASC',
  MetaLastPublicationDateDesc = 'meta_lastPublicationDate_DESC',
  TitleAsc = 'title_ASC',
  TitleDesc = 'title_DESC'
}

export type Toppage = _Document & _Linkable & {
  __typename?: 'Toppage';
  _linkType?: Maybe<Scalars['String']>;
  _meta: Meta;
  title?: Maybe<Scalars['Json']>;
};

/** A connection to a list of items. */
export type ToppageConnectionConnection = {
  __typename?: 'ToppageConnectionConnection';
  /** A list of edges. */
  edges?: Maybe<Array<Maybe<ToppageConnectionEdge>>>;
  /** Information to aid in pagination. */
  pageInfo: PageInfo;
  totalCount: Scalars['Long'];
};

/** An edge in a connection. */
export type ToppageConnectionEdge = {
  __typename?: 'ToppageConnectionEdge';
  /** A cursor for use in pagination. */
  cursor: Scalars['String'];
  /** The item at the end of the edge. */
  node: Toppage;
};

export type WhereBlogpost = {
  /** contents */
  contents_fulltext?: InputMaybe<Scalars['String']>;
  /** description */
  description_fulltext?: InputMaybe<Scalars['String']>;
  /** title */
  title_fulltext?: InputMaybe<Scalars['String']>;
};

export type WhereToppage = {
  /** title */
  title_fulltext?: InputMaybe<Scalars['String']>;
};

/** A prismic document */
export type _Document = {
  _meta: Meta;
};

/** A connection to a list of items. */
export type _DocumentConnection = {
  __typename?: '_DocumentConnection';
  /** A list of edges. */
  edges?: Maybe<Array<Maybe<_DocumentEdge>>>;
  /** Information to aid in pagination. */
  pageInfo: PageInfo;
  totalCount: Scalars['Long'];
};

/** An edge in a connection. */
export type _DocumentEdge = {
  __typename?: '_DocumentEdge';
  /** A cursor for use in pagination. */
  cursor: Scalars['String'];
  /** The item at the end of the edge. */
  node: _Document;
};

/** An external link */
export type _ExternalLink = _Linkable & {
  __typename?: '_ExternalLink';
  _linkType?: Maybe<Scalars['String']>;
  target?: Maybe<Scalars['String']>;
  url: Scalars['String'];
};

/** A linked file */
export type _FileLink = _Linkable & {
  __typename?: '_FileLink';
  _linkType?: Maybe<Scalars['String']>;
  name: Scalars['String'];
  size: Scalars['Long'];
  url: Scalars['String'];
};

/** A linked image */
export type _ImageLink = _Linkable & {
  __typename?: '_ImageLink';
  _linkType?: Maybe<Scalars['String']>;
  height: Scalars['Int'];
  name: Scalars['String'];
  size: Scalars['Long'];
  url: Scalars['String'];
  width: Scalars['Int'];
};

/** A prismic link */
export type _Linkable = {
  _linkType?: Maybe<Scalars['String']>;
};

export type Similar = {
  documentId: Scalars['String'];
  max: Scalars['Int'];
};

実際ほしいのは API クライアントなので、それ作る

$ npm i graphql-request
$ npm i -D @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request

設定ファイルは以下

graphql.config.yml
schema:
  - https://${PRISMIC_REPOSITORY_NAME}.prismic.io/graphql:
      customFetch: codegen-prismic-fetch
documents: graphql/**/*.graphql
extensions:
  codegen:
    generates:
      ./generated-client.ts:
        plugins:
          - typescript
          - typescript-operations
          - typescript-graphql-request

適当に query 書く

graphql/allPost.graphql
query fetchAllPosts {
  allBlogposts {
    totalCount
    edges {
      node {
        _meta {
          id
          uid
        }
        hero
        title
        description
        contents
      }
    }
  }
}

以下でクライアントを生成

$ npm run generate

生成したクライアントを使う。

環境変数がほしいので、dotenv 入れる

$ npm i dotenv

あとは雑に fetch してコンテンツを埋め込めばいい

src/pages/index.astro
---
import { getSdk } from "../../generated-client.ts";
import { GraphQLClient } from 'graphql-request';
import dotenv from "dotenv";
const { parsed: env } = dotenv.config()
const ref = await fetch(`https://${env.PRISMIC_REPOSITORY_NAME}.cdn.prismic.io/api/v2?access_token=${encodeURIComponent(env.PRISMIC_ACCESS_TOKEN)}`)
	.then(r => r.json())
	.then(({ refs }) => refs.find((ref) => ref.isMasterRef))
	.then(({ref}) => ref)
const { allBlogposts } = await getSdk(new GraphQLClient(`https://${env.PRISMIC_REPOSITORY_NAME}.cdn.prismic.io/graphql`, {
    headers: {
      Authorization: `Token ${env.PRISMIC_ACCESS_TOKEN}`,
	  "Prismic-Ref": ref
    },
	method: "GET"
  })).fetchAllPosts();
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>ALl Blog Posts</title>
  </head>
  <body>
    <h1>ブログ</h1>
    <pre>{ JSON.stringify(allBlogposts, null, 2)}</pre>
  </body>
</html>
$ npm run dev

こんな感じになる

基本はこれで、あとは適切にコードの配置を変えればいい。

Preview について

https://prismic.io/docs/core-concepts/preview-setup
https://prismic.io/docs/technologies/previews-and-the-prismic-toolbar-graphql

preview ページは setting の Previews で設定できる。

プレビュー環境は複数作ることができる。
環境名・ドメイン・パス名 (任意) を設定する。

コンテンツ編集画面かリリース画面にある、目のアイコンをクリックするとプレビューのURLが新しいタブで開く。
このとき、一時的に使えるトークンと、プレビュー対象のドキュメントIDがクエリパラメータに付与されている。

また、preview したいバージョンのコンテンツを取得するための ref が JS で取得できるCookie (io.prismic.preview)にセットされている。

なので、Cookie がセットされていたらそれを Prismic-Ref ヘッダーにつけて送信する。

パラメータについている一時トークンは API の可視性を Public にしないと利用できない?

When you preview your website, a preview cookie is generated that contains the preview token. This token can be used as a valid ref to make Prismic API queries.

だから、Cookie の値を ref に入れるだけで十分なのかな。

とりあえず、

  1. プレビュー用ページがトークンとかコンテンツIDを含んだ状態で表示
  2. リダイレクト先を特定して遷移
  3. 遷移先でプレビューコンテンツを取得してコンテンツ表示

という流れで動かす。
2 の段階で Prismic の SDK と リンク解決のロジックを使う。
3 はクライアントから Prismic の API を叩く必要がある。
なので、3 に必要なロジックは環境変数でビルドに含める or 落とすができる状態なのが望ましいはず。

ここを見る限り@prismic/client を使えばcookie チェックは勝手にやりますってことなのか。

https://prismic.io/docs/technologies/preview-drafts-vanilla-javascript

Draft content is not publicly accessible on your API endpoint, so how does it appear in your preview? When you click the "preview" button in the Prismic editor, Prismic sets a cookie in your browser for the preview session. All of the query methods in @prismicio/client check for that cookie when they query content from Prismic.

If the cookie is present, the query methods will include it in the API query, and you will receive draft content for that preview session. (By default, a preview session previews the draft content in your repository. But if you preview a Release, you will see the content from that Release instead.) The Prismic Toolbar includes an "X" button to quit the preview session, finish the preview session, and delete the preview cookie. You can also do this manually in your browser dev tools.

@prismicio/client は graphql のリクエストできるのかな。

以下のように書くと、linkResolver に応じてコンテンツのページにリダイレクトされる。

---
import dotenv from "dotenv";
const { parsed: env } = dotenv.config()
const repository = env.PRISMIC_REPOSITORY_NAME
---
<!DOCTYPE html>
<html>
  <head>
    <title>Preview route</title>
	<script id="toolbar-script" src={`https://static.cdn.prismic.io/prismic.js?new=true&repo=${repository}`}></script>
    <script type="module">
      import * as prismic from 'https://cdn.skypack.dev/@prismicio/client'

      const repoName = document.querySelector("#toolbar-script").getAttribute("src").replace(/^.+repo=/, "")
      const endpoint = prismic.getEndpoint(repoName)
      const client = prismic.createClient(endpoint)
      const init = async () => {
        const defaultURL = '/'
        const url = await client.resolvePreviewURL({
          defaultURL,
          linkResolver: (doc) => {
            if (doc.type === 'blogpost') return `/posts/${doc.uid}`
            return '/'
          }
        })
        window.location.replace(url)
      }

      init()
    </script>
  </head>
  <body>
    <h1>Loading Preview...</h1>
  </body>
</html>

あとは、ランタイムでコンテンツを取得して、表示を書き換えるようになっていればいい。
クエリパラメータのトークンとかドキュメントIDを取得したりとかは prismic client がよしなにやる。
結局 API は Public にする必要があった。

VSCode の GraphQL 拡張機能が customFetch の設定をうまく処理できていない。
graphql の設定を js にして customFetch の値はモジュール名じゃなくて、モジュールを require した戻り値にすれば動く。

ログインするとコメントできます