❗ This is not a tutorial ❗
The next step is to link each of the top level headings on the /blog
page to index pages that display all of the posts that fall under the heading.
Before adding the new index pages, I’m going to update the blog.$slug.tsx
route to add Open Graph meta tags to the blog post page’s headers.
Open Graph meta tags
I’m doing this now because it’s similar to what I did yesterday (this links to the WordPress site for now) to add title
and description
meta tags to the blog post headers.
The first task is to move the truncateText
and stripHtml
functions from the blog.$slug.tsx
file to a utilities file:
$ touch app/utils/utilities.ts
// app/utils/utilities.ts
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 1) + "…";
}
export function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, "");
}
// app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
import { createApolloClient } from "lib/createApolloClient";
import { POST_BY_SLUG_QUERY } from "~/models/wp_queries";
import type { Post } from "~/graphql/__generated__/graphql";
import { stripHtml, truncateText } from "~/utils/utilities";
...
Now update the meta
function in blog.$slug.tsx
to generate meta
tags with the following properties:
og:title
og:description
og:image
The meta
function is already setting title
and description
variables. I’ll update it to set an imageUrl
variable for posts that have a featuredImage
property and add the Open Graph properties:
// app/routes/blog.$slug.tsx
...
export const meta: MetaFunction = ({ data }) => {
const post = data as Post;
const title = post?.title ? post.title : "Simon's blog";
let description = post?.excerpt
? stripHtml(post.excerpt)
: `Read more about ${post.title}`;
description = truncateText(description, 160);
const imageUrl = post?.featuredImage?.node?.sourceUrl
? post.featuredImage.node.sourceUrl
: "";
return [
{ title: title },
{ description: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:image", content: imageUrl },
];
};
That seems to do the trick:
I’ll push the changes to the production server to confirm.
I pushed the changes to the live server and checked the results on the Facebook Sharing Debugger (https://developers.facebook.com/tools/debug/
,) the title, description, and image properties are being pulled in, but there’s a warning about the following missing properties: og:url
, og:type
, fb:app_id
. I won’t bother with the fb:app_id
property (I just checked this on Facebook for convenience,) but I’ll add the other missing properties. (The Open Graph documentation is here: https://ogp.me/.)
// app/routes/blog.$slug.tsx
...
export const meta: MetaFunction = ({ data }) => {
const post = data as Post;
const title = post?.title ? post.title : "Simon's blog";
let description = post?.excerpt
? stripHtml(post.excerpt)
: `Read more about ${post.title}`;
description = truncateText(description, 160);
// todo: set BASE_URL as an environental variable so that it doesn't have to be hard coded here:
const url = post?.slug
? `https://hello.zalgorithm.com/blog/${post.slug}`
: "";
let metaTags = [
{ title: title },
{ description: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:type", content: "website" },
{ property: "og:url", content: url },
];
if (post?.featuredImage?.node?.sourceUrl) {
metaTags.push({
property: "og:image",
content: post.featuredImage.node.sourceUrl,
});
}
return metaTags;
};
...
Posts shared on social media (or on any applications that support Open Graph tags) are looking good:
A brief diversion
After pushing the latest changes to the production site, I ran npm run dev
on the production server. I’d meant to run npm run build
!!! That messed things up. Visiting https://hello.zalgorithm.com
returned an “unexpected server error
” message.
It’s this kind of thing that can make professional programming a stressful job.
I think that what fixed it was stopping the process that was running on port 3000
:
$ sudo lsof -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 163865 scossar 21u IPv6 1927037 0t0 TCP *:3000 (LISTEN)
$ kill -9 163865
Then running:
$ pm2 stop 0
$ pm2 start 0
The issue was likely related to a port conflict. The production app is expected to be running on port 3000
. (As outlined here, I’ve configured an Nginx reverse proxy to route traffic to the app on port 3000
.) Running npm run dev
attempted to start another process on port 3000
.
After getting the things back online, I figured I should refine the deploy process.
Running pm2 list
was returning confusing data:
$ pm2 list
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ npm start │ default │ N/A │ fork │ 163422 │ 16m │ 28 │ online │ 0% │ 58.3mb │ scossar │ disabled
The confusing part was that the process name was “npm start.” That didn’t seem right. It was caused by having initially started the process with `pm2 start “npm start” -i max`. That command didn’t explicitly set a name for the process, so the command that was supplied ("npm start")
was set as the process name.
To fix this, I stopped the process with:
$ pm2 stop 0
I then started the process with an explicit name:
$ pm2 start "npm start" --name "zalgorithm" -i max
pm2 list
now returns more meaningful data:
$ pm2 list
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ zalgorithm │ default │ N/A │ fork │ 163853 │ 112s │ 28 │ online │ 0% │ 58.8mb │ scossar │ disabled
Now I can run pm2 reload zalgorithm
to reload the app after building it.
I’m going to see if I can automate the build/deploy process a bit. I’m pushing changes to the app from my local computer to the production server with:
$ git push live main
“live"
is a git repo that I’ve configured on the production server. In the live
repo’s /hooks
directory, I’ve created a post-receive
file:
#!/bin/sh
git --work-tree=/var/www/hello_zalgorithm_com --git-dir=/var/repo/site.git checkout -f
(See https://www.digitalocean.com/community/tutorials/how-to-set-up-automatic-deployment-with-git-with-a-vps for details.)
It seems that the post-receive
script should be able to handle the following commands:
npm install
npm run build
pm2 reload zalgorithm
Technically, something like this should work for the post-receive
file:
#!/bin/sh
# Define the directory where the app lives
APP_DIR=/var/www/hello_zalgorithm_com
REPO_DIR=/var/repo/site.git
# Checkout the latest code
git --work-tree=$APP_DIR --git-dir=$REPO_DIR checkout -f
# Change to the app directory
cd $APP_DIR
# Install dependencies and build the project
npm install
npm run build
# Restart the specific pm2 process
pm2 restart zalgorithm
but it doesn’t. When I log the output, it’s failing at the npm install
, npm run build
and pm2 restart zalgorithm
commands:
Running npm install
hooks/post-receive: 13: npm: not found
Running npm run build
hooks/post-receive: 16: npm: not found
Reloading pm2 process
hooks/post-receive: 20: pm2: not found
This is probably related to the environment that the post-receive
hook is running in being different from the environment that I’m in when I run the npm
and pm2
commands directly from the /var/www/hello_zalgorithm_com
directory. I’m going to leave this for now.
After trying that, I’d like to do something that’s easily achievable. How about…
Styling block quotes in WordPress posts
Currently, WordPress block quotes on the Remix app look like regular text:
That section has the following markup:
<blockquote class="wp-block-quote">
<p>WPGraphQL is a free, open-source WordPress plugin that provides an extendable GraphQL schema and API for any WordPress site.</p>
<cite>From the plugin’s docs</cite>
</blockquote>
I’ll go with the following styles for now:
// app/tailwind.css
...
.wp-post .wp-block-quote {
@apply border-l border-slate-600 pl-4 font-medium;
}
.wp-post .wp-block-quote cite {
@apply font-normal text-sm italic;
}
...
That’s an improvement:
I’ll make a start on what was supposed to be today’s task:
Add an Archive index route
The “Latest Posts” section of the blog’s index page only lists the 5 most recent posts. I’m going to put a link to an “Archive” route at the bottom of that section:
I’m not sure what the link’s text will be. “Archive” seems too obscure. Maybe “Read All Posts”? In any case, the route’s going to be called archive
in the code. It will be a child of the blog
route:
$ touch app/routes/blog.archive.tsx
// app/routes/blog.archive.tsx
export default function Archive() {
return (
<div>
<h2>Latest</h2>
</div>
);
}
blog._index.tsx
needs to be updated to link to that page:
// app/routes/blog._index.tsx
// I've added the Link import here:
import { json, MetaFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
...
// then added the link here:
export default function BlogIndex() {
const { latestPostsEdges, categoryEdges } = useLoaderData<typeof loader>();
return (
<div className="px-6 mx-auto max-w-screen-lg">
<h2 className="text-3xl text-slate-900 mt-3 font-serif font-bold">
Latest Posts
</h2>
<hr className="mt-2 mb-3 border-solid border-slate-900" />
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{latestPostsEdges.map((edge: PostConnectionEdge) => (
<PostExcerptCard
title={edge.node?.title}
date={edge.node?.date}
featuredImage={edge.node?.featuredImage?.node?.sourceUrl}
excerpt={edge.node?.excerpt}
authorName={edge.node?.author?.node?.name}
slug={edge.node?.slug}
key={edge.node.id}
/>
))}
<Link
className="text-2xl text-sky-700 font-medium hover:underline"
prefetch="intent"
to="archive"
>
See All Latest Posts
</Link>
<hr className="mt-2 mb-2 border-solid border-slate-900" />
</div>
...
The design here is questionable, but I’ll go with it for now:
Query for the latest posts
Pulling in the latest posts is going to require multiple queries. Posts will be loaded in batches of 15. After loading the first 15 posts, if there are more posts in the database, a “Load More” button will be displayed at the bottom of the list. Clicking that button will load the next batch of posts.
Details about how to paginate queries with the WPGraphQL plugin are here: https://www.wpgraphql.com/docs/connections#solution-for-pagination. Note that (I think) there’s a typo in the example queries on that page. When I try to run this query in the GraphiQL IDE on my WordPress site:
{
posts {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
nodes {
id
title
}
}
}
}
I get the error: "Cannot query field \"nodes\" on type \"RootQueryToPostConnectionEdge\". Did you mean \"node\"?"
I think the query is meant to use node
(the post node) not nodes
:
{
posts {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
title
}
}
}
}
I’ll start with this:
{
posts (first: 15, where: {orderby: {field:DATE, order: DESC}}) {
pageInfo {
hasNextPage
}
edges {
cursor
node {
id
title
date
slug
excerpt
author {
node {
name
}
}
featuredImage {
node {
altText
sourceUrl
}
}
}
}
}
}
The data that’s returned looks like this:
{
"data": {
"posts": {
"pageInfo": {
"hasNextPage": true
},
"edges": [
{
"cursor": "YXJyYXljb25uZWN0aW9uOjEzMw==",
"node": {
"id": "cG9zdDoxMzM=",
"title": "link to posts from the index page (local)",
"date": "2024-02-27T02:21:06",
"slug": "link-to-posts-from-the-index-page-local",
"excerpt": "<p>A small update from yesterday. Viewing the production site on mobile, I noticed that long links found in excerpts could cause content to extend past its container. I’ve fixed that by adding a .wp-excerpt class to the excerpt’s outer div element, and then adding the following to the tailwind.css file: The Tailwind @layer and @apply […]</p>\n",
"author": {
"node": {
"name": "scossar"
}
},
"featuredImage": null
}
},
...
"cursor": "YXJyYXljb25uZWN0aW9uOjExOA==",
"node": {
"id": "cG9zdDoxMTg=",
"title": "Mountain Biking in Nanaimo: Gear Essentials for the Trails",
"date": "2024-02-25T01:33:23",
"slug": "mountain-biking-in-nanaimo-gear-essentials-for-the-trails",
"excerpt": "<p>Mountain biking in Nanaimo offers an exhilarating blend of adventure and natural beauty. Nestled on Vancouver Island, Nanaimo’s diverse landscapes provide the perfect backdrop for mountain biking enthusiasts. From challenging technical trails and steep descents to scenic forest routes and coastal paths, there’s something for riders of all levels. The area is renowned for its […]</p>\n",
"author": {
"node": {
"name": "scossar"
}
},
"featuredImage": null
}
}
]
}
},
"extensions": {
"debug": [],
"queryAnalyzer": {
"keys": "2df0fa5ece90ea37aed3e19d7b08cb459e6aeb48f77a078805bef04e89bee6ac graphql:Query list:post cG9zdDoxMzM= cG9zdDoxMjQ= cG9zdDoxMjI= cG9zdDoxMjA= cG9zdDoxMTg= dXNlcjox cG9zdDoxMQ== cG9zdDoxMjY= cG9zdDoxMjc=",
"keysLength": 201,
"keysCount": 12,
"skippedKeys": "",
"skippedKeysSize": 0,
"skippedKeysCount": 0,
"skippedTypes": []
}
}
}
pageInfo.hasNextPage
will be set to true
if there are more posts in the database. The next batch of posts can then be obtained by using the value of the cursor
from the last post in the batch. For example:
{
posts (first: 15, after: "YXJyYXljb25uZWN0aW9uOjExOA==", where: {orderby: {field:DATE, order: DESC}}) {
pageInfo {
hasNextPage
}
edges {
cursor
node {
id
title
date
slug
excerpt
author {
node {
name
}
}
featuredImage {
node {
altText
sourceUrl
}
}
}
}
}
}
The query that’s added to the Remix app at app/models/wp_queries.ts
is going to need to accept an after
variable. Testing the query in the GraphiQL IDE, it seems that the value of after
can be set to an empty string. That will simplify things for dealing with the initial query.
Maybe this will work:
// app/models/wp_queries.ts
export const ARCHIVE_POSTS_QUERY = gql(`
query ArchivePosts($after: String!) {
posts (first: 15, after: $after, where: {orderby: {field:DATE, order: DESC}}) {
pageInfo {
hasNextPage
}
edges {
cursor
node {
id
title
date
slug
excerpt
author {
node {
name
}
}
featuredImage {
node {
altText
sourceUrl
}
}
}
}
}
}
`);
I’ll see if I can copy some of the code from blog._index.tsx
to get this to work:
// app/routes/blog.archive.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createApolloClient } from "lib/createApolloClient";
import { ARCHIVE_POSTS_QUERY } from "~/models/wp_queries";
import type { PostConnectionEdge } from "~/graphql/__generated__/graphql";
import PostExcerptCard from "~/components/PostExcerptCard";
export async function loader() {
const client = createApolloClient();
const response = await client.query({
query: ARCHIVE_POSTS_QUERY,
variables: {
after: "",
},
});
if (response.errors) {
// todo: handle the errors
console.log("An error was returned from the ARCHIVE_POSTS_QUERY");
}
const data = response?.data;
const pageInfo = data?.posts?.pageInfo;
const postEdges = data?.posts?.edges;
// don't worry about errors for now
return json({
pageInfo: pageInfo,
postEdges: postEdges,
});
}
export default function Archive() {
const { pageInfo, postEdges } = useLoaderData<typeof loader>();
return (
<div className="px-6 mx-auto max-w-screen-lg">
<h2 className="text-3xl text-slate-900 mt-3 font-serif font-bold">
Archive
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{postEdges.map((edge: PostConnectionEdge) => (
<PostExcerptCard
title={edge.node?.title}
date={edge.node?.date}
featuredImage={edge.node?.featuredImage?.node?.sourceUrl}
excerpt={edge.node?.excerpt}
authorName={edge.node?.author?.node?.name}
slug={edge.node?.slug}
key={edge.node.id}
/>
))}
</div>
</div>
);
}
That was easy:
I’d better stop for the night. Tomorrow’s project will be to figure out how to add a “Load More Posts” button to the page if PageInfo.hasNextPage
is true
. The next batch of posts will be loaded from the value of the cursor
from the current batch’s last post.
I’m going to push today’s work to the production site.