いいねボタンの解説。
そして、ついこの間
本家でサポートされたbookmarkのScratch開発の解説。
ちょいと喉越しが悪いので
私なりに整理してみることにしました。
整理し終わって分かったのは、
「フロントから考えないと混乱してしまう…」
という自分の弱点や、
「どの処理がサーバーでどこまでがフロントなのかの理解が甘い」
ということが分かりました(´;ω;`)
弱点や甘さを少しでも改善するべく、
アルパカ先生の解説記事を読み返しながら、
ノートにメモしたり、
ぼんやり考えてみたり、、、、
少しずつ言葉にして
文章にしてみたのがこの記事になります 📝
フロントから同じドメインのURLにリクエストする様子を図にすると….
ブログの記事一覧でタイトルをクリックすると記事本文が表示されたり、
記事一覧の末尾にある次ページへのボタンを押すと続きの記事一覧が表示されたり、
タグをクリックするとクリックしたタグを含む記事一覧が表示されたり…..
ブログに遊びに来た人の行動に合わせて表示させる情報を切り替えているのが【いつもの動き】でした。
そして、
easy-notion-blogがツヨツヨになるにつれて
「異なるドメインでやりとりできるようになった」というのが今回の本題です。
Same-Origin Policy違反によってガードされてしまっている異なるドメインへの処理。
この処理をするためにちょっとワンバウンドさせたのがアルパカ先生の解説です。
https://alpacat.com/blog/update-notion-db-by-like-button
私はこれを勝手に【二段階右折】と名付けることにしました 🤟(´・ω・`) ✨
二段階右折とは
軽車両や原付が片側3車線以上の道路を右折する際、2回信号に従うことで右折をすること。法定速度が30kmの原付が、右折のために片側3車線以上の道路の一番右車線を走行すると、周囲の車両にとって交通の妨げになってしまいます。最悪の場合衝突事故に繋がる可能性もあるでしょう。
二段階右折が求められている道路で右折をする際は、左車線から交差点に直進して入り、交差点を渡りきった場所で向きを変えてまた直進しなければなりません。二段階右折をすることで、速度の出せない軽車両や原付が左車線を走行し続けても右折が可能です。
【二段階右折】に従っていいねボタンの仕組みをもう一度図にしてみます。
フロントから組み立てる工程を追いながら整理すると….
【いつもの動き】を実装する流れと同じです。
ただ、今回は『取得』ではなく『更新』なので、
流れが逆になります。
取得の時は….
↓
↓
といった流れで実装していました。
更新の場合は…..
component/like-button.tsx
:フロント
↓ axios.put
api/ like.ts
で更新(PUT)通信:サーバー
↓
incrementLikeコンポネントが教えてくれたLikeの値を更新できるようにする:サーバー
↓
NotionAPIが動いてNotionDBのLike列が更新される
フロント・サーバー・Notionの方のサーバーの順に見ていきます(´・ω・`)
<LikeButton slug={post.Slug} />
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>
)
}
いいねボタンが押されたら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
でどんな処理をしているのか…..
細かい部分はアルパカ先生のコードを参照することにして、
ここではポイント部分だけを表示します。
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を頼りに記事情報を取得する
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])
}
引数に入れられたslugと同じ値を持つ記事を引っ張り出してきてくれるのがgetPostBySlug。
slugが同じということは1件しかない( ゚д゚)ハッ!
いいねボタンが押された記事のslugを頼りに
api内のlike.tsから記事情報を取得できました。
あとは、NotionDBのLike列の値に狙いを定めるのみ(*´∀`*)
const ApiBlogSlug = async function (req: NextApiRequest, res: NextApiResponse) {
try{
// slugを頼りに記事情報を取得する以降
await incrementLikes(post)
// 記事情報=post としてincrementLikesコンポネントに入れる
res.statusCode = 200
res.end()
}
これにて無事に【二段階右折】で渡りきりました 🎊
(*´∀`*) 🛵
渡りきったLike列の値をNotionDBへ更新するときも型定義やプロパティの追記が必要です。
export interface Post {
PageId: string
Title: string
Slug: string
Date: string
Tags: string[]
Excerpt: string
OGImage: string
Rank: number
Like: number 👉 追加
}
Like列を設置した時にプロパティの種類をNumberにしています。
このNumberは公式Notion integration リファレンスで確認すると….
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
ネストネストになっている部分が気になる方は、
JSON構成について深ぼった記事があるのでそちらを覗いてみてください 😁
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)
}
詳しくはアルパカ先生の解説を。
https://alpacat.com/blog/update-notion-db-by-like-button#STEP 3. Likeプロパティを更新するためのメソッドを定義する
更新したい情報を、
記事情報からキューーーーっとLike列のみの情報に絞られています🪠
await client.pages.update
ってなんだ??「HTTPメソッドを理解したいならNotionAPIを使ってコマンドラインで通信せよ」で
NotionAPIで遊んだ記事が過去にあるので興味のある方は覗いてみてください 😁
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)
}
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
これで更新したいLike列の情報をNotionAPIにお渡しすることができました(*^^*)
無事つながりました\(^o^)/\(^o^)/
いいねボタンの仕組みと同じく、
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を実行する
//以降は二段階右折④で触れます
↓
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
↓
↓
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
こんな感じの解釈で納得してきたへろほろです\(^o^)/
以上
(下書きの段階でだいぶ頭を使い、疲れてしまったので最後のまとめは割愛ですwww)
ちょっと自信ない部分がありまして…..
NotionAPIの方で、、、、
フロントを普段扱っているNotionの画面のことでいいのかな!?
という部分。
あれ?
Notionをシェアした時に表示される画面のことをフロントっていうのかな!?
とか。。。。。
フロント=更新結果が表示される場所
っていう解釈で記事を作っていってしまいました(*´ω`*)(*´ω`*)
他にも何か違う部分などありましたらTwitterで教えていただけると幸いです〜〜〜〜♫
記事を投稿してすぐ
アルパカ先生からフロントとバックエンドの仕分けアドバイスをいただき、
驚きました。
私の勘違い具合いにwww
今までずっと「Notion Integrationに載ってるコードはフロントだー」って思っていたんです。
そしたらバックエンドだった。。。。
すごくないですか!?
知らず知らずのうちにバックエンドをいじっていたんですもん 😨
最近度胸試しに開発中のslack my app。
ブラウザに表示させるために取得する情報をなかなか整頓できず
JavaScriptの配列を真面目に学び直したんですが、、、、
それはバックエンドだったんですねーーーーーー
easy-notion-blog恐るべしだわ。。。。。
ちょっとは小さい声で
「私、Node.js扱えますよ〜」って
言えるかもしれません♥
▼ この記事に興味があったら同じタグから関連記事をのぞいてみてね
RSSリーダーにatomのリンクを登録すると通知が行くよ🐌
https://herohoro.com/atom
やってみてね(*´ω`*)(*´ω`*)
フォロー大歓迎\(^o^)/
フォロー大歓迎\(^o^)/