Менеджери регулярно просять додати новий пункт у меню — кожен запит перетворюється на задачу для розробника, гальмує релізи та відволікає команду від основного функціоналу. Ми спроєктували систему керування меню, яка вирішує цю проблему: правки вносяться за хвилини без залучення програмістів. За 10+ років ми впровадили такі системи для проектів з каталогами до 50 000 товарів та мультиязичними сайтами на 12 мов. Одне з таких рішень — власна система керування навігацією, яка окупається в середньому за 2 місяці за рахунок скорочення часу розробників.
Які проблеми вирішує система керування меню?
Проблема 1: ручне правлення коду при кожній зміні. Без спеціального інструменту будь-який новий пункт вимагає редагування шаблонів, деплою, а іноді й узгодження з бекендом. Наша система дає менеджерам drag-and-drop інтерфейс, де зміна структури меню займає 1 хвилину. Економія бюджету на підтримку сягає 60%.
Проблема 2: повільне завантаження меню з великою вкладеністю. Стандартний підхід з рекурсивними запитами до БД дає N+1 запитів — для 500 пунктів це 250 мс завантаження. Ми використовуємо eager loading та кеш, знижуючи час до 2 мс. Навіть для 10 000 пунктів меню віддається за 5 мс.
Як влаштована модель даних? — розробка системи керування
Модель даних будується на двох таблицях: menus та menu_items. Таблиця menus містить записи для кожного меню (головне, футер, мобільне). Поле locale дозволяє створювати окремі структури для кожної мови. Таблиця menu_items зберігає ієрархію пунктів з типом, порядком та посиланням. Вкладеність реалізована через parent_id та order. При зміні порядку пунктів ми перераховуємо order лише зачеплені записи — інкрементальне оновлення.
| Тип пункту | Опис |
|---|---|
link |
Довільний URL, вводиться вручну |
page |
Вибір зі списку сторінок CMS |
category |
Вибір категорії каталогу |
custom |
Якірне посилання (#section) або JS-дія |
Для типів page та category URL генерується автоматично при зміні slug — це виключає биті посилання.
Чому ми обрали @dnd-kit для drag-and-drop?
Бібліотека @dnd-kit/sortable — сучасний стандарт для перетягування на React. Вона працює в 2-3 рази швидше аналогів за рахунок використання closestCenter та вертикальної стратегії сортування. Ось як виглядає редактор:
import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable';
function MenuEditor({ items, onReorder }) {
const [treeItems, setTreeItems] = useState(buildTree(items));
const handleDragEnd = ({ active, over }) => {
if (!over || active.id === over.id) return;
const oldIndex = treeItems.findIndex(i => i.id === active.id);
const newIndex = treeItems.findIndex(i => i.id === over.id);
const reordered = arrayMove(treeItems, oldIndex, newIndex);
setTreeItems(reordered);
onReorder(reordered.map(({ id }, order) => ({ id, order })));
};
return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={treeItems} strategy={verticalListSortingStrategy}>
{treeItems.map(item => (
<SortableMenuItem key={item.id} item={item} />
))}
</SortableContext>
</DndContext>
);
}
Як уникнути N+1 при побудові дерева?
При завантаженні меню з вкладеністю розробники часто допускають N+1 запит — кожен рівень вибирається окремо. Ми використовуємо eager loading: одним запитом отримуємо всі пункти потрібного меню, а побудова дерева відбувається в пам'яті. Для 500 пунктів різниця — 3 мс проти 250 мс.
$items = MenuItem::where('menu_id', $menu->id)
->orderBy('order')
->get()
->toArray();
$tree = $this->buildTree($items);
Що дає кешування з інвалідацією?
Кешуємо результат збірки дерева на 1 годину в Redis. При будь-якій зміні (додаванні, видаленні, перестановці) кеш скидається через Observer. Приклад сервісу на Laravel:
class MenuService
{
public function getMenu(string $slug, string $locale): array
{
return Cache::remember("menu:{$slug}:{$locale}", 3600, function () use ($slug, $locale) {
$menu = Menu::where('slug', $slug)->where('locale', $locale)->first();
if (!$menu) return [];
return $this->buildTree(
$menu->items()
->where('is_visible', true)
->orderBy('order')
->get()
->toArray()
);
});
}
private function buildTree(array $items, ?int $parentId = null): array
{
return collect($items)
->where('parent_id', $parentId)
->map(fn($item) => array_merge($item, [
'children' => $this->buildTree($items, $item['id'])
]))
->values()
->toArray();
}
}
Інвалідація проста:
Menu::observe(MenuObserver::class);
class MenuObserver
{
public function saved(Menu $menu): void
{
Cache::forget("menu:{$menu->slug}:{$menu->locale}");
}
}
| Метрика | Без кешу | З кешем (Redis) |
|---|---|---|
| Час завантаження (100 пунктів) | 150 мс | 2 мс |
| Навантаження на БД (10 000 запитів/год) | 10 000 | 1 (при інвалідації) |
Мультиязичність реалізована через окремі записи menus з полем locale. Кеш розділяється за мовою, тому перемикання мови не впливає на продуктивність. При зміні slug сторінки URL у меню оновлюється автоматично — це забезпечує цілісність навігації.
Що входить у роботу?
- Аналіз поточної структури навігації та вимог
- Проектування моделі даних з урахуванням мультиязичності та вкладеності
- Розробка drag-and-drop редактора на React (з можливістю вибору іншого фронтенду)
- Серверна частина на Laravel або Node.js з кешуванням та інвалідацією
- Інтеграція з CMS (WordPress, Drupal, Strapi та ін.)
- Документація API та інструкція для контент-менеджерів
- Навчання команди та гарантійна підтримка 30 днів
Процес роботи
- Аналітика — вивчаємо типи меню, кількість пунктів, частоту оновлень
- Проектування — описуємо модель, узгоджуємо інтерфейс
- Розробка — пишемо код паралельно з тестами
- Тестування — перевіряємо на N+1, кеш, продуктивність
- Деплой — розгортаємо в прод, налаштовуємо CI/CD
Строк розробки: від 2 до 5 днів залежно від складності. Вартість розраховується індивідуально — пишіть, оцінимо проект за один робочий день.
Отримайте консультацію: зв'яжіться з нами, і ми покажемо, як ваша команда зможе керувати меню без програмістів. Замовте розробку системи керування меню.







