/SKILL.md
# WordPress-to-Jekyll Migration Playbook A reusable guide for migrating a WordPress site (via static HTML clone) to Jekyll 4.4 + Tailwind CSS 3.4, deployed on Netlify. Based on the migration of [andrewmiracle.com](https://andrewmiracle.com) — 429 static HTML pages spanning 2012–2025, converted to a fully themed Jekyll site with dark mode, digital garden features, password-protected projects, and RSS feeds. **Source:** 429 WordPress pages scraped to static HTML **Target:** Jekyll 4.4.1 + Tailwi
npx skillsauth add koolamusic/wpmigrate-skills wpmigrate-skillsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A reusable guide for migrating a WordPress site (via static HTML clone) to Jekyll 4.4 + Tailwind CSS 3.4, deployed on Netlify. Based on the migration of andrewmiracle.com — 429 static HTML pages spanning 2012–2025, converted to a fully themed Jekyll site with dark mode, digital garden features, password-protected projects, and RSS feeds.
Source: 429 WordPress pages scraped to static HTML Target: Jekyll 4.4.1 + Tailwind 3.4.17 + PostCSS Deployment: Netlify (Ruby 3.2, Node 18) Output: 178 blog posts, 50 lab projects, 10 talks, 3 notes, 8 drafts, 6 standalone pages
| Tool | Version | Purpose | |------|---------|---------| | Ruby | 3.2+ | Jekyll runtime | | Bundler | latest | Ruby dependency management | | Node.js | 18+ | Tailwind CSS compilation | | pnpm | latest | Fast Node package manager | | Python 3 | 3.8+ | Content extraction and cleanup scripts | | BeautifulSoup4 | latest | HTML parsing in Python scripts |
gem 'jekyll' # Static site generator
gem 'webrick' # Dev server (Ruby 3.x dropped it from stdlib)
gem 'jekyll-postcss-v2' # PostCSS/Tailwind integration
gem 'jekyll-feed' # RSS/Atom feed generation
gem 'jekyll-sitemap' # XML sitemap generation
gem 'jekyll-seo-tag' # Meta tags and structured data
gem 'logger' # Ruby 3.x stdlib extraction
gem 'csv' # Ruby 3.x stdlib extraction
gem 'base64' # Ruby 3.x stdlib extraction
{
"devDependencies": {
"tailwindcss": "^3.4.17",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.7",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1"
}
}
bundle install && pnpm install # Install all dependencies
bundle exec jekyll serve # Dev server at localhost:4000
bundle exec jekyll build # Production build to _site/
bundle exec jekyll build --drafts # Include _drafts/ content
Scrape the live WordPress site to a local static HTML clone using HTTrack or wget. The goal is a complete mirror preserving the original URL structure.
# HTTrack example — mirror entire site
httrack "https://andrewmiracle.com" -O ./andrewmiracle.com \
--mirror --robots=0 --depth=10
# Alternative with wget
wget --mirror --convert-links --adjust-extension \
--page-requisites --no-parent https://andrewmiracle.com
andrewmiracle.com/
├── index.html # Homepage
├── 2025/02/26/post-slug/index.html # Blog posts (YYYY/MM/DD/slug/)
├── lab/project-name/index.html # Portfolio projects
├── talks/talk-name/index.html # Speaking engagements
├── whoami/index.html # Static pages
├── wp-content/uploads/ # All media files
└── ... # 429 HTML files total
YYYY/MM/DD/slug/ permalink structure — preserve this exactlywp-content/uploads/YYYY/MM/ — these get reorganized later/embed/ variants of posts) that can be skippedA Python script parses each scraped HTML file, classifies it by URL pattern, extracts frontmatter from meta tags, pulls body content from WordPress DOM structure, and writes Jekyll-compatible files.
scripts/extract.pyClassification logic — route files to the correct Jekyll collection based on URL path:
| URL Pattern | Output | Collection |
|-------------|--------|------------|
| /YYYY/MM/DD/slug/ | _posts/YYYY-MM-DD-slug.html | Blog posts |
| /lab/slug/ | _lab/slug.html | Portfolio projects |
| /talks/slug/ | _talks/slug.html | Speaking engagements |
| /whoami/, /now/, etc. | pages/slug.html | Standalone pages |
| Everything else | Skip or manual review | — |
Frontmatter extraction — derive YAML frontmatter from HTML <meta> tags:
# OpenGraph tags → frontmatter fields
<meta property="og:title"> → title
<meta property="og:description"> → description
<meta property="og:image"> → image
<meta property="og:url"> → permalink
# Article tags → frontmatter fields
<meta property="article:published_time"> → date
<meta property="article:modified_time"> → last_modified_at
<meta property="article:tag"> → tags (multiple)
<meta property="article:section"> → categories
Content extraction — isolate the article body from WordPress page chrome:
# Target: div.post-content between header.entry-header and footer.article-tags
# This strips navigation, sidebars, related posts, comments, and footer
content = soup.find("div", class_="post-content")
Image handling:
data-src over src (WordPress lazy-loading stores real URL in data-src)?resize=800,600&ssl=1 → clean URLwp-content/uploads/2024/01/photo.jpg → /assets/images/uploads/2024/01/photo.jpgassets/images/uploads/YYYY/MM/| Collection | Count | Format | Naming Convention |
|------------|-------|--------|-------------------|
| _posts/ | 178 | HTML | YYYY-MM-DD-slug.html |
| _lab/ | 50 | HTML | slug.html (no date prefix) |
| _talks/ | 10 | HTML | slug.html |
| _drafts/ | 8 | HTML | YYYY-MM-DD-slug.html |
| pages/ | 6 | HTML | slug.html |
A second Python script (scripts/clean_lab.py) tackles WordPress markup remnants that survive extraction. This is the most labor-intensive phase — WordPress themes (especially Visual Composer / WPBakery) inject deeply nested wrapper divs, custom classes, and inline styles.
scripts/clean_lab.pyRun modes:
python3 scripts/clean_lab.py # Dry-run: prints what would change
python3 scripts/clean_lab.py --apply # Apply changes in-place
python3 scripts/clean_lab.py --backup # Backup to _lab_backup/ then apply
python3 scripts/clean_lab.py --file x.html # Process single file
The script uses BeautifulSoup and operates in strict order:
<!-- wp:paragraph -->, <!-- /wp:image -->, <!-- wp:jetpack/slideshow {...} /--> etc. via regexthb_animated_color="..." text from Visual Composer<iframe> embeds wrapped in <div class="aspect-video">blockquote.twitter-tweet for widget script injectionfigure.wp-block-image to plain <img>, preserve <figcaption>a.btn-text, div.arrow, div.thb-divider-container, span.vc_sep_holder, etc.div.wpb_row, div.row-fluid, div.vc_inner, figure.wp-block-embed, figure.wp-block-gallery, etc.vc_custom_*, thb-*, wp-block-*, etc.)<a> tags that only wrap an <img> pointing to the same imagedata-src, srcset, sizes, width, height, decoding, fetchpriority, title; remove WP classes (wp-image-*, aligncenter, size-*, etc.); add loading="lazy"; remove inline styles<span style="color:..."> that only wrap textstyle attribute from every element<h2><strong>*Title*</strong></h2> → <h2>Title</h2><h1> — If <h1> text matches frontmatter title, remove it (layout renders it)<p>, <div>, <span>, <h1>–<h6> with no text and no meaningful childrenthb-* IDs and wp-element-* classes from all elements<script async src="https://platform.twitter.com/widgets.js"> at the endAfter DOM cleanup, a text-level pass collapses multiple blank lines, trims trailing whitespace, and ensures files end with a single newline.
wordkyll/
├── _config.yml # Site config, collections, defaults, plugins
├── _posts/ # 178 blog posts (HTML + Markdown)
├── _lab/ # 50 portfolio projects
├── _talks/ # 10 speaking engagements
├── _notes/ # 3 short-form notes (Markdown only)
├── _drafts/ # 8 unpublished posts
├── _layouts/ # 7 templates
│ ├── default.html # Base: <html>, <head>, fonts, Prism.js, SEO
│ ├── home.html # Homepage: hero, feature boxes, posts, talks
│ ├── post.html # Blog: breadcrumbs, growth stage, prose, related
│ ├── portfolio.html # Lab + Talks: password gate, external links
│ ├── note.html # Notes: tags, references grid
│ ├── garden.html # Garden posts: minimal, custom bg
│ └── page.html # Generic: title + prose content
├── _includes/ # 7 reusable components
│ ├── header.html # Nav, mobile menu, dark mode toggle
│ ├── footer.html # Newsletter form, social links, theme toggle
│ ├── post-card.html # Blog post card (used in grids)
│ ├── post-placeholder.html # Emoji-based fallback when no image
│ ├── portfolio-card.html # Project card for lab/talks
│ ├── breadcrumbs.html # Section breadcrumb navigation
│ └── longform-sidebar.html # Homepage desktop sidebar
├── _plugins/
│ └── sha256_filter.rb # Custom Liquid filter for password hashing
├── pages/ # 6 standalone pages
├── assets/
│ ├── css/main.css # Tailwind directives + custom CSS
│ └── images/uploads/ # All media, organized by YYYY/MM/
├── garden/index.html # Garden page with search JS
├── notes/index.html # Notes page with tag filter JS
├── scripts/ # Python migration/cleanup scripts
├── Gemfile # Ruby dependencies
├── package.json # Node dependencies
├── tailwind.config.js # Tailwind theme configuration
├── postcss.config.js # PostCSS pipeline
└── netlify.toml # Netlify deployment config
_config.yml)permalink: /:year/:month/:day/:title/ # Preserves WordPress URL structure
collections:
lab:
output: true
permalink: /lab/:title/
talks:
output: true
permalink: /talks/:title/
notes:
output: true
permalink: /notes/:title/
defaults:
- scope: { path: "", type: "posts" }
values: { layout: "post" }
- scope: { path: "", type: "lab" }
values: { layout: "portfolio" }
- scope: { path: "", type: "talks" }
values: { layout: "portfolio" } # Talks share portfolio layout
- scope: { path: "", type: "notes" }
values: { layout: "note" }
- scope: { path: "pages" }
values: { layout: "page" }
| Plugin | Purpose |
|--------|---------|
| jekyll-postcss-v2 | PostCSS/Tailwind integration during Jekyll build |
| jekyll-feed | RSS/Atom feeds (blog, lab, notes — 3 separate feeds) |
| jekyll-sitemap | Auto-generated sitemap.xml and robots.txt |
| jekyll-seo-tag | <meta> tags, Open Graph, Twitter Cards, JSON-LD |
| sha256_filter.rb | Custom Liquid filter: {{ password \| sha256 }} for client-side password gating |
default.html ← Base: <html>, <head>, fonts, Prism.js, dark mode init, SEO
├── home.html ← Homepage: hero section, feature boxes, recent posts, talks
├── post.html ← Blog: breadcrumbs, growth stage badge, prose, references, related posts
├── portfolio.html ← Lab + Talks: password protection, external links, Schema.org
├── note.html ← Notes: tag badges, references grid
├── garden.html ← Garden posts: minimal layout, custom background
└── page.html ← Generic: title + prose content block
The design system lives in tailwind.config.js with custom tokens for colors, fonts, and typography.
Font Families:
| Token | Family | Usage |
|-------|--------|-------|
| font-serif | STIX Two Text | Headings, prose body, article content |
| font-sans | Noto Sans | Default body text, UI elements |
| font-nav | Shantell Sans | Nav labels, section headers, buttons |
| font-mono | Intel One Mono | Code blocks, dates, technical labels |
All loaded via Google Fonts CDN with <link rel="preconnect"> for performance.
Color System:
Accents: #6a9a7b (links, CTAs) → #5e8e6f (hover) → #dd8940 (alt/orange)
Surfaces: #ebedea (light bg) → #2d2d2b (dark bg) → #323230 (dark cards)
Text: #3a3f3b (body) → #3a4a40 (headers)
Links: #5e9a74 (default) → #b85f2c (hover)
Semantic: green/yellow/emerald scales (100–900) for growth stage badges
Gray: Warm green-tinted gray scale (50–900)
Typography Plugin:
Custom prose-green variant configures link colors, code styling (background, border, border-radius, font), and pre block appearance. The prose-invert variant overrides for dark mode.
Applied to all article bodies as:
<div class="prose prose-lg prose-green dark:prose-invert max-w-none font-serif
prose-headings:font-serif prose-headings:font-normal
prose-a:text-accent prose-a:no-underline hover:prose-a:text-accent-hover
prose-code:font-mono prose-code:text-sm
prose-img:ring-1 prose-img:ring-black/10 dark:prose-img:ring-white/10">
darkMode: 'class' in Tailwind config — .dark class on <html><script> in <head> checks localStorage then prefers-color-scheme before first painttoggleTheme() function in header.html bound to 3 buttons (desktop, mobile menu, footer)localStorage.setItem('theme', 'dark' | 'light')class="light-value dark:dark-value"// Inline in <head> — runs before body render
(function() {
var saved = localStorage.getItem('theme');
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
assets/css/main.css)Beyond Tailwind utilities, custom CSS handles:
pre[class*="language-"] with #2d2d2d background (light) and #1a1a1a (dark), plus zebra-stripe line backgrounds via repeating-linear-gradientlinear-gradient backgrounds using a CSS custom property --ph-hue derived from title length, with separate gradients for light/dark and growth stages::before pseudo-element, dot markers on h2::before, responsive padding.scrollbar-hide utility for horizontal scroll containersMobile-first using Tailwind default breakpoints:
sm: 640px — Tablets
md: 768px — Medium tablets
lg: 1024px — Laptops
xl: 1280px — Desktops
Common patterns:
<!-- Container -->
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<!-- Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Type scale -->
<h1 class="text-3xl sm:text-4xl lg:text-5xl">
<!-- Nav visibility -->
<nav class="hidden sm:flex"> <!-- Desktop -->
<div class="sm:hidden"> <!-- Mobile -->
Posts can declare a growth_stage in frontmatter: seedling, blossoming, or flourishing. The post layout renders a colored badge with emoji:
| Stage | Emoji | Badge Colors |
|-------|-------|-------------|
| seedling | 🌱 | Green background (bg-green-100 text-green-800) |
| blossoming | 🌼 | Yellow background (bg-yellow-100 text-yellow-800) |
| flourishing | 🌳 | Emerald background (bg-emerald-100 text-emerald-800) |
The garden index page (garden/index.html) provides client-side search filtering by title, description, and content snippets, with load-more pagination (25 posts per page).
16 lab projects use protected: true in frontmatter. The protection mechanism:
_config.yml stores the plaintext password (portfolio_password)_plugins/sha256_filter.rb hashes it: {{ site.portfolio_password | sha256 }}<script> block<template id="protected-content"> (not rendered by default)crypto.subtle.digest('SHA-256', ...)<template> into the DOMsessionStorage persists the unlock for the browser sessionLimitation: The hash is visible in page source. This is adequate for casual gating, not real security.
When a post has no featured image, an emoji-based placeholder is generated:
--ph-hue: {{ title.size | modulo: 360 }}linear-gradient uses this hue for a subtle, unique background per post_includes/post-placeholder.html and assets/css/main.cssPosts and notes can declare structured references in frontmatter:
references:
- title: "Resource Name"
url: "https://example.com"
description: "Why it's relevant"
Rendered as a card grid in post.html and note.html with bookmark icons, hover states, and external link targets.
Posts can declare intended_audience in frontmatter, rendered as a callout component above the article body:
intended_audience: "Engineers and product managers working on AI-powered tools"
The notes index page (notes/index.html) supports tag-based filtering with URL hash sync — clicking a tag updates the hash (#typography), and loading the page with a hash pre-selects that filter.
Source files → Jekyll 4.4 → PostCSS (Tailwind + Autoprefixer) → _site/
PostCSS config (postcss.config.js):
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
]
}
Important: cssnano is intentionally excluded from the pipeline due to a csso/css-tree incompatibility. Do not re-add it.
netlify.toml)[build]
command = "bundle exec jekyll build"
publish = "_site"
[build.environment]
JEKYLL_ENV = "production"
RUBY_VERSION = "3.2.0"
NODE_VERSION = "18"
Preserve WordPress URL compatibility and handle renamed pages:
/connect-with-andrew/* → /connect/ (301)
/whoami/now/ → /now/ (301)
/feed/ → /feed.xml (301)
/* → /404.html (404)
Three separate feeds via jekyll-feed:
| Feed | Path | Content |
|------|------|---------|
| Blog | /feed.xml | Latest 20 posts |
| Lab | /lab/feed.xml | Portfolio projects |
| Notes | /notes/feed.xml | Notes |
| File | Generator | Purpose |
|------|-----------|---------|
| /sitemap.xml | jekyll-sitemap | XML sitemap for search engines |
| /robots.txt | jekyll-sitemap | Crawl directives, references sitemap |
| /feed.xml | jekyll-feed | Blog RSS (20 items) |
| /lab/feed.xml | jekyll-feed | Portfolio RSS |
| /notes/feed.xml | jekyll-feed | Notes RSS |
min-w-full on fixed/absolute elements causes horizontal scroll on mobile — replaced with w-fulloverflow-hidden on <body> when open, divide-y navigation, compressed spacingprose-img:w-full prose-img:max-w-full prose-img:h-auto prevents images from overflowing containersPrism.js doesn't wrap individual lines in elements, making per-line styling impossible via selectors. Solution: repeating-linear-gradient on pre background creates zebra-stripe effect:
.prose pre {
background-image: repeating-linear-gradient(
180deg,
transparent, transparent 1.4rem,
rgba(0, 0, 0, 0.04) 1.4rem, rgba(0, 0, 0, 0.04) 2.8rem
) !important;
background-position: 0 1.25em;
background-size: 100% 2.8rem;
}
Dark mode uses rgba(255, 255, 255, 0.03) and a darker base (#1a1a1a) with an inset box-shadow border.
{% seo %} plugin + description frontmatterpage.image is setarticle:tag and article:section meta tags from frontmatterSelect posts converted from legacy HTML to Markdown for easier editing. During conversion:
references frontmatter arrayintended_audience moved from content to frontmatter fieldcssnano breaks with csso/css-tree — Installing cssnano pulls in csso which has an incompatibility with certain css-tree versions. CSS minification had to be dropped from the PostCSS pipeline. The site ships unminified CSS.
Tailwind arbitrary calc() values fail with spaces — w-[calc(100%-2rem)] works but w-[calc(100% - 2rem)] does not compile. Remove spaces inside calc() in arbitrary Tailwind values.
jekyll-postcss-v2 requires empty frontmatter — The CSS file (assets/css/main.css) must start with empty YAML frontmatter delimiters (---\n---) for Jekyll to process it through PostCSS. Without this, Tailwind directives pass through unprocessed.
Visual Composer nesting is extreme — A single image can be wrapped in 5+ layers of div.wpb_row > div.row-fluid > div.vc_inner > div.thb-image-inner > figure > a > img. The cleanup script needs multiple unwrapping passes (up to 5 iterations).
WordPress artifacts persist in unexpected places — Social share buttons, portfolio navigation markup, Gutenberg comments, and VC decorative elements (div.arrow, div.thb-divider-container) survive initial extraction and must be explicitly targeted.
data-src vs src for lazy-loaded images — WordPress lazy-loading plugins store the real image URL in data-src and put a placeholder in src. Always check data-src first during extraction.
Image query parameters must be stripped — WordPress appends ?resize=800,600&ssl=1 to image URLs. These break when served statically and must be removed during extraction.
Gutenberg block comments have varied syntax — Some are self-closing (<!-- wp:jetpack/slideshow {...} /-->), some have content between open/close tags. The regex must handle both: <!--\s*/?wp:.*?-->.
Inline style attributes override Tailwind dark mode — WordPress content often has inline style="color: #333" on elements. These override dark:text-cream classes because inline styles have higher specificity. Must strip all inline styles during cleanup.
Prism.js line styling requires background gradients — Prism doesn't wrap lines in elements, so you can't use :nth-child for zebra striping. Use repeating-linear-gradient on the pre element's background-image, carefully calibrated to line-height.
min-w-full on positioned elements causes mobile overflow — Fixed or absolute elements with min-w-full extend beyond the viewport. Use w-full instead, and add overflow-x-hidden on <body> as a safety net.
Client-side password protection is transparent — The SHA-256 hash is visible in page source. Anyone can extract it and compute the password (or just read the <template> content). This is by design — adequate for "please don't casually browse this" gating, not real access control.
Multiple collections can share a layout — _lab/ and _talks/ both use portfolio.html via _config.yml defaults. This works well when the content structure is similar, avoiding layout duplication.
Preserve WordPress permalink structure — Setting permalink: /:year/:month/:day/:title/ in _config.yml ensures all existing URLs continue working. Combined with Netlify redirects for renamed pages, this prevents 404s from external links.
Quick-reference for all project files and when you'd touch them.
| File | Purpose | When to Touch |
|------|---------|---------------|
| _config.yml | Site config, collections, defaults, plugins, feed settings | Adding collections, changing permalinks, updating plugins |
| Gemfile | Ruby dependencies | Adding Jekyll plugins |
| package.json | Node dependencies, build scripts | Adding PostCSS plugins, updating Tailwind |
| tailwind.config.js | Colors, fonts, dark mode, typography plugin | Changing design tokens, adding content paths |
| postcss.config.js | PostCSS plugin pipeline | Adding/removing PostCSS plugins (do NOT add cssnano) |
| netlify.toml | Build command, env vars, redirects | Adding redirects, changing build settings |
| File | Extends | Used By |
|------|---------|---------|
| _layouts/default.html | — | All other layouts |
| _layouts/home.html | default | Homepage (index.html) |
| _layouts/post.html | default | _posts/ |
| _layouts/portfolio.html | default | _lab/, _talks/ |
| _layouts/note.html | default | _notes/ |
| _layouts/garden.html | default | Garden-tagged posts |
| _layouts/page.html | default | pages/ |
| File | Used In | Parameters |
|------|---------|------------|
| _includes/header.html | default.html | — |
| _includes/footer.html | default.html | — |
| _includes/post-card.html | home.html, post.html | post (object) |
| _includes/post-placeholder.html | post-card.html, layouts | post, class, emoji_size |
| _includes/portfolio-card.html | home.html, experiments.html | project (object) |
| _includes/breadcrumbs.html | post.html, portfolio.html, note.html | section, section_url |
| _includes/longform-sidebar.html | home.html | — |
| Directory | Count | Format | Layout |
|-----------|-------|--------|--------|
| _posts/ | 178 | HTML + Markdown | post |
| _lab/ | 50 | HTML + Markdown | portfolio |
| _talks/ | 10 | HTML | portfolio |
| _notes/ | 3 | Markdown | note |
| _drafts/ | 8 | HTML | post |
| pages/ | 6 | HTML + Markdown | page/default |
| File | Purpose |
|------|---------|
| scripts/clean_lab.py | WordPress markup cleanup (BeautifulSoup-based, 19-step pipeline) |
| File | Purpose |
|------|---------|
| _plugins/sha256_filter.rb | Custom Liquid filter for password hashing |
| assets/css/main.css | Tailwind directives + custom CSS (Prism, placeholders, timeline) |
| garden/index.html | Garden page with client-side search and pagination |
| notes/index.html | Notes page with tag filtering and URL hash sync |
| CLAUDE.md | Engineering directive for AI-assisted development |
development
Migrate WordPress content to Jekyll. Use when asked to "convert WordPress to Jekyll", "migrate WP to Jekyll", "set up Jekyll from WordPress", "WordPress to static site", or "export WordPress to markdown". Covers content extraction, format conversion, Jekyll architecture setup, and deployment.
tools
Best practices for migrating content out of WordPress. Use when asked to "migrate from WordPress", "export WordPress content", "move off WordPress", "WordPress migration strategy", or "extract WordPress data". Covers XML export, site mirroring, plugin-specific content, and migration planning.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.