diff --git a/components/footer.tsx b/components/footer.tsx index f820ef5..0e4f0b7 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -4,33 +4,54 @@ import React, { useState, useEffect, useRef } from 'react' const HOME = 'https://hovanhoa.net' type FooterLink = { label: string; href: string } +type FooterSection = { title: string; links: FooterLink[] } -const PAGES: FooterLink[] = [ - { label: 'home', href: 'https://hovanhoa.net' }, - { label: 'insight', href: 'https://insight.hovanhoa.net' }, - { label: 'gallery', href: 'https://gallery.hovanhoa.net' }, - { label: 'music', href: 'https://music.hovanhoa.net' }, - { label: 'status', href: 'https://status.hovanhoa.net' }, -] - -const ELSEWHERE: FooterLink[] = [ - { label: 'github', href: 'https://github.com/hovanhoa' }, - { label: 'twitter', href: 'https://twitter.com/_hovanhoa_' }, - { label: 'linkedin', href: 'https://linkedin.com/in/hovanhoa' }, - { label: 'connect', href: 'https://info.hovanhoa.net' }, +const SECTIONS: FooterSection[] = [ + { + title: 'apps', + links: [ + { label: 'home', href: 'https://hovanhoa.net' }, + { label: 'insight', href: 'https://insight.hovanhoa.net' }, + { label: 'gallery', href: 'https://gallery.hovanhoa.net' }, + { label: 'music', href: 'https://music.hovanhoa.net' }, + { label: 'status', href: 'https://status.hovanhoa.net' }, + ], + }, + { + title: 'elsewhere', + links: [ + { label: 'github', href: 'https://github.com/hovanhoa' }, + { label: 'twitter', href: 'https://twitter.com/_hovanhoa_' }, + { label: 'linkedin', href: 'https://linkedin.com/in/hovanhoa' }, + ], + }, + { + title: 'more', + links: [ + { label: 'connect', href: 'https://info.hovanhoa.net' }, + { label: 'llms.txt', href: 'https://hovanhoa.net/llms.txt' }, + ], + }, ] const linkClass = 'font-[family-name:var(--font-mono)] text-sm text-[var(--rd-text-2)] transition-colors duration-200 hover:text-[var(--rd-accent-ink)]' -function LinkRow({ links }: { links: FooterLink[] }) { +function Section({ title, links }: FooterSection) { return ( -
- {links.map((l) => ( - - {l.label} - - ))} +
+

+ {title} +

+
) } @@ -74,23 +95,33 @@ export function Footer() { }} >
- - hovanhoa - .net - -

- software engineer — building, writing, and shipping. one - small corner of the internet, a few sites deep. -

+
+ {/* wordmark + blurb */} +
+ + hovanhoa + + .net + + +

+ software engineer — building, writing, and shipping. + one small corner of the internet, a few sites deep. +

+
-
- - + {/* sectioned links */} +
+ {SECTIONS.map((s) => ( +
+ ))} +
+ {/* bottom row */}

© 2026 hovanhoa

@@ -127,3 +158,5 @@ export function Footer() { ) } + +export default Footer diff --git a/utils/generateBlurPlaceholder.ts b/utils/generateBlurPlaceholder.ts index bb5bfeb..eb1bb01 100644 --- a/utils/generateBlurPlaceholder.ts +++ b/utils/generateBlurPlaceholder.ts @@ -11,10 +11,16 @@ export default async function getBase64ImageUrl( if (url) { return url; } - const response = await fetch( - `https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/f_jpg,w_8,q_70/${image.public_id}.${image.format}`, - ); - const buffer = await response.arrayBuffer(); + let buffer: ArrayBuffer; + try { + const response = await fetch( + `https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/f_jpg,w_8,q_70/${image.public_id}.${image.format}`, + ); + buffer = await response.arrayBuffer(); + } catch { + // DIAG: tolerate offline blur fetch so the page can render locally + return "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + } const minified = await imagemin.buffer(Buffer.from(buffer), { plugins: [imageminJpegtran()], });