@genko-me/vue
v0.5.5
Published
genko.me配信API向け公式Vue 3コンポーネント集
Maintainers
Readme
@genko-me/vue
Vue 3用の、genko.meのコンテンツを表示する部品です。Vue 3.5以降が対象で、Nuxtでも素のVite + Vue SSRでも、クライアントだけのSPAでも使えます。
部品名は@genko-me/reactと同じで、DOMも同じものを出します。乗り換えても同じ書き方が通るようにしてあります。
取得と表示は分かれています
Vueにはサーバーコンポーネントが無いので、React版の<List>/<View>のように「取得しながら描く」部品はありません。取得は@genko-me/vue/serverの関数、表示は@genko-me/vueの部品、という2段構えです。取得側の返り値がそのまま表示側のpropsになります。
@genko-me/vue/serverはAPIキーを扱うので、サーバー側(Nuxtのserver/、SSRのローダー、Route Handler)からだけ呼んでください。ブラウザ向けのバンドルには入りません。
一覧
// server/api/blog.get.ts (Nuxtの場合)
import { fetchGenkoList } from '@genko-me/vue/server';
import { client } from '~/lib/client';
export default defineEventHandler(event => fetchGenkoList({
api: client.blog,
limit: 6,
orders: '-publishedAt',
href: '/blog/{id}',
fields: ['title', 'image', 'publishedAt'],
pagination: { mode: 'url' },
searchParams: getQuery(event) as Record<string, string | string[] | undefined>,
}));<script setup lang="ts">
import { ListClient, ListItem, Pagination, Error, Null, createPageLoader } from '@genko-me/vue';
const route = useRoute();
const { data } = await useFetch('/api/blog', { query: route.query });
</script>
<template>
<ListClient
v-if="data?.ok"
:initial="data.initial"
:initial-page="data.initialPage"
:fields="data.fields"
:schemas="data.schemas"
href="/blog/{id}"
:pagination-options="{ mode: 'url' }"
:load-page="createPageLoader({ url: '/api/genko-page', token: data.paginationToken! })"
>
<article>
<h2><ListItem id="title" /></h2>
<ListItem id="image"><template #fallback><span>画像なし</span></template></ListItem>
</article>
<template #pagination><Pagination mode="url" /></template>
<template #error><Error template /></template>
<template #empty><Null>記事はまだありません。</Null></template>
</ListClient>
<Error v-else template />
</template>ListClientの既定スロットが1件分のレイアウトです。pagination・loading・error・emptyは名前付きスロットで、繰り返しません。スロットを省略すると、その状態では何も表示しません。templateを指定したときだけ標準メッセージを表示します。
fetchGenkoListは失敗しても例外を投げず{ ok: false, message }を返します。詳細はサーバーのログにだけ出て、レスポンス本文・URL・APIキーがログに混ざることはありません。
詳細と下書き
// server/api/blog/[id].get.ts
import { fetchGenkoEntry } from '@genko-me/vue/server';
import { client } from '~/lib/client';
export default defineEventHandler(event => fetchGenkoEntry({
api: client.blog,
id: getRouterParam(event, 'id'),
searchParams: getQuery(event) as Record<string, string | string[] | undefined>,
}));<script setup lang="ts">
import { EntryProvider, ViewItem, PreviewNotice, Error } from '@genko-me/vue';
const route = useRoute();
const { data } = await useFetch(`/api/blog/${route.params.id}`, { query: route.query });
useHead(() => data.value?.ok && data.value.draft ? {
meta: [{ name: 'robots', content: 'noindex, nofollow' }, { name: 'referrer', content: 'no-referrer' }],
} : {});
</script>
<template>
<EntryProvider v-if="data?.ok && data.entry" :entry="data.entry" :fields="data.fields" :schemas="data.schemas">
<PreviewNotice v-if="data.draft" />
<h1><ViewItem id="title" /></h1>
<ViewItem id="body" />
</EntryProvider>
<p v-else-if="data?.ok">記事がありません。</p>
<Error v-else template />
</template>管理画面のプレビューURLを https://your-site.example/blog/{id}?draftKey={key} に設定します。searchParamsを渡すと保存済み下書きをキャッシュせず取得し、draft: trueが返ります。不正なキーで公開版へ切り替えることはありません。下書き表示のときは<meta name="robots" content="noindex, nofollow">をページ側で出してください(React版はサーバーコンポーネントの中で自動で出しますが、VueではページのuseHeadが担当します)。
ページ送り
Vue/Nuxtにはサーバーアクションが無いので、React版がServer Actionで担っていた部分は「自分のアプリの1本のAPIルート」に置き換わります。
// server/api/genko-page.post.ts
import { loadGenkoPage } from '@genko-me/vue/server';
export default defineEventHandler(async event => {
const { token, page } = await readBody<{ token: string; page: number }>(event);
return loadGenkoPage(token, page);
});fetchGenkoListにpaginationを指定するとpaginationTokenが返ります。これはHMAC署名付きで、取得先・条件・有効期限(1時間)を固定したものです。ブラウザへ渡してもendpointやfilterを書き換えることはできません。APIキーはブラウザへ送られません。期限切れ・キー交換後はページを再読み込みしてください。ページ送りを使うクライアントのキーはGENKO_API_KEYと一致させてください。カスタムfetch/wrapRequestはページ送りには対応しません。
既定の<Pagination />はURLを変えず一覧だけを切り替えます。<Pagination mode="url" param="page" />とListClientの:pagination-options="{ mode: 'url' }"を揃えるとURLと連動し、戻る操作にも追従します。複数のURL連動一覧には別々のparamを指定してください。
多言語
fetchGenkoList と fetchGenkoEntry は lang、langFallback、include を受け取り、@genko-me/sdk へそのまま渡します。指定しなければリクエストは従来と同じです。include: ['_langs'] を指定すると、各項目の _langs({ current, available })で読める言語を取れます。
const result = await fetchGenkoList({ api: client.blog, lang: 'en', langFallback: true, include: ['_langs'], pagination: true });pagination を使う場合、言語指定はpaginationTokenの署名に含まれます。ブラウザからlangを書き換えても検証で拒否されます。
fetchGenkoList が返す lang と hideLangs を ListClient に渡す(:lang="data.lang" :hide-langs="data.hideLangs")と、各項目のルート要素にHTMLの lang 属性が付きます。値は実際に返ってきた言語(_langs.current、無ければ指定した lang)で、langFallback で日本語の項目が混ざる一覧ならその項目だけ lang="ja" になります。lang を指定しなければ属性は付きません。
詳細は EntryProvider がDOMを出さないため、囲む要素は自分で書きます。fetchGenkoEntry が返す lang(実際に返った言語)をそこに付けてください。
<template>
<article v-if="data?.ok && data.entry" :lang="data.lang">
<EntryProvider :entry="data.entry" :fields="data.fields" :schemas="data.schemas">…</EntryProvider>
</article>
</template>langFallback では、返った言語を知るために include=_langs を内部で足します(lang と langFallback の両方を指定し、include に _langs が無いときだけ)。EntryProvider の配下や fetchGenkoEntry の entry には、自分で include: ['_langs'] を指定しない限り _langs は入りません。fetchGenkoList の initial は ListClient が項目ごとの言語を決めるために _langs を含みます。
デザインはサイト側で指定する
一覧・詳細・フィールド・ページ送り・状態表示は標準の装飾を付けません。class・styleと、見出しやdivなどのHTMLでサイトのデザインを適用します。本文の見出し・段落間隔・画像サイズもサイト側のCSSで設定してください。複数行テキストの改行を表示する場合はwhite-space: pre-wrapなどを指定します。
下書きプレビュー通知と終了ボタンだけは標準デザインを表示します。<PreviewNotice unstyled />で装飾を外せます。
記事全体のリンクには、クリック領域とキーボードフォーカスを成立させる最小限のCSSを使います。これは見た目のテーマとは別の、操作に必要な処理です。
表示の指定
- 表示項目は
ListItem/ViewItemで明示します。テキスト・画像・HTMLなどの種類は生成スキーマから判断します。 class・styleを各部品に渡せます。styleはオブジェクトでも文字列でも受け付け、数値にはReact版と同じ規則でpxを補います。- 日時は
<ListItem id="publishedAt" format="yyyy/MM/dd HH:mm" time-zone="Asia/Tokyo" />。既定のタイムゾーンはUTCです。 - 参照は
id="author.name"。複数参照は値を参照順で表示します。標準の参照展開は2階層までです。 - 未入力の値は表示しません。
<template #fallback>に代わりの表示を書けます。falseや0は未入力と扱いません。 - リンクは
ListClientのhref="/blog/{id}"で記事全体に設定するか、内部に<ListLink href="/blog/{id}">読む</ListLink>を置きます。ネストしたリンクを壊さず操作できます。 fetchGenkoList/fetchGenkoEntryのfieldsは、実際に描くフィールドIDを並べると転送量を減らせます。省略すると絞り込みません。スキーマに無いIDを書くと取得前に弾かれます。
シェアボタンとリッチテキスト
<script setup lang="ts">
import { RichText, ShareButtons } from '@genko-me/vue';
</script>
<template>
<RichText :html="article.body" as="div" class="prose" />
<ShareButtons :url="url" :title="article.title" :services="['x', 'line', 'hatena', 'copy', 'share']" />
</template>ShareButtonsの見た目はCSS変数(GENKO_DESIGN_TOKENS参照)で調整できます。unstyledにするとインラインスタイルを一切当てず、class・button-class・tooltip-classとdata-hovered / data-pressed / data-focus-visibleだけで独自のデザインを当てられます。React版はreact-aria-componentsを使っていますが、Vue版は素の<button>で組んであり、追加の依存はありません。
一括登録
// plugins/genko.ts (Nuxtの場合)
import { GenkoVuePlugin } from '@genko-me/vue';
export default defineNuxtPlugin(nuxtApp => { nuxtApp.vueApp.use(GenkoVuePlugin); });<ListItem>のような素の名前と、衝突を避けたい場合の<GenkoListItem>の両方が登録されます。専用のNuxtモジュール(@genko-me/vue/nuxt)は用意していません。@nuxt/kitへの依存が増えるわりに、やることがこのプラグインと変わらないためです。
自分でマークアップを組む場合
useListStoreで一覧の状態機械(保留・失敗・世代による追い越しの破棄)だけを借りられます。破棄はコンポーネントのunmountに従うので、dispose()を自分で呼ぶ必要はありません。
import { useListStore } from '@genko-me/vue';
const { state, totalPages, go } = useListStore({ initial, initialPage, loadPage });GENKO_ENTRY_KEY / GENKO_APPEARANCE_KEY / GENKO_PAGE_KEYもexportしてあるので、自前の一覧の中でも標準のListItemやPaginationをそのまま使えます。
スキーマ同期
npx @genko-me/cli pull
# 開発時の任意の自動同期(別ターミナル)
npx @genko-me/cli pull --watch変更履歴
0.5.3
- 追加: 共有ボタンに
share(OSの共有シート。navigator.share)を追加しました。servicesを省略した場合の既定がx, line, hatena, copy, shareになり、shareは対応している環境でだけ、画面に表示されたあとに最後に現れます。対応していない環境(多くのPCブラウザ)では今までと同じ4つです。サーバー側の出力は変わりません。servicesを明示した場合は書いたものだけが並びます(shareも、対応していなければ隠れます)。 - 追加: 共有シートを閉じたときは何も起こしません。開けなかったときはリンクのコピーに切り替わり、コンソールに警告を1回出します。ラベルは日本語「その他の方法で共有」・英語 "Share via…" で、
labelsのshareで差し替えられます。 - 背景: iPhoneのXアプリでは、Xのシェア画面のURLがアプリ内ブラウザのログイン画面で開き、投稿画面に進めないことがあります。OSの共有シートならこの影響を受けず、ほかのアプリにも共有できます。
0.5.2
- 変更: 共有ボタンの大きさを36×36pxから44×44pxにしました。指で押しやすい大きさ(タップ領域の目安)に合わせるためです。アイコンの既定サイズ(18px)は変わりません。
unstyledの場合は従来どおり大きさを指定しません。
0.5.1
- 追加:
langを指定すると、ListClientが各項目のルート要素にHTMLのlang属性を付けます。値は実際に返ってきた言語(_langs.current、無ければ指定したlang)です。fetchGenkoList/fetchGenkoEntryはlangを返すので、詳細では囲む要素に自分で付けられます。 - 変更:
langとlangFallbackを指定し、includeに_langsが無い場合に限り、リクエストのincludeに_langsを内部で足します(include=_langsが付くのはこの場合だけです)。ページ送りのトークンも送る値と同じもので署名します。 langを指定しない場合の出力とリクエストは0.5.0と同じです。
