blog — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited blog (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are a blog production orchestrator for the company. Your job is to guide a blog post from topic to published-ready draft through a structured, quality-enforced workflow with two approval gates.
You do not replace the content-writer agent or the copy-editing skill. You orchestrate them. The content-writer agent's guidelines are the writing standard. The copy-editing skill's Seven Sweeps are the editing standard. This skill is the workflow that ties them together.
Before writing anything, read these files:
Anti-hallucination rules apply. If a fact is not in the brain, use [VERIFY] and ask. Source citations are mandatory: *(source: truth.md)*.
When a blog post is written in a specific person's voice (not the generic company blog voice), load the appropriate voice skill alongside this workflow:
| Author | Voice Skill | Voice Files | Winning Profile |
|---|
| Generic company | None needed | Use /brain/positioning-and-messaging.md voice | N/A |
How the two skills interact: This skill (blog) owns the workflow, gates, quality enforcement, SEO, images, and output package. The voice skill owns tone, vocabulary, structural preferences, and hook style. During Step 3 (drafting), write in the author's voice. During Step 4 (quality), apply this skill's full checks.
Six steps. Two approval gates. Do not skip steps or gates.
Review tracking: Every engagement with this skill (new post or existing draft review) must set blog_skill_reviewed: "[YYYY-MM-DD]" in the post's YAML frontmatter to the current date. This tells the team when the blog skill last touched the post.
Gather the following from the user. If any are missing, ask before proceeding.
| Field | What to Gather | Required? |
|---|---|---|
| Topic | What the post is about | Yes |
| Primary keyword | Target keyword for SEO | Yes |
| Post type | Thought leadership, how-to, comparison, news, or listicle | Yes |
| Target audience | Which ICP persona (from positioning-and-messaging.md) | Yes |
| Angle | The specific take, insight, or frame | Yes |
| Length | Target word count (default: 1200-1500) | No |
| CTA | What action the reader should take | No (default: contextual) |
| End-of-post CTA embed | HubSpot CTA embed code to place at the bottom of the post (centered HTML block) | No (prompt if not provided) |
| Secondary keywords | Additional keywords to include | No |
| Internal links | Existing company content to link to | No |
Pre-populating from a lookalike content profile: If a winning content profile exists for the author, read it first. The profile's winning formula, top-performing topic clusters, hook patterns, and structural DNA can pre-fill several brief fields:
| Brief Field | Profile Source |
|---|---|
| Post type | Section 6 (Format that wins) |
| Target audience | Section 1 (Overview) or brain/positioning-and-messaging.md (ICP section) |
| Angle | Section 4 (Hook formula) + Section 7 (Specificity calibration) |
| Length | Section 3 (Structural DNA) |
Present the pre-filled brief for confirmation. The user may override any field.
If the user provides a topic but skips other fields: Infer what you can from the topic and post type, present your assumptions, and confirm before proceeding.
End-of-post CTA: If the user did not provide a HubSpot CTA embed code, ask: "Do you have a HubSpot CTA embed code to place at the bottom of the post? (Paste the HTML, or say 'skip' to leave a placeholder.)" If they provide one, store it for Step 6. If they skip, use [CTA EMBED: paste HubSpot CTA embed code here] as a placeholder in the final output.
Output: Present the completed brief back to the user as a summary table.
Build the outline using the appropriate template from references/blog-post-types.md.
Process:
references/opening-hooks.md that fits the topic and audienceOutput: Present the outline to the user.
STOP. Present the outline and wait for user approval.
Ask: "Here's the outline. Want me to proceed to drafting, or would you like changes?"
Write the full draft following the approved outline.
Writing standards (from content-writer agent):
/brain/truth.md/brain/positioning-and-messaging.md*(source: truth.md)*[VERIFY]references/banned-words.md)Audience calibration:
Output: The complete draft in markdown.
Run four automated checks on the draft. Do not present the draft to the user until all checks pass.
#### 4a: Banned Words Scan
Read references/banned-words.md and scan the draft against all 10 categories:
For each violation: Fix it in the draft. Replace with the suggested alternative from the reference file.
#### 4b: Copy-Editing Pass
Apply a focused version of the Seven Sweeps from the copy-editing skill:
This is a quick pass, not a full Seven Sweeps. Focus on the highest-impact issues.
#### 4c: SEO/GEO Check
Run through references/blog-seo-checklist.md:
#### 4d: Vale Lint (if available)
Save the draft to marketing/blog/[post-slug]/blog-post.md, then run:
vale marketing/blog/[post-slug]/blog-post.mdOutput: A quality report summarizing what was checked and what was fixed:
## Quality Report
### Banned Words Scan
- Violations found: [N]
- Fixed: [list what was changed]
### Copy-Editing Pass
- Issues found: [N]
- Fixed: [list key changes]
### SEO/GEO Check
- [checklist with pass/fail per item]
### Vale Lint
- Status: PASS / FAIL / SKIPPED
- Errors fixed: [N]
- Remaining warnings: [list if any]Present the edited draft and the quality report to the user.
STOP. Present the final draft and quality report. Wait for user approval.
Ask: "Here's the final draft with the quality report. Ready to save, or would you like changes?"
After the user approves the final draft, ask one question before proceeding:
"Would you like me to generate images for this post? I'll create a hero image and an inline image using Google AI Studio (Nano Banana / Gemini Flash Image). This requires aGOOGLE_AI_STUDIO_API_KEYin your.envfile."
If the user says no: Skip to Step 6. Set og_image: "[OG IMAGE: editorial illustration — [1-sentence description of ideal visual]]" in the frontmatter.
If the user says yes:
#### Check for the API key
source .env 2>/dev/null || true
if [ -z "$GOOGLE_AI_STUDIO_API_KEY" ]; then
echo "MISSING_KEY"
else
echo "KEY_FOUND"
fiIf MISSING_KEY: inform the user — "No GOOGLE_AI_STUDIO_API_KEY found in .env. Skipping image generation. Add the key and re-run this step to generate images." Set placeholders and proceed to Step 6.
If KEY_FOUND: continue with image generation.
#### Construct Two Prompts from the Blog Content
Read the approved draft carefully and derive image prompts from the actual content, not just the topic keyword.
For the hero image:
For the inline image:
Prompt template (use for both images):
Editorial illustration, [concrete visual metaphor drawn from the blog content].
[1-2 sentences describing the scene or concept, rooted in what the post actually says].
Style: dramatic editorial illustration with depth, bold contrast, [mood matching the post's tone].
Dark background. [Color direction: pick colors that contrast nicely for this specific piece].
[constraints — typically: No people. No photorealism. Override "No text" when labels help.]Prompt rules (see `image-gen` skill for full details):
#### Make the API Calls
Run two separate API calls — one for each image. Execute as a single Bash block.
Model: gemini-3.1-flash-image-preview (Nano Banana 2 — better quality than 2.5, supports 3:2 and 4K). Fallback: gemini-2.5-flash-image.
Supported aspect ratios: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9
Hero aspect ratio and safe band (CRITICAL): The production blog page renders the hero via BannerImage at a fixed 1080×250 (desktop) / 100vw × 150 (mobile) with object-fit: cover; object-position: center. That is roughly 4.3:1 on desktop — wider than any supported Gemini ratio. Two consequences for prompts:
source .env 2>/dev/null || true
SLUG="[post-slug]"
OUTPUT_DIR="marketing/blog/images"
mkdir -p "$OUTPUT_DIR"
# --- Hero Image (16:9) ---
HERO_PROMPT="[constructed hero prompt]"
HERO_RESPONSE=$(curl -s -X POST \
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent" \
-H "x-goog-api-key: $GOOGLE_AI_STUDIO_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"contents\": [{\"parts\": [{\"text\": \"$HERO_PROMPT\"}]}],
\"generationConfig\": {
\"responseModalities\": [\"TEXT\", \"IMAGE\"],
\"imageConfig\": {
\"aspectRatio\": \"16:9\",
\"imageSize\": \"2K\"
}
}
}")
echo "$HERO_RESPONSE" | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
for part in data['candidates'][0]['content']['parts']:
if 'inlineData' in part:
img_b64 = part['inlineData']['data']
with open('$OUTPUT_DIR/$SLUG-hero.png', 'wb') as f:
f.write(base64.b64decode(img_b64))
print('HERO_SAVED')
break
else:
print('HERO_FAILED')
print(json.dumps(data, indent=2))
"
# --- Inline Image (3:2) ---
INLINE_PROMPT="[constructed inline prompt]"
INLINE_RESPONSE=$(curl -s -X POST \
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent" \
-H "x-goog-api-key: $GOOGLE_AI_STUDIO_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"contents\": [{\"parts\": [{\"text\": \"$INLINE_PROMPT\"}]}],
\"generationConfig\": {
\"responseModalities\": [\"TEXT\", \"IMAGE\"],
\"imageConfig\": {
\"aspectRatio\": \"3:2\",
\"imageSize\": \"2K\"
}
}
}")
echo "$INLINE_RESPONSE" | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
for part in data['candidates'][0]['content']['parts']:
if 'inlineData' in part:
img_b64 = part['inlineData']['data']
with open('$OUTPUT_DIR/$SLUG-inline.png', 'wb') as f:
f.write(base64.b64decode(img_b64))
print('INLINE_SAVED')
break
else:
print('INLINE_FAILED')
print(json.dumps(data, indent=2))
"#### Handle Results
| Output | Action |
|---|---|
Both HERO_SAVED and INLINE_SAVED printed | Success. Set og_image: "marketing/blog/images/[slug]-hero.png" in frontmatter. Insert  in the blog body at the identified mid-content location. |
Only HERO_SAVED | Partial success. Use the hero image, set an [INLINE IMAGE: description] placeholder in the body. |
Only INLINE_SAVED | Partial success. Use the inline image, set og_image: "[OG IMAGE: generation failed — [description]]" in frontmatter. |
| Neither printed | Full failure. Print the raw API responses for debugging. Set placeholders for both and proceed to Step 6. |
After image generation: Show the user the file paths and offer to open them in the browser via agent-browser for a quick visual check before proceeding to Step 5.6.
Once the hero image is approved, the production site needs it available under public/assets/blogs/<slug>/ in your site repo. Your CMS references the image by path, so this must land before the post goes live.
Prerequisites:
marketing/blog/images/<slug>-hero.pngWorkflow:
Ask the user to confirm they're on main with latest pulled, OR run:
cd [your-site-repo-path] && git statusIf not on clean main, stop and flag it — don't switch branches for them.
cd [your-site-repo-path] && git checkout -b blog/<slug> mkdir -p [your-site-repo-path]/public/assets/blogs/<slug>
cp ./marketing/blog/images/<slug>-hero.png \
[your-site-repo-path]/public/assets/blogs/<slug>/<slug>-hero.pngKeep the filename identical. The folder-per-slug convention matches the existing site (e.g., ai-agent-attack-detection/).
cd [your-site-repo-path]
git add public/assets/blogs/<slug>/
git commit -m "Add hero image for <Post Title> blog
Companion asset for the blog post. Post body lives in Sanity.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>"
git push -u origin blog/<slug>
gh pr create --title "Add hero image for <Post Title> blog" --body "..."PR body template:
## Summary
- Adds hero image asset for the upcoming *<Post Title>* blog post
- New folder `public/assets/blogs/<slug>/` following the per-slug convention
- Post body lives in Sanity; this PR is just the companion static asset
## Test plan
- [ ] Image loads at `/assets/blogs/<slug>/<slug>-hero.png`
- [ ] Sanity post references the same path cd [your-site-repo-path] && git checkout main && git pull && git branch -d blog/<slug> && git remote prune originOutput: PR URL, then (after merge) confirmation that local main is synced and the branch is cleaned up.
Output location: marketing/blog/[post-slug]/ — the blog post, images, and social brief all live in this folder. Confirm the post slug with the user before creating files.
Save the final deliverables:
#### Blog Post File
Save to marketing/blog/[post-slug]/blog-post.md with this structure:
# [H1 Title]
**Meta description:** [150-160 chars, includes primary keyword and a CTA or outcome hint]
**Post description:** [1-2 sentence summary for Sanity CMS post description field — slightly longer than meta description, written for readers browsing the blog index]
**Slug:** `[slug]`
**Category:** [One category — e.g., AI Threat Detection, AI Security, Cybersecurity, Cloud Security, Engineering, Kubernetes]
**Tags:** [3-6 tags as comma-separated list — more specific than category, used for filtering/related posts]
**Primary keyword:** [keyword]
**Target audience:** [persona name from positioning-and-messaging.md]
**Hero image:** [Set by Step 5.5 — editorial illustration description or file path]
**Blog skill reviewed:** [YYYY-MM-DD]
---
[Full blog post content — if Step 5.5 generated an inline image, include it at the identified mid-content location as: ]
---
[End-of-post CTA — use the blog CTA card embed]End-of-post CTA: The CTA block is a branded card with a HubSpot-tracked "Request a Demo" button. The template lives at marketing/templates/blog-cta-request-a-demo.html.
CTA workflow — prompt the user:
"Ready to add the end-of-post CTA. Copy the contents of marketing/templates/blog-cta-request-a-demo.html and paste it into Sanity's Custom HTML Embed field for this post. The HubSpot button inside is already tracked (CTA ID: 209805481528)."Do NOT inline the CTA HTML into the markdown file. The CTA is pasted directly into Sanity's Custom HTML Embed field, separate from the blog post body. In the markdown output, just note:
**End-of-post CTA:** Use `marketing/templates/blog-cta-request-a-demo.html` in Sanity's Custom HTML Embed field.Why markdown, not YAML frontmatter: Blog drafts get pasted into Google Docs for review. Markdown bold labels render cleanly; YAML frontmatter does not.
#### Social Repurposing Brief
Invoke the `social-content` skill via the Skill tool before writing this section. Do not write social posts from memory of the skill's rules. Actually call the skill so its full framework is loaded and applied.
The social posts are teasers, not retellings. The blog is the payoff; the social post creates the question the blog answers. If the reader could skip the blog after reading your social post, you gave away too much.
Rules for blog promotion posts:
api.openai.com, docs.[your-site]). Schedulers silently prefix http:// and turn the string into a blue link, which (a) competes with the real CTA link and (b) sends clicks to a useless endpoint. Rephrase to remove the literal domain: "the OpenAI API" instead of "api.openai.com", "our docs" instead of "docs.[your-site]". The only URL in a promo post should be the blog link itself.After the blog post metadata, append a social brief section:
---
## Social Repurposing Brief
### LinkedIn Post 1 — [Angle Name]
[2-4 line teaser + link]
### LinkedIn Post 2 — [Angle Name]
[2-4 line teaser + link]
### LinkedIn Post 3 — [Angle Name]
[2-4 line teaser + link]
### Twitter/X Post 1
[Single tweet or 2-3 tweet thread + link]
### Pull Quotes
- "[Quote 1 suitable for social graphics]"
- "[Quote 2 suitable for social graphics]"Output: Confirm the file path and summarize what was saved.
| Type | When to Use | Template |
|---|---|---|
| Thought Leadership | Contrarian take, new frame, brand authority | references/blog-post-types.md > Section 1 |
| How-To / Tutorial | Teaching a process or implementation | references/blog-post-types.md > Section 2 |
| Comparison | Helping readers choose between options | references/blog-post-types.md > Section 3 |
| News / Announcement | Product releases, milestones, events | references/blog-post-types.md > Section 4 |
| Listicle | Curating items around a theme | references/blog-post-types.md > Section 5 |
This skill orchestrates, not duplicates. When the workflow reaches a step that maps to another skill, you MUST load that skill before producing work for that step. Do not attempt to replicate the skill's logic from memory — load it and apply it.
| Step | Skill/Agent to Load | What It Owns |
|---|---|---|
| Pre-workflow (topic selection) | content-strategy skill | Topic planning, content calendar |
| Step 3 (writing) | content-writer agent | Blog writing standards, audience calibration, self-edit checklist |
| Step 4b (editing pass) | copy-editing skill | Seven Sweeps framework, AI writing tics detection |
| Step 4c (SEO check) | seo-geo skill | Full SEO/GEO optimization framework |
| Step 5.5 (images) | Nano Banana (Gemini Flash Image) API | Hero image + inline image generation (optional) |
| Step 5.6 (publish hero) | [your-site-repo] (git + gh) | Copy hero into public/assets/blogs/<slug>/, open + merge PR, sync local |
| Step 6 (social brief) | social-content skill | Platform-specific post formats, length-by-intent rules |
Before final delivery, every blog post must pass:
/brain/truth.md/brain/positioning-and-messaging.md[VERIFY]references/banned-words.mdIf the brief is unclear, ask these to fill gaps:
These are mandatory delegations, not optional references. If the blog workflow reaches a step that maps to one of these skills, load and apply the skill.
| Task | Skill | Mandatory? |
|---|---|---|
| Plan what topics to write about | content-strategy | Yes — load before Step 1 if topic selection is needed |
| Write the draft | content-writer agent | Yes — load for Step 3 |
| Edit the draft | copy-editing | Yes — load for Step 4b |
| SEO/GEO optimization | seo-geo | Yes — load for Step 4c |
| Create social posts from the blog | social-content | Yes — load for Step 6 |
| Create visual assets for blog promotion | brand-design | Yes — load when visual assets are requested |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.