From f3b92e8b1aff5df17c756247e1f08dda2a5b4aef Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Fri, 23 Jan 2026 15:04:44 +0100 Subject: [PATCH] refactor: clean up recipe routes and reduce bundle size - Eliminate duplicate API fetch in recipe page by passing item from server load to universal load instead of fetching twice - Replace cheerio with simple regex in stripHtmlTags, removing ~200KB dependency - Refactor multiplier buttons in IngredientsPage to use loop instead of 5 repeated form elements - Move /rezepte/untranslated to /[recipeLang]/admin/untranslated and delete legacy /rezepte/ layout files --- package.json | 1 - pnpm-lock.yaml | 120 ------------------ src/lib/components/IngredientsPage.svelte | 81 ++++-------- src/lib/js/stripHtmlTags.ts | 14 +- .../[name]/+page.server.ts | 10 +- .../[recipeLang=recipeLang]/[name]/+page.ts | 14 +- .../admin}/untranslated/+page.server.ts | 10 +- .../admin}/untranslated/+page.svelte | 6 +- .../administration/+page.svelte | 2 +- src/routes/rezepte/+layout.server.ts | 9 -- src/routes/rezepte/+layout.svelte | 43 ------- 11 files changed, 58 insertions(+), 252 deletions(-) rename src/routes/{rezepte => [recipeLang=recipeLang]/admin}/untranslated/+page.server.ts (72%) rename src/routes/{rezepte => [recipeLang=recipeLang]/admin}/untranslated/+page.svelte (95%) delete mode 100644 src/routes/rezepte/+layout.server.ts delete mode 100644 src/routes/rezepte/+layout.svelte diff --git a/package.json b/package.json index 8a4b078..4f114f4 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@auth/sveltekit": "^1.11.1", "@sveltejs/adapter-node": "^5.0.0", "chart.js": "^4.5.0", - "cheerio": "1.0.0-rc.12", "file-type": "^19.0.0", "ioredis": "^5.9.0", "mongoose": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 862327d..7fbce3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: chart.js: specifier: ^4.5.0 version: 4.5.0 - cheerio: - specifier: 1.0.0-rc.12 - version: 1.0.0-rc.12 file-type: specifier: ^19.0.0 version: 19.6.0 @@ -813,9 +810,6 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bson@6.10.4: resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} engines: {node: '>=16.20.1'} @@ -831,13 +825,6 @@ packages: resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} engines: {pnpm: '>=8'} - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -874,17 +861,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -951,23 +931,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -1040,9 +1003,6 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1212,18 +1172,9 @@ packages: resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} engines: {node: '>=6.0.0'} - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - oauth4webapi@3.8.1: resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==} - parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} - - parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -2188,8 +2139,6 @@ snapshots: dependencies: require-from-string: 2.0.2 - boolbase@1.0.0: {} - bson@6.10.4: {} buffer-from@1.1.2: {} @@ -2200,25 +2149,6 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - - cheerio@1.0.0-rc.12: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.1.0 - htmlparser2: 8.0.2 - parse5: 7.1.2 - parse5-htmlparser2-tree-adapter: 7.0.0 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -2249,21 +2179,11 @@ snapshots: cookie@0.6.0: {} - css-select@5.1.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.1.0 - nth-check: 2.1.1 - css-tree@3.1.0: dependencies: mdn-data: 2.12.2 source-map-js: 1.2.1 - css-what@6.1.0: {} - css.escape@1.5.1: {} cssstyle@5.3.3: @@ -2305,26 +2225,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.1.0: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - entities@4.5.0: {} - entities@6.0.1: {} es-module-lexer@1.7.0: {} @@ -2408,13 +2308,6 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2585,21 +2478,8 @@ snapshots: node-cron@4.2.1: {} - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - oauth4webapi@3.8.1: {} - parse5-htmlparser2-tree-adapter@7.0.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.1.2 - - parse5@7.1.2: - dependencies: - entities: 4.5.0 - parse5@8.0.0: dependencies: entities: 6.0.1 diff --git a/src/lib/components/IngredientsPage.svelte b/src/lib/components/IngredientsPage.svelte index 113ed79..8b9b46b 100644 --- a/src/lib/components/IngredientsPage.svelte +++ b/src/lib/components/IngredientsPage.svelte @@ -124,6 +124,15 @@ const labels = $derived({ ingredients: isEnglish ? 'Ingredients' : 'Zutaten' }); +// Multiplier button options +const multiplierOptions = [ + { value: 0.5, label: '1/2x' }, + { value: 1, label: '1x' }, + { value: 1.5, label: '3/2x' }, + { value: 2, label: '2x' }, + { value: 3, label: '3x' } +]; + // Calculate yeast IDs for each yeast ingredient const yeastIds = $derived.by(() => { const ids = {}; @@ -447,65 +456,31 @@ h3 a:hover {

{labels.adjustAmount}

-
- - {#each Array.from(currentParams.entries()) as [key, value]} - {#if key !== 'multiplier'} - - {/if} - {/each} - -
-
- - {#each Array.from(currentParams.entries()) as [key, value]} - {#if key !== 'multiplier'} - - {/if} - {/each} - -
-
- - {#each Array.from(currentParams.entries()) as [key, value]} - {#if key !== 'multiplier'} - - {/if} - {/each} - -
-
- - {#each Array.from(currentParams.entries()) as [key, value]} - {#if key !== 'multiplier'} - - {/if} - {/each} - -
-
- - {#each Array.from(currentParams.entries()) as [key, value]} - {#if key !== 'multiplier'} - - {/if} - {/each} - -
+ {#each multiplierOptions as opt} +
+ + {#each Array.from(currentParams.entries()) as [key, value]} + {#if key !== 'multiplier'} + + {/if} + {/each} + +
+ {/each}
{#each Array.from(currentParams.entries()) as [key, value]} {#if key !== 'multiplier'} - + {/if} {/each} - o.value === multiplier) ? multiplier : ''} oninput={handleCustomInput} /> diff --git a/src/lib/js/stripHtmlTags.ts b/src/lib/js/stripHtmlTags.ts index aab2b2d..175070b 100644 --- a/src/lib/js/stripHtmlTags.ts +++ b/src/lib/js/stripHtmlTags.ts @@ -1,10 +1,16 @@ // Function to strip HTML tags from a string -import {load} from 'cheerio'; - export function stripHtmlTags(input: string | undefined | null): string { if (!input) { return ''; } - const $ = load(input.replace(/­/g, '')); - return $.text(); + return input + .replace(/­/g, '') // Remove soft hyphens + .replace(/<[^>]*>/g, '') // Remove HTML tags + .replace(/&/g, '&') // Decode common HTML entities + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, ' ') + .trim(); } diff --git a/src/routes/[recipeLang=recipeLang]/[name]/+page.server.ts b/src/routes/[recipeLang=recipeLang]/[name]/+page.server.ts index 01228d2..ae46146 100644 --- a/src/routes/[recipeLang=recipeLang]/[name]/+page.server.ts +++ b/src/routes/[recipeLang=recipeLang]/[name]/+page.server.ts @@ -2,18 +2,13 @@ import { redirect, error } from '@sveltejs/kit'; import { stripHtmlTags } from '$lib/js/stripHtmlTags'; export async function load({ params, fetch }) { - // Fetch recipe data to strip HTML tags server-side - // This avoids bundling cheerio in the client bundle const isEnglish = params.recipeLang === 'recipes'; const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte'; const res = await fetch(`${apiBase}/items/${params.name}`); if (!res.ok) { - // Let the universal load function handle the error - return { - strippedName: '', - strippedDescription: '', - }; + const errorData = await res.json().catch(() => ({ message: 'Recipe not found' })); + throw error(res.status, errorData.message); } const item = await res.json(); @@ -21,6 +16,7 @@ export async function load({ params, fetch }) { const strippedDescription = stripHtmlTags(item.description); return { + item, strippedName, strippedDescription, }; diff --git a/src/routes/[recipeLang=recipeLang]/[name]/+page.ts b/src/routes/[recipeLang=recipeLang]/[name]/+page.ts index a27221f..da8d024 100644 --- a/src/routes/[recipeLang=recipeLang]/[name]/+page.ts +++ b/src/routes/[recipeLang=recipeLang]/[name]/+page.ts @@ -1,15 +1,10 @@ -import { error } from "@sveltejs/kit"; import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd'; export async function load({ fetch, params, url, data }) { const isEnglish = params.recipeLang === 'recipes'; - const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte'; - const res = await fetch(`${apiBase}/items/${params.name}`); - let item = await res.json(); - if(!res.ok){ - throw error(res.status, item.message) - } + // Use item from server load - no duplicate fetch needed + let item = { ...data.item }; // Check if this recipe is favorited by the user let isFavorite = false; @@ -118,8 +113,11 @@ export async function load({ fetch, params, url, data }) { const englishShortName = !isEnglish ? (item.translations?.en?.short_name || '') : ''; const germanShortName = isEnglish ? (item.germanShortName || '') : item.short_name; + // Destructure to exclude item (already spread below) + const { item: _, ...serverData } = data; + return { - ...data, // Include server load data (strippedName, strippedDescription) + ...serverData, // Include server load data (strippedName, strippedDescription) ...item, isFavorite, multiplier, diff --git a/src/routes/rezepte/untranslated/+page.server.ts b/src/routes/[recipeLang=recipeLang]/admin/untranslated/+page.server.ts similarity index 72% rename from src/routes/rezepte/untranslated/+page.server.ts rename to src/routes/[recipeLang=recipeLang]/admin/untranslated/+page.server.ts index e3b10f2..3731e25 100644 --- a/src/routes/rezepte/untranslated/+page.server.ts +++ b/src/routes/[recipeLang=recipeLang]/admin/untranslated/+page.server.ts @@ -1,12 +1,13 @@ import type { PageServerLoad } from "./$types"; import { redirect, error } from '@sveltejs/kit'; -export const load: PageServerLoad = async ({ fetch, locals }) => { +export const load: PageServerLoad = async ({ fetch, locals, url, params }) => { const session = await locals.auth(); // Redirect to login if not authenticated if (!session?.user?.nickname) { - throw redirect(302, '/login?callbackUrl=/rezepte/untranslated'); + const callbackUrl = encodeURIComponent(url.pathname); + throw redirect(302, `/login?callbackUrl=${callbackUrl}`); } // Check user group permission @@ -21,6 +22,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => { return { untranslated: [], session, + recipeLang: params.recipeLang, error: 'Fehler beim Laden der unübersetzten Rezepte' }; } @@ -29,12 +31,14 @@ export const load: PageServerLoad = async ({ fetch, locals }) => { return { untranslated, - session + session, + recipeLang: params.recipeLang }; } catch (e) { return { untranslated: [], session, + recipeLang: params.recipeLang, error: 'Fehler beim Laden der unübersetzten Rezepte' }; } diff --git a/src/routes/rezepte/untranslated/+page.svelte b/src/routes/[recipeLang=recipeLang]/admin/untranslated/+page.svelte similarity index 95% rename from src/routes/rezepte/untranslated/+page.svelte rename to src/routes/[recipeLang=recipeLang]/admin/untranslated/+page.svelte index 467739d..8830392 100644 --- a/src/routes/rezepte/untranslated/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/admin/untranslated/+page.svelte @@ -127,16 +127,16 @@ h1 { {/each} {:else}
-

🎉 Alle Rezepte sind übersetzt!

+

Alle Rezepte sind übersetzt!

- Zurück zu den Rezepten + Zurück zu den Rezepten

{/if} diff --git a/src/routes/[recipeLang=recipeLang]/administration/+page.svelte b/src/routes/[recipeLang=recipeLang]/administration/+page.svelte index 7da2f5e..fd9e8f5 100644 --- a/src/routes/[recipeLang=recipeLang]/administration/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/administration/+page.svelte @@ -16,7 +16,7 @@ description: isEnglish ? 'View and manage recipes that need translation' : 'Rezepte ansehen und verwalten, die übersetzt werden müssen', - href: '/rezepte/untranslated', + href: `/${data.recipeLang}/admin/untranslated`, icon: '🌐' }, { diff --git a/src/routes/rezepte/+layout.server.ts b/src/routes/rezepte/+layout.server.ts deleted file mode 100644 index 0e4f90a..0000000 --- a/src/routes/rezepte/+layout.server.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { LayoutServerLoad } from './$types'; - -export const load: LayoutServerLoad = async ({ locals }) => { - const session = await locals.auth(); - - return { - session - }; -}; diff --git a/src/routes/rezepte/+layout.svelte b/src/routes/rezepte/+layout.svelte deleted file mode 100644 index dde6a4b..0000000 --- a/src/routes/rezepte/+layout.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
- {#snippet links()} - - {/snippet} - - {#snippet language_selector_mobile()} - - {/snippet} - - {#snippet language_selector_desktop()} - - {/snippet} - - {#snippet right_side()} - - {/snippet} - - {@render children()} -