Open74

Next+MongoDB+Prisma+GraphQLで自作ブログを作る

0Yu0Yu

フロントエンド構成

React.js

  • フロントエンド。UIを作る
    Next.js
  • SSRする
    Jotai
  • 状態管理をする

バックエンド構成

Node.js + Express.js

  • バックエンド。APIエンドポイントを作成する
    MongoDB or Notion API(Notion DataBase)
  • ノンリレーショナルデータベース or Notionをデータストアに使う

デプロイ構成

Vercel

認証・認可(Notion APIを使う場合)

Firebase Authentication

0Yu0Yu
0Yu0Yu

共通:MongoDB Atlas のセットアップ

https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/

# Install the Atlas CLI
% brew install mongodb-atlas
% atlas

# Update the Atlas CLI
% brew update
% brew upgrade mongodb-atlas
% atlas --version
# 1. Run the authentication command
% atlas auth login

# 2. Sign into Atlas.
# If you aren't signed in already, sign into your Atlas account in the browser.

# 3. Enter the authorization code.
# Paste your activation code into the browser and click Confirm Authorization.

# 4. Return to the Atlas CLI.
# Return to the terminal. If you connect successfully, you see a message:

# Successfully logged in as {Your Email Address}.
0Yu0Yu

https://www.mongodb.com/docs/database-tools/installation/installation-macos/#installing-the-database-tools-on-macos

# 1. https://www.mongodb.com/docs/database-tools/installation/installation-macos/#install-homebrew
# 2. https://www.mongodb.com/docs/database-tools/installation/installation-macos/#tap-the-mongodb-formula
% brew tap mongodb/brew
# 3. https://www.mongodb.com/docs/database-tools/installation/installation-macos/#install-the-mongodb-database-tools
% brew install mongodb-database-tools
# 4. https://www.mongodb.com/docs/database-tools/installation/installation-macos/#run-the-installed-tools
0Yu0Yu

DBクラスターへの接続

Connect to Cluster0 (シェルから接続する)

# Run your connection string in your command line
# Use this connection string in your application:
% mongosh "mongodb+srv://cluster0.tbegt85.mongodb.net/myFirstDatabase" --apiVersion 1 --username denham

Node.jsから接続する

mongodb+srv://denham:<password>@cluster0.tbegt85.mongodb.net/?retryWrites=true&w=majority
example
const { MongoClient, ServerApiVersion } = require('mongodb');
const uri = "mongodb+srv://denham:<password>@cluster0.tbegt85.mongodb.net/?retryWrites=true&w=majority";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true, serverApi: ServerApiVersion.v1 });
client.connect(err => {
  const collection = client.db("test").collection("devices");
  // perform actions on the collection object
  client.close();
});
0Yu0Yu

ハマりポイント

  • Atlasで構築したDBのURLは コンソールのConnect -> Connect Using VS Code に記載されているものを使う
mongodb+srv://<ユーザー名>:<password>@cluster0.tbegt85.mongodb.net/<データベース名>
  • ドキュメントの初期データはInsert Documentからbson形式で挿入する
0Yu0Yu

Prisma/TSでGraphQLを扱うためのライブラリを追加する

GraphQL server with TypeScript with the following stack:

GraphQL Yoga: GraphQL server
Pothos: Code-first GraphQL schema definition library
Prisma Client: Databases access (ORM)
Prisma Migrate: Database migrations

https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql#graphql-server-example

https://www.npmjs.com/package/graphql
https://www.npmjs.com/package/graphql-scalars
https://www.npmjs.com/package/graphql-yoga
https://www.npmjs.com/package/@pothos/plugin-prisma

pothosについて(後述)
https://pothos-graphql.dev/docs/plugins/prisma

{
  ...
  "devDependencies": {
+    "@types/graphql": "14.5.0",
    "@types/node": "^18.15.11",
    "prisma": "^4.12.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.2"
  },
  "dependencies": {
+    "@apollo/client": "3.7.10",
+    "@pothos/plugin-prisma": "3.47.2",
+    "@pothos/core": "3.29.0",
    "@prisma/client": "^4.12.0",
+    "graphql": "16.6.0",
+    "graphql-scalars": "1.21.3",
+    "graphql-yoga": "3.8.0"
  },
+  "prisma": {
+    "seed": "ts-node prisma/seed.ts"
+  }
}

package.json
{
  "name": "my-blog",
  "version": "1.0.0",
  "description": "",
  "keywords": [],
  "license": "MIT",
  "author": "",
  "devDependencies": {
    "@types/graphql": "14.5.0",
    "@types/node": "^18.15.11",
    "prisma": "^4.12.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.2"
  },
  "dependencies": {
    "@apollo/client": "3.7.10",
    "@pothos/plugin-prisma": "3.47.2",
    "@pothos/core": "3.29.0",
    "@prisma/client": "^4.12.0",
    "graphql": "16.6.0",
    "graphql-scalars": "1.21.3",
    "graphql-yoga": "3.8.0"
  },
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }
}
0Yu0Yu

prisma

セットアップ(初回のみ)

npx prisma init
  • 設定ファイル類(prisma/schema.prisma, .envなど)が自動的に生成される

スキーマファイルのフォーマット

npx prisma format

DBスキーマの同期

npx prisma db push

https://www.prisma.io/docs/reference/api-reference/command-reference#db-push

  • マイグレーションファイルを生成せずスキーマを同期する

migrate dev コマンドはMongoDBではサポートされていないため、db push でマイグレーション(DBを更新)する

This command is not supported on MongoDB. Use db push instead.

https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-dev

モデルの更新

npx prisma generate

https://www.prisma.io/docs/reference/api-reference/command-reference#generate

  • スキーマ(prisma/schema.prisma)に定義したデータベースに変更が加わるたび、Prisma Clientを手動で再生成して、ディレクトリ内に生成されたコードを更新する
✔ Generated Prisma Client (4.12.0 | library) to ./node_modules/@prisma/client in 98ms

✔ Generated Pothos integration to ./node_modules/@pothos/plugin-prisma/generated.ts in 15ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

FensWfo
https://res.cloudinary.com/prismaio/image/upload/v1628761155/docs/FensWfo.png

seedデータの投入

npx prisma db seed

https://www.prisma.io/docs/reference/api-reference/command-reference#db-seed

0Yu0Yu

Prisma Studio

ブラウザからデータベースのデータを確認・操作できるツール

npx prisma studio

http://localhost:5555/で確認可能

0Yu0Yu

Pothosでスキーマを定義する

prismaObject でオブジェクトを、prismaField でフィールドを定義する

pages/api/graphql.ts
import { createYoga } from "graphql-yoga";
import SchemaBuilder from "@pothos/core";
import PrismaPlugin from "@pothos/plugin-prisma";
import { DateTimeResolver } from "graphql-scalars";

import type PrismaTypes from "@pothos/plugin-prisma/generated";
import type { NextApiRequest, NextApiResponse } from "next";

import prisma from "../../lib/prisma";

const builder = new SchemaBuilder<{
  PrismaTypes: PrismaTypes;
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: prisma,
  },
});

builder.queryType({});

builder.mutationType({});

builder.prismaObject("User", {
  fields: (t) => ({
    id: t.exposeID("id"),
    email: t.exposeString("email"),
    name: t.exposeString("name", { nullable: true }),
    posts: t.relation("posts"),
  }),
});

builder.prismaObject("Post", {
  fields: (t) => ({
    id: t.exposeID("id"),
    title: t.exposeString("title"),
    content: t.exposeString("content", { nullable: true }),
    published: t.exposeBoolean("published"),
    author: t.relation("author"),
  }),
});

builder.queryField("feed", (t) =>
  t.prismaField({
    type: ["Post"],
    resolve: async (query, _parent, _args, _info) =>
      prisma.post.findMany({
        ...query,
        where: { published: true },
      }),
  })
);

builder.queryField("post", (t) =>
  t.prismaField({
    type: "Post",
    args: {
      id: t.arg.id({ required: true }),
    },
    nullable: true,
    resolve: async (query, _parent, args, _info) =>
      prisma.post.findUnique({
        ...query,
        where: {
          id: String(args.id),
        },
      }),
  })
);

builder.queryField("drafts", (t) =>
  t.prismaField({
    type: ["Post"],
    resolve: async (query, _parent, _args, _info) =>
      prisma.post.findMany({
        ...query,
        where: { published: false },
      }),
  })
);

builder.queryField("filterPosts", (t) =>
  t.prismaField({
    type: ["Post"],
    args: {
      searchString: t.arg.string({ required: false }),
    },
    resolve: async (query, _parent, args, _info) => {
      const or = args.searchString
        ? {
            OR: [
              { title: { contains: args.searchString } },
              { content: { contains: args.searchString } },
            ],
          }
        : {};
      return prisma.post.findMany({
        ...query,
        where: { ...or },
      });
    },
  })
);

builder.mutationField("signupUser", (t) =>
  t.prismaField({
    type: "User",
    args: {
      name: t.arg.string({ required: false }),
      email: t.arg.string({ required: true }),
    },
    resolve: async (query, _parent, args, _info) =>
      prisma.user.create({
        ...query,
        data: {
          email: args.email,
          name: args.name,
        },
      }),
  })
);

builder.mutationField("deletePost", (t) =>
  t.prismaField({
    type: "Post",
    args: {
      id: t.arg.id({ required: true }),
    },
    resolve: async (query, _parent, args, _info) =>
      prisma.post.delete({
        ...query,
        where: {
          id: String(args.id),
        },
      }),
  })
);

builder.mutationField("publish", (t) =>
  t.prismaField({
    type: "Post",
    args: {
      id: t.arg.id({ required: true }),
    },
    resolve: async (query, _parent, args, _info) =>
      prisma.post.update({
        ...query,
        where: {
          id: String(args.id),
        },
        data: {
          published: true,
        },
      }),
  })
);

builder.mutationField("createDraft", (t) =>
  t.prismaField({
    type: "Post",
    args: {
      title: t.arg.string({ required: true }),
      content: t.arg.string(),
      authorEmail: t.arg.string({ required: true }),
    },
    resolve: async (query, _parent, args, _info) =>
      prisma.post.create({
        ...query,
        data: {
          title: args.title,
          content: args.content,
          author: {
            connect: { email: args.authorEmail },
          },
        },
      }),
  })
);

const schema = builder.toSchema();

export default createYoga<{
  req: NextApiRequest;
  res: NextApiResponse;
}>({
  schema,
  graphqlEndpoint: "/api/graphql",
});

export const config = {
  api: {
    bodyParser: false,
  },
};

https://pothos-graphql.dev/docs/plugins/prisma#creating-types-with-builderprismaobject
https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield

クエリを投げて、データが取得できることを確認する

クエリの確認

0Yu0Yu

フロントエンドで記事のfeedを取得する

https://www.apollographql.com/docs/react/get-started/
https://github.com/prisma/prisma-examples/tree/latest/typescript/graphql-nextjs

pages/index.tsx
// データをUI上に表示する処理を書く
...
// GraphQLのクエリを叩いてデータを取得する
export async function getServerSideProps() {
  const { data } = await client.query({
    query: gql`
      query FeedQuery {
        feed {
          id
          title
          content
          published
          author {
            id
            name
          }
        }
      }
    `,
  });

  return {
    props: {
      data,
    },
  };
}

記事のfeedを取得した

0Yu0Yu

エディタをWASM製のマークダウンパーサーに置き換える

submoduleで紐付けする
https://github.com/yud0uhu/markdown-parser/tree/c06d8da88dc96d1c1ebcb60e4ca17f456247fd53

% cd markdown-parser
% wasmpask build
% cd ../

クライアントでWASMを扱えるように、next.config.jsでWebPackの設定を行う
https://nextjs-ja-translation-docs.vercel.app/docs/api-reference/next.config.js/introduction

/**
 * @type {import('next').NextConfig}
 */
module.exports = {
  webpack: (config, { isServer }) => {
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    config.output.webassemblyModuleFilename =
      (isServer ? "../" : "") + "static/wasm/[modulehash].wasm";
    return config;
  },
};

https://webpack.js.org/configuration/experiments/
https://zenn.dev/razokulover/articles/fb64150be7a667

pkgをそのままimportして使う

import { text_to_token } from "../../markdown-parser/pkg";

https://qiita.com/SoraKumo/items/d68b78bedda91ff08435#nextjsからwasmを呼び出すコード

0Yu0Yu

マークダウンエディタを埋め込む

import { text_to_token } from "../../markdown-parser/pkg";

でインポートしたtext_to_tokenに入力したマークダウンテキストを渡し、convertContentでインナーHTMLにコンバートする。

dangerouslySetInnerHTMLでインナーHTMLをコンバートする

<div
            dangerouslySetInnerHTML={{
              __html: markdownContent,
            }}
          />

マークダウンエディタサンプル

create/index.ts
import React, { use, useEffect, useState } from "react";
import Layout from "../../components/layout";
import Router from "next/router";
import gql from "graphql-tag";
import { useMutation } from "@apollo/client";
import { text_to_token } from "../../markdown-parser/pkg";

const CreateDraftMutation = gql`
  mutation CreateDraftMutation(
    $title: String!
    $content: String
    $authorEmail: String!
  ) {
    createDraft(title: $title, content: $content, authorEmail: $authorEmail) {
      id
      title
      content
      published
      author {
        id
        name
      }
    }
  }
`;

function Draft() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [markdownContent, setMarkdownContent] = useState("");
  const [authorEmail, setAuthorEmail] = useState("");

  const convertContent = (content: string) => {
    console.log(content);
    setContent(content);
    setMarkdownContent(text_to_token(content));
  };

  const [createDraft] = useMutation(CreateDraftMutation);

  return (
    <Layout>
      <div>
        <form
          onSubmit={async (e) => {
            e.preventDefault();

            await createDraft({
              variables: {
                title,
                content,
                authorEmail,
              },
            });
            Router.push("/drafts");
          }}
        >
          <h1>Create Draft</h1>
          <input
            autoFocus
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Title"
            type="text"
            value={title}
          />
          <input
            onChange={(e) => setAuthorEmail(e.target.value)}
            placeholder="Author (email adress)"
            type="text"
            value={authorEmail}
          />
          <textarea
            cols={50}
            onChange={(e) => convertContent(e.target.value)}
            placeholder="Content"
            rows={8}
            value={content}
          />
          <h1>Preview</h1>
          <div
            dangerouslySetInnerHTML={{
              __html: markdownContent,
            }}
          />
          <input
            disabled={!content || !title || !authorEmail}
            type="submit"
            value="Create"
          />
          <a className="back" href="#" onClick={() => Router.push("/")}>
            or Cancel
          </a>
        </form>
      </div>
      <style jsx>{`
        .page {
          background: white;
          padding: 3rem;
          display: flex;
          justify-content: center;
          align-items: center;
        }

        input[type="text"],
        textarea {
          width: 100%;
          padding: 0.5rem;
          margin: 0.5rem 0;
          border-radius: 0.25rem;
          border: 0.125rem solid rgba(0, 0, 0, 0.2);
        }

        input[type="submit"] {
          background: #ececec;
          border: 0;
          padding: 1rem 2rem;
        }

        .back {
          margin-left: 1rem;
        }
      `}</style>
    </Layout>
  );
}

export default Draft;
0Yu0Yu

ホスティング先をどうする?
-> AmplifyがSSRサポートしている ので試す
https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/deploy-nextjs-app.html

  • submoduleをbackendとしてdeployするyamlを書く
    • yamlでbackendのデプロイを同時にする
amplify.yml
version: 1
backend:
  phases:
    preBuild:
      commands:
        - cd markdown-parser
        # install rust
        - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
        - source ~/.cargo/env
        # add WASM target
        - rustup target add wasm32-unknown-unknown
        # install wasm-pack
        - cargo install wasm-pack
    build:
      commands:
        - wasm-pack build . --target web
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - "**/*"
  cache:
    paths:
      - node_modules/**/*

amplify console

0Yu0Yu

Amplify CLIからHostingする

https://docs.amplify.aws/cli/start/install/

Amplifyのインストール

npm install -g @aws-amplify/cli
amplify configure
  • IAMユーザーを作成する
  • regionはap-northeast-1(東京リージョン)
  • IAM PollicyはAdministratorAccess-Amplifyをアタッチする

サイトを公開する

apmlify add hosting でS3の静的ホスティングを有効にする
バケット名に何も指定しない場合、ユニークなバケットが生成される
ビルド後はCloudFrontで公開される

amplify init # プロジェクトの初期化 
amplify add hosting # hostingの有効化
amplify publish # アプリケーションのビルド

バックエンドのGraphQLを追加する

amplify add api
0Yu0Yu

TIPS

mongodbが繋がらなくなった
Console > connect > Add Current IP Adressからipアドレスを割り当てる

0Yu0Yu

Hasura+Code Generator(移行用)

https://hasura.io/docs/latest/mutations/postgres/insert/
https://tech.buysell-technologies.com/entry/adventcalendar2021-12-09
https://tech.wasd-inc.com/entry/2023/03/24/152936
https://tech.wasd-inc.com/entry/2022/08/17/122059
https://qiita.com/shin_k_2281/items/f422fb88e552bfd5dbee
https://qiita.com/yoshii0110/items/b461e608dc0cff78982e#フロントエンド側でgraphqlを使用する場合
https://chaika.hatenablog.com/entry/2022/06/02/083000
https://zenn.dev/nbstsh/scraps/f01024249984de
https://cloud.hasura.io/project/9918a651-7aca-4892-b24c-b759f8983cd7/console/data/my-blog-postgres/schema/public
https://zenn.dev/knaka0209/books/261398faf9b13a/viewer/a7a2da
https://zenn.dev/mh4gf/articles/graphql-codegen-client-preset
https://the-guild.dev/graphql/codegen/plugins/presets/preset-client
https://techblg.app/articles/how-to-serve-graphql-api-by-next.js-in-vercel/
https://blog.uzumaki-inc.jp/hasuragraphqlspa
https://tech.wasd-inc.com/entry/2023/03/24/152936
https://zenn.dev/knaka0209/books/befdda3d27a264/viewer/009cfa
https://hasura.io/learn/ja/graphql/react/optimistic-update-mutations/2-mutation-cache/
https://hasura.io/docs/latest/mutations/postgres/insert/
https://tech.buysell-technologies.com/entry/adventcalendar2021-12-09
https://tech.wasd-inc.com/entry/2022/08/17/122059
https://qiita.com/shin_k_2281/items/f422fb88e552bfd5dbee
https://qiita.com/yoshii0110/items/b461e608dc0cff78982e
https://chaika.hatenablog.com/entry/2022/06/02/083000
https://zenn.dev/nbstsh/scraps/f01024249984de
https://zenn.dev/nbstsh/scraps/fa6637c67c5b74

0Yu0Yu

vercel postgressのhobby planの利用制限に引っかかった
vercel-postgress

0Yu0Yu

テーブル設計

https://supabase.com/docs/guides/database/tables

CREATE TABLE "User" (
  "id" SERIAL PRIMARY KEY,
  "name" VARCHAR(255)
);

CREATE TABLE "Post" (
  "id" SERIAL PRIMARY KEY,
  "authorId" INT,
  "content" TEXT,
  "published" BOOLEAN DEFAULT FALSE,
  "title" VARCHAR(255) NOT NULL,
  "createdAt" TIMESTAMP DEFAULT NOW(),
  "updatedAt" TIMESTAMP,
  "viewCount" INT DEFAULT 0,
  "tagId" INT,
  FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL,
  FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE SET NULL
);

CREATE TABLE "Tag" (
  "id" SERIAL PRIMARY KEY,
  "label" VARCHAR(255)
);

リレーションは以下のように作成

ALTER TABLE "Post" ADD CONSTRAINT "FK_Post_authorId_User_id" FOREIGN KEY ("authorId") REFERENCES "User"("id");
ALTER TABLE "Post" ADD CONSTRAINT "FK_Post_tagId_Tag_id" FOREIGN KEY ("tagId") REFERENCES "Tag"("id");
  • PostテーブルのauthorIdフィールドがUserテーブルのidフィールドを参照する
  • tagIdフィールドがTagテーブルのidフィールドを参照する
0Yu0Yu

Vercelで手動デプロイ

Vercel Consoleから、Settings > GitDeploy Hooksで適当なフック名とブランチを指定
hook

ターミナルからcurlでフックを呼び出す

curl -X POST 
https://api.vercel.com/v1/XXXXX
0Yu0Yu

パフォーマンス改善

LightHouseで計測
初期(Score:75)
75

ここから以下のように修正

  • useQueryが500msごとにクエリを発行するようになっていた(pollInterval: 500)
  • optionでfetchPolicy: "cache-and-network" を指定

https://www.apollographql.com/docs/react/data/queries/#cache-and-network

修正後(Score:78)
78

さらに以下のように修正

  • キャッシュ制御ヘッダーを追加
    public, max-age=31536000, immutable で1年間ブラウザがキャッシュからリソースを取得するように設定
next.config.js
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, immutable",
          },
        ],
      },
    ];
  },
  • index.tsxの使われていないコードを削除

修正後(Score: 90)
90

0Yu0Yu

知見置場

TS/バンドル周り

https://typescript-jp.gitbook.io/deep-dive/getting-started/why-typescript
https://jsprimer.net/use-case/ajaxapp/entrypoint/
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Introduction

React

https://zenn.dev/uhyo/articles/react-18-alpha-essentials

Mongo

https://qiita.com/Brutus/items/8a67a4db0fdc5a33d549

GraphQL・API設計

https://engineering.mercari.com/blog/entry/20220303-concerns-with-using-graphql/
https://qiita.com/teradonburi/items/2ad98c7c21f1f6cc4390
https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-nextjs

Vercelビルド設定周り

https://vercel.com/docs/concepts/functions/serverless-functions/quickstart
https://vercel.com/docs/concepts/projects/project-configuration#functions
https://www.npmjs.com/package/@vercel/build-utils
https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/vercel-caching-issue

Prisma

https://zenn.dev/tsucchiiinoko/articles/f222dbbfa23325
https://www.prisma.io/blog/fullstack-nextjs-graphql-prisma-2-fwpc6ds155
https://qiita.com/am_765/items/5e42bd5f87b296f61fbc
https://www.prisma.io/docs/concepts/overview/what-is-prisma

Styled-components

https://styled-components.com/
https://qiita.com/7tsuno/items/8fc1f4124b0519b58e63

NextAuth

https://qiita.com/kage1020/items/195fdd8749f2439849c1#secret
https://zenn.dev/tkengineer/articles/5eb78800e9cd5f
https://dev.classmethod.jp/articles/auth_js/
https://next-auth.js.org/configuration/options#secrethttps://qiita.com/Hirohana/items/e3b71af64311e26582a6
https://next-auth.js.org/configuration/options
https://tmokmss.hatenablog.com/entry/20230109/1673237629
https://qiita.com/kage1020/items/e5b0053d7046a9b1f628
https://zenn.dev/nrikiji/articles/d37393da5ae9bc
https://next-auth.js.org/getting-started/client
https://reffect.co.jp/react/next-auth/

マークダウンパーサー

https://www.m3tech.blog/entry/2021/08/23/124000

ディレクトリ設計

https://zenn.dev/yodaka/articles/eca2d4bf552aeb

TIPS

ダークモード

https://mantine.dev/guides/dark-theme/
https://note.com/tom_js/n/n8e4a747cda45
https://mantine.dev/theming/colors/
https://blog.35d.jp/2020-10-06-notion-blog-dark-mode
https://www.wantedly.com/companies/tsunagu-grp/post_articles/425027

cloudflareでカスタムドメインを買って、Vercelで設定する

https://zenn.dev/keitakn/articles/add-cloudflare-domain-to-vercel
https://dash.cloudflare.com/174e43b58ff72652ad928a6646c3e8d2/yud0uhu.work/ssl-tls
https://tars0x9752.com/posts/custome-domain

404ページ

https://illustrain.com/?p=24609

メモ
Google Analytics入れたい
https://zenn.dev/rh820/articles/8af90011c573fe

0Yu0Yu
index.tsx
import { Inter } from 'next/font/google'
import styles from '../styles/component.module.css'
 
const inter = Inter({
  variable: '--font-inter',
})

実行すると以下のエラーが出る

`next/font` error:
Preload is enabled but no subsets were specified for font `Inter`. Please specify subsets or disable preloading if your intended subset can't be preloaded.
Available subsets: `cyrillic`, `cyrillic-ext`, `greek`, `greek-ext`, `latin`, `latin-ext`, `vietnamese`

Read more: https://nextjs.org/docs/messages/google-fonts-missing-subsets

https://nextjs.org/docs/pages/api-reference/components/font
デフォルトでは preload が true となっており、その場合は subsets 指定が必須
サブセットを指定

import { Inter } from "next/font/google";

const inter = Inter({
  variable: "--font-inter",
  subsets: ["latin"],
});

以下で適用

  return (
      <main style={inter.style}>
      </main>
  );
0Yu0Yu

https://nextjs.org/docs/messages/built-in-next-font
バージョン 13.2 以降では、next/fontNext.js に組み込まれており、@next/fontパッケージが冗長になっています。この@next/fontパッケージは Next.js 14 で完全に削除されます。
以下でアンインストール

npx @next/codemod built-in-next-font
0Yu0Yu
_document.tsx
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
  DocumentInitialProps,
} from 'next/document'
import { ServerStyleSheet } from 'styled-components'
class MyDocument extends Document {
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<DocumentInitialProps> {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
  render() {
    return (
      <Html lang="en">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument
0Yu0Yu

ハマりポイント

以下のエラーで弾かれた

- error Error [TypeError]: Cannot read properties of undefined (reading 'Symbol(Pothos.contextCache)')
    at file:///Users/denham/Documents/my-blog-hasura/node_modules/@pothos/core/esm/utils/context-cache.js:10:33
    at SchemaBuilder.prismaObject (file:///Users/denham/Documents/my-blog-hasura/node_modules/@pothos/plugin-prisma/esm/schema-builder.js:18:22)
    at eval (webpack-internal:///(api)/./pages/api/graphql.ts:27:9) {
  digest: undefined
}
- wait compiling /_error (client and server)...
- event compiled client and server successfully in 2.4s (1476 modules)
- warn Fast Refresh had to perform a full reload due to a runtime error.

npx prisma generate のときの以下の警告がにおう

warn Versions of prisma@4.13.0 and @prisma/client@4.16.2 don't match.
This might lead to unexpected behavior.
Please make sure they have the same version.

【解決】
@prisma/clientのバージョンは明示的に@prismaに合わせる

package.json
"@prisma/client": "4.13.0",
0Yu0Yu

テキストコンテンツがサーバーでレンダリングされた HTMLと一致しない

https://nextjs.org/doreact-hydration-error
cs/messages/react-hydration-error
以下のようにして解決

Solution 1: Using useEffect to run on the client only
Ensure that the component renders the same content server-side as it does during the initial client-side render to prevent a hydration mismatch. You can intentionally render different content on the client with the useEffect hook.

具体例
import { useState, useEffect } from 'react'
 
export default function App() {
  const [isClient, setIsClient] = useState(false)
 
  useEffect(() => {
    setIsClient(true)
  }, [])
 
  return <h1>{isClient ? 'This is never prerendered' : 'Prerendered'}</h1>
}
create.tsx
import React, { Suspense, useEffect, useState } from 'react'
import Layout from '../../components/layout'
import init from '../../markdown-parser/pkg/markdown_parser'
import Create from '../../features/create/components/Create'

function CreatePage() {
  const [isClient, setIsClient] = useState(false)
  // init関数は、コンポーネントのマウント時ではなく、外部のebAssemblyモジュールを非同期でロードするため、useEffectフックを使用する
  useEffect(() => {
    const loadWasm = async () => {
      await init()
    }
    setIsClient(true)
    loadWasm()
  }, [])

  return (
    <>
      {isClient ? (
        <Layout>
          <Create />
        </Layout>
      ) : (
        <Suspense fallback={<p>Loading feed...</p>}> </Suspense>
      )}
    </>
  )
}

export default CreatePage