Привет, Хабр! Меня зовут Алексей Фомин, я Technical Lead во Frontend в компании Devs Universe. В своей работе я часто сталкиваюсь с тем, что даже опытные разработчики не всегда задумываются о проектировании URL-структуры приложения, а ведь это критически важный элемент пользовательского опыта, SEO и архитектуры. В этой статье я хочу системно разобрать анатомию URL и дать практические рекомендации по его проектированию.
Содержание
Плохой URL — это как кривая вывеска на магазине: он путает пользователей, усложняет навигацию и портит впечатление о всем сайте. Хороший URL, наоборот, понятен, предсказуем и несет смысловую нагрузку.
Анатомия URL: из чего он состоит?
Прежде чем проектировать, давайте разберемся, как URL устроен. Возьмем пример:
 https://www.example.com:8080/catalog/search?q=watch&sort=price#product-list
| Часть URL | Пример | Описание | 
| Протокол | https: | Правила обмена данными (http, https, ftp) | 
| Доменное имя | www.example.com | Адрес сервера. www — поддомен, example — доменное имя, .com — домен верхнего уровня (TLD) | 
| Порт | :8080 | "Дверь" на сервере. По умолчанию для HTTP — 80, для HTTPS — 443 (в URL обычно не отображается) | 
| Путь (Path) | /catalog/search | Иерархический путь к конкретному ресурсу на сервере. Основа для роутинга | 
| Query-параметры | ?q=watch&sort=price | Начинаются с ?. Пара ключ=значение, разделенные &. Нужны для параметров страницы (фильтры, поиск, сортировка) | 
| Хэш (Фрагмент) | #product-list | Начинается с #. Якорь внутри страницы. Браузер автоматически прокрутит к элементу с id="product-list". На сервер не отправляется | 
Иерархия и структура (Правильное название URL)
URL должен отражать структуру ваших данных, быть читаемым и логичным.
Ключевые принципы:
- 
Человеко-понятность (Readable): используйте слова, а не ID там, где это уместно: - Плохо: - /p/12345;
- Хорошо: - /products/modern-wristwatch;
- Отлично: - /catalog/watches/wrist/modern-wristwatch(показывает иерархию).
 
- 
Иерархичность (Hierarchical): стройте путь от общего к частному, как путь в файловой системе: - /{section}/{category}/{subcategory}/{item};
- Пример: - /blog/javascript/frameworks/vue-3-composition-api— сразу понятно, где мы находимся.
 
- 
Множественное число: часто используют множественное число для ресурсов-коллекций: - /users/(список пользователей),- /users/annasmith(конкретный пользователь);
- Это не строгое правило, но распространенная конвенция в RESTful API. 
 
- 
Лаконичность: не усложняйте путь без необходимости. Избегайте длинных предложений: - Плохо: - /site.com/blog/posts/article-about-how-to-build-a-website-in-2024/;
- Хорошо: - /blog/website-development-2024.
 
- Постоянство (Persistence): URL — это обещание. Once published, forever available. Не меняйте URL существующих страниц без настройки редиректа со старого адреса на новый (301 Moved Permanently). Иначе вы потеряете пользователей и SEO-вес. 
- Регистр букв: единообразие! Чаще всего используют нижний регистр. Сервера могут быть чувствительны к регистру ( - Website.com/PAGEи- website.com/pageмогут вести в разные места), что приводит к ошибкам 404.
- 
Разделители: для разделения слов в составе пути используйте дефисы ( -):- Плохо: - modern_wristwatch(подчеркивание плохо выделяется в подчеркнутых ссылках);
- Плохо: - modern%20wristwatch(пробел, виден как %20);
- Хорошо: - modern-wristwatch(дефис, читается легко и дружелюбен для SEO).
 
Проектирование роутинга (Маршрутизация)
Роутинг — это механизм, который сопоставляет URL с кодом, который должен выполниться для обработки запроса.
Подходы:
- 
Серверный роутинг (Traditional): каждый URL ведет на отдельную HTML-страницу, которую генерирует и возвращает сервер. При переходе по ссылке браузер полностью обновляет страницу: - Плюсы: проще для SEO (HTML приходит сразу), не требует JavaScript на клиенте; 
- Минусы: медленнее с точки зрения пользователя, больше нагрузки на сервер. 
 
- 
Клиентский роутинг (SPA - Single Page Application): сервер отдает один HTML-файл, а JavaScript на стороне клиента (например, React Router, Vue Router) управляет URL и подгружает нужные "страницы" (на самом деле, компоненты) динамически, без полной перезагрузки: - Плюсы: очень быстрое переключение между "страницами", поведение как у нативного приложения; 
- Минусы: сложнее с SEO (изначально решается с помощью SSR - Server-Side Rendering), первоначальная загрузка может быть дольше. 
 
Паттерны проектирования роутов
- Статические пути: - /about,- /contact.
- 
Динамические параметры: Используются для идентификации конкретного ресурса: - Путь: - /users/:userIdили- /products/:productId;
- Пример: URL - /users/annasmith-> параметр- userId = "annasmith".
 
- 
Вложенные роуты (Nested Routes): Отражают иерархию в UI: - Роут: - /settings/:tab(e.g.,- /settings/profile,- /settings/security);
- Роут: - /dashboard/analytics/overview.
 
Query Parameters (Параметры строки запроса)
Это часть URL, которая начинается с ? и состоит из пар ключ=значение, разделенных &.
 https://example.com/products?category=watches&sort=price\_asc&page=2
Когда использовать?
- 
Фильтрация, сортировка, поиск: - ?category=electronics&price_min=100&price_max=500;
- ?sort=date_desc(сортировка по дате, по убыванию);
- ?q=search+query(поисковый запрос).
 
- Пагинация: - ?page=3&limit=25.
- Настройки представления: - ?view=gridили- ?view=list.
- Отслеживание (UTM-метки): - ?utm_source=newsletter&utm_medium=email&utm_campaign=promo.
- Сохранение состояния, которое не должно быть в пути: Параметры запроса не уникальны для страницы. Страница - /productsс разными query-параметрами — это все та же страница- /products, просто в разном состоянии.
Важно: Query-параметры не влияют на то, какой HTML-документ вернет сервер (в классической модели). Они обрабатываются уже на загруженной странице.
Hash (Фрагмент)
Это часть URL, которая начинается с символа #.
 https://example.com/documentation#chapter-2
Когда использовать?
- Якорь на странице (Основное назначение): Браузер автоматически прокручивает страницу к элементу с - id="chapter-2". Это чисто клиентская навигация, серверу хэш не отправляется.
- 
Клиентский роутинг в старых SPA (History API): Раньше, до появления History API, хэш ( #и#!) использовали для организации роутинга в одностраничных приложениях, так как изменение хэша не вызывает перезагрузки страницы и не отправляется на сервер:- Пример: - example.com/#/dashboard,- example.com/#/users;
- Сейчас это устаревший подход. Современные фреймворки используют History Mode (роуты без - #), который создает красивые URL вида- example.com/dashboard. Это требует правильной настройки сервера.
 
Сводная таблица: что и когда использовать?
| Компонент URL | Пример | Когда использовать? | Отправляется на сервер? | 
| Путь (Path) | 
 | Для определения ресурса или страницы. Основа SEO и структуры сайта. | Да | 
| Query-параметры | 
 | Для состояния страницы: сортировка, фильтры, поиск, пагинация. Не уникально для страницы. | Да | 
| Хэш (Fragment) | 
 | Для внутренней навигации по разделам одной страницы. (Устарел для роутинга между страницами в SPA). | Нет | 
Как получить части URL в JavaScript?
В браузере весь текущий URL доступен в глобальных объектах window.location или просто location. Этот объект содержит все части URL в разобранном виде.
Пример URL для разбора:
 https://www.example.com:8080/catalog/search?q=watch&sort=price#product-list
// Объект location для нашего примера
console.log(location);
// Выведет:
// {
//   href: "https://www.example.com:8080/catalog/search?q=watch&sort=price#product-list",
//   protocol: "https:",
//   hostname: "www.example.com",
//   port: "8080",
//   host: "www.example.com:8080", // hostname + port
//   pathname: "/catalog/search",
//   search: "?q=watch&sort=price",
//   hash: "#product-list"
// }
// 1. Получить весь URL
const fullUrl = location.href;
// 2. Получить путь (Path)
const path = location.pathname; // Вернёт: "/catalog/search"
// 3. Получить строку query-параметров
const searchString = location.search; // Вернёт: "?q=watch&sort=price"
// 4. Получить хэш
const hash = location.hash; // Вернёт: "#product-list"
// 5. Работа с Query-параметрами
// Простой способ получить значение конкретного параметра
const urlParams = new URLSearchParams(location.search);
const searchQuery = urlParams.get('q'); // Вернёт: "watch"
const sortType = urlParams.get('sort'); // Вернёт: "price"
const nonExistentParam = urlParams.get('page'); // Вернёт: null
// Перебрать все параметры
for (let [key, value] of urlParams) {
  console.log(`${key}: ${value}`);
}
// Выведет:
// q: watch
// sort: price
// 6. Динамическое создание URL с параметрами
// Полезно для формирования ссылок с query-параметрами
const newParams = new URLSearchParams();
newParams.append('category', 'electronics');
newParams.append('page', '2');
const newUrl = `${location.origin}/search?${newParams.toString()}`;
// newUrl будет: "https://www.example.com:8080/search?category=electronics&page=2"
Работа с "неидеальными" Query-параметрами
На практике вы часто будете сталкиваться с query-строками, которые не соответствуют идеальному шаблону ключ=значение. JavaScript-методы должны корректно обрабатывать эти случаи.
Рассмотрим пример URL с проблемными параметрами:
 https://example.com/search?q=watch&sort=&page=2&filter=size&filter=color&flag&&invalid\_value=#test
Разберем его части:
- q=watch- нормальный параметр;
- sort=- ключ есть, значение пустое;
- page=2- нормальный параметр;
- filter=size&filter=color- два параметра с одинаковым ключом;
- flag- ключ без знака равенства и значения;
- &invalid_value=- значение без ключа (редко, но бывает);
- #test- хэш.
Как это обрабатывает URLSearchParams?
// Представим, что мы на странице с таким URL
const urlParams = new URLSearchParams('?q=watch&sort=&page=2&filter=size&filter=color&flag&&invalid_value=');
// 1. Нормальный параметр
console.log(urlParams.get('q')); // "watch"
// 2. Параметр с пустым значением
console.log(urlParams.get('sort')); // "" (пустая строка, не null!)
// 3. Несколько параметров с одинаковым ключом
// .get() возвращает только ПЕРВОЕ значение
console.log(urlParams.get('filter')); // "size"
// Чтобы получить все значения, нужно использовать .getAll()
console.log(urlParams.getAll('filter')); // ["size", "color"]
// 4. Ключ без значения и без знака "=" (flag)
// Интерпретируется как параметр с пустым значением
console.log(urlParams.get('flag')); // "" (пустая строка)
// 5. Значение без ключа (invalid_value=)
// Интерпретируется как параметр с ключом "invalid_value" и пустым значением
console.log(urlParams.get('invalid_value')); // ""
// 6. Проверка существования ключа
// .has() проверяет наличие ключа, даже если значение пустое
console.log(urlParams.has('sort')); // true
console.log(urlParams.has('flag')); // true
console.log(urlParams.has('nonexistent')); // false
// 7. Перебор всех параметров
for (let [key, value] of urlParams) {
  console.log(`Ключ: "${key}", Значение: "${value}"`);
}
// Выведет:
// Ключ: "q", Значение: "watch"
// Ключ: "sort", Значение: ""
// Ключ: "page", Значение: "2"
// Ключ: "filter", Значение: "size"
// Ключ: "filter", Значение: "color"
// Ключ: "flag", Значение: ""
// Ключ: "invalid_value", Значение: ""
Важные выводы
- 
Пустое значение ( ?key=) ≠ Отсутствие значения (?key) ≠ Отсутствие ключа:- И - ?key=, и- ?keyвернут- ""(пустую строку) при вызове- .get('key');
- Но в URL они выглядят по-разному, и некоторые бэкенд-фреймворки могут интерпретировать их неодинаково. 
 
- Метод - .get()всегда возвращает строку или- null. Если значения нет — вернется- null. Если значение пустое — вернется пустая строка- "".
- Для работы с множественными значениями используйте - .getAll(). Это особенно важно для чекбоксов, мультиселектов в фильтрах.
Практический совет: всегда явно проверяйте и нормализуйте параметры.
// Вместо этого:
if (urlParams.get('sort')) { 
  // Это не сработает, если sort="", а нам нужно было обработать и этот случай
}
// Делайте так:
const sortValue = urlParams.get('sort');
if (sortValue !== null) {
  // Обрабатываем, даже если sortValue является пустой строкой
  // Это означает, что параметр 'sort' был в URL (пусть и без значения)
}
// Или так:
if (urlParams.has('sort')) {
  // Параметр 'sort' присутствует в URL (даже без значения)
}
Этот подход делает ваш код более надежным и защищенным от нестандартных, но возможных входных данных.
Чеклист правильного проектирования URL
- URL читается как путь: - /blog/category/article-name;
- Используется нижний регистр и дефисы; 
- Динамические ID скрыты там, где важен смысл: - /users/annasmith, а не- /users/4815162342;
- Query-параметры используются для необязательных настроек (сортировка, фильтры), а не для обязательной информации о странице; 
- Структура предсказуема: пользователь, посмотрев на URL, может понять, где он и как перейти на главную страницы раздела; 
- Для SPA настроен History Mode (убирает - #из URL), а сервер сконфигурирован корректно (отдавать- index.htmlна все несуществующие пути, которые обрабатывает фронтенд).
Потратив время на проектирование правильной URL-структуры, вы создаете не только удобный и понятный сайт для пользователей, но и закладываете основу для его легкой поддержки и успешного продвижения в поисковых системах.
Также к прочтению:
20 частых антипаттернов в React и как их исправить: кратко, понятно, без мифов
 
          