keystonejs
Node.js、GraphQL、Prismaを基盤とするKeystoneJSの専門家として、コンテンツスキーマ定義、管理画面構築、アクセス制御、GraphQL API作成を支援し、コンテンツ活用アプリケーション開発を効率化するSkill。
📜 元の英語説明(参考)
Expert guidance for KeystoneJS, the open-source headless CMS and application platform built on Node.js, GraphQL, and Prisma. Helps developers define content schemas, build admin interfaces, implement access control, and create custom GraphQL APIs for content-driven applications.
🇯🇵 日本人クリエイター向け解説
Node.js、GraphQL、Prismaを基盤とするKeystoneJSの専門家として、コンテンツスキーマ定義、管理画面構築、アクセス制御、GraphQL API作成を支援し、コンテンツ活用アプリケーション開発を効率化するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o keystonejs.zip https://jpskill.com/download/15039.zip && unzip -o keystonejs.zip && rm keystonejs.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/15039.zip -OutFile "$d\keystonejs.zip"; Expand-Archive "$d\keystonejs.zip" -DestinationPath $d -Force; ri "$d\keystonejs.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
keystonejs.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
keystonejsフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
KeystoneJS — Headless CMS & アプリケーションフレームワーク
概要
KeystoneJS は、Node.js、GraphQL、Prisma 上に構築されたオープンソースの headless CMS およびアプリケーションプラットフォームです。開発者がコンテンツスキーマを定義し、管理インターフェースを構築し、アクセス制御を実装し、コンテンツ駆動型アプリケーション向けのカスタム GraphQL API を作成するのに役立ちます。
手順
スキーマ定義
Keystone の list API を使用してデータモデルを定義します。
// keystone.ts — メインの Keystone 設定
import { config, list } from "@keystone-6/core";
import { allowAll } from "@keystone-6/core/access";
import {
text, relationship, timestamp, select, integer,
image, json, checkbox, password, float,
} from "@keystone-6/core/fields";
import { document } from "@keystone-6/fields-document";
export default config({
db: {
provider: "postgresql", // "postgresql" | "sqlite" | "mysql"
url: process.env.DATABASE_URL!,
},
lists: {
// ブログ投稿コンテンツタイプ
Post: list({
access: allowAll, // 後でカスタマイズします
fields: {
title: text({
validation: { isRequired: true },
isIndexed: "unique",
}),
slug: text({
validation: { isRequired: true },
isIndexed: "unique",
hooks: {
// スラッグが提供されていない場合は、タイトルから自動生成します
resolveInput({ resolvedData, item }) {
if (!resolvedData.slug && resolvedData.title) {
return resolvedData.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
return resolvedData.slug;
},
},
}),
status: select({
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
{ label: "Archived", value: "archived" },
],
defaultValue: "draft",
ui: { displayMode: "segmented-control" },
}),
content: document({
// カスタムブロック付きのリッチテキストエディタ
formatting: true,
links: true,
dividers: true,
layouts: [
[1, 1], // 2 カラムレイアウト
[1, 1, 1], // 3 カラムレイアウト
],
componentBlocks: {}, // カスタムエディタコンポーネント
}),
heroImage: image({
storage: "localImages", // storage セクションで設定
}),
publishedAt: timestamp(),
author: relationship({
ref: "User.posts", // 双方向リレーションシップ
ui: { displayMode: "cards", cardFields: ["name", "email"] },
}),
categories: relationship({
ref: "Category.posts",
many: true, // 多対多リレーションシップ
ui: { displayMode: "select" },
}),
readingTime: integer({
ui: { createView: { fieldMode: "hidden" } }, // 自動計算
hooks: {
resolveInput({ resolvedData }) {
if (resolvedData.content) {
const wordCount = JSON.stringify(resolvedData.content).split(/\s+/).length;
return Math.ceil(wordCount / 200); // 1 分あたり ~200 語
}
return undefined;
},
},
}),
},
}),
// カテゴリ分類
Category: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true }, isIndexed: "unique" }),
description: text({ ui: { displayMode: "textarea" } }),
posts: relationship({ ref: "Post.categories", many: true }),
},
}),
// 認証付きユーザー
User: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ validation: { isRequired: true }, isIndexed: "unique" }),
password: password({
validation: { isRequired: true, length: { min: 8 } },
}),
role: select({
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Author", value: "author" },
],
defaultValue: "author",
}),
posts: relationship({ ref: "Post.author", many: true }),
},
}),
},
// ファイルストレージ設定
storage: {
localImages: {
kind: "local",
type: "image",
generateUrl: (path) => `/images${path}`,
serverRoute: { path: "/images" },
storagePath: "public/images",
},
},
// 認証
session: {
maxAge: 60 * 60 * 24 * 30, // 30 日
secret: process.env.SESSION_SECRET!,
},
});
アクセス制御
きめ細かい権限を実装します。
// access.ts — ロールベースのアクセス制御ルール
import { ListAccessArgs } from "@keystone-6/core/types";
// ユーザーが認証されているかを確認するヘルパー
function isSignedIn({ session }: ListAccessArgs) {
return !!session;
}
// ユーザーロールを確認するヘルパー
function hasRole(role: string) {
return ({ session }: ListAccessArgs) => session?.data?.role === role;
}
// 投稿固有のアクセスルール
export const postAccess = {
operation: {
query: allowAll, // 誰でも公開された投稿を読める
create: isSignedIn, // 作成するにはログインする必要がある
update: isSignedIn,
delete: hasRole("admin"), // 管理者のみが削除できる
},
filter: {
// 管理者以外は公開された投稿のみを表示できる (クエリ結果)
query: ({ session }: ListAccessArgs) => {
if (session?.data?.role === "admin") return {}; // 管理者はすべて表示できる
return { status: { equals: "published" } };
},
// 作成者は自分の投稿のみを更新できる
update: ({ session }: ListAccessArgs) => {
if (session?.data?.role === "admin") return {};
return { author: { id: { equals: session?.itemId } } };
},
},
(原文がここで切り詰められています) 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
KeystoneJS — Headless CMS & Application Framework
Overview
KeystoneJS, the open-source headless CMS and application platform built on Node.js, GraphQL, and Prisma. Helps developers define content schemas, build admin interfaces, implement access control, and create custom GraphQL APIs for content-driven applications.
Instructions
Schema Definition
Define your data model with Keystone's list API:
// keystone.ts — Main Keystone configuration
import { config, list } from "@keystone-6/core";
import { allowAll } from "@keystone-6/core/access";
import {
text, relationship, timestamp, select, integer,
image, json, checkbox, password, float,
} from "@keystone-6/core/fields";
import { document } from "@keystone-6/fields-document";
export default config({
db: {
provider: "postgresql", // "postgresql" | "sqlite" | "mysql"
url: process.env.DATABASE_URL!,
},
lists: {
// Blog post content type
Post: list({
access: allowAll, // Will customize below
fields: {
title: text({
validation: { isRequired: true },
isIndexed: "unique",
}),
slug: text({
validation: { isRequired: true },
isIndexed: "unique",
hooks: {
// Auto-generate slug from title if not provided
resolveInput({ resolvedData, item }) {
if (!resolvedData.slug && resolvedData.title) {
return resolvedData.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
return resolvedData.slug;
},
},
}),
status: select({
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
{ label: "Archived", value: "archived" },
],
defaultValue: "draft",
ui: { displayMode: "segmented-control" },
}),
content: document({
// Rich text editor with custom blocks
formatting: true,
links: true,
dividers: true,
layouts: [
[1, 1], // Two-column layout
[1, 1, 1], // Three-column layout
],
componentBlocks: {}, // Custom editor components
}),
heroImage: image({
storage: "localImages", // Configured in storage section
}),
publishedAt: timestamp(),
author: relationship({
ref: "User.posts", // Bidirectional relationship
ui: { displayMode: "cards", cardFields: ["name", "email"] },
}),
categories: relationship({
ref: "Category.posts",
many: true, // Many-to-many relationship
ui: { displayMode: "select" },
}),
readingTime: integer({
ui: { createView: { fieldMode: "hidden" } }, // Auto-calculated
hooks: {
resolveInput({ resolvedData }) {
if (resolvedData.content) {
const wordCount = JSON.stringify(resolvedData.content).split(/\s+/).length;
return Math.ceil(wordCount / 200); // ~200 words per minute
}
return undefined;
},
},
}),
},
}),
// Category taxonomy
Category: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true }, isIndexed: "unique" }),
description: text({ ui: { displayMode: "textarea" } }),
posts: relationship({ ref: "Post.categories", many: true }),
},
}),
// User with authentication
User: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ validation: { isRequired: true }, isIndexed: "unique" }),
password: password({
validation: { isRequired: true, length: { min: 8 } },
}),
role: select({
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Author", value: "author" },
],
defaultValue: "author",
}),
posts: relationship({ ref: "Post.author", many: true }),
},
}),
},
// File storage configuration
storage: {
localImages: {
kind: "local",
type: "image",
generateUrl: (path) => `/images${path}`,
serverRoute: { path: "/images" },
storagePath: "public/images",
},
},
// Authentication
session: {
maxAge: 60 * 60 * 24 * 30, // 30 days
secret: process.env.SESSION_SECRET!,
},
});
Access Control
Implement fine-grained permissions:
// access.ts — Role-based access control rules
import { ListAccessArgs } from "@keystone-6/core/types";
// Helper to check if user is authenticated
function isSignedIn({ session }: ListAccessArgs) {
return !!session;
}
// Helper to check user role
function hasRole(role: string) {
return ({ session }: ListAccessArgs) => session?.data?.role === role;
}
// Post-specific access rules
export const postAccess = {
operation: {
query: allowAll, // Anyone can read published posts
create: isSignedIn, // Must be logged in to create
update: isSignedIn,
delete: hasRole("admin"), // Only admins can delete
},
filter: {
// Non-admins can only see published posts (in query results)
query: ({ session }: ListAccessArgs) => {
if (session?.data?.role === "admin") return {}; // Admins see everything
return { status: { equals: "published" } };
},
// Authors can only update their own posts
update: ({ session }: ListAccessArgs) => {
if (session?.data?.role === "admin") return {};
return { author: { id: { equals: session?.itemId } } };
},
},
item: {
// Additional per-item checks
update: async ({ session, item }: any) => {
// Editors can update any post, authors only their own
if (session?.data?.role === "editor") return true;
return item.authorId === session?.itemId;
},
},
};
Custom GraphQL Extensions
Add custom queries and mutations:
// schema.extensions.ts — Extend the auto-generated GraphQL API
import { graphql } from "@keystone-6/core";
export const extendGraphqlSchema = graphql.extend((base) => ({
query: {
// Custom query: get featured posts
featuredPosts: graphql.field({
type: graphql.list(graphql.nonNull(base.object("Post"))),
args: { limit: graphql.arg({ type: graphql.Int, defaultValue: 5 }) },
async resolve(source, { limit }, context) {
return context.db.Post.findMany({
where: {
status: { equals: "published" },
},
orderBy: { publishedAt: "desc" },
take: limit,
});
},
}),
// Custom query: site statistics
siteStats: graphql.field({
type: graphql.object("SiteStats")({
fields: {
totalPosts: graphql.field({ type: graphql.Int }),
totalUsers: graphql.field({ type: graphql.Int }),
totalCategories: graphql.field({ type: graphql.Int }),
},
}),
async resolve(source, args, context) {
const [posts, users, categories] = await Promise.all([
context.db.Post.count({ where: { status: { equals: "published" } } }),
context.db.User.count(),
context.db.Category.count(),
]);
return {
totalPosts: posts,
totalUsers: users,
totalCategories: categories,
};
},
}),
},
mutation: {
// Custom mutation: publish a post
publishPost: graphql.field({
type: base.object("Post"),
args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) },
async resolve(source, { id }, context) {
return context.db.Post.updateOne({
where: { id },
data: {
status: "published",
publishedAt: new Date().toISOString(),
},
});
},
}),
},
}));
Webhooks and Hooks
React to data changes with lifecycle hooks:
// hooks/post-hooks.ts — Lifecycle hooks for post content
import { ListHooks } from "@keystone-6/core/types";
export const postHooks: ListHooks<any> = {
// Before saving to the database
resolveInput: {
create: async ({ resolvedData, context }) => {
// Auto-set the author to the current user
if (!resolvedData.author && context.session) {
resolvedData.author = { connect: { id: context.session.itemId } };
}
return resolvedData;
},
},
// After saving — trigger side effects
afterOperation: {
create: async ({ item, context }) => {
// Send notification when a new post is created
console.log(`New post created: ${item.title}`);
},
update: async ({ item, originalItem, context }) => {
// Trigger revalidation when a post is published
if (originalItem.status !== "published" && item.status === "published") {
await fetch(`${process.env.FRONTEND_URL}/api/revalidate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.REVALIDATION_SECRET,
path: `/posts/${item.slug}`,
}),
});
}
},
},
// Validate before saving
validateInput: async ({ resolvedData, addValidationError }) => {
if (resolvedData.status === "published" && !resolvedData.content) {
addValidationError("Cannot publish a post without content");
}
},
};
Installation
# Create a new Keystone project
npm create keystone-app@latest my-cms
# Or add to existing project
npm install @keystone-6/core @keystone-6/fields-document
# Run in development mode (auto-generates Prisma schema + Admin UI)
npx keystone dev
# Admin UI at http://localhost:3000
# GraphQL playground at http://localhost:3000/api/graphql
Examples
Example 1: Setting up Keystonejs with a custom configuration
User request:
I just installed Keystonejs. Help me configure it for my TypeScript + React workflow with my preferred keybindings.
The agent creates the configuration file with TypeScript-aware settings, configures relevant plugins/extensions for React development, sets up keyboard shortcuts matching the user's preferences, and verifies the setup works correctly.
Example 2: Extending Keystonejs with custom functionality
User request:
I want to add a custom access control to Keystonejs. How do I build one?
The agent scaffolds the extension/plugin project, implements the core functionality following Keystonejs's API patterns, adds configuration options, and provides testing instructions to verify it works end-to-end.
Guidelines
- Schema is code — Define your content model in TypeScript; get auto-generated Admin UI, GraphQL API, and database migrations
- Use relationships over IDs — Let Keystone manage foreign keys; bidirectional relationships (
ref: "Post.author") keep data consistent - Access control from the start — Don't use
allowAllin production; define operation + filter + item level access per list - Document field for rich content — The document field gives editors a structured rich text experience with custom blocks
- Hooks for business logic — Use
afterOperationfor side effects (notifications, cache invalidation, webhooks) - Custom GraphQL for complex queries — Extend the schema for aggregations, full-text search, or multi-step operations
- Prisma migrations — Keystone uses Prisma under the hood; run
keystone prisma migratefor production migrations - Separate API from admin — In production, protect the admin UI behind authentication; expose only the GraphQL API publicly