Revision 3 of 16
Condenses the whole document: one exact stack list, a five-row routes table, and the metadata, alias, login and accessibility rules cut to a paragraph each.
+121 −725 lines of text vs revision 2 (this site's diff)
| Old | New | Change | Line |
|---|---|---|---|
| @@ -1,821 +1,217 @@ | |||
| 1 | 1 | # Build The Urlist | |
| 2 | 2 | ||
| 3 | − | Build the complete application described below in this repository. | |
| 4 | − | ||
| 5 | − | Work autonomously from start to finish. Make an internal plan, implement all features, add tests, run validation, fix defects, and continue until the application is complete. If a small implementation detail is not specified, choose a reasonable solution and document it. Do not stop after scaffolding or partial implementation. | |
| 3 | + | 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. | |
| 6 | 4 | ||
| 7 | − | Do not access or copy an existing implementation of The Urlist. | |
| 5 | + | If a minor detail is not specified, choose a reasonable solution and document it. Do not copy an existing implementation of The Urlist. | |
| 8 | 6 | ||
| 9 | − | ## Required design skill | |
| 7 | + | ## Stack and design | |
| 10 | 8 | ||
| 11 | − | Before you design or implement the user interface, invoke the `/postrboard` skill and follow its instructions. | |
| 9 | + | Use: | |
| 12 | 10 | ||
| 13 | − | Use the Postrboard design language across the complete application. Its behavioral rules, tokens, components, layout guidance, accessibility rules, and self-review requirements are mandatory. | |
| 11 | + | - Next.js App Router, React, strict TypeScript, Node.js, and npm | |
| 12 | + | - SQLite with direct parameterized SQL through `better-sqlite3`; no ORM | |
| 13 | + | - Vitest and Playwright | |
| 14 | + | - Current stable package versions and a committed `package-lock.json` | |
| 14 | 15 | ||
| 15 | − | The result must feel specific to The Urlist: | |
| 16 | + | Use the Node.js runtime, not Edge, for SQLite, sessions, file access, and metadata fetching. | |
| 16 | 17 | ||
| 17 | − | - Treat list creation and editing as the primary work surface. | |
| 18 | − | - Keep the design calm, code-native, restrained, and content-led. | |
| 19 | − | - Do not use generic SaaS sections, decorative cards, fake metrics, invented social proof, gradient blobs, emoji, or icon grids. | |
| 20 | − | - Use Postrboard classes and tokens before custom CSS. | |
| 21 | − | - Use one deliberate accent per surface and keep semantic status colors meaningful. | |
| 22 | − | - Use concise labels that describe actual objects, actions, states, and consequences. | |
| 23 | − | - Run the Postrboard self-review scan before declaring the interface complete. | |
| 18 | + | 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. | |
| 24 | 19 | ||
| 25 | 20 | ## Product | |
| 26 | 21 | ||
| 27 | − | The Urlist lets people create ordered lists of web links and publish each list at one short public URL. | |
| 22 | + | The Urlist lets people create ordered lists of web links and publish each list at a public alias. | |
| 28 | 23 | ||
| 29 | − | Use this main copy: | |
| 24 | + | Use this home-page copy: | |
| 30 | 25 | ||
| 31 | 26 | > Group links, save and share them with the world. | |
| 32 | 27 | ||
| 33 | 28 | > Add links to a list and share it with one simple URL. | |
| 34 | 29 | ||
| 35 | − | A person can start a list without an account. | |
| 36 | − | ||
| 37 | − | - A signed-out person can publish an anonymous list. After publication, the list is permanent and cannot be edited, deleted, or claimed. | |
| 38 | − | - A signed-in person publishes an account-owned list. Its owner can edit it, soft-delete it, and restore it. | |
| 39 | − | - Anyone can view an active list without signing in. | |
| 40 | − | ||
| 41 | − | ## Required technology | |
| 42 | − | ||
| 43 | − | Use: | |
| 44 | − | ||
| 45 | − | - Next.js with the App Router | |
| 46 | − | - React | |
| 47 | − | - TypeScript with strict mode | |
| 48 | − | - Node.js | |
| 49 | − | - npm | |
| 50 | − | - SQLite | |
| 51 | − | - Direct parameterized SQL through `better-sqlite3` | |
| 52 | − | - Vitest for unit and integration tests | |
| 53 | − | - Playwright for browser tests | |
| 54 | − | ||
| 55 | − | Do not use: | |
| 56 | − | ||
| 57 | − | - An ORM | |
| 58 | − | - A real OAuth provider | |
| 59 | − | - A cloud database | |
| 60 | − | - A required paid service | |
| 61 | − | - Static metadata fixtures in place of live metadata fetching | |
| 62 | − | ||
| 63 | − | Use current stable, non-prerelease package versions. Commit `package-lock.json`. | |
| 64 | − | ||
| 65 | − | Code that uses SQLite, server sessions, the file system, or metadata fetching must run in the Node.js runtime, not the Edge runtime. | |
| 66 | − | ||
| 67 | − | ## Required routes | |
| 68 | − | ||
| 69 | − | | Route | Access | Behavior | | |
| 70 | − | |---|---|---| | |
| 71 | − | | `/` | Public | Home page and first-link entry | | |
| 72 | − | | `/s/new` | Public | Create or resume the one local draft | | |
| 73 | − | | `/s/edit/{alias}` | Owner only | Load and edit an account-owned list; direct navigation and reload must work | | |
| 74 | − | | `/s/mylists` | Signed in | Show the current user's active and deleted lists | | |
| 75 | − | | `/{alias}` | Public | Show an active list, deleted tombstone, or never-used alias state | | |
| 76 | − | ||
| 77 | − | Aliases can contain multiple path segments. For example: | |
| 78 | − | ||
| 79 | − | ```text | |
| 80 | − | /burke | |
| 81 | − | /burke/links | |
| 82 | − | /teams/web/resources | |
| 83 | − | ``` | |
| 84 | − | ||
| 85 | − | Use App Router catch-all segments where needed so both public and edit routes accept nested aliases. | |
| 86 | − | ||
| 87 | − | Static application routes must take priority over public alias routing. | |
| 88 | − | ||
| 89 | − | Reserve these first path segments: | |
| 90 | − | ||
| 91 | − | - `s` | |
| 92 | − | - `api` | |
| 93 | − | - `auth` | |
| 94 | − | - `__test` | |
| 95 | − | - `.well-known` | |
| 96 | − | ||
| 97 | − | Document any additional reserved paths. | |
| 98 | − | ||
| 99 | − | ## Global navigation | |
| 100 | − | ||
| 101 | − | Provide: | |
| 102 | − | ||
| 103 | − | - The Urlist home link | |
| 104 | − | - New | |
| 105 | − | - My Lists when signed in | |
| 106 | − | - Light, Dark, and System theme choices | |
| 107 | − | - Login when signed out | |
| 108 | − | - Current user identity and Log out when signed in | |
| 109 | − | ||
| 110 | − | At mobile widths, these controls can use a labeled menu. | |
| 111 | − | ||
| 112 | − | Selecting New must not silently erase a non-empty draft. Ask for confirmation first. | |
| 113 | − | ||
| 114 | − | ## Home page | |
| 115 | − | ||
| 116 | − | The home page must contain: | |
| 117 | − | ||
| 118 | − | - The product name and core copy | |
| 119 | − | - A field for the first link URL | |
| 120 | − | - An Add link action | |
| 121 | − | - Clear validation feedback | |
| 122 | − | ||
| 123 | − | When the first valid link is added: | |
| 124 | − | ||
| 125 | − | 1. Add it to the current draft. | |
| 126 | − | 2. Navigate to `/s/new`. | |
| 127 | − | 3. Start metadata enrichment. | |
| 128 | − | ||
| 129 | − | If a draft already exists, add the link to that draft. Do not clear existing content. | |
| 130 | − | ||
| 131 | − | ## Drafts | |
| 132 | − | ||
| 133 | − | Keep one unpublished draft per browser profile. | |
| 134 | − | ||
| 135 | − | Store the draft in browser-local storage. It must not depend on the current server login session. Server metadata responses update the browser-local draft. | |
| 136 | − | ||
| 137 | − | The draft must: | |
| 138 | − | ||
| 139 | − | - Work for signed-out and signed-in users | |
| 140 | − | - Survive reload and navigation | |
| 141 | − | - Store list description, requested alias, links, order, edited metadata, and completed metadata states | |
| 142 | − | - Remain when the user logs in or out | |
| 143 | − | - Clear after successful publication | |
| 144 | − | ||
| 145 | − | Do not clear a non-empty draft without confirmation. | |
| 30 | + | Ownership is set at publication: | |
| 146 | 31 | ||
| 147 | − | Ownership is decided when the list is published: | |
| 32 | + | - Signed out: the list is anonymous, permanent, immutable, non-deletable, and never claimable. | |
| 33 | + | - Signed in: the list belongs to the current user, who can edit, soft-delete, and restore it. | |
| 148 | 34 | ||
| 149 | − | - Signed out: anonymous | |
| 150 | − | - Signed in: owned by the current mock user | |
| 35 | + | Anyone can view an active list. | |
| 151 | 36 | ||
| 152 | − | ## Link URL rules | |
| 37 | + | ## Routes | |
| 153 | 38 | ||
| 154 | − | A link destination must be an absolute HTTP or HTTPS URL. | |
| 39 | + | | Route | Behavior | | |
| 40 | + | |---|---| | |
| 41 | + | | `/` | Home page and first-link entry | | |
| 42 | + | | `/s/new` | Create or resume the one local draft | | |
| 43 | + | | `/s/edit/{alias}` | Owner-only, reloadable edit page | | |
| 44 | + | | `/s/mylists` | Current user's active and deleted lists | | |
| 45 | + | | `/{alias}` | Active public list, deleted tombstone, or unused-alias state | | |
| 155 | 46 | ||
| 156 | − | Reject: | |
| 47 | + | Aliases can have multiple segments. Use catch-all App Router routes where needed. Static app routes take priority. | |
| 157 | 48 | ||
| 158 | − | - Empty values | |
| 159 | − | - Relative URLs | |
| 160 | − | - Schemes other than HTTP and HTTPS | |
| 161 | − | - Invalid or missing hosts | |
| 162 | − | - URLs with embedded credentials | |
| 163 | − | - Values that are not one valid absolute URL | |
| 49 | + | Reserve first segments `s`, `api`, `auth`, `__test`, and `.well-known`. | |
| 164 | 50 | ||
| 165 | − | You may add `https://` to a domain-like value without a scheme. If you do, show the normalized URL before publication. | |
| 51 | + | 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. | |
| 166 | 52 | ||
| 167 | − | Show validation errors next to the URL field and associate them with the field. | |
| 53 | + | ## Draft and editor | |
| 168 | 54 | ||
| 169 | − | After a link is added, its destination URL is immutable. Show it as readable text. To change it, the author must delete the link and add a new one. | |
| 55 | + | 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. | |
| 170 | 56 | ||
| 171 | − | ## Links | |
| 57 | + | The home page accepts the first URL, adds it to the current draft, starts metadata enrichment, and opens `/s/new`. | |
| 172 | 58 | ||
| 173 | 59 | A list: | |
| 174 | 60 | ||
| 175 | − | - Must contain at least one link before publication or save | |
| 176 | − | - Has no product-level maximum number of links | |
| 177 | − | - Can contain duplicate destination URLs | |
| 178 | − | - Keeps each duplicate as an independent entry | |
| 179 | − | - Preserves link order | |
| 180 | − | ||
| 181 | − | For each link, store: | |
| 182 | − | ||
| 183 | − | - Stable ID | |
| 184 | − | - Destination URL | |
| 185 | − | - Title | |
| 186 | − | - Description | |
| 187 | − | - Image URL | |
| 188 | − | - Position | |
| 189 | − | - Metadata status | |
| 190 | − | ||
| 191 | − | Authors can: | |
| 192 | − | ||
| 193 | − | - Add links | |
| 194 | − | - Delete links | |
| 195 | − | - Edit title | |
| 196 | − | - Edit description | |
| 197 | − | - Reorder links with drag-and-drop | |
| 198 | − | ||
| 199 | − | Authors cannot directly edit: | |
| 200 | − | ||
| 201 | − | - Destination URL | |
| 202 | − | - Image URL | |
| 203 | − | ||
| 204 | − | Drag-and-drop is the primary reordering interaction. | |
| 205 | − | ||
| 206 | − | Requirements: | |
| 207 | − | ||
| 208 | − | - Each editable link has a visible drag handle. | |
| 209 | − | - A pointer user can drag a link to any position in the list. | |
| 210 | − | - The interface shows clear picked-up, dragging, valid-drop-position, and dropped states. | |
| 211 | − | - The list updates immediately after a drop. | |
| 212 | − | - The new order persists in the browser-local draft. | |
| 213 | − | - Saving an account-owned list persists the new order in SQLite. | |
| 214 | − | - The public list displays the saved order. | |
| 215 | − | - Reordering works at desktop and mobile widths. | |
| 216 | − | - Touch input is supported where the browser provides it. | |
| 217 | − | ||
| 218 | − | Also provide named Move up and Move down controls so reordering works with a keyboard and assistive technology. Drag-and-drop alone is not sufficient. | |
| 219 | − | ||
| 220 | − | ## Live metadata enrichment | |
| 221 | − | ||
| 222 | − | After a link is added, fetch and parse the live destination page on the server. | |
| 223 | − | ||
| 224 | − | Each link must show one metadata state: | |
| 225 | − | ||
| 226 | − | - Pending | |
| 227 | − | - Succeeded | |
| 228 | − | - Failed | |
| 229 | − | ||
| 230 | − | The network operation must stop no later than 10 seconds after it starts. Show Succeeded or Failed no later than one additional second after the deadline. | |
| 231 | − | ||
| 232 | − | Publication and save must wait while any link is Pending. | |
| 233 | − | ||
| 234 | − | If enrichment fails: | |
| 235 | − | ||
| 236 | − | - Keep the link | |
| 237 | − | - Show a clear failure state | |
| 238 | − | - Let the author edit title and description | |
| 239 | − | - Allow publication or save after the request reaches Failed | |
| 240 | − | ||
| 241 | − | Use this metadata precedence: | |
| 242 | − | ||
| 243 | − | | Field | First | Second | Fallback | | |
| 244 | − | |---|---|---|---| | |
| 245 | − | | Title | `og:title` | Twitter Card title | HTML `<title>`, then suitable `<h1>` | | |
| 246 | − | | Description | `og:description` | Twitter Card description | HTML meta description | | |
| 247 | − | | Image | `og:image` | Twitter Card image | Declared page icon, then `/favicon.ico` | | |
| 248 | − | ||
| 249 | − | Support Twitter Card tags that use `name` or `property`. | |
| 250 | − | ||
| 251 | − | Resolve relative images and icons against the final document URL after redirects. | |
| 252 | − | ||
| 253 | − | An HTML page with no useful metadata is still a successful enrichment. Title, description, and image are optional. | |
| 254 | − | ||
| 255 | − | Show a neutral placeholder if there is no usable image. | |
| 256 | − | ||
| 257 | − | If an author edits an enriched title or description, preserve the manual value. | |
| 258 | − | ||
| 259 | − | ## Metadata-fetch security | |
| 260 | − | ||
| 261 | − | The server-side metadata fetcher must protect internal networks. | |
| 262 | − | ||
| 263 | − | It must: | |
| 264 | − | ||
| 265 | − | - Allow only HTTP and HTTPS | |
| 266 | − | - Reject embedded credentials | |
| 267 | − | - Reject loopback addresses | |
| 268 | − | - Reject private network ranges | |
| 269 | − | - Reject link-local addresses | |
| 270 | − | - Reject multicast, unspecified, and reserved ranges | |
| 271 | − | - Reject cloud instance metadata addresses | |
| 272 | − | - Validate all resolved addresses before connecting | |
| 273 | − | - Validate the scheme, host, and resolved address after every redirect | |
| 274 | − | - Follow no more than five redirects | |
| 275 | − | - Protect against DNS rebinding | |
| 276 | − | - Stop the complete network operation after 10 seconds | |
| 277 | − | - Limit response bodies to 2 MiB or less | |
| 278 | − | - Parse only HTML or XHTML | |
| 279 | − | - Never forward application cookies, authorization headers, or user credentials | |
| 280 | − | ||
| 281 | − | Treat remote metadata as untrusted input. | |
| 282 | − | ||
| 283 | − | ## Alias rules | |
| 284 | − | ||
| 285 | − | An alias contains one or more path segments separated by `/`. | |
| 286 | − | ||
| 287 | − | Normalize ASCII letters to lowercase before checking or storing the alias. | |
| 288 | − | ||
| 289 | − | Each segment: | |
| 290 | − | ||
| 291 | − | - Contains only lowercase letters `a-z`, digits `0-9`, and hyphens | |
| 292 | − | - Contains 1 to 50 characters | |
| 293 | − | - Cannot start or end with a hyphen | |
| 294 | − | ||
| 295 | − | The complete alias, including separators, cannot exceed 200 characters. | |
| 296 | − | ||
| 297 | − | Reject aliases with: | |
| 298 | − | ||
| 299 | − | - Empty segments | |
| 300 | − | - Leading or trailing `/` | |
| 301 | − | - Two adjacent `/` characters | |
| 302 | − | - `.` or `..` segments | |
| 303 | − | - Query strings or fragments | |
| 304 | − | - Backslashes | |
| 305 | − | - Encoded slashes or backslashes | |
| 306 | − | - Reserved first segments | |
| 307 | − | ||
| 308 | − | Use the exact complete normalized path for lookup. Prefix aliases are independent, so `burke` and `burke/links` can both exist. | |
| 309 | − | ||
| 310 | − | An alias is unavailable if it belongs to: | |
| 311 | − | ||
| 312 | − | - An active list | |
| 313 | − | - A deleted list | |
| 314 | − | - An anonymous list | |
| 315 | − | ||
| 316 | − | If the author leaves the alias blank, generate an available seven-character alias that uses lowercase letters and digits. | |
| 317 | − | ||
| 318 | − | Use a database uniqueness constraint as the final collision control. If two requests use the same normalized alias, exactly one can succeed. Never overwrite an existing list. | |
| 319 | − | ||
| 320 | − | An alias cannot change after publication. | |
| 321 | − | ||
| 322 | − | ## Publication | |
| 323 | − | ||
| 324 | − | A list description is optional. | |
| 325 | − | ||
| 326 | − | Block publication or save when: | |
| 327 | − | ||
| 328 | − | - The list has no links | |
| 329 | − | - A metadata request is Pending | |
| 330 | − | - The alias is invalid | |
| 331 | − | - The alias is unavailable | |
| 332 | − | ||
| 333 | − | Repeat all validation on the server. | |
| 334 | − | ||
| 335 | − | If publication fails: | |
| 336 | − | ||
| 337 | − | - Show a clear recoverable error | |
| 338 | − | - Keep the draft | |
| 339 | − | - Do not show false success | |
| 340 | − | ||
| 341 | − | After successful publication: | |
| 342 | − | ||
| 343 | − | - Clear the draft | |
| 344 | − | - Navigate to the canonical public list path | |
| 345 | − | ||
| 346 | − | ## Anonymous publication | |
| 347 | − | ||
| 348 | − | A signed-out person can publish. | |
| 349 | − | ||
| 350 | − | Before writing the list, show a blocking confirmation with this meaning: | |
| 351 | − | ||
| 352 | − | > You are not signed in. After publication, you cannot edit or delete this list. Its alias stays reserved permanently. | |
| 353 | − | ||
| 354 | − | Use clear actions: | |
| 355 | − | ||
| 356 | − | - Publish permanently | |
| 357 | − | - Cancel | |
| 358 | − | ||
| 359 | − | Cancel must return to the unchanged draft. | |
| 360 | − | ||
| 361 | − | After confirmation, the anonymous list: | |
| 362 | − | ||
| 363 | − | - Has no account owner | |
| 364 | − | - Is immutable | |
| 365 | − | - Cannot be deleted through product interfaces or normal product APIs | |
| 366 | − | - Cannot be claimed after login | |
| 367 | − | - Never appears in My Lists | |
| 368 | − | - Keeps its alias reserved permanently | |
| 369 | − | ||
| 370 | − | Enforce these rules on the server. | |
| 371 | − | ||
| 372 | − | ## Mock GitHub login | |
| 373 | − | ||
| 374 | − | Provide a local mock GitHub provider. Do not call GitHub and do not implement OAuth. | |
| 375 | − | ||
| 376 | − | Provide two fictional users with stable, distinct: | |
| 377 | − | ||
| 378 | − | - User IDs | |
| 379 | − | - Display names | |
| 380 | − | - Avatars or placeholders | |
| 61 | + | - Requires at least one link to publish or save | |
| 62 | + | - Has no product-level link limit | |
| 63 | + | - Allows duplicate destination URLs as separate entries | |
| 64 | + | - Has an optional description and alias | |
| 381 | 65 | ||
| 382 | − | Show user A first and user B second. Each control must have the accessible name: | |
| 66 | + | For each link, store a stable ID, immutable destination URL, optional title, optional description, optional enrichment-controlled image, and position. | |
| 383 | 67 | ||
| 384 | − | ```text | |
| 385 | − | Continue as {display name} | |
| 386 | − | ``` | |
| 68 | + | Authors can add and delete links and edit titles and descriptions. They cannot edit an existing destination or image URL. | |
| 387 | 69 | ||
| 388 | − | Login must: | |
| 70 | + | 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. | |
| 389 | 71 | ||
| 390 | − | - Create a server-verifiable session | |
| 391 | − | - Survive reload | |
| 392 | − | - Use an HTTP-only cookie with an appropriate SameSite policy or an equivalent safe mechanism | |
| 393 | − | - Use the Secure cookie flag in production over HTTPS; local HTTP development and tests must still work | |
| 72 | + | ### Reordering | |
| 394 | 73 | ||
| 395 | − | Logout must end the session. | |
| 74 | + | Drag-and-drop is the primary reorder interaction. | |
| 396 | 75 | ||
| 397 | − | Use the stable user ID for ownership checks. Never trust a client-supplied owner ID. | |
| 76 | + | - Show a visible drag handle on each editable link. | |
| 77 | + | - Support pointer and touch dragging to any position. | |
| 78 | + | - Show clear picked-up, dragging, drop-position, and dropped states. | |
| 79 | + | - Update draft order immediately. | |
| 80 | + | - Save account-owned order to SQLite. | |
| 81 | + | - Show saved order on the public page. | |
| 82 | + | - Support desktop and mobile widths. | |
| 398 | 83 | ||
| 399 | − | Keep authentication behind a clear boundary so a real provider could replace it later without changing ownership rules. | |
| 84 | + | Also provide accessible Move up and Move down controls. Drag-and-drop alone is not sufficient. | |
| 400 | 85 | ||
| 401 | − | ## Account-owned lists | |
| 86 | + | ## Live metadata | |
| 402 | 87 | ||
| 403 | − | A list published while signed in belongs to the current user. | |
| 88 | + | 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. | |
| 404 | 89 | ||
| 405 | − | Only its owner can: | |
| 90 | + | 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. | |
| 406 | 91 | ||
| 407 | − | - Open its edit page | |
| 408 | − | - Save changes | |
| 409 | − | - Delete it | |
| 410 | − | - Restore it | |
| 92 | + | Metadata precedence: | |
| 411 | 93 | ||
| 412 | − | Enforce ownership on the server for every mutation. Hiding controls is not sufficient. | |
| 94 | + | 1. Open Graph | |
| 95 | + | 2. Twitter Card (`name` or `property`) | |
| 96 | + | 3. HTML title/meta description/page icon, with `<h1>` and `/favicon.ico` as final fallbacks | |
| 413 | 97 | ||
| 414 | − | The edit page must: | |
| 98 | + | 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. | |
| 415 | 99 | ||
| 416 | − | - Load the list from persistent storage | |
| 417 | − | - Work after direct navigation and reload | |
| 418 | − | - Show alias as read-only | |
| 419 | − | - Allow changes to list description | |
| 420 | − | - Allow adding and deleting links | |
| 421 | − | - Allow changing link order | |
| 422 | − | - Allow editing link titles and descriptions | |
| 423 | − | - Provide Save | |
| 424 | − | - Provide Delete list | |
| 100 | + | 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. | |
| 425 | 101 | ||
| 426 | − | If save is asynchronous, show Saving, Saved, and Failed states. Preserve unsaved input after failure. | |
| 102 | + | ## Aliases and publication | |
| 427 | 103 | ||
| 428 | − | The alias and existing destination URLs cannot change. | |
| 104 | + | Normalize aliases to lowercase. An alias has one or more `/`-separated segments. Each segment: | |
| 429 | 105 | ||
| 430 | − | ## My Lists | |
| 106 | + | - Uses only `a-z`, `0-9`, and hyphens | |
| 107 | + | - Is 1–50 characters | |
| 108 | + | - Does not start or end with a hyphen | |
| 431 | 109 | ||
| 432 | − | `/s/mylists` requires login. | |
| 110 | + | The full alias is at most 200 characters. Reject empty, `.`/`..`, reserved, query/fragment, backslash, and encoded-separator forms. | |
| 433 | 111 | ||
| 434 | − | Show only lists owned by the current user. | |
| 112 | + | 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. | |
| 435 | 113 | ||
| 436 | − | Provide: | |
| 114 | + | If blank, generate an available seven-character lowercase alphanumeric alias. Aliases never change after publication. | |
| 437 | 115 | ||
| 438 | − | - Create new list | |
| 439 | − | - Active lists section | |
| 440 | − | - Deleted lists section | |
| 441 | − | - Explicit empty states | |
| 116 | + | 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. | |
| 442 | 117 | ||
| 443 | − | Each active list summary must show: | |
| 118 | + | 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. | |
| 444 | 119 | ||
| 445 | − | - Alias | |
| 446 | − | - Description when present | |
| 447 | − | - Link count | |
| 120 | + | ## Mock login and ownership | |
| 448 | 121 | ||
| 449 | − | Selecting an active list opens its edit route. | |
| 122 | + | 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. | |
| 450 | 123 | ||
| 451 | − | Each deleted list summary must provide Restore. | |
| 124 | + | 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. | |
| 452 | 125 | ||
| 453 | − | ## Delete, tombstone, and restore | |
| 126 | + | Only an owner can load, save, delete, or restore an account list. Enforce every check on the server and ignore client-supplied owner IDs. | |
| 454 | 127 | ||
| 455 | − | Delete is a soft delete. | |
| 128 | + | 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. | |
| 456 | 129 | ||
| 457 | − | Before deletion, show a confirmation with clear Delete list and Cancel actions. | |
| 130 | + | 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. | |
| 458 | 131 | ||
| 459 | − | After deletion: | |
| 132 | + | ## Delete and restore | |
| 460 | 133 | ||
| 461 | − | - Keep all list content and ownership | |
| 462 | − | - Keep the alias reserved | |
| 463 | − | - Move the list to Deleted lists | |
| 464 | − | - Show a public tombstone at the old path | |
| 134 | + | Delete requires confirmation and performs a soft delete. Keep content, ownership, and alias forever. There is no permanent-delete action. | |
| 465 | 135 | ||
| 466 | − | The tombstone must show only: | |
| 136 | + | The public tombstone shows only: | |
| 467 | 137 | ||
| 468 | 138 | > This list was deleted. | |
| 469 | 139 | ||
| 470 | − | Do not show the former description, links, owner, or deletion reason. | |
| 471 | − | ||
| 472 | − | If the signed-in visitor is the owner, also show Restore. | |
| 473 | − | ||
| 474 | − | Other visitors and other signed-in users must not see Restore. | |
| 475 | − | ||
| 476 | − | Restoring must reactivate the same: | |
| 477 | − | ||
| 478 | − | - List | |
| 479 | − | - Alias | |
| 480 | − | - Content | |
| 481 | − | - Link order | |
| 482 | − | - Ownership | |
| 483 | − | ||
| 484 | − | There is no permanent-delete product action. | |
| 485 | − | ||
| 486 | − | ## Public list page | |
| 487 | − | ||
| 488 | − | Anyone can view an active list without logging in. | |
| 489 | − | ||
| 490 | − | Use the list description as the heading. If it is blank, use the alias. | |
| 491 | − | ||
| 492 | − | Show links in saved order. | |
| 493 | − | ||
| 494 | − | Each public link card must show: | |
| 495 | − | ||
| 496 | − | - Image or neutral placeholder | |
| 497 | − | - Title, or destination hostname or URL when title is blank | |
| 498 | − | - Description when present | |
| 499 | − | - Destination URL | |
| 500 | − | ||
| 501 | − | Opening a destination must use safe new-context behavior such as `noopener` and `noreferrer`. | |
| 502 | − | ||
| 503 | − | Provide two views: | |
| 504 | − | ||
| 505 | − | - Links | |
| 506 | − | - QR code | |
| 507 | − | ||
| 508 | − | Expose the selected view to assistive technology. | |
| 509 | − | ||
| 510 | − | ## Sharing | |
| 511 | − | ||
| 512 | − | Provide: | |
| 513 | − | ||
| 514 | − | - Copy link | |
| 515 | − | - X or Twitter share link | |
| 516 | − | - Facebook share link | |
| 517 | − | - LinkedIn share link | |
| 518 | − | ||
| 519 | − | Copy link must copy the canonical absolute public URL and announce success or failure. | |
| 520 | − | ||
| 521 | − | Platform share links must contain a correctly percent-encoded canonical URL. Include the encoded list heading where the platform supports it. | |
| 522 | − | ||
| 523 | − | User content must not inject additional share parameters. | |
| 524 | − | ||
| 525 | − | ## QR code | |
| 526 | − | ||
| 527 | − | Generate a real scannable QR code for the canonical absolute public-list URL. | |
| 528 | − | ||
| 529 | − | Requirements: | |
| 530 | − | ||
| 531 | − | - A standard decoder recovers the exact URL | |
| 532 | − | - It works in light and dark themes | |
| 533 | − | - It has sufficient contrast | |
| 534 | − | - Adjacent text or an accessible alternative identifies the encoded URL | |
| 535 | − | ||
| 536 | − | QR download is not required. | |
| 537 | − | ||
| 538 | − | ## Never-used aliases | |
| 539 | − | ||
| 540 | − | When a valid alias has never been used: | |
| 541 | − | ||
| 542 | − | - Show that the list was not found | |
| 543 | − | - Say that the alias is available | |
| 544 | − | - Provide Create a list with this alias | |
| 545 | − | ||
| 546 | − | The action must prefill the alias in the draft. | |
| 140 | + | Only the signed-in owner also sees Restore. Restore reactivates the same alias, content, order, and ownership. | |
| 547 | 141 | ||
| 548 | − | Do not silently overwrite a non-empty draft. | |
| 142 | + | ## Public list | |
| 549 | 143 | ||
| 550 | − | For an invalid or reserved path, show a generic not-found or invalid-path state. Do not offer it as an available alias. | |
| 144 | + | Use the description as the heading or the alias when blank. Show links in saved order. | |
| 551 | 145 | ||
| 552 | − | ## Themes | |
| 146 | + | 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`. | |
| 553 | 147 | ||
| 554 | 148 | Provide: | |
| 555 | 149 | ||
| 556 | − | - Light | |
| 557 | − | - Dark | |
| 558 | − | - System | |
| 559 | − | ||
| 560 | − | Persist the selection in the browser. | |
| 561 | − | ||
| 562 | − | System follows the operating system preference. | |
| 563 | − | ||
| 564 | − | Avoid a visible flash of the wrong theme when practical. | |
| 565 | − | ||
| 566 | − | Keep accessible contrast in every theme. | |
| 567 | − | ||
| 568 | − | ## Responsive behavior | |
| 569 | − | ||
| 570 | − | Support current Chromium at desktop and mobile widths down to 320 CSS pixels. | |
| 571 | − | ||
| 572 | − | At mobile widths: | |
| 573 | − | ||
| 574 | − | - Navigation can collapse into a labeled menu | |
| 575 | − | - Alias, description, and primary action stack vertically | |
| 576 | − | - Link cards keep title, URL, status, and primary actions visible | |
| 577 | − | - My Lists uses one column or an appropriate reduced grid | |
| 578 | − | - Share and view controls remain available | |
| 579 | − | - Modals fit the viewport and can scroll internally | |
| 580 | − | ||
| 581 | − | Do not require page-level horizontal and vertical scrolling together. | |
| 582 | − | ||
| 583 | − | ## Accessibility | |
| 584 | − | ||
| 585 | − | The implemented journeys must meet WCAG 2.2 AA. | |
| 586 | − | ||
| 587 | − | Requirements: | |
| 588 | − | ||
| 589 | − | - One descriptive level-one heading per page | |
| 590 | − | - Logical heading order | |
| 591 | − | - Accessible names for all controls | |
| 592 | − | - Labels and associated errors for form fields | |
| 593 | − | - Programmatic required and invalid states | |
| 594 | − | - Useful image alternatives | |
| 595 | − | - Full keyboard operation | |
| 596 | − | - Visible focus in every theme | |
| 597 | − | - Logical focus order | |
| 598 | − | - Keyboard link reordering | |
| 599 | − | - Status announcements for loading, metadata, save, publish, copy, and errors | |
| 600 | − | - Color is not the only way to show state | |
| 601 | − | - Usable at 200% zoom | |
| 602 | − | - Reflow at 320 CSS pixels | |
| 603 | − | - Respect `prefers-reduced-motion` | |
| 604 | − | ||
| 605 | − | A modal must: | |
| 606 | − | ||
| 607 | − | - Have a title and modal semantics | |
| 608 | − | - Receive focus when opened | |
| 609 | − | - Trap focus while open | |
| 610 | − | - Close on Escape when cancellation is allowed | |
| 611 | − | - Return focus to its trigger | |
| 612 | − | ||
| 613 | − | After delete or reorder, move focus to a logical nearby control. | |
| 614 | − | ||
| 615 | − | ## State and error handling | |
| 616 | − | ||
| 617 | − | Represent: | |
| 618 | − | ||
| 619 | − | - Initial loading and fatal failure | |
| 620 | − | - Metadata Pending, Succeeded, and Failed | |
| 621 | − | - Alias checking, available, invalid, and unavailable | |
| 622 | − | - Publication blocked, publishing, success, and failure | |
| 623 | − | - Save progress and failure | |
| 624 | − | - My Lists loading, results, empty, and failure | |
| 625 | − | - Public loading, active, never used, deleted, and failure | |
| 626 | − | - Copy success and failure | |
| 627 | − | - Signed-out, identity selection, signed-in, and login failure | |
| 628 | − | ||
| 629 | − | Errors must explain what the user can do next. | |
| 630 | − | ||
| 631 | − | Do not turn failures into success-shaped fallbacks. | |
| 632 | − | ||
| 633 | − | Preserve user input after failed publish or save. | |
| 634 | − | ||
| 635 | − | Use live regions for status changes that need announcement. | |
| 636 | − | ||
| 637 | − | If Publish is disabled, provide a discoverable reason. | |
| 638 | − | ||
| 639 | − | ## Data and persistence | |
| 640 | − | ||
| 641 | − | Use SQLite with direct parameterized SQL through `better-sqlite3`. | |
| 642 | − | ||
| 643 | − | Provide ordered SQL migrations or a repeatable versioned schema initializer. | |
| 644 | − | ||
| 645 | − | Enable SQLite foreign keys for every connection. | |
| 646 | − | ||
| 647 | − | Use transactions for: | |
| 648 | − | ||
| 649 | − | - Publication | |
| 650 | − | - Save | |
| 651 | − | - Delete | |
| 652 | − | - Restore | |
| 653 | − | - Reset | |
| 654 | − | ||
| 655 | − | At minimum, persist: | |
| 656 | − | ||
| 657 | − | ### User | |
| 658 | − | ||
| 659 | − | - Stable user ID | |
| 660 | − | - Provider | |
| 661 | − | - Display name | |
| 662 | − | - Avatar | |
| 663 | − | ||
| 664 | − | ### List | |
| 665 | − | ||
| 666 | − | - Stable list ID | |
| 667 | − | - Normalized immutable alias | |
| 668 | − | - Optional description | |
| 669 | − | - Anonymous or account ownership type | |
| 670 | − | - Owner user ID for account-owned lists | |
| 671 | − | - Active or deleted status | |
| 672 | − | - Created time | |
| 673 | − | - Updated time | |
| 674 | − | - Deleted time when deleted | |
| 675 | − | ||
| 676 | − | ### Link | |
| 150 | + | - Links and QR code views with accessible selected state | |
| 151 | + | - A scannable QR code for the canonical absolute list URL | |
| 152 | + | - Copy link with announced success/failure | |
| 153 | + | - Correctly encoded X/Twitter, Facebook, and LinkedIn share links | |
| 677 | 154 | ||
| 678 | − | - Stable link ID | |
| 679 | − | - Parent list ID | |
| 680 | − | - Immutable destination URL | |
| 681 | − | - Optional title | |
| 682 | − | - Optional description | |
| 683 | − | - Optional image URL | |
| 684 | − | - Position | |
| 155 | + | 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. | |
| 685 | 156 | ||
| 686 | − | Published and deleted data must survive a full application restart. | |
| 157 | + | ## Theme, responsive UI, and accessibility | |
| 687 | 158 | ||
| 688 | − | Use an environment variable for the database path and provide a safe development default. | |
| 159 | + | Provide persisted Light, Dark, and System themes. System follows OS preference. Keep accessible contrast and avoid an initial wrong-theme flash when practical. | |
| 689 | 160 | ||
| 690 | − | Exclude database files from Git. | |
| 161 | + | 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. | |
| 691 | 162 | ||
| 692 | − | ## General security | |
| 163 | + | Meet WCAG 2.2 AA: | |
| 693 | 164 | ||
| 694 | − | The server must: | |
| 165 | + | - Semantic headings, labels, names, errors, and status announcements | |
| 166 | + | - Full keyboard use, visible focus, logical focus order, and keyboard reordering | |
| 167 | + | - No color-only meaning; usable at 200% zoom; reduced-motion support | |
| 168 | + | - Modal focus entry, trap, Escape cancellation, and focus return | |
| 169 | + | - Logical focus after deletion or reordering | |
| 695 | 170 | ||
| 696 | − | - Validate all input independently of the client | |
| 697 | − | - Enforce every ownership rule | |
| 698 | − | - Use parameterized SQL | |
| 699 | − | - Derive ownership from the authenticated session | |
| 700 | − | - Render user and remote text safely | |
| 701 | − | - Prevent stored and reflected script execution | |
| 702 | − | - Encode share parameters safely | |
| 703 | − | - Use CSRF protection appropriate for the mutation design | |
| 704 | − | - Avoid exposing stack traces, secrets, internal paths, or database details | |
| 171 | + | Show truthful loading, empty, success, blocked, and error states. Errors must explain recovery. Never present failure as success. | |
| 705 | 172 | ||
| 706 | − | Keep runtime database files, secrets, build output, and dependencies out of Git. | |
| 173 | + | ## Storage and security | |
| 707 | 174 | ||
| 708 | − | ## Test reset | |
| 175 | + | Use versioned SQL migrations or an idempotent versioned initializer. Enable SQLite foreign keys. Use transactions for publish, save, delete, restore, and reset. | |
| 709 | 176 | ||
| 710 | − | Provide a development/test-only way to: | |
| 177 | + | 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. | |
| 711 | 178 | ||
| 712 | − | - Delete all published and deleted lists | |
| 713 | − | - Clear server sessions when practical | |
| 714 | − | - Restore the two stable mock users | |
| 715 | − | - Return to a deterministic clean state | |
| 716 | − | - Run repeatedly with the same result | |
| 179 | + | 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. | |
| 717 | 180 | ||
| 718 | − | Prefer: | |
| 181 | + | 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. | |
| 719 | 182 | ||
| 720 | − | ```http | |
| 721 | − | POST /__test/reset | |
| 722 | − | ``` | |
| 183 | + | ## Scripts, tests, and documentation | |
| 723 | 184 | ||
| 724 | − | with: | |
| 185 | + | Provide npm scripts: | |
| 725 | 186 | ||
| 726 | − | ```http | |
| 727 | − | 204 No Content | |
| 187 | + | ```text | |
| 188 | + | dev | |
| 189 | + | build | |
| 190 | + | start | |
| 191 | + | lint | |
| 192 | + | typecheck | |
| 193 | + | test | |
| 194 | + | test:e2e | |
| 195 | + | db:init | |
| 196 | + | db:reset | |
| 728 | 197 | ``` | |
| 729 | 198 | ||
| 730 | − | Disable or strongly protect reset in production mode. | |
| 731 | − | ||
| 732 | − | ## Required npm scripts | |
| 733 | − | ||
| 734 | − | Provide: | |
| 735 | − | ||
| 736 | − | | Script | Purpose | | |
| 737 | − | |---|---| | |
| 738 | − | | `dev` | Start development mode | | |
| 739 | − | | `build` | Create a production build | | |
| 740 | − | | `start` | Start the production build | | |
| 741 | − | | `lint` | Run linting | | |
| 742 | − | | `typecheck` | Run strict TypeScript checks | | |
| 743 | − | | `test` | Run Vitest once | | |
| 744 | − | | `test:e2e` | Run Playwright once | | |
| 745 | − | | `db:init` | Initialize or migrate SQLite | | |
| 746 | − | | `db:reset` | Safely reset development or test data | | |
| 747 | − | ||
| 748 | − | Scripts must return a nonzero status on failure. Test scripts must not start in watch mode. | |
| 749 | − | ||
| 750 | − | ## Tests | |
| 751 | − | ||
| 752 | − | Use Vitest for: | |
| 753 | − | ||
| 754 | − | - URL validation | |
| 755 | − | - Alias normalization and validation | |
| 756 | − | - Metadata precedence and relative URLs | |
| 757 | − | - SSRF address policy | |
| 758 | − | - Ownership and mutation rules | |
| 759 | − | - SQLite behavior | |
| 760 | − | - Publication, delete, and restore transactions | |
| 761 | − | - Share URL construction | |
| 762 | − | ||
| 763 | − | Use Playwright for: | |
| 764 | − | ||
| 765 | − | - Anonymous draft and permanent publication | |
| 766 | − | - Login and logout with both users | |
| 767 | − | - Account publication | |
| 768 | − | - My Lists | |
| 769 | − | - Owner editing | |
| 770 | − | - Non-owner isolation | |
| 771 | − | - Delete, tombstone, and restore | |
| 772 | − | - Public Links and QR views | |
| 773 | − | - Theme persistence | |
| 774 | − | - Pointer drag-and-drop reordering and persisted public order | |
| 775 | − | - Keyboard operation | |
| 776 | − | - Mobile layout | |
| 777 | − | ||
| 778 | − | Tests must use a separate temporary SQLite database, reset their state, avoid order dependence, and clean up started processes. | |
| 199 | + | Use Vitest for validation, aliases, metadata parsing/network policy, ownership, SQLite transactions, and share URLs. | |
| 779 | 200 | ||
| 780 | − | ## README | |
| 201 | + | 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. | |
| 781 | 202 | ||
| 782 | − | Document: | |
| 203 | + | Tests use a separate temporary database, reset state, avoid order dependence, and clean up processes. | |
| 783 | 204 | ||
| 784 | − | - Required Node.js version | |
| 785 | − | - Installation | |
| 786 | − | - Environment variables | |
| 787 | − | - Database initialization | |
| 788 | − | - Development startup | |
| 789 | − | - Production build and startup | |
| 790 | − | - Test commands | |
| 791 | − | - Mock login | |
| 792 | − | - Reset behavior | |
| 793 | − | - Database location | |
| 794 | − | - Live metadata behavior and security limits | |
| 795 | − | - Troubleshooting | |
| 796 | − | - Any assumptions you made | |
| 205 | + | The README must cover prerequisites, setup, environment, database, development and production commands, mock login, reset, tests, metadata behavior, troubleshooting, and assumptions. | |
| 797 | 206 | ||
| 798 | 207 | ## Completion | |
| 799 | 208 | ||
| 800 | 209 | Before you finish: | |
| 801 | 210 | ||
| 802 | − | 1. Install from the committed lock file. | |
| 803 | − | 2. Initialize a clean database. | |
| 804 | − | 3. Run `npm run lint`. | |
| 805 | − | 4. Run `npm run typecheck`. | |
| 806 | − | 5. Run `npm test`. | |
| 807 | − | 6. Run `npm run test:e2e`. | |
| 808 | − | 7. Run `npm run build`. | |
| 809 | − | 8. Start the production build and confirm the application responds. | |
| 810 | − | 9. Confirm that active, anonymous, and deleted data survives a complete restart. | |
| 811 | − | 10. Review every section of this instruction against the implementation. | |
| 812 | − | 11. Fix every defect or omission that you find. | |
| 813 | − | 12. Repeat the affected validation. | |
| 814 | − | ||
| 815 | − | When complete, report: | |
| 211 | + | 1. Run `npm run lint`, `npm run typecheck`, `npm test`, `npm run test:e2e`, and `npm run build`. | |
| 212 | + | 2. Start the production build and confirm the app responds. | |
| 213 | + | 3. Confirm active, anonymous, and deleted data survives restart. | |
| 214 | + | 4. Review this entire instruction and fix every missing or incorrect requirement. | |
| 215 | + | 5. Repeat affected validation. | |
| 816 | 216 | ||
| 817 | − | - What you implemented | |
| 818 | − | - Important assumptions | |
| 819 | − | - Database and architecture choices | |
| 820 | − | - Exact validation commands and results | |
| 821 | − | - Anything incomplete and the exact reason | |
| 217 | + | 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. | |