Skip to content

Instantly share code, notes, and snippets.

@burkeholland
Created February 28, 2026 01:18
Show Gist options
  • Select an option

  • Save burkeholland/ed4beda0024c09426aa084bfb8c8e41a to your computer and use it in GitHub Desktop.

Select an option

Save burkeholland/ed4beda0024c09426aa084bfb8c8e41a to your computer and use it in GitHub Desktop.

URL List — Product Requirements Document

1. Overview

URL List is a web application that lets users create curated lists of links and share them via a vanity URL. Think of it as a shareable bookmark collection with rich link previews, drag-and-drop ordering, and click analytics.

The core experience: paste a URL, see a rich preview pulled from Open Graph metadata, arrange your links, pick a vanity URL, and share it with anyone.

1.1 Target Use Cases

  • Content creators sharing resource collections with their audience
  • Teams sharing curated link sets (tools, articles, references)
  • Anyone who wants a clean, shareable page of links with a memorable URL
  • Conference speakers sharing talk resources
  • Educators sharing course materials

1.2 Firebase Project

  • Project Name: LinkList
  • Project ID: linklist-demo-app
  • Project Number: 391119676828
  • This is an existing Firebase project. All existing resources in this project can be cleared and repurposed for URL List.

2. User Roles

Role Description
Visitor Anyone with a vanity URL. Can view published lists and click links.
Anonymous Composer A user composing a list who has not yet signed in. List state is persisted in localStorage.
Authenticated User A signed-in user who can publish, edit, unpublish, and delete lists. Can view analytics for their own lists.

3. Authentication

  • Provider: Google Sign-In via Firebase Authentication
  • Flow: Users can compose a list without signing in. Authentication is required only at the point of publishing.
  • Session persistence: Firebase Auth default (persisted across browser sessions)
  • Account deletion: Users can delete their account from a settings/profile page. Deleting an account permanently removes:
    • All published lists
    • All analytics data
    • The user's auth record from Firebase
    • All associated vanity URLs (freed for reuse)

4. Pages & Navigation

4.1 Homepage (/)

Purpose: Marketing/landing page that immediately invites action.

Layout:

  • Hero section with app name, tagline, and value proposition
    • Suggested tagline: "Create beautiful, shareable link collections in seconds."
  • A single, prominent input field with placeholder text: "Paste your first link"
  • Subtle supporting text beneath the input: "No sign-up required to start building your list."
  • Below the fold (optional): brief feature highlights (rich previews, vanity URLs, analytics)

Behavior:

  • When the user pastes or types a valid URL and submits (Enter or button), they are redirected to the Compose Page (/compose) with that URL pre-loaded as the first link
  • The URL is immediately resolved for Open Graph metadata
  • If the input is not a valid URL, show inline validation error: "Please enter a valid URL"

4.2 Compose / Edit Page (/compose and /edit/:listId)

Purpose: The primary workspace for building and editing a link list.

Sections:

Link Input

  • Input field at the top: "Add a link"
  • On submission, the URL is fetched for Open Graph metadata (title, description, image/favicon)
  • While fetching, show a loading skeleton card
  • If OG fetch fails, show the raw URL with a generic link icon and allow manual entry of title/description
  • Duplicate URL detection — warn (but don't block) if the same URL is already in the list

Link Cards

Each link is displayed as a card showing:

  • Thumbnail/Image: OG image if available; fallback to site favicon; fallback to generic icon
  • Title: Pulled from OG; editable by user (inline edit)
  • Description: Pulled from OG; editable by user (inline edit)
  • URL: Displayed (truncated if long), not editable
  • Drag handle: For reorder via drag-and-drop
  • Delete button: Removes the link from the list (with confirmation if list is published)

Link Ordering

  • Drag-and-drop reordering of link cards
  • Order is preserved on save/publish
  • Newly added links appear at the bottom of the list

List Metadata

  • List Title (required): Text input, max 100 characters
    • Validation: Cannot be empty at publish time
  • List Description (optional): Textarea, max 500 characters
    • Used in OG tags when the vanity URL is shared
  • Vanity URL (required for publish):
    • Input with the domain prefix shown as a non-editable label: urllist.app/
    • Supports path segments (e.g., burke/dev-toolsurllist.app/burke/dev-tools)
    • Allowed characters: alphanumeric, hyphens, forward slashes, underscores
    • No leading or trailing slashes; no consecutive slashes
    • Minimum 2 characters, maximum 128 characters
    • Real-time uniqueness check — as the user types, debounced check against Firestore. Show ✓ (available) or ✗ (taken) indicator
    • Once published, the vanity URL cannot be changed. Display it as read-only on the edit page with a note: "Vanity URLs cannot be changed after publishing."
    • Reserved slugs: The path segment app is reserved (for application routes like /app/my-lists, /app/settings). Any vanity URL starting with app/ or equal to app is rejected with the message: "This URL is reserved."

Publish / Save Controls

  • Save Draft button: Saves to Firestore (if authenticated) or localStorage (if not). Does not make the list publicly accessible.
  • Publish button:
    • If user is not authenticated: Trigger Google Sign-In flow. On success, proceed to publish. The list data in localStorage is migrated to Firestore under the user's account.
    • If user is authenticated: Publish immediately. The list becomes accessible at its vanity URL.
    • Validation before publish:
      • At least one link is required
      • List title is required
      • Vanity URL is required and must be available
  • Unpublish button (only on edit page for published lists):
    • Removes public access. The vanity URL now shows a "This list is no longer available" page.
    • The vanity URL remains reserved/owned by the user
    • The user can re-publish at any time

localStorage Persistence

  • All compose state (links, metadata, vanity URL input) is continuously saved to localStorage
  • On page load, if localStorage has compose data and no listId param, restore the draft
  • On successful publish or save to Firestore, clear the localStorage draft
  • Storage key: urllist_draft
  • Data format: JSON with links[], title, description, vanityUrl, lastModified

4.3 My Lists Page (/app/my-lists)

Purpose: Dashboard for authenticated users to manage their lists.

Requires authentication. Redirect to homepage with a toast message if not signed in.

Layout:

  • Grid or list of cards, one per list
  • Each card shows:
    • List title
    • Vanity URL (as a clickable link)
    • Status badge: Published / Draft / Unpublished
    • Link count: e.g., "12 links"
    • View count: Total views (from analytics)
    • Last modified date
  • Sorted by last modified (most recent first)
  • Empty state: "You haven't created any lists yet." with a CTA button to start composing

Actions per list:

  • Edit: Navigates to /edit/:listId
  • Delete: Confirmation modal → permanently deletes the list, frees the vanity URL, removes analytics data
  • Copy vanity URL: One-click copy to clipboard

4.4 Public List Page (/:vanityUrl)

Purpose: The shared, public-facing view of a published list.

Layout:

  • List title (prominent, top of page)
  • List description (below title, if provided)
  • Author attribution (optional): Display name from Google profile
  • Link cards in the order set by the author:
    • Thumbnail/image
    • Title
    • Description
    • URL domain label (e.g., "github.com")
    • Entire card is clickable — opens the link in a new tab
  • Clean, focused design — no edit controls, no navigation clutter
  • Footer with subtle "Made with URL List" branding and link to homepage

Behavior:

  • Each page load increments the view counter for the list
  • Each link click increments the click counter for that specific link
  • Click tracking is done via a redirect endpoint or client-side event before navigation

OG Meta Tags (for social sharing):

  • og:title → List title
  • og:description → List description (or auto-generated: "A curated collection of X links")
  • og:image → Thumbnail of the first link's OG image (or a generated branded image)
  • og:url → The full vanity URL
  • twitter:cardsummary_large_image

Unpublished/Deleted List:

  • If a vanity URL points to an unpublished or deleted list, show a clean page:
    • "This list is no longer available."
    • Link back to homepage

4.5 Analytics Page (/app/analytics/:listId)

Purpose: Simple analytics dashboard for a specific list.

Accessible from: My Lists page or Edit page (via an "Analytics" button/link).

Displays:

  • Total views for the list
  • Link-by-link click counts:
    • Table or card layout showing each link with its title, URL, and total clicks
    • Sorted by click count (highest first), with an option to sort by list order
  • Top-level summary: "This list has been viewed X times with Y total link clicks"

5. Data Model (Firestore)

5.1 Collections

users collection

users/{userId}
{
  displayName: string,        // from Google profile
  email: string,              // from Google profile
  photoURL: string,           // from Google profile
  createdAt: timestamp,
  updatedAt: timestamp
}

lists collection

lists/{listId}
{
  ownerId: string,            // Firebase Auth UID
  title: string,
  description: string,
  vanityUrl: string,          // unique, indexed
  status: "draft" | "published" | "unpublished",
  links: [
    {
      id: string,             // auto-generated unique ID
      url: string,
      title: string,
      description: string,
      image: string,          // OG image URL
      favicon: string,        // site favicon URL
      order: number,          // sort order
      clicks: number,         // total click count
      addedAt: timestamp
    }
  ],
  views: number,              // total view count
  createdAt: timestamp,
  updatedAt: timestamp,
  publishedAt: timestamp | null,
  unpublishedAt: timestamp | null
}

vanityUrls collection

vanityUrls/{vanityUrlSlug}    // slug with slashes encoded or as subcollections
{
  listId: string,
  ownerId: string,
  createdAt: timestamp
}

This collection enables fast uniqueness checks and lookups for public page routing.

5.2 Firestore Security Rules (High-Level)

  • Users can only read/write their own user document
  • Lists can be read by anyone if status == "published"; only the owner can write
  • VanityUrls can be read by anyone (for uniqueness checks); only created/deleted by the list owner
  • Analytics counters (views, clicks) need to be incrementable by anyone (use increment() in security rules or Cloud Functions)

5.3 Indexes

  • lists collection: composite index on ownerId + updatedAt (for My Lists query)
  • vanityUrls collection: document ID is the slug (natural index)

6. Open Graph Metadata Fetching

6.1 Server-Side Fetching

  • OG metadata must be fetched server-side (Firebase Cloud Function or similar) because most sites block client-side CORS requests
  • Endpoint: Cloud Function fetchOgMetadata
    • Input: { url: string }
    • Output: { title, description, image, favicon, url }
    • Behavior:
      • Fetch the URL with a reasonable timeout (5 seconds)
      • Parse HTML for OG meta tags (og:title, og:description, og:image)
      • Fallback to <title> tag and meta[name="description"] if OG tags are missing
      • Extract favicon from <link rel="icon"> or fall back to /favicon.ico
      • Return partial data if some fields are unavailable
      • Rate limit: 30 requests per minute per user

7. Vanity URL Routing

7.1 Route Resolution

  • All routes under /app/* are reserved for application pages
  • All other routes (/*) are treated as potential vanity URLs
  • Resolution order:
    1. Check if the path matches a known application route → serve app page
    2. Look up the path in the vanityUrls collection → serve public list page
    3. No match → serve 404 page

7.2 Firebase Hosting Rewrites

  • Configure Firebase Hosting rewrites:
    • /app/** → SPA index.html (or relevant app route)
    • /** → Cloud Function resolveVanityUrl (handles OG tag rendering for social sharing via SSR)

7.3 Server-Side Rendering for OG Tags

  • The public list page must be server-rendered (or pre-rendered) so that social media crawlers can read OG meta tags
  • Implementation: Firebase Cloud Function that renders the HTML <head> with correct OG tags, then the client-side app hydrates the rest
  • Cache the rendered page with appropriate Cache-Control headers

8. Analytics Implementation

8.1 View Tracking

  • On each public list page load, increment the views counter on the list document
  • Use Firestore FieldValue.increment(1) to avoid race conditions
  • Optional: debounce by IP/session to avoid inflated counts from refreshes (can be a v2 enhancement)

8.2 Click Tracking

  • On link click, increment the clicks counter for that specific link in the links array
  • Implementation: client-side event fires before navigation (use navigator.sendBeacon() or a small Cloud Function endpoint)
  • Endpoint: Cloud Function trackClick
    • Input: { listId: string, linkId: string }
    • Output: 204 No Content

9. Validation Rules Summary

Field Rule
Link URL Must be a valid URL (http/https). Show error: "Please enter a valid URL"
List Title Required for publish. 1-100 characters.
List Description Optional. Max 500 characters.
Vanity URL Required for publish. 2-128 characters. Alphanumeric, hyphens, underscores, forward slashes. No leading/trailing/consecutive slashes. Must be unique. Cannot start with app.
Links At least 1 link required to publish.

10. Error States & Edge Cases

Scenario Behavior
OG fetch fails Show raw URL with generic icon. Allow manual title/description entry.
OG fetch times out Same as fetch fail. Show toast: "Couldn't fetch link details. You can edit them manually."
Vanity URL taken Real-time indicator. Block publish. Message: "This URL is already taken."
Vanity URL reserved Block publish. Message: "This URL is reserved."
User loses connection while composing localStorage ensures no data loss. Show offline indicator.
User signs in after composing as anonymous Migrate localStorage draft to Firestore under user's account.
User navigates away from unsaved changes Browser beforeunload prompt: "You have unsaved changes."
Published list has 0 links (all deleted) Auto-unpublish. Notify user.
Account deletion with published lists All lists are deleted. Vanity URLs show "no longer available" during a grace period, then are freed.
Very long list (100+ links) Virtualized rendering for performance. Pagination not required for v1 but consider lazy loading.

11. Non-Functional Requirements

  • Performance: Public list pages should load in under 2 seconds on a 3G connection
  • Accessibility: WCAG 2.1 AA compliance. Keyboard-navigable drag-and-drop (arrow keys to reorder). Screen reader support for all interactive elements.
  • Responsive Design: Mobile-first. All pages must be fully functional on mobile, tablet, and desktop.
  • SEO: Public list pages must be crawlable with proper OG tags and semantic HTML.
  • Security:
    • All Firestore access controlled by security rules
    • Cloud Functions validate all input
    • Rate limiting on OG fetch and analytics endpoints
    • XSS prevention on user-provided content (titles, descriptions)

12. Tech Stack

Layer Technology
Hosting Firebase Hosting
Frontend Framework TBD (React/Next.js/SvelteKit recommended) — must support SSR or SSG for OG tags
Authentication Firebase Authentication (Google Sign-In)
Database Cloud Firestore
Server Functions Firebase Cloud Functions (OG fetch, click tracking, vanity URL resolution, SSR)
Storage Firebase Storage (if needed for generated OG images)
Firebase Project linklist-demo-app (existing project — clear and repurpose)

13. Future Considerations (Out of Scope for v1)

  • Custom themes/branding for lists
  • Collaborative list editing (multiple editors)
  • Link health checking (detect broken links)
  • Import from browser bookmarks
  • Embed widget (embed a list on another site)
  • API access for programmatic list creation
  • Time-series analytics (views/clicks over time)
  • Custom domains for vanity URLs
  • Categories/tags for links within a list
  • Search across public lists (discovery)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment