Skip to content

About

Index of my data-viz side projects. Lives at drewhoover.com.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

drewhoover.com

The index site for my data-visualization side projects. Each card on the homepage links to a standalone app that lives in its own repo and deploys independently to GitHub Pages. This repo is just the front door.

Built with Astro and deployed as a GitHub Pages User Site (DrewHoo/DrewHoo.github.io), with a custom domain.

Adding a new project

  1. Create src/content/projects/<slug>.md.

  2. Fill in the frontmatter (see src/content.config.ts for the schema):

    ---
    title: My New Thing
    blurb: A short description (max 280 chars).
    liveUrl: https://drewhoo.github.io/my-new-thing/
    repoUrl: https://github.com/DrewHoo/my-new-thing
    stack:
      - React
      - D3
    pinned: false
    order: 10            # higher = earlier in the list
    updated: 2026-04-19
    ---
  3. Optionally add an MDX body under the frontmatter for a longer writeup — it will render at /projects/<slug>/.

  4. git push. The GitHub Action rebuilds and redeploys.

Sports sites

A sports site is a project entry with section: sports. It is listed on /sports instead of /projects, as a row rather than a card, and it has no /projects/<slug>/ detail page: the row links straight to the live site, and astro.config.mjs redirects the old detail URL there. A sports entry adds three fields the row shows:

section: sports
league: CFB            # CFB, NFL, WNBA, NCAA — picks the one-color mark the row shows (src/lib/sports.ts)
span: 1936–present     # the seasons covered; in the manifest, not on the row
line: "One sentence for the row. The blurb stays for the manifest and any future use."

The marks live in public/sports/logos/ (nba, nfl, wnba, ncaa for the championships grid, ncaa-football for CFB) and were made with the ink-density pipeline from cfb-streak-king/scripts/gen-logos.mjs. A new league needs a mark there and a line in LEAGUE_MARK.

The /sports build also writes /sports/index.json, the sports manifest: every sports entry with its title, line, league, span, live URL and repo. The sports sites read it so none of them keeps a hand-written list of the others. A site built from DrewHoo/dataviz-project-template fetches it in scripts/prerender.mjs at build time and renders "more sports from drewhoover.com" into the HTML; an older site includes public/embed/sports-nav.js instead and gets the same list at runtime (see below).

Blog content (headless Hashnode)

Blog posts live in Hashnode and are fetched at build time via their GraphQL API (gql.hashnode.com) — no migration, no local markdown files. Keep writing in the Hashnode editor; the next build/deploy picks up changes.

  • Publication host is configured in src/content.config.ts (defaults to drewhoo.hashnode.dev, overridable via HASHNODE_PUBLICATION_HOST env var).
  • The loader lives at src/loaders/hashnode.ts and paginates through all published posts.
  • Images (cover and inline) stay on Hashnode's CDN.
  • New/edited posts auto-deploy via a Hashnode webhook → Netlify Function → GitHub repository_dispatch → deploy workflow. To force a rebuild manually, push a commit or re-run the workflow from the Actions tab.

Local development

npm install
npm run dev       # localhost:4321
npm run build
npm run preview

Hosting / deploy

  • Repo: DrewHoo/DrewHoo.github.io (a GitHub User Site — name must match the username).
  • GitHub Pages source: GitHub Actions (not "deploy from branch").
  • Custom domain: drewhoover.com, committed in public/CNAME.
  • Workflow: .github/workflows/deploy.yml uses the official withastro/action + actions/deploy-pages.

DNS (one-time, when the domain is active)

Apex (drewhoover.com) — four A records to GitHub Pages' IPs:

185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153

www — CNAME → drewhoo.github.io.

Why the subpath routing just works

Any other repo owned by DrewHoo that has Pages enabled is automatically served under this domain as drewhoover.com/<repo-name>/. No per-project DNS needed. So DrewHoo/space-rock appears at drewhoover.com/space-rock/ as soon as drewhoover.com is the custom domain on the User Site repo.

Search engines

  • Google Search Console — verified as a Domain property via a DNS TXT record at the registrar. That covers http/https, apex/www, and every subpath, so the sibling project sites are included and need no registration of their own. There is deliberately no google-site-verification meta tag; adding one would only create a second, narrower URL-prefix property.
  • Bing Webmaster Tools — import the verified site from Search Console rather than re-verifying. Submit https://drewhoover.com/sitemap-index.xml (the index, not sitemap-0.xml). Bing's index is also what feeds DuckDuckGo, Yahoo, Ecosia and Copilot.
  • IndexNow — scripts/indexnow.mjs, run by the indexnow job in deploy.yml after each deploy. It maps the push's changed files onto URLs, intersects them with the live sitemap, and submits only that set. Reaches Bing, Yandex, Naver and Seznam; Google ignores the protocol.

The IndexNow key lives at public/<key>.txt and is duplicated in scripts/indexnow.mjs. It is public by design — serving the key back from the site root is the ownership proof — so it is not a secret and there is nothing to configure in GitHub. Rotating it means changing the filename and the KEY constant together.

To resubmit every URL (seeding a newly registered engine, or a template change that rewrote every page), run the workflow manually from the Actions tab with Also resubmit every sitemap URL to IndexNow checked. Don't wire that into the push path: repeatedly submitting unchanged URLs is what the protocol asks you not to do.

"Back to the index" bar on project sites

public/embed/back-bar.js is a ~50-line standalone script that any project site can include to add a sticky top strip linking home. To wire it up in a sibling repo (e.g. cfb-all-time-records), add this to the project's HTML head:

<script src="https://drewhoover.com/embed/back-bar.js" async></script>

To opt out on a specific page, add data-dhv-back-bar="off" to the <html> or <body> tag.

A site in a section links back to the section page instead of home. The sports sites pass data-section="sports" on the script tag and get "← drewhoover.com/sports" linking to /sports/:

<script src="https://drewhoover.com/embed/back-bar.js" data-section="sports" async></script>

The bar's colors follow prefers-color-scheme. A sports-section bar defaults to the dark palette, since every sports site is dark; any page can pin it with data-dhv-back-bar-theme="dark" or "light" on <html> or <body>. Clicks report through the analytics embed as the event back bar with to set to home or the section.

"More sports" list on the sports sites

public/embed/sports-nav.js fetches /sports/index.json and lists every other sports site, so a site that predates the template's build-time list can include:

<script src="https://drewhoover.com/embed/sports-nav.js" async></script>

It mounts into <div id="sports-nav"> when the page has one, else just before <div id="comments">, else at the end of <body>. Opt out with data-dhv-sports-nav="off". Clicks report as the event sports nav with to set to the slug.

The actual integration in DrewHoo/cfb-all-time-records and DrewHoo/space-rock is a follow-up task in those repos; this repo only ships the snippet.

Comments on project sites

public/embed/giscus.js mounts a giscus comment thread on any sibling site. Comments are stored in this repo's GitHub Discussions (category: Comments), so moderation happens in one place regardless of which project a visitor comments on. Threads are keyed by pathname, so drewhoover.com/space-rock/ and drewhoover.com/cfb-all-time-records/ each get their own.

Usage in a sibling repo's HTML:

<script src="https://drewhoover.com/embed/giscus.js" async></script>

Placement: if the page includes <div id="comments">, the widget mounts there. Otherwise it appends to the bottom of <body>. Opt out per-page with data-dhv-giscus="off" on <html> or <body>.

Theme follows prefers-color-scheme to match the rest of the ecosystem. Requires Discussions enabled on DrewHoo/DrewHoo.github.io and the giscus GitHub app installed on it.

Structure

src/
  components/   ProjectCard, ProjectGrid, Divider, Header, Footer, BaseHead, ...
  content/
    projects/   One markdown file per project card.
  loaders/
    hashnode.ts Custom Astro content loader — pulls blog posts from Hashnode's GraphQL API at build time.
  layouts/      BlogPost layout.
  pages/
    index.astro             Post index (the front page). /blog redirects here.
    about.astro
    projects/index.astro    Card grid (entries without a section).
    sports/index.astro      The sports list (entries with section: sports).
    sports/index.json.ts    The sports manifest the sports sites read.
    blog/[...slug].astro    Single post.
    projects/[...slug].astro  Per-project detail/writeup page (not for sports entries).
    rss.xml.js              Blog RSS feed.
  styles/global.css         One hand-written stylesheet.
public/
  CNAME, favicon.svg, fonts/, embed/back-bar.js, embed/giscus.js, embed/sports-nav.js

Style / design notes

  • Palette: warm cream paper, warm near-black ink, cadmium orange, ultramarine, a yellow-green highlight. Defined as CSS custom properties in src/styles/global.css.
  • Typography: Space Mono for display, Atkinson Hyperlegible for body. Both self-hosted from public/fonts/.
  • Dark mode is automatic via prefers-color-scheme; no manual toggle.
  • Animations are minimal and respect prefers-reduced-motion.
  • Hand-written CSS, no Tailwind — that's on purpose.

License

MIT (see LICENSE when added). Content (blog posts, project writeups) © Drew Hoover.

About

Index of my data-viz side projects. Lives at drewhoover.com.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages