Skip to content

📅 2025-02-05 | 🔄 2025-03-09

先人の知恵を借りまくってMarkdownで書けるブログを作る

#other


先人たちの知恵[1][2]を大いに借りながら、このブログを作成し、Cloudflare Pagesにデプロイした時の作業記録。 といいつつ作って後から書いたので、抜けがあるかも。

ソースコードはGithubにもあるので、細かいところはそちらを。

参考にしたサイト様

VitePressの準備

Getting Startedに沿ってVitePressをセットアップする。

shell
bun install -D vitepress

VitePressの初期設定をする。configやmdファイルなどを置く場所は./docsにして、TitleとDescriptionは適当、他はデフォルトにした。

shell
bun vitepress init

ディレクトリ構成

posts/<year>の下に、<MMDD-title>というフォーマットでディレクトリを作って記事を書くことにする。

.
├── docs
│   ├── posts
│   │   ├── <year>
│   │   │   └── <MMDD-title>
│   │   │       ├── index.md
│   │   │       └── ...

Tailwind CSSを入れる

Tailwind CSSを使いたいので入れる。 どうやら最近(2025-1-22)にv4.0.0が出たようで、インストール体系が変わったぽい。 Web初心者には難しかったので今回はv3系を使う。時間があるときに勉強してv4にしようかな。

shell
bun install -D [email protected] autoprefixer postcss

次に各種設定ファイルを書く。

js
module.exports = {
    plugins: {
        tailwindcss: {},
        autoprefixer: {},
    },
}
js
export default {
    content: [
        './docs/index.md',
        './docs/**/index.md',
        './.vitepress/**/*.{js,ts,vue}'
    ],
    theme: {
        extend: {},
    },
    plugins: [],
    important: true,
}
css
@tailwind base;
@tailwind components;
@tailwind utilities;
ts
import DefaultTheme from 'vitepress/theme'
import './custom.css'

export default DefaultTheme

これでTailwind CSSの導入はおわり。

Markdown-it の Pluginを入れる

VitePressはMarkdown-it使っている[3]ので、色々なプラグインを入れて機能拡張できる。

Footnote[4]

sh
bun install -D markdown-it-footnote

config.mtsに以下を追記

ts
import markdownItFootnote from 'markdown-it-footnote'

export default defineConfig({
    markdown: { 
        config: (md) => { 
            md.use(markdownItFootnote) 
        } 
    }, 
})

Vitepress Plugin Group Icons[5]

::: code-groupにアイコンをつけたり、::: code-group以外でもCode BlockにTitle Barをつけたりできる。

shell
bun install -D vitepress-plugin-group-icons

このプラグインは、importしてmd.useする以外にもviteの設定やthemeのindex.tsへの追記が必要。

ts
import {groupIconMdPlugin, groupIconVitePlugin} from 'vitepress-plugin-group-icons'

export default defineConfig({
    markdown: {
        config: (md) => {
            md.use(groupIconMdPlugin) 
        }
    },


    vite: { 
        plugins: [ 
            groupIconVitePlugin()  
        ] 
    }, 
})
ts
import 'virtual:group-icons.css'

LaTeX

まずはmarkdown-it-mathjax3をインストール。

shell
bun install -D markdown-it-mathjax3

そしてconfig.mtsを編集。

ts
export default {
  markdown: {
    math: true
  }
}

なんとこれだけでOK。

VitePressのPlugin達を入れる

Git-based page histories

https://nolebase-integrations.ayaka.io/pages/en/integrations/vitepress-plugin-git-changelog/configure-vite-plugins

VitePressのConfigとthemeのconfig両方をいじる必要がある。

自分で使う場合は、mapAuthorsのそれぞれの項目を変更すると良い。 GitHubの場合、アイコンの生URLはプロファイルページのアイコン画像から取得できるはず。 その他それぞれの設定の意味などはPluginの公式Documentを参照。

shell
bun install -D @nolebase/vitepress-plugin-git-changelog
ts
import { GitChangelog, GitChangelogMarkdownSection } from '@nolebase/vitepress-plugin-git-changelog/vite'

export default defineConfig({
    vite: {
        plugins: [
            GitChangelog({ 
                repoURL: () => "https://github.com/bqc0n/blog", 
                mapAuthors: [{ 
                    avatar: "https://avatars.githubusercontent.com/u/89625049?v=4", 
                    name: "bqc0n", username: "bqc0n", 
                    mapByNameAliases: ["bqc0n"], 
                }] 
            }), 
            GitChangelogMarkdownSection({ 
                exclude: (id) => id.endsWith('index.md'), 
            }), 
        ],
    },
})
ts
import DefaultTheme from 'vitepress/theme'
import './custom.css'
import 'virtual:group-icons.css'
import { NolebaseGitChangelogPlugin } from "@nolebase/vitepress-plugin-git-changelog/client"
import '@nolebase/vitepress-plugin-git-changelog/client/style.css'

export default DefaultTheme 
export default { 
    ...DefaultTheme, 
    enhanceApp({app}) { 
        app.use(NolebaseGitChangelogPlugin) 
    } 
} 

vitepress-plugin-lightbox

https://github.com/BadgerHobbs/vitepress-plugin-lightbox

shell
bun install -D vitepress-plugin-lightbox

画像をクリックしたら拡大するやつ。

config.mtstheme/index.tsへの追記、theme/Layout.vueの作成が必要。

ts
import lightbox from "vitepress-plugin-lightbox"
export default defineConfig({
    markdown: {
        config: (md) => {
            md.use(lightbox) 
        }
    },
})
ts
import Layout from "./Layout.vue"; 

export default {
    ...DefaultTheme,
    Layout, 
}
vue
<script setup>
import DefaultTheme from "vitepress/theme";
import { onMounted } from "vue";
import { useRouter } from "vitepress";
import mediumZoom from "medium-zoom";

const { Layout } = DefaultTheme;
const router = useRouter();

// Setup medium zoom with the desired options
const setupMediumZoom = () => {
  mediumZoom("[data-zoomable]", {
    background: "transparent",
  });
};

// Apply medium zoom on load
onMounted(setupMediumZoom);

// Subscribe to route changes to re-apply medium zoom effect
router.onAfterRouteChanged = setupMediumZoom;
</script>

<template>
  <Layout />
</template>

<style>
.medium-zoom-overlay {
  backdrop-filter: blur(5rem);
}

.medium-zoom-overlay,
.medium-zoom-image--opened {
  z-index: 999;
}
</style>

Comment With Giscus

https://giscus.app/ja

GitHub Discussionsを利用したコメントシステム。利用するには、リポジトリがPublicである必要があるので注意。

GitHub Discussionsの有効化

このガイドに従ってリポジトリのDiscussionsを有効化する。

Giscus Appのインストール

https://github.com/apps/giscus をリポジトリにインストールする。

vitepress-plugin-comment-with-giscusのインストール

https://giscus.app/ja で設定すべき項目の内容を取得できる。

shell
bun install -D vitepress-plugin-comment-with-giscus

theme/index.tsに追記

.vitepress/theme/index.ts
ts
import giscusTalk from 'vitepress-plugin-comment-with-giscus'; 
import { useData, useRoute } from 'vitepress'; 
import { toRefs } from "vue"; 

export default {
    // ...
    setup() { 
        const { frontmatter } = toRefs(useData()); 
        const route = useRoute(); 
        giscusTalk({ 
                repo: 'bqc0n/blog', 
                repoId: 'R_kgDONyqpCg', 
                category: 'Announcements', 
                categoryId: 'DIC_kwDONyqpCs4Cmvcv', 
                mapping: 'title', 
                inputPosition: 'top', 
                lang: 'jp', 
                locales: { 
                    'ja-JP': 'ja', 
                    'en-US': 'en'
                }, 
                homePageShowComment: false, 
                lightTheme: 'light', 
                darkTheme: 'transparent_dark', 
            }, { 
                frontmatter, route 
            }, 
            true, 
        ); 
    } 
}

config.mtsで追加の設定

  • lastUpdated: true: ページの下にちっちゃく最終更新日が表示される
  • cleanUrls: true: URL末尾の.htmlが消える。デプロイ先によっては追加の設定が必要かもしれないが、今回はそのままで動いてくれた。

トップページの作成

トップページには記事一覧を表示したいので、まず記事一覧を取得するためにBuild-Time Data Loadingという機能を利用する。

.vitepress/posts.data.mts
ts
import { createContentLoader } from 'vitepress';

export default createContentLoader('posts/**/index.md', {
    includeSrc: false,
    transform(rawData) {
        return rawData
            .filter(page => page.url != "/posts/")
            .sort((a, b) => +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date) )
    }
});

またcustom.cssに以下を追記。

shell
.home-posts-article {
    border-top: 1px solid var(--vp-c-divider);
    padding: 10px 0;
}

momentをインストール。

shell
bun install -D moment

トップページを作成する。

md
---
title: Home
layout: doc
next: false
prev: false
---

<script setup>
import { data as posts } from '.vitepress/posts.data.mts';
import moment from 'moment';
</script>

# blog.bqc0n.com

個人的な備忘録集です。記事の内容は、[MIT-License](https://github.com/bqc0n/blog/blob/main/LICENSE)で利用可能です。

<article v-for="post of posts" class="home-posts-article">
  <a :href="post.url" class="block text-inherit no-underline hover:underline">
    <p class="text-2xl">{{ post.frontmatter.title }}</p>
    <p class="text-sm text-gray-500">{{ moment(post.frontmatter.date).format('YYYY-MM-DD') }}</p>
    <p>{{ post.frontmatter.description }}</p>
  </a>   
</article>

記事一覧ページの作成

トップページと同じように記事一覧ページも作る。

md
---
title: Posts | blog.bqc0n.com
layout: doc
next: false
prev: false
---
<script lang="ts" setup>
import { data as posts } from "../.vitepress/posts.data"
import moment from 'moment';
</script>

# 記事一覧ページの作成

<ul>
    <li v-for="post of posts">
        <a :href="post.url" class="font-semibold text-lg">{{ post.frontmatter.title }}</a>
        <span class="text-sm"> - {{ moment(post.frontmatter.date).format('YYYY-MM-DD') }}</span>
    </li>
</ul>

こんな感じになる。 blog-posts-preview

タグページの作成

Dynamic Routesを使って、build時にタグページを動的に作成するようにする。

Dynamic Routesは、[placeholder].mdのように[]を名前に含むファイルやディレクトリを作ることで、 placeholderに文字列を代入して動的にページを作成できる機能である。

ディレクトリ構造はこのようにする。

docs
└── tags
    ├── [tag]
    │   ├── index.md
    │   └── index.paths.js
    └── index.md

次にplaceholderに代入される文字列を作成する Paths Loader File を作成する。 tag:[tag]に代入される文字列である。content:はmdファイルの<!-- @content -->に展開される文字列で、これを利用して ページの内容を動的に変化させることができる。

js
import fs from 'fs'
import { globSync } from 'glob'

var tags = {}

var files = globSync("docs/posts/**/index.md");

files.forEach(file => {
    var data = fs.readFileSync(file, 'utf8');
    var found = data.match(/^tags:\s*\[(.+)]\s*$/m)
    if (found) {
        found[1].split(",")
            .map(tag => tag.replaceAll('"', '') )
            .forEach(tag => tags[tag.replaceAll(' ', '')] = tag )
    }
});

export default {
    paths: () => {
        return Object.keys(tags).map((key) => {
            return { params: { tag: key }, content: `# タグ: ${tags[key]}`}
        })
    }
}

[tag]/index.mdも作成

md
---
next: false
prev: false
---

<script setup>
import { useData } from 'vitepress'
import { data as posts } from '../../.vitepress/posts.data.mts'

const { params } = useData()
const current_tag = params.value.tag

var pages = []
posts.forEach(post => {
    if (post.frontmatter.tags){
        var tags = post.frontmatter.tags.map(tag => tag.replaceAll(" ", "") )
        if (tags.includes(current_tag)) pages.push(post) 
    }
})

</script>

<!-- @content -->

<ul>
  <li v-for="page of pages">
    <a :href="page.url">{{ page.frontmatter.title }}</a>
  </li>
</ul>

これでタグページの作成も完了。

共通ヘッダー

記事の投稿日、最終更新日、付けられたタグを表示する。

vue
<script setup>
  import { useData } from 'vitepress'
  import moment from 'moment';

  const { frontmatter } = useData()
  const lastUpdated = moment(useData().page.value.lastUpdated).format('YYYY-MM-DD');
  const date = moment(frontmatter.value.date).format('YYYY-MM-DD');
</script>

<template>
  <div class="vp-doc">
    <p>
      <span>📅 {{ date }}</span> | <span>🔄 {{ lastUpdated }}</span>
    </p>
    <h1>{{ frontmatter.title }}</h1>
    <p>
      <a v-for="tag in frontmatter.tags" :href="'/tags/' + encodeURIComponent(tag.replaceAll(' ', '')) + '/'"> #{{ tag }} </a>
    </p>
  </div>
</template>
vue
<script setup>
// ...  
import {useRouter} from "vitepress"; 
import {useData, useRouter} from "vitepress"; 
import PostHeader from "./PostHeader.vue"; 
// ...
</script>

<template>
  <Layout /> 
  <Layout>  
    <template #doc-before> 
      <PostHeader /> 
    </template> 
  </Layout> 
</template>

SEO対策 (WIP)

とりあえずサイトマップの生成だけやってみる。

.vitepress/config.mts
ts
export default defineConfig({
    sitemap: { 
        hostname: "https://blog.bqc0n.com"
    }, 
})

デプロイ

ここまで出来たら、いよいよCloudflare Pagesにデプロイする。

INFO

Cloudflare Dashboardの仕様はよく変わるので、その時は都度読み替えてください。

GitHubとの連携は済ませてある前提とする。

Cloudflare Dashboardから、Workers & Pages -> 右上の作成ボタン -> Pages -> Gitと遷移する。

cloudflare-git-repo

blogのリポジトリを選択しセットアップ開始。

ビルドコマンドにはbun run docs:build、ビルド出力ディレクトリにはdocs/.vitepress/distを設定する。 加えて、環境変数にNODE_VERSION=23.7.0を追加しておく。

cloudflare-build-settings

「保存してデプロイ」を押すとビルドが始まる。

ビルドが終わったら、Pagesからblogの設定ページを開いて、カスタムドメインを追加する。

cloudflare-pages-domain

以上でデプロイ完了、ブログ完成。


  1. https://nshmura.com/posts/migration-to-vitepress/ ↩︎

  2. https://blog.hakuteialpha.com/posts/vitepress-blog/ ↩︎

  3. https://vitepress.dev/guide/markdown#advanced-configuration ↩︎

  4. https://github.com/markdown-it/markdown-it-footnote ↩︎

  5. https://vpgi.vercel.app ↩︎

Released Under the MIT License