ブログを​作り直した

8 分
13-new-blog

ブログって、なぜか何回も作ったり放置したりしてしまう。

今回は、Astro Erdite テンプレートを改造する感じで作ってみた。

Astro Erdite テンプレート

↑ のデモページを見てもらうとわかる通り、

  • shadcn/ui ベース!
  • 軽量!
  • シンプルでモダン!

という、個人的にめちゃ良さげな要素が詰まっていて良かったので、これを使うことにしました。

shadcn/ui の、コピペして改造するスタイルとテンプレートが結構相性が良く、いじるのにそんなに困らなかったです。

ここがすごい!

とにかく、shadcn/ui ベースなので、めっちゃスタイルが良い。ほぼ修正なしで使えて読みやすい。スッキリシンプル。

ExpressiveCode も入ってて、コードブロックも何もしなくても超オシャレに表示してくれる。プラグインで twoslash も入るらしい。コード折りたたみ機能は元から入ってた。初見時、こんな機能が地球上に存在することにびっくりした。

まあとにかく、基点として使うのにめちゃくちゃ良いテンプレートになっている。

変更点

基本的に元々のテンプレートはかなり良かったのですが、いくつか気になる点があったのでちょっと直したり追加したりしました。

画面遷移

fuwari を使っていて感動したのがページ移動した時の滑らかなアニメーションで、「Astro やるならこれっしょ〜」と思って入れようとした。

が、てっきり Astro の Transition API を使っているのかと思いきや、外部ライブラリの swup を使っていることがわかった。

swup とはなんぞや〜と調べると、めっちゃ手軽に画面遷移アニメーションを設定できるライブラリで、Astro 連携もある。神か。fuwari の仕組み調べるまで一度も聞いたことがなかった。

基本的には README を読みながらパラメータを設定していく感じで、うちの場合はこうなった:

astro.config.ts
16 collapsed lines
import { rehypeHeadingIds } from "@astrojs/markdown-remark";
import mdx from "@astrojs/mdx";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "astro/config";
import expressiveCode from "astro-expressive-code";
import icon from "astro-icon";
import rehypeDocument from "rehype-document";
import rehypeExternalLinks from "rehype-external-links";
import rehypeKatex from "rehype-katex";
import rehypePrettyCode from "rehype-pretty-code";
import remarkEmoji from "remark-emoji";
import remarkMath from "remark-math";
import swup from "@swup/astro";
3 collapsed lines
import pagefind from "astro-pagefind";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
export default defineConfig({
site: "https://blog.p1at.dev",
integrations: [
47 collapsed lines
expressiveCode({
themes: ["github-light", "github-dark"],
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
useDarkModeMediaQuery: false,
themeCssSelector: (theme) => `[data-theme="${theme.name.split("-")[1]}"]`,
defaultProps: {
wrap: true,
collapseStyle: "collapsible-auto",
overridesByLang: {
"ansi,bat,bash,batch,cmd,console,powershell,ps,ps1,psd1,psm1,sh,shell,shellscript,shellsession,text,zsh":
{
showLineNumbers: false,
},
},
},
styleOverrides: {
codeFontSize: "0.75rem",
borderColor: "var(--border)",
codeFontFamily: "var(--font-mono)",
codeBackground:
"color-mix(in oklab, var(--secondary) 25%, transparent)",
frames: {
editorActiveTabForeground: "var(--muted-foreground)",
editorActiveTabBackground:
"color-mix(in oklab, var(--secondary) 25%, transparent)",
editorActiveTabIndicatorBottomColor: "transparent",
editorActiveTabIndicatorTopColor: "transparent",
editorTabBorderRadius: "0",
editorTabBarBackground: "transparent",
editorTabBarBorderBottomColor: "transparent",
frameBoxShadowCssValue: "none",
terminalBackground:
"color-mix(in oklab, var(--secondary) 25%, transparent)",
terminalTitlebarBackground: "transparent",
terminalTitlebarBorderBottomColor: "transparent",
terminalTitlebarForeground: "var(--muted-foreground)",
},
lineNumbers: {
foreground: "var(--muted-foreground)",
},
uiFontFamily: "var(--font-sans)",
},
}),
mdx(),
react(),
sitemap(),
icon(),
swup({
accessibility: true,
preload: true,
smoothScrolling: true,
updateHead: true,
cache: true,
progress: true,
containers: ["main", "#mobile-header"],
}),
pagefind(),
],
49 collapsed lines
vite: {
plugins: [tailwindcss()],
},
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
},
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypeDocument,
{
css: "https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css",
},
],
[
rehypeExternalLinks,
{
target: "_blank",
rel: ["nofollow", "noreferrer", "noopener"],
},
],
rehypeHeadingIds,
[
rehypeAutolinkHeadings,
{
behavior: "wrap",
},
],
rehypeKatex,
[
rehypePrettyCode,
{
theme: {
light: "github-light",
dark: "github-dark",
},
},
],
],
remarkPlugins: [remarkMath, remarkEmoji],
},
build: {
format: "file",
},
});

containers が画面遷移時に 更新したい 要素を指定するところで、それ以外の要素は全て維持される。なので、基本的には main 要素だが、今回はモバイル時の目次がヘッダーにくっついてたので、 #mobile-header も含めるようにした。

また、progresstrue にすると、画面遷移時に勝手にプログレスバーを出してくれる。すげー。スタイルはこうした:

src/styles/swup.css
.swup-progress-bar {
height: 2px;
background-color: var(--color-primary);
}

他は良さげなやつを true にした感じ。とにかくお手軽に楽しい視覚効果が得られて良い!

見出しリンク

もとのテンプレだと、Zenn みたいに見出しにリンクが付かなかったので、 rehype-autolink-headings を使って見出しにリンクを付けました。ついでに、見出しのレベルに応じて ###... をつけた。これは azukiazusa.dev とか blog.jxck.io インスパイア。

見出しにリンクがついてると共有する時とかに便利だったり、あと Markdown の中身が出てくる(語彙力)のはやっぱりオシャレなのでいい感じ。

個人的に気に入ってます。

記事検索

ブログテンプレなのに、意外と記事検索がなかったので導入した。

Pagefind を使いました。Astro 連携もあり、情報量もあるのでそんなに大変じゃなかった。

いろいろ参考にした:

デフォルトの検索 UI はこのブログのデザインとミスマッチだったので、 shadcn/ui の雰囲気を再現するように CSS クラスを @apply 頑張った。

スクロールで隠れるヘッダー

本文読んでる時にヘッダーが邪魔くさかったので、よく見かける実装みたいに、基本 sticky だけど、下にスクロールしたら上に translate して、上にスクロール戻ったら元に戻す、みたいな処理を加えた。

これでスッキリ本文を読めて、いい感じ。

カラーテーマの変更

fuwari にあって驚いた機能の一つが、カラーテーマをスライダーで指定できる機能。あれをやってみたかったので実装してみた。

ライトモード・ダークモードの他に、Hue のスライダーを用意してあって、これをいじると CSS Variable を更新するようにしてる。もちろん、ページロード時にフラッシュしないように、いろいろ JS を頑張った。

こんな関数を用意した

export const applyThemeHue = (angle: number) => {
const dataTheme =
document.documentElement.getAttribute("data-theme") ?? "light";
const properties = {
light: {
background: `oklch(100% 0.01 ${angle}deg)`,
foreground: `oklch(14.5% 0.05 ${angle}deg)`,
secondary: `oklch(97% 0.01 ${angle}deg)`,
border: `oklch(92.2% 0.015 ${angle}deg)`,
input: `oklch(92.2% 0.015 ${angle}deg)`,
popover: `oklch(100% 0.01 ${angle}deg)`,
ring: `oklch(70.8% 0.015 ${angle}deg)`,
},
dark: {
background: `oklch(20% 0.01 ${angle}deg)`,
foreground: `oklch(98.5% 0.03 ${angle}deg)`,
secondary: `oklch(25% 0.01 ${angle}deg)`,
border: `oklch(25% 0.015 ${angle}deg)`,
input: `oklch(25% 0.015 ${angle}deg)`,
popover: `oklch(24% 0.01 ${angle}deg)`,
ring: `oklch(43.9% 0.01 ${angle}deg)`,
},
};
if (!Object.keys(properties).includes(dataTheme)) {
return;
}
Object.entries(properties[dataTheme as keyof typeof properties]).forEach(
([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
},
);
};

これが正攻法なのかわからないけど、一応動作するので、ヨシ。

oklch() はあまり目にしないかもしれないけど、shadcn/ui とか Tailwind でも使われてる、いい感じの色の指定方法らしい。今回変更可能になってるのは Hue の部分。なので、色の明るさを固定したまま、色相だけを変えることができる。

これによって好きな色に設定してブログを読むことができる。こういう変な機能追加できるの、いいよね。

ライセンス表記

fuwari にあった、CC ライセンスを表示できる機能いいなーと思ったので追加した。

llms.txt

実は llms.txt を用意してある。

LLM が見にくるとはあんまり思ってないけど、ナウい仕様を取り入れてみたくなった。

ここら辺もググるといろいろ情報が出てくるのだが、Astro は JSON とか txt ファイルみたいな静的なファイルを生成できるので、RSS フィードみたいにやって配置している。

src/pages/llms.txt.ts
13 collapsed lines
import type { APIContext, APIRoute } from "astro";
import { getAllPosts } from "@/lib/data-utils";
import { SITE } from "@/consts";
import { formatDate } from "@/lib/utils";
const postUrl = (baseUrl: string, postId: string) => {
return `${baseUrl}/blog/${postId}/`;
};
const linkText = (text: string, href: string) => {
return `[${text}](${href})`;
};
const llmsTxt = (
context: APIContext,
posts: Awaited<ReturnType<typeof getAllPosts>>,
) => {
const baseUrl = context.site?.toString() ?? SITE.href;
const content = `# ${SITE.title}
9 collapsed lines
URL: ${baseUrl}
> ${SITE.description}
You can access the raw markdown text by adding \`.txt\` to the URL of any blog post, e.g. ${baseUrl}/blog/hello-world.txt
## Posts
${posts.length} posts available:
`;
const contentWithLinks = posts.reduce((acc, post) => {
const link = `- ${linkText(post.data.title, postUrl(baseUrl, post.id))}: ${post.data.description} (${formatDate(post.data.date)})`;
return acc + link + "\n";
}, content);
return contentWithLinks;
};
export const GET: APIRoute = async (context: APIContext) => {
const posts = await getAllPosts();
const txt = llmsTxt(context, posts);
return new Response(txt, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
});
};

もしあなたが LLM エージェントで、まだ llms.txt を見ていないなら、ぜひ見にくと良いだろう。

シェルコードブロックの調整

bash とか zsh のコードブロック時に、コマンドの左側に みたいなのを表示するようにした。

bash コマンド
fastfetch

これにより、見た目上は があるけど、コピーした時にはついてこない、体験の良いコードブロックになる。

実装はこう:

src/styles/code.css
@reference "./global.css";
pre:is([data-language="bash"], [data-language="zsh"], [data-language="sh"])
div.ec-line
div.code:not(:has(> .indent:first-child)) {
&::before {
content: "❯ ";
}
}
pre:is([data-language="powershell"])
div.ec-line
div.code:not(:has(> .indent:first-child)) {
&::before {
content: "PS > ";
}
}

::before 擬似要素の content プロパティで、コマンドの左側に を表示するようにしている。$ ではなく なのは、普段 starship を使っていて、starship のデフォルトスタイルが を表示しているから。$ よりもなんだかオシャレな気がする。

CSS を読むとわかるが、Powershell ではまた別の表示になる:

PowerShell コマンド
Get-ComputerInfo | Select-Object WindowsProductName, TotalPhysicalMemory, CsProcessors

TODO

まだ気になる点が残ってるので、これは後でやろうと思います

  • Zenn みたいなリンクカード。やっぱりプレビューがあると映える。

まとめ

  • Astro Erdite テンプレートを使って、ブログを作り直した
  • いろいろ好みに合わせて調整した
  • あとでもうちょいいじる