0% прочитано

Laravel News vs Posts: чому я зробила News окремим модулем, а не ще одним типом Post

Коли в ICanUp зʼявилися News, найпростішим рішенням було додати ще один тип до Post. Але схожі поля не означають однакову доменну сутність. Показую, чому News отримала власне сховище та адмінку, а локалізація, Media, ContentBlocks, SEO, публікація й аналітика залишилися спільною інфраструктурою.

18 вересня 2026 р. 11 хв читанняЛаравель

Коли у ICanUp зʼявилася потреба публікувати короткі новини про сайт, продукт, релізи та оновлення, найпростіше рішення було очевидним: додати ще один тип до Post.

У News справді багато спільного з Post. Є slug, переклади, статус публікації, published_at, обкладинка, повний контент, SEO та публічна сторінка.

Але під час проєктування я вирішила не питати: скільки однакових колонок мають ці сутності?

Кориснішим виявилося інше питання: чи означають News і Post для системи одне й те саме?

Схожа структура даних ще не означає, що дві сутності повинні мати одну модель, одну таблицю та один життєвий цикл.
Laravel architecture showing Post and News as separate domain entities sharing localization, media, content blocks, SEO and analytics infrastructure
Post і News мають окремі доменні моделі, але використовують спільні інфраструктурні можливості замість дублювання локалізації, Media, ContentBlocks, SEO та аналітики.

На рівні полів Post і News справді дуже схожі

Якщо дивитися лише на таблиці, об’єднання здається логічним.

Можливість

Post

News

Slug

Так

Так

UK / EN

Так

Так

Статус

Так

Так

published_at

Так

Так

Обкладинка через Media

Так

Так

ContentBlocks

Так

Так

SEO

Так

Так

Якби я приймала рішення лише за повторенням колонок, News майже напевно стала б ще одним варіантом Post.

Найпростіший шлях був додати type у posts

Концептуально така модель могла б виглядати дуже просто:

PHP - Одна таблиця для різних типів контенту
posts idslugtypestatuspublished_atimage_media_id...

Наприклад:

PHP - Розрізняти контент через type
type = posttype = news

На першій версії це навіть могло б бути швидше.

Але кожна наступна відмінність почала б перетворювати спільну модель на набір умов.

Проблема починається не з таблиці, а з поведінки

Post у моєму проєкті вже має власну роль: це велика технічна публікація з категоріями, можливістю входити в серію та своїм редакторським процесом.

News потрібна для іншої задачі: коротко зафіксувати подію, реліз, зміну продукту або самого сайту, а за потреби додати повний матеріал через ContentBlocks.

Post

News

Технічна публікація

Новина або анонс

Категорія

Фіксований тип News

Може входити в Post Series

Серія Post їй не потрібна

Основний довгий контент

Може бути короткою або мати повний контент

Окремий список Posts в адмінці

Окремий список News в адмінці

Тобто спільні технічні можливості є, але редакторське значення сутностей уже різне.

Я відокремила домен, але не стала дублювати інфраструктуру

Для мене це стало ключовим рішенням.

Окремий домен не означає, що потрібно вдруге написати локалізацію, завантаження зображень, редактор блоків або SEO.

Окремі сутності використовують спільну інфраструктуру
Post domain --------------------+                                |News domain --------------------+--> Localization                                +--> Media                                +--> ContentBlocks                                +--> SEO                                +--> Publication rules                                +--> Analytics

Я повторно використовую можливості платформи, але не змушую різні доменні сутності ставати однією моделлю лише заради повторного використання коду.

News отримала власне сховище

У результаті News має власні таблиці news і news_translations.

Це означає, що її стан не потрібно відновлювати з таблиці Posts через постійну умову type = news.

Окреме зберігання доменних сутностей
postspost_translations newsnews_translations

При цьому я не створювала окрему систему перекладів. News використовує той самий підхід до локалізованих даних, який уже працює в інших частинах ICanUp.

Власний type для News не означає категорії

У першій версії я свідомо не додавала окреме дерево категорій для News.

Для базового поділу достатньо обмеженого набору типів:

Типи News
packageproductsitereleaseupdate

Це ще одна відмінність від Post.

Post має повноцінну категоризацію. Для News мені потрібна значно простіша ознака, яка описує характер самої новини.

Якби News була Post, умови почали б розростатися

Спільна модель спочатку економить кілька класів і таблиць.

Але з розвитком починають з’являтися перевірки:

PHP - Умови всередині спільної моделі
if ($post->type === 'news') {    // News-specific behavior.} if ($post->type !== 'news') {    // Post-only behavior.}

Потім такі умови з’являються у валідації, запитах, політиках, адмінці, аналітиці, SEO, виборі категорій і публічному відображенні.

У певний момент одна модель уже не спрощує систему, а змушує постійно пам’ятати, з яким підтипом вона зараз працює.

Окрема адмінка була частиною доменного рішення

Редактор News не повинен шукати коротку новину серед великого списку технічних Post.

Тому News отримала власний розділ в адмінці зі своїм списком, створенням, редагуванням, пошуком і фільтрами.

Для News

Навіщо

Окремий список

Не змішувати два різні редакторські потоки

Фільтр за статусом

Керувати Draft, Published і запланованими матеріалами

Фільтр за типом

Розрізняти product, release, update та інші News

Сортування за датою

Новини природно орієнтовані на час

Media Picker

Повторно використати спільну медіатеку

ContentBlocks я повторно використала без News-specific редактора

Окремий домен не став причиною створювати ще один формат великого тексту.

News стала blockable-сутністю і використовує той самий ContentBlocks pipeline, що й інший підтримуваний публічний контент.

News використовує спільні ContentBlocks
News  |  vshared ContentBlocks  |  +-- Heading  +-- Rich Text  +-- Image  +-- Code  +-- Notice  +-- CTA  +-- other supported blocks

Короткий excerpt залишається зручним для картки або списку, а detail page може мати повноцінний матеріал.

Це дало окремій сутності можливість розвиватися без окремого редактора контенту.

SEO теж не стало окремою системою для News

Те саме стосується SEO.

News має власну публічну адресу та власний контент, але їй не потрібна окрема реалізація canonical, hreflang, Open Graph чи sitemap.

SEO можливість

Підхід

Meta title / description

Спільний SEO contract

Canonical

Спільна URL-інфраструктура

hreflang UK / EN

Чинний localization contract

Open Graph

Спільний social metadata pipeline

Sitemap

Спільна інфраструктура індексації

IndexNow

Спільний механізм надсилання URL

Тобто окрема доменна сутність не означає окремий SEO framework.

Правила публічної видимості теж можна повторно використати концептуально

News, як і Post, не повинна ставати публічною лише тому, що запис уже існує в базі.

Звичайний користувач бачить тільки матеріал, який справді дозволено публікувати зараз.

Контракт публічної видимості News
Draft  -> hidden Scheduled before published_at  -> hidden Published and eligible  -> public Deleted / inactive  -> hidden

При цьому попередній перегляд Draft або запланованої News використовує захищений адміністративний контракт і не повинен робити такий матеріал індексованим.

Окрема News не означає окрему аналітику

Цей принцип став особливо помітним, коли я додала аналітику для News.

Було б легко створити окрему таблицю на кшталт news_analytics, окремі запити для країн, браузерів, пристроїв і окрему погодинну агрегацію.

Я цього не робила.

News стала ще одним суб’єктом спільної аналітики
Public Analytics    |    +-- Post    +-- Page    +-- Post Series    +-- News

News стала ще одним підтримуваним типом публічного контенту всередині наявної аналітичної архітектури.

Так я отримала перегляди, дедупліковані перегляди, дні, години, країни, пристрої, браузери, операційні системи, джерела трафіку, локалі та розподіл Human/Bot без паралельної системи.

Це хороший приклад різниці між доменом та інфраструктурою: News окрема як бізнес-сутність, але аналітичний механізм для неї спільний.

Окремий модуль дав News власний простір для розвитку

Для мене це одна з найважливіших причин.

Якщо через рік News отримає нові типи, власні фільтри, окремі правила показу у стрічці, спеціальні CTA або інший редакторський процес, мені не доведеться щоразу запитувати, як це вплине на всі Post.

І навпаки, розвиток Post Series, категорій або функцій технічних статей не повинен автоматично ускладнювати News.

Я розділяю відповідальність приблизно так

Відповідальність

Окрема чи спільна

News model та persistence

Окрема

Post model та persistence

Окрема

News types

Окрема

News Admin

Окрема

Localization

Спільна

Media

Спільна

ContentBlocks

Спільна

SEO infrastructure

Спільна

Publication concepts

Спільні правила, адаптовані до сутності

Public Analytics

Спільна

Що саме я повторно використовую між Post і News

  • локалізацію UK / EN;
  • Media Picker та Media Usage;
  • ContentBlocks і публічний renderer;
  • SEO metadata та localized URL contract;
  • canonical і hreflang;
  • Open Graph та social metadata;
  • правила індексації;
  • sitemap та IndexNow infrastructure;
  • основні підходи до Draft, scheduling і preview;
  • спільну Public Analytics architecture.

Де окремий модуль починає окупатися

  • Коли редактор працює з News і Post як із різними типами матеріалів.
  • Коли для News потрібні власні типи, але не категорії Post.
  • Коли одна сутність не повинна успадковувати функції іншої лише через спільну таблицю.
  • Коли майбутні зміни однієї сутності не повинні збільшувати кількість умов у другій.
  • Коли окрема адмінка робить роботу зрозумілішою.
  • Коли спільні технічні можливості вже можна перевикористати через сервіси та контракти.

Але окремий модуль потрібен не завжди

Я б не створювала нову доменну сутність лише тому, що в Post з’явився ще один невеликий варіант оформлення.

Якщо дані мають той самий життєвий цикл, ті самі правила, ту саму адмінку і відрізняються лише візуальним шаблоном, окремий модуль може бути зайвим.

Для мене межа проходить не за кількістю однакових колонок, а за поведінкою та відповідальністю сутності.

Питання

Що воно допомагає зрозуміти

Чи однаковий редакторський процес?

Якщо ні, окремий домен стає логічнішим

Чи однакові правила розвитку?

Якщо ні, спільна модель накопичуватиме умови

Чи потрібна окрема адмінка?

Це сильний сигнал окремої сутності

Чи можна повторно використати інфраструктуру?

Окремий домен не обов’язково означає дублювання

Чи відрізняється лише вигляд?

Тоді окрема модель може бути зайвою

Тести теж повинні захищати межу між сутностями

Сценарій

Що я перевіряю

Створення News

Не створює Post

News localization

UK та EN працюють через спільний localization contract

News cover

Використовує спільний Media module

Draft / Scheduled News

Не стає публічною раніше часу

News ContentBlocks

Працюють через спільний renderer

News SEO

Не потребує окремої SEO системи

News Analytics

Працює через наявний analytics pipeline

Post regressions

Додавання News не змінює поведінку Post

Що я не роблю в такій архітектурі

  • Не роблю News категорією Post лише тому, що це контент.
  • Не додаю нескінченні умови if type === news у Post.
  • Не створюю другу систему Localization для News.
  • Не створюю окремий News Media Picker.
  • Не дублюю ContentBlocks для News.
  • Не створюю News-only SEO infrastructure.
  • Не створюю окремі таблиці аналітики лише через новий тип контенту.
  • Не змушую News успадковувати Post Series або категорії, якщо вони їй не потрібні.
  • Не розділяю сутності лише через різний дизайн.

Мій короткий checklist перед створенням нового content module

  • Порівняти не тільки поля, а й поведінку сутностей.
  • Визначити окремі правила та життєвий цикл.
  • Перевірити, чи потрібен власний редакторський процес.
  • Не змішувати повторне використання коду з повторним використанням доменної моделі.
  • Винести спільні можливості в reusable infrastructure.
  • Не дублювати Localization, Media, ContentBlocks, SEO та Analytics без причини.
  • Дати новій сутності тільки ті можливості, які їй справді потрібні.
  • Захистити межі автоматичними тестами.
  • Перевірити, чи майбутній розвиток однієї сутності не змушуватиме змінювати іншу.

Схожі поля дозволяють повторно використати інфраструктуру. Вони не зобов’язують різні доменні поняття жити в одній моделі.

Висновок

Коли я додавала News у ICanUp, технічно було легко зробити її ще одним типом Post.

Але це вирішило б лише першу версію. Далі кожна відмінність News поступово перетворювалася б на нову умову в моделі, запитах, валідації, адмінці та публічному коді Post.

Тому News отримала власний домен, власне сховище, власні типи та окремий редакторський розділ.

При цьому я не дублювала те, що насправді є спільною можливістю платформи: Localization, Media, ContentBlocks, SEO, правила публікації та Analytics.

Для мене правильне розділення тут не означає написати все двічі. Воно означає відокремити поведінку сутностей і повторно використати інфраструктуру там, де вона справді спільна.