Add post synchronization functionality for Markdown files
- Implemented a parser for YAML front-matter in Markdown files. - Developed functions to convert Markdown content to HTML. - Created a pipeline to sync WordPress posts from a specified folder in a Git repository. - Added media import capabilities to handle images referenced in Markdown. - Implemented author resolution and post slug generation. - Included error handling and logging for sync operations. - Enabled trashing of posts that are no longer present in the repository.
This commit is contained in:
@@ -39,6 +39,13 @@ add_action( 'admin_post_oribi_sync_save_settings', function () {
|
|||||||
update_option( 'oribi_sync_branch', $branch, 'no' );
|
update_option( 'oribi_sync_branch', $branch, 'no' );
|
||||||
update_option( 'oribi_sync_provider', $provider, 'no' );
|
update_option( 'oribi_sync_provider', $provider, 'no' );
|
||||||
|
|
||||||
|
// Posts sync settings
|
||||||
|
$posts_enabled = isset( $_POST['oribi_sync_posts_enabled'] ) ? '1' : '0';
|
||||||
|
$posts_folder = sanitize_text_field( wp_unslash( $_POST['oribi_sync_posts_folder'] ?? 'posts' ) );
|
||||||
|
$posts_folder = trim( $posts_folder, '/' ) ?: 'posts';
|
||||||
|
update_option( 'oribi_sync_posts_enabled', $posts_enabled, 'no' );
|
||||||
|
update_option( 'oribi_sync_posts_folder', $posts_folder, 'no' );
|
||||||
|
|
||||||
// Only update PAT if a new one was provided (non-empty)
|
// Only update PAT if a new one was provided (non-empty)
|
||||||
if ( ! empty( $pat ) ) {
|
if ( ! empty( $pat ) ) {
|
||||||
oribi_sync_save_pat( $pat );
|
oribi_sync_save_pat( $pat );
|
||||||
@@ -106,7 +113,10 @@ add_action( 'admin_post_oribi_sync_push', function () {
|
|||||||
$post_id = (int) ( $_POST['oribi_sync_push_post_id'] ?? 0 );
|
$post_id = (int) ( $_POST['oribi_sync_push_post_id'] ?? 0 );
|
||||||
|
|
||||||
if ( $post_id > 0 ) {
|
if ( $post_id > 0 ) {
|
||||||
$result = oribi_sync_push_page( $post_id );
|
$post = get_post( $post_id );
|
||||||
|
$result = ( $post && $post->post_type === 'post' )
|
||||||
|
? oribi_sync_push_post( $post_id )
|
||||||
|
: oribi_sync_push_page( $post_id );
|
||||||
set_transient( 'oribi_sync_push_result', $result, 60 );
|
set_transient( 'oribi_sync_push_result', $result, 60 );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +259,36 @@ function oribi_sync_settings_page() {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top:1.5em;">Posts Sync</h2>
|
||||||
|
<p class="description">Import WordPress posts from Markdown files with YAML front-matter stored in a repo subfolder.</p>
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Enable Posts Sync</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="oribi_sync_posts_enabled" value="1"
|
||||||
|
<?php checked( get_option( 'oribi_sync_posts_enabled', '' ), '1' ); ?> />
|
||||||
|
Sync posts from the repository
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="oribi_sync_posts_folder">Posts Folder</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="oribi_sync_posts_folder" id="oribi_sync_posts_folder"
|
||||||
|
value="<?php echo esc_attr( get_option( 'oribi_sync_posts_folder', 'posts' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="posts" />
|
||||||
|
<p class="description">
|
||||||
|
Repo folder containing <code>.md</code> files (e.g. <code>posts</code> or <code>content/posts</code>).
|
||||||
|
Files may use <code>YYYY-MM-DD-slug.md</code> naming. Supported front-matter keys:
|
||||||
|
<code>title</code>, <code>slug</code>, <code>status</code>, <code>date</code>,
|
||||||
|
<code>author</code>, <code>categories</code>, <code>tags</code>,
|
||||||
|
<code>excerpt</code>, <code>featured_image</code>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<?php submit_button( 'Save Settings' ); ?>
|
<?php submit_button( 'Save Settings' ); ?>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -333,6 +373,61 @@ function oribi_sync_settings_page() {
|
|||||||
</table>
|
</table>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// ── Synced posts ──────────────────────────────────────────────────
|
||||||
|
if ( get_option( 'oribi_sync_posts_enabled', '' ) ):
|
||||||
|
$synced_posts = new WP_Query( [
|
||||||
|
'post_type' => 'post',
|
||||||
|
'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
|
||||||
|
'meta_key' => '_oribi_sync_checksum',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( $synced_posts->have_posts() ):
|
||||||
|
?>
|
||||||
|
<h2>Synced Posts</h2>
|
||||||
|
<table class="widefat fixed striped oribi-sync-pages">
|
||||||
|
<thead><tr>
|
||||||
|
<th>Post</th>
|
||||||
|
<th style="width:80px;">Push</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php while ( $synced_posts->have_posts() ): $synced_posts->the_post();
|
||||||
|
$pid = get_the_ID();
|
||||||
|
$last_push = get_post_meta( $pid, '_oribi_sync_last_push', true );
|
||||||
|
$pr_url = get_post_meta( $pid, '_oribi_sync_pr_url', true );
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><?php the_title(); ?></strong>
|
||||||
|
<span class="oribi-sync-muted"><?php echo esc_html( get_post_field( 'post_name', $pid ) ); ?></span>
|
||||||
|
<span class="oribi-sync-muted"> — <?php echo esc_html( get_post_status( $pid ) ); ?></span>
|
||||||
|
<?php if ( $pr_url ): ?>
|
||||||
|
<a href="<?php echo esc_url( $pr_url ); ?>" target="_blank" rel="noopener" class="oribi-sync-pr-badge">PR</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $last_push ): ?>
|
||||||
|
<br /><span class="oribi-sync-muted">pushed <?php echo esc_html( $last_push ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin:0;">
|
||||||
|
<input type="hidden" name="action" value="oribi_sync_push" />
|
||||||
|
<input type="hidden" name="oribi_sync_push_post_id" value="<?php echo esc_attr( $pid ); ?>" />
|
||||||
|
<?php wp_nonce_field( 'oribi_sync_push' ); ?>
|
||||||
|
<button type="submit" class="button button-small">Push</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endwhile; wp_reset_postdata(); ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
endif; // have_posts
|
||||||
|
endif; // posts_enabled
|
||||||
|
?>
|
||||||
|
|
||||||
<?php // ── Theme preview (conditional) ────────────────────────────── ?>
|
<?php // ── Theme preview (conditional) ────────────────────────────── ?>
|
||||||
<?php if ( isset( $_GET['oribi_sync_tab'] ) && $_GET['oribi_sync_tab'] === 'theme' ): ?>
|
<?php if ( isset( $_GET['oribi_sync_tab'] ) && $_GET['oribi_sync_tab'] === 'theme' ): ?>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -374,12 +469,15 @@ function oribi_sync_settings_page() {
|
|||||||
*/
|
*/
|
||||||
function oribi_sync_render_result_list( array $r ): void {
|
function oribi_sync_render_result_list( array $r ): void {
|
||||||
$items = [];
|
$items = [];
|
||||||
if ( ! empty( $r['created'] ) ) $items[] = 'Created: ' . implode( ', ', $r['created'] );
|
if ( ! empty( $r['created'] ) ) $items[] = 'Pages created: ' . implode( ', ', $r['created'] );
|
||||||
if ( ! empty( $r['updated'] ) ) $items[] = 'Updated: ' . implode( ', ', $r['updated'] );
|
if ( ! empty( $r['updated'] ) ) $items[] = 'Pages updated: ' . implode( ', ', $r['updated'] );
|
||||||
if ( ! empty( $r['theme_updated'] ) ) $items[] = 'Theme: ' . implode( ', ', $r['theme_updated'] );
|
if ( ! empty( $r['theme_updated'] ) ) $items[] = 'Theme: ' . implode( ', ', $r['theme_updated'] );
|
||||||
if ( ! empty( $r['trashed'] ) ) $items[] = 'Trashed: ' . implode( ', ', $r['trashed'] );
|
if ( ! empty( $r['trashed'] ) ) $items[] = 'Pages trashed: ' . implode( ', ', $r['trashed'] );
|
||||||
if ( ! empty( $r['skipped'] ) ) $items[] = 'Skipped: ' . implode( ', ', $r['skipped'] );
|
if ( ! empty( $r['posts_created'] ) ) $items[] = 'Posts created: ' . implode( ', ', $r['posts_created'] );
|
||||||
if ( ! empty( $r['errors'] ) ) $items[] = 'Errors: ' . implode( '; ', $r['errors'] );
|
if ( ! empty( $r['posts_updated'] ) ) $items[] = 'Posts updated: ' . implode( ', ', $r['posts_updated'] );
|
||||||
|
if ( ! empty( $r['posts_trashed'] ) ) $items[] = 'Posts trashed: ' . implode( ', ', $r['posts_trashed'] );
|
||||||
|
if ( ! empty( $r['skipped'] ) ) $items[] = 'Skipped: ' . implode( ', ', $r['skipped'] );
|
||||||
|
if ( ! empty( $r['errors'] ) ) $items[] = 'Errors: ' . implode( '; ', $r['errors'] );
|
||||||
|
|
||||||
if ( empty( $items ) ) { echo '<p>No changes.</p>'; return; }
|
if ( empty( $items ) ) { echo '<p>No changes.</p>'; return; }
|
||||||
|
|
||||||
|
|||||||
1065
includes/post-sync.php
Normal file
1065
includes/post-sync.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -44,36 +44,6 @@ function oribi_sync_detect_pages_prefix(): string {
|
|||||||
return 'pages/';
|
return 'pages/';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Auto-push on page save ──────────────────────────────────────────────────
|
|
||||||
add_action( 'save_post_page', 'oribi_sync_maybe_push_on_save', 20, 3 );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push a synced page to the repo whenever it is saved.
|
|
||||||
*
|
|
||||||
* Only fires for pages that were previously pulled (have _oribi_sync_checksum
|
|
||||||
* meta), skips autosaves, revisions, and non-publish statuses.
|
|
||||||
* Uses a static guard to prevent re-entry when we update post meta after push.
|
|
||||||
*/
|
|
||||||
function oribi_sync_maybe_push_on_save( int $post_id, WP_Post $post, bool $update ): void {
|
|
||||||
// Guard: only on genuine content saves
|
|
||||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
|
|
||||||
if ( wp_is_post_revision( $post_id ) ) return;
|
|
||||||
if ( $post->post_status !== 'publish' ) return;
|
|
||||||
|
|
||||||
// Guard: only pages that came from a sync (have checksum meta)
|
|
||||||
$checksum = get_post_meta( $post_id, '_oribi_sync_checksum', true );
|
|
||||||
if ( empty( $checksum ) ) return;
|
|
||||||
|
|
||||||
// Guard: prevent re-entry when push updates meta on the same post
|
|
||||||
static $pushing = [];
|
|
||||||
if ( isset( $pushing[ $post_id ] ) ) return;
|
|
||||||
$pushing[ $post_id ] = true;
|
|
||||||
|
|
||||||
oribi_sync_push_page( $post_id );
|
|
||||||
|
|
||||||
unset( $pushing[ $post_id ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Generic authenticated request helpers ────────────────────────────────────
|
// ─── Generic authenticated request helpers ────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -105,13 +105,16 @@ function oribi_sync_execute_php( string $php_source, string $slug ) {
|
|||||||
*/
|
*/
|
||||||
function oribi_sync_run( bool $dry_run = false ): array {
|
function oribi_sync_run( bool $dry_run = false ): array {
|
||||||
$result = [
|
$result = [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'created' => [],
|
'created' => [],
|
||||||
'updated' => [],
|
'updated' => [],
|
||||||
'trashed' => [],
|
'trashed' => [],
|
||||||
'skipped' => [],
|
'skipped' => [],
|
||||||
'errors' => [],
|
'errors' => [],
|
||||||
'theme_updated' => [],
|
'theme_updated' => [],
|
||||||
|
'posts_created' => [],
|
||||||
|
'posts_updated' => [],
|
||||||
|
'posts_trashed' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Gather settings ────────────────────────────────────────────────────
|
// ── Gather settings ────────────────────────────────────────────────────
|
||||||
@@ -296,6 +299,18 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
$result['errors'][] = '[theme] ' . $err;
|
$result['errors'][] = '[theme] ' . $err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sync posts from repo posts folder ─────────────────────────────────
|
||||||
|
$posts_sync = oribi_sync_run_posts( $api_base, $branch, $provider, $pat, $tree, $dry_run );
|
||||||
|
$result['posts_created'] = $posts_sync['created'];
|
||||||
|
$result['posts_updated'] = $posts_sync['updated'];
|
||||||
|
$result['posts_trashed'] = $posts_sync['trashed'];
|
||||||
|
foreach ( $posts_sync['skipped'] as $sk ) {
|
||||||
|
$result['skipped'][] = '[post] ' . $sk;
|
||||||
|
}
|
||||||
|
foreach ( $posts_sync['errors'] as $err ) {
|
||||||
|
$result['errors'][] = '[post] ' . $err;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Record run ─────────────────────────────────────────────────────────
|
// ── Record run ─────────────────────────────────────────────────────────
|
||||||
if ( ! $dry_run ) {
|
if ( ! $dry_run ) {
|
||||||
oribi_sync_record_run( $result );
|
oribi_sync_record_run( $result );
|
||||||
@@ -387,6 +402,9 @@ function oribi_sync_record_run( array $result ): void {
|
|||||||
'skipped' => $result['skipped'],
|
'skipped' => $result['skipped'],
|
||||||
'errors' => $result['errors'],
|
'errors' => $result['errors'],
|
||||||
'theme_updated' => $result['theme_updated'] ?? [],
|
'theme_updated' => $result['theme_updated'] ?? [],
|
||||||
|
'posts_created' => $result['posts_created'] ?? [],
|
||||||
|
'posts_updated' => $result['posts_updated'] ?? [],
|
||||||
|
'posts_trashed' => $result['posts_trashed'] ?? [],
|
||||||
] );
|
] );
|
||||||
|
|
||||||
// Keep last 20 entries
|
// Keep last 20 entries
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ require_once ORIBI_SYNC_DIR . 'includes/crypto.php';
|
|||||||
require_once ORIBI_SYNC_DIR . 'includes/api-client.php';
|
require_once ORIBI_SYNC_DIR . 'includes/api-client.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/sync-engine.php';
|
require_once ORIBI_SYNC_DIR . 'includes/sync-engine.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/push-client.php';
|
require_once ORIBI_SYNC_DIR . 'includes/push-client.php';
|
||||||
|
require_once ORIBI_SYNC_DIR . 'includes/post-sync.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/admin.php';
|
require_once ORIBI_SYNC_DIR . 'includes/admin.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/rest.php';
|
require_once ORIBI_SYNC_DIR . 'includes/rest.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/theme-preview.php';
|
require_once ORIBI_SYNC_DIR . 'includes/theme-preview.php';
|
||||||
@@ -30,12 +31,14 @@ register_deactivation_hook( __FILE__, 'oribi_sync_deactivate' );
|
|||||||
|
|
||||||
function oribi_sync_activate() {
|
function oribi_sync_activate() {
|
||||||
// Ensure default options exist
|
// Ensure default options exist
|
||||||
add_option( 'oribi_sync_repo', '', '', 'no' );
|
add_option( 'oribi_sync_repo', '', '', 'no' );
|
||||||
add_option( 'oribi_sync_branch', 'main', '', 'no' );
|
add_option( 'oribi_sync_branch', 'main', '', 'no' );
|
||||||
add_option( 'oribi_sync_provider', '', '', 'no' );
|
add_option( 'oribi_sync_provider', '', '', 'no' );
|
||||||
add_option( 'oribi_sync_pat', '', '', 'no' );
|
add_option( 'oribi_sync_pat', '', '', 'no' );
|
||||||
add_option( 'oribi_sync_last_run', '', '', 'no' );
|
add_option( 'oribi_sync_last_run', '', '', 'no' );
|
||||||
add_option( 'oribi_sync_log', [], '', 'no' );
|
add_option( 'oribi_sync_log', [], '', 'no' );
|
||||||
|
add_option( 'oribi_sync_posts_enabled', '', '', 'no' );
|
||||||
|
add_option( 'oribi_sync_posts_folder', 'posts', '', 'no' );
|
||||||
}
|
}
|
||||||
|
|
||||||
function oribi_sync_deactivate() {
|
function oribi_sync_deactivate() {
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ delete_option( 'oribi_sync_log' );
|
|||||||
delete_option( 'oribi_sync_webhook_secret' );
|
delete_option( 'oribi_sync_webhook_secret' );
|
||||||
delete_option( 'oribi_sync_theme_applied' );
|
delete_option( 'oribi_sync_theme_applied' );
|
||||||
delete_option( 'oribi_sync_push_log' );
|
delete_option( 'oribi_sync_push_log' );
|
||||||
|
delete_option( 'oribi_sync_posts_enabled' );
|
||||||
|
delete_option( 'oribi_sync_posts_folder' );
|
||||||
|
|
||||||
// Remove sync metadata from posts
|
// Remove sync metadata from pages and posts
|
||||||
$posts = get_posts( [
|
$posts = get_posts( [
|
||||||
'post_type' => 'page',
|
'post_type' => [ 'page', 'post' ],
|
||||||
'post_status' => 'any',
|
'post_status' => 'any',
|
||||||
'posts_per_page' => -1,
|
'posts_per_page' => -1,
|
||||||
'meta_key' => '_oribi_sync_checksum',
|
'meta_key' => '_oribi_sync_checksum',
|
||||||
@@ -36,5 +38,17 @@ foreach ( $posts as $post_id ) {
|
|||||||
delete_post_meta( $post_id, '_oribi_sync_pr_url' );
|
delete_post_meta( $post_id, '_oribi_sync_pr_url' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove origin src meta from media attachments
|
||||||
|
$attachments = get_posts( [
|
||||||
|
'post_type' => 'attachment',
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_key' => '_oribi_sync_origin_src',
|
||||||
|
'fields' => 'ids',
|
||||||
|
] );
|
||||||
|
foreach ( $attachments as $att_id ) {
|
||||||
|
delete_post_meta( $att_id, '_oribi_sync_origin_src' );
|
||||||
|
}
|
||||||
|
|
||||||
// Clear any scheduled cron
|
// Clear any scheduled cron
|
||||||
wp_clear_scheduled_hook( 'oribi_sync_cron_run' );
|
wp_clear_scheduled_hook( 'oribi_sync_cron_run' );
|
||||||
|
|||||||
Reference in New Issue
Block a user