URL и permalink

← Шаблоны · Назад к README · Конфигурация →

Как вычисляется url документа

Колонка url вычисляется один раз, на импорте, внутри DocumentImportService::persist() — никогда во время запроса или рендера. Порядок приоритета:

  1. url из front matter (непустая строка) — используется как есть. Шаблон permalink() модели вообще не применяется.
  2. Иначе — шаблон permalink() модели, с токеном :slug, подставленным значением slug из front matter (непустая строка), если оно задано.
  3. Иначе — шаблон permalink() модели, с :slug, подставленным именем файла (относительно documentsPath() модели, без .md).

Результат сохраняется с обрезанными ведущим и конечным / (trim($url, '/')).

Модель permalink() Файл Front matter Итоговый url
Page /:slug pages/about.md нет about
Post /blog/:slug posts/2019-old-post.md slug: old-post blog/old-post
Post /blog/:slug любой url: archive/legacy archive/legacy — без префикса blog/, url из front matter полностью минует шаблон
Category /:slug categories/example.md нет example

permalink() объявляется на каждой модели (Yazar\Contracts\Documentable::permalink()) как строка с ведущим слешем и плейсхолдерами :token. На данный момент реализован только :slug. Сам резолвер универсален — Yazar\Documents\PermalinkResolver::resolve() подставляет каждый :key, присутствующий в переданном ему массиве $tokens:

// src/Documents/PermalinkResolver.php
PermalinkResolver::resolve('/blog/:slug', ['slug' => 'hello-world']); // => 'blog/hello-world'

Чтобы добавить второй токен (например, :category), нужно изменить вызывающий код в DocumentImportService::persist() — единственном месте, которое строит массив $tokens — а не сам резолвер, который уже умеет работать с произвольной картой токенов.

url — единственный источник истины для обоих режимов рендеринга

documents.url — единственное, что читают и ContentController (динамический HTTP-роутинг — обычный поиск WHERE url = ?), и Document::getPathForStaticPageAttribute() (путь статической сборки), чтобы разместить документ. Отдельного вычисления URL для статики и динамики нет. Пост, доступный по HTTP на /blog/hello-world, всегда собирается в blog/hello-world/index.html (либо blog/hello-world.html при use_html_suffix — см. Конфигурация) на диске static_output.

Маршрутизация определена в routes/web.php:

Route::get('/', [ContentController::class, 'renderMainPage'])->name('front-page');
Route::get('/{pageNumber}', [ContentController::class, 'renderMainPage'])->whereNumber('pageNumber');
Route::get('/{url}/{pageNumber}', [ContentController::class, 'showCategoryPage'])->where('url', '.+')->whereNumber('pageNumber');
Route::get('/{url}', [ContentController::class, 'show'])->where('url', '.+');

Коллизии

Перед сохранением DocumentImportService::persist() проверяет, не занят ли вычисленный url другим документом — любого типа, не только текущего (проверка идёт прямым запросом к таблице documents, в обход global scope каждой модели, ограничивающего её своим типом):

Отчёт о коллизиях выводится только при php artisan build. BuildCommand::handle() печатает его после импорта:

2 документов не получили уникальный url при импорте:
  - posts/duplicate.md: url 'blog/hello-world' уже занят другим документом
  - pages/dup2.md: url 'about' уже занят другим документом

Этот текст захардкожен на русском в BuildCommand::handle() независимо от локали хост-приложения или настроек language.* в .kodla/config.yaml — под окружение он не переводится. Сборка всё равно завершается с кодом 0 — коллизия — предупреждение, а не сбой сборки.

Та же коллизия, произошедшая при автоимпорте в динамическом режиме (middleware ImportEmptyContentContentImporter::importIfEmpty()), вообще не даёт отчётаimportIfEmpty() отбрасывает свои конфликты, поскольку у веб-запроса нет консоли для вывода. Проигравший документ просто не резолвится ни по какому URL, пока коллизию не устранят и контент не переимпортируют.

Смотри также