docs(future): plan — view unpublished trips on frontend when logged in
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
# View unpublished trips (drafts) on the frontend when logged in
|
||||||
|
|
||||||
|
**Status:** 📋 Not started
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
An unpublished trip (`published: false`) currently returns a hard **404** on its own
|
||||||
|
route, even for the logged-in owner. Example: `http://localhost:8081/trips/denmark-2026`
|
||||||
|
→ `HTTP 404` (verified 2026-07-08, anonymous *and* authenticated). The owner should be
|
||||||
|
able to preview a draft trip page at its real URL before publishing, while the public
|
||||||
|
still gets a 404.
|
||||||
|
|
||||||
|
The rest of the site is **already owner-aware** — the trip template, the trips listing,
|
||||||
|
and the home page all render drafts to `grav.user.authenticated` (via `.published()`
|
||||||
|
filters + `is-draft`/Draft badges). The only missing piece is the **direct route** to a
|
||||||
|
draft's own page.
|
||||||
|
|
||||||
|
## Current behaviour — verified mechanism
|
||||||
|
|
||||||
|
Traced through the Grav core running in the container (Grav 2.0.x):
|
||||||
|
|
||||||
|
- `Page::routable()` (`system/src/Grav/Common/Page/Page.php`) returns:
|
||||||
|
```php
|
||||||
|
return $this->routable && $this->published();
|
||||||
|
```
|
||||||
|
So `published: false` ⇒ `routable()` is `false`, regardless of the `routable` flag.
|
||||||
|
|
||||||
|
- `PagesProcessor.php:67` gates the request on exactly that:
|
||||||
|
```php
|
||||||
|
if (!$page->routable()) {
|
||||||
|
// build 404, fire onPageNotFound...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `PagesProcessor.php` ~line 80: after firing `onPageNotFound`, if a listener set
|
||||||
|
`$event->page`, Grav serves **that** page directly with no further routable check:
|
||||||
|
```php
|
||||||
|
if (isset($event->page)) {
|
||||||
|
unset($this->container['page']);
|
||||||
|
$this->container['page'] = $page = $event->page;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException('Page Not Found', 404);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That last hook is the clean insertion point.
|
||||||
|
|
||||||
|
## Proposed approach — small custom plugin (~40 lines)
|
||||||
|
|
||||||
|
Mirror the existing `user/plugins/cache-on-save/` custom-plugin pattern. Subscribe to
|
||||||
|
`onPageNotFound` and, for authenticated users only, resolve the requested route including
|
||||||
|
unpublished pages and hand it back:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function onPageNotFound(Event $e) {
|
||||||
|
$user = $this->grav['user'];
|
||||||
|
if (!$user->authenticated) {
|
||||||
|
return; // owners only — public still 404s
|
||||||
|
}
|
||||||
|
$route = $this->grav['uri']->path();
|
||||||
|
$page = $this->grav['pages']->find($route, true); // include unpublished
|
||||||
|
if ($page && !$page->published()) {
|
||||||
|
$e->page = $page; // serve the draft → 200
|
||||||
|
$e->stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The page then renders with its normal template. Because the theme is already owner-aware,
|
||||||
|
the trip page will display correctly for the logged-in owner.
|
||||||
|
|
||||||
|
## Decisions to make before building (brainstorm first)
|
||||||
|
|
||||||
|
1. **Scope of page types.** All unpublished pages, or just the trip tree
|
||||||
|
(`/trips/*`)? Entries and stories already show inline as drafts on the owner's trip
|
||||||
|
feed; do they also need standalone-route preview? Leaning: gate to trip/entry/story
|
||||||
|
templates to avoid unintentionally exposing every draft everywhere.
|
||||||
|
2. **Draft banner.** Add a trip-level "Draft — not published" banner when viewing an
|
||||||
|
unpublished trip (entry-level draft badges already exist; this is the trip equivalent).
|
||||||
|
3. **Non-existent vs. unpublished.** Ensure a genuinely missing route still 404s — the
|
||||||
|
`find(..., true)` + `!published()` check already distinguishes them, but cover it in a test.
|
||||||
|
|
||||||
|
## The one real risk — page-cache leak to the public
|
||||||
|
|
||||||
|
If Grav caches the 200 we serve to the owner and later hands it to an anonymous visitor,
|
||||||
|
the "owners only" gate is defeated. **Verify, don't assume:**
|
||||||
|
|
||||||
|
- Grav's Login plugin disables page caching for authenticated sessions by default.
|
||||||
|
- The theme already serves owner-only draft *content* inline today, so this exposure is
|
||||||
|
presumably mitigated somewhere already.
|
||||||
|
|
||||||
|
Add an explicit **anonymous-request assertion** (draft route → 404 for anon, even
|
||||||
|
after an authenticated hit warmed any cache).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Playwright spec:
|
||||||
|
- Authenticated owner → `GET /trips/<draft-slug>` returns 200 and renders the trip page.
|
||||||
|
- Anonymous → same route returns 404.
|
||||||
|
- Anonymous after an authenticated hit → still 404 (cache-leak guard).
|
||||||
|
- Genuinely missing route → 404 for everyone.
|
||||||
|
|
||||||
|
## Effort
|
||||||
|
|
||||||
|
**Low** — roughly half a day including the Playwright spec. Single custom plugin plus an
|
||||||
|
optional small theme partial for the draft banner.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Investigation session: 2026-07-08 ("hotfixes").
|
||||||
|
- Pattern to copy: `user/plugins/cache-on-save/`.
|
||||||
|
- Related owner-aware theme logic: `templates/trip.html.twig` (`owner_can_edit`),
|
||||||
|
`templates/trips.html.twig` (`is_owner`), `templates/home.html.twig`.
|
||||||
Reference in New Issue
Block a user