# WordPress Content Exporter - posts & pages to JSON/Markdown (`beguiling_equator/wordpress-content-export`) Actor

Export posts, pages, custom post types, categories, tags, authors and media metadata from any public WordPress site via the REST API. Clean HTML, Markdown and plain text per post, incremental sync since last run, sitemap/RSS fallback when the API is disabled.

- **URL**: https://apify.com/beguiling_equator/wordpress-content-export.md
- **Developed by:** [WEIGUO HAN](https://apify.com/beguiling_equator) (community)
- **Categories:** AI, SEO tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 post or page exporteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## WordPress Content Exporter - export posts & pages to JSON, Markdown and plain text (REST API, no plugin)

Export **every public post, page and custom post type from any WordPress site** in one run - no plugin to install, no login, no API key. Give the Actor a site URL; it finds the site's **WordPress REST API** (`/wp-json/wp/v2/`), walks all pages of results and returns clean, structured items: title, slug, URL, excerpt, **content as HTML, Markdown and plain text**, word count, author, categories, tags, featured image (URL + alt text), published/modified dates and language. Categories, tags, authors and media-library metadata can be exported too.

Built for **RAG / LLM knowledge bases, content migration, backups, SEO audits, competitor blog monitoring and news aggregation**. Run it on a schedule with `sinceLastRun` and it only exports (and only charges for) what changed.

**Pricing: $1.50 per 1,000 posts + $0.02 per site** (pay per event, see below). Taxonomy, author and media items are free.

### Why this WordPress scraper

What this Actor delivers (facts checked 2026-10-16):

| | This Actor |
|---|---|
| Data source | WordPress REST API (structured JSON straight from the CMS); no browser, no plugin, no login |
| Content formats | HTML **+ Markdown + plain text**, word count, reading time |
| Coverage | posts, pages, **any custom post type** (`product`, `docs`, ...), categories, tags, authors, media metadata |
| Price | **$1.50 per 1,000 posts + $0.02 per site**; category, tag, author and media items, skipped unmodified items and failed sites are free |
| Incremental sync | `modifiedAfter` + `sinceLastRun` cursor in a key-value store |
| REST API disabled? | **Automatic fallback**: sitemap / RSS discovery + article extraction |
| Speed | 100 posts per request, pure HTTP, 256 MB memory |
| Politeness | <= 2 requests/s per host, exponential backoff, `Retry-After`, automatic slow-down after a 429, robots.txt |

### How to export WordPress content (tutorial)

1. Open the Actor and paste one or more **WordPress site URLs** - the homepage (`https://example.com`), a blog in a sub-directory (`https://wordpress.org/news`) or any page of the site. The API root is discovered automatically (`<link rel="https://api.w.org/">`, `/wp-json/`, or `/?rest_route=/` for sites with plain permalinks).
2. Pick **Post types** (`post`, `page` by default; add `product`, `portfolio`, ... or `all`) and optionally **Max items per site**, a **Search phrase** or **Modified after**.
3. Turn on **Export categories and tags / authors / media metadata** if you need them (free).
4. For recurring exports enable **Only content changed since the last run** and schedule the Actor.
5. Click **Start**. Results land in the dataset (table / JSON / CSV / Excel / API); a per-site summary with the source used and any warnings is stored under the `SUMMARY` key of the run's key-value store.

#### Input example

```json
{
  "startUrls": [
    { "url": "https://wordpress.org/news" },
    { "url": "https://wptavern.com" }
  ],
  "postTypes": ["post", "page"],
  "maxItemsPerSite": 500,
  "includeContentMarkdown": true,
  "includeTaxonomies": true,
  "sinceLastRun": true
}
```

#### Output example (one dataset item per post / page)

Recorded from `https://wordpress.org/news` (content fields shortened):

```json
{
  "id": 21753,
  "type": "post",
  "siteUrl": "https://wordpress.org/news",
  "siteName": "WordPress News",
  "url": "https://wordpress.org/news/2026/10/wordpress-7-1-3-maintenance-and-security-release/",
  "slug": "wordpress-7-1-3-maintenance-and-security-release",
  "title": "WordPress 7.1.3 Maintenance and Security Release",
  "status": "publish",
  "excerptHtml": "<p>This security and maintenance release features 7 security fixes and 4 bug fixes. ...</p>",
  "excerptText": "This security and maintenance release features 7 security fixes and 4 bug fixes. ...",
  "contentHtml": "<p class=\"wp-block-paragraph\">This security and maintenance release features 7 security fixes and <a href=\"https://core.trac.wordpress.org/query?milestone=7.1.3...\">4 bug fixes</a>.</p> ...",
  "contentMarkdown": "This security and maintenance release features 7 security fixes and [4 bug fixes](https://core.trac.wordpress.org/query?milestone=7.1.3...).

**Because this is a security release, it is recommended that you update your sites immediately.**

### Security updates included in this release

- ...",
  "contentText": "This security and maintenance release features 7 security fixes and 4 bug fixes.

Because this is a security release, it is recommended that you update your sites immediately. ...",
  "wordCount": 400,
  "readingTimeMinutes": 2,
  "authorId": 1182764,
  "authorName": "Jake Spurlock",
  "authorSlug": "whyisjake",
  "authorUrl": "https://profiles.wordpress.org/whyisjake/",
  "categories": [
    { "id": 14, "name": "Releases", "slug": "releases", "taxonomy": "category", "url": "https://wordpress.org/news/category/releases/" },
    { "id": 15, "name": "Security", "slug": "security", "taxonomy": "category", "url": "https://wordpress.org/news/category/security/" }
  ],
  "tags": [],
  "otherTerms": [],
  "categoryNames": ["Releases", "Security"],
  "tagNames": [],
  "featuredImageId": null,
  "featuredImageUrl": null,
  "featuredImageAlt": null,
  "publishedAt": "2026-10-06T17:00:00Z",
  "modifiedAt": "2026-10-07T08:07:00Z",
  "language": "en-US",
  "format": "standard",
  "parentId": null,
  "menuOrder": null,
  "sticky": false,
  "commentStatus": "closed",
  "commentCount": null,
  "source": "rest",
  "scrapedAt": "2026-10-07T09:12:44Z"
}
```

`featuredImageUrl` / `featuredImageAlt` are filled when the post has a featured image (this one has none); `otherTerms` holds custom-taxonomy terms (e.g. WooCommerce product categories). Items from the HTML fallback have the same shape with `source: "html"`.

With `includeRaw: true` the untouched REST object (including `_embedded` and Yoast SEO metadata when the site exposes it) is attached under `raw`.

Extra items share the same dataset and are distinguished by `type`:

```json
{ "type": "category", "id": 3, "name": "Community", "slug": "community", "description": null, "count": 113, "parentId": null, "url": "https://wordpress.org/news/category/community/", "siteUrl": "https://wordpress.org/news" }
{ "type": "author", "id": 1182764, "name": "Jake Spurlock", "slug": "whyisjake", "description": null, "website": "https://profiles.wordpress.org/whyisjake/", "avatarUrl": "https://secure.gravatar.com/avatar/...", "url": "https://profiles.wordpress.org/whyisjake/", "siteUrl": "https://wordpress.org/news" }
{ "type": "media", "id": 180512, "url": "https://wptavern.com/wp-content/uploads/2026/10/example.png", "altText": "Screenshot of the block editor", "caption": null, "mediaType": "image", "mimeType": "image/png", "width": 1600, "height": 900, "attachedToPostId": 180500, "publishedAt": "2026-10-06T16:40:11Z", "siteUrl": "https://wptavern.com" }
```

Use the dataset views **Posts & pages**, **Content (Markdown)** and **Categories, tags, authors, media** in the Output tab, or filter by `type` through the API.

### Incremental sync - only new and updated posts

- `modifiedAfter` - export only content modified after a date/time (sent to the API as `modified_after` and verified client-side, so it also works on WordPress < 5.7).
- `sinceLastRun` - the Actor remembers the newest `modifiedAt` per site in the named key-value store `stateStoreName` (default `wordpress-content-export-state`) and continues from there next time. The first run exports everything.
- Sync runs (either option set) walk each site **oldest-modified to newest** (`orderby=modified&order=asc`), so the cursor can always advance safely - even when a run is capped by `maxItemsPerSite` or stopped by your charge limit, everything unseen is newer than the cursor and is picked up next time. That also lets you spread a huge initial sync over several runs. The cursor is not advanced for a site whose listing failed with an error, so nothing is skipped.
- Combine both: `modifiedAfter` sets a floor, `sinceLastRun` keeps moving forward. Plain exports (neither option) list newest-first, which is what you want with `maxItemsPerSite`.

Schedule the Actor daily, connect a webhook or an integration (Google Sheets, Make, n8n, Zapier, Airbyte, your vector database) and you have a live mirror of any WordPress site.

### When the REST API is disabled (fallback mode)

Some sites turn the REST API off (plugins such as "Disable REST API", returning `401 rest_authorization_required` / `403`) or hide it behind a CDN. With `mode: auto` the Actor then:

1. lists URLs from the site's XML sitemaps (`Sitemap:` lines in robots.txt, `/wp-sitemap.xml`, Yoast/RankMath `sitemap_index.xml`, `post-sitemap.xml`, ...) - keeping only post/page sitemaps and skipping category/tag/author maps; if no sitemap exists it reads the RSS feed (`/feed/`, newest 10-50 posts),
2. fetches each page and extracts the article with WordPress theme conventions: `.entry-content` / `articleBody`, `rel="category tag"` links, `<link rel="shortlink" href="?p=ID">`, `postid-123` body classes, Open Graph and JSON-LD metadata.

Items from this path carry `source: "html"`. Expect small gaps (see Limitations). Force the path with `mode: html` when a site's API returns truncated content; use `mode: rest` to skip sites without an API.

### Pricing (pay per event)

| Event | What it means | Price |
|---|---|---|
| `post` | One post, page or custom-post-type item written to the dataset (HTML + Markdown + text) | **$0.0015** ($1.50 / 1,000 posts) |
| `site-crawled` | One site whose REST API or sitemap/RSS was successfully discovered in a run | **$0.02** per site per run |
| Actor start | Apify's standard synthetic start event at the platform default | platform default |

Category, tag, author and media items, skipped unmodified items, unreachable or blocked sites and failed pages are never charged. Proposed volume discounts for higher Apify plans: `post` $0.0012 (Silver) / $0.0010 (Gold).

**Cost examples**

- One-off export of a 2,000-post blog with Markdown and taxonomies: 2,000 x $0.0015 + $0.02 = **$3.02**.
- Daily sync of 20 competitor blogs with `sinceLastRun`, ~60 changed posts per day: 60 x $0.0015 + 20 x $0.02 = **$0.49 per day**.
- Migrating 50 client sites with 500 posts each (25,000 posts): $37.50 + $1.00 = **$38.50**.

### Use cases

- **RAG / AI knowledge bases** - Markdown per post with headings, links and code blocks preserved; `wordCount` for chunking; `modifiedAt` for re-indexing only what changed.
- **Content migration & backups** - move a blog to Hugo, Astro, Ghost, Notion or another WordPress; keep slugs, dates, authors and taxonomies.
- **SEO & content audits** - word counts, publish cadence, authors, categories, missing featured images or alt texts across whole sites.
- **Competitor & industry monitoring** - scheduled incremental exports of other blogs into Google Sheets, Slack or your CRM.
- **News & research datasets** - clean article text with dates and authors from thousands of WordPress-powered newsrooms.
- **AI agents / MCP** - "summarize everything example.com published this month" becomes one Actor call.

### Limitations and good-to-know

- **Public content only.** The Actor never authenticates, so only published, non-password-protected content is exported (password-protected posts appear with metadata but no body). Drafts, private posts and members-only content are out of scope by design.
- **Some sites restrict endpoints.** The `/users` endpoint is frequently hidden (`401`); authors are still available inside each post through `_embed`. Custom types must be exposed to the REST API by the site (`show_in_rest`).
- **Fallback mode is heuristic.** Without the API, IDs come from shortlinks/body classes (may be missing), excerpts from meta descriptions, tags from `rel="tag"` links, and the content container is picked by theme conventions - unusual themes can include related-post boxes or miss sidebars. RSS feeds only list the most recent posts. Taxonomy/author/media lists are REST-only.
- **CDN / bot protection.** A few sites answer `403` to datacenter IPs. The Actor stops on persistent 401/403/429 and reports it in `SUMMARY`; try the `proxyConfiguration` input (Apify Proxy) for those sites. The Actor does not bypass CAPTCHAs, logins or paywalls.
- **Politeness limits throughput.** At most 2 requests per second per site, halved every time a site answers `429` (down to one request per 10 s): a 10,000-post site takes ~1 minute through the API (100 posts per request) but ~1.5 hours in fallback mode (one page per post). `includeTaxonomies` on a site with thousands of tags adds one request per 100 terms.
- **Dates** are normalized to UTC ISO-8601 from `date_gmt` / `modified_gmt`. `language` comes from Polylang/WPML fields when present, otherwise from the site's `<html lang>`.
- You are responsible for having the right to use the exported content; the Actor honours each site's robots.txt and `Retry-After` headers.

### FAQ

**Does this work on any WordPress site?** Yes for any public site with the REST API enabled (the WordPress default since 4.7, including WordPress.com and WordPress VIP sites). Sites that disabled the API go through the sitemap/RSS fallback.

**Do I need to install a plugin on my WordPress site?** No. Everything is read through public endpoints - which also makes it a good way to export a site you do not administer (with permission).

**Can I export WooCommerce products or other custom post types?** Yes - add the type's name (`product`, `docs`, `portfolio`) to `postTypes` or use `all`; the Actor reads `/wp/v2/types` to find the right route. WooCommerce product *data* (prices, stock) lives in a separate authenticated API and is not included.

**How do I get only new or updated posts?** Set `sinceLastRun: true` and run on a schedule, or pass `modifiedAfter`.

**Why is `authorName` empty?** The site hides the users endpoint and does not embed authors. Fallback-mode items use byline / JSON-LD / meta author tags.

**Does it download images or media files?** No - media export returns URLs and metadata only (fast and cheap). Pipe the URLs to a downloader if you need the files.

**Can it run locally?** Yes: `apify run` with the Apify CLI, or `python -m src` inside a venv. Unit tests: `pytest`.

### Changelog

- **1.0.0** - initial release: REST API discovery (`wp-json` / `rest_route`), posts/pages/custom types with pagination, Markdown + text conversion, taxonomies/authors/media export, incremental sync, sitemap/RSS/HTML fallback, pay-per-event charging.

# Actor input Schema

## `startUrls` (type: `array`):

One entry per site: the homepage (https://example.com), a blog in a sub-directory (https://wordpress.org/news) or any page of the site. The REST API root is discovered automatically (link rel="https://api.w.org/", /wp-json/ or /?rest_route=/).

## `postTypes` (type: `array`):

WordPress post types to export: post, page, or any custom type exposed by the REST API (product, portfolio, docs ...). Use "all" to export every public content type the site reports under /wp/v2/types.

## `maxItemsPerSite` (type: `integer`):

Stop after this many posts/pages per site (0 = everything). Newest first. Also caps media items when media export is on.

## `search` (type: `string`):

Passed to the REST API `search` parameter: only posts/pages matching the phrase are exported. REST mode only.

## `modifiedAfter` (type: `string`):

Only export content modified after this moment (ISO date or datetime, UTC). Example: 2026-01-01 or 2026-01-01T12:00:00Z.

## `sinceLastRun` (type: `boolean`):

Remembers the newest modification time per site in the named key-value store below and exports only newer content on the next run (combined with Modified after when both are set). The first run exports everything.

## `stateStoreName` (type: `string`):

Name of the key-value store that keeps the per-site sync cursor. Use different names for independent sync jobs.

## `includeContentHtml` (type: `boolean`):

Cleaned HTML of the post body (scripts, styles and forms removed; relative links made absolute).

## `includeContentMarkdown` (type: `boolean`):

Markdown conversion of the post body (headings, lists, links, images, tables, code) - ready for LLMs, RAG pipelines and static-site migrations.

## `includeContentText` (type: `boolean`):

Plain text of the post body; wordCount and readingTimeMinutes are always computed.

## `includeRaw` (type: `boolean`):

Attach the untouched WordPress REST object (including `_embedded` and Yoast SEO data when present) under `raw`. Makes items several times bigger.

## `includeTaxonomies` (type: `boolean`):

Also write one dataset item per category and per tag (type = category / tag). REST mode only; free of charge.

## `includeAuthors` (type: `boolean`):

Also write one dataset item per author with public posts (type = author; name, slug, description, avatar - never e-mail). REST mode only; some sites hide the users endpoint.

## `includeMedia` (type: `boolean`):

Also write one dataset item per media-library entry (type = media; URL, alt text, caption, dimensions, mime type). Files are not downloaded. REST mode only; capped by Max items per site.

## `postStatus` (type: `string`):

Status filter sent to the REST API. Without authentication WordPress only returns `publish`; other values are passed through and fall back to published content with a warning.

## `mode` (type: `string`):

auto: REST API first, sitemap/RSS/HTML fallback when it is disabled or blocked. rest: REST API only (site is skipped if unavailable). html: force the sitemap/RSS/HTML path (useful when the REST API returns partial content).

## `maxSitesInParallel` (type: `integer`):

How many sites are exported at the same time (1-10). Per-host politeness limits always apply.

## `maxConcurrencyPerHost` (type: `integer`):

Parallel requests to a single site (1-4).

## `requestsPerSecondPerHost` (type: `integer`):

Hard-capped at 2. The Actor also honours Retry-After, backs off exponentially on 429/5xx and obeys robots.txt.

## `maxRetries` (type: `integer`):

Retries for 429/5xx/network errors with exponential backoff (0-5).

## `proxyConfiguration` (type: `object`):

Optional. Route requests through Apify Proxy (or your own proxies) when a site's CDN blocks datacenter IPs with 403. Not needed for most WordPress sites.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://wordpress.org/news"
    },
    {
      "url": "https://wptavern.com"
    }
  ],
  "postTypes": [
    "post",
    "page"
  ],
  "maxItemsPerSite": 50,
  "sinceLastRun": false,
  "stateStoreName": "wordpress-content-export-state",
  "includeContentHtml": true,
  "includeContentMarkdown": true,
  "includeContentText": true,
  "includeRaw": false,
  "includeTaxonomies": false,
  "includeAuthors": false,
  "includeMedia": false,
  "postStatus": "publish",
  "mode": "auto",
  "maxSitesInParallel": 3,
  "maxConcurrencyPerHost": 2,
  "requestsPerSecondPerHost": 2,
  "maxRetries": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `content` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://wordpress.org/news"
        },
        {
            "url": "https://wptavern.com"
        }
    ],
    "postTypes": [
        "post",
        "page"
    ],
    "maxItemsPerSite": 50,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("beguiling_equator/wordpress-content-export").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        { "url": "https://wordpress.org/news" },
        { "url": "https://wptavern.com" },
    ],
    "postTypes": [
        "post",
        "page",
    ],
    "maxItemsPerSite": 50,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("beguiling_equator/wordpress-content-export").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://wordpress.org/news"
    },
    {
      "url": "https://wptavern.com"
    }
  ],
  "postTypes": [
    "post",
    "page"
  ],
  "maxItemsPerSite": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call beguiling_equator/wordpress-content-export --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,beguiling_equator/wordpress-content-export"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/eIHLPD9FodHUSsVo2/builds/gDhHYOpY2c3CAVgPq/openapi.json
