Skip to content

Commit 2eec5f2

Browse files
committed
feat: enhance KB permalink handling with custom slug sanitization and root-level support
1 parent e948aa7 commit 2eec5f2

File tree

1 file changed

+183
-6
lines changed

1 file changed

+183
-6
lines changed

includes/class-cpt.php

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,29 @@ public function __construct() {
3737
* @since 2.3.0
3838
*/
3939
public static function register_post_type() {
40-
$slug = sanitize_title( \wzkb_get_option( 'kb_slug', 'knowledgebase' ) );
40+
// Sanitize KB slug (remove placeholders, preserve slashes).
41+
$slug = self::sanitize_slug( \wzkb_get_option( 'kb_slug', 'knowledgebase' ) );
42+
$article_structure = \wzkb_get_option( 'article_permalink', '' );
43+
44+
// If article permalink is set to just %postname%, don't use a slug prefix.
45+
// This allows KB articles to use the same permalink structure as regular posts.
46+
$use_slug_prefix = ! ( '%postname%' === $article_structure );
47+
4148
$archives = defined( 'WZKB_DISABLE_ARCHIVE' ) && WZKB_DISABLE_ARCHIVE ? false : $slug;
42-
$rewrite = defined( 'WZKB_DISABLE_REWRITE' ) && WZKB_DISABLE_REWRITE ? false : array(
43-
'slug' => $slug,
49+
50+
$rewrite = defined( 'WZKB_DISABLE_REWRITE' ) && WZKB_DISABLE_REWRITE ? false : array(
51+
'slug' => $use_slug_prefix ? $slug : 'wz_knowledgebase',
4452
'with_front' => false,
4553
'feeds' => \wzkb_get_option( 'disable_kb_feed' ) ? false : true,
4654
);
4755

56+
// If not using slug prefix, we'll add custom rewrite rules and filter permalinks.
57+
// Only if Pro Custom Permalinks class is not active (Pro handles this itself).
58+
if ( ! $use_slug_prefix && ! class_exists( 'WebberZone\Knowledge_Base\Pro\Custom_Permalinks' ) ) {
59+
add_action( 'init', array( __CLASS__, 'add_root_level_rewrite_rules' ), 99 );
60+
add_filter( 'post_type_link', array( __CLASS__, 'filter_root_level_permalink' ), 10, 2 );
61+
}
62+
4863
$ptlabels = array(
4964
'name' => _x( 'Knowledge Base', 'Post Type General Name', 'knowledgebase' ),
5065
'singular_name' => _x( 'Knowledge Base', 'Post Type Singular Name', 'knowledgebase' ),
@@ -110,6 +125,29 @@ public static function register_post_type() {
110125
register_post_type( 'wz_knowledgebase', $ptargs );
111126
}
112127

128+
/**
129+
* Sanitize slug while preserving slashes and optionally removing placeholders.
130+
*
131+
* @since 3.0.0
132+
*
133+
* @param string $slug The slug to sanitize.
134+
* @param bool $remove_placeholders Whether to remove placeholders. Default true.
135+
* @return string Sanitized slug.
136+
*/
137+
private static function sanitize_slug( string $slug, bool $remove_placeholders = true ): string {
138+
// Remove any placeholders (e.g., %product_name%, %section_name%) if requested.
139+
if ( $remove_placeholders ) {
140+
$slug = preg_replace( '/%[^%]+%/', '', $slug );
141+
}
142+
143+
// Split by slash, sanitize each part, then rejoin.
144+
$parts = explode( '/', $slug );
145+
$parts = array_map( 'sanitize_title', $parts );
146+
$parts = array_filter( $parts ); // Remove empty parts.
147+
148+
return implode( '/', $parts );
149+
}
150+
113151
/**
114152
* Get base arguments for taxonomies.
115153
*
@@ -198,9 +236,10 @@ private static function get_taxonomy_labels( string $singular, string $plural ):
198236
*/
199237
public static function register_taxonomies() {
200238
// Get taxonomy slugs from options.
201-
$catslug = sanitize_title( \wzkb_get_option( 'category_slug', 'kb/section' ) );
202-
$tagslug = sanitize_title( \wzkb_get_option( 'tag_slug', 'kb/tags' ) );
203-
$productslug = sanitize_title( \wzkb_get_option( 'product_slug', 'kb/products' ) );
239+
// Use custom sanitization that preserves slashes and removes placeholders.
240+
$catslug = self::sanitize_slug( \wzkb_get_option( 'category_slug', 'kb/section' ) );
241+
$tagslug = self::sanitize_slug( \wzkb_get_option( 'tag_slug', 'kb/tags' ) );
242+
$productslug = self::sanitize_slug( \wzkb_get_option( 'product_slug', 'kb/products' ) );
204243

205244
// Register products taxonomy first.
206245
$product_args = self::get_taxonomy_base_args( $productslug, false );
@@ -246,5 +285,143 @@ public static function register_taxonomies() {
246285
$tag_args = apply_filters( 'wzkb_tag_args', $tag_args );
247286

248287
register_taxonomy( 'wzkb_tag', array( 'wz_knowledgebase' ), $tag_args );
288+
289+
// Add taxonomy rewrite rules with 'top' priority to ensure they match before post type rules.
290+
add_action( 'init', array( __CLASS__, 'add_taxonomy_rewrite_rules' ), 20 );
291+
}
292+
293+
/**
294+
* Filter KB article permalinks to remove the CPT slug when using %postname% structure.
295+
*
296+
* @since 3.0.0
297+
*
298+
* @param string $permalink The post's permalink.
299+
* @param \WP_Post $post The post object.
300+
* @return string Filtered permalink.
301+
*/
302+
public static function filter_root_level_permalink( $permalink, $post ) {
303+
// Only process KB articles.
304+
if ( 'wz_knowledgebase' !== $post->post_type ) {
305+
return $permalink;
306+
}
307+
308+
// Remove the 'wz_knowledgebase/' prefix from the permalink.
309+
$permalink = str_replace( '/wz_knowledgebase/', '/', $permalink );
310+
311+
return $permalink;
312+
}
313+
314+
/**
315+
* Maybe query KB article if regular post isn't found.
316+
*
317+
* When using %postname% structure, WordPress will try to find a regular post first.
318+
* If not found, we check if there's a KB article with that slug.
319+
*
320+
* @since 3.0.0
321+
*
322+
* @param \WP_Query $query The WP_Query instance.
323+
*/
324+
public static function maybe_query_kb_article( $query ) {
325+
// Only on main query, not admin.
326+
if ( ! $query->is_main_query() || is_admin() ) {
327+
return;
328+
}
329+
330+
// Get the post slug from query vars.
331+
$post_name = $query->get( 'postname' );
332+
if ( empty( $post_name ) ) {
333+
$post_name = $query->get( 'name' );
334+
}
335+
336+
// If no post name, nothing to check.
337+
if ( empty( $post_name ) ) {
338+
return;
339+
}
340+
341+
// If it's already querying a specific post type, don't interfere.
342+
$post_type = $query->get( 'post_type' );
343+
if ( ! empty( $post_type ) && 'post' !== $post_type ) {
344+
return;
345+
}
346+
347+
// Check if there's a regular post with this slug first.
348+
$regular_post = get_posts(
349+
array(
350+
'name' => $post_name,
351+
'post_type' => 'post',
352+
'post_status' => 'publish',
353+
'posts_per_page' => 1,
354+
)
355+
);
356+
357+
// If a regular post exists, let WordPress handle it.
358+
if ( ! empty( $regular_post ) ) {
359+
return;
360+
}
361+
362+
// No regular post found, check if there's a KB article with this slug.
363+
$kb_post = get_posts(
364+
array(
365+
'name' => $post_name,
366+
'post_type' => 'wz_knowledgebase',
367+
'post_status' => 'publish',
368+
'posts_per_page' => 1,
369+
)
370+
);
371+
372+
// If a KB article exists, modify the query to fetch it instead.
373+
if ( ! empty( $kb_post ) ) {
374+
$query->set( 'post_type', 'wz_knowledgebase' );
375+
$query->set( 'name', $post_name );
376+
}
377+
}
378+
379+
/**
380+
* Add root-level rewrite rules for KB articles when using %postname% structure.
381+
*
382+
* This allows KB articles to use the same permalink structure as regular posts.
383+
* Note: These rules are added but WordPress will try regular posts first, then fall back to KB articles.
384+
*
385+
* @since 3.0.0
386+
*/
387+
public static function add_root_level_rewrite_rules() {
388+
// We don't actually need custom rewrite rules because WordPress's default post rules
389+
// will handle the URL matching. We just need to hook into the query to check for KB articles
390+
// when a regular post isn't found. Use parse_query which runs before WordPress sets p=0.
391+
add_action( 'parse_query', array( __CLASS__, 'maybe_query_kb_article' ), 1 );
392+
}
393+
394+
/**
395+
* Add taxonomy rewrite rules with top priority.
396+
*
397+
* This ensures taxonomy archive URLs are matched before post type attachment rules.
398+
*
399+
* @since 3.0.0
400+
*/
401+
public static function add_taxonomy_rewrite_rules() {
402+
$productslug = self::sanitize_slug( \wzkb_get_option( 'product_slug', 'kb/products' ) );
403+
$catslug = self::sanitize_slug( \wzkb_get_option( 'category_slug', 'kb/section' ) );
404+
$tagslug = self::sanitize_slug( \wzkb_get_option( 'tag_slug', 'kb/tags' ) );
405+
406+
// Add product taxonomy rules.
407+
add_rewrite_rule(
408+
'^' . preg_quote( $productslug, '/' ) . '/([^/]+)/?$',
409+
'index.php?wzkb_product=$matches[1]',
410+
'top'
411+
);
412+
413+
// Add section taxonomy rules (hierarchical).
414+
add_rewrite_rule(
415+
'^' . preg_quote( $catslug, '/' ) . '/(.+?)/?$',
416+
'index.php?wzkb_category=$matches[1]',
417+
'top'
418+
);
419+
420+
// Add tag taxonomy rules.
421+
add_rewrite_rule(
422+
'^' . preg_quote( $tagslug, '/' ) . '/([^/]+)/?$',
423+
'index.php?wzkb_tag=$matches[1]',
424+
'top'
425+
);
249426
}
250427
}

0 commit comments

Comments
 (0)