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)