У ICanUp видимість публікації перестала зводитися до простої перевірки status === published, щойно з’явилися відкладена публікація, попередній перегляд для редактора, локалізація, SEO, публічні стрічки та аналітика.
Небезпечна ситуація може виникнути майже непомітно: один маршрут перевіряє статус, інша частина коду - лише дату публікації, а стрічка або SEO використовують ще одну окрему умову. У результаті матеріал може залишатися чернеткою в адмінпанелі й водночас уже бути доступним у публічній частині сайту.
Для Tips я залишила одне доменне правило видимості, від якого залежать публічний доступ, попередній перегляд, URL у ресурсі, SEO та всі побічні дії, пов’язані з публікацією.
Видимість - це доменне правило, а не окрема умова в контролері
Для Tip є одна відповідь на запитання: чи має цей матеріал бути доступним публічно саме зараз?
Це правило враховує м’яке видалення, статус, час публікації, наявність Topic і заголовка для поточної локалі.
Це важливо, тому що правило публічної видимості потрібне не лише одному контролеру. Його використовують списки матеріалів, Explorer, сторінки Topic, пошук, пов’язаний контент, sitemap, сторінка самого матеріалу та аналітика.
public function isPubliclyVisible(?string $locale = null): bool{ $locale ??= app()->getLocale(); return ! $this->trashed() && $this->status === PostStatusEnum::PUBLISHED && $this->published_at !== null && ! $this->published_at->isFuture() && $this->topic()->exists() && filled( $this->translationForLocale( 'title', $locale, ) );}Чотири стани, але не чотири статуси
Стан | Контракт |
|---|---|
Draft |
|
Scheduled |
|
Published | Publication time настав, запис не deleted, Topic існує, localized title заповнений. Public. |
Unavailable | Deleted, wrong locale, missing Topic або інша неповна public-умова. Не public і не preview. |
Попередній перегляд не змінює стан публікації.
public function previewState(): ?string{ if ($this->trashed()) { return null; } if ( $this->status === PostStatusEnum::DRAFT ) { return 'draft'; } return $this->status === PostStatusEnum::PUBLISHED && $this->published_at?->isFuture()canBeViewed() навмисно має ширші правила, ніж isPubliclyVisible(). Доступ можливий з двох різних причин: матеріал уже опублікований або користувач має право на попередній перегляд підтримуваного неопублікованого стану.
Право доступу не змінює доменний стан матеріалу. Чернетка не стає опублікованою, запланований матеріал не публікується раніше визначеного часу, а видалений Tip не отримує права на попередній перегляд.
Preview - це дозвіл побачити ще не опублікований матеріал, а не виняток, який робить його публічним.
Право доступу перевіряється на вході, а доменне правило визначає видимість
public function __invoke( Request $request, Tip $tip,): Response { $canPreview = $request ->user() ?->can( PermissionEnum::TIPS_PREVIEW->value ) ?? false; $tip = $this->tipService ->getTip( $tip, $canPreview, ); Контролер сам не вирішує, що означають стани Draft або Scheduled. Він лише перевіряє право користувача на TIPS_PREVIEW.
Далі публічний сервіс передає результат цієї перевірки доменному правилу. Якщо canBeViewed() повертає false, користувач отримує 404.
Самого факту входу в систему або загального доступу до адмінпанелі для цього недостатньо.
Доступний для попереднього перегляду - не означає доступний за публічним посиланням
return [ 'id' => $tip->id, 'slug' => $tip->slug, 'url' => $tip->isPubliclyVisible() ? $tip->showRoute() : null, 'title' => $tip->translation('title'), 'publishedAt' => $tip->published_at ?->toDateTimeString(), 'previewState' =>Матеріал, доступний для попереднього перегляду, може відкриватися через маршрут детальної сторінки, але ресурс не подає його як звичайну публічну сторінку. Для Draft і Scheduled поле url залишається null.
Інтерфейс бачить, що матеріал доступний для попереднього перегляду, але не поводиться так, ніби для нього вже існує звичайне публічне посилання.
404 - це частина правил публікації
$this->get( route( 'tips.show', $published->slug, ))->assertOk(); $this->get( route( 'tips.show', $draft->slug, ))->assertNotFound(); $this->get( route(Наявність маршруту ще не означає, що матеріал існує для публічного доступу. Для звичайного відвідувача неопублікований контент має поводитися так, ніби його немає.
Це також не дозволяє зовнішньому клієнту за окремою відповіддю визначити, що неопублікований slug насправді існує.
Доступ до адмінпанелі ще не означає права на попередній перегляд
$this->actingAsAdmin([ PermissionEnum::ADMIN_ACCESS,]); $this->get( route( 'tips.show', $draft->slug, ))->assertNotFound(); $this->get( route( 'tips.show', $scheduled->slug, )Попередній перегляд не повинен робити матеріал доступним для SEO
$this->get( route( 'tips.show', $draft->slug, )) ->assertOk() ->assertInertia( fn (Assert $page) => $page ->where( 'tip.data.previewState', 'draft', ) ->where( 'tip.data.url',Відповідь HTTP 200 під час дозволеного попереднього перегляду ще не робить сторінку публічною. Якщо така сторінка отримає звичайний URL і директиву index, follow, пошуковий робот може сприйняти її як повноцінно опублікований матеріал.
Тому тест одночасно перевіряє три умови: сторінка перебуває в режимі попереднього перегляду, публічний URL відсутній, а значення robots дорівнює noindex, nofollow.
Побічні ефекти теж мають спиратися на єдине правило видимості
$tip = $this->tipService->getTip( $tip, $canPreview,); if ( $tip->isPubliclyVisible()) { $this->publicContentViewTracker ->track( $request, AnalyticsSubject::entity( ContentSubjectTypeEnum::TIP ->definition(), AnalyticsEntityIdentity::fromString(canBeViewed() може повертати true для попереднього перегляду, але аналітика не повинна зараховувати такий запит як звичайний публічний перегляд.
Після перевірки доступу код ставить суворіше запитання: isPubliclyVisible(). Матеріал можна показати в режимі попереднього перегляду, але це не повинно впливати на публічні показники аналітики.
Де Draft найчастіше може просочитися
Шар | Небезпечне спрощення |
|---|---|
Detail route | Перевірити лише результат прив’язки моделі за |
Scheduled publishing | Вважати |
Preview | Дозволити будь-якому користувачу адмінпанелі переглядати чернетки без окремого права на попередній перегляд. |
Resource / UI | Повертати звичайний публічний URL навіть для матеріалу, доступного лише в режимі попереднього перегляду. |
SEO | Залишити |
Analytics | Зараховувати будь-яку відповідь HTTP 200 як публічний перегляд. |
Search / Sitemap / Related | Використовувати окремі локальні перевірки видимості замість єдиного правила публічної доступності. |
Тести мають перевіряти межі правил, а не лише один успішний сценарій.
Одного тесту «Published відкривається» недостатньо. Він нічого не говорить про чернетки, заплановані публікації, видалений контент, неправильну локаль або межі прав доступу.
Надійний регресійний тест має перевіряти цілу матрицю сценаріїв: звичайний відвідувач, користувач адмінпанелі без права на попередній перегляд, користувач адмінпанелі з таким правом, SEO для публічної сторінки та SEO для попереднього перегляду.
Практичний список перевірок для видимості публікацій
Одне доменне правило визначає, чи є матеріал публічним саме зараз.
Редакційний статус не замінює перевірку часу публікації.
Попередній перегляд є окремим рішенням про доступ, а не послабленням правил публічної видимості.
Право на попередній перегляд перевіряється на межі HTTP-запиту.
Матеріал, доступний лише для попереднього перегляду, не отримує звичайного публічного URL.
Для попереднього перегляду встановлюється
noindex, nofollow.Попередній перегляд не зараховується як публічний перегляд в аналітиці.
Списки матеріалів, пошук, пов’язаний контент і sitemap використовують те саме правило публічної видимості.
Регресійні тести охоплюють чернетки, заплановані й опубліковані матеріали, видалений контент, локалі та права доступу.
Висновок
Видимість публікації здається дрібною умовою, доки контент має лише два стани. Але щойно з’являються чернетки, відкладена публікація, попередній перегляд, локалізація, SEO та аналітика, це вже стає системним правилом.
У ICanUp я розділяю три окремі питання: чи є матеріал публічним, чи може конкретний користувач побачити його до публікації і які публічні побічні дії для нього дозволені.
Чернетка не повинна ставати публічною лише тому, що одна з багатьох точок входу перевірила не всі умови. Єдине правило видимості прибирає саме цей клас помилок.



