components-web-app / components-web-app/api-components-bundle

Feature: Route-level live / scheduled publication date

Open
#224 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
32
Forks
8
PR merge metrics
No merged PRs in 30d

Description

Problem

A Route is live the moment it exists. There is no way to say "this URL goes live on Friday", and no way to take a URL offline without deleting the Route — which loses the URL, its redirectedFrom chain, and any children hanging off cascadeChildPaths.

Today the only publication signal for a page is the presence of a Route, which is all-or-nothing and immediate.

Desired behaviour

A per-Route publication state:

  • Live — current behaviour, resolvable by anyone.
  • Scheduled — a publishedAt-style datetime. Before that moment the route behaves, for public traffic, exactly as if it did not exist (404). At/after it, it is live with no further action.
  • (Implied third state: a datetime of null = not live / taken offline indefinitely, keeping the URL reserved.)

Admins (ROLE_ADMIN, or whatever publishable.permission resolves to) must still resolve and edit a scheduled route so the page can be previewed and built before launch.

The scheduling semantics already exist

PublishableExtension::updateQueryBuilderForUnauthorizedUsers() is already exactly this rule:

->andWhere("$alias.$configuration->fieldName IS NOT NULL")
->andWhere("$alias.$configuration->fieldName <= :currentTime")

So "future publishedAt = not yet visible to the public, becomes visible on its own" is proven machinery — a component with a future publishedAt is already a scheduled component.

But #[Silverback\Publishable] is probably the wrong tool here, and this is the first thing to decide:

  • Publishable is a draft/published pairpublishedResource / draftResource, PublishableEventListener merging a draft into its published twin, ?published= on every write. A Route has no draft twin; there is one Route row and the question is only when it starts resolving.
  • Adopting the full attribute would drag the whole pairing lifecycle, the Route:publishable serialization groups and the ?published= write-routing onto an entity that needs none of it.

Likely shape instead: a plain nullable publishedAt (name TBD — liveAt/publishedAt) on Route, plus a small query extension + voter check reusing the same IS NOT NULL AND <= :now predicate. Worth reviewing whether the predicate can be factored out of PublishableExtension so there is one implementation of "is it live yet".

Where the gate has to apply

Route resolution is not the only path to a page's content, so a single check in RouteStateProvider is not enough. Every one of these needs deciding:

  • RouteVoter::voteOnAttribute() / read_route — the item GET /_/routes/{path}. This is the natural home for the check, and gets most of the rest for free (below).
  • GetCollection of routes — a not-yet-live route must not appear in the public collection (a Doctrine query extension, since voters do not run per-item on collections — same reasoning as the DenyAccessListener docblock).
  • ResourceManifestVoter / ResourceManifestStateProvider/_/resource_manifest/{path|uuid} is a second door to the same tree.
  • ComponentVoter::voteByRoute() and isRouteReachableResource() — these issue a sub-request to the route IRI and treat 401/403 as "not reachable". A 404 is not caught thereisPathReachable() only swallows HTTP_UNAUTHORIZED / HTTP_FORBIDDEN and rethrows anything else. If a scheduled route 404s, that rethrow turns a component access check into a 500. Decide whether a scheduled route should 403 (cleanest for the voter chain) and be translated to a 404 at the edge, or whether isPathReachable() should also catch 404.
  • DenyAccessListener::isPageDataAllowedByRoute() — same, via RouteVoter.
  • RouteChildrenStateProvider and /routes/{id}/redirects — should a public caller see not-yet-live children? (Probably not; admin yes.)
  • cascadeChildPaths — a scheduled parent whose path changes before go-live should still cascade.
  • Redirects — a Route whose redirect target is not yet live should behave as if the target does not exist, rather than 308ing into a 404.

Caching (the non-obvious one)

CacheHeadersEventListener currently marks authenticated requests to Route as private, no-store and leaves anonymous ones public with API Platform's s-maxage.

A scheduled route breaks that assumption: an anonymous 404 for /launch cached with a long s-maxage will still be a 404 after the go-live moment, because nothing invalidates it — the resource did not change, the clock moved. Options:

  • Cap s-maxage at min(default, secondsUntilGoLive) for a scheduled route (and for a route that is live, at the next scheduled state change if one exists).
  • Or purge on schedule, which needs a scheduler and is worse.

The same applies to the manifest response and to any component reachable only via that route. This needs solving as part of the feature, not after — a cached pre-launch 404 is the exact failure mode the feature is meant to avoid.

Relationship to #186

#186 (#[Publishable] on AbstractPage / AbstractPageData) is the page-level draft/live toggle and is deliberately deferred — the blockers recorded there are component permission inheritance, undecided front-end draft/live UX, and the hero-component state conflict (a page and a hero component each having their own publishedAt producing incoherent states).

Those blockers mostly do not apply to a Route-level gate, which is why this is worth doing separately rather than waiting on #186:

  • It gates URL resolution, not resource draft state, so there is no second publishedAt on the page or its components to disagree with.
  • No hero-component conflict: the page's content publishing story is unchanged.
  • The front-end UX question is much smaller — one flag on the route admin, not a whole draft-preview scheme.

What it does not give you is #186's other half: taking a page offline while keeping the entity in its current state is possible here (clear the date), but "this page is a draft even though its URL is live" is not.

Decision to record before implementing: whether this supersedes #186, complements it, or should be implemented as the same mechanism applied at both levels.

Acceptance criteria

  • Route gains a nullable publication datetime, exposed on the admin-readable groups and writable by ROLE_ADMIN
  • Anonymous GET /_/routes/{path} for a route with a future/absent date does not resolve the page (403/404 — decision above)
  • Anonymous GET /_/resource_manifest/{path} for the same route likewise
  • The route does not appear in the anonymous route collection, the children endpoint, or the sitemap source
  • Admin access, editing and preview of a scheduled route is unaffected
  • Components reachable only via a not-yet-live route are not publicly readable, and the check does not 500 (isPathReachable() behaviour above)
  • Cache headers cannot outlive the go-live moment
  • Existing routes migrate to "live" (backwards compatible)
  • Behat: public denied before the date, public allowed after, admin allowed throughout, collection/children/redirect behaviour

Front-end counterpart: components-web-app/cwa-nuxt-module#287

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing RouteVoter, the route collection query extension, ResourceManifestVoter/ResourceManifestStateProvider, ComponentVoter, RouteChildrenStateProvider, and CacheHeadersEventListener. Trace how anonymous and admin requests reach these entry points, then resolve the stated 403/404 and caching decisions. Done means the acceptance criteria pass, including Behat coverage for scheduling, collections, children, redirects, manifests, components, caching, and migration.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.