diff --git a/backend/src/quick-import/parsers/generic.parser.ts b/backend/src/quick-import/parsers/generic.parser.ts
index d8227fc5..2515d0e0 100644
--- a/backend/src/quick-import/parsers/generic.parser.ts
+++ b/backend/src/quick-import/parsers/generic.parser.ts
@@ -77,7 +77,41 @@ export class GenericRecipeParser extends RecipeParser {
private extractOgImage(html: string): string | undefined {
const match = html.match(/]+property="og:image"[^>]+content="([^"]+)"/i)
|| html.match(/]+content="([^"]+)"[^>]+property="og:image"/i);
- return match ? match[1].trim() : undefined;
+ return match ? this.decodeHtmlEntities(match[1].trim()) : undefined;
+ }
+
+ private decodeHtmlEntities(value: string): string {
+ return value
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/</g, '<')
+ .replace(/>/g, '>');
+ }
+
+ private extractImageValue(image: any): string | undefined {
+ if (!image) return undefined;
+ if (typeof image === 'string') return image;
+
+ if (Array.isArray(image)) {
+ for (const item of image) {
+ const extracted = this.extractImageValue(item);
+ if (extracted) return extracted;
+ }
+ return undefined;
+ }
+
+ if (typeof image === 'object') {
+ return (
+ image.url ||
+ image['@id'] ||
+ image.contentUrl ||
+ image.thumbnailUrl ||
+ this.extractImageValue(image.image)
+ );
+ }
+
+ return undefined;
}
private extractFromJsonLd(recipe: any, ogImage?: string): ParsedRecipe {
@@ -86,14 +120,9 @@ export class GenericRecipeParser extends RecipeParser {
// Extrahera bildurl från JSON-LD
let imageUrl: string | undefined = ogImage;
- if (recipe.image) {
- if (typeof recipe.image === 'string') {
- imageUrl = recipe.image;
- } else if (Array.isArray(recipe.image) && recipe.image.length > 0) {
- imageUrl = typeof recipe.image[0] === 'string' ? recipe.image[0] : recipe.image[0]?.url;
- } else if (recipe.image?.url) {
- imageUrl = recipe.image.url;
- }
+ const extractedImage = this.extractImageValue(recipe.image);
+ if (extractedImage) {
+ imageUrl = this.decodeHtmlEntities(extractedImage);
}
const ingredients: Array<{ quantity: number; unit: string; name: string; note?: string }> = [];
diff --git a/backend/src/quick-import/parsers/ica.parser.ts b/backend/src/quick-import/parsers/ica.parser.ts
index 90d871f8..b6ae86eb 100644
--- a/backend/src/quick-import/parsers/ica.parser.ts
+++ b/backend/src/quick-import/parsers/ica.parser.ts
@@ -76,7 +76,41 @@ export class IcaRecipeParser extends RecipeParser {
private extractOgImage(html: string): string | undefined {
const match = html.match(/]+property="og:image"[^>]+content="([^"]+)"/i)
|| html.match(/]+content="([^"]+)"[^>]+property="og:image"/i);
- return match ? match[1].trim() : undefined;
+ return match ? this.decodeHtmlEntities(match[1].trim()) : undefined;
+ }
+
+ private decodeHtmlEntities(value: string): string {
+ return value
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/</g, '<')
+ .replace(/>/g, '>');
+ }
+
+ private extractImageValue(image: any): string | undefined {
+ if (!image) return undefined;
+ if (typeof image === 'string') return image;
+
+ if (Array.isArray(image)) {
+ for (const item of image) {
+ const extracted = this.extractImageValue(item);
+ if (extracted) return extracted;
+ }
+ return undefined;
+ }
+
+ if (typeof image === 'object') {
+ return (
+ image.url ||
+ image['@id'] ||
+ image.contentUrl ||
+ image.thumbnailUrl ||
+ this.extractImageValue(image.image)
+ );
+ }
+
+ return undefined;
}
private extractFromJsonLd(recipe: any, ogImage?: string): ParsedRecipe {
@@ -88,14 +122,9 @@ export class IcaRecipeParser extends RecipeParser {
// Extrahera bildurl från JSON-LD (kan vara sträng eller array)
let imageUrl: string | undefined = ogImage;
- if (recipe.image) {
- if (typeof recipe.image === 'string') {
- imageUrl = recipe.image;
- } else if (Array.isArray(recipe.image) && recipe.image.length > 0) {
- imageUrl = typeof recipe.image[0] === 'string' ? recipe.image[0] : recipe.image[0]?.url;
- } else if (recipe.image?.url) {
- imageUrl = recipe.image.url;
- }
+ const extractedImage = this.extractImageValue(recipe.image);
+ if (extractedImage) {
+ imageUrl = this.decodeHtmlEntities(extractedImage);
}
// Extrahera ingredienser
diff --git a/backend/src/quick-import/quick-import.service.ts b/backend/src/quick-import/quick-import.service.ts
index bc7a07b4..db240e17 100644
--- a/backend/src/quick-import/quick-import.service.ts
+++ b/backend/src/quick-import/quick-import.service.ts
@@ -320,7 +320,9 @@ export class QuickImportService {
let imageUrl: string | undefined;
let imageWarning: string | undefined;
if (recipe.imageUrl) {
+ this.logger.log(`Bildkandidat från parser: ${recipe.imageUrl}`);
const normalizedImageUrl = this.normalizeImageUrl(recipe.imageUrl, url);
+ this.logger.log(`Normaliserad bild-URL: ${normalizedImageUrl ?? 'null'}`);
if (!normalizedImageUrl) {
imageWarning = 'Receptbild kunde inte tolkas till en giltig URL.';
this.logger.warn(
@@ -331,7 +333,9 @@ export class QuickImportService {
imageUrl = await downloadAndOptimizeImage(normalizedImageUrl, IMAGE_DEST_DIR);
this.logger.log(`Bild optimerad och sparad: ${imageUrl}`);
} catch (imgErr) {
- imageWarning = 'Receptbild kunde inte laddas ner.';
+ // Fallback: behåll extern URL så klienten ändå kan visa bild.
+ imageUrl = normalizedImageUrl;
+ imageWarning = 'Receptbild kunde inte laddas ner lokalt; extern URL används.';
this.logger.warn(
`Kunde inte ladda ner bild: ${imgErr} (källa: ${normalizedImageUrl})`,
);
diff --git a/backend/src/recipes/recipes.service.ts b/backend/src/recipes/recipes.service.ts
index 653e4690..ca2c67cf 100644
--- a/backend/src/recipes/recipes.service.ts
+++ b/backend/src/recipes/recipes.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
@@ -27,6 +27,8 @@ interface ParsedRecipe {
@Injectable()
export class RecipesService {
+ private readonly logger = new Logger(RecipesService.name);
+
constructor(private readonly prisma: PrismaService) {}
async getInventoryPreview(id: number) {
@@ -301,17 +303,25 @@ export class RecipesService {
}
async create(createRecipeDto: CreateRecipeDto, userId: number) {
+ this.logger.log(
+ `[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`,
+ );
+
// Om imageUrl är en extern URL — ladda ner och optimera
let imageUrl: string | null = createRecipeDto.imageUrl || null;
if (imageUrl && imageUrl.startsWith('http')) {
+ const externalImageUrl = imageUrl;
try {
imageUrl = await downloadAndOptimizeImage(imageUrl, IMAGE_DEST_DIR);
} catch (err) {
console.warn('[RecipesService] Kunde inte ladda ner receptbild:', err);
- imageUrl = null;
+ // Behåll extern URL som fallback så bild fortfarande visas.
+ imageUrl = externalImageUrl;
}
}
+ this.logger.log(`[create] Final imageUrl persisted to DB: ${imageUrl ?? 'null'}`);
+
const recipe = await this.prisma.recipe.create({
data: {
name: createRecipeDto.name,
diff --git a/frontend/app/api/quick-import-proxy/route.ts b/frontend/app/api/quick-import-proxy/route.ts
index 6b4f8aa5..26ab6720 100644
--- a/frontend/app/api/quick-import-proxy/route.ts
+++ b/frontend/app/api/quick-import-proxy/route.ts
@@ -19,6 +19,22 @@ export const POST = withAuth(async (request, session) => {
});
const text = await response.text();
+ try {
+ const parsed = JSON.parse(text);
+ // eslint-disable-next-line no-console
+ console.log('[QuickImportProxy] backend response', {
+ status: response.status,
+ hasMarkdown: Boolean(parsed?.markdown),
+ imageUrl: parsed?.imageUrl ?? null,
+ imageWarning: parsed?.imageWarning ?? null,
+ });
+ } catch {
+ // eslint-disable-next-line no-console
+ console.log('[QuickImportProxy] backend non-json response', {
+ status: response.status,
+ contentType: response.headers.get('content-type'),
+ });
+ }
return new NextResponse(text, {
status: response.status,
headers: { 'Content-Type': response.headers.get('content-type') ?? 'application/json' },
diff --git a/frontend/app/import/ImportTabsClient.tsx b/frontend/app/import/ImportTabsClient.tsx
index fa44767d..8bce3d86 100644
--- a/frontend/app/import/ImportTabsClient.tsx
+++ b/frontend/app/import/ImportTabsClient.tsx
@@ -83,12 +83,23 @@ function ReceptImport() {
const res = await fetch('/api/quick-import-proxy', { method: 'POST', body: formData });
if (!res.ok) throw new Error(await parseErrorResponse(res));
const data = await res.json();
+ // eslint-disable-next-line no-console
+ console.log('[ImportTabsClient:file] quick-import response', {
+ imageUrl: data.imageUrl ?? null,
+ imageWarning: data.imageWarning ?? null,
+ markdownLength: (data.markdown ?? '').length,
+ });
sessionStorage.setItem('prefilled_markdown', data.markdown ?? '');
if (data.imageUrl) {
sessionStorage.setItem('prefilled_image_url', data.imageUrl);
} else {
sessionStorage.removeItem('prefilled_image_url');
}
+ // eslint-disable-next-line no-console
+ console.log('[ImportTabsClient:file] sessionStorage snapshot', {
+ prefilled_markdown: sessionStorage.getItem('prefilled_markdown')?.length ?? 0,
+ prefilled_image_url: sessionStorage.getItem('prefilled_image_url'),
+ });
router.push('/recipes/write');
} catch (err) {
setError(err instanceof Error ? err.message : 'Importen misslyckades.');
@@ -110,12 +121,23 @@ function ReceptImport() {
});
if (!res.ok) throw new Error(await parseErrorResponse(res));
const data = await res.json();
+ // eslint-disable-next-line no-console
+ console.log('[ImportTabsClient:url] quick-import response', {
+ imageUrl: data.imageUrl ?? null,
+ imageWarning: data.imageWarning ?? null,
+ markdownLength: (data.markdown ?? '').length,
+ });
sessionStorage.setItem('prefilled_markdown', data.markdown ?? '');
if (data.imageUrl) {
sessionStorage.setItem('prefilled_image_url', data.imageUrl);
} else {
sessionStorage.removeItem('prefilled_image_url');
}
+ // eslint-disable-next-line no-console
+ console.log('[ImportTabsClient:url] sessionStorage snapshot', {
+ prefilled_markdown: sessionStorage.getItem('prefilled_markdown')?.length ?? 0,
+ prefilled_image_url: sessionStorage.getItem('prefilled_image_url'),
+ });
router.push('/recipes/write');
} catch (err) {
setError(err instanceof Error ? err.message : 'Importen misslyckades.');
diff --git a/frontend/app/recipes/import/ImportFilePage.tsx b/frontend/app/recipes/import/ImportFilePage.tsx
index 8c806fdd..0d5a79b9 100644
--- a/frontend/app/recipes/import/ImportFilePage.tsx
+++ b/frontend/app/recipes/import/ImportFilePage.tsx
@@ -39,12 +39,23 @@ export default function ImportFilePage() {
}
const data = await res.json();
+ // eslint-disable-next-line no-console
+ console.log('[ImportFilePage:file] quick-import response', {
+ imageUrl: data.imageUrl ?? null,
+ imageWarning: data.imageWarning ?? null,
+ markdownLength: (data.markdown ?? '').length,
+ });
sessionStorage.setItem('prefilled_markdown', data.markdown ?? '');
if (data.imageUrl) {
sessionStorage.setItem('prefilled_image_url', data.imageUrl);
} else {
sessionStorage.removeItem('prefilled_image_url');
}
+ // eslint-disable-next-line no-console
+ console.log('[ImportFilePage:file] sessionStorage snapshot', {
+ prefilled_markdown: sessionStorage.getItem('prefilled_markdown')?.length ?? 0,
+ prefilled_image_url: sessionStorage.getItem('prefilled_image_url'),
+ });
router.push('/recipes/write');
} catch (err) {
setError(err instanceof Error ? err.message : 'Importen misslyckades.');
@@ -77,12 +88,23 @@ export default function ImportFilePage() {
}
const data = await res.json();
+ // eslint-disable-next-line no-console
+ console.log('[ImportFilePage:url] quick-import response', {
+ imageUrl: data.imageUrl ?? null,
+ imageWarning: data.imageWarning ?? null,
+ markdownLength: (data.markdown ?? '').length,
+ });
sessionStorage.setItem('prefilled_markdown', data.markdown ?? '');
if (data.imageUrl) {
sessionStorage.setItem('prefilled_image_url', data.imageUrl);
} else {
sessionStorage.removeItem('prefilled_image_url');
}
+ // eslint-disable-next-line no-console
+ console.log('[ImportFilePage:url] sessionStorage snapshot', {
+ prefilled_markdown: sessionStorage.getItem('prefilled_markdown')?.length ?? 0,
+ prefilled_image_url: sessionStorage.getItem('prefilled_image_url'),
+ });
router.push('/recipes/write');
} catch (err) {
setError(err instanceof Error ? err.message : 'Importen misslyckades.');
diff --git a/frontend/app/recipes/write/WriteRecipePage.tsx b/frontend/app/recipes/write/WriteRecipePage.tsx
index 60ecb8b1..ec6d02c9 100644
--- a/frontend/app/recipes/write/WriteRecipePage.tsx
+++ b/frontend/app/recipes/write/WriteRecipePage.tsx
@@ -62,6 +62,11 @@ export default function WriteRecipePage() {
// Kontrollera om det finns förifylld Markdown från snabbimport
const prefilledMarkdown = sessionStorage.getItem('prefilled_markdown');
const prefilledImageUrl = sessionStorage.getItem('prefilled_image_url');
+ // eslint-disable-next-line no-console
+ console.log('[WriteRecipePage] prefilled values', {
+ prefilledMarkdownLength: prefilledMarkdown?.length ?? 0,
+ prefilledImageUrl,
+ });
if (prefilledImageUrl) {
setImageUrl(prefilledImageUrl);
sessionStorage.removeItem('prefilled_image_url');
@@ -191,6 +196,8 @@ export default function WriteRecipePage() {
note: ing.editedNote || undefined,
})),
};
+ // eslint-disable-next-line no-console
+ console.log('[WriteRecipePage] create payload imageUrl', body.imageUrl ?? null);
try {
const res = await authFetch('/api/recipes', {
@@ -203,6 +210,9 @@ export default function WriteRecipePage() {
throw new Error(errorMessage);
}
+ // eslint-disable-next-line no-console
+ console.log('[WriteRecipePage] recipe create success');
+
setStep('saved');
router.refresh();
setTimeout(() => router.push('/recipes'), 2000);