herohoroブログ

クエリパラメータで異なるサーバーと通信できるようにする | PUT・bookmark-scratch・SameOriginPolicy・CORS_ Blog learn08



🔄   2023-03-14

いいねボタンの解説。

そして、ついこの間

本家でサポートされたbookmarkのScratch開発の解説。

ちょいと喉越しが悪いので

私なりに整理してみることにしました。

整理し終わって分かったのは、

「フロントから考えないと混乱してしまう…」

という自分の弱点や、

「どの処理がサーバーでどこまでがフロントなのかの理解が甘い」

ということが分かりました(´;ω;`)

弱点や甘さを少しでも改善するべく、

アルパカ先生の解説記事を読み返しながら、

ノートにメモしたり、

ぼんやり考えてみたり、、、、

少しずつ言葉にして

文章にしてみたのがこの記事になります 📝

いつもの動き

フロントから同じドメインのURLにリクエストする様子を図にすると….

いつもの動き

ブログの記事一覧でタイトルをクリックすると記事本文が表示されたり、

記事一覧の末尾にある次ページへのボタンを押すと続きの記事一覧が表示されたり、

タグをクリックするとクリックしたタグを含む記事一覧が表示されたり…..

ブログに遊びに来た人の行動に合わせて表示させる情報を切り替えているのが【いつもの動き】でした。

そして、

easy-notion-blogがツヨツヨになるにつれて

「異なるドメインでやりとりできるようになった」というのが今回の本題です。

  • いいねボタンを押してNotionAPIに値を追加してもらう
ドメインが異なるとできない

  • ブックマークブロックを埋め込んだ時に埋め込み先のサムネやタイトルを表示させたい
ドメインが異なるとできない

Same-Origin Policy違反によってガードされてしまっている異なるドメインへの処理。

この処理をするためにちょっとワンバウンドさせたのがアルパカ先生の解説です。

image block

https://alpacat.com/blog/update-notion-db-by-like-button

私はこれを勝手に【二段階右折】と名付けることにしました 🤟(´・ω・`) ✨

二段階右折とは

軽車両や原付が片側3車線以上の道路を右折する際、2回信号に従うことで右折をすること。法定速度が30kmの原付が、右折のために片側3車線以上の道路の一番右車線を走行すると、周囲の車両にとって交通の妨げになってしまいます。最悪の場合衝突事故に繋がる可能性もあるでしょう。
二段階右折が求められている道路で右折をする際は、左車線から交差点に直進して入り、交差点を渡りきった場所で向きを変えてまた直進しなければなりません。二段階右折をすることで、速度の出せない軽車両や原付が左車線を走行し続けても右折が可能です。

異なるドメインでアクセスするなら二段階右折

【二段階右折】に従っていいねボタンの仕組みをもう一度図にしてみます。

二段階右折①
二段階右折②
二段階右折③

組み立てる

フロントから組み立てる工程を追いながら整理すると….

【いつもの動き】を実装する流れと同じです。

ただ、今回は『取得』ではなく『更新』なので、

流れが逆になります。

処理工程

取得の時は….

取得準備(型定義・プロパティ追加)

取得

表示

といった流れで実装していました。

更新の場合は…..

component/like-button.tsx:フロント

  • 表示:いいねボタン
  • 押す:いいねボタン

axios.put

api/ like.tsで更新(PUT)通信:サーバー

  • slugから記事情報を取得する
  • 取得した記事情報をincrementLikeコンポネントに入れる

incrementLikeコンポネントが教えてくれたLikeの値を更新できるようにする:サーバー

  • 更新準備(型定義・プロパティ追加)
  • 更新:incrementLikeコンポネントを実行する

NotionAPIが動いてNotionDBのLike列が更新される

🙉
表示が先になってる〜〜〜

工程ごとにコードを確認

フロント・サーバー・Notionの方のサーバーの順に見ていきます(´・ω・`)

😨
… もしかしてほとんどサーバー部分ってこと!?
修正していきますので「どこまでフロントだ?」と考えながら読み進めていただければと思いますー。
無意識にNode.jsを動かしていたと思うとなんか感動でたまらんです( ゚д゚)
アクション待ちのフロント

二段階右折①

  • 表示:いいねボタン
<LikeButton slug={post.Slug} />
表示したい場所に追記 [slug].tsx内のfooter内

  • 押せる:いいねボタン
type Props = {
  id: string
}
const LikeButton = (props: Props) => {
  const [active, setActive] = useState(false)

  const handleClick = () => {
    if (!active) {
      axios.put(`/api/like?slug=${props.slug}`, {})
      setActive(true)
    }
  }

  return (
    <button onClick={handleClick}>Like</button>
  )
}
component/like-button.tsx

いいねボタンが押されたらaxios.put()が動いて、

apiディレクトリ内にあるlike.tsファイルへslugを渡してくれます。

🤔
パスにある/api/like?slug= の【 ? 】ってなんだろう….????

例えば、Tシャツの商品一覧ページを、Sサイズだけフィルタリングできるように仮定したとしましょう。
基本のTシャツの商品一覧ページは、「http://○△×□.jp/category/tshirt/ 」です。SサイズをフィルタリングしたTシャツの商品一覧ページは、「http://○△×□.jp/category/tshirt/?t=shirt_size=S」となります。

クエリ文字列(URLパラメーター)とは?Webサービス上の用途とその役割

パラメーターによって表示する内容を切り替えることができるということは…..

slugによって切り替わるっていうこと( ゚д゚)ハッ!

待ってましたと言わんばかりのサーバー

api/ like.ts でどんな処理をしているのか…..

細かい部分はアルパカ先生のコードを参照することにして、

ここではポイント部分だけを表示します。

  • slugから記事情報を取得する
const ApiBlogSlug = async function (req: NextApiRequest, res: NextApiResponse) {
	if (req.method !== 'PUT') {
	    res.statusCode = 400
	    res.end()
	    return
	  }
	// PUTだったら処理するよってこと
	
	  const { slug } = req.query
	//さっきのパラメータにあったslugを取り出す
	
	  try {
	    const post = await getPostBySlug(slug as string)
	// slugを頼りに記事情報を取得する
	

api/like.tsの前半:サーバー

🤔
getPostBySlugってなんだっけ….????
export async function getPostBySlug(slug: string) {
	// キャッシュの記述は省略

  const data = await client.databases.query({
    database_id: DATABASE_ID,
    filter: _buildFilter([
      {
        property: 'Slug',
        rich_text: {
          equals: slug,
        },
      },
    ]),
    sorts: [
      {
        property: 'Date',
        timestamp: 'created_time',
        direction: 'ascending',
      },
    ],
  })
// null処理の記述省略

  return _buildPost(data.results[0])
}
lib/notion/client.ts:サーバー

引数に入れられたslugと同じ値を持つ記事を引っ張り出してきてくれるのがgetPostBySlug。

slugが同じということは1件しかない( ゚д゚)ハッ!

いいねボタンが押された記事のslugを頼りに

api内のlike.tsから記事情報を取得できました。

あとは、NotionDBのLike列の値に狙いを定めるのみ(*´∀`*)

  • 取得した記事情報をincrementLikeコンポネントに入れる
const ApiBlogSlug = async function (req: NextApiRequest, res: NextApiResponse) {

	try{
	// slugを頼りに記事情報を取得する以降
	
	    await incrementLikes(post)
// 記事情報=post としてincrementLikesコンポネントに入れる

			res.statusCode = 200
	    res.end()
	  }
api/like.tsの後半:サーバー

これにて無事に【二段階右折】で渡りきりました 🎊

二段階右折②

(*´∀`*) 🛵

よし来た!!NotionAPI

渡りきったLike列の値をNotionDBへ更新するときも型定義やプロパティの追記が必要です。

  • 更新準備(型定義)
export interface Post {
    PageId: string
    Title: string
    Slug: string
    Date: string
    Tags: string[]
    Excerpt: string
    OGImage: string
    Rank: number
    Like: number  👉 追加
  }
lib/notion/interfaces.ts:サーバー

🤔
なんでnumberなの??

Like列を設置した時にプロパティの種類をNumberにしています。

image block

このNumberは公式Notion integration リファレンスで確認すると….

image block

https://developers.notion.com/reference/property-object#number-configuration

こういうわけでnumberを型にしています。

  • 更新準備(プロパティ追加)
function _buildPost(data) {
	const prop = data.properties
	const post: Post = {
		    PageId: data.id,
		    Title: prop.Page.title[0].plain_text,
		    Slug: prop.Slug.rich_text[0].plain_text,
		    Date: prop.Date.date.start,
		    Tags: prop.Tags.multi_select.map(opt => opt.name),
		    Excerpt:
		      prop.Excerpt.rich_text.length > 0
		        ? prop.Excerpt.rich_text[0].plain_text
		        : '',
		    OGImage:
		      prop.OGImage.files.length > 0 ? prop.OGImage.files[0].file.url : null,
		    Rank: prop.Rank.number,
		    Like: prop.Like.number,  👉 追加
		  }
		
	  return post
lib/notion/client.ts:サーバー

ネストネストになっている部分が気になる方は、

JSON構成について深ぼった記事があるのでそちらを覗いてみてください 😁

image block

https://herohoro.com/blog/blog-learn_notion-api-read#取得の動きが詳しく分かる_buildPost関数

今回Like列で使ったNumberプロパティはシンプルなJSONなので分かりやすかったです\(^o^)/

さて。

更新の準備も整ったので

更新するためのコンポネントを確認してみます。

  • 更新する

記事情報を取得したincrementLikesコンポネントは….

export async function incrementLikes(post: Post) {
  const result = await client.pages.update({
    page_id: post.PageId,
    properties: {
      Like: (post.Like || 0) + 1,
    },
  })

//null処理の記述省略

  return _buildPost(result)
}
lib/notion/client.ts:サーバー

詳しくはアルパカ先生の解説を。

https://alpacat.com/blog/update-notion-db-by-like-button#STEP 3. Likeプロパティを更新するためのメソッドを定義する

更新したい情報を、

記事情報からキューーーーっとLike列のみの情報に絞られています🪠

🤔
await client.pages.update ってなんだ??

HTTPメソッドを理解したいならNotionAPIを使ってコマンドラインで通信せよ」で

NotionAPIで遊んだ記事が過去にあるので興味のある方は覗いてみてください 😁

image block

https://herohoro.com/blog/http_method-notion-api#PATCH:データベース内のページを削除・修正

指定した行に対して列の値を修正できるメソッドです。

もう一度先程のコードを確認すると…..

export async function incrementLikes(post: Post) { 👉 slugによって得た記事情報で行を指定
  const result = await client.pages.update({
    page_id: post.PageId,   
    properties: {
      Like: (post.Like || 0) + 1,  👉 Like列のみを更新する
    },
  })

//null処理の記述省略

  return _buildPost(result)
}
lib/notion/client.ts:サーバー

image block

returnで _buildPostが返されます。

記事を取得するときも_buildPostを使いましたが、

更新でも同じ_buildPostを使ってNotionAPIへビビビッと送ります。

さっき更新準備でプロパティを追加したあの関数です。

function _buildPost(data) {
	const prop = data.properties
	const post: Post = {
		    PageId: data.id,
		    Title: prop.Page.title[0].plain_text,
		    Slug: prop.Slug.rich_text[0].plain_text,
		    Date: prop.Date.date.start,
		    Tags: prop.Tags.multi_select.map(opt => opt.name),
		    Excerpt:
		      prop.Excerpt.rich_text.length > 0
		        ? prop.Excerpt.rich_text[0].plain_text
		        : '',
		    OGImage:
		      prop.OGImage.files.length > 0 ? prop.OGImage.files[0].file.url : null,
		    Rank: prop.Rank.number,
		    Like: prop.Like.number,  👉 追加したよね(*´∀`*)
		  }
		
	  return post
lib/notion/client.ts:サーバー

これで更新したいLike列の情報をNotionAPIにお渡しすることができました(*^^*)

二段階右折③

無事つながりました\(^o^)/\(^o^)/

bookmarkブロックの場合も同じ

いいねボタンの仕組みと同じく、

bookmarkブロックも二段階右折。

クリックではなくNotion上で埋め込んだURLから情報を拝借してくるので…..

二段階右折①
const Bookmark = ({ block }) => {
  let sURL: string | null
  if (block.Bookmark) {
    sURL = block.Bookmark.Url
  } else if (block.LinkPreview) {
    sURL = block.LinkPreview.Url
  } else if (block.Embed) {
    sURL = block.Embed.Url
  }
// bookmarkブロックにURLを登録したら変数sURLに収納する


  const [metadata, setMetadata] = useState<Metadata | null>()

  useEffect(() => {
    try {
      const url = new URL(sURL)
      axios.get(`/api/url-metadata?url=${url.toString()}`).then((res) => {
        setMetadata(res.data as Metadata)
      })
			// apiディレクトリ内のurl-metadataファイルの処理をするよ👉 二段階右折②のこと
    } catch (e) {
      console.log(e)
    }
  }, [sURL])
// sURLの変更があったらuseEffectを実行する

//以降は二段階右折④で触れます
components/notion-blocks/bookmark.tsx 前半:フロント

二段階右折②
import { NextApiRequest, NextApiResponse } from 'next'
import got from 'got'
import createMetascraper from 'metascraper'
import metascraperDescription from 'metascraper-description'
import metascraperImage from 'metascraper-image'
import metascraperTitle from 'metascraper-title'

const metascraper = createMetascraper([metascraperDescription(), metascraperImage(), metascraperTitle()])

const ApiUrlMetadata = async function(req: NextApiRequest, res: NextApiResponse) {
  res.setHeader('Content-Type', 'application/json')

  if (req.method !== 'GET') {
    res.statusCode = 400
    res.end()
    return
  }
// 取得(GET)の通信

  const { url: urls } = req.query

// 400処理省略

  try {
    new URL(urls.toString())
  } 
// catch(e)の400処理省略

  try {
    const { body: html, url } = await got(urls.toString())
    const metadata = await metascraper({ html, url })

		// metadataでない処理省略

    res.json(metadata)
    res.statusCode = 200
    res.end()
  } 
// catch(e)の500処理省略
}

export default ApiUrlMetadata
pages/api/url-metadata.ts:サーバー

二段階右折③

二段階右折④

interface Metadata {
  title: string | null
  description: string | null
  image: string | null
}

const Bookmark = ({ block }) => {
	// ブロック処理省略 二段階右折①参照

  const [metadata, setMetadata] = useState<Metadata | null>()

// useEffectのapiディレクトリへの処理省略 _二段階右折①参照 

  let url: URL
  try {
    url = new URL(sURL)
  }
		// catch(e)処理省略 
// metadataでない または urlでもない処理省略

  const { title, description, image } = metadata

  return (
    <div className={styles.bookmark}>
      <a href={url.toString()} target="_blank" rel="noopener noreferrer">
        <div>
          <div>{title ? title : ''}</div>
          <div>{description ? description : ''}</div>
          <div>
            <div>
              <img
                src={`https://www.google.com/s2/favicons?domain=${url.hostname}`}
                alt="title"
                loading="lazy"
                decoding="async"
              />
            </div>
            <div>{url.origin}</div>
          </div>
        </div>
        <div>
          {image ? (
            <img src={image} alt="title" loading="lazy" decoding="async" />
          ) : null}
        </div>
      </a>
    </div>
  )
}

export default Bookmark
components/notion-blocks/bookmark.tsx:フロント

こんな感じの解釈で納得してきたへろほろです\(^o^)/

以上

(下書きの段階でだいぶ頭を使い、疲れてしまったので最後のまとめは割愛ですwww)

自信ない解説あります

ちょっと自信ない部分がありまして…..

二段階右折③

NotionAPIの方で、、、、

フロントを普段扱っているNotionの画面のことでいいのかな!?

という部分。

あれ?

Notionをシェアした時に表示される画面のことをフロントっていうのかな!?

とか。。。。。

フロント=更新結果が表示される場所

っていう解釈で記事を作っていってしまいました(*´ω`*)(*´ω`*)

他にも何か違う部分などありましたらTwitterで教えていただけると幸いです〜〜〜〜♫

バックエンド扱えてる!?

記事を投稿してすぐ

アルパカ先生からフロントとバックエンドの仕分けアドバイスをいただき、

驚きました。

私の勘違い具合いにwww

今までずっと「Notion Integrationに載ってるコードはフロントだー」って思っていたんです。

そしたらバックエンドだった。。。。

すごくないですか!?

知らず知らずのうちにバックエンドをいじっていたんですもん 😨

最近度胸試しに開発中のslack my app。

ブラウザに表示させるために取得する情報をなかなか整頓できず

JavaScriptの配列を真面目に学び直したんですが、、、、

それはバックエンドだったんですねーーーーーー

easy-notion-blog恐るべしだわ。。。。。

ちょっとは小さい声で

「私、Node.js扱えますよ〜」って

言えるかもしれません♥

Twitterでは更新のお知らせを随時行っています

興味ある方はLet'sフォロー★

▼ この記事に興味があったら同じタグから関連記事をのぞいてみてね

Buy Me A Coffee

新着記事を通知したい??


RSSリーダーにatomのリンクを登録すると通知が行くよ🐌

https://herohoro.com/atom

やってみてね(*´ω`*)(*´ω`*)

Twitter Timeline


フォロー大歓迎\(^o^)/