Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ jobs:
# Authenticates the build-time GitHub API fetch on the home page
# (unauthenticated requests are rate-limited on shared runners).
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# GoatCounter site code (the "xyz" in xyz.goatcounter.com), set as
# a repository variable. Unset means the site ships no analytics.
NEXT_PUBLIC_GOATCOUNTER_CODE: ${{ vars.GOATCOUNTER_CODE }}

- name: Upload artifact
uses: actions/upload-pages-artifact@v5
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ pnpm check-links # after a build: fail on broken internal links in ./out
pnpm format # format with Prettier
```

## Analytics

Page views and resume PDF downloads are counted with
[GoatCounter](https://www.goatcounter.com/) (no cookies). It is off unless the
`GOATCOUNTER_CODE` repository variable is set (Settings → Secrets and
variables → Actions → Variables) to the site code, the `xyz` in
`xyz.goatcounter.com`. The next deploy picks it up.

## Credits

The design is based on the [Spotlight](https://spotlight.tailwindui.com/)
Expand Down
21 changes: 21 additions & 0 deletions app/components/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Script from 'next/script';

/* GoatCounter: cookieless page-view counts, plus click events for elements
with a data-goatcounter-click attribute (e.g. the resume PDF links).
The site code comes from the GOATCOUNTER_CODE repository variable at
deploy time; without it, nothing is loaded. */
const code = process.env.NEXT_PUBLIC_GOATCOUNTER_CODE;

export function Analytics() {
if (!code) {
return null;
}

return (
<Script
src="https://gc.zgo.at/count.js"
data-goatcounter={`https://${code}.goatcounter.com/count`}
strategy="afterInteractive"
/>
);
}
15 changes: 11 additions & 4 deletions app/components/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ export function ContainerOuter({
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={clsx('sm:px-8', className)} {...props}>
<div className="mx-auto h-full w-full max-w-7xl lg:px-8">{children}</div>
<div className={clsx('sm:px-8 print:px-0', className)} {...props}>
<div className="mx-auto h-full w-full max-w-7xl lg:px-8 print:px-0">
{children}
</div>
</div>
);
}
Expand All @@ -19,10 +21,15 @@ export function ContainerInner({
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
className={clsx('relative h-full px-4 sm:px-8 lg:px-12', className)}
className={clsx(
'relative h-full px-4 sm:px-8 lg:px-12 print:px-0',
className,
)}
{...props}
>
<div className="mx-auto h-full max-w-2xl lg:max-w-5xl">{children}</div>
<div className="mx-auto h-full max-w-2xl lg:max-w-5xl print:max-w-none">
{children}
</div>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function SocialIconLink({

export function Footer() {
return (
<footer className="mt-8 flex-none">
<footer className="mt-8 flex-none print:hidden">
<ContainerOuter>
<div className="border-t border-zinc-100 pt-10 pb-16 dark:border-zinc-700/40">
<ContainerInner>
Expand Down
2 changes: 1 addition & 1 deletion app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export function Header({ articles }: { articles: PaletteArticle[] }) {
return (
<>
<header
className="pointer-events-none relative z-50 flex flex-none flex-col"
className="pointer-events-none relative z-50 flex flex-none flex-col print:hidden"
style={{
height: 'var(--header-height)',
marginBottom: 'var(--header-mb)',
Expand Down
23 changes: 21 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@
@plugin '@tailwindcss/typography';

/* next-themes toggles the `dark` class on <html>, so dark: must key off the
class rather than the default prefers-color-scheme media query. */
@custom-variant dark (&:where(.dark, .dark *));
class rather than the default prefers-color-scheme media query. Screen
only: printing (e.g. the resume) always uses the light palette, since
dark-theme text is near-white and vanishes on paper. */
@custom-variant dark {
@media screen {
&:where(.dark, .dark *) {
@slot;
}
}
}

@page {
margin: 0.5in;
}

/* Everything is sized in rem, so this one value scales the printed page. */
@media print {
html {
font-size: 12px;
}
}

@keyframes fade-up {
from {
Expand Down
16 changes: 15 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './globals.css';

import { Footer } from './components/Footer';
import { Header } from './components/Header';
import { Analytics } from './components/Analytics';
import type { Metadata } from 'next';
import { Providers } from './providers';
import { pageMetadata } from './lib/metadata';
Expand Down Expand Up @@ -46,6 +47,18 @@ const personSchema = {
url: 'https://www.maplarge.com',
},
sameAs: [socials.github, socials.linkedin],
knowsAbout: [
'C#',
'.NET',
'TypeScript',
'React',
'Angular',
'Node.js',
'Go',
'Kubernetes',
'SQL',
'AWS',
],
};

export default async function RootLayout({
Expand All @@ -65,7 +78,7 @@ export default async function RootLayout({
dangerouslySetInnerHTML={{ __html: JSON.stringify(personSchema) }}
/>
<Providers>
<div className="fixed inset-0 flex justify-center sm:px-8">
<div className="fixed inset-0 flex justify-center sm:px-8 print:hidden">
<div className="flex w-full max-w-7xl lg:px-8">
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div>
Expand All @@ -76,6 +89,7 @@ export default async function RootLayout({
<Footer />
</div>
</Providers>
<Analytics />
</body>
</html>
);
Expand Down
3 changes: 3 additions & 0 deletions app/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const navigation = [
{ href: '/uses', label: 'Uses' },
];

export const resumePdfUrl =
'https://will-white.github.io/resume/William_White_Resume.pdf';

export const socials = {
github: 'https://github.com/will-white',
linkedin: 'https://www.linkedin.com/in/will-white-dev/',
Expand Down
16 changes: 15 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import Image from 'next/image';
import clsx from 'clsx';
import { GitHubIcon, LinkedInIcon } from './components/SocialIcons';
import { Card } from './components/Card';
import { siteDescription, socials } from './lib/site';
import { resumePdfUrl, siteDescription, socials } from './lib/site';
import { Button } from './components/Button';
import { pageMetadata } from './lib/metadata';
import headshot from './images/headshot.webp';

Expand Down Expand Up @@ -227,6 +228,19 @@ export default async function Home() {
{socials.email}
</SocialLink>
</ul>
<div className="mt-8 flex flex-wrap gap-3">
<Button href="/resume" variant="primary">
View resume
</Button>
<Button
href={resumePdfUrl}
target="_blank"
variant="secondary"
data-goatcounter-click="resume-pdf-home"
>
Download PDF
</Button>
</div>
</div>
</div>
</Container>
Expand Down
33 changes: 31 additions & 2 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { pageMetadata } from '../lib/metadata';
import Image, { type StaticImageData } from 'next/image';
import Link from 'next/link';

import k8s from '../images/logos/k8s.svg';
import github from '../images/logos/github.svg';
Expand All @@ -17,6 +18,10 @@ type Project = {
link: { href: string; label: string };
logo: StaticImageData;
tags: string[];
/** Blog post about the project, if there is one. */
writeUp?: string;
/** What I personally did on it, for professional work. */
contribution?: string;
};

const personalProjects: Project[] = [
Expand All @@ -30,6 +35,7 @@ const personalProjects: Project[] = [
},
logo: k8s,
tags: ['Kubernetes', 'Talos', 'Flux', 'GitOps'],
writeUp: '/blog/spouse-approved-kubernetes-homelab/',
},
{
name: 'Dahua Companion',
Expand All @@ -41,6 +47,7 @@ const personalProjects: Project[] = [
},
logo: github,
tags: ['Go', 'MQTT', 'Home Assistant'],
writeUp: '/blog/watching-the-doorbell-with-go/',
},
{
name: 'AdGuard external-dns Sidecar',
Expand Down Expand Up @@ -77,6 +84,8 @@ const companyProjects: Project[] = [
},
logo: skyslopeForms,
tags: ['.NET', 'Node.js', 'React', 'AWS'],
contribution:
'Senior engineer, 2022–2024. Helped keep it at 99.999% uptime for 10,000+ daily concurrent users, and led a streaming and deferred-execution effort that cut cluster costs by 40%.',
},
{
name: 'AgriSompo AgriNet',
Expand All @@ -88,6 +97,8 @@ const companyProjects: Project[] = [
},
logo: armtech,
tags: ['C#', '.NET', 'Angular', 'MSSQL'],
contribution:
'Eight years at ARMtech Insurance Services, 2014–2022. Built a unified single sign-on for 10,000+ users and a 45x speedup of a core processing service.',
},
{
name: 'RASI',
Expand Down Expand Up @@ -161,6 +172,14 @@ function ProjectSection({
</Card.Link>
</h3>
<Card.Description>{project.description}</Card.Description>
{project.contribution && (
<p className="relative z-10 mt-3 text-sm text-zinc-700 dark:text-zinc-300">
<span className="font-semibold text-zinc-800 dark:text-zinc-100">
My part:
</span>{' '}
{project.contribution}
</p>
)}
<div className="relative z-10 mt-4 flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span
Expand All @@ -175,6 +194,15 @@ function ProjectSection({
<LinkIcon className="h-6 w-6 flex-none" />
<span className="ml-2">{project.link.label}</span>
</p>
{project.writeUp && (
// z-30 lifts it above Card.Link's full-card click target.
<Link
href={project.writeUp}
className="relative z-30 mt-3 text-sm font-medium text-cyan-600 hover:underline dark:text-cyan-400"
>
Read the write-up →
</Link>
)}
</Card>
))}
</ul>
Expand All @@ -184,14 +212,15 @@ function ProjectSection({

export const metadata = pageMetadata({
title: 'Projects',
description: 'My creative footprint on the internet.',
description:
'Products I’ve helped build and run at work, and the open-source tools I run at home.',
path: '/projects/',
});

export default function Projects() {
return (
<SimpleLayout
title="My creative footprint on the internet."
title="Things I’ve built, at work and at home."
intro="I’ve worked on tons of little projects but these are the ones that I’m most proud of. Some of them are open-source, so if you see something that piques your interest, check out the code!"
>
<div className="space-y-20">
Expand Down
Loading
Loading