Closed8

Remix+CloudflareでWebサイトを作る 41(dangerouslySetInnerHTML削除、next-themes、原因切り分けのためにVercelにもデプロイ)

saneatsusaneatsu

【2024-10-31】dangerouslySetInnerHTMLを使わないコードに修正する

背景

https://biomejs.dev/linter/rules/no-dangerously-set-inner-html/

ところどころで使っているがBiomeに怒られている。

書いてみる

以下のようにしてみた。

pnpm add html-react-parser isomorphic-dompurify
import parse from "html-react-parser";
import DOMPurify from "isomorphic-dompurify";

// loaderは省略

export default function Page() {
  const { html } = useLoaderData<typeof loader>();
  const sanitizedHtml = DOMPurify.sanitize(html);

  return (
    <div>
      <div{parse(sanitizedHtml)}</div>
    </div>
  );
}

注意点

https://www.npmjs.com/package/dompurify

isomorphic-dompurify ではなく dompurify を使うと以下のエラーが出る。

TypeError: __vite_ssr_import_3__.default.sanitize is not a function\

SSRゆえに起こる問題っぽいのでisomorphic-dompurify を使う。
こっちを使うことは公式のREADMEにも書かれている。

If you have problems making it work in your specific setup, consider looking at the amazing isomorphic-dompurify project which solves lots of problems people might run into.

参考までにトレンド推移。

https://npm-compare.com/dompurify,isomorphic-dompurify,sanitize-html

エラーが発生

デプロイ時に以下のエラーが発生。

[ERROR] Deployment failed!
  
    Failed to publish your Function. Got error: Uncaught ReferenceError: window is not defined
      at functionsWorker-0.5473523066909975.js:84924:22 in ../node_modules/isomorphic-dompurify/browser.js

https://github.com/remix-run/remix/issues/8628

Remixで使えないよ、というIssueがremix-runにたっている(2024-01-30)。

https://github.com/kkomelin/isomorphic-dompurify/issues/214
Issue内にはisomorphic-dompurifyにIssueたてたよ、と書かれているので見に行ってみるが、そっちではRemix側の問題なのでCloseということになっている。

う〜〜んわからん。
一旦サニタイズはスキップするようにした。

saneatsusaneatsu

【2024-11-01】テーマ(Light/Dark)をトグルで切り替えて永続化させるためにaction()を発火させているため、再検証のため現在のページのloader()が発火してしまう

背景

https://zenn.dev/link/comments/a2222239b799db

ここでテーマをLight/Darkで変えられるようにした。
しかし、action()を発火させているコードがあるため、再検証が実行されている。
つまり、テーマを変更したページのloader()が走ってしまっている。

解決策として shouldRevalidate が最初に思い浮かんだけど、どのページでも起こり得るから全く根本的な解決方法ではない。

前のScrapで参考にしたページのコードでいうと以下の部分が問題。
The Complete Guide to Dark Mode with Remix | Matt Stobbs

function ThemeProvider({ children }: { children: ReactNode }) {
  // ...

  useEffect(() => {
    if (!mountRun.current) {
      mountRun.current = true;
      return;
    }
    if (!theme) {
      return;
    }

    // ここでPOSTしてaction()を発火させている
    persistThemeRef.current.submit({ theme }, { action: 'action/set-theme', method: 'post' });
  }, [theme]);
}

解決方法

https://www.npmjs.com/package/next-themes

これを使おう。

https://zenn.dev/harukii/articles/bdd28efb9df66c

↑使い方。

saneatsusaneatsu

【2024-11-09】VercelでデプロイしたらTTFBはどう変化するのか

背景

https://zenn.dev/link/comments/f783cc9e04cf52

1つ前のScrapのTTFBで3.8sかかってる問題の続き。
原因の切り分けのためにVercelにデプロイして計測してみる。

Vercelへのデプロイ

設定ファイル書き換え

https://vercel.com/docs/frameworks/remix

pnpm i @vercel/remix
vite.config.ts
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      envOnlyMacros(),
-     remixCloudflareDevProxy({ getLoadContext }),
      remix({
        routes,
        future: {
          v3_singleFetch: true,
        },
+       presets: [vercelPreset()],
      }),
      tsconfigPaths(),
    ],
tsconfig.json
{
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
-   "types": ["@remix-run/cloudflare", "vite/client"],
+   "types": ["@vercel/remix", "vite/client"],

@remix-run/cloudflare@vercel/remix

約100箇所、ひたすらに置換。

@remix-run/cloudflareは1ヶ月前に更新されていてv2.13.1なのに、@remix-run/vecelは最終更新が2023-08と、1年以上前でv1.19.3で止まってる。

「Remix v2.13使ってるからSingle Fetch使ってる書き方してるけど@remix-run/vercel 使って動くのかな」か思ったけどそれをやるのは @vercel/remix の方。

なかなかうまくいかない

全部Vercel用に書き換えるの骨折れる。
別のRepositoryを作って完全に新しく作成して少しずつコピペしていくほうが早い気がしてきた。

saneatsusaneatsu

【2024-11-10】Vercelだとdeferで取得するようなPromise型のデータが画面上に表示されない問題

背景

CloudflareからVercel用に色々書き換えたらdeferで取得しているデータに関してずっと<Suspence>fallbackの状態になってしまいデータが表示されない。

解決方法

https://github.com/kuc-arc-f/remix47/blob/main/sample/turso_test/app/entry.server.tsx

entry.server.tsxrenderToString を使わずにrenderToPipeableStream を使う。

Before

entry.server.tsx
import { RemixServer } from "@remix-run/react";
import { createInstance } from "i18next";
import { initReactI18next } from "react-i18next";

import { renderToString } from "react-dom/server";
import { I18nextProvider } from "react-i18next";
import { i18nextConfig } from "~/config/i18n";
import i18nServer from "~/services/i18n.server";

import type { AppLoadContext, EntryContext } from "@vercel/remix";

export default async function handleRequest(
  request: Request,
  initialResponseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  // This is ignored so we can keep it in the template for visibility.  Feel
  // free to delete this parameter in your app if you're not using it!
  loadContext: AppLoadContext,
) {
  
  const body = renderToString(    
    <RemixServer context={remixContext} url={request.url} />    
  );

  responseHeaders.set("Content-Type", "text/html");
  return new Response(body, {
    headers: responseHeaders,
    status: initialResponseStatusCode,
  });
}

After

entry.server.tsx
import { PassThrough } from "node:stream";

import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import type { AppLoadContext, EntryContext } from "@vercel/remix";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  loadContext: AppLoadContext,
) {
  return isbot(request.headers.get("user-agent") || "")
    ? handleBotRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext,
      )
    : handleBrowserRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext,
      );
}

function handleBotRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
) {
  return new Promise((resolve, reject) => {
    let shellRendered = false;
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
      {
        onAllReady() {
          shellRendered = true;
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            }),
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          responseStatusCode = 500;
          // Log streaming rendering errors from inside the shell.  Don't log
          // errors encountered during initial shell rendering since they'll
          // reject and get logged in handleDocumentRequest.
          if (shellRendered) {
            console.error(error);
          }
        },
      },
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
) {
  return new Promise((resolve, reject) => {
    let shellRendered = false;
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
      {
        onShellReady() {
          shellRendered = true;
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            }),
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          responseStatusCode = 500;
          // Log streaming rendering errors from inside the shell.  Don't log
          // errors encountered during initial shell rendering since they'll
          // reject and get logged in handleDocumentRequest.
          if (shellRendered) {
            console.error(error);
          }
        },
      },
    );

    setTimeout(abort, ABORT_DELAY);
  });
}
saneatsusaneatsu

【2024-11-11】Vercel用にGitHub Actionsを修正

https://naopoyo.com/docs/deploy-based-on-tag-push-in-vercel

ここを参考に以下3つをGitHub Secrets Variablesとして登録してStagingにデプロイするGitHub Actionsを修正。

  • VERCEL_ORG_ID
  • VERCEL_PROJECT_ID
  • VERCEL_TOKEN
Vercel CLI 37.14.0
No Project Settings found locally. Run `vercel pull --yes` to retrieve them.
Error: Process completed with exit code 1.

このエラーが消えない。

saneatsusaneatsu

丸1日近くの時間スタックした。
色々直してやっとVercelにデプロイした。

ちょっと遅い気がするけどさすがに3800msもかからない。

デプロイ1

初回

2回目

デプロイ2

初回

デプロイ3

初回

saneatsusaneatsu

【2024-11-13】Warning: Extra attributes from the server: class,style

エラー内容

[Error] Warning: Extra attributes from the server: class,style
html
App@http://localhost:5173/app/root.tsx:205:11
AppWithProviders@http://localhost:5173/app/root.tsx:333:29
RenderedRoute@http://localhost:5173/node_modules/.vite/deps/chunk-AEHPLVJH.js:402:11
RenderErrorBoundary@http://localhost:5173/node_modules/.vite/deps/chunk-AEHPLVJH.js:359:10
...

調査

修正方法がない?

https://zenn.dev/msk1206/articles/e933af315d8fdf

next-themeを入れると起こるWarning。

これはDarkModeなどのテーマを切り替えるような場合、使用しているUIライブラリに関係なく起こります。

根本的な解決策はないが、エラーの抑制をする事で対応する。

まじか。
<body suppressHydrationWarning={true}> とすることでコンソールにログを出さないようにすることができるようだが...消えないな??

解決方法

https://zenn.dev/dk_/articles/dd9b0426e58f7d

この方法で対応した

saneatsusaneatsu

ん〜わからん。違うリポジトリで1から作り直していって原因特定したほうが早い気がしてきた。

このスクラップは8日前にクローズされました