Slack APIの確認ダイアログ

Slackメッセージ内のボタンクリックをトリガーとしてダイアログを表示させる場合。

ダイアログ内でユーザーのインプットが必要なときは dialog.open が必要。
Interacting with users through dialogs | Slack

シンプルに OK , Cancel などの2択のボタンをダイアログで置く場合は buttonconfirm フィールドで実装できる。
Reference: Composition objects | Slack

// main/function.gs
const blockKit = [
  {
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": "ボタン横のテキスト"
    },
    "accessory": {
      "type": "button",
      "text": {
        "type": "plain_text",
        "text": "ボタンのラベルテキスト",
        "emoji": true
      },
      "value": "BUTTON CLICKED",
      "action_id": "button-action-id",
      "confirm": {
        "title": {
          "type": "plain_text",
          "text": "確認ダイアログのタイトルテキスト"
        },
        "text": {
          "type": "mrkdwn",
          "text": "確認ダイアログの本文テキスト"
        },
        "confirm": {
          "type": "plain_text",
          "text": "OK"
        },
        "deny": {
          "type": "plain_text",
          "text": "Cancel"
        }
      }
    }
  }
]
postSlack(blockKit)
// post/slack.gs
const postSlack = (blocks) => {
  const blockKit = blocks
  const endpoint = 'https://slack.com/api/chat.postMessage'
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + SLACK_AUTH_TOKEN
  }
  const payload = {
    channel: SLACK_CHANNEL_ID,
    blocks: blockKit,
    unfurl_links: false,
    unfurl_media: false,
  }
  const params = {
    method : 'post',
    headers: headers,
    payload : JSON.stringify(payload),
  }
  UrlFetchApp.fetch(endpoint, params)
}

GASで受け取る場合、 doPost() の値にOKを押したときのみ BUTTON CLICKED が入ってくる。

// event/doPost.gs
const doPost = (event) => {
  const parameter = event.parameter
  const data = parameter.payload
  const json = JSON.parse(decodeURIComponent(data))

  const value = json.actions[0].value
  console.log(value) // BUTTON CLICKED
}

Newt + Next.js + Vercel でポートフォリオサイトを作る

以前の WordPress + Nuxt.js(Netlify) から Newt + Next.js(Vercel)の構成に変更しました。

GitHub: https://github.com/yuheijotaki/works-next
Vercel: https://works.yuheijotaki.com/

絞り込み機能のところで、 useState() を使って状態管理してみましたが、TypeScriptも相まってなかなか苦戦しました。

SSGのためサイトの表示速度が速いなど表に見える面は満足していますが、状態管理やPropsあたりの使いこなしはまだまだです。
また別のサイト作ったりで学んでいきたいと思います。

@next/font でローカルフォントを扱う

ページ遷移時にローカルフォントのチラつきが気になったので対策。
結果改善されたようなのでメモ。

コード

@next/font モジュールをインストール

$ npm install @next/font

ローカルフォントを定義。
フォントのデータは pages/fonts/** に配置する。

display: 'swap'preload: true あたりの設定は 公式のドキュメント を参照した。

// _app.tsx

...
import localFont from '@next/font/local'
const PostgroteskBookFont = localFont({
  src: [
    {
      path: './fonts/PostGrotesk-Book.woff',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/PostGrotesk-Book.eot',
      weight: '400',
      style: 'normal',
    }
  ],
  display: 'swap',
  preload: true
})

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <style jsx global>{`
        :root {
          --postgrotesk-book: ${PostgroteskBookFont.style.fontFamily};
        }
      `}</style>
      <Component {...pageProps} />
    </>
  )
}

定義したCSS変数をSCSSで指定する。

  body {
    font-family: var(--postgrotesk-book), ...;
  }

他参考

GASからAWS API Gateway(Lambda)へリクエスト送信、ファイルをS3へアップ

GAS → API Gatewayのリクエストのみ(レスポンスなし)でJSONファイルをS3へアップロードする。

主な手順

コード

ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ExampleStmt",
            "Action": [
                "s3:PutObject"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::MY-BACKET-NAME/*"
            ]
        }
    ]
}

Lambda

少しハマった点として、object putする際に ContentType='application/json' など明示的に指定をしないと、Lambda側で自動でContentTypeが設定されてしまう。
参考: python - AWS Content Type Settings in S3 Using Boto3 - Stack Overflow

lambda_function.py

import json
import boto3

s3 = boto3.resource('s3')
# バケット名を指定
BUCKET_NAME = "MY-BACKET-NAME"

def lambda_handler(event, context):

    # request_idを取得
    request_id = context.aws_request_id

    # バケット名、オブジェクト名を指定
    bucket = s3.Bucket(BUCKET_NAME)
    object_key_name = "PATH/MY-JSON-NAME.json"

    # オブジェクトを生成
    obj = bucket.Object(object_key_name)

    # 対象のバケットにjsonデータをアップロード
    json_data = event
    r = obj.put(Body = json.dumps(json_data), ContentType='application/json')

    return {
        'request_id': request_id,
        'statusCode': 200,
    }

GAS

function uploadS3() {
  const endpoint = 'MY-API-END-POINT'
  const json = JSON.stringify({"key1": "value1"})

  const params = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload' : json
  };
  const req = UrlFetchApp.fetch(endpoint, params)
  Logger.log(req.getContentText())
}

その他参考

GASからSlack AppのBlock Kitでメッセージを送る & ボタンの値を受け取る

Block Kit を使ってGAS → Slackにメッセージを送り、Slackで押下したボタンの値を受け取る。 ※ WebhookではなくSlack APIを使用する場合。

attachments でもリッチなメッセージは送れるが、SlackはBlock Kit Builderなどを利用した blocks 押しらしい。

メッセージを送る

function sendToSlack() {
  const endpoint = 'https://slack.com/api/chat.postMessage'
  const token = 'MY-TOKEN'
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  }

  const blockKit = [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "Block Kitのデモです。"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "ボタンの説明テキストが入ります。"
      },
      "accessory": {
        "type": "button",
        "text": {
          "type": "plain_text",
          "text": "ボタンのラベルテキスト",
          "emoji": true
        },
        "value": "MY-VALUE-01",
        "action_id": "button-action"
      }
    }
  ]

  const payload = {
    channel: 'MY-CHANNEL-NAME',
    blocks: blockKit,
  }

  const params = {
    method : 'post',
    headers: headers,
    payload : JSON.stringify(payload),
  }

  UrlFetchApp.fetch(endpoint, params)
}

payload が text の場合は、 token を payload に格納していけたが、 blocks の場合にはheaderに渡してあげないと成功しなかった。

押下したボタンの値を受け取る

function doPost(e) {
  const parameter = e.parameter
  const data = parameter.payload
  const json = JSON.parse(decodeURIComponent(data))
  const value = json.actions[0].value
  console.log(value) // 'MY-VALUE-01'
}

doPost() のGASは変更する度にデプロイし、デプロイされたGASのエンドポイントを Slack Appページ > Features > Interactivity & Shortcuts の Request URL に都度設定する必要がある。

Next.js マークアップ観点の勘所

フロントをNext.js(TypeScript)、ヘッドレスCMSをNewtでサイトを作ってみて、勘所とまでいくか分からないですがつまづいたところ中心のメモです。

型の作成

型の作成はNewtのJSONプレビューから quicktype 使ったら早かった。
ここで生成したコードを types/**.ts にコピペして整える。

子要素を渡さないときの型

デフォルトのESLint設定でエラーになるが、 ReactNode を使うようにした。

// components/children.tsx
import { ReactNode } from 'react'

export default function Children({ children }: {children: ReactNode}) {
...

参考: TypeScriptでReactを開発するとき、childrenの型をどうするか考える 👦 - みかづきブログ・カスタム

コンポーネント

ファイル階層

正解というか一般的なものがつかめてないが、

.
├── components/
│   └── **.tsx
├── pages/
│   └── **.tsx
└── styles/
     ├── foundation/
     │   ├── variables.scss
     │   ├── mixin.scss
     │   └── global.scss
     ├── components/
     │   └── **.module.scss
     └── page/
         └── **.module.scss

のような感じで、コンポーネントが多いようならさらに階層切ってあげるとかでとりあえずの管理はできそう。

CSS

リセットCSS(ress)

今の時代は ress があついらしい。

// _app.tsx
import 'ress'

SCSSの有効化

sass をインストール後、

// next.config.js
const path = require('path')

const nextConfig = {
  ...
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
  },
  ...
}

これで **.modules.scss が使えるようになる。
ファイル名に modules. が付いたSCSSがモジュール用と定義される。

参考: Next.jsにCSS Modulesを導入する

共通SCSS

**.modules.scss 以外のグローバルなSCSSを各PagesやComponentsでインポートしようとすると怒られる。

先の foundation/variables.scss のようにして、 global.scss の中でインポートする。
global.scss_app.tsx でインポートするとすべての画面で適用される。(html, body への指定など)

// styles/foundation/global.scss
@import './variables.scss';
@import './mixin.scss';

body {
  background: $hoge;
  @include fuga {
  }
}
...
// _app.tsx
import '@/styles/foundation/global.scss'

CSS Modules

importしたSCSSに記述したクラスを参照する。
慣れないとなかなか辛い。

import styles from '@/styles/components/Button.module.scss'
...
return (
  // クラス単数(ハイフンなし)の場合
  <button className={styles.className}>ラベルテキスト</button>

  // クラス単数(ハイフンあり)の場合
  <button className={styles['class-name']}>ラベルテキスト</button>

  // クラス複数の場合
  <button className={`${styles['className01']} ${styles['class-name-02']}`}>ラベルテキスト</button>
)

参考: CSS ModulesでCSSを書く時に実務で必要になる書き方まとめ - Satoshi Murata

CSS変数

よく使うものでもないが、取ってきたデータに色情報があってCSS変数とする場合、 styled-components をインストールして使う。

// Post.tsx
import { createGlobalStyle } from 'styled-components'

export default function Post({ post, posts }: { post: Post, posts: Post[] }) {
  const GlobalStyles = createGlobalStyle`
    html {
      --color-text: ${post.colorText};
    }
  `
  return (
    <>
      <GlobalStyles />
      ...
    </>
  )
}
// Post.module.scss
$color_text: var(--color-text);

.title {
  color: $color_text;
}

参考: How to use CSS variables with React

画像

next/image

<Image> コンポーネントで読み込むといいことがたくさんあるらしい。

import Image from 'next/image'
...
<Image
  src={post.image.src}
  width={post.image.width}
  height={post.image.height}
  alt={post.title}
  quality={70}
  priority={true}
  unoptimized
/>

最後の unoptimized をしないと逆に表示がもたついた。
じゃあ意味あるの、って感じだが外部でホスティングしている画像だからかもしれない。

参考: next/image | Next.js
参考: 【Next.js和訳】API Reference/ next/image

外部でホスティングしている画像を読み込みする際は設定にドメインの追加が必要。

// next.config.js
const nextConfig = {
  ...
  images: { // next/image で外部画像を参照
    domains: ['storage.googleapis.com'],
  },
  ...
}

JSX

ifの分岐

export default function Posts({ posts }: { posts: Post[] }) {
  let detectStatus = (post: Post) => {
    if (post.isPublish) {
      return (
        <p>公開中の投稿</p>
        ...
      )
    } else {
      return (
        <p>非公開の投稿</p>
        ...
      )
    }
  }
  return (
    <>
      <ul>
        {posts.map((post) => {
          return (
            <li key={post._id}>
              {detectStatus(post)}
            </li>
          )
        })}
      </ul>
    </>
  )
}

参考: React JSX の中で if で分岐させたい - かもメモ

テキストの改行

いろいろやり方あるようだが react-nl2br を使うと簡単。

import nl2br from 'react-nl2br'
...
<p>{nl2br(post.description)}</p>

その他

定数

これもいろいろやり方あるようだが next.config.js に作ってみた。

// next.config.js
const nextConfig = {
  ...
  env: { // 定数
    customKey: 'my-value',
  },
  ...
}
<p>{process.env.customKey}</p>

参考: next.config.js: Environment Variables | Next.js

urlの末尾にスラッシュつけたい

デフォルトで [id].tsx とか動的にするとスラッシュがつかない。
これも next.config.js で設定。

// next.config.js
const nextConfig = {
  ...
  trailingSlash: true, // URL末尾にスラッシュを追加
  ...
}

参考: next.config.js: 末尾のスラッシュ | Next.js

meta設定

定数作ってちまちま pages/**.tsx のHeadに書いていたが、 この方法 の方ががよさそう。

画面遷移時のローディング

NProgress はちゃちゃっと入れれる。
TypeScriptの場合は types/nprogress もインストールする。

// _app.tx
import Router from 'next/router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

Router.events.on('routeChangeStart', () => NProgress.start())
Router.events.on('routeChangeComplete', () => NProgress.done())
Router.events.on('routeChangeError', () => NProgress.done())

Vercelのドメイン設定

CNAMEは cname.vercel-dns.com. を登録する。

参考: Vercelでデプロイしたサイトに独自ドメインのサブドメインを設定する | okaryo.log

ひとこと

最低限マークアップするくらいだと抵抗は少なくなってきました。
値の受け渡しは雛形通りだったりストア周り触れてなかったりするので触れていきたいです。

WordPress(ACF)とNewtの管理画面の違い

Newtをもう少し触る機会があったので、管理画面のフィールド作成や編集画面をWordPressと比べてみました。

フィールド作成

WordPress(ACF)

Newt

編集画面

WordPress(ACF)

Newt

所感

  • WordPressは元々ブログCMSのため、日付やカテゴリーが標準で用意されている。
    • Newtはブログ形式にとらわれないため、日付は日付フィールドを、カテゴリーは別モデルを作成し参照するなどで対応する。
  • WordPress(ACF)は入力制御(バリデーション)が豊富。
    • Newtは例えばテキストフィールドの場合、
      • 必須フィールド
      • 一意の文字列(ユニーク)
      • 最大/最小文字数
        • がオプションとして用意されている。ACFのようにURLなどがあるとよいと思った。
  • WordPress(ACF)はカラーピッカーなどフィールド作成タイプの幅が広い。
    • Newtもカラーフィールドはあるが登録したパレットから選択の必要あり。その他も含め豊富なほうだと感じるがACFと比べると幅はまだ狭い。(これから使いやすくはなるはず)
  • 繰り返しフィールドはNewtでも「カスタムフィールドタイプ」を「マルチタイプ」と組み合わせることで作れるよう。
  • 画像アップはメディアアップローダーがWordPress、Newtともにある。
    • Newtは一度に10ファイル制限(無料プランのため?)

Newtに限らないですが、ヘッドレスCMSはゼロの状態からフィールドを作るので、自由度が高いのと、逆に言うとすべて作る必要があるのだと改めて認識しました。
ただそういう煩雑さを避けたいユーザー向けに スターター があるのかと思います。
実際に今回作ってみる際もカテゴリーの作り方が分からなかったので、スターターのBlogのモデルを参考にしました。

Newtの「カスタムフィールドタイプ」あたり、今回テストでしか作らなかったのでもう少し使い慣れていきたいと思います。