Files
homepage/src/routes/api/[recipeLang=recipeLang]/search/+server.ts
T
Alexander eb3604f9ea perf: drop redundant JSON.parse(JSON.stringify()) in recipe API
Every recipe list endpoint wrapped its result in
`JSON.parse(JSON.stringify(...))` before handing it to `json()`, which
then serialises again — a full extra stringify+parse cycle per response.
`lean()` already returns plain objects and ObjectIds/Dates serialise
correctly through `json()`'s single `JSON.stringify`, so the extra round
trip was pure waste.

Removed from the 9 output-side call sites (all_brief, category,
category/[cat], tag, tag/[tag], icon, icon/[icon], in_season/[month],
search, favorites/recipes, offline-db, translate/untranslated).
Kept the two deep-clone-before-mutation usages in items/[name] and
json-ld/[name] — those are load-bearing.

Shuffle stays server-side: moving it to the client would need a hero
preload + hydration rework that's bigger than a perf tweak.
2026-04-23 15:00:37 +02:00

79 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { isEnglish, briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ url, params, locals }) => {
await dbConnect();
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
const en = isEnglish(params.recipeLang!);
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
const category = url.searchParams.get('category');
const singleTag = url.searchParams.get('tag');
const multipleTags = url.searchParams.get('tags');
const tags = multipleTags
? multipleTags.split(',').map(t => t.trim()).filter(Boolean)
: (singleTag ? [singleTag] : []);
const icon = url.searchParams.get('icon');
const singleSeason = url.searchParams.get('season');
const multipleSeasons = url.searchParams.get('seasons');
const seasons = multipleSeasons
? multipleSeasons.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n))
: (singleSeason ? [parseInt(singleSeason)].filter(n => !isNaN(n)) : []);
const favoritesOnly = url.searchParams.get('favorites') === 'true';
try {
let dbQuery: Record<string, unknown> = { ...approvalFilter };
if (category) {
dbQuery[`${prefix}category`] = category;
}
if (tags.length > 0) {
dbQuery[`${prefix}tags`] = { $all: tags };
}
if (icon) {
dbQuery.icon = icon;
}
if (seasons.length > 0) {
dbQuery.season = { $in: seasons };
}
const dbRecipes = await Recipe.find(dbQuery, projection).lean();
let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang!));
// Handle favorites filter
const session = await locals.auth();
if (favoritesOnly && session?.user) {
const { UserFavorites } = await import('$models/UserFavorites');
const userFavorites = await UserFavorites.findOne({ username: session.user.nickname });
if (userFavorites?.favorites) {
const favoriteIds = userFavorites.favorites;
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
} else {
recipes = [];
}
}
// Apply text search
if (query) {
const searchTerms = query.normalize('NFD').replace(/\p{Diacritic}/gu, "").split(" ");
recipes = recipes.filter(recipe => {
const searchString = `${recipe.name} ${recipe.description || ''} ${recipe.tags?.join(' ') || ''}`.toLowerCase()
.normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/&shy;|­/g, '');
return searchTerms.every(term => searchString.includes(term));
});
}
return json(recipes);
} catch (e) {
return json({ error: 'Search failed' }, { status: 500 });
}
};