Next.jsで作ったサイトをAstroに変えた際の、Mardocの取り扱い
hidetaka.devという個人のポートフォリオサイトを運用しており、Next.jsとNetlifyで公開していたが、SSRへの移行を検討。CloudflareとAstro採用し、MarkdocからNext.jsにカスタマイズ追加。Astroではastro.config.mjsに設定記述。取得処理やAstroコンポーネント実装も実施。Next.jsからAstroに移行し、getStaticProps内のカスタマイズもAstroコンポーネント側に移行。最終的にCloudflare Pagesでfsが使えないことが判明し、Markdocでのコンテンツ管理諦めたが、Astroの使用を今後他の環境でも検討。
目次
https://hidetaka.dev という個人のポートフォリオサイトを運用しています。そのホスティング環境やフレームワークを変えたので、その時の覚書を整理しました。
背景
もともとSSGとしてNext.jsを利用し、Netlifyでお手軽に公開していたサイトでした。ただ、npmモジュールやWordPressプラグインなどの情報もポートフォリオへ含めようとした際、ウェブフックイベントなどがこれらには存在しませんでした。そのため更新情報の反映がSSGでは難しくなってきたことから、SSRできる環境への移行を検討し始めました。
その中で今後使うことの増えそうな予感がしたCloudflareとAstroを使うことにしました。
Markdocのレンダー処理を変える
Markdownで書いていたコンテンツは、Markdocを使って描画していました。そして外部リンクのようなMarkdown単独では表現しにくい部分に、独自タグのレンダリング用に入れていたカスタマイズを、Next.jsに追加していました。
import { parse, transform } from '@markdoc/markdoc'
import { readFileSync } from 'fs'
import { join } from 'path'
export const loadMarkdownFile = (path: string) => {
const content = readFileSync(join(process.cwd(), path), 'utf-8')
return transform(parse(content), {
tags: {
externalLink: {
render: 'ExternalLink',
attributes: {
className: {
type: String,
},
href: {
type: String,
},
label: {
type: String,
},
},
},
},
})
}
この設定からカスタムのReactコンポーネントを描画するためのrender処理を入れるところまでをやっていました。
import { renderers } from '@markdoc/markdoc'
import React, { FC } from 'react'
import { ExternalLink } from './ExternalLink'
export const MarkdocContent: FC<{
content: string
}> = ({ content }) => {
return (
<>
{renderers.react(JSON.parse(content), React, {
components: {
ExternalLink,
},
})}
</>
)
}
Astroの場合は、astro.config.mjs
にConfigを書くことになります。
import { defineConfig } from 'astro/config';
import markdoc from "@astrojs/markdoc";
// https://astro.build/config
export default defineConfig({
integrations: [markdoc({
tags: {
externalLink: {
render: 'ExternalLink',
attributes: {
class: {
type: String,
},
href: {
type: String,
},
lebel: {
type: String,
}
}
},
},
})]
});
あとは取得処理などをアイランド内に記述し、Astroコンポーネントとして実装しています。
---
import { getEntryBySlug } from 'astro:content';
import ExternalLink from '../components/markdoc/ExternalLink.astro';
const lang = getLanguageFromURL(new URL(Astro.request.url).pathname) || 'en-US'
const entry = await getEntryBySlug('profiles', `hero${/ja/.test(lang) ? '-ja' : ''}`);
const speakerProfile = await getEntryBySlug('profiles', `speaker-profile${/ja/.test(lang) ? '-ja' : ''}`);
const { Content } = entry ? await entry.render() : { Content: null};
const { Content: SpeakerProfile } = speakerProfile ? await speakerProfile.render() : {Content: null}
---
{Content ? (
<Content
components={{ ExternalLink }}
/>
): null}
{SpeakerProfile ? (
<SpeakerProfile
components={{ ExternalLink }}
/>
): null}
Next.jsの場合、いくつかgetStaticProps
内で見た目のカスタマイズを入れていたため、この辺りについてもAstroのコンポーネント側に移すことができました。
export const getStaticProps: GetStaticProps<{
profiles: {
hero: string
speakerBio: string
}
}> = async (context) => {
const heroArticle = loadMarkdownFile('contents/profiles/hero.md')
const heroContent = (heroArticle as any).children
heroContent[0].attributes = {
...heroContent[0].attributes,
className: 'mt-3 text-base text-gray-500 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl',
}
const speakerBioFileName = isJapanese(context.locale)
? 'speakerProfile.ja.md'
: 'speakerProfile.md'
const speakerBio = loadMarkdownFile(`contents/profiles/${speakerBioFileName}`)
const profiles = {
hero: JSON.stringify(heroContent),
speakerBio: JSON.stringify(speakerBio),
}
return {
props: {
profiles,
},
revalidate: 1 * 60 * 60,
}
}
やってみての感想
この後、Cloudflare Pagesでfs
が使えないことが判明し、Markdocでのコンテンツ管理は結局諦めました。しかしどちらのやり方もざっと触ることができたので、今後他の環境でAstroを使うことになった際は使ってみようかと思います。