Skip to content

Draft article preview is broken on production (dead "View" link + unauthenticated draft fetch) #151

@JohnRDOrazio

Description

@JohnRDOrazio

Symptom

Opening a preview of an unpublished article draft fails. The only affordances the editor surfaces for a draft are the clickable slug and the "View" link in the save snackbar; both point at the ?p=<id> permalink placeholder, which the headless frontend has no route for, so it renders the site homepage instead of the draft. (Repro draft: post 1326, title "Rebuilding the City, not the Tower".)

There is no Gutenberg "Preview in new tab" affordance in this editor, so the preview_post_link filter (which is wired correctly) is never reached by the user.

Root cause — two independent problems

1. Production frontend can't authenticate the draft fetch (hard blocker)

/api/preview?id=<id> works: it validates the secret, enables draft mode, sets the preview cookies, and 307-redirects to /blog/<id>. But /blog/<id> then 404s, because the page calls getPostPreview(id)wpQuery(..., { draft: true }) and gets null.

Verified against production:

  • Unauthenticated GraphQL post(1326, idType: DATABASE_ID){ post: null } (WPGraphQL hides drafts).
  • The same query with WP Application Password (Basic auth) → returns the draft (status: draft). So the backend and the /graphql app-password opt-in filter are working.

Therefore the frontend's { draft: true } request is going out unauthenticated → published-only → draft resolves to nothing → 404. lib/wordpress/client.ts falls back to Basic auth from the server-only WP_APP_USERNAME / WP_APP_PASSWORD env vars and logs Draft WPGraphQL request without credentials when they're absent.

Fix: set WP_APP_USERNAME / WP_APP_PASSWORD in the production frontend runtime environment (server-only; a WP Application Password for a user with edit_posts). This is a deployment/config change, not code. Until it's in place, no preview path can render a draft.

2. The draft's slug / "View" link never enters the preview flow

includes/frontend-permalinks.phpcdcf_frontend_path_for() returns null for any non-publish status, so cdcf_frontend_permalink passes the default ?p=<id> permalink through unchanged for drafts. Since the editor only exposes the slug and the snackbar "View" (both derived from get_permalink()), the user has no link that routes to /api/preview. The file comment assumes drafts are "handled by preview_post_link", but that filter only feeds the Preview button, which isn't surfaced here.

Fix (needs a decision): route unpublished routable posts (post/page) from get_permalink() into the preview entry point. Two approaches, differing only in how the shared WP_PREVIEW_SECRET is handled:

  • (A) WP redirect endpoint (preferred). Draft permalink → a WP admin-post.php action (cookie-authenticated). It checks current_user_can('edit_post', $id), then redirects to the frontend preview entry point with the secret. The shared secret stays server-side — never baked into get_permalink() / REST link output — and is capability-gated. ~1 handler + filter branch.
  • (B) Embed the preview URL in the permalink. cdcf_frontend_permalink returns the frontend preview URL (with the secret) for unpublished posts, reusing the existing preview_post_link construction. Smallest change, but the shared secret then appears anywhere a draft's get_permalink() is read (block editor + authenticated edit-context REST link).

Acceptance criteria

  • Production frontend has working WP credentials for authenticated draft GraphQL (problem 1).
  • Clicking the slug / snackbar "View" on a draft opens the rendered draft (problem 2), via the chosen approach.
  • Published-post permalinks are unchanged.
  • If approach (A): the secret never appears in get_permalink() / REST output; the redirect is capability-gated.

Notes

🤖 Filed from a debugging session with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions