Поиск и фильтры в URL
«Маршрут» уже умеет показывать карточки и хранить компактный вид в браузере. Теперь добавим поиск по названию урока. Поисковую строку полезно передавать другому читателю, поэтому источником состояния станет URL. Результат — каталог, который одинаково получает фильтр при прямом открытии адреса и при отправке формы.
Будем использовать обычный GET, а не серверное действие записи. Поиск не меняет закладки, профиль или прогресс. Вы уже знаете load и url.searchParams; осталось определить правила нормализации и показать пустой результат. Учебный экран не запускался, поэтому примеры адресов и списков являются ожидаемыми результатами вычисления.
Что сохраняется в адресе
Строка q означает запрос по названию, course — курс. Они не содержат пароля, токена сессии или личного идентификатора. URL виден в истории браузера и может попасть в журнал запросов, поэтому секретам в нём не место. Для публичного запроса по каталогу такая видимость подходит назначению функции.
Замените или создайте полный src/routes/+page.server.ts. Публичный каталог уже определён в $lib/catalog; загрузка возвращает отобранный список и нормализованные параметры для формы.
import { lessons } from '$lib/catalog';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = ({ url }) => {
const q = (url.searchParams.get('q') ?? '').trim().slice(0, 80);
const course = url.searchParams.get('course') === 'web' ? 'web' : '';
const needle = q.toLocaleLowerCase('ru');
const filtered = lessons.filter(lesson =>
(!course || lesson.course === course) &&
lesson.title.toLocaleLowerCase('ru').includes(needle)
);
return { lessons: filtered, q, course, total: lessons.length };
};
Ограничение восемьюдесятью символами делает пример предсказуемым и не позволяет бессмысленно передавать очень длинное поле. Оно не заменяет общий предел размера HTTP-запроса и защиту от нагрузки. При подключении настоящей базы понадобятся ещё параметризованный запрос, индексы и согласованное поведение поиска; локальный includes этого не обещает.
Нормализация регистра относится только к сравнению. Заголовок сохраняет исходный вид, а q возвращается без внешних пробелов. Человек видит то, с чем сервер действительно работал. Не стоит выводить очищенный запрос в одном месте и искать по другому необработанному значению: тогда интерфейс объяснял бы не фактическое условие отбора.
Форма, которая выполняет чтение
В +page.svelte получайте data через $props и используйте список data.lessons. Следующая разметка добавляется перед существующим списком карточек. Она выражает полный контракт поиска, а локальные массивы закладок пока остаются демонстрацией страницы.
<form method="GET" action="/">
<label for="q">Название урока</label>
<input id="q" name="q" value={data.q} maxlength="80">
<label for="course">Курс</label>
<select id="course" name="course" value={data.course}>
<option value="">Все курсы</option>
<option value="web">Веб-разработка</option>
</select>
<button>Найти</button>
</form>
<p>Найдено {data.lessons.length} из {data.total}.</p>
{#if data.lessons.length === 0}
<p>По этому запросу уроков нет. Измените название или курс.</p>
{/if}
<a href="/">Сбросить фильтр</a>
GET-форма помещает значения полей в query string. Поэтому адрес можно скопировать и открыть позже. Нативное поведение остаётся понятным даже без клиентского JavaScript: браузер отправляет запрос и получает новую страницу. SvelteKit может улучшать переходы, но предметная модель не зависит от ручного вызова fetch в обработчике кнопки.
Ожидаем, что /?q=страницы покажет два материала: структуру и раскладку. Запрос /?q=браузера оставит один урок о событиях. Пустая строка вернёт весь каталог, потому что любое название содержит пустую подстроку. Неизвестный курс нормализуется к общему списку; это выбранное мягкое правило, которое следует сохранить и в тексте формы.
Фильтр не меняет знаменатель прогресса
Второй урок считал процент по трём материалам курса. После поиска список на экране может содержать только один. Если теперь разделить число завершённых на число найденных, прогресс внезапно изменится от ввода поисковой строки. Завершённые уроки не изменились, изменился только видимый набор.
Следовательно, показатель курса должен опираться на полный каталог соответствующего курса, а счётчик результатов — на фильтр. Это разные величины. Для демонстрационного проекта можно вернуть оба набора или отдельное число материалов курса. В конечных исходниках кабинет вычисляет прогресс независимо от поисковой страницы, чтобы условия отображения не переопределяли историю обучения.
То же рассуждение относится к режиму «только закладки». Он выбирает подмножество уроков, но не превращает сохранённые материалы в весь курс. Название показателя должно сообщать область вычисления. «Завершено два из трёх уроков курса» и «найдено два сохранённых материала» отвечают разным вопросам.
История браузера и повторный запрос
Поиск в URL естественно сочетается с кнопками назад и вперёд. Один адрес означает одно условие отбора. Если хранить q только в $state, после перехода назад можно увидеть прежнюю страницу с новым локальным полем или потерять запрос полностью. Это не всегда ошибка, но для каталога с отправляемыми ссылками URL даёт более подходящее состояние.
Не отправляйте новый запрос на каждый символ без необходимости. В маленьком примере форма с кнопкой достаточна. Живой поиск требует задержки, отмены устаревших запросов и объяснения загрузки. Иначе медленный ответ для старого текста способен перекрыть свежий результат. Такие сложности полезно добавлять по задаче, а не считать обязательным свойством современного интерфейса.
Функция загрузки выполняется при чтении, поэтому многократное открытие адреса не должно создавать закладки или отмечать завершение. Это продолжает правило пятого урока. Параметры URL влияют на выборку, но не на пользователя, от имени которого разрешена запись.
Переход к серверному изменению
В кабинете форма будет использовать POST: там читатель меняет собственные данные. Отличие метода следует из смысла операции, а не из вкуса автора. GET удобен для безопасного воспроизводимого чтения, POST — для команды, которую сервер должен проверить и выполнить. Данные формы в обоих случаях остаются входом, которому нельзя доверять без проверки.
Один запрос и несколько представлений
Вы можете добавить ссылку «Только HTML», заранее задав q в адресе. Она использует ту же загрузку, что форма, поэтому не создаёт отдельного поиска с другим правилом регистра или длины. Это полезное свойство URL: разные элементы управления выбирают одно понятное состояние ресурса.
Если позже появится страница результатов, параметр номера страницы тоже относится к адресу. При изменении запроса его нужно сбросить, иначе человек может остаться на третьей странице набора, в котором теперь всего одна. Такая ситуация не означает отсутствие найденных уроков; она означает несовместимые параметры представления.
В текущем проекте все найденные записи выводятся сразу. Не называйте его поиском по всему ProfessorWeb: источник содержит только три фиктивных материала. Подключение настоящего индекса потребует правил обновления, ранжирования и доступности архивных страниц. Сохранённый GET-контракт при этом остаётся хорошей внешней формой, а внутренний способ поиска может измениться.
Поиск опирается на механизмы загрузки и query string из документации SvelteKit. Правила длины запроса, допустимого курса и знаменателя прогресса определены нами для «Маршрута». Следующий урок добавит первый серверный POST с явным желаемым состоянием закладки и разберёт, почему повтор запроса не должен инвертировать результат.