Closed27

nuxt/content を Astro に移行した

yamanokuyamanoku

なぜ nuxt/content を辞める?

  • nuxt/content の v2 系が求めている挙動にはならなかった
    • 見出しリンクや、ページ遷移時のスクロール位置リセットなど
    • 選定時で SSG 対応がなさそうだった
  • Nuxt v3 が正式版リリースされておらず安定していない
    • RC にて SSG 対応もされるとのことだが待てなかった
    • Nuxt modules の v3 対応もあるかどうか分からない
  • 求めているものは Nuxt をラップさせる必要はない、独自のカスタマイズでいけるものを欲していた
yamanokuyamanoku

なぜ Astro を選んだ?

  • 導入事例があり、それが参考になった
  • 静的出力ができる
  • Vue.js や React.js、Svelte といったフレームワークやライブラリを使ってコンポーネント指向で開発できる
    • 11ty にはない部分
  • 設定が簡易的
  • vite で開発できる
yamanokuyamanoku

不要なモジュールを剪定、必要なモジュールを追加

package.json
  "dependencies": {
-    "@nuxt/content": "^1.15.1",
-    "core-js": "^3.21.1",
-    "nuxt": "2.13.3"
+   "@astrojs/rss": "1.0.1",
+    "@astrojs/sitemap": "1.0.0",
+    "astro": "1.2.6",
  },
  "devDependencies": {
-    "@nuxt/types": "^2.15.3",
-    "@nuxt/typescript-build": "^2.1.0",
-    "@nuxtjs/eslint-config-typescript": "^6.0.0",
-    "@nuxtjs/eslint-module": "^3.0.2"
  }
yamanokuyamanoku

npm scripts をアップデート

package.json
  "scripts": {
-    "dev": "nuxt",
-    "build": "nuxt build",
-    "start": "nuxt start",
-    "generate": "nuxt generate",
+    "dev": "astro dev",
+    "build": "astro build",
+    "preview": "astro preview"
  }
yamanokuyamanoku

Astro 自体の settings

astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

const rehypePlugins = [
  'remark-gfm',
  ['rehype-external-links', { target: ['_blank'], rel: ['noopener'] }],
  [
    'rehype-plugin-auto-resolve-layout-shift',
    { type: 'maxWidth', maxWidth: 720 },
  ],
  'rehype-plugin-image-native-lazy-loading',
];

// https://astro.build/config
export default defineConfig({
  site: 'https://archives.yamanoku.net',
  integrations: [sitemap()],
  markdown: {
    syntaxHighlight: 'prism',
    rehypePlugins,
    remarkRehype: {
      footnoteLabel: '脚注',
      footnoteBackLabel: 'コンテンツに戻る',
    },
  },
});
yamanokuyamanoku

rss 配信に伴う rss.xml.js 設定

Nuxt.js の時は特にやってなかったが、参考にしていたもので導入されてたので試しにやってみてる

rss.xml.js
import rss from '@astrojs/rss';
import { SITE_TITLE, SITE_DESCRIPTION } from '../config';

const postImportResult = import.meta.glob('./*.md', { eager: true });
const posts = Object.values(postImportResult);

export const get = () =>
  rss({
    title: SITE_TITLE,
    description: SITE_DESCRIPTION,
    site: import.meta.env.SITE,
    items: posts.map((post) => ({
      link: post.url,
      title: post.frontmatter.title,
      pubDate: post.frontmatter.date,
    })),
  });
yamanokuyamanoku

tsconfig.json

以下設定だけでOK

{
  "extends": "astro/tsconfigs/base"
}
yamanokuyamanoku

.astro ファイル

各種必要な astro ファイルを設定・設置

yamanokuyamanoku

src/components/BaseHead.astro

---
import 'modern-normalize'
import 'yama-normalize'
import 'prism-themes/themes/prism-a11y-dark.css'
export interface Props {
  title: string
  description: string
}
const { title, description } = Astro.props as Props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const pathName = canonicalURL.pathname.split('/').join('');
const ogpImageURL = canonicalURL.pathname === '/' ? new URL('/ogp-image.png', Astro.url) : new URL(`/og-images/${pathName}.png`, Astro.url)
---

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<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={ogpImageURL} />
<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={ogpImageURL} />
<script type="module" src="https://unpkg.com/budoux/bundle/budoux-ja.min.js"></script>

yamanokuyamanoku

src/components/Footer.astro

---
const today = new Date()
---

<footer>
  <p>&copy; Copyright {today.getFullYear()}, Okuto Oyama</p>
  <p>
    Source :
    <a
      href="https://github.com/yamanoku/archives/"
      target="_blank"
      rel="noopener">yamanoku/archives</a
    >
  </p>
</footer>
<style>
  footer {
    padding: 25px;
    text-align: center;
  }
</style>
yamanokuyamanoku

src/components/Header.astro

---
import { SITE_TITLE } from '../config'
---

<header>
  <a href="/">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="246"
      height="242"
      viewBox="0 0 246 242"
      role="img"
      aria-labelledby="title"
    >
      <title id="title">yamanoku.net</title>
      <path
        class="cls-1"
        d="M64,67v54l82,82-46,46v60h56L310,155V96H230l-21,20L160,67H64ZM176,203l-45,46h25L293,113H230l-39,39-31-31H94Z"
        transform="translate(-64 -67)"></path>
    </svg>
    {SITE_TITLE}
  </a>
</header>
<style>
  header {
    max-width: 80ch;
    margin: auto;
    padding: 0 var(--y-rhythm-2);
  }
  a {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin: var(--y-rhythm-2) 0;
    text-decoration: none;
  }
  svg {
    width: 48px;
    height: 48px;
    vertical-align: middle;
  }
  .cls-1 {
    fill: rgb(54, 70, 93);
    fill-rule: evenodd;
  }
  @media (prefers-color-scheme: dark) {
    .cls-1 {
      fill: var(--y-white-base);
    }
  }
</style>
yamanokuyamanoku

src/layouts/ArchivesPost.astro

---
import BaseHead from '../components/BaseHead.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import dayjs from 'dayjs'
export interface Props {
  content: {
    title: string
    description: string
    date: string
  }
}
const {
  content: { title, description, date },
} = Astro.props as Props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const pathName = canonicalURL.pathname.split('/').join('');
const editLink = `https://github.com/yamanoku/archives/edit/main/src/pages/${pathName}.md`
const gitHubLink = `https://github.com/yamanoku/archives/issues/new?title=アーカイブのドキュメントにまつわる修正依頼&labels=feedback&body=URL:https://archives.yamanoku.net/${pathName}%0A修正依頼内容:%0A`
const twitterLink = `https://twitter.com/share?url=https://archives.yamanoku.net/${pathName}&text=@yamanoku`
---

<html lang="ja">
  <head>
    <BaseHead title={title} description={description} />
    <style>
      .notes {
        margin: var(--y-rhythm-3) 0;
      }
      .article-header {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        margin: calc(var(--y-rhythm-2) * -1);
        padding: 0;
      }
      .article-header > * {
        margin: var(--y-rhythm-2);
      }
    </style>
    <style is:global>
      .footnotes {
        position: relative;
        padding-top: var(--y-rhythm-3);
      }
      .footnotes::before {
        content: '';
        width: 100%;
        height: 1px;
        background-color: var(--y-arcticle-border-color);
        position: absolute;
        top: 0;
        left: 0;
      }
      .footnotes h2 {
        position: absolute;
        left: -10000px;
        width: 1px;
        height: 1px;
        overflow: hidden;
      }
      .footnotes li p {
        margin: 0;
      }
    </style>
  </head>

  <body>
    <Header />
    <main>
      <article>
        <h1 class="title">
          <budoux-ja>{title}</budoux-ja>
        </h1>
        <div class="article-header">
          <time>created at: {dayjs(date).format('YYYY-MM-DD')}</time>
          <a href={editLink} target="_blank" rel="noopener">GitHub Edit Page</a>
        </div>
        <div class="notes">
          <strong>
            この記事は公開から1年以上が経過しています。内容が一部古い箇所があります。
          </strong>
        </div>
        <slot />
      </article>
      <p>
        アーカイブ記事のため、内容に関する更新依頼は受け付けておりませんが、誤字や脱字などありましたらご連絡ください。
      </p>
      <details>
        <summary>この記事に関する修正依頼</summary>
        <ul>
          <li>
            <a href={gitHubLink} target="_blank" rel="noopener">
              GitHub Issue を作成する
            </a>
          </li>
          <li>
            <a href={twitterLink} target="_blank" rel="noopener">
              著者にツイートする
            </a>
          </li>
        </ul>
      </details>
      <a href="/">アーカイブ一覧へ戻る</a>
    </main>
    <Footer />
  </body>
</html>
yamanokuyamanoku

static に設置していたファイルを public に移動

フレームワークを通さない静的なファイルを設置

  • Nuxt.js では static ディレクトリ
  • Astro では public ディレクトリ
yamanokuyamanoku

md ファイルを移動

src/content 配下においていたものを src/pages に移動

yamanokuyamanoku

/content に置いたまま [slug].astro を使って Astro.glob で読み込むのもできそうだったが、
現状のブログコンテンツがルート直下に置かれていたのと archives.yamanoku.net/content のような URL 配下に生成されてしまうので愚直に /pages 配下に置いてみた

yamanokuyamanoku

以降に伴い、レイアウトを適応するため以下情報 を md ファイルに追加

layout: '../layouts/ArchivesPost.astro'
yamanokuyamanoku

index にページ一覧を描画

Astro.glob で md ファイルを取得し、date で比較したもの配列にする

const posts = (await Astro.glob('./*.md')).sort(
  (a, b) =>
    new Date(b.frontmatter.date).valueOf() -
    new Date(a.frontmatter.date).valueOf()
)

map() で posts の中身を描画

{
  posts.map((post) => (
    <article>
      <time datetime={dayjs(post.frontmatter.date).format('YYYY-MM-DD')}>
        {dayjs(post.frontmatter.date).format('YYYY-MM-DD')}
      </time>
      <h2>
        <a href={post.url}>
          <budoux-ja>{post.frontmatter.title}</budoux-ja>
        </a>
      </h2>
      <p>{post.frontmatter.description}</p>
    </article>
  ))
}
yamanokuyamanoku

Nuxt.js の場合

  async asyncData({ $content }) {
    const query = $content('/', { deep: true }).sortBy('date', 'desc')
    const articles = await query.fetch()
    return {
      articles,
    }
  },
 <article v-for="article in articles" :key="article.slug">
  <p>
    <time :datetime="dateTime(article.date)">
      {{ dateTime(article.date) }}
    </time>
  </p>
  <h2>
    <nuxt-link :to="article.path">
      <budoux-ja>
        {{ article.title }}
      </budoux-ja>
    </nuxt-link>
  </h2>
  <p>
    {{ article.description }}
  </p>
</article>
yamanokuyamanoku

Astro で調整したこと

同じ見た目に再現するために一部は調整が必要だった箇所があるのでそれに関すること

yamanokuyamanoku

footnotes 部分の描画

そのままの状態だと md で書かれている脚注部分がリストのようにならなかったので astor.config.mjs で設定

  markdown: {
    remarkRehype: {
      footnoteLabel: '脚注',
      footnoteBackLabel: 'コンテンツに戻る',
    },
  },

その場合脚注の見出しが表示されることとなり、Nuxt.js での状態とは違う形になる。

<style is:global>
.footnotes h2 {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>

Visually Hidden のスタイルを適応し、視覚的には表示しないようにした

yamanokuyamanoku

シンタックスハイライト

デフォルトでは Shiki が推奨されているようだが、Nuxt.js の頃は Prism を使っていたのでそちらを導入したい。

公式ページに併せて設定をする
https://docs.astro.build/ja/guides/markdown-content/#prismの設定

ただ Prismテーマの中から、あらかじめ用意されているスタイルシートを選択する。 の部分は手動管理がしたくなかったので、これまた前回の設定通り npm packages から呼び出して使うことにしている

  "dependencies": {
    "prism-themes": "1.9.0",
  },
BaseHead.astro
import 'prism-themes/themes/prism-a11y-dark.css'
yamanokuyamanoku

rehypePlugins 有効化に併せた再設定

rehypePlugins では以前より使用していたプラグインを設定している

const rehypePlugins = [
  ['rehype-external-links', { target: ['_blank'], rel: ['noopener'] }],
  [
    'rehype-plugin-auto-resolve-layout-shift',
    { type: 'maxWidth', maxWidth: 720 },
  ],
  'rehype-plugin-image-native-lazy-loading',
];

ただ、この設定を有効にすることによりビルトインサポートされていた GitHub-flavored Markdown が無効になってしまい remarkRehype の footnote が使えなくなるので、明示的に指定してあげる必要がある。

const rehypePlugins = [
+  'remark-gfm',
  ['rehype-external-links', { target: ['_blank'], rel: ['noopener'] }],
  [
    'rehype-plugin-auto-resolve-layout-shift',
    { type: 'maxWidth', maxWidth: 720 },
  ],
  'rehype-plugin-image-native-lazy-loading',
];

yamanokuyamanoku

移行してみてよかったこと

  • そもそも複雑な構成でなかったのもあるが、割とすぐにできた
  • Vue SFC の HTML, CSS 部分はほぼそのまま流用できた
  • 見出しの文字組みに BudouX を使用しているが script type="module" を BaseHead.astro で呼び出してそのまま Web Components を使えた
    • Nuxt.js (Vue.js)でカスタムコンポーネントを使う場合は ignore する必要があった
    • Vue.config.ignoredElements = ['budoux-ja']
  • TypeScript の設定が簡易的ですぐ導入できる
  • Vercel でホスティングしており、Astro もテンプレートとしてあるのがよかった
yamanokuyamanoku

まだやってない・未調査な部分

  • ESLint や Prettier は使えるのか?
    • コンポーネント単位でかけることはできるが .astro にできるのかどうか
  • ページネーション機能
    • できるらしいので index ページで適応したい(量が多くなってきた)
このスクラップは2022/10/19にクローズされました