# Build The Urlist

Build the complete application in this repository. Work autonomously from start to finish: plan internally, implement all requirements, test them, fix defects, 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.

## 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 can have multiple segments. Use catch-all App Router routes where needed. Static app 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:

- Requires at least one link to publish or save
- 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.

- Show a visible drag handle on each editable link.
- Support pointer and touch dragging to any position.
- Show clear picked-up, dragging, drop-position, and dropped states.
- Update draft order immediately.
- Save account-owned order to SQLite.
- Show saved order on the public page.
- Support desktop and mobile widths.

Also provide accessible Move up and Move down controls. Drag-and-drop alone is not sufficient.

## Live metadata

Fetch and parse the live destination page on the server after a link is added. Show Pending, Succeeded, or Failed per link. Block publish and save while any link is Pending.

Stop the network operation after 10 seconds and show a terminal state within one more second. 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 allow only HTTP/HTTPS; reject credentials and non-public addresses, including loopback, private, link-local, reserved, and cloud metadata ranges; revalidate DNS and every redirect; prevent DNS 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. Reject empty, `.`/`..`, reserved, query/fragment, backslash, and encoded-separator forms.

Match the exact full path, so `burke` and `burke/links` can both exist. Active, deleted, and anonymous aliases are all reserved. Enforce uniqueness in SQLite.

If blank, generate an available seven-character lowercase alphanumeric alias. Aliases never change after publication.

Block publish/save when the list is empty, metadata is Pending, or the alias is invalid/unavailable. Repeat validation on the server. On failure, keep all user input.

For anonymous publication, require a blocking confirmation that says the list cannot be edited or deleted and its alias stays reserved permanently. Use **Publish permanently** and **Cancel**. Enforce immutability on the server. 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. Enforce every check on the server and ignore client-supplied owner IDs.

The edit page loads persistent data directly, keeps alias and destinations read-only, and permits description, membership, order, title, and 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, Restore, and clear empty/loading/error states.

## Delete and restore

Delete requires confirmation and performs a soft delete. Keep content, ownership, and alias forever. 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. Keep navigation, editor fields, drag controls, link actions, sharing, QR, lists, and modals usable without page-level two-dimensional scrolling.

Meet WCAG 2.2 AA:

- Semantic headings, labels, names, errors, and status announcements
- Full keyboard use, visible focus, logical focus order, and keyboard reordering
- No color-only meaning; usable at 200% zoom; reduced-motion support
- Modal focus entry, trap, Escape cancellation, and focus return
- Logical focus after deletion or reordering

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.

Validate all untrusted input on the server, use parameterized SQL, escape user and remote content, prevent stored/reflected script execution, encode share parameters, apply suitable CSRF protection, and do not expose secrets or internals in 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 validation, aliases, metadata parsing/network policy, ownership, SQLite transactions, and share URLs.

Use Playwright for anonymous and account publication, both users, My Lists, owner/non-owner access, drag-and-drop plus persisted order, keyboard reordering, delete/restore/tombstone, public/QR/share views, themes, accessibility basics, and mobile layout.

Tests use a separate temporary database, reset state, avoid order dependence, and clean up processes.

The README must cover prerequisites, setup, environment, database, development and production commands, mock login, reset, tests, metadata behavior, troubleshooting, 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.
