无法使自定义重写标记、查询变量、永久结构(永久链接结构)和重写规则一起正常工作

时间:2020-08-30 作者:Jivan Pal

问题规范

我正在实现一个网站,该网站旨在托管播客的shownotes和成绩单,因此我需要定制网页的永久链接和短链接:

表单的永久链接/podcasts/<episode_number>/<episode_title>, e、 g。/podcasts/12/news-for-august/<episode_number> 重定向到永久链接,例如。/12 重定向到/podcasts/12/news-for-august./podcasts/12/news-for-august:

  • /podcasts/12
  • /podcasts/12/incorrect-title
我们使用CPT UI创建的自定义播客帖子类型。的价值<episode_number> 存储在带有元密钥的ACF字段中episode_number; 如果多个已发布的播客具有相同的集号(这在实践中显然不应该发生),则会提供帖子ID最低的播客。的价值<episode_title> 只是帖子的头条,因为播客帖子的标题保持着实际的插曲标题(例如。News for August).

作为一个(也许很重要?)旁注,我使用的是Nginx,而不是Apache-我看到很多关于.htaccess 修改,这显然不适用于这里,所以只需提及这一点。

到目前为止,我已经阅读了重写API,经过几天的努力,我终于了解了相关的内部工作原理。。。

当CPT UI注册自定义帖子类型时podcast, 它还添加了一个名为podcast. 因为我的帖子的permastruct(在[设置>Permalinks>自定义结构]中设置)是/articles/%post_id%/%postname%, 这个podcast 磁导率为/articles/podcast/%postname%. 在CPT UI中,我可以禁用;带“front”;(因此/articles 已删除)并设置“;“重写slug”;到podcasts (复数)而非默认值podcast (单数),这将导致形式的永久对齐/podcasts/<episode_title> 相反如果我不能达到我想要的,我可能不得不接受这个。

重写标记

我定义了一个重写标记,%podcast_episode_number%, 这样我就可以为播客定义我的自定义permastruct。我只是用名称覆盖了permastructpodcast CPT UI添加了,因此它自动应用于播客。我还定义了一个重写规则来处理短链接。这是我的主题的相关部分functions.php:

function wpse373987_add_tag_and_permastruct() {
    /** Define the tag */
    add_rewrite_tag( \'%podcast_episode_number%\', \'([0-9]+)\' );

    /** Override the default permastruct for the podcast post type */
    add_permastruct(
        \'podcast\',
        \'podcasts/%podcast_episode_number%/%postname%\',
        [ \'with_front\' => false ]
    );

    /** Define podcast shortlinks */
    add_rewrite_rule( \'^([0-9]+)/?\', [ \'podcast_episode_number\' => \'$matches[1]\' ], \'top\' );
}
add_action( \'init\', \'wpse373987_add_tag_and_permastruct\' );

标记替换

然后定义如何%podcast_episode_number% 应通过挂接到post_linkpost_type_link 过滤器。奇怪的是,在播客permalinks的背景下%postname% 标签没有像普通博客帖子那样被填充,所以我也在这里这样做:

function wpse373987_handle_tag_substitution( $permalink, $post ) {
    // Do nothing if the tag isn\'t present
    if ( strpos( $permalink, \'%podcast_episode_number%\' ) === false ) {
        return $permalink;
    }
    
    $fallback = \'_\';
    
    $episode_number = \'\';
    if ( function_exists( \'get_field\' ) && $post->post_type === \'podcast\' ) {
        $episode_number = get_field( \'episode_number\', $post->ID, true );
    }
    if ( ! $episode_number ) {
        $episode_number = $fallback;
    }

    $permalink = str_replace( \'%podcast_episode_number%\', $episode_number, $permalink );
    $permalink = str_replace( \'%postname%\', $post->post_name, $permalink ); // Strangely, this is needed.

    return $permalink;
}
/**
 * Filter permalinks using `wpse373987_handle_tag_substitution()`. Late priority (100) is
 * assigned so that this filter is called last, when the tags are present.
 */
add_filter( \'post_link\', \'wpse373987_handle_tag_substitution\', 100, 2 );
add_filter( \'post_type_link\', \'wpse373987_handle_tag_substitution\', 100, 2 );

查询过滤器

最后,我定义了查询变量podcast_episode_number (对应于标签%podcast_episode_number%, 并且在以下情况下隐式注册add_rewrite_tag() ,以便当我们访问problem specification, WordPress可以使用podcast_episode_number 参数来确定相应的岗位ID,从而为岗位服务。我们在request 筛选以执行此操作。

function wpse373987_handle_query_var( $query_vars ) {
    /** Ignore requests that don\'t concern us. */
    if ( ! isset( $query_vars[\'podcast_episode_number\'] ) ) {
        return $query_vars;
    }

    /** Validate the episode number; it must be a positive integer. */
    if ( preg_match( \'/^[0-9]+$/\', $query_vars[\'podcast_episode_number\'] ) !== 1 ) {
        /**
         * The episode number is invalid; respond with a 404 Not Found.
         * We do this by requesting the post that has ID -1,
         * which is guaranteed to not exist.
         */
        return [ \'p\' => \'-1\' ];
    }

    /** Casting to `int` removes leading zeroes from the SQL query */
    $episode_number = (int)( $query_vars[\'podcast_episode_number\'] );

    /** Determine the ID of the post with the given episode number. */
    global $wpdb;

    $post_ids = $wpdb->get_col(
        $wpdb->prepare(
            "SELECT post_id FROM {$wpdb->postmeta} WHERE
                    meta_key = \'episode_number\'
                AND meta_value = %d
            ORDER BY post_id ASC",
            
            $episode_number
        )
    );

    /**
     * String representing `$post_ids` in SQL syntax,
     * e.g. "(\'12\',\'14\',\'15\',\'18\')".
     */
    $sql_post_ids = "(\'" . implode( "\',\'", $post_ids ) . "\')";

    $post_ids = $wpdb->get_col(
        "SELECT id FROM {$wpdb->posts} WHERE
                id IN {$sql_post_ids}
            AND post_type = \'podcast\'
            AND post_status = \'publish\'
        ORDER BY id ASC"
    );

    if ( count( $post_ids ) === 0 ) {
        /**
         * There are no published podcasts with the given episode number;
         * respond with 404.
         */
        return [ \'p\' => \'-1\' ];
    }

    /**
     * Request the post with the lowest post ID among published
     * podcasts with the given episode number.
     */
    return [ \'p\' => $post_ids[0] ];
}
/**
 * Filter queries using `wpse373987_handle_query_var()`.
 * Late priority (100) is assigned to ensure that this filter is applied last.
 */
add_filter( \'request\', \'wpse373987_handle_query_var\', 100 );
在所有这些之后,并通过[设置>永久链接>保存设置]刷新重写规则,链接结构就会工作!也就是说,例如,web服务器响应以下所有URL的请求,并将301重定向到/podcasts/12/news-for-august:

  • /12
  • /podcasts/12
  • /podcasts/12/incorrect-title/podcasts/12/news-for-august) WordPress找不到。。。WordPress提供我的主题404模板(404.php) HTTP响应是404,就像其他找不到的URL一样。显然,这是因为WordPress不知道使用什么模板。我可以通过在中返回帖子类型和帖子ID来解决此问题handle_query_var() (即。return [ \'p\' => $post_ids[0], \'post_type\' => \'podcast\' ]), 但这会产生不良影响,使上面列出的所有别名URL也只服务于内容,而不是重定向到永久链接-这对于SEO来说显然是可怕的。

    问题是什么?当客户端访问podcast permalink时,如果没有其他URL提供相同的内容,我如何才能获得正确的模板来加载?也许会template_redirect 或使用wp_redirect() 还是别的什么?也许我在这里的总体方法是错误的,有人可以为我指出正确的方向?

    非常感谢您的任何建议。

1 个回复
最合适的回答,由SO网友:Jivan Pal 整理而成

在通过挂接过滤器和记录变量值对重写进行了大量检查之后,我设法解决了这个问题!

查询和查询重写当查询发生时,WordPress将使用正确的模板提供内容,只要它有足够的信息来明确确定模板和帖子是什么。对于非定制的post类型,WordPress只需要知道post slug。对于自定义的post类型,它需要知道post slug和post类型;因此,对于播客,查询需要指定post_type=podcast 和例如。name=news-for-august. 这是因为post段塞对于给定的post类型是唯一的,但不必跨post类型是唯一的,因此段塞本身不足以识别post。此外,必须知道帖子类型,才能选择正确的模板。因此/?post_type=podcast&name=news-for-august 可以解析并正确呈现帖子。

此外,注册post类型时,将注册一个重写标记和一个查询变量,以允许压缩此查询。例如,对于我的podcast post类型,重写标记为%podcast% (不是%postname% 与非自定义帖子一样),查询变量为podcast=; 这是post_typename. 例如,请求/?podcast=news-for-august 内部重写为/?podcast=news-for-august&post_type=podcast&name=news-for-august, 从而导致该职位的任职。

这解释了以下问题:

奇怪的是,在播客permalinks的背景下%postname% 标签没有像普通博客帖子那样被填充。

此外,关于以下内容。。。

当CPT UI注册自定义帖子类型时podcast, 它还添加了一个名为podcast. 因为我的帖子的permastruct(在[设置>Permalinks>自定义结构]中设置)是/articles/%post_id%/%postname%, the podcast permastruct is /articles/podcast/%postname%.

。。。默认的permalink结构实际上是/articles/podcast/%podcast%.

在查询中指定帖子ID时(通过p=), 它优先于任何post_type 和/或name 变量如果这些变量与指定的ID不一致,则会发生重定向。实际上,如果指定了ID,例如,如果播客帖子的IDNews for August50, 然后/?p=50 也在内部重写为/?post_type=podcast&name=news-for-august, 这将导致重定向到该帖子的永久链接。

我们可以利用这种行为来确保对要实现的其他URL格式重定向到永久链接。

调整permastruct和标记替换

我们将调整permastruct以使用%podcast% 而不是%postname%:

function wpse373987_add_tag_and_permastruct() {
    /** Define the tag */
    add_rewrite_tag( \'%podcast_episode_number%\', \'([0-9]+)\' );

    /** Override the default permastruct for the podcast post type */
    add_permastruct(
        \'podcast\',
        \'podcasts/%podcast_episode_number%/%podcast%\',   // This line changed
        [ \'with_front\' => false ]
    );

    /** Define podcast shortlinks */
    add_rewrite_rule( \'^([0-9]+)/?\', [ \'podcast_episode_number\' => \'$matches[1]\' ], \'top\' );
}
add_action( \'init\', \'wpse373987_add_tag_and_permastruct\' );
因为我们不再使用%postname% 标签在我们的permastruct中,我们也不再需要替换%postname% 对于slug;这可以通过%podcast% 自动标记。正在筛选post_link 也没有必要,因为post_type_link 用于自定义帖子类型:

function wpse373987_handle_tag_substitution( $permalink, $post ) {
    // Do nothing if the tag isn\'t present
    if ( strpos( $permalink, \'%podcast_episode_number%\' ) === false ) {
        return $permalink;
    }
    
    $fallback = \'_\';
    
    $episode_number = \'\';
    if ( function_exists( \'get_field\' ) && $post->post_type === \'podcast\' ) {
        $episode_number = get_field( \'episode_number\', $post->ID, true );
    }
    if ( ! $episode_number ) {
        $episode_number = $fallback;
    }

    $permalink = str_replace( \'%podcast_episode_number%\', $episode_number, $permalink );
    
    // The following line is now not needed.
    // $permalink = str_replace( \'%postname%\', $post->post_name, $permalink );

    return $permalink;
}
add_filter( \'post_type_link\', \'wpse373987_handle_tag_substitution\', 100, 2 );
// The following line is not needed.
// add_filter( \'post_link\', \'wpse373987_handle_tag_substitution\', 100, 2 );

调整我们的查询重写

在进行了上述两项调整之后,播客的永久链接的形式如下/podcasts/<episode_number>/<episode_title>, 而且内容是从该URL正确提供的,因为它在内部解析为查询/?post_type=podcast&name=<episode_title>&podcast_episode_number=<episode_number>, 其中包含post_typename 确定服务哪个帖子和使用哪个模板所需的变量。

但是,对于其他URL格式,即:

  • /podcasts/<episode_number>/<incorrect_title>;
  • /podcasts/<episode_number>; 和/<episode_number>;
我们仍然需要确定如何解决<episode_number> 我们通过连接到request 滤器之前,我们正在将播客的所有查询重写到表单中/?p=<podcast_post_id>, <当我们访问permalink URL时,包括在内,这就是导致404错误的原因。这是因为,如果客户端访问永久链接URL,WordPress不会对该表单的查询发出重定向-相反,查询处理会继续,WordPress在意识到查询不包含post_typename (因为我们的查询重写删除了这些内容),因此它无法确定要服务哪个帖子,也无法确定要使用哪个模板。

因此,我们应该只将查询重写为表单/?p=<podcast_post_id> 当我们当前访问的URL不是永久链接时。内容已在永久链接URL上正确提供;我们只想将其他URL重定向到permalink,我们可以像以前一样,通过重写查询来获得post ID,但不能在客户端访问permalink URL时这样做。

还有,而不是返回[ \'p\' => \'-1\' ] 要在需要时引发404响应,正确的方法是返回[ \'error\' => 404 ].

以下是修改后的过滤器:

function wpse373987_handle_query_var( $query_vars ) {
    /** Ignore requests that don\'t concern us. */
    if ( ! isset( $query_vars[\'podcast_episode_number\'] ) ) {
        return $query_vars;
    }

    /** Validate the episode number; it must be an unsigned integer. */
    if ( preg_match( \'/^[0-9]+$/\', $query_vars[\'podcast_episode_number\'] ) !== 1 ) {
        /** The episode number is invalid; respond with a 404 Not Found. */
        return [ \'error\' => 404 ];
    }

    /**
     * Episode number, with any leading zeroes stripped;
     * they must be stripped for the SQL query to work.
     */
    $episode_number = (int)( $query_vars[\'podcast_episode_number\'] );

    global $wpdb;
    
    /** Array of IDs of posts that have the given episode number */
    $post_ids = $wpdb->get_col(
        $wpdb->prepare(
            "SELECT post_id FROM {$wpdb->postmeta} WHERE
                    meta_key = \'episode_number\'
                AND meta_value = %d
            ORDER BY post_id ASC",
            
            $episode_number
        )
    );

    /** String representing `$post_ids` in SQL syntax */
    $sql_post_ids = "(\'" . implode( "\',\'", $post_ids ) . "\')";

    // The logic after this point has been adjusted.

    /**
     * Determine the ID and name of the published podcast with the given episode
     * number (and lowest ID, if multiple such podcasts exist).
     */
    $podcast = $wpdb->get_row(
        "SELECT id, post_name AS name FROM {$wpdb->posts} WHERE
                id IN {$sql_post_ids}
            AND post_type = \'podcast\'
            AND post_status = \'publish\'
        ORDER BY id ASC"
    );

    /**
     * If there are no published podcasts with the given episode number,
     * respond with 404.
     */
    if ( $podcast === null ) {
        return [ \'error\' => 404 ];
    }

    /**
     * If the podcast name specified in the query doesn\'t correspond to the
     * episode number specified in the query, we need to redirect to the right
     * page, based on the episode number (ignoring the specified name). We do
     * this by issuing a query for the post ID; that query will then redirect
     * to the podcast\'s permalink, where we won\'t take action.
     * 
     * Else, the specified name matches the specified episode number,
     * so we are already at the podcast\'s permalink, and thus do nothing.
     */
    if (    ! isset( $query_vars[\'name\'] )
        ||  $query_vars[\'name\'] !== $podcast->name
    ) {
        return [ \'p\' => $podcast->id ];
    }

    return $query_vars;
}
add_filter( \'request\', \'wpse373987_handle_query_var\', 100 );

结果

太好了,它工作了!

表单的URL/podcasts/<episode_number>, 然后是错误的slug或无slug,将重定向到具有该集号的播客的永久链接。现在,我们在中添加的重写规则也可以正确处理短链接add_tag_and_permastruct(); 它解析表单的URL/<episode_number> 到表单的查询/?podcast_episode_number=<episode_number>. 我们的request 滤器handle_query_var(), 将其改写为表单/?p=<post_id>, WordPress然后重定向到相应的播客permalink。全部排序!

相关推荐

Problem with permalinks

我已经更改了类别的基本名称,现在它是“博客”,工作正常。但当我通过/blog/%category%/%postname%/更改结构时。显示404。如果我删除结构中的blog,它会再次工作,但我想使用blog word。问题出在哪里?非常感谢。