Open26

HP作成ログ

Tomoki OtaTomoki Ota

個人HPのテンプレ作成

技術スタック

  • SSG
  • Hono
  • Bun
  • Cloudflare Pages
Tomoki OtaTomoki Ota

インストール

Bun - Honoを参考にsetupしていく。

bun create .
bun i
bun add hono

これでhonoのインストールは完了。

hello ssg

ssgの設定をしていく。

SSG Helper - Honoを参考にする。

vite

viteを入れることでSSGのビルドと開発サーバーの立ち上げを統合でき、ホットリロードが可能になる。
vite-plugins/packages/ssg at main · honojs/vite-plugins

bun add vite @hono/vite-ssg
package.json
{
+  "type": "module"
}

@hono/vite-dev-serverとかは以下を参考。

vite.config.ts
import ssg from "@hono/vite-ssg";
import devServer from "@hono/vite-dev-server";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [ssg({ entry }), devServer({ entry })],
  server: {
    port: 3001,
    host: "0.0.0.0",
  },
});
pacakge.json
{
  "scripts": {
    "dev": "vite"
  },
    ・・・
}

css

tailwindを使用する。Installation - Tailwind CSS

bun install -D tailwindcss postcss autoprefixer
bunx tailwindcss init
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
+  content: ["./src/**/*.{html,tsx, jsx, ts, js}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
src/input.css
@tailwind base;
@tailwind components;
@tailwind utilities;
bunx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

すると、dist配下にoutput.cssができる。

Hello.tsx
export function Hello() {
  return (
    <html>
      <head>
        <link rel="stylesheet" href="../dist/output.css" />
        <link href={styles} rel="stylesheet" />
      </head>
      <body>
        <div>
          <h1 class="text-3xl font-bold underline">Hello, World!</h1>
          <p>My First Page</p>
        </div>
      </body>
    </html>
  );
}

なぜかリロードすると読み込みが遅いときがある。

Hello.tsx
import styles from "../dist/output.css?url";
export function Hello() {
  return (
    <html>
      <head>
        <link href={styles} rel="stylesheet" />
      </head>
      <body>
        <div>
          <h1 class="text-2xl text-red-600 font-bold underline">
            Hello, World!
          </h1>
          <p>My First Page</p>
          <p>a</p>
        </div>
      </body>
    </html>
  );
}

これでもいける。?urlをつけることに注意。

staticファイル

package.json
{
  "name": "workspace",
  "scripts": {
    "dev": "vite",
+    "build": "bun run ./build.ts"
  },
}

bun run buildを叩くと、static/ができる。

Tomoki OtaTomoki Ota

linkを使用するときは、Linkコンポーネントを使用する。
ちなみに同じくscriptタグにもScriptコンポーネントが存在する。

+ import { Link } from "honox/server";
//  省略
-        {import.meta.env.PROD ? (
-          <link href='static/assets/style.css' rel='stylesheet' />
-        ) : (
-          <link href='/app/style.css' rel='stylesheet' />
-        )}
+       <Link rel="stylesheet" href="/static/assets/style.css" />

https://github.com/honojs/honox/pull/181

Tomoki OtaTomoki Ota

global.d.tsで実行はできるが、vscode上でResponseが見つかりませんというエラーが出る。

global.d.ts
import {} from "hono";
import type { Meta } from "./types";

declare module "hono" {
  interface ContextRenderer {
    (content: string | Promise<string>, meta?: Meta & { frontmatter: Meta }):
      | Response
      | Promise<Response>;
  }
}

対処法として、libにdomを追加する

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "esModuleInterop": true,
    "strict": true,
+    "lib": ["esnext", "dom"],
    "types": ["vite/client"],
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}
Tomoki OtaTomoki Ota

デプロイ

github pages

github pagesにデプロイすると、linkとページ遷移が正常に動作しない

linkに関しては、./staticにすれば問題はない。
しかし、ページ遷移は対処方法が不明。viteのhpでは、vite.config.jsにbaseを/repository_name/と設定すれば解決すると書いていたが、反映されなかった。

cloudflare pages

cloudflare pagesには問題なくデプロイできた。

ただし、wranglerのpreviewがコンテナで動作しない。
wranglerの一部オプションがnpmとpnpmでしかサポートしていない問題がある。

Tomoki OtaTomoki Ota

tailwind

ダークモード

darkModeにclassとmediaのどちらかを追加する。mediaの場合は、ユーザが変更することが不可能。

tailwind.config.js
module.exports = {
+  darkMode: 'class',
  // ...
}

ほんで以下のjsを読み込めばいいらしい。(参考: Tailwind CSS のダークモードで System, Light, Dark を切り替える)

ユーザによって設定を変えるdarkmodeはjsが必要なのでSSRで行う必要がある。

if (!('theme' in localStorage) || localStorage.theme === 'system') {
  // OS の設定を読み取る
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    // OS の設定がダークモードの場合、<html> に dark クラスを付与する
    document.documentElement.classList.add('dark')
  }
  // LocalStorage に設定を保存する
  localStorage.setItem('theme', 'system')
} else if (localStorage.theme === 'dark') { // LocalStorage に theme が保存されていて、theme が dark の場合
  document.documentElement.classList.add('dark')
} else { // それ以外の場合
  document.documentElement.classList.remove('dark')
}
Tomoki OtaTomoki Ota

cssがページ遷移で読み込めなくなる問題

ページ遷移したときに再レンダリングがかかり、cssが消える。
→cssファイルやjsファイルは、/app/配下に置く

appは/app/としてapp配下以外はstatic/のように指定する。

Tomoki OtaTomoki Ota

honox

メモ書き忘れ。(時系列が異なる)
vite使うならhonoxってのがあったからhonoxに移行した。

bun create hono@latest .

x-basicを選択する。

bun i
Tomoki OtaTomoki Ota

honox/factory

createRouterヘルパーが入っている

app/server.ts:サーバ側の処理を記述するファイル
showRoutes() → honoのサーバー側のルーティング紐づけ
app/routes/ のファイルベースルーティング

default exportの内容をレスポンスとしてブラウザに表示している。

tsx + honox/factoryでcreateRouteヘルパーを活用することで、jsxを利用した開発が可能

2つの書き方がある。下がクライアントレンダリングで、上がサーバサイドレンダリング。getリクエストを送る。

index.tsx
import { createRoute } from 'honox/factory'

export default createRoute((c) => {
  return c.render(
    <div>
      <h1>Hello!</h1>
    </div>
  )
})
index.tsx
export default function Foo() {
  return (
    <div>
      <h1>Hello!</h1>
    </div>
  );
}
Tomoki OtaTomoki Ota

アイコンを1:1にトリミングするCSS。object-coverがないと縦横比が変わる。また、object-[center_30%]でトリミングを微調整。一番上ならobject-topでもいい。

export const Avatar = (props: Props) => {
  return (
    <a
      href={props.src}
      class="items-center inline-block w-28 flex-shrink-0 max-sm:w-20"
    >
      <img
        class="rounded-full w-full aspect-square object-cover object-[center_30%]"
        src={props.src}
        alt={"アイコン画像"}
      />
    </a>
  );
};
Tomoki OtaTomoki Ota

ある一部のライブラリをインポートした時に、can't find variable: exportsとかcan't find variable: requireとかのエラーが出ていた。

すごいハマったけど、以下のコードがif (mode == production)の中にあったのが原因だった。。。

vite.config.ts
    ssr: {
      target: "node",
      external: [
        "unified",
        "@mdx-js/mdx",
        "satori",
        "@resvg/resvg-js",
        "feed",
        "budoux",
        "jsdom",
      ],
    }
    ```
Tomoki OtaTomoki Ota
型 '{ build: { assetsDir: string; emptyOutDir: false; ssrEmitAssets: true; rollupOptions: { input: string[]; output: { entryFileNames: string; assetFileNames: () => string; }; }; }; plugins: (PluginOption[] | Plugin<...> | Plugin)[]; server: { ...; }; ssr: { ...; }; }' を型 'UserConfig' に割り当てることはできません。
  'ssr.target' の型は、これらの型同士で互換性がありません。
型 'string' を型 'SSRTarget | undefined' に割り当てることはできません。

というエラーが出た。

vite.config.ts
- import { defineConfig } from "vite";
+ import { defineConfig, UserConfig, SSRTarget } from "vite";
                   // ・・・
const entry = "./app/server.ts";

- export default defineConfig(({ mode }) => {
+ export default defineConfig(({ mode }): UserConfig => {
  if (mode === "client") {
    return {
      plugins: [client()],
    };
  }
  const commonConfig = {
            // ・・・・
    ssr: {
-      target: "node" ,
+      target: "node" as SSRTarget ,
            //・・・・
    }
  };
            //・・・・
  return commonConfig;
});

変更点

  • ssr.target: "node" as SSRTargetで、targetに型アサーションを適用し、型エラーを防いでいる
  • commonConfig全体をUserConfig型として定義することで、コンパイラが設定の構造を期待通りに解釈している
Tomoki OtaTomoki Ota

Rehype Pretty Code & Shiki

まず、ベースとなるvite.config.tsは以下のようになる。

vite.config.ts
export default defineConfig(({ mode }): UserConfig => {
  const highlightOptions = {
            // ここの設定を変える
  }
  const commonConfig = {
    plugins: [
      ssg({ entry }),
      honox({}),
      mdx({
        jsxImportSource: "hono/jsx",
        providerImportSource: "./app/components/feature/blogs/mdxComponents",
        remarkPlugins: [
          remarkFrontmatter,
          remarkMdxFrontmatter,
          [
            remarkRehype,
            {
              footnoteBackContent: "↩︎",
              footnoteLabel: " ",
              footnoteLabelTagName: "hr",
              footnoteBackLabel: "Back to reference 1",
            },
          ],
          remarkGfm,
          remarkParse,
        ],
        rehypePlugins: [rehypeStringify, [rehypePrettyCode, highlightOptions]],
      }),
    ]
  }
         // ・・・・

jsonで指定

オリジナルのテーマをjsonで設定する場合を考える。
ここではmoonlight-ii.jsonというテーマを適用する。jsonは公式のexampleに載っている。(rehype-pretty-code/examples/next/assets/moonlight-ii.json at master · rehype-pretty/rehype-pretty-code)

まず、importする。

vite.config.ts
import moonlight from "./public/static/assets/themes/moonlight-ii.json";
vite.config.ts
  const highlightOptions = {
    theme: moonlight,
    defaultLang: "plaintext",
  }
tailwind.css
.markdown code:not([data-theme="moonlight-ii"]) {
  @apply font-mono text-sm inline bg-gray-200 text-gray-600 dark:bg-gray-600 dark:text-gray-200 rounded px-1.5 py-1 mx-0.5 tracking-wide;
}

.markdown code[data-theme="moonlight-ii"]{
  @apply font-mono text-sm inline rounded px-1.5 py-1 mx-0.5 tracking-wide;
}

テーマセットからテーマを指定

vite.config.ts
  const highlightOptions = {
    theme:"material-theme",
    grid:false,
    defaultLang: "plaintext",
  }

なんかcode以外のcodeが丸くならんくて微妙

tailwind.css
.markdown code[data-theme="material-theme"] {
  @apply font-mono text-sm inline rounded  py-1 tracking-wide;
}

マルチテーマ

テーマは https://shiki.style/themes#themes でpreviewできる。

vite.config.ts
  const highlightOptions = {
    theme: {
      dark: "solarized-dark",
      light: "solarized-light",
    },
    defaultLang: "plaintext",
  }
tailwind.css
.markdown code[data-theme="solarized-dark solarized-light"] {
  @apply font-mono text-sm rounded tracking-wide;
} 

html:not(.dark) .markdown code[data-theme*="solarized-light"] span {
  color: var(--shiki-light);
  background-color: var(--shiki-light-bg);
}

html.dark .markdown code[data-theme*="solarized-dark"] span {
  color: var(--shiki-dark);
  background-color: var(--shiki-dark-bg);
}

preのpaddigの色が変わるからpaddingをなくす。

.markdown pre {
-  @apply bg-gray-100 rounded p-1;
+  @apply bg-gray-100 rounded p-0;
}
Tomoki OtaTomoki Ota

RSS配信

rssを配信するための機能を追加する

Content-Type

application/rssを使用する
application/rss+xmlだとxmlファイルかrssファイルしか読み込めない
また、application/rss+xmlだとrss.html、application/rssだとrss.txtにビルドされる。

/rssにアクセスした際に、rss.htmlだとxmlにならないが、rss.txtだとxmlで表示される。

app/routes/rss/index.tsx
const feeds = async () => await generatedRssFeed();

export default createRoute(
  async (c) => {
    const rssFeed = await feeds();
    c.header('Content-Type', 'application/rss')
    return c.text(rssFeed, 200);
  }
);
Tomoki OtaTomoki Ota

zennのような折りたたみはMDXに<Accordion>コンポーネントをインポートして実現する

accordion.tsx
type AccordionProps = {
  title: string;
  children: any;
};

export function Accordion({ title, children }: AccordionProps) {
  return (
    <details class="accordion">
      <summary class="accordion-header">{title}</summary>
      <div class="accordion-content">{children}</div>
    </details>
  );
}

以下のようにしてmdxでは書く

foo.mdx
import { Accordion } from "/app/components/parts/accordion"

<Accordion title="タイトル">
foo
</Accordion>
Tomoki OtaTomoki Ota

wrangler cli

wrangler cliがbunとdevcontainerで動作しない場合の対処法。

devcontaier

devcontainerでwrangler loginをすると以下の画面でallowを押したら動作しなくなる。

対処法としては、開発者ツールを開いてallowを押した時に出るcallbackのcURLをコピペする。

devcontainer上で別のタブを開き、コピペしたものを実行すると、成功する。

bun

wranglerはbunを現状サポートしていない。
対処法としては2つ存在する。

  • wranglerだけnpmで実行する
  • 手動でpatchをあてる

手動でpatchをあてる場合は以下のように修正する

node_modules/wrangler/wrangler-dist/cli.js
    function markResourceTiming(timingInfo, originalURL, initiatorType, globalThis2, cacheState) {
-      if (nodeMajor > 18 || nodeMajor === 18 && nodeMinor >= 2) {
-        performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis2, cacheState);
-      }
+      // if (nodeMajor > 18 || nodeMajor === 18 && nodeMinor >= 2) {
+      //   performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis2, cacheState);
+      // }
    }