Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/dyed-in-the-wool/llms.txt

Use this file to discover all available pages before exploring further.

Dyed in the Wool is deployed as a fully static site on GitHub Pages. Because GitHub Pages serves files from a flat directory without server-side redirect rules, the app uses React Router v6’s HashRouter — routing via the URL hash (/#/about) rather than the path (/about). This means every direct URL visit lands on index.html, and the hash fragment tells the router which page to display.

Why HashRouter?

GitHub Pages returns a 404 for any path it doesn’t have a file for. With BrowserRouter, navigating directly to https://example.github.io/dyed-in-the-wool/about would produce a 404 because there’s no about/index.html. With HashRouter, the URL becomes https://example.github.io/dyed-in-the-wool/#/about — the hash is never sent to the server, so index.html is always served and React Router handles the rest client-side.

Route Definitions

The app defines six routes, all nested under the root "/" path which renders <Layout> as their shared shell via <Outlet />.
// Simplified from assets/main.js
import { HashRouter, Routes, Route } from "react-router-dom";
import { Layout } from "../components/Layout";
import Home     from "./Home";
import About    from "./About";
import Projects from "./Projects";
import Skills   from "./Skills";
import Work     from "./Work";
import Contact  from "./Contact";

function App() {
  return (
    <HashRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index        element={<Home />}     />
          <Route path="about"    element={<About />}    />
          <Route path="projects" element={<Projects />} />
          <Route path="skills"   element={<Skills />}   />
          <Route path="work"     element={<Work />}     />
          <Route path="contact"  element={<Contact />}  />
        </Route>
      </Routes>
    </HashRouter>
  );
}
Hash URLRoute pathComponentPage title
/#// (index)Home”DYED IN THE WOOL” hero
/#/aboutaboutAbout”THE HUMAN BEHIND THE DYE”
/#/projectsprojectsProjects”PATTERNS I’VE MADE”
/#/skillsskillsSkills”COLOR PALETTE”
/#/workworkWork”THE TAPESTRY”
/#/contactcontactContact”LEAVE A MARK”
The header nav is rendered inside Layout and maps over a static navLinks array to produce NavLink components for each route. Active state is detected via React Router’s isActive prop and visually indicated by a Framer Motion underline with a shared layoutId, so the indicator slides smoothly between nav items when the active route changes.
const navLinks = [
  { path: "/",        label: "Home"    },
  { path: "/about",   label: "About"   },
  { path: "/projects",label: "Projects"},
  { path: "/skills",  label: "Skills"  },
  { path: "/work",    label: "Work"    },
  { path: "/contact", label: "Contact" },
];

// Inside Layout's <nav>
<ul className="flex gap-6 font-sans text-sm font-medium tracking-widest uppercase">
  {navLinks.map((link) => (
    <li key={link.path}>
      <NavLink
        to={link.path}
        className={({ isActive }) =>
          `relative px-2 py-1 transition-colors hover:text-dye-light
           ${isActive ? "text-dye-light" : "text-white/70"}`
        }
      >
        {({ isActive }) => (
          <>
            {link.label}
            {isActive && (
              <motion.div
                layoutId="nav-indicator"
                className="absolute -bottom-1 left-0 right-0 h-0.5 bg-dye-light rounded-full"
                initial={false}
                transition={{ type: "spring", stiffness: 300, damping: 30 }}
              />
            )}
          </>
        )}
      </NavLink>
    </li>
  ))}
</ul>
The layoutId="nav-indicator" causes Framer Motion to treat every instance of that motion.div as the same element — when the active route changes, the indicator bar animates its position from the old link to the new one via a spring transition. The initial={false} prop prevents the bar from animating in from scratch on the first render. The header itself uses mix-blend-difference so the white text and indicator remain legible regardless of whatever colorful background content is behind it.

Static HTML Page Stubs

Each route has a corresponding pre-built HTML file in /pages/ (e.g. pages/About.html). These files allow the deployed site to handle bookmarked or shared links that include the full path — the user’s browser fetches the stub, which immediately sets the correct hash and loads the main bundle.
<!-- pages/About.html -->
<script>
  window.__STATIC_PAGE_ROUTE__ = "/about";
  if (!window.location.hash || window.location.hash === "#/" || window.location.hash === "#") {
    window.location.hash = "/about";
  }
</script>
The script sets window.__STATIC_PAGE_ROUTE__ (available for inspection) and redirects the hash to #/about if it isn’t already set, then the main bundle loads and the HashRouter renders the correct page.

Page Transitions

AnimatePresence with mode="wait" wraps a motion.div keyed on location.pathname inside Layout. When a route changes, AnimatePresence first plays the exit animation on the outgoing page, waits for it to complete, then plays the enter animation on the incoming page — preventing both from rendering simultaneously. The transition uses opacity paired with a filter: blur() effect, giving route changes a soft, out-of-focus dissolve feel rather than a hard cut or slide.
// Inside Layout — animates between routes
const location = useLocation();

<AnimatePresence mode="wait">
  <motion.div
    key={location.pathname}
    initial={{ opacity: 0, filter: "blur(10px)" }}
    animate={{ opacity: 1, filter: "blur(0px)" }}
    exit={{    opacity: 0, filter: "blur(10px)" }}
    transition={{ duration: 0.5 }}
    className="h-full"
  >
    <Outlet />
  </motion.div>
</AnimatePresence>
The key={location.pathname} is what triggers a remount (and therefore a new animation cycle) whenever the active route changes. Without the key, React would reuse the same DOM node and no transition would play.

Build docs developers (and LLMs) love