Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/aurora-borealis/llms.txt

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

The Aurora Borealis app is a Vite-built React 18 SPA that uses React Router DOM v6’s BrowserRouter internally, yet deploys to a static file host without any server-side routing support. This is achieved by pairing each route with its own static HTML shell that contains a small redirect script, so a visitor who lands on /about directly is seamlessly bounced into the SPA’s hash-based navigation before the React tree even mounts.

Why Static HTML Shells?

Static file hosts (GitHub Pages, Netlify in “static” mode, S3 + CloudFront) serve whatever HTML file matches the requested path. They cannot rewrite every URL to index.html unless explicitly configured. Rather than requiring server configuration, the Aurora Borealis app ships a dedicated HTML file for each route under a pages/ directory — for example pages/about.html handles the /about path. Each shell sets window.__STATIC_PAGE_ROUTE__ to its canonical path string and then runs a redirect script before any React code loads.
The static HTML shells are thin entry points only. They contain no page content. Once the redirect fires and the SPA takes over, all rendering is handled by React.

The window.__STATIC_PAGE_ROUTE__ Variable

Every static page shell sets a global variable that identifies which route the user intended to visit:
<!-- pages/about.html (representative shell) -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>About | aurora-borealis</title>
    <script type="module" crossorigin src="../assets/main.js"></script>
    <script>
      window.__STATIC_PAGE_ROUTE__ = "/about";

      // If the URL does not already carry the correct hash, redirect to it.
      (function () {
        if (!window.location.hash || window.location.hash === "#") {
          window.location.replace(
            window.location.pathname +
            window.location.search +
            "#/about"
          );
        }
      })();
    </script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
The inline IIFE runs synchronously before the module script is parsed. It checks whether the URL already carries a hash — if the hash is absent or bare (#), it calls window.location.replace() to redirect to the same path with #/about appended. The replace() call means the intermediate URL is not added to the browser history stack.
1

Visitor requests /about

The static host serves pages/about.html. The page begins loading.
2

Redirect script runs

The inline IIFE checks window.location.hash. Because this is a fresh navigation, the hash is empty. The script calls window.location.replace(pathname + search + "#/about"), preserving the current path and any query string while appending the hash.
3

Browser loads with hash

The browser reloads pages/about.html (or the root, depending on host configuration) with #/about now present in the URL.
4

React mounts and routes

assets/main.js loads, React mounts, and the app reads the hash (#/about) to determine which <Route> to display, rendering the correct page component.

React Router Configuration

Inside assets/main.js the application tree is wrapped in a single BrowserRouter. All visual layer components (AuroraBackground, StarField, ConstellationNav) are placed outside the <Routes> block so they remain mounted across every navigation. <AnimatePresence> wraps <Routes> and is keyed by location.pathname to coordinate enter and exit animations.
// Simplified excerpt from assets/main.js
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import { AnimatePresence } from "framer-motion";

function App() {
  const location = useLocation();

  return (
    <>
      {/* Background layer — always mounted, never transitions */}
      <AuroraBackground />
      <StarField />

      {/* Navigation layer — always mounted */}
      <ConstellationNav />

      {/* Content layer — transitions on every route change */}
      <AnimatePresence mode="wait">
        <Routes location={location} key={location.pathname}>
          <Route path="/"            element={<PageTransition><Home /></PageTransition>} />
          <Route path="/about"       element={<PageTransition><About /></PageTransition>} />
          <Route path="/projects"    element={<PageTransition><Projects /></PageTransition>} />
          <Route path="/skills"      element={<PageTransition><Skills /></PageTransition>} />
          <Route path="/writing"     element={<PageTransition><Writing /></PageTransition>} />
          <Route path="/case-studies" element={<PageTransition><CaseStudies /></PageTransition>} />
          <Route path="/contact"     element={<PageTransition><Contact /></PageTransition>} />
        </Routes>
      </AnimatePresence>
    </>
  );
}

export default function Root() {
  return (
    <BrowserRouter>
      <App />
    </BrowserRouter>
  );
}
useLocation must be called inside a component that is itself a child of BrowserRouter. The App component is split from Root for exactly this reason — calling useLocation at the same level as BrowserRouter would throw a context error.

AnimatePresence and PageTransition

<AnimatePresence mode="wait"> ensures that React waits for the currently mounted route’s exit animation to complete before mounting the next one. This prevents two page components from overlapping mid-transition. The key prop on <Routes> — set to location.pathname — tells React that a new pathname means a completely new subtree, triggering the unmount/mount cycle that AnimatePresence intercepts. PageTransition is a lightweight Framer Motion wrapper applied to every route’s element:
// PageTransition component definition
import { motion } from "framer-motion";

const pageVariants = {
  initial: { opacity: 0, filter: "blur(10px)" },
  animate: { opacity: 1, filter: "blur(0px)" },
  exit:    { opacity: 0, filter: "blur(10px)" },
};

const pageTransition = {
  duration: 0.8,
  ease: "easeInOut",
};

export function PageTransition({ children }) {
  return (
    <motion.div
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      transition={pageTransition}
    >
      {children}
    </motion.div>
  );
}
On enter, opacity animates from 0 → 1 and filter: blur(10px) collapses to blur(0px) over 800 ms. On exit the same values reverse, giving each page a soft dissolve-and-focus feel that complements the aurora aesthetic.

Full Route Table

PathComponentStatic Shell
/Homeindex.html
/aboutAboutpages/about.html
/projectsProjectspages/projects.html
/skillsSkillspages/skills.html
/writingWritingpages/writing.html
/case-studiesCaseStudiespages/case-studies.html
/contactContactpages/contact.html
Because all seven route components are bundled together in assets/main.js, navigating between any two routes requires zero additional network requests after the initial page load.

Static Shell Deployment Checklist

1

Confirm all shells are present

Ensure pages/about.html, pages/projects.html, pages/skills.html, pages/writing.html, pages/case-studies.html, and pages/contact.html are all committed and deployed alongside index.html.
2

Verify asset paths are relative

Each shell must reference ../assets/main.js (relative to pages/) so the script resolves correctly whether served from the project root or a subdirectory.
3

Check host URL rewriting is off

The hash-redirect strategy requires the static host to serve the HTML file that matches the path literally. Do not enable “SPA fallback” or “404 → index.html” rewrites — those would bypass the shell redirect and can cause double-navigation artefacts.

Build docs developers (and LLMs) love