HP作成ログ

個人HPのテンプレ作成
技術スタック
- SSG
- Hono
- Bun
- Cloudflare Pages

インストール
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
{
+ "type": "module"
}
@hono/vite-dev-server
とかは以下を参考。
- viteとhonoあたりの組み合わせ - 🍊miyamonz🍊
- ブログをAstroからHonoのSSGに移行しました
- honojs/vite-plugins: Vite Plugins for Hono
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",
},
});
{
"scripts": {
"dev": "vite"
},
・・・
}
css
tailwindを使用する。Installation - Tailwind CSS
bun install -D tailwindcss postcss autoprefixer
bunx tailwindcss init
/** @type {import('tailwindcss').Config} */
export default {
+ content: ["./src/**/*.{html,tsx, jsx, ts, js}"],
theme: {
extend: {},
},
plugins: [],
};
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;
bunx tailwindcss -i ./src/input.css -o ./dist/output.css --watch
すると、dist配下にoutput.cssができる。
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>
);
}
なぜかリロードすると読み込みが遅いときがある。
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ファイル
{
"name": "workspace",
"scripts": {
"dev": "vite",
+ "build": "bun run ./build.ts"
},
}
bun run build
を叩くと、static/
ができる。

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" />

global.d.ts
で実行はできるが、vscode上でResponseが見つかりませんというエラーが出る。
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を追加する
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"strict": true,
+ "lib": ["esnext", "dom"],
"types": ["vite/client"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}

デプロイ
github pages
github pagesにデプロイすると、linkとページ遷移が正常に動作しない
linkに関しては、./staticにすれば問題はない。
しかし、ページ遷移は対処方法が不明。viteのhpでは、vite.config.jsにbaseを/repository_name/
と設定すれば解決すると書いていたが、反映されなかった。
cloudflare pages
cloudflare pagesには問題なくデプロイできた。
ただし、wranglerのpreviewがコンテナで動作しない。
wranglerの一部オプションがnpmとpnpmでしかサポートしていない問題がある。

tailwind
ダークモード
darkModeにclassとmediaのどちらかを追加する。mediaの場合は、ユーザが変更することが不可能。
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')
}

cssがページ遷移で読み込めなくなる問題
ページ遷移したときに再レンダリングがかかり、cssが消える。
→cssファイルやjsファイルは、/app/配下に置く
appは/app/としてapp配下以外はstatic/のように指定する。

honox
メモ書き忘れ。(時系列が異なる)
vite使うならhonoxってのがあったからhonoxに移行した。
bun create hono@latest .
x-basicを選択する。
bun i

honox/factory
createRouterヘルパーが入っている
app/server.ts:サーバ側の処理を記述するファイル
showRoutes()
→ honoのサーバー側のルーティング紐づけ
app/routes/ のファイルベースルーティング
default exportの内容をレスポンスとしてブラウザに表示している。
tsx + honox/factoryでcreateRouteヘルパーを活用することで、jsxを利用した開発が可能
2つの書き方がある。下がクライアントレンダリングで、上がサーバサイドレンダリング。getリクエストを送る。
import { createRoute } from 'honox/factory'
export default createRoute((c) => {
return c.render(
<div>
<h1>Hello!</h1>
</div>
)
})
export default function Foo() {
return (
<div>
<h1>Hello!</h1>
</div>
);
}

アイコンを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>
);
};

ある一部のライブラリをインポートした時に、can't find variable: exports
とかcan't find variable: require
とかのエラーが出ていた。
すごいハマったけど、以下のコードがif (mode == production)の中にあったのが原因だった。。。
ssr: {
target: "node",
external: [
"unified",
"@mdx-js/mdx",
"satori",
"@resvg/resvg-js",
"feed",
"budoux",
"jsdom",
],
}
```

型 '{ 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' に割り当てることはできません。
というエラーが出た。
- 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型として定義することで、コンパイラが設定の構造を期待通りに解釈している

Rehype Pretty Code & Shiki
まず、ベースとなる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する。
import moonlight from "./public/static/assets/themes/moonlight-ii.json";
const highlightOptions = {
theme: moonlight,
defaultLang: "plaintext",
}
.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;
}
テーマセットからテーマを指定
const highlightOptions = {
theme:"material-theme",
grid:false,
defaultLang: "plaintext",
}
なんかcode以外のcodeが丸くならんくて微妙
.markdown code[data-theme="material-theme"] {
@apply font-mono text-sm inline rounded py-1 tracking-wide;
}
マルチテーマ
テーマは https://shiki.style/themes#themes でpreviewできる。
const highlightOptions = {
theme: {
dark: "solarized-dark",
light: "solarized-light",
},
defaultLang: "plaintext",
}
.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;
}

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で表示される。
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);
}
);

TOC作成
bun i tocbot rehype-slug
参考文献

TOC作成
bun i tocbot rehype-slug
CSS
目次のスクロールバー
.is-active-link{
font-weight:700
}
.toc-link::before{
background-color:#eee;
content:" ";
display:inline-block;
height:inherit;
left:0;
margin-top:-1px;
position:absolute;
width:2px
}
.is-active-link::before{
background-color:#54bc4b
}
参考文献

うーん。開発環境のみでの事象なので本番環境は問題ないけど、yusukebeさんのmdxの例でも発生してたからダメだ。原因不明。viteのバグかhonoのバグかremarkのバグかすら....

zennのような折りたたみはMDXに<Accordion>コンポーネントをインポートして実現する
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では書く
import { Accordion } from "/app/components/parts/accordion"
<Accordion title="タイトル">
foo
</Accordion>

wrangler cli
wrangler cliがbunとdevcontainerで動作しない場合の対処法。
devcontaier
devcontainerでwrangler login
をすると以下の画面でallowを押したら動作しなくなる。
対処法としては、開発者ツールを開いてallowを押した時に出るcallbackのcURLをコピペする。
devcontainer上で別のタブを開き、コピペしたものを実行すると、成功する。
bun
wranglerはbunを現状サポートしていない。
対処法としては2つ存在する。
- wranglerだけnpmで実行する
- 手動でpatchをあてる
手動でpatchをあてる場合は以下のように修正する
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);
+ // }
}

"markdownlint.config": {
"MD010": {
"spaces_per_tab": 4
},
"MD025": false,
"MD033": false,
"MD034": false
},

robots.txt
robots.txtとは検索エンジンのクローラーに対して、サイトのどの URL にアクセスしてよいかを伝えるもの。クローラビリティに大きく影響する。特に、クローリングされるページ数が多いECサイトなどでは、robots.txt を慎重に設定しなければならない。
robots.txtの書き方を間違えた場合は検索結果に一切表示されなくなる

budoux
いい感じに折り返してくれる。
import { model as jaModel } from "budoux/dist/data/models/ja";
import { Parser } from "budoux/dist/parser";
const parser = new Parser(jaModel);
const splitedTitle = parser.parse(post?.frontmatter.title ?? "");
flex-wrapを使えばいい感じに折り返してくれる(参考 : BudouXとSatoriを使ってタイトルが分かち書きされたOGP画像を出力する。 - return $lock;)
<h1
class={`text-center leading-tight text-3xl mb-0 mt-6 pb-2 font-bold flex justify-center flex-wrap ${titleLen > 20 ? "md:w-[90%]" : ""}`}
>
{splitedTitle.map((phrase, index) => (
<span key={index}>{phrase}</span>
))}
</h1>

Next.jsでの環境変数の扱い
サーバサイド
.env.local
, .env.production
で管理すればいい。
環境変数の呼び出しはprocess.env.FOO
で行う
もしくは、以下のようにしても可能。
const foo = 'FOO'
console.log(process.env[foo])
クライアントサイド
process.env.FOO
だと呼び出せない(undefinedになる)
環境変数の定義でprefixにNEXT_PUBLIC_
をつけると呼び出すことができる。
NEXT_PUBLIC_FOO
を.envファイルに定義して、process.env.NEXT_PUBLIC_FOO
とすると呼び出せる。
ちなみに、以下の書き方はクライアントサイドでは不可能。
const foo = 'FOO'
console.log(process.env[foo])

Next.js多言語化対応
app routerでの多言語化対応(v15.0.3)
middlewareで設定する。next.config.tsを使用しない!
Domain Routing
Sub-path Routing
参考文献

middleware
middleware.tsはsrc/配下に置く

画像配信
背景
amplifyは4.3MBまで。jpgであげたら、image/nextでwebpに変換されずjpegになっている。
Cloudinary
Cloudinary を使えば爆速で配信できる。
Cloudfront + Lambda@Edge
Cloudinaryは従量課金制でコストが高くなる可能性があるので、 Cloudfront + Lambda@Edgeを使用する。

inline code
インラインコードと普通のコードブロックの違い
<span data-rehype-pretty-code-figure="">
<code data-language="plaintext" data-theme="material-theme-palenight everforest-light"
style="--shiki-dark:#babed8;--shiki-light:#5c6a72;--shiki-dark-bg:#292D3E;--shiki-light-bg:#fdf6e3">
<span data-line="">
<span>foo</span>
</span>
</code>
</span>
<figure data-rehype-pretty-code-figure="">
<pre style="
--shiki-dark: #babed8;
--shiki-light: #5c6a72;
--shiki-dark-bg: #292d3e;
--shiki-light-bg: #fdf6e3;
" tabindex="0" data-language="sh" data-theme="material-theme-palenight everforest-light">
<code data-language="sh" data-theme="material-theme-palenight everforest-light" style="display: grid">
<span data-line="">
<span style="--shiki-dark: #ffcb6b; --shiki-light: #8da101">foo</span>
</span>
</code>
</pre>
</figure>
styleとかわかりやすく消してみると、インラインはspan
タグ内にcode
タグがあって、コードブロックはfigure
タグとpre
タグの中にcode
タグがある。
<span data-rehype-pretty-code-figure="">
<code data-language="plaintext" data-theme="everforest-light">
<span data-line="">
<span>foo</span>
</span>
</code>
</span>
<figure data-rehype-pretty-code-figure="">
<pre tabindex="0" data-language="sh" data-theme="everforest-light">
<code data-language="sh" data-theme="material-theme-palenight everforest-light" style="display: grid">
<span data-line="">
<span>foo</span>
</span>
</code>
</pre>
</figure>