content-writer
This skill should be used when generating full article drafts with brand-consistent voice, Eleventy frontmatter, and structured data. It requires a brand guide and existing blog infrastructure.
Content Writer
Generate full publication-ready article drafts with brand-consistent voice, Eleventy frontmatter, JSON-LD structured data, and optional FAQ sections. Content is validated against the brand guide and presented for user approval before writing to disk.
Headless Mode Detection
If $ARGUMENTS contains --headless, set HEADLESS_MODE=true and strip --headless from $ARGUMENTS. The remainder is the topic/arguments.
Argument format: <topic> [--outline <outline>] [--keywords <keywords>] [--audience <audience>] [--headless]
Headless defaults for interactive gates:
- Phase 3 (User Approval): auto-selects Accept when all citations are PASS or SOURCED. When any citation is FAIL, auto-selects Fix — removes or replaces the failed claims, re-runs fact-checker, and accepts only when all claims pass (max 2 fix cycles, then accepts with UNSOURCED markers for any remaining failures).
- If citation verification was skipped (fact-checker unavailable), auto-selects Accept with a warning in the issue.
Phase 0: Prerequisites
<critical_sequence>
Before generating content, verify both prerequisites. If either fails, display the error message and stop.
1. Brand Guide
Check if knowledge-base/marketing/brand-guide.md exists.
If missing:
No brand guide found. Run the brand-architect agent first to establish brand identity:
Use the brand-architect agent to define our brand.
Stop execution.
2. Blog Infrastructure
Check if an Eleventy config file exists (eleventy.config.js or .eleventy.js).
If missing:
No Eleventy config found. Run the docs-site skill to scaffold blog infrastructure first.
Stop execution.
</critical_sequence>
Phase 1: Parse Input
Parse the arguments provided after the skill name:
<topic>(required): the article topic or title--outline "..."(optional): article structure as inline text (Markdown list format)--keywords "kw1, kw2, kw3"(optional): target keywords, comma-separated--path <output-path>(optional): where to write the file--audience "technical|general"(optional): audience register from brand guide.technicaluses engineering vocabulary and developer proof points.generaluses plain language and business-outcome proof points. Defaults to channel-appropriate (blog → technical, landing page → general).
Default output path (if --path not provided): auto-generate from topic slug as plugins/soleur/docs/blog/YYYY-MM-DD-<slug>.md.
Phase 2: Generate Draft
Read the brand guide sections that inform content generation:
- Read
## Voice-- apply brand voice, tone, do's and don'ts - Read
## Channel Notes > ### Blog-- apply blog-specific guidelines (if the section exists) - Read
## Identity-- use mission and positioning for content alignment - If
--audienceis set, read### Audience Voice Profilesfrom brand guide and apply the matching register's vocabulary, explanation depth, and proof point selection rules. If--audienceis not set, infer from--pathor topic context (blog posts default totechnical, landing pages and onboarding content default togeneral).
Generate a full article draft that:
-
Follows the brand voice from
## Voice -
Incorporates target keywords naturally (if
--keywordsprovided) -
Follows the provided outline structure (if
--outlineprovided) -
Includes complete Eleventy frontmatter:
--- title: "<Article Title>" date: "YYYY-MM-DD" description: "<Meta description, 120-160 characters, includes primary keyword>" tags: - <relevant-tag> ---Note:
layout: "blog-post.njk"andogType: "article"are inherited fromblog/blog.json— do NOT add them to individual post frontmatter. The blog-post layout handles BlogPosting JSON-LD and OG meta tags automatically — do NOT generate inline JSON-LD in the post body. -
Generates a FAQ section with FAQPage schema if the topic naturally raises 2+ questions. Include the FAQ schema inline:
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "<question>", "acceptedAnswer": { "@type": "Answer", "text": "<answer>" } } ] } </script>
If existing posts are present in the target directory, read 1-2 of them to match frontmatter schema, layout name, and tag conventions.
Phase 2.5: Citation Verification
<validation_gate>
After generating the draft, verify all factual claims before presenting to the user.
Invoke the fact-checker agent via the Task tool, passing the full draft content:
Task fact-checker: "Verify this draft:
<full draft text>"
Parse the returned Verification Report. For each claim:
- PASS: No annotation needed
- FAIL: Insert
[FAIL: <reason>]inline after the claim in the draft - UNSOURCED: Insert
[UNSOURCED]inline after the claim in the draft
If the fact-checker agent is unavailable (e.g., Task tool not accessible), warn: "Citation verification skipped -- fact-checker agent not available. Proceed with manual verification." Continue to Phase 3.
Re-verification runs after each Edit cycle in Phase 3 -- when the user selects "Edit" and the draft is regenerated in Phase 2, Phase 2.5 re-runs on the updated draft.
</validation_gate>
Phase 3: User Approval
If Phase 2.5 produced a Verification Report, display the summary first (total claims, verified, failed, unsourced), then present the draft with any inline FAIL/UNSOURCED markers visible. If all claims passed, note "All citations verified." If verification was skipped, note "Citation verification was skipped -- manual review recommended."
If HEADLESS_MODE=true:
- If all citations are PASS/SOURCED, or verification was skipped: auto-select Accept. Proceed to Phase 4.
- If any citation has a FAIL marker: auto-select Fix. For each FAIL claim:
- Remove the unsupported statistic, quote, or claim entirely, OR
- Replace it with a verifiable alternative (search for a real source via WebSearch/WebFetch)
- Remove the
[FAIL: ...]marker after fixing
- After fixing all FAIL claims, re-run Phase 2.5 (fact-checker) on the updated draft.
- If re-verification passes (all PASS/SOURCED): auto-select Accept. Proceed to Phase 4.
- If FAIL claims persist after 2 fix cycles: convert remaining
[FAIL: ...]markers to[UNSOURCED], remove the specific claim text, and Accept the article. Do not abort — an article with conservative claims is better than no article. Note the removed claims in the GitHub audit issue.
If HEADLESS_MODE is not set (interactive mode):
Present the generated draft with word count displayed. Use the AskUserQuestion tool with three options:
- Accept -- Write article to disk
- Edit -- Provide feedback to revise the draft (return to Phase 2 with feedback incorporated)
- Reject -- Discard the draft and exit
If "Edit" is selected, ask for specific feedback, then regenerate incorporating the changes. The user can choose Edit as many times as needed.
Phase 4: Write to Disk
On acceptance, write the article to the output path.
Report: "Article written to <path>. Review and commit when ready."
Phase 4.5: OG Image Generation
Every blog post must have a unique OG image for social sharing differentiation. After writing the article:
- Check for existing
ogImagein the frontmatter. If already set, skip. - Generate a unique OG image (1200x630px) using the
gemini-imagegenskill or Pillow fallback:- Brand colors: dark background
#1a1a1a, gold accent#c4a35a - Abstract/thematic visual matching the article topic -- no text in the image (og:title provides text)
- Save to
plugins/soleur/docs/images/blog/og-<slug>.png
- Brand colors: dark background
- Add
ogImageto frontmatter:ogImage: "blog/og-<slug>.png" - The base template resolves this as
/images/{{ ogImage }}for og:image meta tags
Headless mode: Auto-generate without prompting. Interactive mode: Show the generated image and ask for approval.
Important Guidelines
- All content requires explicit user approval before writing -- no auto-write (unless
--headlessis passed, which auto-accepts on PASS citations and auto-fixes FAIL claims before accepting) - Brand guide is a hard prerequisite. Without it, the skill cannot generate brand-consistent content.
- Read the brand guide Voice section during draft generation, not as a separate post-hoc validation pass
- If outline is provided, follow it. If not, generate a reasonable article structure from the topic.
- Do not scaffold blog infrastructure. If missing, direct the user to the docs-site skill.
- The blog-post.njk layout generates BlogPosting JSON-LD automatically. Do not duplicate it in the post body.
- Frontmatter fields should match existing posts in the target directory when possible. The
date:field must be unquoted (e.g.,date: 2026-03-26, notdate: "2026-03-26") -- Eleventy'sdateToRfc3339filter requires a Date object, and quoted dates are parsed as strings. - If the brand guide's
## Channel Notes > ### Blogsection is missing, generate content using only the## Voicesection (no error). - Every factual claim, statistic, and attributed quote must have a verifiable source URL. Phase 2.5 enforces this via the fact-checker agent -- claims without citations are flagged as UNSOURCED and claims with unsupporting sources are flagged as FAIL [enforced: fact-checker agent via Phase 2.5].