diff --git a/app/layout.tsx b/app/layout.tsx
index b854079..9f157ef 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -6,11 +6,13 @@ import { ThemeProvider } from "@/components/theme-provider"
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
+ display: "swap",
});
const outfit = Outfit({
variable: "--font-outfit",
subsets: ["latin"],
+ display: "swap",
});
export const metadata: Metadata = {
diff --git a/components/sections/contact.tsx b/components/sections/contact.tsx
index d50743f..05bf11c 100644
--- a/components/sections/contact.tsx
+++ b/components/sections/contact.tsx
@@ -19,7 +19,7 @@ export async function ContactSection() {
+
diff --git a/package-lock.json b/package-lock.json
index 0617d0c..960b6f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@tailwindcss/typography": "^0.5.19",
- "framer-motion": "^12.38.0",
"lucide-react": "^0.563.0",
"next": "16.1.4",
"next-themes": "^0.4.6",
@@ -3786,33 +3785,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/framer-motion": {
- "version": "12.38.0",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
- "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
- "license": "MIT",
- "dependencies": {
- "motion-dom": "^12.38.0",
- "motion-utils": "^12.36.0",
- "tslib": "^2.4.0"
- },
- "peerDependencies": {
- "@emotion/is-prop-valid": "*",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/is-prop-valid": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6114,21 +6086,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/motion-dom": {
- "version": "12.38.0",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
- "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
- "license": "MIT",
- "dependencies": {
- "motion-utils": "^12.36.0"
- }
- },
- "node_modules/motion-utils": {
- "version": "12.36.0",
- "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
- "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
- "license": "MIT"
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/package.json b/package.json
index 5929831..f11e592 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,6 @@
},
"dependencies": {
"@tailwindcss/typography": "^0.5.19",
- "framer-motion": "^12.38.0",
"lucide-react": "^0.563.0",
"next": "16.1.4",
"next-themes": "^0.4.6",
diff --git a/public/about-me.avif b/public/about-me.avif
new file mode 100644
index 0000000..2e12a82
Binary files /dev/null and b/public/about-me.avif differ
diff --git a/public/about-me.webp b/public/about-me.webp
index 19426ad..437378d 100644
Binary files a/public/about-me.webp and b/public/about-me.webp differ
diff --git a/public/profile-cutout.avif b/public/profile-cutout.avif
new file mode 100644
index 0000000..6be29b9
Binary files /dev/null and b/public/profile-cutout.avif differ
diff --git a/public/profile-cutout.webp b/public/profile-cutout.webp
index 4549b31..3124a88 100644
Binary files a/public/profile-cutout.webp and b/public/profile-cutout.webp differ
diff --git a/public/swift-logo.avif b/public/swift-logo.avif
new file mode 100644
index 0000000..765f0f8
Binary files /dev/null and b/public/swift-logo.avif differ
diff --git a/public/tri.avif b/public/tri.avif
new file mode 100644
index 0000000..d0931a6
Binary files /dev/null and b/public/tri.avif differ
diff --git a/scripts/generate-avif.mjs b/scripts/generate-avif.mjs
new file mode 100644
index 0000000..2974448
--- /dev/null
+++ b/scripts/generate-avif.mjs
@@ -0,0 +1,36 @@
+import sharp from 'sharp';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const publicDir = path.join(__dirname, '../public');
+
+const images = [
+ 'profile-cutout.webp',
+ 'about-me.webp',
+ 'swift-logo.webp',
+ 'tri.webp',
+];
+
+async function generateAvif() {
+ for (const img of images) {
+ const inputPath = path.join(publicDir, img);
+ const outputPath = path.join(publicDir, img.replace('.webp', '.avif'));
+
+ try {
+ await sharp(inputPath)
+ .avif({ quality: 80 })
+ .toFile(outputPath);
+
+ const inputSize = (await import('fs')).statSync(inputPath).size;
+ const outputSize = (await import('fs')).statSync(outputPath).size;
+ const savings = Math.round((1 - outputSize / inputSize) * 100);
+
+ console.log(`✓ ${img.padEnd(25)} → ${img.replace('.webp', '.avif').padEnd(25)} (${savings}% smaller)`);
+ } catch (err) {
+ console.error(`✗ Failed to generate AVIF for ${img}:`, err.message);
+ }
+ }
+}
+
+generateAvif();
diff --git a/scripts/resize-images.mjs b/scripts/resize-images.mjs
new file mode 100644
index 0000000..dcfd3d2
--- /dev/null
+++ b/scripts/resize-images.mjs
@@ -0,0 +1,31 @@
+import sharp from 'sharp';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const publicDir = path.join(__dirname, '../public');
+
+const images = [
+ { file: 'profile-cutout.webp', width: 600, height: 695 },
+ { file: 'about-me.webp', width: 650, height: 888 },
+];
+
+async function resizeImages() {
+ for (const img of images) {
+ const inputPath = path.join(publicDir, img.file);
+ const outputPath = path.join(publicDir, img.file);
+
+ try {
+ const result = await sharp(inputPath)
+ .resize(img.width, img.height, { fit: 'cover' })
+ .toFile(outputPath + '.tmp');
+
+ await sharp(outputPath + '.tmp').toFile(outputPath);
+ console.log(`✓ Resized ${img.file} to ${img.width}x${img.height}`);
+ } catch (err) {
+ console.error(`✗ Failed to resize ${img.file}:`, err.message);
+ }
+ }
+}
+
+resizeImages();
diff --git a/todo.md b/todo.md
index 9445754..384630f 100644
--- a/todo.md
+++ b/todo.md
@@ -1,5 +1,31 @@
# Todo
+## Deploy to Vercel
+
+Migrate from GitHub Pages to Vercel's free tier for better performance and caching.
+
+**Benefits:**
+- HTTP/2 Server Push (fixes critical request chains, saves ~109ms on LCP)
+- Custom cache headers (fixes 10-min GitHub Pages TTL → 1yr for hashed assets)
+- Free default domain: `andsang-github-io.vercel.app`
+- Optional: add custom domain later ($10-15/year)
+- Automatic deployments on git push
+
+**Steps:**
+- [ ] Push current branch (`feat/liquidglass`) to GitHub
+- [ ] Connect repo to Vercel (or use `npm i -g vercel && vercel`)
+- [ ] Vercel auto-detects Next.js + `output: 'export'`
+- [ ] Test site on Vercel domain
+- [ ] Update any hardcoded links from `andsang.github.io` to new Vercel domain (if any)
+- [ ] Merge to `master` and deploy to production
+
+**What this unlocks (GitHub Pages cannot fix these):**
+
+| Issue | Current (GitHub Pages) | With Vercel | Impact |
+|-------|-------|---------|--------|
+| **Cache lifetimes** | 10 min TTL | 1 year for hashed assets | 365 KiB savings per repeat visit |
+| **Critical request chains** | CSS blocks rendering at 348→456ms | HTTP/2 Server Push | ~109ms LCP improvement |
+
## Render-blocking CSS
Next.js App Router injects CSS via `
`. Because of the `data-precedence` attribute (React's internal hydration ordering), `beasties` inlines critical CSS but leaves the blocking `
` intact — Lighthouse still flags it. Work was reverted.
diff --git a/tsconfig.json b/tsconfig.json
index 3a13f90..15c7b97 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "ES2017",
+ "target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,