パスワード保護された投稿を表示する
管理画面で投稿ごとに「閲覧パスワード」を設定すると、公開サイトの訪問者は正しいパスワードを入力したときだけ本文を読めます。この保護された投稿を公開サイト側でどう扱うか——保護の見分け方、本文がゲートされた
get() の挙動、unlock() による本文取得、そして静的サイト(SSG)での実装までをまとめます。
is_password_protected
を見るだけでよく、サイト設定の状態はAPIが織り込み済みです(保護OFFのサイトでは常に
false で、本文はそのまま返ります)。unlock()
は SDK 0.6 以上が必要です。
1. 保護の有無を知る
投稿の要約(一覧の各項目)・詳細の両方に is_password_protected(真偽値)が入ります。
true の投稿は本文がゲートされており、鍵アイコンを出すなどの表示に使えます。タイトル・カテゴリ・アイキャッチなどのメタ情報は保護対象ではなく、そのまま返ります(一覧でタイトルを隠す必要はありません)。
---
import { cms } from '../../lib/cms';
const { items } = await cms.posts.list('news', { perPage: 20 });
---
<ul>
{items.map((p) => (
<li>
<a href={`/news/${p.slug}`}>{p.title}</a>
{p.is_password_protected && <span aria-label="パスワード保護">🔒</span>}
</li>
))}
</ul> 2. 本文は get() で返らない
保護中の投稿を cms.posts.get() で取得すると、本文(body)は null で返ります(related_urls や
meta は返ります)。SDK 0.6 で PostDetail.body の型は
ProseMirrorNode | null になりました。renderProseMirror()
に渡す前に null チェックしてください。
const post = await cms.posts.get('news', slug);
if (post.body === null) {
// 保護中でパスワード未照合。本文は返っていない。
// → is_password_protected は true。パスワード入力フォームを表示する。
} else {
// 保護なし(またはサイト設定で保護OFF)。本文はそのまま使える。
const html = renderProseMirror(post.body);
} 3. unlock() で本文を取得する
本文は cms.posts.unlock(postType, slug, password)
に正しい閲覧パスワードを送ったときだけ返ります(本文入りの PostDetail)。パスワード不一致は
401(isUnauthorized・E001006)、試行回数超過は
429(isRateLimited・E001007・retryAfter)、未公開・未存在は 404(isNotFound)です。応答はキャッシュされません(Cache-Control: no-store)。多言語オプション有効時は第4引数に任意の locale を渡すと、本文も個別取得と同じ言語の公開版で返ります(多言語サイトを作る)。
import { ShareDanCmsApiError } from '@sharedan/cms-sdk';
import { renderProseMirror } from '@sharedan/cms-sdk';
async function revealBody(slug: string, password: string): Promise<string> {
try {
const post = await cms.posts.unlock('news', slug, password);
// 照合成功。body が入って返る。
return post.body ? renderProseMirror(post.body) : '';
} catch (err) {
if (err instanceof ShareDanCmsApiError) {
if (err.isUnauthorized) throw new Error('パスワードが違います'); // 401 E001006
if (err.isRateLimited) throw new Error(`しばらく待ってから再試行してください(${err.retryAfter ?? '数分'}秒)`); // 429 E001007
if (err.isNotFound) throw new Error('記事が見つかりません'); // 404
}
throw err;
}
} Retry-After に待機秒数が入ります。入力フォームでは、連投を抑え、429 のときは retryAfter
を見てメッセージを出しましょう。
4. 静的サイト(SSG)での実装
SSG では、保護投稿の本文をビルド時に静的HTMLへ焼けません(焼いたらソースに本文が露出してしまいます)。そこでビルド時は「ゲート(パスワード入力フォーム)」だけを出力し、本文は閲覧者の入力を受けてブラウザから実行時に
unlock() を呼ぶ構成にします。
---
// src/pages/news/[slug].astro(SSG・ビルド時)
import { cms } from '../../lib/cms';
import { renderProseMirror } from '@sharedan/cms-sdk';
export async function getStaticPaths() {
const { items } = await cms.posts.list('news', { perPage: 100 });
return items.map((p) => ({ params: { slug: p.slug }, props: { post: p } }));
}
const { post } = Astro.props; // 一覧由来=本文は無い(軽量)
const detail = await cms.posts.get('news', post.slug);
const locked = detail.body === null; // 保護中=ビルド時に本文を焼けない
const html = locked ? '' : renderProseMirror(detail.body!);
---
<article>
<h1>{detail.title}</h1>
{locked ? (
<form data-unlock data-slug={post.slug}>
<input type="password" name="password" autocomplete="off" required />
<button type="submit">本文を表示</button>
<p data-unlock-error hidden></p>
</form>
) : (
<div set:html={html} />
)}
<div data-unlock-body></div>
</article>
ブラウザから公開APIを直接叩くと読み取り用APIキーが露出します。問い合わせフォーム連携と同じく、同一オリジンの薄いプロキシ(cms-proxy.php)を経由し、鍵の付与はサーバー側に任せます。プロキシは
POST .../unlock と Retry-After の透過に対応しています。
<script>
import { ShareDanCmsClient, ShareDanCmsApiError, renderProseMirror } from '@sharedan/cms-sdk';
// 鍵はブラウザに出さない。同一オリジンのプロキシ(cms-proxy.php)がサーバー側で付与する。
const cms = new ShareDanCmsClient({
baseUrl: `${location.origin}/cms-proxy.php`,
apiKey: 'via-proxy',
siteCode: 'via-proxy',
});
document.querySelector('[data-unlock]')?.addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.currentTarget as HTMLFormElement;
const slug = form.dataset.slug!;
const password = new FormData(form).get('password') as string;
const err = form.querySelector('[data-unlock-error]') as HTMLElement;
try {
const post = await cms.posts.unlock('news', slug, password);
const html = post.body ? renderProseMirror(post.body) : '';
document.querySelector('[data-unlock-body]')!.innerHTML = html;
form.remove();
} catch (ex) {
err.hidden = false;
err.textContent =
ex instanceof ShareDanCmsApiError && ex.isRateLimited
? '試行回数が多すぎます。しばらく待ってください。'
: 'パスワードが違います。';
}
});
</script> getStaticPaths で URL は生成しておき、本文だけを実行時に出す——この分離で、SSGの速さを保ったまま限定公開を実現できます。
5. 注意点
- 本文をキャッシュに載せない:
unlock()の応答はno-storeです。取得した本文(HTML)を自前のCDNやエッジにキャッシュしないでください。 - 検索エンジン・SSR:保護本文はビルド時に出力されないため、インデックスもされません。限定公開の記事はこれが期待挙動です。
- サイト設定でOFFにすると保護は無効化されます:その間
is_password_protectedはfalseになり、本文はパスワードなしで返ります(設定したパスワード自体はCMSに保持され、再度ONにすると復活します)。 - パスワードは機密情報ではありません:閲覧パスワードは「限定公開のための軽い鍵」です。機微情報の保護には、APIキー保護や会員認証など、別の仕組みを使ってください。
- 多言語オプションでは本文も訳される:
unlock()もget()と同じく任意のlocale(第4引数)を受け取り、個別取得と同じ言語を渡すと、ゲートされた本文も指定言語の公開版で返り、タイトルと本文の言語が揃います。未訳の本文は原文(日本語)を返しtranslation_available: falseを含めます(多言語サイトを作る)。
このページはお役に立ちましたか?
改善要望・不具合・ドキュメントの分かりにくい点など、お気軽にお寄せください。いただいた内容は今後の改善に役立てます。
お寄せいただきありがとうございます。今後の改善に役立てます。