Portfolioen er selv et projekt. Her er arkitekturen, de valg jeg traf undervejs — og en fejl, der lærte mig noget.
Idéen
En rekrutterer, en udvikler og min familie leder ikke efter det samme. Netflix løser det med profiler, så det gør siden også: indholdet er det samme, men forsiden, rækkerne og tonen skifter med den profil, du vælger.
Alt om profilerne står ét sted. Ruter, navigation, profilvælger, sitemap og forsider læser fra den samme fil, så en ny profil er én post — ikke en rundtur i ti komponenter. Filen er holdt fri for React, så reglerne kan testes med Nodes indbyggede test-runner.
export const PROFILES: Record<ProfileId, Profile> = {
developer: { href: "/developer", label: "Udvikler", pages: [...], home: workHome("Udvikler") },
recruiter: { href: "/recruiter", label: "Rekrutterer", pages: [...], home: workHome("Rekrutterer") },
family: { href: "/family", label: "Familie & venner", pages: ALL_PAGES, home: { hero: "wishlist", ... } },
};Indhold
Projekter, erfaring og kompetencer ligger i Sanity. Siderne er statiske, men regenereres i baggrunden hvert minut, så et nyt projekt i Studio er live uden at jeg rører koden.
Det lyder som en standardindstilling, men den kom af en fejl: Sanity-svarene lå i Nexts data-cache med et års levetid, og den cache overlevede deploys på serveren. Nye projekter dukkede simpelthen aldrig op. Projektsiderne bygges desuden ved første besøg, så et nyt projekt ikke peger på en 404, indtil næste build.
Ønskelisten
Ønskelisten er sidens mest alvorlige stykke kode. Gæster logger ind og reserverer et ønske, så to ikke køber det samme — men jeg må ikke kunne se, hvad der er reserveret. Ellers er der ingen overraskelse.
Det er ikke bare skjult i brugerfladen. Row Level Security sørger for, at man kun kan læse sine egne reservationer. Hvilke ønsker der er taget, udleveres af én databasefunktion, som kun returnerer ønskets id — aldrig hvem — og som returnerer ingenting, når det er mig, der spørger.
Login er Google eller en sekscifret engangskode på mail. Kode frem for link er et bevidst valg: Outlooks linkscanner åbner engangslinks, før modtageren når at klikke, og så er linket brugt.
create or replace function public.reserved_wish_ids()
returns setof text
language sql security definer stable
as $$
select wish_id from public.reservations
where not (public.wishlist_hide_from_owner() and public.is_wishlist_owner())
$$;En fejl jeg lærte af
Efter login med engangskode så ønskelisten ud, som om intet var reserveret. Først efter en genindlæsning stod tallene rigtigt. Login-koden var fin, og serveren sendte de rigtige data. Fejlen lå et helt tredje sted.
Komponenten kopierede serverens lister over i useState. En useState-initializer kører kun, når komponenten mountes, og router.refresh() mounter ikke noget på ny — den giver bare den eksisterende komponent nye props. De friske lister blev altså leveret og ignoreret. Alt, der blev beregnet direkte fra props, opdaterede sig fint, og derfor lignede det en login-fejl.
Rettelsen er et fingeraftryk af serverens data: når det ændrer sig, overtager de nye lister under selve renderen. Læringen er den gamle: kopier ikke props til state, medmindre du også har besluttet, hvem der vinder, når de ændrer sig.
const serverState = `${reservedIds.join()}|${myIds.join()}`;
const [synced, setSynced] = useState(serverState);
if (synced !== serverState) {
setSynced(serverState);
setReserved(new Set(reservedIds));
setMine(new Set(myIds));
}Detaljerne
"Fortsæt hvor du slap" bor i browserens localStorage, men læses gennem useSyncExternalStore med en tom server-snapshot. Serveren og første klient-render er dermed ens for alle, og rækken dukker op uden hydration-fejl.
Temaet sættes af et lille inline-script, før browseren maler første frame, så man aldrig ser et glimt af det forkerte tema. Fontene ligger på mit eget domæne og preloades med siden i stedet for at blive hentet gennem en kæde af kald til Google.
Spotify-data huskes i serverprocessen. Nexts fetch-cache har Authorization-headeren med i nøglen, og med et nyt token ved hvert kald ramte den aldrig — den skrev bare en ny fil til disken, hver gang afspilleren spurgte. Top 10-rækken på forsiden er rangeret efter rigtige visninger, talt én gang pr. besøg gennem en databasefunktion, som er den eneste vej ind til tabellen.
Drift
Siden kører på en VPS bag nginx. Et push til main starter en GitHub Action, som logger ind på serveren og bygger. Det er bevidst enkelt: ingen platform imellem, og jeg ved præcis, hvad der kører hvor.
Det næste skridt er en port foran: lint, test og build skal være grønne, før noget når serveren — sammen med en browser-test af login på ønskelisten, som ville have fanget fejlen ovenfor.
Vil du se ønskelisten i praksis, eller høre mere om valgene?