Display WordPress Posts on a Remix App

Adventures in web development continued…

❗ This is not a tutorial ❗

I’ve got a growing list of things to try on https://hello.zalgorithm.com. A lot of it depends on establishing a connection between this WordPress site (https://zalgorithm.com) and the Remix app (https://hello.zalgorithm.com).

Install the WPGraphQL plugin

To get things started, I’m going to install the WPGraphQL plugin on this site (https://zalgorithm.com.) I’m not sure if that plugin is intended to be used on WordPress sites that can still be accessed normally, or if it’s only intended to be used on headless WordPress sites. This is low consequence web development. I’ll just test it out and see what happens.

Hmmm, it looks safe:

WPGraphQL is a free, open-source WordPress plugin that provides an extendable GraphQL schema and API for any WordPress site.

From the plugin’s docs

The plugin’s installed and the site is still functioning normally. I’ve also got a WordPress site with the GraphQL plugin running at localhost on my home computer.

Tell Google to not index the remix test site

I don’t expect to get much of a boost from Google, but I’m not ready to trash this domain’s SEO yet. Since I’m going to be displaying this site’s posts on hello.zalgorithm.com, I’ll tell Google to not index any of the Remix site’s pages:

// app/root.tsx
// add a noindex meta tag to tell Google to not index the site

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="robots" content="noindex" />
        <Meta />
        <Links />
      </head>
      <body>
        <Header />
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

I think that will do the trick.

Configure Apollo Client

Some details to fill in later:

  • What is GraphQL?
  • Why use GraphQL to connect Remix to a headless WordPress site when I could just make requests to the WordPress Rest API?

The Apollo documentation is here: https://www.apollographql.com/. It’s worth going through this tutorial: https://www.apollographql.com/tutorials/lift-off-part1.

One thing to note is that I think their Remix docs are out of date: https://www.apollographql.com/blog/how-to-use-apollo-client-with-remix. In any case, I’m going to take a different approach. (There’s a topic on the Apollo forum that’s related to the issue: https://community.apollographql.com/t/how-to-load-data-with-react-router-dom-v6-and-handling-errorpage/5347/3.)

I’m tempted to start re-reading docs, but instead I’ll just jump right into this.

Install Apollo Client and Graphql on the Remix app

$ npm install graphql @apollo/client

For my own reference, these steps are loosely based on https://www.apollographql.com/tutorials/lift-off-part1/08-apollo-client-setup.

Generate GraphQL types

Because I’m using Typescript for the Remix app, I need to get the app to generate the Typescript types for all the GraphQL types that exist in the WordPress API. This can be done with a GraphQL Code Generator.

$ npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

After installing the packages, open the app’s package.json file and add a generate command to its scripts section:

// package.json

 "scripts": {
    "build": "remix build",
    "dev": "remix dev --manual",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "remix-serve ./build/index.js",
    "typecheck": "tsc",
    "generate": "graphql-codegen"
  },

Following the tutorial (https://www.apollographql.com/tutorials/lift-off-part1/09-codegen), running npm run generate now should return an error:

$ npm run generate
> generate
> graphql-codegen

Error: Unable to find Codegen config file! 

        Please make sure that you have a configuration file under the current directory!

To fix that:

$ touch codegen.ts

The fun starts here 🙂

I want to be able to run the same code on my production server as I’m running on my local server. Since the production site will have a different GraphQL server endpoint than the local site, I’ll set the server endpoint in the .env file (the .env file doesn’t get pushed to production.)

// .env

DATABASE_URL="file:./dev.db"
GRAPHQL_SERVER_ENDPOINT="http://wp-discourse.test/graphql"

The local WordPress site at http://wp-discourse.test is from a project that I’ve done a bit of work on.

It seems that the GRAPHQL_SERVER_ENDPOINT variable can be accessed with: process.env.GRAPHQL_SERVER_ENDPOINT. process is the NodeJS process. env is an object that contains the user’s environment.

For now, I’m going with this for the codegen.ts file:

// codegen.ts

import { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: process.env.GRAPHQL_SERVER_ENDPOINT,
  documents: ["./app/models/wp_queries.ts"],
  generates: {
    "./app/graphql/__generated__/": {
      preset: "client",
      presetConfig: {
        gqlTagName: "gql",
      },
    },
  },
  ignoreNoDocuments: true,
};

export default config;

I’ll try running the generate command again:

$ npm run generate

# returns an 'invalid config' error

Fixed with:

$ npm install dotenv

Then update the config file to import dotenv and call dotenv.config()

// codegen.ts

import { CodegenConfig } from "@graphql-codegen/cli";
import * as dotenv from "dotenv";
dotenv.config();

const config: CodegenConfig = {
  schema: process.env.GRAPHQL_SERVER_ENDPOINT,
  documents: ["./app/models/wp_queries.ts"],
  generates: {
    "./app/graphql/__generated__/": {
      preset: "client",
      presetConfig: {
        gqlTagName: "gql",
      },
    },
  },
  ignoreNoDocuments: true,
};

export default config;

For the codegen.ts file, it’s worth noting:

  • schema defines the GraphQL server endpoint
  • documents defines the documents that GraphQL Code Generator should use to generate types. (I’m not fully understanding that.)
  • generates tells the Code generator where to output the code it generates. For my config, it will create a graphql/_generated_ directory.
  • I’m not certain that my app will use the "client" preset that’s in the config file.

I’ll try generating the types:

$ npm run generate
> generate
> graphql-codegen

schema endpoint: http://wp-discourse.test/graphql
✔ Parse Configuration
✔ Generate outputs

That created a graphql/_generated_/graphql.ts file that contains type definitions for all the types that can be queried with the WPGraphQL API. For example:

// graphql/_generated_/graphql.ts

...
/** The category type */
export type Category = DatabaseIdentifier & HierarchicalNode & HierarchicalTermNode & MenuItemLinkable & Node & TermNode & UniformResourceIdentifiable & {
  __typename?: 'Category';
  /** The ancestors of the node. Default ordered as lowest (closest to the child) to highest (closest to the root). */
  ancestors?: Maybe<CategoryToAncestorsCategoryConnection>;
  /**
   * The id field matches the WP_Post-&gt;ID field.
   * @deprecated Deprecated in favor of databaseId
   */
  categoryId?: Maybe<Scalars['Int']['output']>;
  /** Connection between the category type and its children categories. */
  children?: Maybe<CategoryToCategoryConnection>;
  /** Connection between the Category type and the ContentNode type */
  contentNodes?: Maybe<CategoryToContentNodeConnection>;
  /** The number of objects connected to the object */
  
  ...
};

Create an Apollo Client

This is where things get away from the documentation that I’ve been able to find. I’m going to create an Apollo Client that can be used to make requests to WordPress:

$ mkdir lib
$ touch lib/createApolloClient.ts

I’m skipping through some of the logic here. Eventually I want to be able to make authenticated requests with a Bearer authToken to WordPress. This code makes it possible to call createApolloClient(authToken) to make authenticated queries to WordPress. (Details: https://github.com/wp-graphql/wp-graphql-jwt-authentication and https://remix.run/docs/en/main/utils/sessions.)

// lib/createApolloClient.ts

import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

// to pull in environmental variables (might try a different approach later)
import * as dotenv from "dotenv";
dotenv.config();

export function createApolloClient(authToken = "") {
  const httpLink = createHttpLink({
    uri: process.env.GRAPHQL_SERVER_ENDPOINT,
  });

  const authLink = setContext((_, { headers }) => {
    // Include the Authorization header only if authToken is provided
    const authHeader = authToken
      ? { authorization: `Bearer ${authToken}` }
      : {};
    return {
      headers: {
        ...headers,
        ...authHeader,
      },
    };
  });

  return new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
  });
}

Get some posts from WordPress

Initially the goal was to pull this exact post to WordPress, but this post doesn’t exist on my local dev site. It’s getting late and I’ve got something else to do tonight. To simplify things, I’ll just try to pull some posts from WordPress to Remix and display the post titles.

WPGraphQL adds an IDE to WordPress. It can be used to test GraphQL queries:

I’ll copy that query to app/models/wp_queries.ts:

// app/models/wp_queries.ts

import { gql } from "@apollo/client/core/index.js";

export const POSTS_QUERY = gql(`
query GetPosts {
    posts {
        nodes {
            id
            title
            content
            date
            slug
            excerpt
            author {
                node {
                    name
                }
            }
            featuredImage {
                node {
                    caption
                    description
                    id
                    sourceUrl
                }
            }
        }
    }
}
`);

This will be refined later, but for now I’ll just create a Remix /posts route:

$ touch app/routes/posts.tsx

This is rough, but what if it works 🙂

// app/routes/posts.tsx
import type { MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createApolloClient } from "lib/createApolloClient";
import type { Post } from "~/graphql/__generated__/graphql";
import { POSTS_QUERY } from "~/models/wp_queries";

export const meta: MetaFunction = () => {
  return [
    { title: "Posts" },
    { name: "description", content: "WordPress Posts" },
  ];
};

export async function loader() {
  const client = createApolloClient();
  const response = await client.query({
    query: POSTS_QUERY,
  });

  if (response.errors) {
    // todo: use an ErrorBoundary here to handle the error
    console.log("An error was returned from the Posts loader function.");
  }

  const posts = response?.data?.posts?.nodes;
  return posts;
}

export default function Posts() {
  const posts = useLoaderData<typeof loader>();
  return (
    <main className="max-w-prose mx-auto">
      <header>
        <h2 className="text-3xl font-serif py-1">Posts</h2>
      </header>
      <ul>
        {posts.map((post: Post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

Restarting the server returns:

Error: Directory import '/home/scossar/remix/starting-over/hello_world/node_modules/@apollo/client/link/context'
is not supported resolving ES modules imported from /home/scossar/remix/starting-over/hello_world/build/index.js

That’s not a surprise. I’ll fix the imports in createApolloClient.ts

// lib/createApolloClient.ts

import { ApolloClient, createHttpLink } from "@apollo/client/core/index.js";
import { InMemoryCache } from "@apollo/client/cache/index.js";
import { setContext } from "@apollo/client/link/context/index.js";

...

Success 🙂

I’ve got 17 minutes. Going to get rid of the serif font in the title and push these changes to production.

$ git add .
$ git commit -m "Connect to WordPress; Pull in WordPress posts."
$ git push live main

On the production server:

$ cd /var/www/hello_zalgorithm_com
$ emacs .env
// .env (note, there's nothing sensitive in here
// on a real production site I'd approach this differently.)

DATABASE_URL="file:./prod.db"
GRAPHQL_SERVER_ENDPOINT="https://zalgorithm.com/graphql"

Build and reload the app:

$ npm install
$ npm run build
$ pm2 reload 0

Success 🙂

https://hello.zalgorithm.com/posts