Authentication (part one)

This is definitely not a tutorial and doesn’t have much to say about authentication. It’s a long post, unedited. There’s information here that I’ll want to come back to.


What I’ve done so far can be classified as low consequence web development – the worst possible outcome is that I’ll push a change that crashes the site. Adding an authentication system changes that.

I’m adding it now because I want to put a /music route on the non-WordPress part of the site. The goal is to create a system for managing and sharing music files that I’ve got on a Digital Ocean space. I want to be able to order, rename, rate the files on the Remix site. That requires me to be able to log into the site with the role “admin.”

The headless WordPress site is going to function as the authentication provider. The WPGraphQL JWT Authentication plugin will be used to authenticate users on the Remix site with JSON Web Tokens. Remix sessions will be used to determine a user’s login state.

I set this up locally a few months ago. I’m fairly confident about the approach. That said, if anyone can figure out how to hack the site, get in touch with me and I’ll send you $100 (the poor man’s Hackerone.)

Easing into this, the first thing that’s needed has nothing to do with authentication. I want to add a “login” button to the site’s header. It should be visible on all routes. This will be easier to do if the site has one Header component on its root route than with the current two header setup.

(Somewhat related, I’ve always pronounced “route” to rhyme with “shoot” or “boot,” not to rhyme with “shout” or “about.” Remix’s “root route” has changed that. I’m Canadian BTW.)

Create a site wide header

RIght now I’ve got a SiteHeader component on the _index.tsx route, and a BlogHeader component on the blog.tsx route. The difference between the two (so far) is that the blog’s category menu is only displayed on the BlogHeader.

This works well enough, but it’s going to get complex as I add more elements to the header. So the plan is to only have a single header for all the app’s routes.

The goal is to get different elements to display on the header, depending on where the user is on the site.

Possible options (I don’t know what I’m doing):

  • Add all elements to the root.tsx header, then use useMatches on the root route to add route specific CSS classes to the body tag. Those classes could be used to conditionally hide and display header elements. This feels like a bit of a hack. Especially because some data on the header is pulled into the site via API calls to WordPress.
  • Maybe this is what React Composition is for?

I asked ChatGPT and it suggested useOutletContext. At this point I know about as much about it as what’s on that page – not much, but here it goes.

$ git checkout -b site_header
$ touch app/components/Header.tsx

To get started:

// app/components/Header.tsx

import { Link } from "@remix-run/react";

export default function BlogHeader() {
  const containerHeightClass: string = "h-14";
  return (
    <header className="bg-sky-800 text-slate-50 px-3 py-2 top-0 sticky">
      <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}`}></div>
      </div>
    </header>
  );
}

The Remix docs say useOutletContext is a convenience API over React Context. Hmm, I try the Remix doc’s example code.

I’m guessing here. Outlet renders the matching child route of a parent route. The Outlet on root.tsx is the parent route of all the site’s routes. Outlet accepts a context prop: <Outlet context={myContextValue} />. This seems like the opposite of what I’m trying to accomplish, but I’ll keep going:

// app/root.tsx

//...
export default function App() {
  return (
    <Document>
      <div className="flex-1">
        <Header />
        <Outlet context="foo"/>
      </div>
      <ScrollRestoration />
      <Scripts />
      <LiveReload />
      <Footer />
    </Document>
  );
// ...
// app/routes/_index.tsx

import { useOutletContext } from "@remix-run/react";

export default function Index() {
  const bar = useOutletContext();
  return (
    <div>
      <div className="sm:max-w-prose mx-auto my-2 px-3">
        <h2 className="text-3xl my-1">Hello</h2>
        <p className="my-2">
          The value of <code>bar</code> is {bar}
        </p>
      </div>
    </div>
  );
}

// ...

// Problems:
// Type 'unknown' is not assignable to type 'ReactNode'.

Back to dealing with type unknown. I wonder if there’s a <typeof functions_return_val> way of dealing with that. For now:

const bar = useOutletContext<string>();

The string “foo” is output as expected. That’s neat, but I’m not seeing how it can be used for composition yet.

Maybe move the Header component to _index.tsx so it’s a child of the root route? Then:

$ touch app/components/BlogMenu.tsx

Create a BlogMenu component:

// app/components/BlogMenu.tsx

export default function BlogMenu() {
  return (
    <>
      <details className="text-slate-50">
        <summary>blog menu</summary>
        <ul className="bg-slate-600">
          <li>this</li>
          <li>that</li>
          <li>and</li>
          <li>the other</li>
        </ul>
      </details>
    </>
  );
}

To test things out, just pass { blogPage: true } from the root route’s Outlet:

// app/root.tsx

//...
export interface HeaderProps {
  blogPage: boolean;
}
//...
export default function App() {
  const headerProps: HeaderProps = {
    blogPage: true,
  };
  return (
    <Document>
      <div className="flex-1">
        <Outlet context={headerProps} />
      </div>
      <ScrollRestoration />
      <Scripts />
      <LiveReload />
      <Footer />
    </Document>
  );
}
//...

Then useOutletContext in the Header component:

// app/components/Header.tsx

import { Link, useOutletContext } from "@remix-run/react";
import BlogMenu from "~/components/BlogMenu";
import type { HeaderProps } from "~/root";

export default function BlogHeader() {
  const { blogPage } = useOutletContext<HeaderProps>();
  const containerHeightClass: string = "h-14";
  return (
    <header className="bg-sky-800 text-slate-50 px-3 py-2 top-0 sticky">
      <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}`}>
          {blogPage ? <BlogMenu /> : ""}
        </div>
      </div>
    </header>
  );
}

That works! Set blogPage to true:

  const headerProps: HeaderProps = {
    blogPage: true,
  };

Set blogPage to false:

Great! I guess the “composition” part of this is that the Header component is now composed of the Header and BlogMenu components?

The tricky part is to figure out how to set the blogPage boolean.

Here’s a test:

// app/root.tsx

//...
export default function App() {
  const matches = useMatches();
  console.log(`matches in the root route: ${JSON.stringify(matches, null, 2)}`);
//...
// app/components/Header.tsx

//...
export default function BlogHeader() {
  const matches = useMatches();
  console.log(
    `matches in the Header component: ${JSON.stringify(matches, null, 2)}`
  );

From the index page, that outputs:

matches in the root route: [
  {
    "id": "root",
    "pathname": "/",
    "params": {},
    "data": null
  },
  {
    "id": "routes/_index",
    "pathname": "/",
    "params": {},
    "data": null
  }
]
matches in the Header component: [
  {
    "id": "root",
    "pathname": "/",
    "params": {},
    "data": null
  },
  {
    "id": "routes/_index",
    "pathname": "/",
    "params": {},
    "data": null
  }
]

The output’s identical! That means I don’t need to pass data from root.tsx to the Header component. I can just add the header to root.tsx and use the last match returned from useMatches() in the Header component to figure out if it’s on the blog route or any of its children:

// app/components/Header.tsx

import { Link, useMatches } from "@remix-run/react";
import BlogMenu from "~/components/BlogMenu";

export default function Header() {
  const matches = useMatches();
  const path: string = matches.slice(-1)?.[0].pathname;
  const pathParts: string[] = path.split("/");
  const blogPage: boolean = pathParts.includes("blog");

  const containerHeightClass: string = "h-14";
  return (
    <header className="bg-sky-800 text-slate-50 px-3 py-2 top-0 sticky">
      <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}`}>
          {blogPage ? <BlogMenu /> : ""}
        </div>
      </div>
    </header>
  );
}

The BlogMenu component requires WordPress category details to create its menu. I’m not sure what’s the best way to pass the category details to the component. For now, I’ll add a loader function to root.tsx (where the menu is rendered) and use it to get the categories to the component:

// app/root.tsx

//...
import { createApolloClient } from "lib/createApolloClient";
import { CATEGORIES_DETAILS_QUERY } from "./models/wp_queries";

import Footer from "~/components/Footer";
import Header from "~/components/Header";
import styles from "./tailwind.css";

export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];

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

  if (response.errors) {
    // probably don't throw an error here
  }

  return response?.data?.categories;
};

//...

export default function App() {
  const categories = useLoaderData<typeof loader>();
  return (
    <Document>
      <div className="flex-1">
        <Header categories={categories} />
        <Outlet />
      </div>
      <ScrollRestoration />
      <Scripts />
      <LiveReload />
      <Footer />
    </Document>
  );
}
//...

Then add a categories prop to the Header component. The Header component will then pass the categories to the BlogMenu component:

// app/components/Header.tsx

import { Link, useMatches } from "@remix-run/react";
import BlogMenu from "~/components/BlogMenu";

import { type RootQueryToCategoryConnection } from "~/graphql/__generated__/graphql";
import { Maybe } from "graphql/jsutils/Maybe";

interface HeaderProps {
  categories: Maybe<RootQueryToCategoryConnection>;
}

export default function Header({ categories }: HeaderProps) {
  const matches = useMatches();
  const path: string = matches.slice(-1)?.[0].pathname;
  const pathParts: string[] = path.split("/");
  const blogPage: boolean = pathParts.includes("blog");

  const containerHeightClass: string = "h-14";
  return (
    <header className="bg-sky-800 text-slate-50 px-3 py-2 top-0 sticky">
      <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}`}>
          {blogPage ? <BlogMenu categories={categories} /> : ""}
        </div>
      </div>
    </header>
  );
}

And BlogMenu will accept the categories as a prop:

// app/components/BlogMenu.tsx

//...
interface BlogMenuProps {
  categories: Maybe<RootQueryToCategoryConnection>;
}

export default function BlogMenu({ categories }: BlogMenuProps) {
//...

This seems to work, but why?

I think that what’s going on is that the categories data is passed from the server to the client when the root route is visited. It’s in the client’s memory, attached to the React component tree.

A render tree represents a single render pass of a React application. With conditional rendering, a parent component may render different children depending on the data passed.

React docs

Those docs are worth a read. I’m getting the sense that the more I understand about React, the better I’ll understand Remix. For context, I’ve worked a lot with PHP/WordPress and Ruby/Ruby on Rails. I have some experience with using Ember as the front end for a Rails app. The React/Remix world is new to me.

I’ll merge the site_header branch into the main branch now, to avoid things getting too out of sync.

Header links

There needs to be a way to get from the homepage to the blog:

I’ll add a couple more routes to the app while I’m at it:

$ touch app/routes/hire-me.tsx
// app/routes/hire-me.tsx

export default function hireMe() {
  return (
    <>
      <h2>Hire Me</h2>
      <p>Soon... let me finish this site first.</p>
    </>
  );
}
$ touch app/routes/music.tsx
// app/routes/music.tsx

export default function Music() {
  return (
    <>
      <h2>All Tracks</h2>
      <p>Coming soon...</p>
    </>
  );
}

The new routes highlight something that needs to be fixed. The root route is the parent route of all the app’s routes. It should be setting site-wide left and right margins. Otherwise I’m likely to end up with inconsistent margins between the routes. (I’ll deal with that later.)

… 12 hours later

I’ll add a couple more routes to the app while I’m at it

me (yesterday)

That opened up a can of worms. Having the blog at /blog instead of the root of the site (/) adds complexity to the site’s structure. The idea is that the site will contain multiple applications:

  • a blog for WordPress posts
  • a music section for audio files hosted on Digital Ocean

So the site will have two or more top level sections. Each section should have a similar structure, in the same way as branches of a single tree share the same structure.

The problem came when I tried to represent this kind of structure in the Header component. There’s just not enough room on mobile devices.

This got me thinking about adding a side panel to the app.

A bit of learning

Basic pattern to toggle a boolean on the client:

import { useState } from "react";

export default function Scratch() {
  const [isPanelOpen, setIsPanelOpen] = useState(false);
  const togglePanel = () => setIsPanelOpen(!isPanelOpen);
  console.log(`isPanelOpen: ${isPanelOpen}`);

  return (
    <div className="px-2 w-full">
      <div className="flex flex-row-reverse">
        <button onClick={togglePanel}>click me</button>
      </div>
    </div>
  );
}

The logical && operator:

import { useState } from "react";

export default function Scratch() {
  const [isPanelOpen, setIsPanelOpen] = useState(false);
  const togglePanel = () => setIsPanelOpen(!isPanelOpen);
  return (
    
    <div className="px-2 w-full">
      <div className="flex flex-row-reverse">
        <button onClick={togglePanel}>click me</button>
      </div>

      {isPanelOpen && <p>the panel is open</p>}
    </div>
  );
}

I was getting by without this by using the ternary operator and returning an empty string for false conditions:

Get the current path parts on a parent route:

import { useMatches } from "@remix-run/react";

export default function Scratch() {
  const matches = useMatches();
  const path: string = matches.slice(-1)?.[0].pathname;
  const pathParts: string[] = path.split("/");
  const isScratchPage: boolean = pathParts.includes("scratch");

  return (
    <div className="px-2 w-full">
      {isScratchPage && (
        <p>On the "scratch" route, or one of it's child routes.</p>
      )}
    </div>
  );
}

Pull in data from a Resource Route:

// app/routes/scratchResource.tsx

import { json } from "@remix-run/node";

export const loader = async (): Promise<Response> => {
  const scratches: string[] = ["this", "that", "and", "the other"];
  return json(scratches);
};
// app/routes/scratch.tsx

import { useFetcher } from "@remix-run/react";

export default function Scratch() {
  const fetcher = useFetcher();
  const handleButtonClick = () => {
    fetcher.load("/scratchResource");
  };
  let scratches: string[] | undefined;

  if (fetcher.data) {
    scratches = fetcher.data as string[];
  }

  return (
    <div className="px-2 w-full">
      <button onClick={() => handleButtonClick()}>click me</button>
      {scratches &&
        scratches.map((scratch: string, index: number) => (
          <p key={index}>{scratch}</p>
        ))}
    </div>
  );
}

(I’m being kind of pedantic about types.)

I think the code above gives me what I need to add a Sidebar component to the app’s layout. The content of the sidebar will be updated depending on the current route.

Adding a sidebar

$ touch app/components.Sidebar.tsx

Demonstrate that the current path can be returned:

// app/components/Sidebar.tsx

import { useMatches } from "@remix-run/react";

export default function Sidebar() {
  const matches = useMatches();
  const path: string = matches.slice(-1)?.[0].pathname;
  const pathParts: string[] = path.split("/");

  return (
    <div className="fixed left-0 top-16 right-24 h-screen bg-slate-200 z-10">
      {pathParts && pathParts.map((part, index) => <p key={index}>{part}</p>)}
    </div>
  );
}

So now I’ve got a Header component and a Sidebar component. I’m not sure how much they are going to have to do with each other, except clicking the menu button in the header will trigger the sidebar to open. I’m looking at the options for how to get information about the clicks on the header menu button to the sidebar:

Direct DOM manipulation vs Declarative UI

This is what I’d likely have done in the past, but with JQuery or something instead of useEffect:

useEffect(() => {
  const headerButton = document.getElementById("header-button");
//... toggle the sidebar's open/close class

It seems that the preferred method for React/Remix is Declarative UI Updates. I’m liking that a lot. So the question is, if one way of toggling the state of the sidebar is to click the header’s menu button, how do I get data about this from the header to the sidebar if they are in separate components?

// app/components/Header.tsx

//...
export default function Header() {
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  const toggleSidebar = () => {
    setIsSidebarOpen(!isSidebarOpen);
  };

//...
  <>
  <header>
    <button
      onClick={toggleSidebar}
    >
    click me
    </button>
  </header>
  </>

Possibly this can be done with useOutletContext, but that’s starting to seem convoluted. This is an easy solution:

// app/components/Header.tsx

//...
import Sidebar from "~/components/Sidebar";
//...

export default function Header() {
//...

  <Sidebar isSidebarOpen={isSidebarOpen} />  
}
// app/components/Sidebar.tsx

interface SidebarProps {
  isSidebarOpen: boolean;
}
export default function Sidebar({ isSidebarOpen }: SidebarProps) {

It’s easy to understand. Just in terms of the names of the components, it feels wrong to have the Sidebar as a part of the Header. I was tempted to rename Sidebar to SiteNavigation, but that’s off too…

In any case, the sidebar opens and closes when the header’s menu button is clicked. That was the goal.

AppLayout component

Since I’m on this composition kick, I’ve gone ahead and moved the app’s layout into an AppLayout component

// app/components/AppLayout.tsx

import { Outlet } from "@remix-run/react";

import Header from "./Header";
import Footer from "./Footer";

export default function AppLayout() {
  return (
    <>
      <div className="flex-1">
        <Header />
        <Outlet />
      </div>
      <Footer />
    </>
  );
}

The "flex-1" div is to keep the footer at the bottom of pages that don’t have much content.

With the AppLayout component, the default export for root.tsx can be simplified to this:

// app/root.tsx

//...
export default function App() {
  return (
    <Document>
      <AppLayout />
      <ScrollRestoration />
      <Scripts />
      <LiveReload />
    </Document>
  );
}
//...

Eventually some props will need to be passed to the AppLayout component.

Get content into the sidebar

What’s the max length for a WordPress post?

I’ll work from the root (or trunk?) of the app. On the index page (/), the sidebar should have a listing for each of the site’s sections. I’ll start by adding some new routes:

$ touch app/routes/music.tsx
$ touch app/routes/stories.tsx
$ touch app/routes/hire-me.tsx

I’m not sure if anyone will hire me after reading this blog 🙂 (I’ll do yard work in a pinch.)

Find the current route

The Sidebar component needs to know the current route. This works, but seemed verbose to me:

  const matches = useMatches();
  const pathStart: string = matches.slice(-1)?.[0].pathname.split("/")[1] || "";

  let isRoot = false,
    isBlog = false,
    isMusic = false,
    isStories = false,
    isHireMe = false;

  switch (pathStart) {
    case "blog":
      isBlog = true;
      break;
    case "music":
      isMusic = true;
      break;
    case "stories":
      isStories = true;
      break;
    case "hire-me":
      isHireMe = true;
      break;
    default:
      isRoot = true;
  }

So I asked ChatGPT for suggestions to tighten it up. I’m not sure this is an improvement:

  const pathStart = useMatches().slice(-1)?.[0].pathname.split('/')[1] || '';

  const routeStates = {
    isRoot: pathStart === '',
    isBlog: pathStart === 'blog',
    isMusic: pathStart === 'music',
    isStories: pathStart === 'stories',
    isHireMe: pathStart === 'hire-me'
  };

  // Enforcing mutual exclusivity: Only one state can be true, others must be false.
  Object.keys(routeStates).forEach(key => {
    if (key !== `is${pathStart.charAt(0).toUpperCase() + pathStart.slice(1)}` && key !== 'isRoot') {
      routeStates[key] = false;
    }
  });

  // If none of the specific paths matched, it's the root.
  if (!['isBlog', 'isMusic', 'isStories', 'isHireMe'].some(key => routeStates[key])) {
    routeStates.isRoot = true;
  }

I’m a big fan of ChatGPT as a coding assistant, but leaving coding entirely in its hands would be a mistake. Programming languages were written for people, not LLMs. After a few iterations of LLMs training on their own code, we’d be stuck trying to debug gibberish.

Populating the sidebar for the index route

I think this part of the sidebar can be hardcoded:

// app/components/Sidebar.tsx

//...
      {isRoot && (
        <div>
          <NavLink to="/">
            <h2>Zalgorithm</h2>
          </NavLink>
          <ul>
            <NavLink to="/blog">
              <li>Blog</li>
            </NavLink>
            <NavLink to="/music">
              <li>Music</li>
            </NavLink>
            <NavLink to="/stories">
              <li>Stories</li>
            </NavLink>
            <NavLink to="/hire-me">
              <li>Hire Me</li>
            </NavLink>
          </ul>
        </div>
      )}
//...

I also broke down and used useEffect to close the sidebar when the location changes (so it closes when a sidebar link is clicked):

// app/component/Header.tsx

//...
export default function Header() {
  const location = useLocation();
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  const toggleSidebar = () => {
    setIsSidebarOpen(!isSidebarOpen);
  };

  useEffect(() => {
    setIsSidebarOpen(false);
  }, [location]);
//...

Populating the sidebar for the blog

This is where I start guessing again…

Create an api.blog-categories resource route? If this doesn’t work, I can pass the data to the AppLayout component from the root route:

$ touch app/routes/api.blog-categories.tsx
// app/routes/api.blog-categories.tsx

import { createApolloClient } from "lib/createApolloClient";
import { CATEGORIES_DETAILS_QUERY } from "~/models/wp_queries";

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

  if (response.errors) {
    // probably don't throw an error here
  }

  return response?.data?.categories;
};

This is neat, but only loads the categories if the blog is accessed by clicking the “blog” link in the sidebar:

// app/components/Sidebar.tsx

//...
  const fetcher = useFetcher();
  let blogCategories: RootQueryToCategoryConnection;
  const loadCategoryData = () => {
    fetcher.load("/api/blog-categories");
  };
  if (fetcher.data) {
    blogCategories = fetcher.data as RootQueryToCategoryConnection;
    console.log(`blogCategories: ${JSON.stringify(blogCategories, null, 2)}`);
  }
  return (
    <div
      className={`fixed left-0 top-16 right-24 h-screen bg-slate-200 z-10 ${
        isSidebarOpen
          ? "transition-transform -translate-x-0"
          : "transition-transform -translate-x-full"
      }`}
    >
      {isRoot && (
        <div>
          <NavLink to="/">
            <h2>Zalgorithm</h2>
          </NavLink>
          <ul>
            <NavLink onClick={() => loadCategoryData()} to="/blog">
              <li>Blog</li>
            </NavLink>
//...
  

I don’t think it’s going to be possible to use useFetcher to handle all possible ways that a user could navigate to the blog.

I wonder if Outlet context will work here? Nope 🙂

I’ll just pass the data from root.tsx:

// app/root.tsx

//...
export const loader = async () => {
  const client = createApolloClient();
  const response = await client.query({
    query: CATEGORIES_DETAILS_QUERY,
  });

  if (response.errors) {
    // probably don't throw an error here
  }

  return response?.data?.categories;
};
//...
export default function App() {
  const categories = useLoaderData<typeof loader>();
  return (
    <Document>
      <AppLayout categories={categories} />
//...

Then from AppLayout to the Header component:

// app/components/Header.tsx

import { Outlet } from "@remix-run/react";
import { Maybe } from "graphql/jsutils/Maybe";
import { type RootQueryToCategoryConnection } from "~/graphql/__generated__/graphql";

interface AppLayoutProps {
  categories: Maybe<RootQueryToCategoryConnection>;
}

import Header from "./Header";
import Footer from "./Footer";

export default function AppLayout({ categories }: AppLayoutProps) {
  return (
    <>
      <div className="flex-1">
        <Header categories={categories} />
        <Outlet />
      </div>
      <Footer />
    </>
  );
}

Then from the Header component to the Sidebar component:

// app/components/Sidebar.tsx

//...
interface SidebarProps {
  isSidebarOpen: boolean;
  categories: Maybe<RootQueryToCategoryConnection>;
}
export default function Sidebar({ isSidebarOpen, categories }: SidebarProps) {
  const matches = useMatches();
  const pathStart: string = matches.slice(-1)?.[0].pathname.split("/")[1] || "";
  console.log(`categories: ${JSON.stringify(categories, null, 2)}`);
//...

That works:

// app/components/Sidebar.tsx

//...
      {isBlog && categories?.nodes && (
        <div>
          <ul>
            {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>
        </div>
      )}
//...
$ git commit -m "get the sidebar more or less working"

I think this is the right approach. I wanted to avoid sending category data to the client when there’s a chance the user won’t visit a category page. It’s not much data though:

categories: {  "__typename": "RootQueryToCategoryConnection",  "nodes": [    {      "__typename": "Category",      "name": "adventure sports",      "slug": "adventure-sports"    },    {      "__typename": "Category",      "name": "modular synthesizers",      "slug": "modular-synthesizers"    },    {      "__typename": "Category",      "name": "outdoor photography",      "slug": "outdoor-photography"    },    {      "__typename": "Category",      "name": "Uncategorized",      "slug": "uncategorized"    },    {      "__typename": "Category",      "name": "web development basics",      "slug": "web-development-basics"    },    {      "__typename": "Category",      "name": "web server configuration",      "slug": "web-server-configuration"    }  ]}

I could see using a resource route and useFetcher in the sidebar if I want to pull in additional data based on an action that’s taken in the sidebar. For example, clicking a category heading in the blog menu could trigger a request to pull in the category’s posts. That would essentially turn the sidebar into each category’s archive route.

Since the Music, Stories, and Hire Me sections are just placeholders at the moment, I’ll display the isRoot menu section for those routes as well:

Style the sidebar

The styles are basic for now:

I’ll document what I learned while getting the sidebar’s “close” button to work:

The sidebar’s open/close state is set from the Header component. The Header passes an isSidebarOpen boolean prop to the Sidebar.

The value of isSidebarOpen is set here:

// app/components/Header.tsx

//...
export default function Header({ categories }: HeaderProps) {
  const location = useLocation();
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  const toggleSidebar = () => {
    setIsSidebarOpen(!isSidebarOpen);
  };

  useEffect(() => {
    setIsSidebarOpen(false);
  }, [location]);
//...

Originally the value was getting passed to the Sidebar component like this:

// app/components/Header.tsx

//...
      <Sidebar
        isSidebarOpen={isSidebarOpen}
        categories={categories}
      />
    </>
  );
}

When I added the “close” button to the Sidebar component:

// app/components/Sidebar.tsx

//...
     <button
        onClick={someFunction}
        className="text-slate-600 fixed top-1 right-4 text-xl py-1 px-1 rounded hover:bg-slate-300"
      >
//...

I couldn’t figure out how to get someFunction to work. It turned out the trick was to allow the Header component to control the sidebar’s open/close state from inside the sidebar – give the Sidebar a way to inform the Header that the sidebar should be closed. This is done by adding a function to the Sidebar props that calls the Header component’s setIsSidebarOpen function with the argument false:

// app/components/Header.tsx

//...
      <Sidebar
        isSidebarOpen={isSidebarOpen}
        categories={categories}
        onClose={() => setIsSidebarOpen(false)}
      />
    </>
  );
}

Then add that function to the Sidebar props:

// app/components/Sidebar.tsx

//...
interface SidebarProps {
  isSidebarOpen: boolean;
  categories: Maybe<RootQueryToCategoryConnection>;
  onClose: () => void;
}
export default function Sidebar({
  isSidebarOpen,
  categories,
  onClose,
}: SidebarProps) {
//...

This gives the Sidebar component access to the function. Then all it needs to do is this:

// app/components/Sidebar.tsx

//...
      <button
        onClick={onClose}
        className="text-slate-600 fixed top-1 right-4 text-xl py-1 px-1 rounded hover:bg-slate-300"
      >
        close
      </button>
//...

That kind of blew my mind. Thanks ChatGPT 🙂

Add header links

Trying to add links to the header is how this all started. It’s been productive. I’m not sure I solved the header link problem though.

… the solution for small screens is to either shrink the text, or hide the links. For now, I’ll hide the links, then show them on screens larger than 390px. Tomorrow I’ll add an icon to reveal the links in a drop down menu for small screens:

And it’s live: https://hello.zalgorithm.com/.