Autonomous 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:
```bash
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:
```bash
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)共 1 个版本
暂无安全检测报告