SuperAPI AI Directory

soia-media-publish-x-article

Writing & research

View repository

Updated 10-16 08:49

About this skill

soia-media-publish-x-article

Send a finished Obsidian/local Markdown draft straight to the X Articles drafts, completing the X long-form outlet of the publish family (publish-x-thread produces thread text; this skill produces rich-text long-form drafts).

Customer-readable description

What this skill can do

Customer wants The skill will Customer can see
Post a Markdown long-form article to X Articles Parse title/cover/body images → create a new draft in the browser → paste rich text → insert images in place → mechanical verification The live browser operation process, draft URL, verification checklist, and final receipt
The article has no cover image Stop and remind (an X Article without a cover looks bad), and continue without a cover only after explicit consent Reminders and confirmation questions
Missing dependencies / not logged in / no Premium+ Stop and clearly state what is missing Login/subscription guidance, without filling in any credentials on your behalf

Security bottom line: save drafts only,never click "Publish"; the login state stays in the browser only, no cookies are exported, and no credentials are written to disk.

How customers use it

Other recognizable phrasings include "upload to X article", "X Articles draft", "post this as an X long-form article"; when there is only a regular short post or thread, hand off to soia-media-publish-x-thread。

  1. Say "post <file> as an X Article". First-time use requires logging in once: the script pops up a browser window, and after manual login it is reused long-term (shares the same login state as soia-media-publish-x-thread so once either skill has logged in, neither needs to log in again); the account needs a subscription that includes the "write articles" entitlement.
  2. The Agent first does a dry-run parse and reports: title, cover, number of body images, list of missing images; if the cover is missing, it asks you first.
  3. The entire browser stage is visible (by default it uses a host-agnostic Playwright script, the same in any environment that can run Python); after completion it gives the draft URL and verification checklist, for you to review manually and publish.

Dependencies and installation

claude plugin marketplace add soia-team/soia-open-skills
claude plugin install soia-media-content@soia

When you only want this one skill, you can use the npx route. Note that the skill will land in the shared source of truth ~/.agents/skills; if the plugin is also installed, the same skill will appear as two indexes and drift apart, so it is recommended to choose one of the two:

npx skills add soia-team/soia-open-media-content-skills -g -a '*' -s soia-media-publish-x-article -y
  • Cross-platform Python 3.9+ (macOS / Windows / Linux all work): the main route uses Playwright's bundled browser + native clipboard API, with no dependency on system-specific tools like osascript / sips.
    pip install playwright && python -m playwright install chromium
    
  • The login state shares the same local profile directory with soia-media-publish-x-thread (~/.config/soia-skills/soia-media-publish-x-thread/x-profile, can be overridden with --profile-dir / SOIA_X_PROFILE_DIR ; the script does a read-only fallback for the old namespace): if you have already logged in with x_post.py login , it works directly; if not logged in, go to that skill and run python3 scripts/x_post.py login(this skill itself does not provide a login subcommand and relies on the shared profile to avoid logging in a second time).
  • The alternative route (when the host happens to provide browser control tools, such as Claude Code's claude-in-chrome) requires macOS and uses clipboard_x.py(osascript/sips); see "Alternative route" below. If there is no host tool, or you are in a non-macOS environment, just use the main route.
  • The X account must have a Premium subscription that includes the "Write Articles" entitlement (the US$4 Premium tier shown on the 2026-07 purchase page includes it; refer to the real-time entitlement table on the X purchase page)
  • No private config.yml: this skill does not touch any key/cookie

WorkBuddy The loading unit is role-based experts rather than plugins,npx skills add -a '*' cannot cover it, it needs to be installed separately, see docs/install/workbuddy.md。

Logging and completion receipt

After execution, you must output: what was done, draft URL, validation results (item by item), skipped/failed items, next steps (manual review and publication).

Positioning and closed loop

compose-article-draft produces the finished article → this skill pushes an X Articles draft (benchmark publish-wechat-draft versus WeChat Official Account). "Only create drafts, never publish" is the same safety philosophy as on the WeChat Official Account side.

Trigger words

The user says Action
"Publish as X Article", "Upload X article", "Push to X drafts" Full flow (parse → upload → validate)
"First see whether this can be posted to X" Run only the parse dry-run, report without touching the browser
"Post an X / post a tweet" (ordinary short post) Not part of this skill: for short post/thread text use soia-media-publish-x-thread; this skill only does Articles long-form drafts

Workflow

Step 1: Parse (without touching the browser)

python3 scripts/parse_x_article.py <article.md> > /tmp/x_article.json
python3 scripts/parse_x_article.py <article.md> --html-only > /tmp/x_article_body.html

JSON fields:title / cover_image(only when the article starts with an image) / content_images[](including block_index and after_text)/ dividers[] / html / missing_images。

Two gates, pass them before touching the browser:

  1. missing_images > 0 → stop, give the user the list of missing images (you can use --search-dir to specify a directory to search for them).
  2. cover_image is null → stop, and remind them "an X Article with no cover looks bad; it is recommended to add an image at the beginning of the article"; continue only if the user explicitly says not to add one (in that case, all images are treated as body images).

Step 2: Write the draft to X Articles

The main route is the bundled Playwright script x_article_post.py, host-agnostic—it behaves consistently in any host that can run Python, such as Claude Code, Codex, Gemini CLI, opencode, etc., and does not depend on any host-specific browser tooling; both routes share the same set of mechanical validation criteria from Step 3.

Main route: scripts/x_article_post.py (host-agnostic)

Dependencies (one-time):pip install playwright && python -m playwright install chromium。

python3 scripts/x_article_post.py status                     # 检查登录态(与 x_post.py 共用 profile)
python3 scripts/x_article_post.py check                      # 探测 Articles 入口
python3 scripts/x_article_post.py draft /tmp/x_article.json  # 完整草稿:封面→标题→正文→插图→分割线→校验
  • Log in: this skill itself does not provide the login subcommand. If not logged in, go to soia-media-publish-x-thread and run python3 scripts/x_post.py loginonce; both sides share the same local profile, so logging in once works for both.

  • check returns one of four states:

    • not_logged_in → not logged in; follow the instructions above to log in and retry.
    • no_articles_access → the account does not have the "Write Articles" entitlement, or /compose/articles 404; the JSON includes the following fixed reminder text.
    • ok → a clickable creation entry was found, and you can continue draft。
    • unknown → the page structure does not match expectations (possibly the editor was redesigned, or it may be a real DOM that has never been verified); the JSON includes the screenshot path, do not guess clicks; handle it according to "Boundaries and exceptions" below.

    no_articles_access Fixed reminder text (never subscribe on the user's behalf; byte-for-byte identical to the script's NO_PREMIUM_MESSAGE constant, not paraphrased):

    The current account does not have X Premium with the "Write Articles" entitlement enabled; please go to https://x.com/i/premium_sign_up after subscribing (as of 2026-07, the US$4/month Premium tier includes this entitlement) and try again.

  • draft The first internal step is to reuse the check determination: if not ok immediately stop and return that status as-is, without continuing to click anything.

  • Body images and dividers are always inserted in block_index from largest to smallestreverse order—the editor is a dynamic document, and inserting the earlier ones first shifts all later anchors; this is a hard correctness requirement, not a style preference.

  • Clipboard uses Playwright's native Clipboard API (navigator.clipboard.write + granted clipboard permission), working across macOS / Windows / Linux; the paste key is chosen automatically by platform: Cmd+V (macOS) / Ctrl+V (other platforms), with no reliance on osascript.

  • Cover upload uses a real file input control (input[type=file] + set_input_files), which is more reliable than clipboard/drag-and-drop.

  • There is no code path related to publishing:x_article_post.py there is no code capable of clicking or locating the "Publish" button—this is a hard constraint of the script itself, stricter than publish-x-thread 's draft/send two tiers (in the Articles scenario, the send tier does not exist at all).

  • Unexpected exceptions are caught by an overall fallback: it produces a structured error JSON (error message + page URL/title + screenshot path), does not throw a bare traceback, and does not keep clicking blindly while in an exception state.

  • Output is a single-line JSON receipt, machine-readable by any host.

Alternative route: host browser tools

When the host happens to provide browser control (such as Claude Code's claude-in-chrome, which reuses the user's real Chrome login state via a browser extension), it may be used; the client has zero login cost, and manual intervention during page redesigns is more intuitive;when no host tool is available, this must not be used as a reason to skip the script's main route. The acceptance criteria are exactly the same as the main route (see step 3).

  1. Browser surface choice: claude-in-chrome (real Chrome already logged in) or Browser pane (if not logged in, have the user log in;do not enter credentials on their behalf)。

  2. Navigate to https://x.com/compose/articles—the landing page is thedrafts listpage, not the editor; click the "create/write" button directly to enter the editor (do not wait for "Add title" to appear; it only renders after the click).

  3. Redirect to the login page = not logged in;/compose/articles 404 or no "Articles" entry in the creator studio = the account has no Articles entitlement. Stop and remind using the fixed wording (never subscribe on their behalf; wording as above).

  4. Has cover: click the cover upload control, use file upload to upload cover_image ; X will pop up a media editing layer, and **you must click "Apply"** to close it, otherwise the editor is blocked by the overlay and the cover does not take effect. In the title box (placeholder "Add title/添加标题") enter title。

  5. Rich-text body paste:

    python3 scripts/clipboard_x.py html --file /tmp/x_article_body.html
    

    click the editor body area → Cmd+V. Headings/bold/links/lists/quotes are preserved by the HTML clipboard flavor. After pasting, spot-check whether the text of the first and last paragraphs is both present.

  6. Images and dividers (both in block_index from largest to smallestreverse order): for each content_images[i]:

    python3 scripts/clipboard_x.py image <path> --max-bytes 3000000
    

    use after_text to find the target paragraph → click → press the End keyto move the cursor to the end of the paragraph (to prevent clicking into a link within the paragraph) → Cmd+V Paste → wait for "Uploading media/Uploading" to disappear before inserting the next one. Divider (dividers[], also in reverse order): X ignores <hr>, you must click the corresponding position and then use the editor's "Insert > Divider" menu.

    Reason for reverse order: the editor is a dynamic document; inserting earlier items first will shift all subsequent anchors.

Step 3: Mechanical verification (not complete unless all pass)

Verification item Standard
Title Editor title == JSON title
First/last sentence of body Both can be found in the editor
Media count Number of images in editor == cover (0/1) + content_images count
Divider count == dividers count
Draft status autosave_confirmed: after all images/dividers are handled, first wait a while for the (presumed) debounced autosave to take effect, then try to observe the "Saved/Saved" prompt;draft_url: record the draft URL

The main route's draft subcommand has already completed these five items in the checks field of the JSON receipt (draft_url/autosave_confirmed When an exact result cannot be obtained, honestly give null/false, do not fabricate;autosave_confirmed: false does not equal failure, it just means it could not be confirmed, handle it according to the "Boundaries and Exceptions" above); the alternative route requires manual or host browser tool item-by-item verification.

Step 4: Receipt

Draft URL + item-by-item results of the verification checklist + "Please review manually and click publish yourself."Under no circumstances click publish.

Format support boundaries

Element Handling
H2/H3, bold, italic, links, ordered/unordered lists, quotes Preserved natively
Code block Convert to quote block (X does not support <pre>)
Tables / mermaid X does not support: first manually convert to PNG images and insert, or accept degraded loss; the parser passes through the text as-is
--- Divider Insert via editor Insert/Insert menu (main route see x_article_post.py of insert_divider_at, alternative route see step 2 step 6)
H1 Only used as a title, not included in the body

Boundaries and exceptions

Scenario Handling
Redirected to login page Stop, have the user log in in the browser and retry; do not touch credentials
No Articles entry (404 on /compose/articles or no create entry) Stop, remind using the fixed wording from step 2 and provide the activation direct link https://x.com/i/premium_sign_up; never subscribe on their behalf; main route corresponds to check/draft of no_articles_access status
Page structure cannot be determined to belong to any of the above cases Stop, report truthfully as unknown(main route automatically attaches screenshot + page URL/title), do not force categorization, do not guess clicks
Missing image Step 1 gate intercepts, provide a checklist
No cover Gate reminder, continue only after explicit consent
Formatting lost after paste Check the clipboard path (main route see JSON's warnings; alternative route checks clipboard script output); retry once; if it still fails, report the actual result rather than falsely claiming success
Image upload stuck Wait for the upload indicator to disappear (the main route has a 15s timeout cap); on timeout, take a screenshot and report instead of blindly continuing
Divider insertion failed Do not abort the entire draft: the main route counts the failed item in warnings(including block_index/after_text), and the rest of the content is completed as usual
X editor redesign makes controls unfindable Stop and report the actual page state; do not guess at clicks
draft retry after a mid-way failure Each call on the main route clicks "create" to start a new article; it does not continue or deduplicate the previously failed draft. Before retrying, if you suspect the previous attempt partially wrote something, first manually check the Articles draft list on X and clean up duplicate/half-finished drafts; retrying only adds a new article and will not automatically merge or overwrite
checks.autosave_confirmed is false This means the script could not observe the "saved/Saved" prompt—the content may have been pasted but not necessarily persisted;warnings will also show a prompt; do not judge success based only on ok: true ; you need to manually open checks.draft_url to verify

Script specifications (scripts/)

Script Responsibility Dependencies
parse_x_article.py MD → JSON (title/cover/images/dividers/HTML); adapted from wshuyi/x-article-publisher-skill (MIT, see repository THIRD_PARTY_NOTICES) stdlib
x_article_post.py Host-agnostic Playwright main route: login-state reuse (shares the same profile with x_post.py ), Articles entry detection (check: not_logged_in/no_articles_access/ok/unknown), fully automated draft (draft: cover→title→body→inline images→dividers→mechanical validation), cross-platform clipboard (Playwright native Clipboard API, no osascript); contains no publishing code path whatsoever pip install playwright && python -m playwright install chromium
clipboard_x.py For the alternative route only: HTML/images onto the macOS clipboard (osascript;--max-bytes automatically downsamples to a temporary copy when over the limit) stdlib + macOS osascript/sips

Validation and testing

Script-layer fixture forward test (no X account needed; must be run after changing scripts;parse_x_article.py/clipboard_x.py was not changed this time, so the standard is unchanged):

  1. Create a test article containing a frontmatter title, a cover image at the start, 2 body images (png+jpg), a--- divider, a code block, ordered/unordered lists, and links.
  2. parse_x_article.py Assertions:title taken from frontmatter;cover_image is an absolute path and points to the image at the start;content_images[].block_index increments,after_text has no ___CODE___ marker and has had markdown notation stripped; after deleting the image at the start, cover_image is null (gate semantics).
  3. clipboard_x.py html After osascript -e 'clipboard info' must contain «class HTML»;image produces for png/jpg respectively «class PNGf»/JPEG picture;--max-bytes when over the limit, goes through a sips temporary copy andthe original file's byte count remains unchanged。

x_article_post.py After one round of implementation + three-lens independent adversarial review (security / correctness / cross-platform), fixes were made based on the findings: added wait_for_autosave_signal(after all inline images and dividers have been processed and before formally closing the browser, first wait for the debounced autosave to take effect and then try to observe "saved/Saved"; the result is written into checks.autosave_confirmed, and when it cannot be observed, a warning is given instead of silently continuing—fixing "the close timing may precede persistence, yet it still reports ok:true" blocking-level issue), fall back to the body editing area when the divider anchor cannot be found (consistent with body images, to avoid landing at a residual cursor position from the previous operation), degrade to a warning instead of aborting the entire draft when the cover file upload fails, and change the media count validation to count only within the role="main" area (to avoid page-level <img> such as the navigation bar avatar polluting the count). Cross-platform lens review found no issues.

x_article_post.py——Actually run before this PR was merged:

  1. python3 -m py_compile + ast.parse syntax validation passed;importlib actually executed module top-level code (regex compilation, etc.) with no exceptions.
  2. Manual review + grep -in "publish\|发布" full-text verification: the hits are all comments/docstrings/path names, with not a single hit of .click()/.press() calls—verifying the hard constraint that there is "no release code path whatsoever."
  3. status/check Already tested and passed with a real X account (local shared login session):status correctly reported logged_in: true;check correctly returned for an account known not to be subscribed to Premium no_articles_access,message fields matched byte-for-byte with the script's NO_PREMIUM_MESSAGE constants; directly scraping that page verified the classification hit path—title 未找到页面 / X, body contains "this page doesn't exist", hitting the NOT_FOUND_TEXT_MARKERS "page doesn't exist" substring (going through the body-text matching branch, not the title "404" match, and not a lucky hit on the Premium-upsell fallback branch).

x_article_post.py——Not yet run before this PR was merged, honestly left for follow-up verification(not packaged as "tested"):

  1. check the ok branch and the complete draft flow (cover upload, title, rich-text body paste, inserting images in reverse order by block_index , divider menu insertion,autosave_confirmed observation, mechanical validation)—requires a real Premium account with a subscription that includes the "write articles" entitlement; there is currently no available account to verify with;find_create_control / find_title_input / find_body_editor / SAVED_INDICATOR_RE / divider menu and other selectors are all reasonable guesses based on text/role location and have not been verified on the real editor DOM; the first real run will very likely hit boundaries such as "X editor redesign, control not found" or autosave_confirmed: false , and the selectors and this document need to be updated according to the actual page.
  2. In the cross-platform clipboard path, navigator.clipboard.write + Control+V on Windows/Linux—this development environment is macOS, and only the sys.platform platform detection logic itself was verified, not run on a real Windows/Linux machine.

The browser end-to-end standard remains unchanged: pass criterion is all five items green in the step 3 mechanical validation (including autosave_confirmed: true) + a draft URL; when selectors fail due to an editor redesign, update the selector descriptions in this document based on the actual page.

Completion receipt

  1. What was done: one sentence + draft URL.
  2. Validation checklist: step 3 item-by-item results; mark failed items honestly.
  3. Next steps: publish on your own after manual review; suggest using soia-media-generate-article-image to generate the cover.