me

Ryosuke Nishida

hosi_mo / ほしーもさん

テクニカルディレクター @ ライトフライヤースタジオ

C++ / C# | Game Development
Script / Camera / Graphics / DataSchema

週末は昼からビール飲んで寝てたい
お料理と相撲とお風呂がすき

個人サイトの引越しとWordPressのサルベージログ


何年もサーバをレンタルし放置していた壊れかけの個人サイトとWordPressブログ(!)

学生の頃に作ったんだったっけな…覚えていない。

いつまでもサーバ費用を払い続ける現状を変えるべく、全体的なリニューアルと静的サイトへの移行、記事のサルベージをしました。年末のインターネット大掃除です。

今回は、マークダウンから静的サイト生成をということでAstroを採用してみました。結果的に、よいサイズの週末プログラミング遊びとなりました。

(また何年かして、Astroってなんだっけ?と自分がなりそうなので、移行作業のログとして将来の自分のために残しておく:)

要件

  • マークダウンで記述

  • マークダウンをGithubにpushして記事公開

  • Github PagesとGithub Actionsで、できる限り楽をしたい

  • 静的サイトがよい

  • 過去の記事をサルベージしたい

技術選定

Github Pagesにお任せしたいので、ホスティング先は現在のさくらインターネットからGithubに乗り換えでよい。

マークダウンからhtmlを吐き出すのは…pythonのライブラリなどいろいろ出ているのは確認したが、ブログっぽい動きをさせるために、わざわざデータ構造を独自で拵えるのは少しマッチョすぎる。

インターネッツでいろんな方の移行記事をみてみるに、マークダウンを採用した静的サイトジェネレータとしてはAstro https://astro.build が技術者界隈だと比較的選択されている気配を感じた。少し触ってみたところ、機能がそこまで多くなく高速で、私にとってはカスタマイズしやすく、かゆいところに手が届くAPI構成なのでこちらを採用。

平たく言うと、マークダウンを飲み込んでよしなにhtmlを吐き出してくれるコードジェネレータ。画像のwebp化もビルド時にやってくれるのでアウトプットもモダンで軽いようです。

移行作業

WordPressのデータサルベージとマークダウンへの変換

まずは旧サイト(WordPress)から Export mediaで全ての画像をzip化してダウンロード。DB内の記事データはxmlで受け取れた。これらをローカルに置いておいて、これらをよしなにマークダウンへ整形する

マークダウンへの変換は wordpress-export-to-markdown を採用し、いくつかオプションを指定

移行先予定のAstroでは、投稿年月などをフォルダ分けせず 平置きしてマークダウンを並べておきたい。画像に関してはどうせ相対パスがめちゃくちゃになる想定なので保存オプションは外しておく

npx wordpress-export-to-markdown

移行先のAstroとはマークダウンのdateやアイキャッチ画像の指定が少し違っていたので、その辺りの整形はAIにスクリプト書いてもらって一括変換。ついでに画像の相対パスも直してもらうなど(こういうときのAIはとてもたすかる…)



Astroのカスタマイズをしてみる

npm create astro@latest

これを叩くとプロジェクトの新規作成ウィザードが立ち上がってくれる

tmpl   How would you like to start your new project?
         ○ A basic, helpful starter project 
         ● Use blog template 
         ○ Use docs (Starlight) template 
         ○ Use minimal (empty) template 

はじめは、A basic, helpful starter projectを選択して中身を見たのですが、週末すぐブログをでっち上げるには必要な情報にアクセスするのが難儀で頓挫しました。

もういちどプロジェクトを作り直して、上から2番目のブログテンプレートを引っ張ってくると、なるほど技術者ならば大まかな構成はこれで飲み込めるちょうどいいサイズのシンプルなブログが出力されていました。

この構成をもとに、自前のデザインとサイト構成に合わせてカスタマイズしていきましょう



めざすページ構成

シンプルな3ページのみで構成。ページの種類が増えるとメンテナンスコストも増えそうだし…

  • Home兼プロフィールページ(いわゆるAboutページ
  • Blogページ
    • indexページ
    • 記事ページ

どうやら .astroファイルが htmlとtsで構成された本丸なので、こいつをコネコネすればよい

基本的に.astroとglobal.cssをいじって、ローカルホストで見た目を逐一確認する形になるかと思います

以下で確認できます

npx astro dev

最終的な構成は以下でいきました

.
├── assets
│   ├── 記事画像たち.jpg
│   └── プロフィール画像たち.jpg
├── components
│   ├── BaseHead.astro
│   ├── Footer.astro
│   ├── FormattedDate.astro 
│   ├── Header.astro
│   └── HeaderLink.astro
├── consts.ts
├── content
│   └── blog
│       ├── 2025-12-20-記事たち.md
│       └── 2025-12-23-記事たち.md
├── content.config.ts
├── layouts
│   └── BlogPost.astro
├── pages
│   ├── blog
│   │   ├── [...slug].astro
│   │   └── index.astro
│   ├── index.astro
│   └── rss.xml.js
└── styles
    └── global.css

デザインなどは、私のように よしなに自前で作るか、面倒であればインターネットからテンプレート持ってくるのが良いかと思います。

少し特殊なカスタマイズ

OGP対応

ブログ記事ではよくある、Facebookなどにブログ記事のurlが貼り付けられると画像が自動で表示されるアレを組み込みます

OGP(Open Graph Protocol)は、SNSでシェアされた際にページ情報(タイトル、URL、概要、画像)を正しく伝えるためのHTMLタグです。

今回のケースではBlogPost.astroを個別記事の親元テンプレートにしているので、ここのBaseHeadタグで画像情報をインジェクションして伝えてあげて

---
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import FormattedDate from '../components/FormattedDate.astro';
import Header from '../components/Header.astro';

type Props = CollectionEntry<'blog'>['data'];

const { title, description, pubDate, updatedDate, heroImage} = Astro.props;
---

<html lang="en">
   <head>
      <BaseHead title={title} description={description} metaImage={heroImage}/>
   </head>

受け取った画像をBaseHead.astro側で受け取ります

---
// Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component.
import '../styles/global.css';
import type { ImageMetadata } from 'astro';
import FallbackImage from '../assets/デフォルトのSNS用アイキャッチ画像.jpg';
import { SITE_TITLE } from '../consts';

interface Props {
   title: string;
   description: string;
   metaImage?: ImageMetadata; //個別の記事からinjectionして画像データがあればmetaImageに格納されている
}

const canonicalURL = new URL(Astro.url.pathname, Astro.site);

let { title, description, metaImage } = Astro.props;

if (!metaImage) {  
   metaImage = FallbackImage;
}

BaseHead.astroのhtmlタグのところで、個別記事の画像urlを教えてあげる

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(metaImage.src, Astro.url)} />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(metaImage.src, Astro.url)} />

Facebookに試しに投稿して表示されるのを確認。うむ

site_renew_sample3



最新の記事一覧

トップページがプロフィールページになるので、最新のブログ一覧をどこかに忍ばせてあげたい気持ちでちょっとだけいじる

// src/pages/index.astro
import { getCollection } from 'astro:content';

const allPosts = await getCollection('blog');
const sortedPosts = allPosts.sort((a, b) =>
  b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
const recentPosts = sortedPosts.slice(0, 10);

index.astroでブログコレクションを取得して日付順にソートしたあと、10件だけ保持しておいて

<section class="section">
<h2>最近のブログ</h2>
<ul class="blog-list">
   {recentPosts.map((post) => (
         <li class="blog-item">
             <a href={`/blog/${post.id}/`}>
   {post.data.heroImage && (
      <div class="blog-thumbnail">
         <Image width={720} height={360} src={post.data.heroImage} alt="" />
      </div>
   )}
<div class="blog-info">
<h4 class="blog-title">{post.data.title}</h4>
<p class="blog-date">
<FormattedDate date={post.data.pubDate} />
</p>
</div>
</a>
</li>
   ))}
</ul>
</section>

表示したいところに挿入するだけ

site_renew_sample1こんな感じに表示される

デプロイ

repositoryの.github/workflows/deploy.yml などデプロイ用のワークフローを定義するだけ

name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v4
      - name: Install, build, and upload your site
        uses: withastro/action@v3

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

今回は独自ドメインなのでドメイン管理側のAレコードを埋めてあげて

(IPは以下のサイトから取得可能)

https://docs.github.com/ja/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site

Github Pages側でドメイン入力して少し待てば完了

site_renew_sample2

おしまい

動いてるのか動いてないのかわからないほど放置していたWordPressから、マークダウン記述式の静的サイトへの置き換えがひとまず完了です。マークダウンをpushすれば自動でデプロイされるのが良いところですね。

Github PagesのプライベートRepositoryのPages化はProアカウントが必要とのことで、今のところはドメイン維持費とGithub Pro料金がかかる形で落ち着きました。

Cloud Flare Pagesにデプロイすれば、PrivateのGithub Repositoryでも無料で公開できますし、そもそもrepository公開しちゃってもいいので、実質ドメイン維持費のみでブログと個人サイトを運営することができるようにはなりました

また気が向いたらCloud Flare Pagesに移行するかな?

https://pages.cloudflare.com

年末の大掃除をやり切った気分で、すがすがしいです :)