Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2dc1a1e
chore: add site hardening patch
Dingding-leo Aug 16, 2026
af607a3
chore(ci): apply verified site hardening pass
Dingding-leo Aug 16, 2026
50ccbd6
chore(ci): allow verified non-workflow patch push
Dingding-leo Aug 16, 2026
950f3e4
refactor(site): harden first-load consent and deploy integrity
github-actions[bot] Aug 16, 2026
3478603
ci: validate the complete static export
Dingding-leo Aug 16, 2026
459cecb
ci: gate Pages deploy on full export validation
Dingding-leo Aug 16, 2026
47913f4
chore(ci): inspect production dependency advisories
Dingding-leo Aug 16, 2026
e3b2b99
chore(ci): apply compatible dependency security fixes
Dingding-leo Aug 16, 2026
cd24a87
chore(ci): test patched Next.js release
Dingding-leo Aug 16, 2026
7fb47c9
fix(deps): upgrade Next.js to the patched release
github-actions[bot] Aug 16, 2026
e30d06e
chore: add a supported Next.js ESLint configuration
Dingding-leo Aug 16, 2026
60c8ea4
chore(ci): verify supported Next.js linting
Dingding-leo Aug 16, 2026
60f5ab4
chore: add active-source lint cleanup patch
Dingding-leo Aug 16, 2026
93d0872
chore(ci): apply and verify active-source cleanup
Dingding-leo Aug 16, 2026
e231404
refactor(site): remove dead code and restore clean linting
github-actions[bot] Aug 16, 2026
672d324
chore: make the full site check explicit
Dingding-leo Aug 16, 2026
d2eee66
ci: gate changes on source and production integrity
Dingding-leo Aug 16, 2026
5d05c4a
ci: deploy only verified static exports
Dingding-leo Aug 16, 2026
03fe5af
docs: document the verified Next.js 16 site pipeline
Dingding-leo Aug 16, 2026
bb6c487
chore: remove temporary maintenance workflow
Dingding-leo Aug 16, 2026
259cf4f
chore: remove temporary site patch script
Dingding-leo Aug 16, 2026
d3b09a8
chore: remove temporary lint cleanup script
Dingding-leo Aug 16, 2026
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
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_BUILD_ID: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Type-check and build
run: npm run build
- name: Lint, typecheck, build, and validate export
run: npm run check
- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high
12 changes: 7 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_BUILD_ID: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npx tsc --noEmit
- name: Build
run: npm run build
- name: Lint, typecheck, build, and validate export
run: npm run check
- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
Expand Down
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,27 @@ Personal site, engineering projects showcase, and technical notes archive.

## Overview

Static-exported personal site built with Next.js 15 App Router and React 19. Designed for sub-second page loads, clean semantic HTML structure, and seamless deployment across GitHub Pages and custom targets.
Static-exported personal site built with Next.js 16 App Router and React 19. It combines a cinematic visual journal with project work, field notes, and a clear public identity as a dental student and independent builder in Adelaide.

## Features

* **Site-wide search** — ⌘K (or `/`) command palette across pages, notes, and projects.
* **Notes/journal system** — single data source (`config/notes.ts`) driving the index, homepage journal, per-note metadata, prev/next pagination, share actions, and print styles.
* **SEO layer** — `sitemap.xml`, `robots.txt`, RSS (`/feed.xml`), canonical URLs, Open Graph/Twitter cards, and JSON-LD (Person, WebSite, BlogPosting, BreadcrumbList).
* **PWA** — web manifest plus a service worker for offline reading (network-first pages, stale-while-revalidate assets).
* **Performance** — responsive `srcset` images, content-visibility-friendly layout, zero-CLS media via CSS aspect ratios.
* **Accessibility** — WCAG AA contrast, keyboard-navigable dialogs and tabs, reduced-motion support, correct `lang` tagging for bilingual content.
* **PWA** — ambience is opt-in, the service worker is versioned per deployment, and offline reading cannot pin an older page to stale JavaScript chunks.
* **Performance** — responsive `srcset` images, content-visibility-friendly layout, zero-CLS media via explicit dimensions and aspect ratios.
* **Accessibility** — keyboard-navigable dialogs and menus, reduced-motion support, semantic sharing controls, and correct `lang` tagging for bilingual content.

## Technical Details

* **Framework**: Next.js 15 (App Router, static export mode `output: 'export'`).
* **UI & Styling**: React 19, TypeScript, Tailwind CSS, and `next-themes` for system-aware dark mode switching.
* **Build Pipeline**: Static site generation producing optimized static assets to `out/` for edge deployment; CI typechecks and builds on every push to `main`, then deploys to GitHub Pages.
* **Framework**: Next.js 16 (App Router, static export mode `output: 'export'`).
* **UI & Styling**: React 19, TypeScript, Tailwind CSS, Framer Motion, and `next-themes`.
* **Quality gate**: ESLint, TypeScript, production build, internal route/asset/fragment validation, service-worker syntax validation, and a high-severity production dependency audit.
* **Deployment**: GitHub Pages receives an artifact only after the complete quality gate passes on Node.js 22.

## Development

Requires Node.js 20+.
Requires Node.js 22+.

```bash
npm install
Expand All @@ -35,9 +36,11 @@ npm run dev
## Build & Export

```bash
npm run build
npm run check
```

`npm run check` lints the active application source, typechecks it, builds the complete static export, and verifies internal links, fragments, required metadata files, manifest validity, and service-worker syntax. CI and deployment additionally require a clean high-severity production dependency audit.

## License

MIT
2 changes: 1 addition & 1 deletion app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { pageMetadata } from '@/config/pageMetadata';
export const metadata = pageMetadata({
title: 'About — Austin Liu',
description:
'Builder, traveller, photographer, and writer behind The Last Blue Hour.',
'Dental student, builder, traveller, photographer, and writer behind The Last Blue Hour.',
image: '/assets/blue-hour/afterlight-1200.jpg',
imageAlt: 'A small stone cabin with one warm window on a blue moor',
});
Expand Down
1 change: 0 additions & 1 deletion components/BlueHourJumpShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export function BlueHourPicture({
<picture
className={styles.scenePicture}
data-scene={scene}
aria-hidden="true"
style={
{
'--jump-scene-position': focalPoint.desktop,
Expand Down
6 changes: 3 additions & 3 deletions components/LegacyPages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ export function AboutPage() {
<div className={styles.aboutSplit}>
<div>
<p className={styles.aboutLead}>
A builder and traveller from China, now based in Adelaide — making
small digital tools, collecting places, and keeping a public record
of the things that hold my attention.
A dental student, builder, and traveller from China, now based in
Adelaide — making small digital tools, collecting places, and keeping
a public record of the things that hold my attention.
</p>
<div className={styles.stats}>
<div className={styles.statCard}>
Expand Down
16 changes: 10 additions & 6 deletions components/NoteShare.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
'use client';

import { useEffect, useState } from 'react';
import { useState, useSyncExternalStore } from 'react';
import { Check, Copy, Share2 } from 'lucide-react';
import jumpStyles from '@/components/BlueHourJumpShell.module.css';

const subscribeToShareSupport = () => () => undefined;
const readShareSupport = () =>
typeof navigator !== 'undefined' && typeof navigator.share === 'function';

export function NoteShare({ title }: { title: string }) {
const [copied, setCopied] = useState(false);
const [canShare, setCanShare] = useState(false);

useEffect(() => {
setCanShare(typeof navigator !== 'undefined' && !!navigator.share);
}, []);
const canShare = useSyncExternalStore(
subscribeToShareSupport,
readShareSupport,
() => false,
);

const copyLink = async () => {
if (!navigator.clipboard) return;
Expand Down
4 changes: 1 addition & 3 deletions components/ProjectDeck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@ export function ProjectDeck({
const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState(1);
const [busy, setBusy] = useState(false);
const [clientReady, setClientReady] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [hasFocusWithin, setHasFocusWithin] = useState(false);
const [allowAutoplayWhileFocused, setAllowAutoplayWhileFocused] =
Expand All @@ -297,7 +296,7 @@ export function ProjectDeck({
const [announcement, setAnnouncement] = useState('');
const count = deckProjects.length;
const activeProject = deckProjects[activeIndex];
const motionEnabled = clientReady && !reducedMotion && !conserveMotion;
const motionEnabled = reducedMotion === false && !conserveMotion;
const temporarilyPaused =
isHovered ||
(hasFocusWithin && !allowAutoplayWhileFocused) ||
Expand Down Expand Up @@ -339,7 +338,6 @@ export function ProjectDeck({
setAutoplayEpoch((epoch) => epoch + 1);
}, []);

useEffect(() => setClientReady(true), []);

useEffect(() => {
const selectRequestedProject = (requested: string | null) => {
Expand Down
33 changes: 32 additions & 1 deletion components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,42 @@
import { useEffect, type ReactNode } from 'react';
import { BlueHourAudioProvider } from './blue-hour/AudioExperience';

const BUILD_ID = process.env.NEXT_PUBLIC_BUILD_ID ?? 'local';

export function Providers({ children }: { children: ReactNode }) {
useEffect(() => {
if (process.env.NODE_ENV !== 'production') return;
if (!('serviceWorker' in navigator)) return;
navigator.serviceWorker.register('/sw.js').catch(() => {});

const hadController = Boolean(navigator.serviceWorker.controller);
const reloadKey = `blue-hour-sw-reloaded:${BUILD_ID}`;
const onControllerChange = () => {
if (!hadController) return;
try {
if (window.sessionStorage.getItem(reloadKey) === '1') return;
window.sessionStorage.setItem(reloadKey, '1');
} catch {
// Reloading once is still safe when session storage is unavailable.
}
window.location.reload();
};

navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
void navigator.serviceWorker
.register(`/sw.js?v=${encodeURIComponent(BUILD_ID)}`, {
updateViaCache: 'none',
})
.then((registration) => registration.update())
.catch((error: unknown) => {
console.warn('[Austin Liu site] Service worker registration failed:', error);
});

return () => {
navigator.serviceWorker.removeEventListener(
'controllerchange',
onControllerChange,
);
};
}, []);

return <BlueHourAudioProvider>{children}</BlueHourAudioProvider>;
Expand Down
Loading
Loading