Files
rose-ash/blog/scripts/migrate_sx_html.py
giles b0920a1121 Rename all 1,169 components to path-based names with namespace support
Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
  ~sx-header → ~layouts/header
  ~layout-app-body → ~shared:layout/app-body
  ~blog-admin-dashboard → ~admin/dashboard

209 files, 4,941 replacements across all services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:00:12 +00:00

90 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""
Re-convert sx_content from lexical JSON to eliminate ~kg_cards/kg-html wrappers and
raw caption strings.
The updated lexical_to_sx converter now produces native sx expressions instead
of (1) wrapping HTML/markdown cards in (~kg_cards/kg-html :html "...") and (2) storing
captions as escaped HTML strings. This script re-runs the conversion on all
posts that already have sx_content, overwriting the old output.
Usage:
cd blog && python3 scripts/migrate_sx_html.py [--dry-run]
"""
from __future__ import annotations
import argparse
import asyncio
import sys
from sqlalchemy import select, and_
async def migrate(dry_run: bool = False) -> int:
from shared.db.session import get_session
from models.ghost_content import Post
from bp.blog.ghost.lexical_to_sx import lexical_to_sx
converted = 0
skipped = 0
errors = 0
async with get_session() as sess:
# All posts with lexical content (whether or not sx_content exists)
stmt = select(Post).where(
and_(
Post.lexical.isnot(None),
Post.lexical != "",
)
)
result = await sess.execute(stmt)
posts = result.scalars().all()
print(f"Found {len(posts)} posts with lexical content")
for post in posts:
try:
new_sx = lexical_to_sx(post.lexical)
if post.sx_content == new_sx:
skipped += 1
continue
if dry_run:
old_has_kg = "~kg_cards/kg-html" in (post.sx_content or "")
old_has_raw = "raw! caption" in (post.sx_content or "")
markers = []
if old_has_kg:
markers.append("~kg_cards/kg-html")
if old_has_raw:
markers.append("raw-caption")
tag = f" [{', '.join(markers)}]" if markers else ""
print(f" [DRY RUN] {post.slug}: {len(new_sx)} chars{tag}")
else:
post.sx_content = new_sx
print(f" Converted: {post.slug} ({len(new_sx)} chars)")
converted += 1
except Exception as e:
print(f" ERROR: {post.slug}: {e}", file=sys.stderr)
errors += 1
if not dry_run and converted:
await sess.commit()
print(f"\nDone: {converted} converted, {skipped} unchanged, {errors} errors")
return converted
def main():
parser = argparse.ArgumentParser(
description="Re-convert sx_content to eliminate ~kg_cards/kg-html and raw captions"
)
parser.add_argument("--dry-run", action="store_true",
help="Preview changes without writing to database")
args = parser.parse_args()
asyncio.run(migrate(dry_run=args.dry_run))
if __name__ == "__main__":
main()