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 endpointdocuments
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 agraphql/_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->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 🙂