VOOZH about

URL: https://dev.to/antigymclub/exporting-wordpress-content-to-json-for-headless-and-static-sites-4h2c

⇱ Exporting WordPress Content to JSON for Headless and Static Sites - DEV Community


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: