HJORTUR FREYR
◀ BACK TO WORK
LEVEL 04IN PROGRESS — 2025

Gjafalistar.is

A gift registry for Icelandic occasions — birthdays, weddings, christenings, graduations. Everything around the idea got built. The idea itself did not.

MY ROLE
Sole developer — API, frontend, auth, deploys
TIMELINE
Jun 2024 – Nov 2025, in five bursts with long gaps · unfinished
STACK
Next.js 15, React 19, TypeScript, Laravel 11, Sanctum, NextAuth, Tailwind v4, Sentry
TEAM
Just me, in spare time
01 — THE PROBLEM

Adding a gift should
take one paste.

Every gift list app asks you to type a product name, a price, and a link, and then find an image. That is four fields of friction on a task people do fifteen times in one sitting before a wedding. If adding an item is tedious, the list never gets finished, and an unfinished registry is worse than none because people buy off it anyway.

So the interaction is a single paste. The URL goes in, the server fetches the page, reads its metadata, and the item appears with a title and an image already attached. Getting that to work reliably across arbitrary Icelandic and international retailers turned out to be most of the project — and it is the part I ended up rebuilding worse.

WHAT WAS BREAKING
Retailers structure product metadata inconsistently, and some block server-side fetches outright
Two people buying from the same list have no way to coordinate without spoiling the surprise
The list owner must not see who reserved what, which makes it an access-control problem, not a UI one
Guests should not need an account to give a gift, but the whole API sits behind authentication
02 — THE APPROACH

Three decisions that
shaped everything else.

Two of these were right. The third is why the project is unfinished.

01

Scrape the page, do not ask the user

Adding an item is one field. The server fetches the URL and reads structured metadata, Open Graph tags, and finally the plain title tag, resolving relative image URLs to absolute so images from smaller shops actually load. There is no manual entry form anywhere in the product, which was the whole point.

02

Headless Laravel behind a React app

A JSON API with token authentication and a separate Next.js frontend, rather than server-rendered Laravel. It made the mobile-first list UI far easier to build and it is why membership, roles and policies live cleanly in one place. It also doubled the auth surface, which is where the bugs are.

03

Model roles before modelling reservations

Owner, collaborator and viewer went into a membership table early, with a policy layer and tests. Reserving an item — the actual product — was left as markup. I built the permissions system for a feature that did not exist yet, and then never built the feature.

03 — WHAT I BUILT
THE PASTE

One field, three fallbacks

The first scraper ran server-side in Laravel with a proper cascade: structured product data first, then Open Graph tags, then the bare page title, each tier wrapped in its own error handling. It is the best code in either repository. It is also dead — nothing calls it any more.

OCCASIONS

Seven Icelandic occasions, colour-coded

Birthdays, weddings, Christmas, summer, graduations, christenings and other — seeded as types rather than free text, each mapped to its own colour throughout the interface. The routes are Icelandic too, all the way down. Names run through a hand-written transliteration table that maps Þ, Ð, Æ, Ö and Ý to ASCII so the avatar service returns correct initials instead of question marks.

COLLABORATION

Roles, policies, and tests for them

Lists have members with roles, guarded by a policy and gated in the interface so only an owner sees edit controls. There is a complete friend-request lifecycle behind it — send, accept, decline, unfriend — with five passing tests and no user interface at all. It is finished code for a screen that was never built.

04 — THE HARD PART

I rewrote the good
scraper into a worse one.

The Laravel scraper kept getting blocked. Retailers refuse requests from datacentre IPs, and five commits in a row are just debugging that fight. The fix I reached for was to move scraping into the Next.js app, where the request comes from a different place and can carry a browser user agent. That solved the blocking.

It also threw away the parser. The new version reads the page with regular expressions over raw HTML and dropped the structured-data tier entirely, so it now handles fewer sites correctly than the code it replaced. I fixed a delivery problem by rewriting the part that was working, and the dependencies for the old parser are still sitting in the manifest, unused.

That rewrite is the last substantial thing I did on this project. It landed at the end of October 2025, alongside a half-applied migration to a new component library, and the work stopped there with the tree still dirty. The reservation feature — the reason to build a gift registry at all — exists as commented-out markup showing exactly what it should look like, next to a button with no handler behind it.

ADD AN ITEM
1 Paste a product URL
2 Server fetches the page
3 Read metadata, resolve image URL
4 Optimistic row appears
5 Save, deduplicated by URL
If the fetch fails the row is removed and the user gets an alert — which is where a manual-entry fallback should be, and is not.
05 — OUTCOME

Deployed, working, and
missing the point.

35%
Complete as a gift registry, by my own estimate
25
Backend tests passing across 10 files
0
Lines of reservation logic written
NEXT LEVEL

Timr