DEV Community

Cover image for Exporting WordPress Content to JSON for Headless and Static Sites
anti Gym Club
anti Gym Club

Posted on Originally published at antigymclub.com

Exporting WordPress Content to JSON for Headless and Static Sites

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
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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' ),
) );
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 );
}
Enter fullscreen mode Exit fullscreen mode

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" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)