Skip to content

Content authoring skills: Notion → markdown → PR via Claude Code #21

Description

@wab

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'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:

  1. Interview — Claude asks for all required fields. No external setup needed. Works immediately after pnpm install.
  2. Local file — pass any local file path (markdown draft, text, export). Claude maps what it finds and asks only for what's missing.
  3. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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.
  22. 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.
  23. As a maintainer, I want each PR to be branched as content/<type>-<slug> with a conventional commit, so that the history is predictable.
  24. 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.
  25. 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).
    • notion.js — takes a Notion page object (from MCP), returns {fields, body, assets[]}.
    • Interview mode has no adapter module — it's orchestrated by the skill prompt directly.
  • scripts/cross-ref-resolver.js — loads team/*.md and tools/*.md once, exposes isValidTeamSlug(slug), getActiveTeamMembers(), isValidToolSlug(slug), getActiveTools(). Deep, pure.
  • 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.
  • 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.md files are not unit-tested — they are prompts, validated by manual end-to-end runs.

Out of Scope

  • Fixing the exerpt typo (separate issue).
  • Cleaning up tag / scope / category vocabulary drift (separate issue).
  • Bidirectional Notion ↔ GitHub sync (explicitly rejected; see ADR 0001).
  • Image processing (resizing, generating logo variants).
  • Notion database creation automation — admin scaffolds DBs manually using the NotionAI prompts.
  • Pre-commit hooks (publish-content runs pnpm validate; manual commits bypass it knowingly).
  • Update skills — the dominant flow is creation, updates happen in free-form conversation guided by CLAUDE.md.
  • Migration of existing non-conforming content (deferred to a separate hygiene pass).
  • Translation skill scope is limited to file duplication + helping Claude translate — no integration with translation services.

Implementation Order (6 slices)

  1. [Slice 1] Foundation — all schemas + cross-ref + validate + vitest + publish-content #22 — Foundation: all 5 schemas + cross-ref resolver + validate-content + vitest + publish-content
  2. [Slice 2] new-content skill — interview mode (all 5 types) #23new-content skill, interview mode (all 5 types) → first usable authoring flow, no external setup required
  3. [Slice 3] new-content skill — local file adapter #24new-content skill, local file adapter
  4. [Slice 4] new-content skill — Notion adapter #25new-content skill, Notion adapter + all docs/notion-templates/ + docs/mcp-notion-setup.md
  5. [Slice 7] translate-content skill #28translate-content
  6. [Slice 8] CLAUDE.md entry point + README "Adding content" section #29CLAUDE.md + README "Adding content" section

Slices #22#23 give a complete working product. #24#25 add progressive enhancements. Notion MCP is never a prerequisite.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or requestready-for-humanNeeds human implementation

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions