You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ocobo replaced its CMS with a markdown content repo (ocobo-revops/posts). The team that produces this content — blog posts, customer stories, team member cards, tool entries, job postings — is mostly non-technical and works in Notion + NotionAI day-to-day. Today, every piece of content has to be hand-translated into YAML frontmatter + markdown, slugs must be coordinated across types (a blog post's author must match an existing team member slug), assets uploaded to Vercel Blob, and a PR opened against main. There is no validation: a malformed frontmatter only surfaces when the website fails to render.
This friction means content production funnels through one or two technical maintainers, who become a bottleneck.
Solution
Ship a set of Claude Code skills inside the repo (.claude/skills/) that let any team member, working from Claude Code, take a content draft from any source and publish it as a validated PR — without writing YAML or running git/gh commands by hand.
Three skills total:
new-content — source-agnostic: accepts a Notion URL, a local file path, or no argument (interview mode). Asks for the content type if not provided as --type. Maps the source to validated frontmatter + markdown and writes the file.
publish-content — strict preconditions, then branches, validates, commits, pushes, opens PR.
translate-content — duplicates a file to its other-language equivalent.
Three source modes for new-content:
Interview — Claude asks for all required fields. No external setup needed. Works immediately after pnpm install.
Local file — pass any local file path (markdown draft, text, export). Claude maps what it finds and asks only for what's missing.
Notion URL — pass a Notion page URL. Claude fetches the page via the Notion MCP, maps properties to frontmatter, downloads images. Requires per-user MCP setup (optional, documented in docs/mcp-notion-setup.md).
Notion is the preferred staging area for polished drafts; GitHub is the source of truth after import — corrections happen on the markdown, not back in Notion. (See docs/adr/0001-notion-as-content-staging.md.)
User Stories
As an Ocobo content author, I want to draft a blog post in Notion using a structured template, so that I can write in my familiar tool without learning YAML.
As an Ocobo content author, I want to invoke a Claude Code command with my Notion page URL, so that the import happens in one step.
As an Ocobo content author, I want the skill to ask me for any field missing from my source, so that I don't have to redo the draft.
As an Ocobo content author, I want a fallback interview flow when I have no draft ready, so that I can still publish short content quickly.
As an Ocobo content author, I want to pass a local draft file and have Claude extract what it can, so that I can publish without re-entering information I've already written.
As an Ocobo content author, I want the skill to validate cross-references (the author exists in /team/, the featuredTool exists in /tools/), so that I don't publish a broken reference.
As an Ocobo content author, I want the skill to autocomplete fields like tags, scopes, category from existing values, so that I don't accidentally fragment the vocabulary.
As an Ocobo content author, I want the skill to accept new values for those fields but warn me, so that I'm not blocked but I'm aware I'm introducing a new term.
As an Ocobo content author, I want to drag-and-drop image paths (cover, avatar, client logos) into the conversation, so that I don't have to copy files manually.
As an Ocobo content author, I want the skill to copy images into the right asset folder with the right name, so that I don't have to remember the naming convention.
As an Ocobo content author, I want the skill to preview the final frontmatter before writing the file, so that I can catch mistakes before commit.
As an Ocobo content author, I want a single command to sync assets to Vercel Blob, commit, push, and open a PR, so that I don't have to remember the publish steps.
As an Ocobo content author, I want the PR to have a templated body with a checklist, so that the reviewer knows what to verify.
As an Ocobo content author writing about a new team member, I want the skill to require both French and English role and bio, so that the team member card is not broken on either language of the site.
As an Ocobo content author writing about an existing customer story, I want the skill to require featuredTool to be one of the tools I listed, so that the story page renders consistently.
As an Ocobo content author writing a job posting, I want the skill to enforce the section structure (### La Mission, ### Responsabilités, ### Profil recherché), so that all jobs render with the same layout.
As an Ocobo translator, I want a translate-content skill that takes a French file and produces an English version (or vice versa), so that I can extend a piece of content to the other language without rewriting the frontmatter.
As an Ocobo content maintainer, I want to be able to run pnpm validate and get a clear list of what's broken across the repo, so that I can fix incoherences during housekeeping passes.
As a website developer consuming this content, I want frontmatter that conforms to a documented schema, so that I can trust the shape of the data I render.
As an Ocobo admin, I want documentation explaining how to set up the Notion databases and templates, so that I can roll out the Notion source mode to the team when ready.
As an Ocobo team member starting from scratch, I want a setup guide for the MCP Notion connection, so that I can authenticate against the Ocobo workspace in my Claude Code.
As a maintainer, I want strict git preconditions (clean working tree, on main, up to date) before the publish skill runs, so that PRs are clean and easy to review.
As a maintainer, I want each PR to be branched as content/<type>-<slug> with a conventional commit, so that the history is predictable.
As a maintainer, I want a typo that exists in the current website (exerpt instead of excerpt) to be perpetuated by the schema, so that the website does not break — and a separate issue to fix it later.
As a developer working on the schemas, I want the validation modules to have unit tests, so that I can refactor without breaking the contract.
Implementation Decisions
Domain glossary
Defined in CONTEXT.md at the repo root. Five content types: blog post, story, team member, tool, job. Generic umbrella term: content (avoid "post" alone). Cross-references between types use slugs (kebab-case filenames).
Modules
scripts/schemas/ — one Zod schema per content type (blog-post.schema.js, story.schema.js, team-member.schema.js, tool.schema.js, job.schema.js). Pure functions, written in JS ESM (no TypeScript toolchain). Encode the typo exerpt as-is.
scripts/source-adapters/ — one adapter per source mode:
local-file.js — reads any text file, returns {fields, body} (best-effort extraction).
scripts/asset-path-resolver.js — given (type, slug, variant?), returns the canonical relative path under assets/. Pure.
scripts/validate-content.js — orchestrates: glob all .md under blog/, stories/, team/, tools/, jobs/, parses via gray-matter, applies the right schema, runs cross-ref checks. Wired to pnpm validate.
Skills (3 total, in .claude/skills/)
new-content — source-agnostic skill. Takes an optional --type <type> flag and an optional source argument (Notion URL or local file path). Without a source argument: interview mode. With a file path: local file adapter. With a Notion URL: Notion adapter. All three modes converge on the same validation → preview → write → asset copy flow.
publish-content — strict preconditions (clean tree, on main, up to date), then branches content/<type>-<slug>, runs pnpm sync-assets, runs pnpm validate, commits with feat(<type>): add <slug>, pushes, opens PR via gh pr create with a templated body. No auto-assigned reviewer, no auto labels.
translate-content — duplicates a file from one language directory to the other (blog/story/job) or fills the missing language in the dict frontmatter (team). Tools are monolingual and rejected.
Cross-reference strictness
Validation rejects references to non-existent slugs. It tolerates references to team members with active: false (a member who left keeps their old content valid). Skills, when offering autocomplete pickers, only propose active members/tools.
Free-text fields with vocabulary drift
tags (blog), scopes (story), category (tool) stay as free arrays/strings in the schema. Skills autocomplete from existing values sorted by frequency. Authors can add new values; the skill warns but does not block.
Multi-language
Path-based for blog, story, job — <type>/fr/<slug>.md or <type>/en/<slug>.md. French is the default.
Dict-based for team — frontmatter role: {fr, en} and bio: {fr, en}, both required at creation.
Tools are monolingual (English).
Notion integration
Optional. Each user who wants to use the Notion source mode configures an MCP Notion connection authenticated against the Ocobo workspace (setup guide: docs/mcp-notion-setup.md). The skill detects whether a Notion MCP is available and falls back gracefully if not.
Git workflow
The skill assumes raw git (no GitButler). Pre-condition: clean tree on main up to date. Branch naming: content/<type>-<slug>. Conventional commit feat(<type>): add <slug>. PR title = commit message. PR body templated.
Documentation
CONTEXT.md (already created) — domain glossary.
docs/adr/0001-notion-as-content-staging.md (already created) — the unidirectional sync decision.
CLAUDE.md — entry point for Claude Code: pointers to the 3 skills, canonical workflow, the 3 source modes, traps.
docs/notion-templates/ — five <type>.md files + a README.md, each with the property mapping, the NotionAI scaffolding prompt, and the body template.
docs/mcp-notion-setup.md — per-user setup (optional, only needed for Notion source mode).
Testing Decisions
Tests live in scripts/__tests__/. Framework: Vitest (ESM-native, no TS toolchain required). Tested modules:
scripts/schemas/*.schema.js — happy path + each invalid frontmatter shape.
Problem Statement
Ocobo replaced its CMS with a markdown content repo (
ocobo-revops/posts). The team that produces this content — blog posts, customer stories, team member cards, tool entries, job postings — is mostly non-technical and works in Notion + NotionAI day-to-day. Today, every piece of content has to be hand-translated into YAML frontmatter + markdown, slugs must be coordinated across types (a blog post'sauthormust match an existing team member slug), assets uploaded to Vercel Blob, and a PR opened againstmain. There is no validation: a malformed frontmatter only surfaces when the website fails to render.This friction means content production funnels through one or two technical maintainers, who become a bottleneck.
Solution
Ship a set of Claude Code skills inside the repo (
.claude/skills/) that let any team member, working from Claude Code, take a content draft from any source and publish it as a validated PR — without writing YAML or running git/gh commands by hand.Three skills total:
new-content— source-agnostic: accepts a Notion URL, a local file path, or no argument (interview mode). Asks for the content type if not provided as--type. Maps the source to validated frontmatter + markdown and writes the file.publish-content— strict preconditions, then branches, validates, commits, pushes, opens PR.translate-content— duplicates a file to its other-language equivalent.Three source modes for
new-content:pnpm install.docs/mcp-notion-setup.md).Notion is the preferred staging area for polished drafts; GitHub is the source of truth after import — corrections happen on the markdown, not back in Notion. (See
docs/adr/0001-notion-as-content-staging.md.)User Stories
authorexists in/team/, thefeaturedToolexists in/tools/), so that I don't publish a broken reference.tags,scopes,categoryfrom existing values, so that I don't accidentally fragment the vocabulary.roleandbio, so that the team member card is not broken on either language of the site.featuredToolto be one of the tools I listed, so that the story page renders consistently.### La Mission,### Responsabilités,### Profil recherché), so that all jobs render with the same layout.translate-contentskill that takes a French file and produces an English version (or vice versa), so that I can extend a piece of content to the other language without rewriting the frontmatter.pnpm validateand get a clear list of what's broken across the repo, so that I can fix incoherences during housekeeping passes.main, up to date) before the publish skill runs, so that PRs are clean and easy to review.content/<type>-<slug>with a conventional commit, so that the history is predictable.exerptinstead ofexcerpt) to be perpetuated by the schema, so that the website does not break — and a separate issue to fix it later.Implementation Decisions
Domain glossary
Defined in
CONTEXT.mdat the repo root. Five content types: blog post, story, team member, tool, job. Generic umbrella term: content (avoid "post" alone). Cross-references between types use slugs (kebab-case filenames).Modules
scripts/schemas/— one Zod schema per content type (blog-post.schema.js,story.schema.js,team-member.schema.js,tool.schema.js,job.schema.js). Pure functions, written in JS ESM (no TypeScript toolchain). Encode the typoexerptas-is.scripts/source-adapters/— one adapter per source mode:local-file.js— reads any text file, returns{fields, body}(best-effort extraction).notion.js— takes a Notion page object (from MCP), returns{fields, body, assets[]}.scripts/cross-ref-resolver.js— loadsteam/*.mdandtools/*.mdonce, exposesisValidTeamSlug(slug),getActiveTeamMembers(),isValidToolSlug(slug),getActiveTools(). Deep, pure.scripts/asset-path-resolver.js— given(type, slug, variant?), returns the canonical relative path underassets/. Pure.scripts/validate-content.js— orchestrates: glob all.mdunderblog/,stories/,team/,tools/,jobs/, parses viagray-matter, applies the right schema, runs cross-ref checks. Wired topnpm validate.Skills (3 total, in
.claude/skills/)new-content— source-agnostic skill. Takes an optional--type <type>flag and an optional source argument (Notion URL or local file path). Without a source argument: interview mode. With a file path: local file adapter. With a Notion URL: Notion adapter. All three modes converge on the same validation → preview → write → asset copy flow.publish-content— strict preconditions (clean tree, onmain, up to date), then branchescontent/<type>-<slug>, runspnpm sync-assets, runspnpm validate, commits withfeat(<type>): add <slug>, pushes, opens PR viagh pr createwith a templated body. No auto-assigned reviewer, no auto labels.translate-content— duplicates a file from one language directory to the other (blog/story/job) or fills the missing language in the dict frontmatter (team). Tools are monolingual and rejected.Cross-reference strictness
Validation rejects references to non-existent slugs. It tolerates references to team members with
active: false(a member who left keeps their old content valid). Skills, when offering autocomplete pickers, only propose active members/tools.Free-text fields with vocabulary drift
tags(blog),scopes(story),category(tool) stay as free arrays/strings in the schema. Skills autocomplete from existing values sorted by frequency. Authors can add new values; the skill warns but does not block.Multi-language
<type>/fr/<slug>.mdor<type>/en/<slug>.md. French is the default.role: {fr, en}andbio: {fr, en}, both required at creation.Notion integration
Optional. Each user who wants to use the Notion source mode configures an MCP Notion connection authenticated against the Ocobo workspace (setup guide:
docs/mcp-notion-setup.md). The skill detects whether a Notion MCP is available and falls back gracefully if not.Git workflow
The skill assumes raw git (no GitButler). Pre-condition: clean tree on
mainup to date. Branch naming:content/<type>-<slug>. Conventional commitfeat(<type>): add <slug>. PR title = commit message. PR body templated.Documentation
CONTEXT.md(already created) — domain glossary.docs/adr/0001-notion-as-content-staging.md(already created) — the unidirectional sync decision.CLAUDE.md— entry point for Claude Code: pointers to the 3 skills, canonical workflow, the 3 source modes, traps.docs/notion-templates/— five<type>.mdfiles + aREADME.md, each with the property mapping, the NotionAI scaffolding prompt, and the body template.docs/mcp-notion-setup.md— per-user setup (optional, only needed for Notion source mode).Testing Decisions
Tests live in
scripts/__tests__/. Framework: Vitest (ESM-native, no TS toolchain required). Tested modules:scripts/schemas/*.schema.js— happy path + each invalid frontmatter shape.scripts/cross-ref-resolver.js— happy path + missing slugs + inactive members.scripts/source-adapters/notion.js— happy path per content type + missing properties + image extraction.scripts/source-adapters/local-file.js— file with frontmatter + plain prose + key:value format.scripts/asset-path-resolver.js— all (type, variant) combinations.The three
SKILL.mdfiles are not unit-tested — they are prompts, validated by manual end-to-end runs.Out of Scope
exerpttypo (separate issue).publish-contentrunspnpm validate; manual commits bypass it knowingly).CLAUDE.md.Implementation Order (6 slices)
publish-contentnew-contentskill, interview mode (all 5 types) → first usable authoring flow, no external setup requirednew-contentskill, local file adapternew-contentskill, Notion adapter + alldocs/notion-templates/+docs/mcp-notion-setup.mdtranslate-contentCLAUDE.md+ README "Adding content" sectionSlices #22–#23 give a complete working product. #24–#25 add progressive enhancements. Notion MCP is never a prerequisite.