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.
- 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
- 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.
| 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. |
- 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)
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"
Purpose: The primary workspace for building and editing a link list.
Sections:
- 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
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)
- Drag-and-drop reordering of link cards
- Order is preserved on save/publish
- Newly added links appear at the bottom of the list
- 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-tools→urllist.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
appis reserved (for application routes like/app/my-lists,/app/settings). Any vanity URL starting withapp/or equal toappis rejected with the message: "This URL is reserved."
- Input with the domain prefix shown as a non-editable label:
- 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
- All compose state (links, metadata, vanity URL input) is continuously saved to localStorage
- On page load, if localStorage has compose data and no
listIdparam, 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
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
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 titleog: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 URLtwitter:card→summary_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
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"
users/{userId}
{
displayName: string, // from Google profile
email: string, // from Google profile
photoURL: string, // from Google profile
createdAt: timestamp,
updatedAt: timestamp
}
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/{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.
- 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)
listscollection: composite index onownerId+updatedAt(for My Lists query)vanityUrlscollection: document ID is the slug (natural index)
- 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 andmeta[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
- Input:
- All routes under
/app/*are reserved for application pages - All other routes (
/*) are treated as potential vanity URLs - Resolution order:
- Check if the path matches a known application route → serve app page
- Look up the path in the
vanityUrlscollection → serve public list page - No match → serve 404 page
- Configure Firebase Hosting rewrites:
/app/**→ SPAindex.html(or relevant app route)/**→ Cloud FunctionresolveVanityUrl(handles OG tag rendering for social sharing via SSR)
- 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-Controlheaders
- On each public list page load, increment the
viewscounter 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)
- On link click, increment the
clickscounter for that specific link in thelinksarray - 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
- Input:
| 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. |
| 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. |
- 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)
| 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) |
- 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)