Install
openclaw skills install journal-pipelineAutonomous journal content pipeline for UniqueStaysUSA. Researches keywords, writes editorial content, publishes to Payload CMS, and tracks results. Use when creating journal posts, writing blog content, planning content strategy, or the user says "next post", "write an article", "journal", "content sprint", "publish", or anything about creating travel editorial content. Also triggers on "content calendar", "what should we write next", or "run the pipeline".
openclaw skills install journal-pipelineAutonomous content machine for UniqueStaysUSA. Combines SEO research, editorial writing, quality enforcement, and Payload CMS publishing into one pipeline. Runs as a PRD-driven loop — pick next article, execute 7 phases, commit, repeat.
When invoked without arguments, this skill runs automatically:
KEYWORD_RESEARCH_AND_CONTENT_CALENDAR.md)User overrides:
/journal-pipeline best cabins near Yellowstone/journal-pipeline --research-only/journal-pipeline --draft-onlyIf no override is given, assume autonomous mode and execute the full pipeline.
Each phase maps to a PRD story type. The pipeline reads and updates scripts/ralph/prd.json for sprint state persistence.
Research what to write before writing a single word.
Read these files:
KEYWORD_RESEARCH_AND_CONTENT_CALENDAR.md — content pillars, keyword clusters, monthly scheduledocs/uniquestays-gtm-strategy.md — distribution channels, KPIs, content goalsscripts/ralph/progress.txt — what's been done, learned patternsIf no topic specified — auto-select from calendar:
GET ${NEXT_PUBLIC_SERVER_URL}/api/blog-posts?where[status][equals]=published&depth=0&limit=50
Keyword research (always run):
Article type selection:
| Signal | Type | Template |
|---|---|---|
| Topic names a specific city/region | Destination Dispatch | 3-5 stays, 1,400-2,000 words |
| Topic names a stay category | Curated Roundup | 8-12 stays, 1,800-2,500 words |
| Topic names a season/month | Seasonal Guide | 5-8 stays, 1,500-2,200 words |
| Topic names an activity | Activity-Based Guide | 4-7 stays, 1,400-2,000 words |
| Topic focuses on one property | Stay Spotlight | 1 stay, 1,000-1,500 words |
Stay selection:
# For destination dispatches
GET /api/stays?where[state][equals]={State}&where[rating][greater_than_equal]=4.7&limit=20&depth=1&sort=-rating
# For roundups
GET /api/stays?where[category][equals]={categoryId}&limit=50&depth=1&sort=-rating
# For activity-based (search by tags)
GET /api/stays?where[tags.tag][contains]={activity}&limit=20&depth=1
Select stays ensuring:
rating >= 4.7, reviewCount >= 30affiliateUrl (starts with https://)imageUrl or image relationship)Auto-proceed unless: No clear strategic gap exists — only then stop and propose alternatives.
Output: Sprint plan with target keyword, article type, selected stays, competitive angle. Update scripts/ralph/prd.json with 7 stories for this sprint.
Collect the raw material.
For each selected stay, collect from Payload (depth=1):
title, subtitle, location, state, regionprice, rating, reviewCount, platformdescription, tags (array of tag objects)affiliateUrl, imageUrlsleeps, bedroomsIdentify the "specific detail" for each stay: Find at least one concrete detail that could not apply to any other property. Sources:
tags array (e.g., "Wood-Burning Stove", "Stargazing Deck")description field (look for named landmarks, distances, species, history)External source research:
Verify:
affiliateUrlOutput: Stay data collection with specific details identified per stay. Mark RESEARCH as passed in prd.json.
Write the full editorial draft.
Invoke /elite-copywriter with:
docs/uniquestays-brand-guidelines.md)references/article-templates.mdWriting rules:
[EMBED: stay-slug] placeholders where each stay should appearreferences/quality-checklist.md)File location: content/drafts/{slug}.md
Frontmatter format:
---
title: ""
subtitle: ""
slug: ""
excerpt: ""
city: ""
state: ""
latitude: ""
longitude: ""
metaTitle: ""
metaDescription: ""
publishedAt: ""
status: "draft"
heroImage: "[description or source URL]"
linkedStays:
- stay-slug-1
- stay-slug-2
---
Output: First draft saved. Mark WRITE as passed in prd.json.
Optimize for search and AI citation. Read references/seo-requirements.md for the full checklist.
Keyword placement:
AI citation blocks:
Internal linking:
GET /api/blog-posts?where[status][equals]=published&where[state][equals]={state}&depth=0&limit=10
Meta verification:
metaTitle under 60 chars, includes keyword, reads like editorialmetaDescription under 160 chars, includes keyword + specific detailslug is kebab-case, no dates in pathCannibalization check:
Output: SEO-optimized draft. Mark SEO as passed in prd.json.
Quality gate. Score against the rubric in references/quality-checklist.md.
Scoring: Rate each of the 8 criteria (voice match, specificity, feeling-first, banned words, SEO, embeds, practical value, cut test) on a 1-10 scale with weighted average.
| Score | Action |
|---|---|
| 8.0+ | AUTO-PROCEED to Phase 6 |
| 7.0-7.9 | One more edit pass targeting failing criteria, then re-score |
| Below 7.0 | STOP — identify what's missing, may need partial rewrite |
Quality scans:
Save v2 to content/drafts/{slug}-v2.md.
Output: Quality score, specific improvements made. Mark REVIEW as passed in prd.json.
Publish directly to Payload CMS. No intermediate scripts needed.
Step 1: Resolve stay IDs
# For each stay slug in linkedStays
GET /api/stays?where[slug][equals]={stay-slug}&depth=0&limit=1
# Collect: docs[0].id
Step 2: Upload hero image (if external URL, not already in media collection)
# Fetch image, then upload to Payload media
# Use the two-step pattern from existing scripts
Step 3: Check for existing post
GET /api/blog-posts?where[slug][equals]={slug}&depth=0&limit=1
totalDocs === 0 → POST /api/blog-poststotalDocs === 1 → PATCH /api/blog-posts/{id}Step 4: Construct Lexical JSON
Use these helper functions to build the content:
function text(content: string) {
return { type: 'text', format: 0, style: '', mode: 'normal', text: content, detail: 0, version: 1 }
}
function para(content: string) {
return {
type: 'paragraph', format: '', indent: 0, version: 1, direction: 'ltr',
textFormat: 0, textStyle: '',
children: [text(content)],
}
}
function h2(content: string) {
return {
type: 'heading', tag: 'h2', format: '', indent: 0, version: 1, direction: 'ltr',
children: [text(content)],
}
}
function embedBlock(stayId: number) {
return {
type: 'block', version: 2,
fields: { id: crypto.randomUUID(), blockType: 'stayEmbed', stay: stayId },
}
}
function hr() {
return { type: 'horizontalrule', version: 1 }
}
Step 5: Two-step update (follows the pattern in scripts/update-treehouse-article.ts)
First call — update heroImage + linkedStays + editorial fields:
PATCH /api/blog-posts/{id}
{
"title": "...",
"subtitle": "...",
"excerpt": "...",
"heroImage": <media_id>,
"linkedStays": [<stay_id_1>, <stay_id_2>, ...],
"city": "...",
"state": "...",
"latitude": "...",
"longitude": "...",
"metaTitle": "...",
"metaDescription": "...",
"status": "published",
"publishedAt": "<ISO datetime>"
}
Second call — update content with Lexical JSON:
PATCH /api/blog-posts/{id}
{
"content": { "root": { ... } }
}
Step 6: Verify
# Check the API record
GET /api/blog-posts?where[slug][equals]={slug}&depth=1&limit=1
# Check the public page loads
GET https://uniquestaysusa.com/journal/{slug}
Step 7: Save final version to content/published/{slug}.md
Authentication: Authorization: users API-Key {key} — read from environment, never hardcode.
ISR revalidation: Automatic via Payload's afterChange hook in src/collections/BlogPosts.ts. No manual revalidation needed.
Output: Post live at /journal/{slug}. Mark PUBLISH as passed in prd.json.
Update tracking documents. Mandatory — never skip.
Update scripts/ralph/progress.txt:
Append a sprint summary:
### Sprint {N}: {Article Title}
**PLAN-{N}** ✓ — {keyword}, {article type}, {N} stays selected
**RESEARCH-{N}** ✓ — Stay data collected, {N} specific details identified
**WRITE-{N}** ✓ — First draft: content/drafts/{slug}.md
**SEO-{N}** ✓ — Keyword optimized, {N} internal links, FAQ added
**REVIEW-{N}** ✓ — Quality score: {X}/10, {N} edits
**PUBLISH-{N}** ✓ — content/published/{slug}.md
- Published at: /journal/{slug}
- Target keyword: {keyword}
- Word count: {N}
- Quality score: {X}/10
Record learned patterns in the progress file (what worked, what to do differently next sprint).
Verify sitemap inclusion:
GET https://uniquestaysusa.com/sitemap.xml
# Check that /journal/{slug} appears
Update scripts/ralph/prd.json:
passes: truesprintNumbercurrentStory for next sprintGit commit:
git add content/published/{slug}.md scripts/ralph/prd.json scripts/ralph/progress.txt
git commit -m "journal: publish \"{title}\" (sprint {N})"
Output: All tracking docs updated and committed. Mark SYNC as passed. Loop continues to next sprint.
The loop state lives in three files:
| File | Purpose |
|---|---|
scripts/ralph/prd.json | Sprint stories, pass/fail status, sprint number |
scripts/ralph/progress.txt | Running log of completed work and learned patterns |
.claude/ralph-loop.local.md | Ralph stop hook state (active, iteration count, completion promise) |
.claude/ralph-loop.local.md — is a Ralph loop active?scripts/ralph/prd.json — what's the current sprint and story?scripts/ralph/progress.txt — what patterns have been learned?passes: falseprd.json with passes: true/ralph-cancel or removes active: true from loop state)The existing Ralph stop hook at .claude/ralph-loop.local.md controls loop persistence across context windows. The skill reads this file at the start of each iteration. When a context window closes, the stop hook re-feeds the journal-pipeline prompt to continue where it left off.
The calendar lives at KEYWORD_RESEARCH_AND_CONTENT_CALENDAR.md. Parse the monthly tables:
| Column | Use |
|---|---|
| Week | Scheduling |
| Content Type | Journal Post vs Lead Magnet vs Programmatic |
| Title/Topic | The article topic |
| Target Keywords | Primary + secondary keywords |
| Goal | The strategic purpose |
| Calendar signal | Article type |
|---|---|
| Topic mentions a city/state/region | Destination Dispatch |
| Topic mentions a stay category (treehouses, cabins, domes) | Curated Roundup |
| Topic mentions a season or month | Seasonal Guide |
| Topic mentions an activity (stargazing, fishing, hiking) | Activity-Based Guide |
| Topic mentions a specific property by name | Stay Spotlight |
elite-copywriter for polish passes)