Writing on a Naver blog with AI: fully automated up to saving the draft
Also in: Bahasa Indonesia · हिन्दी · العربية · ไทย
Uploading one post a day to a Naver blog used to take me over 30 minutes by hand every time. Now I've automated it to the point where AI drives the browser directly, puts in the text, photos, captions and title, and saves it as a draft.
Yesterday it worked end to end for the first time. All I do is open the saved draft, skim it once, and press publish. What matters most about this work is that it's good enough to use in my job right away.
How it used to be
Part of my job is moving articles from our company blog, written by an outside agency on inblog, over to Naver blog. Naver is Korea's main search portal. I don't copy the articles as they are. I rework the style and structure to suit Naver search. About one article a day. I was already doing the rewriting with AI. But each time there was still work left: putting it into the Naver editor with lots of clicking, uploading photos, and touching it up again in my own style.
Naver blog has no official API for publishing posts. So whatever the AI wrote, a person ultimately had to carry over by hand. To cut that down, I had made bookmark buttons (bookmarklets) and PowerShell commands.
| Order | Who | Task |
|---|---|---|
| 1 | AI | Make a work sheet with the body text, title options, list of photos, and captions |
| 2 | Person | Copy the "fetch photos" command from the work sheet and paste it into PowerShell |
| 3 | Person | Click "copy body" and paste it into the Naver editor |
| 4 | Person | Click the "convert format" bookmark (changes the shape of quotes and dividers) |
| 5 | Person | Click the "insert photo" bookmark once per photo, choosing the photo in the file picker each time |
| 6 | Person | Click each photo's caption field and paste the caption |
| 7 | Person | Type the title, check the link card, publish |
The buttons did cut it down a lot. Still, with more than ten photos it meant dozens of clicks. Automatic captions had failed, so they were only copied to the clipboard and a person pasted them in. One post took 30 minutes at the very least.
The new way: AI drives the browser with code
The key is Playwright MCP. Playwright is a tool for controlling a browser with code, and once it's connected through MCP, AI can use it directly. It doesn't take a screenshot and click coordinates. It reads the structure of the page, picks elements, and operates on them. So it's fast, and it rarely misses.
This is how I connected it.
| Step | Detail |
|---|---|
| 1 | Add --remote-debugging-port=9222 to the end of the Whale (Naver's browser) shortcut target, and launch Whale from that shortcut |
| 2 | Register Playwright MCP in the Claude desktop app's config file, so it attaches to the Whale already running on port 9222 |
| 3 | Quit the app completely, then open it again |
This is what went into the config file. I was already using the Premiere MCP, so both are in there.
{
"mcpServers": {
"playwright": {
"command": "cmd",
"args": ["/c", "npx", "-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://localhost:9222"]
},
"premiere-pro": { "...": "..." }
}
}
What matters is that it doesn't open a separate new Whale, but attaches to the Whale I use every day. I'm already logged in to Naver, so there's no need to hand the AI my username and password.
Wrestling with the Naver editor
Naver's SmartEditor is stubborn to automate. Here's where I actually got stuck and how I got past it.
| Stuck on | Solution |
|---|---|
| Pasting the body | Skip the clipboard. Dispatch a fake paste event straight to the input area inside the editor. The HTML goes in as is, so tables, bold and line breaks survive |
| Paragraph spacing | Naver ignores paragraph margins. Blank lines have to go in as real empty paragraphs |
| Quote style | A quote pasted as HTML always comes in as the quotation-mark style. After pasting, click the quote and switch its shape with the style buttons |
| Photo positions | Put a placeholder line like [📸 photo 01 | caption: …] in the body, then press the photo button where that line was deleted |
| Deleting a line | The editor intercepts selection keys like Shift+Home. Click at the end of the line and press Backspace as many times as there are characters |
| Uploading photos | Don't open the file picker; put the file in directly with Playwright's file upload. But it rejects files outside the working folder, so copies go in the working folder |
| Caption field | The caption field only appears after clicking the photo first. That's why automatic captions failed before |
| Checking the upload | Right after uploading, the photo address looks like an svg placeholder. That's normal. Wait a few seconds |
| Button won't click | It's covered by a side panel or mid-animation. Recalculate the button position every time and click it with the mouse |
| Hidden tab | If the tab is in the background, the page doesn't respond. Bring the tab to the front, then continue |
This is the code for pasting the body. Once this worked, the rest fell into place.
async (page) => {
const HTML = `...body HTML...`;
const fr = page.frame({ name: 'mainFrame' }) || page.mainFrame();
await fr.locator('.se-component.se-text .se-text-paragraph').first().click();
await page.waitForTimeout(400);
await fr.evaluate((h) => {
const ib = [...document.querySelectorAll('iframe')]
.map(i => { try { return i.contentDocument } catch (e) { return null } })
.find(x => x && x.body && x.body.isContentEditable);
const W = ib.defaultView, dt = new W.DataTransfer();
dt.setData('text/html', h);
dt.setData('text/plain', h.replace(/<[^>]+>/g, ''));
ib.body.dispatchEvent(new W.ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
}, HTML);
await page.waitForTimeout(4000);
}
Photos are handled one at a time in a loop. Each run does two things together: "add the caption to the previous photo" and "delete the next 📸 placeholder and press the photo button". When it reaches the file-picking state, the photo with that number is uploaded. It keeps going until no 📸 placeholders are left.
Publish must never be pressed
The last step is saving the draft. What I was most careful about here was making sure the publish button never gets pressed, not even by accident. The Naver editor's buttons are labeled in Korean: 저장 means "save" and 발행 means "publish". The code reads the button label first, and if it isn't 저장 or contains 발행, it stops without clicking. Success is confirmed by the number next to the save button going up by 1.
async (page) => {
const fr = page.frame({ name: 'mainFrame' }) || page.mainFrame();
const btn = fr.locator('button[class*="save_btn"]').first();
const label = (await btn.textContent()).trim();
if (!/저장/.test(label) || /발행/.test(label)) return { error: label };
const cnt = () => fr.evaluate(() => document.querySelector('[class*="save_btn"]').parentElement.textContent);
const before = await cnt();
const b = await btn.boundingBox();
await page.mouse.click(b.x + b.width / 2, b.y + b.height / 2);
await page.waitForTimeout(2500);
return { before, after: await cnt() };
}
Leaving publishing to a person is deliberate. The post goes public under the company's name, so I do the final check myself. Apart from that check, there's nothing left for me to touch.
| Stage | Before | Now |
|---|---|---|
| Writing | AI | AI |
| Preparing photos | Paste a command into PowerShell | AI |
| Entering the body | Copy and paste | AI |
| Tidying the format | Bookmark buttons | AI |
| Photos and captions | One click per photo, paste captions | AI |
| Title and link card | Person | AI |
| Saving the draft | Person | AI |
| Checking and publishing | Person | Person |
I made one for my personal blog too
I packaged the same method as a separate skill so I can use it on my personal Naver blog as well. The difference from the work version is the voice. I had the AI read a few of my old posts, pull my habits into a table (sentence endings, emoji, paragraph length, how I place photos), and write in that voice. Give it a photo folder and some rough notes, and it writes the post and saves it as a draft.
| Item | Detail |
|---|---|
| Input | Photo folder path + notes |
| Voice | Once at the start, it reads the structure of my 5 most recent posts and saves a style profile |
| No making things up | Shop names, prices or dates not in the notes or photos aren't written; they're left as [needs checking] |
| Photos | HEIC to JPG, shrink anything too large, and a copy with location data removed if I want |
| End | Saved as a draft. I publish it myself |
I've made the skill files public (they're written in Korean). You can add them to Claude as a skill.
| File | Contents |
|---|---|
| SKILL.md | The full procedure and code |
| my-blog.md | Blank style profile template |
I was setting up the same connection on my home PC, but it's stalled halfway. I've put in the config file, but the Claude app still doesn't pick up Playwright. I'll start again by checking, in the developer tab of the app's settings, which config file it actually reads.
In the next post I write about what I thought after getting this done. Why, once this tool existed, I felt I had to hurry.