Skip to content

feat(theme): point admin "View Post" links at the headless frontend#134

Merged
JohnRDOrazio merged 1 commit into
mainfrom
feat/view-post-links-to-frontend
May 23, 2026
Merged

feat(theme): point admin "View Post" links at the headless frontend#134
JohnRDOrazio merged 1 commit into
mainfrom
feat/view-post-links-to-frontend

Conversation

@JohnRDOrazio
Copy link
Copy Markdown
Member

@JohnRDOrazio JohnRDOrazio commented May 23, 2026

Problem

In headless mode WordPress still hands out its own permalink for the admin View Post link. Editing a post and clicking the View icon sends you to e.g. https://cms.catholicdigitalcommons.org/welcome-to-the-catholic-digital-commons-foundation/ — a dead WP-side URL — instead of the live frontend https://catholicdigitalcommons.org/blog/welcome-to-the-catholic-digital-commons-foundation.

A preview_post_link filter already routed drafts to Next.js draft mode, but there was no post_link filter for published posts.

Fix

New includes/frontend-permalinks.php (registered via add_filter('post_link', …) in functions.php):

  • cdcf_frontend_post_url($post) — maps a published post to CDCF_FRONTEND_URL + /blog/<slug>, with an /<lang> prefix for non-default Polylang locales. Mirrors the slug→path mapping in app/api/preview/route.ts:59-61 so preview and published URLs agree.
  • cdcf_filter_post_permalink() — the filter. Bails on WPGraphQL requests: the frontend consumes the unmodified uri field (derived from the permalink — e.g. the page sitemap in lib/wordpress/api.ts), so rewriting permalinks during a GraphQL request would corrupt that data. REST requests — which feed the block editor's View Post link via the link field — are not GraphQL requests, so they still get the frontend URL.

This covers the block editor, classic editor, admin bar, and the post-list "View" row action. Only publish-status posts with a real slug are rewritten; drafts keep WP behavior and use the existing preview link.

Scope

Posts only (the reported case; /blog/<slug> is flat and unambiguous). Pages have the same broken link but can be hierarchical (parent/child) — left for a follow-up rather than guessing the mapping. CPTs keep their default permalinks. Frontend URL is emitted without a trailing slash to match the frontend's canonical routing (sitemap, internal links).

Testing

  • tests/FrontendPostPermalinkTest.php (6 cases): en + non-default locale mapping, non-post types, unpublished/slugless opt-out, the rewrite, and the GraphQL bail. Registered in the test bootstrap.
  • composer test: 342 tests pass. php -l clean on all changed files.

Deployment

This is a WordPress theme change, so it only reaches the live cms. host on a production deploy (if: env.ENVIRONMENT == 'production') — staging shares the prod WP backend and doesn't ship the theme.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Post permalinks now redirect to the Next.js frontend blog instead of WordPress.
    • Automatic language prefix support for multilingual sites.
    • Preserves original URLs during GraphQL requests to maintain data integrity.
  • Tests

    • Added comprehensive test coverage for the permalink rewriting functionality.

Review Change Stack

In headless mode WordPress still served its own permalink
(cms.catholicdigitalcommons.org/<slug>/) for the admin "View Post" link,
sending editors to a dead WP-side URL. Add a post_link filter that
rewrites a published post's permalink to the Next.js frontend at
/blog/<slug>, with an /<lang> prefix for non-default Polylang locales —
the same slug→path mapping as app/api/preview/route.ts. This covers the
block editor (REST `link` field), classic editor, admin bar, and the
post-list "View" row action.

The filter bails on WPGraphQL requests: the frontend consumes the
unmodified `uri` field (derived from the permalink — e.g. the page
sitemap), so rewriting permalinks during a GraphQL request would corrupt
that data. REST requests, which feed the block-editor link, are not
GraphQL requests and so still get the frontend URL.

Scope is posts only (flat /blog/ route); pages and CPTs keep their
default permalinks. Bodies live in includes/frontend-permalinks.php so
they're unit-tested (tests/FrontendPostPermalinkTest.php, 6 cases).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

📝 Walkthrough

Walkthrough

This pull request adds a WordPress-to-Next.js post permalink rewriting system. Published posts that match the post type now generate frontend /blog/<slug> URLs, with Polylang language prefixing for non-default locales, while preserving original permalinks during GraphQL requests and for ineligible posts.

Changes

Post Permalink Rewriting

Layer / File(s) Summary
Frontend Permalink URL Functions
wordpress/themes/cdcf-headless/includes/frontend-permalinks.php
cdcf_frontend_post_url() validates published posts, constructs frontend URLs with optional Polylang language prefixes, and falls back to http://localhost:3000. cdcf_filter_post_permalink() preserves permalinks during GraphQL requests and rewrites them for regular requests.
WordPress Filter Integration
wordpress/themes/cdcf-headless/functions.php
Requires the new permalink module and registers the filter on WordPress's post_link hook.
Test Suite and Bootstrap Setup
wordpress/themes/cdcf-headless/tests/FrontendPostPermalinkTest.php, wordpress/themes/cdcf-headless/tests/bootstrap.php
PHPUnit tests verify URL generation for published/unpublished posts, locale prefixing, GraphQL request handling, and non-post type filtering. Bootstrap loader added to initialize the module during test runs.

Sequence Diagram

sequenceDiagram
  participant WP as WordPress
  participant Filter as cdcf_filter_post_permalink
  participant URLGen as cdcf_frontend_post_url
  participant Frontend as Next.js Frontend
  
  WP->>Filter: post_link hook with permalink and post
  alt GraphQL Request
    Filter-->>WP: return original permalink unchanged
  else Regular Request
    Filter->>URLGen: construct frontend URL for post
    alt Published post with slug
      URLGen-->>Filter: /blog/<slug> with optional locale prefix
    else Ineligible post
      URLGen-->>Filter: null
    end
    alt Frontend URL generated
      Filter-->>WP: rewritten frontend URL
    else No frontend URL available
      Filter-->>WP: original permalink
    end
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A rabbit hops through WordPress links,
Rewriting paths with clever thinks,
To Next.js blogs the posts now go,
With Polylang's locales in tow!
GraphQL requests stay true and pure,
While human browsers see frontend's allure. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: routing admin 'View Post' links to the headless frontend, which is the primary purpose of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/view-post-links-to-frontend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...mes/cdcf-headless/includes/frontend-permalinks.php 77.77% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 21 complexity · 0 duplication

Metric Results
Complexity 21
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wordpress/themes/cdcf-headless/includes/frontend-permalinks.php`:
- Around line 44-50: The code currently treats 'en' as the default language when
building the $prefix; instead of hardcoding 'en' call
pll_default_language('slug') and compare $lang against that value (use
pll_get_post_language($post->ID, 'slug') for $lang as already written), so set
$default = function_exists('pll_default_language') ?
pll_default_language('slug') : 'en' (or empty string) and make $prefix = ($lang
&& $lang !== $default) ? '/' . $lang : ''; update the return that concatenates
$frontend, $prefix and '/blog/' . $post->post_name accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92253cfd-040a-4e95-b53e-06bf7a57346a

📥 Commits

Reviewing files that changed from the base of the PR and between 6c89152 and 0f57a53.

📒 Files selected for processing (4)
  • wordpress/themes/cdcf-headless/functions.php
  • wordpress/themes/cdcf-headless/includes/frontend-permalinks.php
  • wordpress/themes/cdcf-headless/tests/FrontendPostPermalinkTest.php
  • wordpress/themes/cdcf-headless/tests/bootstrap.php

Comment on lines +44 to +50
// Polylang language slug ("en", "it", …); empty when Polylang is off.
$lang = function_exists('pll_get_post_language')
? pll_get_post_language($post->ID, 'slug')
: '';
$prefix = ($lang && $lang !== 'en') ? '/' . $lang : '';

return $frontend . $prefix . '/blog/' . $post->post_name;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use pll_default_language('slug') instead of hardcoding 'en' as the default language.

Line 48 assumes the default language is always 'en', but this breaks if the site is configured with a different default language. The codebase pattern (seen in includes/translation.php:127) is to call pll_default_language('slug') to retrieve the actual default dynamically.

🔧 Proposed fix
     // Polylang language slug ("en", "it", …); empty when Polylang is off.
     $lang = function_exists('pll_get_post_language')
         ? pll_get_post_language($post->ID, 'slug')
         : '';
-    $prefix = ($lang && $lang !== 'en') ? '/' . $lang : '';
+    
+    $default_lang = function_exists('pll_default_language')
+        ? pll_default_language('slug')
+        : 'en';
+    $prefix = ($lang && $lang !== $default_lang) ? '/' . $lang : '';

     return $frontend . $prefix . '/blog/' . $post->post_name;

Based on learnings, the theme uses pll_default_language('slug') to determine the default language throughout (e.g., includes/translation.php:127).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Polylang language slug ("en", "it", …); empty when Polylang is off.
$lang = function_exists('pll_get_post_language')
? pll_get_post_language($post->ID, 'slug')
: '';
$prefix = ($lang && $lang !== 'en') ? '/' . $lang : '';
return $frontend . $prefix . '/blog/' . $post->post_name;
// Polylang language slug ("en", "it", …); empty when Polylang is off.
$lang = function_exists('pll_get_post_language')
? pll_get_post_language($post->ID, 'slug')
: '';
$default_lang = function_exists('pll_default_language')
? pll_default_language('slug')
: 'en';
$prefix = ($lang && $lang !== $default_lang) ? '/' . $lang : '';
return $frontend . $prefix . '/blog/' . $post->post_name;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress/themes/cdcf-headless/includes/frontend-permalinks.php` around lines
44 - 50, The code currently treats 'en' as the default language when building
the $prefix; instead of hardcoding 'en' call pll_default_language('slug') and
compare $lang against that value (use pll_get_post_language($post->ID, 'slug')
for $lang as already written), so set $default =
function_exists('pll_default_language') ? pll_default_language('slug') : 'en'
(or empty string) and make $prefix = ($lang && $lang !== $default) ? '/' . $lang
: ''; update the return that concatenates $frontend, $prefix and '/blog/' .
$post->post_name accordingly.

@JohnRDOrazio JohnRDOrazio merged commit eb9f1b8 into main May 23, 2026
12 checks passed
@JohnRDOrazio JohnRDOrazio deleted the feat/view-post-links-to-frontend branch May 23, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants