NuxtHub で Nuxt Content の queryCollections が null になる場合の対応

諸々のバージョン

"dependencies": {
  "@nuxt/content": "^3.4.0",
  "@nuxthub/core": "^0.8.24",
  "nuxt": "^3.16.2",
  "vue": "^3.5.13",
},

NuxtHub にデプロイする Web サイトにおいて、Nuxt Content で markdown を使ったページを構築しているとき、そのページに直接アクセスするとコンテンツを取得できない。
↓ のような実装において、page.value が null になる。

<script setup lang="ts">
const route = useRoute();

const { data: page } = await useAsyncData(route.path, () => {
  return queryCollection("blog").path(route.path).first();
}); // page.valueがnullになる
</script>

<template>
  <ContentRenderer v-if="page" :value="page" class="prose" />
</template>

NuxtHub を利用しない場合は問題ない。
細かい原因は不明だが、NuxtHub 利用時のみ content のダンプをリストアするタイミングが遅くなる?

他のページを経由してのアクセスにより CSR される場合は正しく表示される。
たとえば、ブログ記事を上記のように実装していた場合、その記事に直接アクセスしても表示されないが、別途記事一覧ページを作成し、そこから個別記事に遷移すれば表示される。

対処法 1 - CSR で妥協

上記で述べたように CSR であれば表示されるので、SEO などを気にしないのであればそれでもよい。
useAsyncDatarefreshをコンポーネントマウント後に実行することで実現できる。

<script setup lang="ts">
const route = useRoute();

const { data: page, refresh } = await useAsyncData(route.path, () => {
  return queryCollection("blog").path(route.path).first();
});

onMounted(() => {
  if (!page.value) {
    // 別ページからの遷移の場合はfalse/直接アクセスの場合はtrue
    refresh();
  }
});
</script>

<template>
  <ContentRenderer v-if="page" :value="page" class="prose" />
</template>

対処法 2 - プリレンダリング

SSR ではなくプリレンダリングしておく。Nuxt Content で構築したページは基本的に静的なはず?なので、これでよさそう。
NuxtHub におけるプリレンダリングはドキュメントに記載があるのでこの通りにやれば基本 OK。
https://hub.nuxt.com/docs/recipes/pre-rendering
ただし、defineRouteRulesマクロは experimental なので注意。

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  // ...
  experimental: {
    inlineRouteRules: true,
  },
  // ...
});

たとえばブログ記事ページを/blog/[id].vueとした場合

<script setup lang="ts">
const route = useRoute();

const { data: page } = await useAsyncData(route.path, () => {
  return queryCollection("blog").path(route.path).first();
});
</script>

<template>
  <ContentRenderer v-if="page" :value="page" class="prose" />
</template>

その一覧を表示するページ /blog/index.vue などで以下のように実装する。

<script setup lang="ts">
// このページ自体にプリレンダリングを設定する
defineRouteRules({ prerender: true });

const { data: blogs } = await useAsyncData("blogs", () => {
  return queryCollection("blog").all();
});

// 取得したblogsに対応するページもすべてプリレンダリングする
prerenderRoutes(blogs.value?.map((blog) => `${blog.path}`) ?? []);
</script>

<template>
  <!-- blogsの一覧を表示するなど -->
</template>

終わり

NuxtHub の開発体験はとてもよいものですが、他と組みあわせたときにうまくいかないことがちょこちょこあってまだまだ安定はしないのかなと思う。

参考