Skip to main content

Listings::Collection

Purpose

A Collection is a named, hand-picked list of public job listings with its own shareable page at /collections/{slug}. It exists because marketing needs to promote a specific set of jobs through channels where the link cannot be edited after it is sent — push notifications, in-app banners, WhatsApp blasts. A pushed link is frozen, so the URL must name a record we control (the collection) instead of encoding the current answer to a query. Marketing edits the collection; everyone who already received the link sees the updated list.

The first collection was built for a real request: Alvin (marketing) needed a page showing only the 10 July McDonald's shifts that still had 0 applicants, pushed to workers before the promotion date. The company landing page was too broad — it mixes other outlets, other dates, and shifts that already have applicants.

Two alternatives were considered and rejected:

  • Job IDs in the query string (/jobs?ids=8231,8232,...) — the list is frozen inside every link already sent, the URL is long and fragile, and internal IDs are exposed publicly.
  • Nesting under the company page (/companies/mcdonalds/urgent-jobs) — the company page is an evergreen search-engine asset while a campaign page is disposable, and nesting locks the feature to one company. A collection can span companies ("urgent shifts this weekend").

Membership is manual in this design: a person picks the exact jobs. A rule-based ("smart") collection — for example "all McDonald's shifts on 10 July with 0 applicants", evaluated automatically — is a known future direction, not part of this model today. It is blocked on two data gaps: job listings cannot be filtered by shift date, and applicant counts are not available in this system.

Model Context

ContextDetails
AggregateListings::Collection + Listings::CollectionItem
LayerMarketplace read surface (merchandising — deciding which jobs get a promoted page)
Upstream dependenciesListings::Job rows must already exist for every member
Downstream dependentsNone today. The public collection page reads it; a future rule-based membership source would extend it

State Machine

FromToTriggerNotes
(new)activeUC-1A collection is publicly resolvable from the moment it exists
activearchivedUC-4End of life. The slug stays reserved and is never reused

Note the difference between three ways a collection stops showing jobs:

  • All members closed — the page shows the ended state. The collection is still active; if a member reopens, it reappears.
  • ends_at passed — the page shows the ended state permanently, but the record stays active for reporting.
  • archived — the collection is not publicly resolvable at all. Visitors following an old link are sent to the main job list.

Use Cases

IDUse CaseTriggerActor
UC-1Create a collection for a marketing campaignMarketing prepares a push that needs a shareable pageAdmin (ops)
UC-2Edit a collection's details and membership while the campaign runsMarketing adds, removes, or reorders promoted jobsAdmin (ops)
UC-3View a collection page with its open member jobsA person opens a pushed or shared collection linkTalent
UC-4Archive a collection when the campaign is overThe promotion has ended and the page should retireAdmin (ops)

UC-1: Create a collection for a marketing campaign

FieldDetails
ActorIdentities::Admin (ops, acting on a marketing request)
TriggerMarketing prepares a push campaign that needs a shareable page

Preconditions:

  • Every job to include already exists as a Listings::Job.
  • The chosen slug is not used by any collection, including archived ones.

System Behaviour:

  1. The system creates the collection with a slug, a title, an optional subtitle, and optional starts_at / ends_at window.
  2. The system creates one Listings::CollectionItem per member, recording the display order marketing chose.
  3. The collection starts as active and indexable = false.

Business Rules:

  • The slug is chosen once. It never changes after creation, because the link may already be distributed.
  • indexable starts as false. Campaign pages must stay out of search engines — they expire and would become dead search results. Only stable, evergreen collections are flipped to true, deliberately.
  • Marketing supplies jobs as job page URLs, not internal IDs — each URL resolves to exactly one Listings::Job.

Postconditions:

  • The collection page is reachable at /collections/{slug} and shows the open members in order.

Open Questions:

  • Should a collection have a maximum member count? Hand-picked lists are naturally small, but nothing enforces it.

UC-2: Edit a collection's details and membership while the campaign runs

FieldDetails
ActorIdentities::Admin (ops, acting on a marketing request)
TriggerMarketing adds, removes, or reorders promoted jobs

Preconditions:

  • The collection is active.

System Behaviour:

  1. The system adds or removes Listings::CollectionItem rows, or updates their positions.
  2. The system may update title, subtitle, starts_at, ends_at, and indexable.
  3. Every visitor who opens the already-distributed link sees the updated list immediately.

Business Rules:

  • The slug cannot be edited. This is the core promise of the model: the link keeps working while the contents change.
  • A job can be a member of many collections, but at most once per collection.

Postconditions:

  • The public page reflects the new membership and order on the next view.

UC-3: View a collection page with its open member jobs

FieldDetails
ActorTalent::Profile (job seeker or gig worker; also anonymous visitors — no login is required)
TriggerA person opens a pushed or shared collection link

Preconditions:

  • The collection is active.

System Behaviour:

  1. The system resolves the slug and loads the members whose Listings::Job status is open, in marketing's order.
  2. The page renders the collection title, subtitle, and the standard job cards; each card opens the normal job detail page.
  3. If indexable is false, the page tells search engines not to index it.
  4. If there are no open members, or ends_at has passed, the page shows an ended message ("All shifts in this promotion have been taken.") with a link to the main job list.
  5. If the slug does not resolve (unknown, or the collection is archived), the visitor is sent to the main job list instead of an error page.

Business Rules:

  • Only open members are shown. A filled or removed job disappears from the page without any change to the collection itself — the membership row stays, the listing's own status hides it.
  • Order is marketing's position, never recency.
  • A previously shared link must never land on an error page. Links live in notification trays for weeks.

Postconditions:

  • Read-only operation — no data changes.

UC-4: Archive a collection when the campaign is over

FieldDetails
ActorIdentities::Admin (ops)
TriggerThe promotion has ended and the page should retire

Preconditions:

  • The collection is active.

System Behaviour:

  1. The system sets the status to archived.
  2. The public page stops resolving; visitors following the old link are sent to the main job list.

Business Rules:

  • Archival is the only way a collection ends. Collections are never hard-deleted, and the slug is never released for reuse — a dead link must not come back to life showing different content.

Postconditions:

  • The collection and its membership remain queryable internally for campaign reporting.

Open Questions:

  • Is un-archiving allowed if a collection is archived by mistake, or is the correction path "create a new collection with a new slug"?

Invariants

  1. A slug is unique across all collections — including archived ones — and never changes after creation.
  2. A Listings::Job appears at most once per collection.
  3. Only members whose Listings::Job status is open are publicly visible. Closed members are hidden by the listing's own status, not removed from the collection.
  4. A collection with indexable = false always instructs search engines not to index its page.
  5. A collection past its ends_at shows the ended state, regardless of how many members are still open.
  6. A previously shared collection link never renders an error page: it shows the collection, the ended state, or forwards to the main job list.
  7. Collections end by archival only — never hard deletion.
  8. Public display order is position, controlled by the editor — never creation time or recency.

Model Interactions

Related ModelRelationshipInteraction
Listings::CollectionItemhas_many (aggregate child)Holds one membership with its display position; created and removed through the collection
Listings::Jobhas_many, through: collection_itemsMembership points at the live listing — no snapshot. Title, pay, and status changes on the listing appear on the collection page immediately
Gig::TempJob / Careers::Jobindirect, via Listings::JobThe source systems flip a listing to closed when a shift fills or a posting ends; that alone prunes the collection page — the collection never tracks applicant counts itself