# Build The Urlist

Build the complete application in this repository. Work autonomously from start to finish and stop only when the app is complete.

If a minor detail is not specified, choose a reasonable solution and document it. Do not copy an existing implementation of The Urlist.

## Stack and design

Use:

- Next.js App Router, React, strict TypeScript, Node.js, and npm
- SQLite with direct parameterized SQL through `better-sqlite3`; no ORM
- Vitest and Playwright
- Current stable package versions and a committed `package-lock.json`

Use the Node.js runtime, not Edge, for SQLite, sessions, file access, and metadata fetching.

Before UI work, invoke `/postrboard` and follow it across the app. Make list creation and editing the primary work surface. Use Postrboard classes and tokens before custom CSS. Avoid generic SaaS sections, decorative cards, fake data, emoji, icon grids, and gradient blobs. Run the Postrboard self-review before completion.

## Product

The Urlist lets people create ordered lists of web links and publish each list at a public alias.

Use this home-page copy:

> Group links, save and share them with the world.

> Add links to a list and share it with one simple URL.

Ownership is set at publication:

- Signed out: the list is anonymous, permanent, immutable, non-deletable, and never claimable.
- Signed in: the list belongs to the current user, who can edit, soft-delete, and restore it.

Anyone can view an active list.

The server is authoritative: revalidate input, ownership, alias availability, and immutability on every write; ignore client-supplied owner IDs.

## Routes

| Route | Behavior |
|---|---|
| `/` | Home page and first-link entry |
| `/s/new` | Create or resume the one local draft |
| `/s/edit/{...alias}` | Owner-only, reloadable edit page |
| `/s/mylists` | Current user's active and deleted lists |
| `/{...alias}` | Active public list, deleted tombstone, or unused-alias state |

Aliases may contain multiple `/`-separated segments. Static routes take priority; reserve first segments `s`, `api`, `auth`, `__test`, and `.well-known`.

Global navigation must include Home, New, My Lists when signed in, Login or the current user with Log out, and Light/Dark/System theme controls. Confirm before New clears a non-empty draft.

## Draft and editor

Keep one draft per browser profile in browser-local storage. It must survive reload, navigation, login, and logout. Clear it only after publication or confirmed discard.

The home page accepts the first URL, adds it to the current draft, starts metadata enrichment, and opens `/s/new`.

A list:

- Has no product-level link limit
- Allows duplicate destination URLs as separate entries
- Has an optional description and alias

For each link, store a stable ID, immutable destination URL, optional title, optional description, optional enrichment-controlled image, and position.

Authors can add and delete links and edit titles and descriptions. They cannot edit an existing destination or image URL.

Accept only absolute HTTP or HTTPS destinations with valid hosts and no embedded credentials. You may add `https://` to domain-like input, but show the normalized URL. Show field-associated validation errors.

### Reordering

Drag-and-drop is the primary reorder interaction: provide a visible handle per editable link, pointer and touch dragging to any position, and clear picked-up, dragging, drop-target, and dropped states. Also provide accessible Move up and Move down controls. Persist order in the draft and SQLite.

## Live metadata

Fetch and parse the live destination page on the server after a link is added. Show Pending, Succeeded, or Failed per link.

Abort the fetch after 10 seconds and settle the link into a terminal state. Failure keeps the link and permits manual title/description editing and publication.

Metadata precedence:

1. Open Graph
2. Twitter Card (`name` or `property`)
3. HTML title/meta description/page icon, with `<h1>` and `/favicon.ico` as final fallbacks

Resolve relative images against the final URL after redirects. Empty metadata is a successful result. Preserve manual title and description edits. Show a neutral image placeholder when needed.

The fetcher must be SSRF-safe: HTTP/HTTPS only, no credentials, reject non-public/internal addresses, revalidate DNS on every redirect to prevent rebinding, follow at most five redirects, limit content to 2 MiB, accept only HTML/XHTML, and never forward app credentials.

## Aliases and publication

Normalize aliases to lowercase. An alias has one or more `/`-separated segments. Each segment:

- Uses only `a-z`, `0-9`, and hyphens
- Is 1–50 characters
- Does not start or end with a hyphen

The full alias is at most 200 characters.

Match the exact full path, so `burke` and `burke/links` can both exist. Once published, aliases are permanent and globally unique in SQLite across active, deleted, and anonymous lists; they never change or become available again.

If blank, generate an available seven-character lowercase alphanumeric alias.

Block publish/save when the list is empty, metadata is Pending, or the alias is invalid/unavailable. On failure, keep all user input.

For anonymous publication, require a blocking confirmation that says the list cannot be edited or deleted after publication. Use **Publish permanently** and **Cancel**. Anonymous lists never appear in My Lists.

## Mock login and ownership

Provide a local mock GitHub provider with two stable fictional users. Show user A first and user B second with controls named `Continue as {display name}`. Do not call GitHub.

Use a server-verifiable HTTP-only session cookie with an appropriate SameSite policy. Use Secure in production HTTPS while allowing local HTTP development. Login survives reload; logout ends the session. Use stable user IDs, not display names, for ownership.

Only an owner can load, save, delete, or restore an account list.

The edit page loads persistent data directly and permits list-description, link-membership, order, and per-link title/description changes. Show save progress and errors without losing input.

My Lists requires login and shows only the current user's lists in separate Active and Deleted sections. Active cards show alias, optional description, and link count. Provide Create new list and Restore.

## Delete and restore

Delete requires confirmation and performs a soft delete that retains content and ownership. There is no permanent-delete action.

The public tombstone shows only:

> This list was deleted.

Only the signed-in owner also sees Restore. Restore reactivates the same alias, content, order, and ownership.

## Public list

Use the description as the heading or the alias when blank. Show links in saved order.

Each link card shows its image/placeholder, title or hostname/URL fallback, optional description, and destination. Open destinations safely in a new context with `noopener` and `noreferrer`.

Provide:

- Links and QR code views with accessible selected state
- A scannable QR code for the canonical absolute list URL
- Copy link with announced success/failure
- Correctly encoded X/Twitter, Facebook, and LinkedIn share links

For a valid unused alias, show Not found and a Create with this alias action. Prefill the draft without silently replacing existing content. Invalid/reserved paths get a generic not-found state.

## Theme, responsive UI, and accessibility

Provide persisted Light, Dark, and System themes. System follows OS preference. Keep accessible contrast and avoid an initial wrong-theme flash when practical.

Support current Chromium from desktop down to 320 CSS pixels with no page-level horizontal scrolling; modals, drag controls, and the editor must remain usable.

Meet WCAG 2.2 AA, including full keyboard operation, reduced-motion support, sensible focus after deletion or reordering, and announced status/error changes.

Show truthful loading, empty, success, blocked, and error states. Errors must explain recovery. Never present failure as success.

## Storage and security

Use versioned SQL migrations or an idempotent versioned initializer. Enable SQLite foreign keys. Use transactions for publish, save, delete, restore, and reset.

Persist users, lists, ownership/state/timestamps, links, and positions. Published and deleted data must survive a complete restart. Configure the database path by environment variable with a safe local default; do not commit database files.

Treat user text and fetched metadata as untrusted when rendering, prevent stored/reflected script execution, encode share parameters, apply CSRF protection, and keep internals out of errors.

Provide a development/test-only deterministic reset, preferably `POST /__test/reset` returning `204`. It clears lists and sessions and restores the two users. Disable or protect it in production.

## Scripts, tests, and documentation

Provide npm scripts:

```text
dev
build
start
lint
typecheck
test
test:e2e
db:init
db:reset
```

Use Vitest for URL/alias validation, metadata parsing and network policy, ownership, SQLite transactions, and share URLs. Use Playwright for anonymous/account publication, owner/non-owner access, drag-and-drop and keyboard reordering with persisted order, delete/restore/tombstone, and public/QR/share views. Tests use a separate temporary database and must not depend on order.

Ship a README covering setup, environment variables, database, commands, mock login, reset, tests, and assumptions.

## Completion

Before you finish:

1. Run `npm run lint`, `npm run typecheck`, `npm test`, `npm run test:e2e`, and `npm run build`.
2. Start the production build and confirm the app responds.
3. Confirm active, anonymous, and deleted data survives restart.
4. Review this entire instruction and fix every missing or incorrect requirement.
5. Repeat affected validation.

Report what you built, assumptions, architecture, exact command results, and anything incomplete with its reason. Do not claim a check passed unless you ran it successfully.
