nuxt/content を Astro に移行した
移行対応したサイトは以下より
移行した対応 PR は以下より
なぜ nuxt/content を辞める?
- nuxt/content の v2 系が求めている挙動にはならなかった
- 見出しリンクや、ページ遷移時のスクロール位置リセットなど
- 選定時で SSG 対応がなさそうだった
- Nuxt v3 が正式版リリースされておらず安定していない
- RC にて SSG 対応もされるとのことだが待てなかった
- Nuxt modules の v3 対応もあるかどうか分からない
- 求めているものは Nuxt をラップさせる必要はない、独自のカスタマイズでいけるものを欲していた
なぜ Astro を選んだ?
- 導入事例があり、それが参考になった
- 静的出力ができる
- Vue.js や React.js、Svelte といったフレームワークやライブラリを使ってコンポーネント指向で開発できる
- 11ty にはない部分
- 設定が簡易的
- vite で開発できる
今回は公式のブログテーマを参考に作成
不要なモジュールを剪定、必要なモジュールを追加
"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"
}
npm scripts をアップデート
"scripts": {
- "dev": "nuxt",
- "build": "nuxt build",
- "start": "nuxt start",
- "generate": "nuxt generate",
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview"
}
Astro 自体の settings
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: 'コンテンツに戻る',
},
},
});
rss 配信に伴う rss.xml.js 設定
Nuxt.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,
})),
});
tsconfig.json
以下設定だけでOK
{
"extends": "astro/tsconfigs/base"
}
.astro
ファイル
各種必要な astro ファイルを設定・設置
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>
src/components/Footer.astro
---
const today = new Date()
---
<footer>
<p>© 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>
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>
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>
static に設置していたファイルを public に移動
フレームワークを通さない静的なファイルを設置
- Nuxt.js では static ディレクトリ
- Astro では public ディレクトリ
md ファイルを移動
src/content
配下においていたものを src/pages
に移動
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>
))
}
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>
Astro で調整したこと
同じ見た目に再現するために一部は調整が必要だった箇所があるのでそれに関すること
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 のスタイルを適応し、視覚的には表示しないようにした
シンタックスハイライト
デフォルトでは Shiki が推奨されているようだが、Nuxt.js の頃は Prism を使っていたのでそちらを導入したい。
公式ページに併せて設定をする
ただ Prismテーマの中から、あらかじめ用意されているスタイルシートを選択する。 の部分は手動管理がしたくなかったので、これまた前回の設定通り npm packages から呼び出して使うことにしている
"dependencies": {
"prism-themes": "1.9.0",
},
import 'prism-themes/themes/prism-a11y-dark.css'
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',
];
移行してみてよかったこと
- そもそも複雑な構成でなかったのもあるが、割とすぐにできた
- 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 もテンプレートとしてあるのがよかった