2026-09-20 08:31:23 +02:00
< ? php
/**
* Plugin Name : RBR Export to Ghost
* Plugin URI :
* Description : Exportiert WordPress - Beiträge und Medien über eine REST - API für den Import in Ghost CMS .
* Version : 1.0 . 0
* Author : Roland Brunner
* Author URI :
* License : GPL - 2.0 +
* License URI : https :// www . gnu . org / licenses / gpl - 2.0 . txt
* Text Domain : rbr - export - ghost
* Domain Path : / languages
*/
if ( ! defined ( 'ABSPATH' )) {
exit ;
}
define ( 'RBR_EXPORT_VERSION' , '1.0.0' );
define ( 'RBR_EXPORT_PATH' , plugin_dir_path ( __FILE__ ));
define ( 'RBR_EXPORT_URL' , plugin_dir_url ( __FILE__ ));
register_activation_hook ( __FILE__ , 'rbr_export_activate' );
register_deactivation_hook ( __FILE__ , 'rbr_export_deactivate' );
function rbr_export_activate () {
flush_rewrite_rules ();
}
function rbr_export_deactivate () {
flush_rewrite_rules ();
}
/**
* Sanitisiert den per_page - Wert und bewahrt - 1 für " alle " .
*/
function rbr_sanitize_per_page ( $value ) {
return ( int ) $value ;
}
add_action ( 'rest_api_init' , function () {
// 1. Endpunkt: Alle Post-IDs abrufen
register_rest_route ( 'custom/v1' , '/posts-ids' , array (
'methods' => 'GET' ,
'callback' => 'rbr_get_all_post_ids' ,
'permission_callback' => '__return_true' ,
'args' => array (
'post_type' => array (
'default' => 'post' ,
'sanitize_callback' => 'sanitize_text_field' ,
),
'per_page' => array (
'default' => - 1 ,
'sanitize_callback' => 'rbr_sanitize_per_page' ,
),
),
));
// 2. Endpunkt: Content & Medien für eine bestimmte Post-ID
register_rest_route ( 'custom/v1' , '/post-details/(?P<id>\d+)' , array (
'methods' => 'GET' ,
'callback' => 'rbr_get_post_details_with_media' ,
'permission_callback' => '__return_true' ,
));
// 3. Endpunkt: Kategorien/Tags für alle Beiträge
register_rest_route ( 'custom/v1' , '/post-taxonomies/(?P<id>\d+)' , array (
'methods' => 'GET' ,
'callback' => 'rbr_get_post_taxonomies' ,
'permission_callback' => '__return_true' ,
));
// 4. Endpunkt: Alle Medien-Dateien (Attachments) abrufen
register_rest_route ( 'custom/v1' , '/media' , array (
'methods' => 'GET' ,
'callback' => 'rbr_get_all_media' ,
'permission_callback' => '__return_true' ,
'args' => array (
'per_page' => array (
'default' => - 1 ,
'sanitize_callback' => 'rbr_sanitize_per_page' ,
),
),
));
// 5. Endpunkt: Medien-Datei direkt über ID abrufen
register_rest_route ( 'custom/v1' , '/media/(?P<id>\d+)' , array (
'methods' => 'GET' ,
'callback' => 'rbr_get_media_by_id' ,
'permission_callback' => '__return_true' ,
));
});
/**
* Liefert ein Array mit allen veröffentlichten Post - IDs .
* Unterstützt verschiedene Post - Types ( post , page , custom ) .
*/
function rbr_get_all_post_ids ( $request ) {
$post_type = sanitize_text_field ( $request [ 'post_type' ]);
$per_page = rbr_sanitize_per_page ( $request [ 'per_page' ]);
$args = array (
'post_type' => $post_type ,
'post_status' => 'publish' ,
'numberposts' => $per_page ,
'fields' => 'ids' ,
'orderby' => 'date' ,
'order' => 'ASC' ,
);
$post_ids = get_posts ( $args );
if ( empty ( $post_ids )) {
return new WP_Error ( 'no_posts' , 'Keine Beiträge gefunden.' , array ( 'status' => 404 ));
}
return rest_ensure_response ( array (
'count' => count ( $post_ids ),
'post_type' => $post_type ,
'ids' => $post_ids ,
));
}
/**
* Liefert Content , Featured Image , Kategorien , Keywords , Comments und alle im HTML / Post genutzten Medien - URLs .
*/
function rbr_get_post_details_with_media ( $data ) {
$post_id = intval ( $data [ 'id' ]);
$post = get_post ( $post_id );
if ( ! $post || $post -> post_status !== 'publish' ) {
return new WP_Error ( 'no_post' , 'Beitrag nicht gefunden.' , array ( 'status' => 404 ));
}
$content = apply_filters ( 'the_content' , $post -> post_content );
$media_urls = array ();
// 1. Featured Image (Titelbild) auslesen
$featured_image_url = '' ;
$featured_image_id = get_post_thumbnail_id ( $post_id );
if ( $featured_image_id ) {
$featured_image_url = wp_get_attachment_url ( $featured_image_id );
if ( $featured_image_url ) {
$media_urls [] = $featured_image_url ;
}
}
// 2. Inhaltsbilder aus dem HTML extrahieren (via Regex für <img> Tags)
preg_match_all ( '/<img[^>]+src=["\']([^"\']+)["\']/i' , $post -> post_content , $matches );
if ( ! empty ( $matches [ 1 ])) {
foreach ( $matches [ 1 ] as $img_url ) {
if ( filter_var ( $img_url , FILTER_VALIDATE_URL )) {
$media_urls [] = $img_url ;
}
}
}
// 3. Attachments die mit dem Post verknüpft sind
$attachments = get_posts ( array (
'post_type' => 'attachment' ,
'numberposts' => - 1 ,
'post_status' => null ,
'post_parent' => $post_id ,
'fields' => 'ids' ,
'post_mime_type' => 'image' ,
));
foreach ( $attachments as $att_id ) {
$att_url = wp_get_attachment_url ( $att_id );
if ( $att_url ) {
$media_urls [] = $att_url ;
}
}
// Duplikate entfernen
$unique_media = array_values ( array_unique ( $media_urls ));
2026-09-22 08:08:51 +02:00
// 4. Kategorien
2026-09-20 08:31:23 +02:00
$categories = array ();
$cats = get_the_category ( $post_id );
if ( ! empty ( $cats ) && ! is_wp_error ( $cats )) {
foreach ( $cats as $cat ) {
$categories [] = array (
'id' => intval ( $cat -> term_id ),
'name' => $cat -> name ,
'slug' => $cat -> slug ,
);
}
}
2026-09-22 08:08:51 +02:00
// 5. Tags (post_tag Taxonomy)
$tags = array ();
$post_tags = get_the_tags ( $post_id );
if ( ! empty ( $post_tags ) && ! is_wp_error ( $post_tags )) {
foreach ( $post_tags as $tag ) {
$tags [] = array (
'id' => intval ( $tag -> term_id ),
'name' => $tag -> name ,
'slug' => $tag -> slug ,
);
}
}
// 6. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
2026-09-20 08:31:23 +02:00
$keywords = '' ;
2026-09-22 08:08:51 +02:00
// Yoast SEO: Prüfe auf Meta-Key direkt (funktioniert in allen Versionen)
$yoast_kw = get_post_meta ( $post_id , '_yoast_wpseo_focuskw' , true );
if ( ! empty ( $yoast_kw )) {
$keywords = $yoast_kw ;
2026-09-20 08:31:23 +02:00
}
// Rank Math
if ( empty ( $keywords ) && function_exists ( 'rank_math_get_single_post_data' )) {
$rank_data = rank_math_get_single_post_data ( $post_id );
2026-09-22 08:08:51 +02:00
if ( method_exists ( $rank_data , 'get_keywords' )) {
$kw = $rank_data -> get_keywords ();
if ( ! empty ( $kw )) {
$keywords = $kw ;
}
2026-09-20 08:31:23 +02:00
}
}
// Fallback: Custom Field 'post_keywords'
if ( empty ( $keywords )) {
$keywords = get_post_meta ( $post_id , 'post_keywords' , true );
}
2026-09-22 08:08:51 +02:00
// Fallback: Tags als Keywords
if ( empty ( $keywords ) && ! empty ( $tags )) {
$keywords = implode ( ', ' , array_column ( $tags , 'name' ));
}
2026-09-20 08:31:23 +02:00
// Fallback: Kategorien als Keywords
if ( empty ( $keywords ) && ! empty ( $categories )) {
$keywords = implode ( ', ' , array_column ( $categories , 'name' ));
}
// 6. Kommentare
$comments = array ();
$wp_comments = get_comments ( array (
'post_id' => $post_id ,
'status' => 'approve' ,
'number' => 100 ,
));
foreach ( $wp_comments as $comment ) {
$comments [] = array (
'id' => intval ( $comment -> comment_ID ),
'author_name' => $comment -> comment_author ,
'author_email' => $comment -> comment_author_email ,
'author_url' => $comment -> comment_author_url ,
'content' => $comment -> comment_content ,
'date' => mysql_to_rfc3339 ( $comment -> comment_date ),
);
}
$response = array (
'id' => intval ( $post -> ID ),
'title' => $post -> post_title ,
'slug' => $post -> post_name ,
'date' => mysql_to_rfc3339 ( $post -> post_date ),
'content' => $content ,
'excerpt' => wp_trim_words ( strip_tags ( $post -> post_excerpt ), 55 ),
'author' => array (
'id' => intval ( $post -> post_author ),
'name' => get_the_author_meta ( 'display_name' , $post -> post_author ),
),
'featured_image' => $featured_image_url ,
'categories' => $categories ,
2026-09-22 08:08:51 +02:00
'tags' => $tags ,
2026-09-20 08:31:23 +02:00
'keywords' => $keywords ,
'comments' => $comments ,
'media_files' => $unique_media ,
);
return rest_ensure_response ( $response );
}
/**
* Liefert Kategorien und Tags für einen bestimmten Beitrag .
*/
function rbr_get_post_taxonomies ( $data ) {
$post_id = intval ( $data [ 'id' ]);
$post = get_post ( $post_id );
if ( ! $post || $post -> post_status !== 'publish' ) {
return new WP_Error ( 'no_post' , 'Beitrag nicht gefunden.' , array ( 'status' => 404 ));
}
$taxonomies = get_object_taxonomies ( 'post' );
$terms = array ();
foreach ( $taxonomies as $taxonomy ) {
$terms_list = get_the_terms ( $post_id , $taxonomy );
if ( ! empty ( $terms_list ) && ! is_wp_error ( $terms_list )) {
foreach ( $terms_list as $term ) {
$terms [] = array (
'id' => intval ( $term -> term_id ),
'name' => $term -> name ,
'slug' => $term -> slug ,
'type' => $taxonomy ,
);
}
}
}
return rest_ensure_response ( array (
'post_id' => $post_id ,
'taxonomies' => $terms ,
));
}
/**
* Liefert alle Medien - Dateien ( Attachments ) der WordPress - Instanz .
*/
function rbr_get_all_media ( $request ) {
$per_page = rbr_sanitize_per_page ( $request [ 'per_page' ]);
$args = array (
'post_type' => 'attachment' ,
'post_status' => 'inherit' ,
'numberposts' => $per_page ,
'orderby' => 'date' ,
'order' => 'ASC' ,
'post_mime_type' => array ( 'image/jpeg' , 'image/png' , 'image/gif' , 'image/webp' , 'image/svg+xml' ),
);
$attachments = get_posts ( $args );
$media_list = array ();
foreach ( $attachments as $att ) {
$media_list [] = array (
'id' => intval ( $att -> ID ),
'url' => wp_get_attachment_url ( $att -> ID ),
'title' => $att -> post_title ,
'alt' => get_post_meta ( $att -> ID , '_wp_attachment_image_alt' , true ),
'mime' => $att -> post_mime_type ,
'date' => mysql_to_rfc3339 ( $att -> post_date ),
);
}
return rest_ensure_response ( array (
'count' => count ( $media_list ),
'media' => $media_list ,
));
}
/**
* Liefert eine einzelne Medien - Datei anhand ihrer ID .
*/
function rbr_get_media_by_id ( $data ) {
$media_id = intval ( $data [ 'id' ]);
$attachment = get_post ( $media_id );
if ( ! $attachment || $attachment -> post_type !== 'attachment' ) {
return new WP_Error ( 'no_media' , 'Medien-Datei nicht gefunden.' , array ( 'status' => 404 ));
}
return rest_ensure_response ( array (
'id' => intval ( $attachment -> ID ),
'url' => wp_get_attachment_url ( $attachment -> ID ),
'title' => $attachment -> post_title ,
'alt' => get_post_meta ( $attachment -> ID , '_wp_attachment_image_alt' , true ),
'mime' => $attachment -> post_mime_type ,
'date' => mysql_to_rfc3339 ( $attachment -> post_date ),
));
}
/**
* Shortcode : [ rbr_export ] – Zeigt eine Export - Statistik im Frontend an .
*/
function rbr_export_shortcode ( $atts ) {
$atts = shortcode_atts ( array (
'post_type' => 'post' ,
), $atts , 'rbr_export' );
$count = wp_count_posts ( $atts [ 'post_type' ]);
$published = $count -> publish ? ? 0 ;
return sprintf (
'<div class="rbr-export-info"><p>Es gibt <strong>%d</strong> veröffentlichte %s-Beiträge, die über die REST-API exportiert werden können.</p></div>' ,
intval ( $published ),
esc_html ( $atts [ 'post_type' ])
);
}
add_shortcode ( 'rbr_export' , 'rbr_export_shortcode' );
/**
* Admin - Notiz : Plugin ist aktiv und bereit für den Export .
*/
add_action ( 'admin_notices' , function () {
if ( current_user_can ( 'manage_options' )) {
echo '<div class="notice notice-info is-dismissible"><p>' ;
printf (
__ ( 'RBR Export to Ghost ist aktiv. <a href="%s">REST-API-Endpunkte</a> stehen unter <code>/wp-json/custom/v1/</code> zur Verfügung.' , 'rbr-export-ghost' ),
admin_url ( 'tools.php' )
);
echo '</p></div>' ;
}
});
/**
* CORS - Header erlauben ( falls nötig für den C #-Importer)
*/
add_filter ( 'rest_pre_serve_request' , function ( $served , $result , $request ) {
if ( strpos ( $request -> get_route (), 'custom/v1/' ) === 0 ) {
header ( 'Access-Control-Allow-Origin: *' );
header ( 'Access-Control-Allow-Methods: GET, OPTIONS' );
header ( 'Access-Control-Allow-Headers: Authorization, Content-Type' );
}
return $served ;
}, 10 , 3 );
add_action ( 'rest_api_init' , function () {
// OPTIONS-Requests für CORS erlauben
register_rest_route ( 'custom/v1' , '/cors' , array (
'methods' => 'OPTIONS' ,
'callback' => function () {
return new WP_REST_Response ( null , 200 );
},
'permission_callback' => '__return_true' ,
));
});