BookStackApp/BookStack · error · NotFoundException

errors.page_not_found

Error message

errors.page_not_found

What it means

PageQueries::findVisibleByIdOrFail fetches a page by ID through the visibility-restricted findVisibleById query. When the query returns null — page absent, soft-deleted, or invisible to the current user — it throws NotFoundException with 'errors.page_not_found'. It is the fail-fast wrapper converting 'no visible row' into a 404 exception.

Source

Thrown at app/Entities/Queries/PageQueries.php:42

    /**
     * @return Builder<Page>
     */
    public function start(): Builder
    {
        return Page::query();
    }

    public function findVisibleById(int $id): ?Page
    {
        return $this->start()->scopes('visible')->find($id);
    }

    public function findVisibleByIdOrFail(int $id): Page
    {
        $page = $this->findVisibleById($id);

        if (is_null($page)) {
            throw new NotFoundException(trans('errors.page_not_found'));
        }

        return $page;
    }

    public function findVisibleBySlugsOrFail(string $bookSlug, string $pageSlug): Page
    {
        /** @var ?Page $page */
        $page = $this->start()->with('book')
            ->scopes('visible')
            ->whereHas('book', function (Builder $query) use ($bookSlug) {
                $query->where('slug', '=', $bookSlug);
            })
            ->where('slug', '=', $pageSlug)
            ->first();

        if (is_null($page)) {
            throw new NotFoundException(trans('errors.page_not_found'));

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Confirm the page ID exists and is not in the trash (Page::withTrashed()->find($id) locally or check the Trash screen).
  2. Verify the user's role has view access or entity-level restrictions allowing the page.
  3. Catch NotFoundException around the call and handle missing pages gracefully.
  4. If the page was moved, its ID persists — check the URL/slug rather than recreating content.

Example fix

// before
$page = $pageQueries->findVisibleByIdOrFail($id);
// after
try {
    $page = $pageQueries->findVisibleByIdOrFail($id);
} catch (NotFoundException $e) {
    Log::warning('Page not visible', ['id' => $id]);
    abort(404);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!\BookStack\Entities\Page::where('id', $id)->exists()) {
    throw new \InvalidArgumentException("Page {$id} does not exist");
}

Type guard

function pageIsVisible(\BookStack\Entities\Queries\PageQueries $q, int $id): bool {
    return $q->findVisibleById($id) !== null;
}

Try / catch

try {
    $page = $pageQueries->findVisibleByIdOrFail($id);
} catch (\BookStack\Exceptions\NotFoundException $e) {
    Log::warning('Page missing/invisible', ['id' => $id]);
    abort(404);
}

Prevention

When it happens

Trigger: Calling findVisibleByIdOrFail($id) with an ID that doesn't exist, references a soft-deleted (trashed) page, or a page the current user cannot view due to role/restriction permissions.

Common situations: API integrations holding stale page IDs after deletion; deep links to pages removed during book reorganization; users without view permissions receiving 404 instead of 403; importing content into the wrong instance.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/722512caf6282ebf. Report an issue: GitHub.