Automated Blogger Theme XML Upload — Zero Manual Clicks with Playwright + 9222 CDP

5 min read · 1,083 words

Tips & Tricks / Blog Management / Python & Automation
Approx. 2,500 characters

One of the downsides of Blogger is that every time you change the theme XML, it takes nearly 5 minutes to go through the admin UI—clicking "Theme → Backup / Restore → Upload" 3 to 4 times, verifying login, and waiting for the changes to apply. We automated this using Playwright and Chrome 9222 CDP, reducing manual clicks to zero. Here, we share the motivation behind this project, how it works, its real-world impact, and how we validated it.

Why We Built It

During peak periods, theme modifications can happen 5 to 10 times a day. If it takes 5 minutes every time you change a single line of color, font weight, or sidebar positioning just to see the live results, your workflow gets completely disrupted.

Furthermore, Blogger's legacy "Restore Theme" path contains a trigger for the 1st-generation Classic mode (like the former site name, BTP). Accidentally clicking it once reverts the entire site layout back to the outdated 1st generation. After experiencing this twice, we permanently blocked the "Restore" path in our code. The only safe route is to directly inject the entire XML using setValue into the CodeMirror editor on the admin's "Edit HTML" screen.

We built an automated module so we wouldn't have to perform this workflow manually every single time.

How It Works

The core mechanism relies on Playwright attaching to an existing Chrome instance already running on port 9222 (CDP attach) to send commands. It does not launch a new browser instance. This ensures that the Google login remains active, bypassing CAPTCHA and 2FA automatically.

The workflow is as follows:

  1. Check if Chrome 9222 is alive — If the response from http://127.0.0.1:9222/json/version is 200, it is active. If it is down, a Discord notification alerts the user to "Please launch Chrome on port 9222." Automatic startup is branched by OS.
  2. Playwright CDP attach — A single line: playwright.chromium.connect_over_cdp("http://127.0.0.1:9222"). No new browser is launched.
  3. Find the target tab — If there is an already open tab matching blogger.com/blog/themes/, navigate to it. Otherwise, open a new tab.
  4. Click the "Edit HTML" button — Using an aria-label or text selector. Supports both Korean and English locales.
  5. Wait for the CodeMirror editor to activate — Poll until document.querySelector('.CodeMirror') appears.
  6. Call CodeMirror.setValue(xml) — Called directly inside Playwright's evaluate function. This is not a clipboard paste or simulated typing; even a 50KB XML is applied instantly.
  7. Click the "Save" button — Once saved, wait for the "Theme updated" toast message to appear.
  8. Live spot check — Wait 60 seconds, then fetch https://blog-url/. If the sentinel comment in the theme.xml (e.g., ) is included in the response, verified=True.

The entire process takes an average of 30 seconds. Zero manual clicks.

Real-World Impact

  • Single upload time: 5 minutes → 30 seconds (90% reduction)
  • Cumulative automated uploads: Approx. 320 times since deployment
  • Incidents of reverting to 1st-generation Classic mode via accidental "Restore" clicks: 2 cases before deployment → 0 cases after deployment
  • Failure cases: 4 instances of Chrome 9222 crashing / 2 instances of Google login expiration / 1 CAPTCHA issue. All triggered user notifications → resolved via manual recovery.
  • Backup: Automatically saves theme.xml.before. before every upload. If an upload fails or goes wrong, we can roll back to the previous version within 1 second.

As an added benefit, we've seen an increase in light experimentation, like "let's just change this one line of the theme." When you think it takes 5 minutes every time, you hesitate, but with a 30-second turnaround, you just go for it. Consequently, design iterations have become much faster.

Validation Methods

We performed three types of validation.

XML round-trip validation — After uploading, we call getValue() again from CodeMirror on the same page to verify if the value is byte-by-byte identical to the XML we sent. This acts as a safety net to catch cases where Blogger's SkinVariables parser silently rejects the upload (e.g., when there are raw HTML tokens inside CDATA). Out of 320 runs, we caught 5 silent reject cases.

Live spot check — After uploading, we wait 60 seconds and fetch the live site. We verify if the sentinel marker in theme.xml is present in the response. 320/320 passed.

Prevention test for 1st-generation Classic mode reversion — We wrote a unit test to verify that if our automation intentionally attempts to click the "Restore" button, it is blocked at the code level. It always passes at assert "restore" not in click_targets, proving the blocking hook works perfectly.

How to Build It Yourself

Rather than porting the entire module, it is more practical to just take the two core lines.

First, launch Chrome on port 9222.


# Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" \
 --remote-debugging-port=9222 \
 --user-data-dir=C:\chrome_debug_profile

# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
 --remote-debugging-port=9222 \
 --user-data-dir=$HOME/chrome_debug_profile

Next, attach to it using Playwright.


import asyncio
from playwright.async_api import async_playwright

BLOG_ID = "1234567890"
THEME_XML = open("theme.xml", encoding="utf-8").read()

async def upload_theme(xml: str):
 async with async_playwright() as p:
 browser = await p.chromium.connect_over_cdp("http://127.0.0.1:9222")
 ctx = browser.contexts[0]
 page = await ctx.new_page()
 await page.goto(f"https://www.blogger.com/blog/themes/{BLOG_ID}")
 # "Edit HTML"
 await page.click("text=현재 테마의 소스 코드 수정")
 await page.wait_for_selector(".CodeMirror", timeout=30000)
 await page.evaluate(
 "(xml) => document.querySelector('.CodeMirror').CodeMirror.setValue(xml)",
 xml,
 )
 await page.click("button:has-text('테마 저장')")
 await page.wait_for_selector("text=테마가 업데이트되었습니다", timeout=60000)
 await browser.close()
 print("uploaded")

asyncio.run(upload_theme(THEME_XML))

The key here is just one line of connect_over_cdp and one line of CodeMirror.setValue. The rest is just refining selectors.

Never click the restore path. The "Restore" button is located on the same page, which was the source of our incidents. Clicking it once reverts the site to 1st-generation Classic, breaking the live site. It is safer to explicitly block that selector in your automation code.

Summary: Launch Chrome on port 9222, attach with Playwright, and call a single line of CodeMirror.setValue to automate your Blogger theme uploads. Once you complete the initial setup, all future updates can be automated with a single-line command.

Category Coverage Notice

This article follows our label-specific editorial criteria. Details:

ToolSignal Pro Editorial

ToolSignal Pro는 AI·IT·소프트웨어 트렌드를 다루는 종합 IT 인사이트 매거진입니다.

이전 글 다음 글