I am creating a web-site on wordpress platform where I want to be able to post my own book texts. So what I want is to have a some kind of hierarchy where I would add a post and then add children to it (chapters). I found this:
register_post_type( 'post', array(
'labels' => array(
'name_admin_bar' => _x( 'Post', 'add new on admin bar' ),
),
'public' => true,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ),
) );
and tried to make the 'hierarchical"=>true
, but there was no effect. Can anyone help?
Here is my workaround. This achieves exactly what you want, to be able to set post parents for the builtin post type post. You can achieve this by adding an action to the registred_post_type
action hook. Just add this to your theme's functions.php.
add_action('registered_post_type', 'igy2411_make_posts_hierarchical', 10, 2 );
// Runs after each post type is registered
function igy2411_make_posts_hierarchical($post_type, $pto){
// Return, if not post type posts
if ($post_type != 'post') return;
// access $wp_post_types global variable
global $wp_post_types;
// Set post type "post" to be hierarchical
$wp_post_types['post']->hierarchical = 1;
// Add page attributes to post backend
// This adds the box to set up parent and menu order on edit posts.
add_post_type_support( 'post', 'page-attributes' );
}
There can be dozens of reasons why making posts hierarchical can be helpful. My use case is that the client wanted to structure their (already existing) posts into issues, where child posts are articles of one issue (parent posts).
This is easily achieved by limiting the query to only show posts that have no parents, using.
'post_parent' => 0,
in your query $args.
WP 4.9.*
Workaround above makes it crazy with Friendly URLs.
My solution to add hierarchy to any existent post type:
add_filter( 'register_post_type_args', 'add_hierarchy_support', 10, 2 );
function add_hierarchy_support( $args, $post_type ){
if ($post_type === 'post') { // <-- enter desired post type here
$args['hierarchical'] = true;
$args['supports'] = array_merge($args['supports'], array ('page-attributes') );
}
return $args;
}
Resave wp settings at /wp-admin/options-permalink.php
Due to comments provoking new use-cases and issues, I rewrote this code and I am using it on my own sites [tested in 5.8.2]. I have provided a gist for it. You can include it in your functions.php, or make it into a plugin.
https://gist.github.com/amurrell/00d29a86fc1a773274bf049ef545b29f
🎉 This new update is leveraging SQL (fast!) to resolve the slug and post id to determine the permalink & routing. It produces the exact matching post id, even if you are using the same post_name
for different post descendants. It's really fast & reliable!
In the gist, the most interesting function is get_post_from_uri($uri)
👇 Let's see how the query works. This may not be a perfect 1-1 of the code, because I made it dynamic, but the concept is there:
I have the following posts:
See it in SQL:
mysql> select id, post_name, post_parent from wp_posts where post_type = 'post' and id in (1811, 1808, 1345, 1395, 547, 518);
+------+-------------------------+-------------+
| id | post_name | post_parent |
+------+-------------------------+-------------+
| 518 | procurement | 0 |
| 547 | climate | 0 |
| 1345 | city-sales-cycle | 518 |
| 1395 | alliance-for-innovation | 0 |
| 1808 | climate | 1395 |
| 1811 | climate | 1345 |
+------+-------------------------+-------------+
alliance-for-innovation/climate
The full query...
mysql> select * from
-> (select TRIM(BOTH '/' FROM concat(
-> IFNULL(p3_slug,''),
-> '/',
-> IFNULL(p2_slug,''),
-> '/',
-> p1_slug
-> )
-> ) as slug,
-> id
-> from (
-> select d2.*, p3.post_name as p3_slug, p3.post_parent as p3_parent from (
-> select d1.*, p2.post_name as p2_slug, p2.post_parent as p2_parent from (
-> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate'
-> ) as d1
-> left join wp_posts p2 on p2.id = d1.p1_parent
-> ) as d2
-> left join wp_posts p3 on p3.id = d2.p2_parent) as d3
->
-> ) as all_slugs
-> where slug = 'alliance-for-innovation/climate';
+---------------------------------+------+
| slug | id |
+---------------------------------+------+
| alliance-for-innovation/climate | 1808 |
+---------------------------------+------+
1 row in set (0.01 sec)
I now have both the post ID
and the slug
, or permalink, I should be using!
It is worth noting I went to the level of p3, which is one extra level than the URL would require (being two parts). This is to prevent something like alliance-for-innovation/climate/something
from matching.
There's an inside query that looks for the last part of the URL, aka basename. In this case it would be climate
.
mysql> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate';
+------+---------+-----------+
| id | p1_slug | p1_parent |
+------+---------+-----------+
| 547 | climate | 0 |
| 1808 | climate | 1395 |
| 1811 | climate | 1345 |
+------+---------+-----------+
Programmatically, we keep adding abstractions around the query that's directly related to the number of /
in the url, so that we can find more information about the post_parent's slug.
mysql> select d1.*, p2.post_name as p2_slug, p2.post_parent as p2_parent from (
-> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate'
-> ) as d1
-> left join wp_posts p2 on p2.id = d1.p1_parent;
+------+---------+-----------+-------------------------+-----------+
| id | p1_slug | p1_parent | p2_slug | p2_parent |
+------+---------+-----------+-------------------------+-----------+
| 547 | climate | 0 | NULL | NULL |
| 1808 | climate | 1395 | alliance-for-innovation | 0 |
| 1811 | climate | 1345 | city-sales-cycle | 518 |
+------+---------+-----------+-------------------------+-----------+
After we abstracted enough times, we can then select concats as slug like: p1_slug + '/' + p2_slug
mysql> select TRIM(BOTH '/' FROM concat(
-> IFNULL(p3_slug,''),
-> '/',
-> IFNULL(p2_slug,''),
-> '/',
-> p1_slug
-> )
-> ) as slug,
-> id
-> from (
-> select d2.*, p3.post_name as p3_slug, p3.post_parent as p3_parent from (
-> select d1.*, p2.post_name as p2_slug, p2.post_parent as p2_parent from (
-> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate'
-> ) as d1
-> left join wp_posts p2 on p2.id = d1.p1_parent
-> ) as d2
-> left join wp_posts p3 on p3.id = d2.p2_parent) as d3
->
-> ;
+--------------------------------------+------+
| slug | id |
+--------------------------------------+------+
| climate | 547 |
| alliance-for-innovation/climate | 1808 |
| procurement/city-sales-cycle/climate | 1811 |
+--------------------------------------+------+
The last step is to add a where
for the original url: alliance-for-innovation/climate
. And that's what you see in the full query example we first examined!
# climate
+---------+-----+
| slug | id |
+---------+-----+
| climate | 547 |
+---------+-----+
# procurement/city-sales-cycle/climate
+--------------------------------------+------+
| slug | id |
+--------------------------------------+------+
| procurement/city-sales-cycle/climate | 1811 |
+--------------------------------------+------+
Escape the climate
, or basename of the URL that we use in the query, because this is technically user-inputted (via url)
$wpdb->_real_escape($basename));
We use PHP arrays, loops, etc to build a string that will be the query so that we do not have to use PHP for logic about the data itself.
This is a snippet showing the dynamic abstractions - eg. how many p1_slug, p2_slug, p3_slug to grab.
// We will do 1 more depth level than we need to confirm the slug would not lazy match
// This for loop builds inside out.
for ($c = 1; $c < $depth + 2; $c++) {
$d = $c;
$p = $c + 1;
$pre = "select d${d}.*, p${p}.post_name as p${p}_slug, p${p}.post_parent as p${p}_parent from (";
$suf = ") as d${d} left join $wpdb->posts p${p} on p${p}.id = d${d}.p${c}_parent";
$sql = $pre . $sql . $suf;
$concats[] = sprintf("IFNULL(p${p}_slug,'')");
}
I came here looking to achieve:
I was able to use the accepted answer to accomplish 1 & 2, but not 3.
Note: to fully get 2 to work, you need to specify the post_type in the template comments of your page template like this:
<?php
/*
Template Name: Your Post Template Name
Template Post Type: post
*/
For 3, I found a plugin that ruined my post_type pages, and it was a lot of pretty awful, unmaintained code.
So I wrote a solution to accomplish all this, borrowing from this answer:
(Tested with 4.9.8)
<?php
add_action('registered_post_type', 'make_posts_hierarchical', 10, 2 );
// Runs after each post type is registered
function make_posts_hierarchical($post_type, $pto){
// Return, if not post type posts
if ($post_type != 'post') return;
// access $wp_post_types global variable
global $wp_post_types;
// Set post type "post" to be hierarchical
$wp_post_types['post']->hierarchical = 1;
// Add page attributes to post backend
// This adds the box to set up parent and menu order on edit posts.
add_post_type_support( 'post', 'page-attributes' );
}
/**
* Get parent post slug
*
* Helpful function to get the post name of a posts parent
*/
function get_parent_post_slug($post) {
if (!is_object($post) || !$post->post_parent) {
return false;
}
return get_post($post->post_parent)->post_name;
}
/**
*
* Edit View of Permalink
*
* This affects editing permalinks, and $permalink is an array [template, replacement]
* where replacement is the post_name and template has %postname% in it.
*
**/
add_filter('get_sample_permalink', function($permalink, $post_id, $title, $name, $post) {
if ($post->post_type != 'post' || !$post->post_parent) {
return $permalink;
}
// Deconstruct the permalink parts
$template_permalink = current($permalink);
$replacement_permalink = next($permalink);
// Find string
$postname_string = '/%postname%/';
// Get parent post
$parent_slug = get_parent_post_slug($post);
$altered_template_with_parent_slug = '/' . $parent_slug . $postname_string;
$new_template = str_replace($postname_string, $altered_template_with_parent_slug, $template_permalink);
$new_permalink = [$new_template, $replacement_permalink];
return $new_permalink;
}, 99, 5);
/**
* Alter the link to the post
*
* This affects get_permalink, the_permalink etc.
* This will be the target of the edit permalink link too.
*
* Note: only fires on "post" post types.
*/
add_filter('post_link', function($post_link, $post, $leavename){
if ($post->post_type != 'post' || !$post->post_parent) {
return $post_link;
}
$parent_slug = get_parent_post_slug($post);
$new_post_link = str_replace($post->post_name, $parent_slug . '/' . $post->post_name, $post_link);
return $new_post_link;
}, 99, 3);
/**
* Before getting posts
*
* Has to do with routing... adjusts the main query settings
*
*/
add_action('pre_get_posts', function($query){
global $wpdb, $wp_query;
$original_query = $query;
$uri = $_SERVER['REQUEST_URI'];
// Do not do this post check all the time
if ( $query->is_main_query() && !is_admin()) {
// get the post_name
$basename = basename($uri);
// find out if we have a post that matches this post_name
$test_query = sprintf("select * from $wpdb->posts where post_type = '%s' and post_name = '%s';", 'post', $basename);
$result = $wpdb->get_results($test_query);
// if no match, return default query, or if there's no parent post, this is not necessary
if (!($post = current($result)) || !$post->post_parent) {
return $original_query;
}
// get the parent slug
$parent_slug = get_parent_post_slug($post);
// concat the parent slug with the post_name to get most of the url
$hierarchal_slug = $parent_slug . '/' . $post->post_name;
// if the concat of parent-slug/post-name is not in the uri, this is not the right post.
if (!stristr($uri, $hierarchal_slug)) {
return $original_query;
}
// pretty high confidence that we need to override the query.
$query->query_vars['post_type'] = ['post'];
$query->is_home = false;
$query->is_page = true;
$query->is_single = true;
$query->queried_object_id = $post->ID;
$query->set('page_id', $post->ID);
return $query;
}
}, 1);
You can save this to a file custom-posts-hierarchy.php
and include it in your functions.php file in your theme, or you can add to the top:
/*
Plugin Name: Custom Posts Hierarchy
Plugin URI:
Description: Add page attributes to posts and support hiearchichal
Author: Angela Murrell
Version:
Author URI:
*/
And drop it into your plugins folder. Good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With