You've got years of content in WordPress. Now you want to move to a static site generator, build a headless frontend, or just back up your content in a portable format.
The WordPress REST API exists, but it requires authentication setup, pagination handling, and multiple requests. Sometimes you just want a clean JSON export.
Here's how to build one.
The Export Class
<?php
/**
* WordPress Content Exporter
*/
class Content_Exporter {
/**
* Export posts based on options
*
* @param array $options Export options
* @return array
*/
public function export( $options = array() ) {
$defaults = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => -1,
'category' => '',
'include' => array( 'content', 'excerpt', 'featured_image', 'taxonomies', 'meta' ),
);
$options = wp_parse_args( $options, $defaults );
$args = array(
'post_type' => sanitize_key( $options['post_type'] ),
'post_status' => sanitize_key( $options['post_status'] ),
'posts_per_page' => intval( $options['posts_per_page'] ),
'orderby' => 'date',
'order' => 'DESC',
);
// Category filter
if ( ! empty( $options['category'] ) ) {
$args['cat'] = intval( $options['category'] );
}
$query = new WP_Query( $args );
$posts = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$posts[] = $this->format_post( get_post(), $options['include'] );
}
wp_reset_postdata();
}
return $posts;
}
/**
* Format a single post
*/
private function format_post( $post, $include ) {
$data = array(
'id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'date' => $post->post_date,
'modified' => $post->post_modified,
'status' => $post->post_status,
'type' => $post->post_type,
'url' => get_permalink( $post->ID ),
'author' => $this->get_author( $post->post_author ),
);
if ( in_array( 'content', $include, true ) ) {
$data['content'] = $post->post_content;
$data['content_rendered'] = apply_filters( 'the_content', $post->post_content );
}
if ( in_array( 'excerpt', $include, true ) ) {
$data['excerpt'] = $post->post_excerpt
? $post->post_excerpt
: wp_trim_words( $post->post_content, 55 );
}
if ( in_array( 'featured_image', $include, true ) ) {
$data['featured_image'] = $this->get_featured_image( $post->ID );
}
if ( in_array( 'taxonomies', $include, true ) ) {
$data['taxonomies'] = $this->get_taxonomies( $post->ID, $post->post_type );
}
if ( in_array( 'meta', $include, true ) ) {
$data['meta'] = $this->get_public_meta( $post->ID );
}
return $data;
}
/**
* Get author data
*/
private function get_author( $author_id ) {
$user = get_userdata( $author_id );
if ( ! $user ) {
return null;
}
return array(
'id' => $user->ID,
'name' => $user->display_name,
'slug' => $user->user_nicename,
);
}
/**
* Get featured image
*/
private function get_featured_image( $post_id ) {
$thumbnail_id = get_post_thumbnail_id( $post_id );
if ( ! $thumbnail_id ) {
return null;
}
$image = wp_get_attachment_image_src( $thumbnail_id, 'full' );
$alt = get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true );
return array(
'id' => $thumbnail_id,
'url' => $image ? $image[0] : '',
'width' => $image ? $image[1] : 0,
'height' => $image ? $image[2] : 0,
'alt' => $alt,
);
}
/**
* Get taxonomies
*/
private function get_taxonomies( $post_id, $post_type ) {
$taxonomies = get_object_taxonomies( $post_type, 'objects' );
$data = array();
foreach ( $taxonomies as $taxonomy ) {
$terms = get_the_terms( $post_id, $taxonomy->name );
if ( ! $terms || is_wp_error( $terms ) ) {
continue;
}
$data[ $taxonomy->name ] = array_map( function( $term ) {
return array(
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
);
}, $terms );
}
return $data;
}
/**
* Get public meta (exclude private _ prefixed keys)
*/
private function get_public_meta( $post_id ) {
$meta = get_post_meta( $post_id );
$data = array();
foreach ( $meta as $key => $values ) {
// Skip private meta
if ( strpos( $key, '_' ) === 0 ) {
continue;
}
$data[ $key ] = count( $values ) === 1 ? $values[0] : $values;
}
return $data;
}
/**
* Convert to JSON
*/
public function to_json( $posts ) {
return wp_json_encode(
$posts,
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
}
}
Usage
$exporter = new Content_Exporter();
// Export all posts
$posts = $exporter->export();
$json = $exporter->to_json( $posts );
// Export pages only
$pages = $exporter->export( array(
'post_type' => 'page',
) );
// Export specific category
$tutorials = $exporter->export( array(
'category' => 5, // category ID
) );
// Minimal export (no content, just metadata)
$minimal = $exporter->export( array(
'include' => array( 'excerpt', 'featured_image' ),
) );
Adding an Admin Interface
/**
* Add export page to admin
*/
function add_export_admin_page() {
add_management_page(
'Export Content',
'Export Content',
'manage_options',
'content-export',
'render_export_page'
);
}
add_action( 'admin_menu', 'add_export_admin_page' );
/**
* Handle export
*/
function render_export_page() {
// Handle form submission
if ( isset( $_POST['export_nonce'] ) &&
wp_verify_nonce( $_POST['export_nonce'], 'content_export' ) ) {
$exporter = new Content_Exporter();
$options = array(
'post_type' => sanitize_key( $_POST['post_type'] ?? 'post' ),
'include' => array_map( 'sanitize_key', $_POST['include'] ?? array() ),
);
$posts = $exporter->export( $options );
$json = $exporter->to_json( $posts );
// Trigger download
header( 'Content-Type: application/json' );
header( 'Content-Disposition: attachment; filename="export-' . date('Y-m-d') . '.json"' );
echo $json;
exit;
}
// Render form
$post_types = get_post_types( array( 'public' => true ), 'objects' );
?>
<div class="wrap">
<h1>Export Content to JSON</h1>
<form method="post">
<?php wp_nonce_field( 'content_export', 'export_nonce' ); ?>
<table class="form-table">
<tr>
<th>Post Type</th>
<td>
<select name="post_type">
<?php foreach ( $post_types as $type ) : ?>
<option value="<?php echo esc_attr( $type->name ); ?>">
<?php echo esc_html( $type->labels->name ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th>Include</th>
<td>
<label><input type="checkbox" name="include[]" value="content" checked> Content</label><br>
<label><input type="checkbox" name="include[]" value="excerpt" checked> Excerpt</label><br>
<label><input type="checkbox" name="include[]" value="featured_image" checked> Featured Image</label><br>
<label><input type="checkbox" name="include[]" value="taxonomies" checked> Taxonomies</label><br>
<label><input type="checkbox" name="include[]" value="meta"> Custom Meta</label>
</td>
</tr>
</table>
<?php submit_button( 'Download JSON' ); ?>
</form>
</div>
<?php
}
Converting to Markdown
For static site generators like Hugo, Jekyll, or Astro, Markdown with frontmatter is often more useful:
/**
* Convert posts array to Markdown files content
*/
public function to_markdown( $posts ) {
$output = '';
foreach ( $posts as $post ) {
// Frontmatter
$output .= "---\n";
$output .= 'title: "\"' . str_replace( '\"', '\"', $post['title'] ) . \"\"\n\";"
$output .= 'slug: ' . $post['slug'] . "\n";
$output .= 'date: ' . $post['date'] . "\n";
if ( ! empty( $post['author']['name'] ) ) {
$output .= 'author: ' . $post['author']['name'] . "\n";
}
if ( ! empty( $post['featured_image']['url'] ) ) {
$output .= 'featured_image: ' . $post['featured_image']['url'] . "\n";
}
if ( ! empty( $post['taxonomies']['category'] ) ) {
$cats = array_column( $post['taxonomies']['category'], 'name' );
$output .= 'categories: [' . implode( ', ', $cats ) . "]\n";
}
$output .= "---\n\n";
// Content
$output .= '# ' . $post['title'] . "\n\n";
if ( ! empty( $post['content'] ) ) {
$output .= $this->html_to_markdown( $post['content'] );
}
$output .= "\n\n---\n\n";
}
return trim( $output );
}
/**
* Basic HTML to Markdown
*/
private function html_to_markdown( $html ) {
// Remove WordPress block comments
$html = preg_replace( '/<!-- \/?wp:[^>]+ -->/', '', $html );
$replacements = array(
'/<h1[^>]*>(.*?)<\/h1>/i' => "# $1\n\n",
'/<h2[^>]*>(.*?)<\/h2>/i' => "## $1\n\n",
'/<h3[^>]*>(.*?)<\/h3>/i' => "### $1\n\n",
'/<p[^>]*>(.*?)<\/p>/is' => "$1\n\n",
'/<strong>(.*?)<\/strong>/i' => "**$1**",
'/<em>(.*?)<\/em>/i' => "*$1*",
'/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/i' => "[$2]($1)",
'/<ul[^>]*>(.*?)<\/ul>/is' => "$1\n",
'/<li[^>]*>(.*?)<\/li>/i' => "- $1\n",
'/<code>(.*?)<\/code>/i' => "`$1`",
'/<br\s*\/?>/i' => "\n",
);
foreach ( $replacements as $pattern => $replacement ) {
$html = preg_replace( $pattern, $replacement, $html );
}
return wp_strip_all_tags( $html );
}
Output Example
{
"id": 123,
"title": "Getting Started with WordPress",
"slug": "getting-started-wordpress",
"date": "2026-04-15 10:30:00",
"modified": "2026-04-18 14:22:00",
"status": "publish",
"type": "post",
"url": "https://example.com/getting-started-wordpress/",
"author": {
"id": 1,
"name": "John Doe",
"slug": "john-doe"
},
"content": "<!-- wp:paragraph -->Raw content here...",
"content_rendered": "<p>Rendered HTML content...</p>",
"excerpt": "A quick introduction to WordPress...",
"featured_image": {
"id": 456,
"url": "https://example.com/wp-content/uploads/image.jpg",
"width": 1200,
"height": 630,
"alt": "WordPress dashboard"
},
"taxonomies": {
"category": [
{ "id": 1, "name": "Tutorials", "slug": "tutorials" }
],
"post_tag": [
{ "id": 5, "name": "WordPress", "slug": "wordpress" }
]
}
}
Use Cases
- Migrating to Gatsby/Next.js export, transform, import
- Backup portable content outside the database
- Headless WordPress pre-generate JSON for frontend consumption
- Static site migration export to Markdown for Hugo/Jekyll
- Content analysis process exported JSON with external tools
ACF Support
If you use Advanced Custom Fields, add this method:
/**
* Get ACF fields
*/
private function get_acf_fields( $post_id ) {
if ( ! function_exists( 'get_fields' ) ) {
return array();
}
$fields = get_fields( $post_id );
return $fields ? $fields : array();
}
Then add 'acf' to the include array option.
If you need a ready-to-use solution with admin UI, date filtering, and format options, I built Content Exporter for exactly this.
Resources:
Top comments (0)