Post Previews in the App Router with Faust.js for headless WordPress

Francis Agulto Avatar

·

Post previews in WordPress allow users to view content exactly as it will appear on the live site before it is published. This feature is essential for authors, editors, and administrators to review and approve posts, ensuring that formatting, images, and layouts are correct and meet the intended design and content standards. Post previews are accessible through the WordPress editor, where a “Preview” button is available.  

Getting this to work in a headless setup is difficult.  Luckily, Faust.js does this out of the box.

This article delves into the mechanics of post previews within the experimental app router example project of Faust.js. We will explore the implementation of Post Previews in traditional WordPress, examine the integration with Faust.js’s App Router, and dissect the underlying processes within the Faust.js framework itself.   

Prerequisites

To fully comprehend and gain insight from this article, you should have a foundational understanding of the App Router, including its naming conventions and its directory and file hierarchy. If you are not yet familiar with these concepts, please reference my previous article on the subject before proceeding with this one.

Post Previews in Traditional WordPress

Before we discuss how Faust.js with the App Router does Post Previews, let’s take a look at WordPress’ traditional monolithic architecture approaches it under the hood:

Draft Saving: When you’re working on a post in WordPress, it automatically saves your changes as a draft. This draft is stored in the WordPress database in the wp_posts table, with a post status of ‘auto-draft’ or ‘draft’.

Preview Request: When you click the “Preview” button, WordPress initiates a request to generate a preview of the post. This request includes a query parameter (usually preview=true) that tells WordPress this is a preview request.

Post Revision Creation: To handle the preview, WordPress creates a post revision. This is a copy of your post at that moment, also stored in the wp_posts table, but with a post type of ‘revision’. This ensures that your current edits, even if not saved as a draft, are captured in this revision.

Preview Link Generation: WordPress generates a preview link that includes a nonce (a one-time use security token) to ensure the preview request is valid. This link points to the post URL but with additional query parameters that instruct WordPress to load the revision instead of the published content.

User Role and Permission Check: When the preview link is accessed, WordPress checks the user’s role and permissions to ensure they have the right to view the preview. This step is crucial for content security and integrity.

Rendering the Preview: If the user has the appropriate permissions, WordPress then loads the post revision data instead of the main post data. The site’s theme and styling are applied to this revision content, and it’s rendered in the user’s browser. This process involves the same template hierarchy and rendering engine as the live site, ensuring an accurate representation of how the post will look once published.

Non-Published Content Handling: It’s important to note that this preview mechanism allows users to see changes in real-time for unpublished (draft or pending) posts as well as changes to already published posts. For published posts, WordPress stores the changes as revisions without affecting the live version until those changes are explicitly published.

Security Measures: The nonce and permission checks play a crucial role in ensuring that only authorized users can access the post previews, protecting unpublished content from unauthorized access.

This engineering design enables WordPress users to securely preview their content, ensuring that only the intended changes are published to the live site. It provides a precise preview of how the content will be presented to the end-users.

Let’s dive into how Faust.js with the App Router replicates this experience and engineering in headless WordPress from a decoupled architecture.

Faust.js and App Router Support

The experimental app router example project from Faust.js is a boilerplate starter kit that includes a sample site showcasing the utility of functions such as getClient, getAuthClient, faustRouteHandler, loginAction, and logoutAction. This project can serve as a foundation for your future endeavors and as an informative reference for App Routing in headless WordPress. It is designed for those who are proficient with the command line, have fundamental knowledge of Faust.js and Next.js routing, and have an understanding of the basics of JavaScript, WordPress, and Bash.

Now, let’s focus on how it does Post Previews.

How Post Previews Work in Faust.js with the App Router

Remember, everything in the App Router in Faust.js defaults to being a server component unless otherwise specified with the ‘use client’ directive.  Let’s look at the Faust.js App Router project’s folders and files that make Post Previews work along with the Faust.js plugin necessary for this.

Environment Variables

The first action necessary to make post previews work in App Router is the Faust.js plugin’s secret key, 

To get the secret key, navigate to the ‘Add Plugins’ page in WP Admin and search for the Faust.js plugin.

Once that is installed and activated, when you hover over the settings option in the left-side hamburger menu, it will display a Faust option.  Click on that and you will see this page:

 It is on this page that you can grab your secret key and your front-end URL which in a development server case, it’s going to be off port 3000 on local host.  The last thing you need to get is your WordPress site URL.

Now, navigate over to your code editor. In the .env.local file that you created when spinning up the boilerplate, you can add those values to their keys like so:

Faust.js API Route Handler

Once you have your environment variables set, ensure that you have a directory in the root of the app folder called api.  In this api folder, you should have a nested folder called faust which will contain a file called [route].ts.  It should look like this:

What this code executes is the handling of the endpoints for the auth token and login/logout functionality.  

Authenticated Requests

Navigate to the dynamic route segment that is set by the slug as its parameter which is the [slug]/hasPreviewProps.ts file in the root of the app folder.  You should see this:

export function hasPreviewProps(props: any) {
  return props?.searchParams?.preview === 'true' && !!props?.searchParams?.p;
}

Code language: JavaScript (javascript)

The function hasPreviewProps checks if the preview mode is activated and if there is a post ID present in the properties passed to it. It returns true only if both conditions are met: the preview parameter is set to ‘true’ and a post ID is specified. This is used to determine if a post preview should be displayed. If either condition is not fulfilled, the function returns false, indicating that the normal, non-preview content should be rendered. This helps in conditionally showing either the live post or its preview during development.

Now that we have a function to help check if previews are activated and present, let’s look at the page.tsx file in the same directory to see how authentication and rendering the preview on the content works.

import { getAuthClient, getClient } from '@faustwp/experimental-app-router';
import { gql } from '@apollo/client';
import { hasPreviewProps } from './hasPreviewProps';
import { PleaseLogin } from '@/components/please-login';

export default async function Page(props) {
  const isPreview = hasPreviewProps(props);
  const id = isPreview ? props.searchParams.p : props.params.slug;

  let client = isPreview ? await getAuthClient() : await getClient();

  if (!client) {
    return <PleaseLogin />;
  }

  const { data } = await client.query({
    query: gql`
      query GetContentNode(
        $id: ID!
        $idType: ContentNodeIdTypeEnum!
        $asPreview: Boolean!
      ) {
        contentNode(id: $id, idType: $idType, asPreview: $asPreview) {
          ... on NodeWithTitle {
            title
          }
          ... on NodeWithContentEditor {
            content
          }
          date
        }
      }
    `,
    variables: {
      id,
      idType: isPreview ? 'DATABASE_ID' : 'URI',
      asPreview: isPreview,
    },
  });

  return (
    <main>
      <h2>{data?.contentNode?.title}</h2>
      <div
        dangerouslySetInnerHTML={{ __html: data?.contentNode?.content ?? '' }}
      />
    </main>
  );
}


Code language: JavaScript (javascript)

At the very top of the file, the necessary modules are imported, including functions for authentication and GraphQL queries, the utility function to check for preview props, and a component to prompt for login if needed.

Following our imports, we define an asynchronous Page function component that takes props as an argument.

Within this function, we have a preview check.  We use the hasPreviewProps helper function to determine if the page is in preview mode (isPreview) and sets the id based on whether it’s a preview or a published page, using query parameters (searchParams.p) for previews or URL parameters (params.slug) for published content.

After that, we initialize the GraphQL Client with authentication if it’s a preview with the getAuthClient function, otherwise it initializes a regular client with getClient. If the client can’t be initialized, it returns a <PleaseLogin /> component to prompt the user to log in.

export default async function Page(props) {
  const isPreview = hasPreviewProps(props);
  const id = isPreview ? props.searchParams.p : props.params.slug;

  let client = isPreview ? await getAuthClient() : await getClient();

  if (!client) {
    return <PleaseLogin />;
  }

Code language: JavaScript (javascript)

Once that is done, we execute a WPGraphQL Query using the client.query from Apollo to fetch content by variables which are the id and the idType, and that differs based on whether it’s a preview or not. It asks for the title, content, and date of a content node.

const { data } = await client.query({
    query: gql`
      query GetContentNode(
        $id: ID!
        $idType: ContentNodeIdTypeEnum!
        $asPreview: Boolean!
      ) {
        contentNode(id: $id, idType: $idType, asPreview: $asPreview) {
          ... on NodeWithTitle {
            title
          }
          ... on NodeWithContentEditor {
            content
          }
          date
        }
      }
    `,
    variables: {
      id,
      idType: isPreview ? 'DATABASE_ID' : 'URI',
      asPreview: isPreview,
    },
  });

Code language: PHP (php)

Finally, the component the main content of the page inside a <main> tag, using the data from the WPGraphQL query to populate the title and the content (using dangerouslySetInnerHTML for the content to render HTML).

return (
    <main>
      <h2>{data?.contentNode?.title}</h2>
      <div
        dangerouslySetInnerHTML={{ __html: data?.contentNode?.content ?? '' }}
      />
    </main>
  );

Code language: JavaScript (javascript)

This setup allows the page to dynamically render the correct version of a post or page based on whether the user is requesting a preview or the live content.

Stoked!!!! Let’s see this work in action!

Conclusion

Faust.js with support for the App Router is a new version of the most used headless WP meta framework on top of Next.js 14. It introduces new ways to handle data, create routes and files as well as rendering methods.  Mix it with headless WordPress and WPGraphQL with Post Previews and you have an easy entry into its core functionality.   We hope you have a better understanding of how it all works together.

As always, stoked to hear your feedback and any questions you might have on headless WordPress! Hit us up in our Discord!