絞り込み

最新の投稿

プログラミング Polygon
プログラミング

Nextjs+microCMSで10秒ごとに更新かつpropsを自動補完できるページの作り方

microCMSで、記事タイトル・スラッグ・サムネイル・本文を取得し、NextjsでISR(Incremental Static Rendering)します。その際、エディタでPropsを全部自動補完できるようにします。 環境VSCodeNode.js v16.14.0microCMS API v1microCMS側の仕様変更や画面デザインの変更があるかもしれないのでご注意ください。 Next.jsの準備npx create-next-app@latest --ts Googleのサジェストでcreate-nuxt-appばっかり出てきて困りますね。Next使いはこれぐらい検索しないので出ないんでしょう(偏見)。 {   "name": "nextjs-microcms-practice",   "private": true,   "scripts": {     "dev": "next dev",     "build": "next build",     "start": "next start",     "lint": "next lint"   },   "dependencies": {     "next": "12.0.10",     "react": "17.0.2",     "react-dom": "17.0.2"   },   "devDependencies": {     "@types/node": "17.0.17",     "@types/react": "17.0.39",     "eslint": "8.9.0",     "eslint-config-next": "12.0.10",     "typescript": "4.5.5"   } } ▲昔と違ってeslint-config-next が入っています。今回はこれ以上lintはいじりません。 microCMSでAPIを用意 /api/v1/article というルートのエンドポイントを作りました。 ブログ的なサイトを作るので、リスト形式にします。 以下のようにフィールドを設定してください。 title / タイトル / テキストフィールド / 必須slug / スラッグ / テキストフィールド / 必須 / ユニーク / 後述の正規表現を適用thumbnail / サムネイル / 画像 / 任意body / 本文 / リッチエディタ / 必須 スラッグの詳細設定コンテンツIDをスラッグとして使うことも可能ですが、非エンジニアに触らせるのは危険すぎるので、別でユニークなスラッグフィールドを作りましょう。 ユニークの設定は「詳細設定」のところにあります。 「特定のパターンのみ入力を許可する」をONにしてください。パターンは^[a-z0-9]+(?:-[a-z0-9]+)*$でフラッグはなしです。 article-1 はOKですが、-artielce-1 みたいな先頭のハイフンはNGで、article--1 みたいな2回スラッグもNG、article-1- みたいな末尾のスラッグもNGになります。 お前こんなのどこで見つけたんだよって感じですが、 https://ihateregex.io/expr/url-slug/ i Hate Regex(正規表現なんて大っ嫌い)という神サイトがあってですね、ここに詳細な解説と一緒に例が載っています。 適当なデータを作成 適当な記事を書いて公開してください。プレースホルダに『ポラーノの広場』を使うとなんかデザイナー感出る。フォントに詳しそう。 microCMSと接続するnpm i microcms-js-sdk ライブラリを追加。 APIキーをコピーします。 echo MICROCMS_SERVICE_DOMAIN=あなたのmicroCMSサービス名 >> .env.local echo MICROCMS_API_KEY=$(pbpaste) >> .env.local 環境変数を書き出し。Linuxだとpbpasteじゃなくてxclip -selection clipboard -o を使えばいいそうです(ソース) あと、クライアントサイドで使わないのでconfigで環境変数を登録する必要はありません。 mkdir lib && touch $_/microcms.ts microcms.ts を作ります。 import { createClient } from 'microcms-js-sdk' export const client = createClient({   serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,   apiKey: process.env.MICROCMS_API_KEY!, }) これでデータ取得の用意が整いました。 で、ここでReact 18からSSRでもSuspenseが...とか語ってたらキリがないので、普通にSSRして、静的にページを書き出せる形で作ります。 (React世界にはいろいろなデータフェッチ方法があるよ!調べてみよう!) 型定義microcms-typescript っていうライブラリが新しいAPIに対応していなかったので、公式のSDKを使います。 API_NAME=article mkdir types && touch $_/article.ts API_NAME はあとでも使います。 import type { MicroCMSDate, MicroCMSImage, MicroCMSListResponse } from 'microcms-js-sdk' export type ArticleType = {   id: string   title: string   slug: string   thumbnail?: MicroCMSImage   body: string } & MicroCMSDate export type ArticleListType = MicroCMSListResponse<Article> MicroCMSListResponse でtotalCount などを付けます。それ以外の部分を自動で書いてくれるライブラリを見つけられませんでした。(aspidaでもここは手動) 記事コンポーネントを作るORGANISMS_DIR=./components/organisms mkdir -p $ORGANISMS_DIR && touch "_/$(echo "$API_NAME" | sed 's/.*/\u&/').tsx" Atomic Designガン無視で突然「Organisms(生体)」が登場しましたね。宇宙の 法則が 乱れる! あと、イキりすぎて全部コマンドで済ませようとしていますね、何をしているかというとcomponents/organisms/(API名の先頭だけ大文字).tsx ってファイルを作っているだけです。大文字にする方法は色々あるそうなんですが、bashのバージョン云々が面倒なので置換してしまいました。 import { ArticleType } from '../../types/article' export default function Article(props: ArticleType) {   return (     <article>       <h1>{props.title}</h1>       {props.thumbnail && <img src={props.thumbnail.url} alt={`${props.title}のサムネイル`} />}       <div dangerouslySetInnerHTML={{ __html: props.body }}></div>     </article>   ) } 記事コンポーネントです。Atomic Designガン無視で生体の中身を書いてしまいましたが、説明のためなので見逃してください。 記事一覧を作るtouch "$ORGANISMS_DIR/$(echo "$API_NAME" | sed 's/.*/\u&/')List.tsx" ArticleList.tsx を作ります。 import { ArticleListType } from '../../types/article' export default function ArticleList(props: ArticleListType) {   return (     <div>       <ul>         {props.contents?.map(           (p) =>             p && (               <li key={p?.id}>                 <a href={`/articles/${p.slug}`}>{p.title}</a>               </li>             )         )}       </ul>     </div>   ) } 最低限の記事リストです。 一覧ページを作成するそれではNext.jsの真骨頂、SSRにおける型の推測をお見せしましょう。 import type { InferGetStaticPropsType, NextPage } from 'next' import Head from 'next/head' import ArticleList from '../components/organisms/ArticleList' import { client } from '../lib/microcms' import { ArticleType } from '../types/article' export const getStaticProps = async () => { const articlesData = await client.getList<ArticleType>({ endpoint: 'article', queries: { limit: 10, orders: 'publishedAt', }, }) return { props: { articlesData: articlesData ?? null }, revalidate: 10, } } type Props = InferGetStaticPropsType<typeof getStaticProps> const Home: NextPage<Props> = (props) => { return ( <div> <Head> <title>記事は{props.articlesData.totalCount}あります</title> </Head> <main>{props.articlesData ? <ArticleList {...props.articlesData} /> : <div>記事を取得できませんでした。</div>}</main> </div> ) } export default Home InferGetStaticPropsType<typeof getStaticProps> で、getStaticProps の返り値の方をReactのchildren と混ぜます。 するとHome のpropsにprops.articlesData が追加され、サーバーサイドの型をわざわざコンポーネント用に再度書く必要がなくなります。 こんな感じにタイトルに反映されます。 また、revalidate: 10 でISRが有効になり、適切なデプロイ環境なら10秒ごとにデータのキャッシュが切れます(10秒ごとにサーバーが動くわけではない。ここ重要)。 記事単体ページを作るmkdir -p pages/${API_NAME}s && touch $_/\[slug\].tsx [slug].tsx はparams.slug を変数として取ります。 import type { GetStaticPaths, GetStaticPropsContext, InferGetStaticPropsType, NextPage } from 'next' import Head from 'next/head' import Article from '../../components/organisms/Article' import { client } from '../../lib/microcms' import { ArticleType } from '../../types/article' export const getStaticPaths: GetStaticPaths = async () => {   const allArticles = await client.getList<ArticleType>({     endpoint: 'article',     queries: {       limit: 10,       fields: ['slug'],     },   })   const paths = allArticles.contents.map((c) => {     return {       params: {         slug: c.slug,       },     }   })   return { paths, fallback: true } } export const getStaticProps = async ({ params }: GetStaticPropsContext) => {   const articlesMatched = await client.getList<ArticleType>({     endpoint: 'article',     queries: {       limit: 1,       filters: 'slug[equals]' + params?.slug,     },   })   if (articlesMatched.contents[0]) {     return {       props: { articleData: articlesMatched.contents[0] ?? null },       revalidate: 10,     }   }   return {     props: {       articleData: null,     },     notFound: true,     revalidate: 10,   } } type Props = InferGetStaticPropsType<typeof getStaticProps> const ArticlePage: NextPage<Props> = (props) => {   if (props.articleData) {     const { title } = props.articleData     return (       <div>         <Head>           <title>{title}</title>         </Head>         <main>           <Article {...props.articleData} />         </main>       </div>     )   }   return <div>記事を取得できませんでした。</div> } export default ArticlePage 長いので分けて説明。 getStaticPathsexport const getStaticPaths: GetStaticPaths = async () => {   const allArticles = await client.getList<ArticleType>({     endpoint: 'article',     queries: {       limit: 10,       fields: ['slug'],     },   })   const paths = allArticles.contents.map((c) => {     return {       params: {         slug: c.slug,       },     }   })   return { paths, fallback: true } } Next.jsのgetStaticPropsで、動的にURLを生成するための関数です。client.getList<ArticleType> で一覧からslugだけを取り出し、それらを[{params: slug: "スラッグ"}] といった配列に収めてpaths に返します。 fallback: true は、初回生成時にないページに訪れても、データを取得する設定です。正直ブログだと必要ないのですが、公開した記事を即見れるようにしたい場合に便利です。 getStaticPropsexport const getStaticProps = async ({ params }: GetStaticPropsContext) => {   const articlesMatched = await client.getList<ArticleType>({     endpoint: 'article',     queries: {       limit: 1,       filters: 'slug[equals]' + params?.slug,     },   })   if (articlesMatched.contents[0]) {     return {       props: { articleData: articlesMatched.contents[0] ?? null },       revalidate: 10,     }   }   return {     props: {       articleData: null,     },     notFound: true,     revalidate: 10,   } }microCMSには2022年2月13日現在ユニークなフィールドからコンテンツ1つを返すAPIが用意されていません。そのため、filters: 'slug[equals]' + params?.slug で絞り込んで長さ0~1の配列をもらい、記事の有無を見ています。Strapiとか使えば自前でAPI作れるから楽しいんですけど、microCMSでは2022年2月13日現在こうするしかありません。 あと、params.slugの型は本来string | string[] | undefined ですから、文字列じゃない可能性を考慮するべきですが、文字列とプラスして文字列にしてしまえばMicroCMSQueries.filters?: string | undefined とたまたま合致し、型チェックが通ります。 しかもURL構造上配列になりえない(クエリパラメータじゃないのでカンマ書いても配列にならない)ので、これで動作に支障はきたしません。 notFound: true を返せば404になります。もちろん404ページはカスタマイズ可能です。 ページコンポーネントtype Props = InferGetStaticPropsType<typeof getStaticProps> const ArticlePage: NextPage<Props> = (props) => {   if (props.articleData) {     const { title } = props.articleData     return (       <div>         <Head>           <title>{title}</title>         </Head>         <main>           <Article {...props.articleData} />         </main>       </div>     )   }   return <div>記事を取得できませんでした。</div> } export default ArticlePage titleだけはページで使いたいので取り出します。ページ以外でheadをいじるのは人間のやることではない。 画像をNext/Imageにする Do not use <img>. Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-elementnext/core-web-vitals っていうeslintプラグインが追加されていて、普通のimgタグだとwarnレベルの警告が出ます。このままでもビルドはできますが、練習がてらNext/Imageを使ってみましょう。 next.config.js  images: {     domains: ['images.microcms-assets.io'],   }, 以上の設定を加えてください。microCMS以外の画像を使う場合は、適宜追加してください。 mkdir ./components/atoms && touch $_/MicroCMSImage.tsx やっと原子を作ります。順番がおかしい。 MicroCMSImage.tsximport type { MicroCMSImage } from 'microcms-js-sdk' import Image from 'next/image' export default function MicroCMSImage(props: { image: MicroCMSImage; alt: string }) {   const { width, height, url } = props.image   return <Image width={width} height={height} src={url} alt={props.alt} /> } next/image用にコンポーネントを分けるのは、サイズの取得が面倒だからです。 Article.tsximport { ArticleType } from '../../types/article' import MicroCMSImage from '../atoms/MicroCMSImage' export default function Article(props: ArticleType) {   return (     <article>       <h1>{props.title}</h1>       {props.thumbnail && <MicroCMSImage image={props.thumbnail} alt={`${props.title}のサムネイル`} />}       <div dangerouslySetInnerHTML={{ __html: props.body }}></div>     </article>   ) } これで画像部分を最適化できました。 HTMLがすごいことになってたら成功です。 参考microCMS: https://dev.classmethod.jp/articles/remix-with-microcms/InferGetStaticPropsType: https://zenn.dev/catnose99/articles/7201a6c56d3c88

> 内容を見る

プログラミング Polygon
プログラミング

VSCodeのVueでpropsの型チェックをする方法 (TypeScript)

注意書き 以下の内容はVSCode 1.16.2Vetur v0.35.0Nuxt v3.0.0-27252999.d2cc9e4 (Vue: 3.2.20)Next 11.1.2 (React: 17.0.2)時点の内容です。あと、当然ですが全てTypeScriptです。 Nuxt3はまだベータ版ですが、起動が早い + TypeScriptが使いやすいので皆さんも試してみてください。 Vueでpropsの型チェックをさせる方法ステップ1. Veturを入れるhttps://github.com/vuejs/vetur これがないと話にならないです。 ステップ2. 設定を変えるなぜかデフォルトで型チェックできないので、settings.json に以下の設定を追記します。 { (前略) "vetur.validation.templateProps": true, "vetur.experimental.templateInterpolationService": true, (後略) } ステップ3. コンポーネントを作成する<template> <div> <h2>{{ name }}様</h2> <div v-if="age"> <b>{{ age }}歳なので</b> <span>わいの{{ age && age / 20 }}倍!</span> </div> </div> </template> <script lang="ts"> export default { name: "TestComponent", props: { name: { type: String, required: true, }, age: { type: Number, required: false, }, }, } </script> name は文字列(必須)で、年齢は数値(任意)です。 「名前と年齢を受け取って、年齢があれば俺の年齢と比較する」という意味不明なコンポーネントです。 ステップ4. ページで使ってみる<template> <div> <TestComponent name="aaa" :age="100" /> </div> </template> <script lang="ts"> import TestComponent from "../components/TestComponent.vue" export default { components: { TestComponent }, } </script> こんな感じでページで使います。 できましたね。 ステップ5. エラーを起こしてみる<TestComponent name="aaa" age="100" /> 試しにこう書いてみてください。 Type 'string' is not assignable to type 'number'. Veturから怒られるはずです。なぜならage="100" が文字列の「100」を渡しているからです。 が、templateInterpolationService がまだ「experimantal」なので、エラーが出る場所がおかしいです。 ちょっと待って!Reactで書けばいいやんあのですね、なんで単なる自動補完に、拡張機能と設定がわざわざ必要なんですか? Reactで同じことをやってみてください。何も設定がいらないです。 ステップ1. コンポーネントを書くinterface Props { name: string; age?: number; } const TestComponent: React.VFC<Props> = (props) => ( <div> <h2>{props.name}様</h2> {props.age && ( <div> <b>{props.age}歳なので</b> <span>わいの{props.age / 20}倍!</span> </div> )} </div> ); export default TestComponent; めっちゃ短くなります。 型の定義も、ただTypeScriptのinterfaceを書けばいいのです。Vueのprops よりスッキリしていて見やすいでしょう? Next.jsのページで使うimport TestComponent from '@/components/TestComponent'; import { NextPage } from 'next'; const TestPage: NextPage = () => ( <div> <TestComponent name="aaa" age={100} /> </div> ); export default TestPage; もしage に数値以外を渡すと、その場でエラーが出ます。繰り返しますが、拡張機能や設定は必要ありません。 Vueはpropsだけで記述量が多すぎるそもそもですね、たかが「名前と年齢を出す」だけで記述量が多すぎるんですよ。 export default { name: "TestComponent", props: { name: { type: String, required: true, }, age: { type: Number, required: false, }, }, } これ、「nameとageを受け取る」ってだけですよ。長すぎるでしょ。 interface Props { name: string; age?: number; } const TestComponent: React.VFC<Props> = (props) => ( <div> <h2>{props.name}様</h2> {props.age && ( <div> <b>{props.age}歳なので</b> <span>わいの{props.age / 20}倍!</span> </div> )} </div> ); export default TestComponent; さっきも書きましたけど、Reactはテンプレート自体を含めてこの長さですからね。絶対こっちの方が時間の節約になります。 以上、俺がReactを使う理由の記事でした。 --- あれ?なんの記事だったっけ? 思い出した!自動補完や! 以上、VSCodeのVueでpropsの型チェックをさせる方法でした。

> 内容を見る