Коли у ICanUp зʼявилася потреба публікувати короткі новини про сайт, продукт, релізи та оновлення, найпростіше рішення було очевидним: додати ще один тип до Post.
У News справді багато спільного з Post. Є slug, переклади, статус публікації, published_at, обкладинка, повний контент, SEO та публічна сторінка.
Але під час проєктування я вирішила не питати: скільки однакових колонок мають ці сутності?
Кориснішим виявилося інше питання: чи означають News і Post для системи одне й те саме?

На рівні полів Post і News справді дуже схожі
Якщо дивитися лише на таблиці, об’єднання здається логічним.
Можливість | Post | News |
|---|---|---|
Slug | Так | Так |
UK / EN | Так | Так |
Статус | Так | Так |
| Так | Так |
Обкладинка через Media | Так | Так |
ContentBlocks | Так | Так |
SEO | Так | Так |
Якби я приймала рішення лише за повторенням колонок, News майже напевно стала б ще одним варіантом Post.
Найпростіший шлях був додати type у posts
Концептуально така модель могла б виглядати дуже просто:
posts idslugtypestatuspublished_atimage_media_id...Наприклад:
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.
Для базового поділу достатньо обмеженого набору типів:
packageproductsitereleaseupdateЦе ще одна відмінність від Post.
Post має повноцінну категоризацію. Для News мені потрібна значно простіша ознака, яка описує характер самої новини.
Якби News була Post, умови почали б розростатися
Спільна модель спочатку економить кілька класів і таблиць.
Але з розвитком починають з’являтися перевірки:
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 | 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, не повинна ставати публічною лише тому, що запис уже існує в базі.
Звичайний користувач бачить тільки матеріал, який справді дозволено публікувати зараз.
Draft -> hidden Scheduled before published_at -> hidden Published and eligible -> public Deleted / inactive -> hiddenПри цьому попередній перегляд Draft або запланованої News використовує захищений адміністративний контракт і не повинен робити такий матеріал індексованим.
Окрема News не означає окрему аналітику
Цей принцип став особливо помітним, коли я додала аналітику для News.
Було б легко створити окрему таблицю на кшталт news_analytics, окремі запити для країн, браузерів, пристроїв і окрему погодинну агрегацію.
Я цього не робила.
Public Analytics | +-- Post +-- Page +-- Post Series +-- NewsNews стала ще одним підтримуваним типом публічного контенту всередині наявної аналітичної архітектури.
Так я отримала перегляди, дедупліковані перегляди, дні, години, країни, пристрої, браузери, операційні системи, джерела трафіку, локалі та розподіл Human/Bot без паралельної системи.
Окремий модуль дав 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.
Для мене правильне розділення тут не означає написати все двічі. Воно означає відокремити поведінку сутностей і повторно використати інфраструктуру там, де вона справді спільна.



