More House Cleaning

Continuing from yesterday:

  • move the header back to the root route
  • improve meta tags on some routes
  • add a custom favicon
  • remove references to “uncategorized” category
  • prevent Remix app from crashing when WordPress is down

New items:

  • change “Previous” and “Next” text used for post navigation to “Older” and “Newer”
  • Fix bug in Previous/Next (older/newer) post links!!!
  • improve alignment of older/newer post links

Maybe I should stop there. What’s going on with previous/next navigation?

Previous/next navigation bug

$ git checkout -b fix_previous_next_navigation_bug

I’ll start by changing the navigation link’s text. Labeling the links with “Previous” and “Next” is throwing me off. The labels mean previous and next in time (older/newer.)

Here’s the “bug”:

That’s not really a bug 🙂 There are two posts on my dev site with the title “Modular Synths: Exploring the Possibilities of Sonic Architecture”. The content on my development site was written by ChatGPT. I used it to quickly generate some posts, then copied the same title and text into two WordPress posts. I’ll delete the duplicate post and make sure things are working as expected.

That seems to have fixed the issue 🙂

At the risk of creating an actual bug, I’m going to move the (newly renamed) older/newer navigation from the bottom of the /blog/posts and /blog/category/$categorySlug/$postId/$slug routes into a component. I’m wary of being too quick to make code “dry.” I think it’s justified for this case though.

$ touch app/components/OlderNewerPostsNavigation.tsx

Copying the relevant code from blog.$postId.$slug into the new component, Typescript’s warnings tell me that the following props are going to have to be passed to the component:

  • previousTitle
  • previousSlug
  • previousId
  • nextTitle
  • nextSlug
  • nextId

Navigation links on category archive pages also require the a categorySlug. I’ll use a basePath prop (either /blog or /blog/category/$categorySlug) to pass the category slug.

Typescript isn’t speeding me up yet, but it’s starting to make sense. The OlderNewerPostNavigation component uses this interface for its props:

export interface PostNavigationProps {
  previousPost: Maybe<Post>;
  nextPost: Maybe<Post>;
  basePath: string;
}

Routes that use the component can import the type:

import type { PostNavigationProps } from "~/components/OlderNewerPostNavigation";

Then call the component with:

export default function BlogPost() {
// ...
  const { post, categorySlug } = useLoaderData<typeof loader>();
// ...
  const postNavigationProps: PostNavigationProps = {
    previousPost: post?.previousPostInCategory,
    nextPost: post?.nextPostInCategory,
    basePath: categorySlug ? `/blog/category/${categorySlug}` : "/blog",
  };
// ...
return (
// ...
      <OlderNewerPostNavigation {...postNavigationProps} />
    </div>
  );
}

Here’s the full component:

OlderNewerPostNavigation.tsx
// app/components/OlderNewerPostNavigation.tsx

import { Link } from "@remix-run/react";
import type { Post } from "~/graphql/__generated__/graphql";
import { Maybe } from "graphql/jsutils/Maybe";
import { Icon } from "~/components/Icon";

export interface PostNavigationProps {
  previousPost: Maybe<Post>;
  nextPost: Maybe<Post>;
  basePath: string;
}

export default function OlderNewerPostNavigation({
  previousPost,
  nextPost,
  basePath,
}: PostNavigationProps) {
  return (
    <div className="my-3 flex justify-between  flex-col min-[431px]:flex-row">
      {previousPost?.title && previousPost?.slug && previousPost?.databaseId ? (
        <div>
          <Link
            prefetch="intent"
            to={`${basePath}/${previousPost.databaseId}/${previousPost.slug}`}
            className="text-lg font-bold text-sky-700 hover:underline"
          >
            <div className="flex items-center">
              {" "}
              <Icon
                key="arrow-left"
                id="arrow-left"
                x={-5}
                className="text-slate-700 w-10 h-10 self-center"
              />{" "}
              <div className="text-slate-700">Older</div>
            </div>
            {previousPost.title}
          </Link>
        </div>
      ) : null}
      {nextPost?.title && nextPost?.slug && nextPost?.databaseId ? (
        <div className="min-[431px]:text-right">
          <Link
            prefetch="intent"
            to={`${basePath}/${nextPost.databaseId}/${nextPost.slug}`}
            className="text-lg font-bold text-sky-700 hover:underline"
          >
            <div className="flex items-center min-[431px]:justify-end">
              <div className="text-slate-700">Newer</div>
              <Icon
                key="arrow-right"
                id="arrow-right"
                x={5}
                className="text-slate-700 inline w-10 h-10"
              />{" "}
            </div>
            {nextPost.title}
          </Link>
        </div>
      ) : null}
    </div>
  );
}

I’m undecided about changing the link text from “previous/next” to “older/newer.” I’ll leave it for a while and see if it makes sense.

Moving on, the next item on the list isn’t on the list:

Increase header menu button size

Last night I tested the site on my phone. The menu button is too small to click. The drop down menu is also too small. It’s difficult to select a single item from it.

$ git checkout -b fix_menu_icon_size

The fix was to add a height to the header’s inner container, then set the width and height of the hamburger icon to the container’s height. I’ll probably change this a few times, so I set height and width as variables:

const containerHeightClass: string = "h-14";
const hamburgerWidthClass: string = "w-14";

I also added an event listener to click events that occur outside of the details element. It’s used to trigger the menu to close:

useEffect(() => {
  const detailsElement = document.getElementById("blog-nav");
  const handleClickOutside = (event: MouseEvent) => {
    const target = event.target as Node;
    if (detailsElement && !detailsElement.contains(target)) {
      detailsElement.removeAttribute("open");
    }
  };
   document.addEventListener("click", handleClickOutside);
   return () => {
    document.removeEventListener("click", handleClickOutside);
  };
}, []);

It works!

BlogHeader.tsx
// app/components/BlogHeader.tsx

import { Link, NavLink, useLocation } from "@remix-run/react";
import { useEffect } from "react";
import { Maybe } from "graphql/jsutils/Maybe";
import { type RootQueryToCategoryConnection } from "~/graphql/__generated__/graphql";
import { Icon } from "~/components/Icon";

interface BlogHeaderProps {
  categories: Maybe<RootQueryToCategoryConnection>;
}

export default function BlogHeader({ categories }: BlogHeaderProps) {
  const location = useLocation();
  const containerHeightClass: string = "h-14";
  const hamburgerWidthClass: string = "w-14";

  useEffect(() => {
    const detailsElement = document.getElementById("blog-nav");
    if (detailsElement) {
      detailsElement.removeAttribute("open");
    }
  }, [location]);

  useEffect(() => {
    const detailsElement = document.getElementById("blog-nav");
    const handleClickOutside = (event: MouseEvent) => {
      const target = event.target as Node;
      if (detailsElement && !detailsElement.contains(target)) {
        detailsElement.removeAttribute("open");
      }
    };

    document.addEventListener("click", handleClickOutside);

    return () => {
      document.removeEventListener("click", handleClickOutside);
    };
  }, []);

  return (
    <header className="bg-sky-800 text-slate-50 px-3 py-2 top-0 sticky overflow-visible">
      <div
        className={`flex justify-between items-center w-full max-w-screen-xl mx-auto relative ${containerHeightClass}`}
      >
        <h1>
          <Link to="/" className="text-3xl">
            Zalgorithm
          </Link>
        </h1>
        <div className={`relative ${containerHeightClass}`}>
          <details
            className="cursor-pointer absolute top-0 right-0 z-10"
            id="blog-nav"
          >
            <summary className="_no-triangle block absolute right-0 top-0 list-none">
              <Icon
                key="hamburger"
                id="hamburger"
                className={`text-slate-50 rounded hover:bg-sky-700 hover:outline hover:outline-sky-700 hover:outline-4 hover:outline-solid ${hamburgerWidthClass} ${containerHeightClass}`}
              />
            </summary>
            <ul className="bg-slate-50 text-slate-800 text-lg p-3 rounded relative top-12 right-3 shadow-lg w-56 divide-slate-300 divide-y">
              <NavLink
                key="posts"
                to="/blog/posts"
                className={({ isActive, isPending }) =>
                  `py-1 pl-1 block hover:bg-slate-200 ${
                    isPending
                      ? "pending"
                      : isActive
                      ? "text-sky-700 font-medium bg-slate-200"
                      : ""
                  }`
                }
              >
                <li>all posts</li>
              </NavLink>
              {categories?.nodes
                ? categories.nodes.map((category, index) => (
                    <NavLink
                      key={index}
                      to={`/blog/category/${category.slug}`}
                      className={({ isActive, isPending }) =>
                        `py-1 pl-1 block hover:bg-slate-200 ${
                          isPending
                            ? "pending"
                            : isActive
                            ? "text-sky-700 font-medium bg-slate-200"
                            : ""
                        }`
                      }
                    >
                      <li>{category.name}</li>
                    </NavLink>
                  ))
                : ""}
            </ul>
          </details>
        </div>
      </div>
    </header>
  );
}

That’s good for today 🙂