Initial commit!
This commit is contained in:
commit
dcd704ec57
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
77
README.md
Normal file
77
README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Drupal2WP Toolkit
|
||||
|
||||
Flexible content migration and taxonomy structuring for modern WordPress sites.
|
||||
|
||||
Drupal2WP Toolkit provides an extensible command-line and plugin-based bridge between Drupal 8 and WordPress 6, optimized for developers handling structured content migration. It includes taxonomy mapping, sortable categories, UI enhancements, and future-ready plans for block-based template integration.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Features
|
||||
|
||||
- **Drupal → WordPress Migration**
|
||||
- Imports all Drupal nodes as WordPress **Pages** by default
|
||||
- Vocabulary terms are converted into **top-level categories**
|
||||
- Associated Drupal terms become **sub-categories** under their vocabularies
|
||||
- Drupal content types are added as **top-level categories** for structural clarity
|
||||
|
||||
- **Admin UI Enhancements**
|
||||
- Adds a sortable **Last Modified** column to Pages view
|
||||
- Top-level categories are also **sortable**
|
||||
- Category filters available directly in admin for Pages
|
||||
|
||||
- **WP-CLI Utilities**
|
||||
- Run `wp drupal2wp_toolkit import` to initiate Drupal content ingestion
|
||||
- Easily convert migrated Pages into **Posts**, retaining taxonomy mapping
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Future Plans
|
||||
|
||||
- **Block Theme Editor Integration**
|
||||
- Assign categories to custom templates or reusable block layouts
|
||||
- Enable GUI-based taxonomy-driven theming inside WordPress's Site Editor
|
||||
|
||||
- **Advanced Taxonomy Mapping**
|
||||
- Support for custom post types and multi-taxonomy assignment
|
||||
|
||||
- **Content Blueprinting**
|
||||
- Define reusable structures based on imported vocabularies and content types
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Installation
|
||||
|
||||
1. Clone the repository into your WordPress plugins directory:
|
||||
|
||||
```bash
|
||||
git clone https://git.bikeshopi.dev/bike/drupal2wp_toolkit.git wp-content/plugins/drupal2wp_toolkit
|
||||
```
|
||||
|
||||
2. Activate the plugin:
|
||||
|
||||
```bash
|
||||
wp plugin activate drupal2wp_toolkit
|
||||
```
|
||||
|
||||
3. Run the import command:
|
||||
```bash
|
||||
wp drupal2wp_toolkit import
|
||||
```
|
||||
|
||||
## 📁 Repo Structure
|
||||
```
|
||||
drupal2wp_toolkit/
|
||||
├── page-taxonomy/
|
||||
│ └── page-taxonomy.php
|
||||
├── drupal2wp_toolkit.php
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🙌 Credits
|
||||
Created and maintained by developers at BikeShopi Dev. Contributions and feedback welcome — this is a toolkit built for extensibility.
|
||||
|
||||
## 📄 License
|
||||
MIT License. See LICENSE.md for details.
|
||||
|
||||
|
||||
Let me know if you want version badges, contributor guidelines, or sample config
|
528
drupal2wp-toolkit.php
Normal file
528
drupal2wp-toolkit.php
Normal file
@ -0,0 +1,528 @@
|
||||
<?php
|
||||
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
|
||||
WP_CLI::add_hook('after_wp_load', function () {
|
||||
register_taxonomy('page_category', 'page', [
|
||||
'label' => 'Page Categories',
|
||||
'hierarchical' => true,
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
class Drupal_Import_Command extends WP_CLI_Command
|
||||
{
|
||||
/**
|
||||
* Imports Drupal content into WordPress.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* --type=<type>
|
||||
* : (Required) Drupal content type(s) to import or delete. Comma-separated if multiple. Example: --type=story,hidden_content,vidcast,news_article,resources
|
||||
*
|
||||
* [--delete-only]
|
||||
* : Only delete existing imported WordPress pages for the specified type.
|
||||
*
|
||||
*/
|
||||
|
||||
public function __invoke($args, $assoc_args)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// 📡 Connect to Drupal DB
|
||||
$drupal_db = new wpdb(
|
||||
getenv('DRUPAL_DB_USER'),
|
||||
getenv('DRUPAL_DB_PASS'),
|
||||
getenv('DRUPAL_DB_NAME'),
|
||||
getenv('DRUPAL_DB_HOST')
|
||||
);
|
||||
|
||||
if (is_wp_error($drupal_db)) {
|
||||
WP_CLI::error("Could not connect to Drupal DB: " . $drupal_db->get_error_message());
|
||||
}
|
||||
|
||||
// Make --type a required arguement
|
||||
if (! isset($assoc_args['type']) || trim($assoc_args['type']) === '') {
|
||||
WP_CLI::error("You must provide a --type=TYPE argument (e.g., --type=story).");
|
||||
}
|
||||
|
||||
|
||||
$type_arg = explode(',', $assoc_args['type']);
|
||||
$escaped_types = implode("','", array_map('esc_sql', $type_arg));
|
||||
|
||||
// 🔍 Fetch valid Drupal NIDs
|
||||
$nids = $drupal_db->get_col("
|
||||
SELECT nid FROM node_field_data WHERE type IN ('$escaped_types')
|
||||
");
|
||||
|
||||
// 🧹 --delete-only logic
|
||||
if (isset($assoc_args['delete-only'])) {
|
||||
if (empty($nids)) {
|
||||
WP_CLI::warning("No Drupal NIDs found—nothing to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
$posts_to_delete = get_posts([
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'any',
|
||||
'meta_key' => '_drupal_nid',
|
||||
'meta_value' => $nids,
|
||||
'numberposts' => -1,
|
||||
]);
|
||||
|
||||
foreach ($posts_to_delete as $post) {
|
||||
wp_delete_post($post->ID, true);
|
||||
WP_CLI::log("Deleted post ID {$post->ID} (NID: " . get_post_meta($post->ID, '_drupal_nid', true) . ")");
|
||||
}
|
||||
|
||||
WP_CLI::success("Deleted " . count($posts_to_delete) . " Drupal-linked pages.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🧾 Fetch content with user & timestamps
|
||||
$nodes = $drupal_db->get_results("
|
||||
SELECT n.nid, n.title, b.body_value, b.body_summary, n.status, n.created, n.changed, n.uid, n.type,
|
||||
u.name AS author_name, u.mail AS author_email
|
||||
FROM node_field_data n
|
||||
JOIN node__body b ON n.nid = b.entity_id
|
||||
LEFT JOIN users_field_data u ON n.uid = u.uid
|
||||
WHERE n.type IN ('$escaped_types')
|
||||
");
|
||||
|
||||
|
||||
$nids_only = array_map(function ($node) {
|
||||
return intval($node->nid);
|
||||
}, $nodes);
|
||||
|
||||
// Taxonomy
|
||||
$term_data = $drupal_db->get_results("
|
||||
SELECT
|
||||
ti.nid,
|
||||
td.vid AS vocabulary, -- Use vid instead of joining missing table
|
||||
ttd.name AS term_name
|
||||
FROM taxonomy_index ti
|
||||
JOIN taxonomy_term_data td ON ti.tid = td.tid
|
||||
JOIN taxonomy_term_field_data ttd ON ti.tid = ttd.tid
|
||||
WHERE ti.nid IN (" . implode(',', array_map('intval', $nids_only)) . ")
|
||||
");
|
||||
|
||||
// foreach ($nodes as $node) {
|
||||
// WP_CLI::log("Node Title: {$node->title}");
|
||||
// WP_CLI::log("Node ID: {$node->nid}");
|
||||
// WP_CLI::log("Created: " . date('Y-m-d H:i:s', $node->created));
|
||||
// WP_CLI::log("Changed: " . date('Y-m-d H:i:s', $node->changed));
|
||||
// break; // Exit after first iteration
|
||||
// }
|
||||
// exit; // End the script
|
||||
|
||||
$taxonomy_by_nid = [];
|
||||
foreach ($term_data as $row) {
|
||||
$nid = $row->nid;
|
||||
$vocab = sanitize_title($row->vocabulary); // safe WP slug
|
||||
$parent = $row->vocabulary;
|
||||
$child = $row->term_name;
|
||||
|
||||
$taxonomy_by_nid[$nid][$vocab][] = ['parent' => $parent, 'child' => $child];
|
||||
}
|
||||
|
||||
if (empty($nodes)) {
|
||||
WP_CLI::warning("No Drupal content found to import.");
|
||||
return;
|
||||
}
|
||||
|
||||
$imported_nids = [];
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$imported_nids[] = $node->nid;
|
||||
|
||||
// TEST a node
|
||||
// if ( $node->nid == 498 ) {
|
||||
// WP_CLI::log( "Raw body for nid 498:\n" . $node->body_value );
|
||||
// }
|
||||
|
||||
// 🗑 Remove any existing post with this NID
|
||||
$existing = get_posts([
|
||||
'meta_key' => '_drupal_nid',
|
||||
'meta_value' => $node->nid,
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'any',
|
||||
'numberposts' => -1,
|
||||
]);
|
||||
foreach ($existing as $post) {
|
||||
wp_delete_post($post->ID, true);
|
||||
}
|
||||
|
||||
// 👤 Ensure author exists
|
||||
$user_login = sanitize_user($node->author_name ?? 'drupal_user');
|
||||
$author_id = username_exists($user_login);
|
||||
if (! $author_id) {
|
||||
$author_id = wp_create_user($user_login, wp_generate_password(), $node->author_email ?: "{$user_login}@example.com");
|
||||
}
|
||||
|
||||
// 🪄 Convert to Gutenberg blocks
|
||||
$block_content = $this->convert_to_blocks($node->body_value);
|
||||
$wp_status = $node->status == 1 ? 'publish' : 'draft';
|
||||
$post_date = date('Y-m-d H:i:s', $node->created);
|
||||
$post_modified = date('Y-m-d H:i:s', $node->changed);
|
||||
|
||||
|
||||
// WP_CLI::log("{$node->title} POST DATE: {$post_date} POST MODIFED: {$post_modified}");
|
||||
// exit;
|
||||
|
||||
// if ($node->nid == 1005) {
|
||||
// WP_CLI::log("Raw body for nid 498:\n" . $block_content);
|
||||
// }
|
||||
|
||||
add_filter('wp_insert_post_data', [$this, 'alter_post_modification_time'], 99, 2);
|
||||
$post_id = wp_insert_post([
|
||||
'post_title' => $node->title,
|
||||
'post_content' => $block_content,
|
||||
'post_status' => $wp_status,
|
||||
'post_type' => 'page',
|
||||
'post_author' => $author_id,
|
||||
'post_date' => $post_date,
|
||||
'post_date_gmt' => get_gmt_from_date($post_date),
|
||||
'post_modified' => $post_modified,
|
||||
'post_modified_gmt' => get_gmt_from_date($post_modified),
|
||||
]);
|
||||
remove_filter('wp_insert_post_data', [$this, 'alter_post_modification_time'], 99, 2);
|
||||
|
||||
|
||||
// 🏷 Assign tags based on each --type value passed to the script
|
||||
$term_name = $node->type;
|
||||
$term_ids = [];
|
||||
|
||||
// 🌱 Create the term if missing
|
||||
$term_info = term_exists($term_name, 'page_category');
|
||||
|
||||
// WP_CLI::log("Vocabulary {$term_name}: " . json_encode($term_info));
|
||||
// exit;
|
||||
|
||||
if (! is_wp_error($term_info)) {
|
||||
$term_id = is_array($term_info) ? $term_info['term_id'] : $term_info;
|
||||
$term_ids[] = $term_id; // 👈 Add node->type term to final list
|
||||
WP_CLI::log("✅ Assigned term '{$term_name}' (ID: {$term_id}) to post {$post_id}");
|
||||
}
|
||||
|
||||
// Gather parent + child terms
|
||||
if (isset($taxonomy_by_nid[$node->nid])) {
|
||||
foreach ($taxonomy_by_nid[$node->nid] as $vocab_slug => $terms) {
|
||||
foreach ($terms as $pair) {
|
||||
$parent_name = $pair['parent'];
|
||||
$child_name = $pair['child'];
|
||||
|
||||
$parent = term_exists($parent_name, 'page_category');
|
||||
if (! $parent) {
|
||||
$parent = wp_insert_term($parent_name, 'page_category');
|
||||
}
|
||||
$parent_id = is_array($parent) ? $parent['term_id'] : $parent;
|
||||
|
||||
$child = term_exists($child_name, 'page_category');
|
||||
if (! $child) {
|
||||
$child = wp_insert_term($child_name, 'page_category', ['parent' => $parent_id]);
|
||||
}
|
||||
$child_id = is_array($child) ? $child['term_id'] : $child;
|
||||
|
||||
$term_ids[] = $parent_id;
|
||||
$term_ids[] = $child_id;
|
||||
|
||||
WP_CLI::log("✔ Assigned '{$child_name}' under '{$parent_name}' to post {$post_id} in page_category");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Assign all terms in one go (prevent overwriting)
|
||||
wp_set_post_terms($post_id, array_unique($term_ids), 'page_category', false);
|
||||
|
||||
|
||||
|
||||
// 🧩 Save original Drupal NID
|
||||
update_post_meta($post_id, '_drupal_nid', $node->nid);
|
||||
|
||||
WP_CLI::log("Imported: {$node->title} (NID: {$node->nid}, Author: {$user_login}, Status: {$wp_status})");
|
||||
}
|
||||
|
||||
// 🧽 Unpublish orphans
|
||||
$all_synced = get_posts([
|
||||
'post_type' => 'page',
|
||||
'meta_key' => '_drupal_nid',
|
||||
'post_status' => ['publish', 'draft'],
|
||||
'numberposts' => -1,
|
||||
]);
|
||||
|
||||
|
||||
// foreach ($all_synced as $page) {
|
||||
// $nid = get_post_meta($page->ID, '_drupal_nid', true);
|
||||
// if (! in_array($nid, $imported_nids)) {
|
||||
// wp_update_post(['ID' => $page->ID, 'post_status' => 'draft']);
|
||||
// // WP_CLI::log("Unpublished orphaned page ID {$page->ID} (Drupal NID {$nid})");
|
||||
// }
|
||||
// }
|
||||
|
||||
foreach ($all_synced as $page) {
|
||||
$nid = get_post_meta($page->ID, '_drupal_nid', true);
|
||||
|
||||
// Skip if the nid was imported this run
|
||||
if (in_array($nid, $imported_nids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get assigned terms
|
||||
$assigned_terms = wp_get_post_terms($page->ID, 'page_category', ['fields' => 'names']);
|
||||
|
||||
// Check if any assigned term matches current types passed in --type
|
||||
$matched_vocab = false;
|
||||
foreach ($assigned_terms as $term_name) {
|
||||
if (in_array($term_name, $type_arg)) {
|
||||
$matched_vocab = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only unpublish if the post matches current vocab context
|
||||
if ($matched_vocab) {
|
||||
wp_update_post(['ID' => $page->ID, 'post_status' => 'draft']);
|
||||
WP_CLI::log("📉 Unpublished orphaned page ID {$page->ID} with term '{$term_name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function alter_post_modification_time($data, $postarr)
|
||||
{
|
||||
if (!empty($postarr['post_modified']) && !empty($postarr['post_modified_gmt'])) {
|
||||
$data['post_modified'] = $postarr['post_modified'];
|
||||
$data['post_modified_gmt'] = $postarr['post_modified_gmt'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
private function convert_to_blocks($html)
|
||||
{
|
||||
$doc = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
libxml_clear_errors();
|
||||
|
||||
$body = $doc->getElementsByTagName('body')->item(0);
|
||||
$output = '';
|
||||
|
||||
foreach ($body->childNodes as $node) {
|
||||
$output .= $this->convert_node_to_block($node);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function convert_node_to_block($node)
|
||||
{
|
||||
if ($node instanceof DOMText) {
|
||||
$text = trim($node->wholeText);
|
||||
return $text !== '' ? "<!-- wp:paragraph -->\n<p>{$text}</p>\n<!-- /wp:paragraph -->\n\n" : '';
|
||||
}
|
||||
|
||||
if (!($node instanceof DOMElement)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tag = strtolower($node->tagName);
|
||||
|
||||
// 🖼 Image handler
|
||||
if ($tag === 'img') {
|
||||
$src = $node->getAttribute('src');
|
||||
$alt = $node->getAttribute('alt') ?: '';
|
||||
$html = '<img src="' . esc_url($src) . '" alt="' . esc_attr($alt) . '" />';
|
||||
return "<!-- wp:image -->\n<figure class='wp-block-image'>{$html}</figure>\n<!-- /wp:image -->\n\n";
|
||||
}
|
||||
|
||||
// 🔗 Image wrapped in <a>
|
||||
if ($tag === 'a' && $node->getElementsByTagName('img')->length > 0) {
|
||||
$img = $node->getElementsByTagName('img')->item(0);
|
||||
$src = $img->getAttribute('src');
|
||||
$alt = $img->getAttribute('alt') ?: '';
|
||||
$href = $node->getAttribute('href');
|
||||
$html = '<a href="' . esc_url($href) . '"><img src="' . esc_url($src) . '" alt="' . esc_attr($alt) . '" /></a>';
|
||||
return "<!-- wp:image -->\n<figure class='wp-block-image'>{$html}</figure>\n<!-- /wp:image -->\n\n";
|
||||
}
|
||||
|
||||
// 🧱 Recognize div and process its children recursively
|
||||
if ($tag === 'div') {
|
||||
$content = '';
|
||||
foreach ($node->childNodes as $child) {
|
||||
$content .= $this->convert_node_to_block($child);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ($tag === 'table') {
|
||||
$innerHTML = $node->ownerDocument->saveHTML($node);
|
||||
|
||||
// Optional: sanitize or restructure table HTML here
|
||||
|
||||
return "<!-- wp:table -->\n{$innerHTML}\n<!-- /wp:table -->\n\n";
|
||||
}
|
||||
|
||||
|
||||
// 🧾 Generic block mapping
|
||||
$innerHTML = $node->ownerDocument->saveHTML($node);
|
||||
switch ($tag) {
|
||||
case 'p':
|
||||
return "<!-- wp:paragraph -->\n{$innerHTML}\n<!-- /wp:paragraph -->\n\n";
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
return "<!-- wp:heading -->\n{$innerHTML}\n<!-- /wp:heading -->\n\n";
|
||||
case 'blockquote':
|
||||
return "<!-- wp:quote -->\n{$innerHTML}\n<!-- /wp:quote -->\n\n";
|
||||
case 'ol':
|
||||
return "<!-- wp:list {\"ordered\":true} -->\n{$innerHTML}\n<!-- /wp:list -->\n\n";
|
||||
case 'ul':
|
||||
return "<!-- wp:list -->\n{$innerHTML}\n<!-- /wp:list -->\n\n";
|
||||
default:
|
||||
return "<!-- wp:paragraph -->\n{$innerHTML}\n<!-- /wp:paragraph -->\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// private function convert_to_blocks($html)
|
||||
// {
|
||||
// $doc = new DOMDocument();
|
||||
// libxml_use_internal_errors(true);
|
||||
// $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
// libxml_clear_errors();
|
||||
|
||||
// $body = $doc->getElementsByTagName('body')->item(0);
|
||||
// $output = '';
|
||||
|
||||
// // foreach ($body->childNodes as $node) {
|
||||
// // if (!($node instanceof DOMElement)) {
|
||||
// // continue;
|
||||
// // }
|
||||
|
||||
// // // Check if this node (or its children) contain an <img>
|
||||
// // $img_tag = $node->getElementsByTagName('img')->item(0);
|
||||
|
||||
// // if ($img_tag) {
|
||||
// // // Extract src and alt
|
||||
// // $src = $img_tag->getAttribute('src');
|
||||
// // $alt = $img_tag->getAttribute('alt') ?: '';
|
||||
|
||||
// // // Check for a wrapping <a>
|
||||
// // $link = $node->getElementsByTagName('a')->item(0);
|
||||
// // $image_html = '<img src="' . esc_url($src) . '" alt="' . esc_attr($alt) . '" />';
|
||||
|
||||
// // if ($link) {
|
||||
// // $href = $link->getAttribute('href');
|
||||
// // $image_html = '<a href="' . esc_url($href) . '">' . $image_html . '</a>';
|
||||
// // }
|
||||
|
||||
// // // Wrap in figure block
|
||||
// // $figure = '<figure class="wp-block-image">' . $image_html . '</figure>';
|
||||
|
||||
// // $output .= "<!-- wp:image -->\n" . $figure . "\n<!-- /wp:image -->\n\n";
|
||||
// // continue;
|
||||
// // }
|
||||
|
||||
// // // Render other HTML blocks
|
||||
// // $innerHTML = $doc->saveHTML($node);
|
||||
|
||||
// // // Fix insecure YouTube embeds
|
||||
// // $innerHTML = preg_replace(
|
||||
// // '#src=["\']http://(www\.)?youtube\.com#i',
|
||||
// // 'src="https://www.youtube.com',
|
||||
// // $innerHTML
|
||||
// // );
|
||||
|
||||
// // switch ($node->nodeName) {
|
||||
// // case 'p':
|
||||
// // $output .= "<!-- wp:paragraph -->\n{$innerHTML}\n<!-- /wp:paragraph -->\n\n";
|
||||
// // break;
|
||||
// // case 'ol':
|
||||
// // $output .= "<!-- wp:list {\"ordered\":true} -->\n{$innerHTML}\n<!-- /wp:list -->\n\n";
|
||||
// // break;
|
||||
// // case 'ul':
|
||||
// // $output .= "<!-- wp:list -->\n{$innerHTML}\n<!-- /wp:list -->\n\n";
|
||||
// // break;
|
||||
// // case 'h2':
|
||||
// // case 'h3':
|
||||
// // $output .= "<!-- wp:heading -->\n{$innerHTML}\n<!-- /wp:heading -->\n\n";
|
||||
// // break;
|
||||
// // case 'blockquote':
|
||||
// // $output .= "<!-- wp:quote -->\n{$innerHTML}\n<!-- /wp:quote -->\n\n";
|
||||
// // break;
|
||||
// // default:
|
||||
// // $output .= "<!-- wp:paragraph -->\n{$innerHTML}\n<!-- /wp:paragraph -->\n\n";
|
||||
// // break;
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// foreach ($body->childNodes as $node) {
|
||||
// if (!($node instanceof DOMElement)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // 🖼 Render all images first
|
||||
// $img_tags = $node->getElementsByTagName('img');
|
||||
// if ($img_tags->length > 0) {
|
||||
// foreach ($img_tags as $img_tag) {
|
||||
// $src = $img_tag->getAttribute('src');
|
||||
// $alt = $img_tag->getAttribute('alt') ?: '';
|
||||
|
||||
// // Check for wrapping <a>
|
||||
// $link = $img_tag->parentNode instanceof DOMElement && $img_tag->parentNode->nodeName === 'a'
|
||||
// ? $img_tag->parentNode
|
||||
// : null;
|
||||
|
||||
// $image_html = '<img src="' . esc_url($src) . '" alt="' . esc_attr($alt) . '" />';
|
||||
// if ($link) {
|
||||
// $href = $link->getAttribute('href');
|
||||
// $image_html = '<a href="' . esc_url($href) . '">' . $image_html . '</a>';
|
||||
// }
|
||||
|
||||
// $figure = '<figure class="wp-block-image">' . $image_html . '</figure>';
|
||||
// $output .= "<!-- wp:image -->\n" . $figure . "\n<!-- /wp:image -->\n\n";
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 💡 Continue with other content
|
||||
// $innerHTML = $doc->saveHTML($node);
|
||||
|
||||
// // Fix insecure embeds
|
||||
// $innerHTML = preg_replace(
|
||||
// '#src=["\']http://(www\.)?youtube\.com#i',
|
||||
// 'src="https://www.youtube.com',
|
||||
// $innerHTML
|
||||
// );
|
||||
|
||||
// switch ($node->nodeName) {
|
||||
// case 'p':
|
||||
// $output .= "<!-- wp:paragraph -->\n{$innerHTML}\n<!-- /wp:paragraph -->\n\n";
|
||||
// break;
|
||||
// case 'ol':
|
||||
// $output .= "<!-- wp:list {\"ordered\":true} -->\n{$innerHTML}\n<!-- /wp:list -->\n\n";
|
||||
// break;
|
||||
// case 'ul':
|
||||
// $output .= "<!-- wp:list -->\n{$innerHTML}\n<!-- /wp:list -->\n\n";
|
||||
// break;
|
||||
// case 'h2':
|
||||
// case 'h3':
|
||||
// $output .= "<!-- wp:heading -->\n{$innerHTML}\n<!-- /wp:heading -->\n\n";
|
||||
// break;
|
||||
// case 'blockquote':
|
||||
// $output .= "<!-- wp:quote -->\n{$innerHTML}\n<!-- /wp:quote -->\n\n";
|
||||
// break;
|
||||
// default:
|
||||
// $output .= "<!-- wp:paragraph -->\n{$innerHTML}\n<!-- /wp:paragraph -->\n\n";
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// return $output;
|
||||
// }
|
||||
}
|
||||
|
||||
WP_CLI::add_command('drupal-import', 'Drupal_Import_Command');
|
||||
}
|
183
page-taxonomy.php
Normal file
183
page-taxonomy.php
Normal file
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Plugin Name: Page Categories Taxonomy
|
||||
*/
|
||||
|
||||
function add_page_categories_taxonomy()
|
||||
{
|
||||
register_taxonomy('page_category', ['page', 'post'], [
|
||||
'label' => 'Page Categories',
|
||||
'hierarchical' => true,
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => true,
|
||||
]);
|
||||
}
|
||||
add_action('init', 'add_page_categories_taxonomy');
|
||||
|
||||
// For WP-CLI (where 'init' may not trigger reliably)
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
WP_CLI::add_hook('after_wp_load', function () {
|
||||
register_page_category_taxonomy();
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new column to the Pages list
|
||||
add_filter('manage_page_posts_columns', 'add_page_category_column');
|
||||
function add_page_category_column($columns)
|
||||
{
|
||||
$new_columns = array();
|
||||
foreach ($columns as $key => $value) {
|
||||
$new_columns[$key] = $value;
|
||||
if ('author' === $key) {
|
||||
$new_columns['page_category'] = 'Categories';
|
||||
}
|
||||
}
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
|
||||
// Populate the custom column with taxonomy terms
|
||||
add_action('manage_page_posts_custom_column', 'show_page_category_column', 10, 2);
|
||||
function show_page_category_column($column, $post_id)
|
||||
{
|
||||
if ('page_category' === $column) {
|
||||
$terms = get_the_terms($post_id, 'page_category');
|
||||
if (!empty($terms) && !is_wp_error($terms)) {
|
||||
$links = array();
|
||||
foreach ($terms as $term) {
|
||||
$url = admin_url('edit.php?post_type=page&page_category=' . $term->slug);
|
||||
$links[] = '<a href="' . esc_url($url) . '">' . esc_html($term->name) . '</a>';
|
||||
}
|
||||
echo implode(', ', $links);
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter taxonomy
|
||||
add_action('restrict_manage_posts', 'filter_page_by_top_level_category');
|
||||
function filter_page_by_top_level_category()
|
||||
{
|
||||
global $typenow;
|
||||
|
||||
if ($typenow !== 'page') return;
|
||||
|
||||
$taxonomy = 'page_category';
|
||||
$terms = get_terms([
|
||||
'taxonomy' => $taxonomy,
|
||||
'parent' => 0, // 👈 Only top-level categories
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
if (!empty($terms)) {
|
||||
echo '<select name="' . $taxonomy . '" style="max-width:160px">';
|
||||
echo '<option value="">Filter by Category</option>';
|
||||
foreach ($terms as $term) {
|
||||
$selected = isset($_GET[$taxonomy]) && $_GET[$taxonomy] === $term->slug ? ' selected="selected"' : '';
|
||||
echo '<option value="' . esc_attr($term->slug) . '"' . $selected . '>' . esc_html($term->name) . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
}
|
||||
}
|
||||
|
||||
add_filter('parse_query', 'apply_top_level_category_filter');
|
||||
function apply_top_level_category_filter($query)
|
||||
{
|
||||
global $pagenow;
|
||||
|
||||
if (
|
||||
$pagenow === 'edit.php' &&
|
||||
$query->is_main_query() &&
|
||||
isset($_GET['page_category']) &&
|
||||
!empty($_GET['page_category']) &&
|
||||
$query->get('post_type') === 'page'
|
||||
) {
|
||||
$taxonomy = 'page_category';
|
||||
$term_slug = sanitize_text_field($_GET['page_category']);
|
||||
|
||||
// Get full term object by slug
|
||||
$term = get_term_by('slug', $term_slug, $taxonomy);
|
||||
|
||||
if ($term && !is_wp_error($term)) {
|
||||
$query->set('tax_query', [[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => [$term->term_id],
|
||||
'include_children' => true, // 👈 Includes all subcategories too
|
||||
'operator' => 'IN' // Matches pages with this term, even with others
|
||||
]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 1️⃣ Add the Last Modified column
|
||||
add_filter('manage_page_posts_columns', function($columns) {
|
||||
$columns['modified_date'] = 'Last Modified';
|
||||
return $columns;
|
||||
});
|
||||
|
||||
// 2️⃣ Display the modified time
|
||||
add_action('manage_page_posts_custom_column', function($column, $post_id) {
|
||||
if ($column === 'modified_date') {
|
||||
echo get_post_modified_time('F j, Y g:i a', false, $post_id);
|
||||
}
|
||||
}, 10, 2);
|
||||
|
||||
// 3️⃣ Make it sortable
|
||||
add_filter('manage_edit-page_sortable_columns', function($columns) {
|
||||
$columns['modified_date'] = 'modified_date';
|
||||
return $columns;
|
||||
});
|
||||
|
||||
// 4️⃣ Handle the sorting logic
|
||||
add_action('pre_get_posts', function($query) {
|
||||
if (!is_admin() || !$query->is_main_query()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($query->get('orderby') === 'modified_date') {
|
||||
$query->set('orderby', 'modified');
|
||||
}
|
||||
});
|
||||
|
||||
// Taxonomy based templates
|
||||
add_action('admin_init', 'generate_page_category_templates');
|
||||
function generate_page_category_templates()
|
||||
{
|
||||
// Define plugin directory and template folder
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$template_dir = $plugin_dir . 'templates/';
|
||||
|
||||
if (!is_dir($template_dir)) {
|
||||
mkdir($template_dir); // Create if not exists
|
||||
}
|
||||
|
||||
// Get all top-level terms
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'page_category',
|
||||
'parent' => 0,
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$slug = $term->slug;
|
||||
$filename = "taxonomy-page_category-{$slug}.php";
|
||||
$filepath = $template_dir . $filename;
|
||||
|
||||
if (!file_exists($filepath)) {
|
||||
// Create a basic scaffold file
|
||||
file_put_contents($filepath, "<?php\n// Template for '{$term->name}'\nget_header();\n?>
|
||||
<h1>Category: <?php echo esc_html('{$term->name}'); ?></h1>
|
||||
<?php if (have_posts()) : while (have_posts()) : the_post(); ?>
|
||||
<h2><a href=\"<?php the_permalink(); ?>\"><?php the_title(); ?></a></h2>
|
||||
<p><?php the_excerpt(); ?></p>
|
||||
<?php endwhile; endif; ?>
|
||||
<?php get_footer(); ?>
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user