Skip to content

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

@wab

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 Notion page they've drafted and publish it as a validated PR. Each skill knows the canonical structure for its content type, reads the source page from Notion via an authenticated MCP connection, maps Notion properties to markdown frontmatter, downloads page images into the right asset folder, runs schema validation, and opens the PR with a templated body — without the author having to write YAML or run git/gh commands by hand.

An interview fallback covers cases where Notion is impractical (urgent fix, short content like a new tool entry).

Notion is the staging area; once a page is imported, GitHub is the source of truth — 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 Notion page, so that I don't have to redo the page in Notion.
  4. As an Ocobo content author, I want a fallback interview flow when I don't want to use Notion, so that I can still publish a short piece of content quickly.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. As an Ocobo admin, I want documentation explaining how to set up the Notion databases and templates, so that I can roll out the skills to the team.
  20. As an Ocobo admin, I want a NotionAI prompt that scaffolds the right database for each content type, so that I don't have to build the schema manually.
  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/cross-ref-resolver.js — loads team/*.md and tools/*.md once, exposes isValidTeamSlug(slug), getActiveTeamMembers(), isValidToolSlug(slug). Deep, pure.
  • scripts/notion-to-markdown.js — given a Notion page object (from MCP), returns {frontmatter, body, assets[]}. Handles property→field mapping and image discovery.
  • 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 (7 total, in .claude/skills/)

  • new-blog-post, new-story, new-team-member, new-tool, new-job — each multimode: argument = Notion URL → import flow, no argument → interview fallback. Production scope is creation only; updates happen in free-form conversation with Claude guided by CLAUDE.md.
  • 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.
  • 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 (type, source [Notion URL or interview], files added, review checklist). No auto-assigned reviewer, no auto labels.

Cross-reference strictness

Validation rejects references to non-existent slugs (cas 1–4 in the grill session). 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.

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. A separate issue tracks vocabulary cleanup.

Multi-language

  • Path-based for blog, story, job — write in <type>/fr/<slug>.md or <type>/en/<slug>.md. French is the default; the skill asks before writing English only when explicitly requested.
  • Dict-based for team — frontmatter role: {fr, en} and bio: {fr, en}, both required at creation.
  • Tools are monolingual (English).

Notion integration

Each user configures an MCP Notion connection authenticated against the Ocobo workspace (setup guide: docs/mcp-notion-setup.md). The skill fetches the page via MCP, maps Notion properties → frontmatter following the mapping documented in docs/notion-templates/<type>.md. Each <type>.md includes a NotionAI prompt the admin can paste into Notion to scaffold the database and a sample template page.

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 7 skills, conventions, 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.

Testing Decisions

A good test here tests behaviour observable from outside the module, not its internals. For schema modules: input frontmatter object → expected {ok, errors}. For the cross-ref resolver: given a fixture set of fake team/tool files, expected query outputs. For notion-to-markdown: given a fixture Notion page JSON, expected {frontmatter, body, assets[]}. For asset-path-resolver: pure input → output.

Tests live in scripts/__tests__/ next to the modules. Framework: Vitest (ESM-native, no TS toolchain required, fast). Tested modules:

  • scripts/schemas/*.schema.js — happy path + each invalid frontmatter shape.
  • scripts/cross-ref-resolver.js — happy path + missing slugs + inactive members.
  • scripts/notion-to-markdown.js — happy path per content type + missing properties + image extraction.
  • scripts/asset-path-resolver.js — all (type, variant) combinations.

The seven SKILL.md files are not unit-tested — they are prompts, validated by manual end-to-end runs (see verification list in the design plan).

The repo has no prior tests; this PRD introduces the test infrastructure.

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.

Further Notes

  • The repo is on GitButler for maintainer work, but the skills target raw git so any team member can use them after a fresh clone.
  • The 14 grilling decisions that produced this PRD are recorded in the design plan (in the maintainer's local plan store, not shipped). Key decisions visible in the repo: CONTEXT.md and docs/adr/0001-notion-as-content-staging.md.
  • Suggested implementation order: (1) Zod schemas + cross-ref resolver + validate-content + vitest baseline; (2) one end-to-end skill (new-tool is the simplest, smallest schema) including publish-content; (3) replicate to the four other types; (4) translate-content; (5) docs/notion-templates/ + docs/mcp-notion-setup.md; (6) CLAUDE.md entry point.

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