@mailoo/blog 1.33.0 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/index.ts","../../src/server/client.ts"],"sourcesContent":["import type { NextRequest } from 'next/server'\nimport {\n defaultMailooLogger,\n jsonError,\n proxyMailooWebhookJsonPost,\n type MailooIntegrationConfig,\n type MailooLogger,\n} from '@mailoo/next-core'\nimport { getMailooBlogConfigFromEnv } from '../server/client.js'\n\nexport type CreateBlogRoutesOptions = {\n /** Explicit config; defaults to MAILOO_BLOG_* env */\n getConfig?: () => MailooIntegrationConfig | null\n logger?: MailooLogger\n}\n\nfunction resolveConfig(\n options?: CreateBlogRoutesOptions\n): MailooIntegrationConfig | null {\n return options?.getConfig?.() ?? getMailooBlogConfigFromEnv()\n}\n\nfunction notConfigured(logger: MailooLogger): Response {\n logger.error(\n 'MAILOO_BLOG_PROJECT_UID, MAILOO_BLOG_INTEGRATION_ID, MAILOO_BLOG_API and MAILOO_BLOG_API_KEY must be set'\n )\n return jsonError({ error: 'Blog API not configured' }, 503)\n}\n\nasync function proxyGet(\n config: MailooIntegrationConfig,\n pathSuffix: string,\n searchParams?: URLSearchParams\n): Promise<Response> {\n const url = new URL(\n `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}${pathSuffix}`\n )\n if (searchParams) {\n searchParams.forEach((value, key) => {\n url.searchParams.append(key, value)\n })\n }\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n })\n const data = await response.json()\n return new Response(JSON.stringify(data), {\n status: response.status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\n/** Factory: GET list posts — mount at `app/api/v1/blog/route.ts` */\nexport function createBlogListHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(config, '', request.nextUrl.searchParams)\n } catch (error) {\n logger.logException?.(error, 'Blog API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET post by slug — mount at `app/api/v1/blog/slug/[slug]/route.ts` */\nexport function createBlogSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n const locale = request.nextUrl.searchParams.get('locale')\n const qs = new URLSearchParams()\n if (locale) qs.set('locale', locale)\n return await proxyGet(config, `/slug/${encodeURIComponent(slug)}`, qs)\n } catch (error) {\n logger.logException?.(error, 'Blog slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET categories */\nexport function createBlogCategoriesHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(config, '/categories', request.nextUrl.searchParams)\n } catch (error) {\n logger.logException?.(error, 'Blog categories API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tags */\nexport function createBlogTagsHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(config, '/tags', request.nextUrl.searchParams)\n } catch (error) {\n logger.logException?.(error, 'Blog tags API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tag by slug */\nexport function createBlogTagSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n request.nextUrl.searchParams\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tag slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET category description by slug */\nexport function createBlogCategoryDescriptionHandler(\n options?: CreateBlogRoutesOptions\n) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n const locale = request.nextUrl.searchParams.get('locale')\n const qs = new URLSearchParams()\n if (locale) qs.set('locale', locale)\n return await proxyGet(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n qs\n )\n } catch (error) {\n logger.logException?.(error, 'Blog category description API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET JSON-LD by slug — mount at `app/api/v1/blog/slug/[slug]/json-ld/route.ts` */\nexport function createBlogJsonLdHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n const locale = request.nextUrl.searchParams.get('locale')\n const qs = new URLSearchParams()\n if (locale) qs.set('locale', locale)\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n qs\n )\n } catch (error) {\n logger.logException?.(error, 'Blog JSON-LD API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\nasync function proxyMarkPost(\n options: CreateBlogRoutesOptions | undefined,\n request: NextRequest,\n slug: string,\n pathKind: 'reaction' | 'feedback',\n logLabel: string\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonError({ error: 'Invalid JSON body' }, 400)\n }\n const url = `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}/slug/${encodeURIComponent(slug)}/${pathKind}`\n try {\n return await proxyMailooWebhookJsonPost({\n request,\n url,\n apiKey: config.apiKey,\n body,\n })\n } catch (error) {\n logger.logException?.(error, logLabel)\n return jsonError({ error: 'Internal server error' }, 500)\n }\n}\n\n/** Factory: POST reaction — mount at `app/api/v1/blog/slug/[slug]/reaction/route.ts` */\nexport function createBlogReactionHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'reaction',\n 'Blog reaction proxy error'\n )\n }\n}\n\n/** Factory: POST feedback — mount at `app/api/v1/blog/slug/[slug]/feedback/route.ts` */\nexport function createBlogFeedbackHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'feedback',\n 'Blog feedback proxy error'\n )\n }\n}\n\nexport function createMailooBlogRoutes(options?: CreateBlogRoutesOptions) {\n return {\n list: createBlogListHandler(options),\n slug: createBlogSlugHandler(options),\n jsonLd: createBlogJsonLdHandler(options),\n categories: createBlogCategoriesHandler(options),\n tags: createBlogTagsHandler(options),\n tagSlug: createBlogTagSlugHandler(options),\n categoryDescription: createBlogCategoryDescriptionHandler(options),\n reaction: createBlogReactionHandler(options),\n feedback: createBlogFeedbackHandler(options),\n }\n}\n","import { unstable_rethrow } from 'next/navigation'\nimport type { MailooIntegrationConfig } from '@mailoo/next-core'\nimport { readMailooIntegrationConfig } from '@mailoo/next-core'\nimport {\n rewriteMailooBlogMediaUrls,\n rewriteMailooPublicImageUrls,\n rewriteMailooPublicImageUrlsAnyHost,\n} from '@mailoo/images'\nimport type {\n BlogArticleMarkState,\n BlogCategoryApi,\n BlogTagApi,\n BlogListPagination,\n BlogListQuery,\n BlogPostApiData,\n MailooBlogFetchOptions,\n SetBlogFeedbackInput,\n SetBlogReactionInput,\n} from '../types.js'\n\nexport type MailooBlogConfig = MailooIntegrationConfig\nexport type { MailooBlogFetchOptions }\n\n/** Host image proxy. Omit to keep canonical Mailoo embed URLs. */\nexport type CreateMailooBlogClientOptions = {\n /**\n * Integrator prefix ending with `?id=` (absolute or same-origin path).\n * Example: `/api/v1/images/public/content?id=`\n */\n imageProxyPrefixWithIdEquals?: string\n}\n\nconst DEFAULT_REVALIDATE = 60\n\n/** Read MAILOO_BLOG_* env (API, API_KEY, PROJECT_UID, INTEGRATION_ID). */\nexport function getMailooBlogConfigFromEnv(): MailooBlogConfig | null {\n return readMailooIntegrationConfig({ prefix: 'MAILOO_BLOG' })\n}\n\nfunction blogBasePath(config: MailooBlogConfig): string {\n return `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}`\n}\n\nfunction resolveRevalidate(options?: MailooBlogFetchOptions): number {\n return options?.revalidate ?? DEFAULT_REVALIDATE\n}\n\nasync function blogFetchJson(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; json: unknown; status: number } | { ok: false; status: number }\n> {\n const url = new URL(`${blogBasePath(config)}${path}`)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== '') url.searchParams.set(k, v)\n }\n }\n try {\n const revalidate = resolveRevalidate(options)\n const res = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n next: { revalidate },\n } as RequestInit)\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n return { ok: true, json: await res.json(), status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogPostJson(\n config: MailooBlogConfig,\n path: string,\n body: unknown\n): Promise<\n | { ok: true; data: BlogArticleMarkState; status: number }\n | { ok: false; status: number }\n> {\n try {\n const res = await fetch(`${blogBasePath(config)}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n body: JSON.stringify(body),\n cache: 'no-store',\n })\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n const json = (await res.json()) as { data?: BlogArticleMarkState }\n if (!json.data || typeof json.data !== 'object') {\n return { ok: false, status: 500 }\n }\n return { ok: true, data: json.data, status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogGet<T>(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; data: T; status: number } | { ok: false; status: number }\n> {\n const fetched = await blogFetchJson(config, path, query, options)\n if (!fetched.ok) return fetched\n const json = fetched.json as { data?: T } & T\n const data =\n (json as { data?: T }).data !== undefined\n ? (json as { data: T }).data\n : (json as T)\n return { ok: true, data, status: fetched.status }\n}\n\nfunction isBlogListPagination(value: unknown): value is BlogListPagination {\n if (value === null || typeof value !== 'object') return false\n const p = value as Record<string, unknown>\n return (\n typeof p.page === 'number' &&\n typeof p.limit === 'number' &&\n typeof p.total === 'number' &&\n typeof p.totalPages === 'number' &&\n typeof p.hasNextPage === 'boolean' &&\n typeof p.hasPrevPage === 'boolean'\n )\n}\n\nfunction queryRecord(query: BlogListQuery): Record<string, string | undefined> {\n const q: Record<string, string | undefined> = {}\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue\n q[k] = String(v)\n }\n return q\n}\n\nfunction imageProxyPrefix(\n options?: CreateMailooBlogClientOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction rewritePost(\n post: BlogPostApiData,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): BlogPostApiData {\n return rewriteMailooBlogMediaUrls(post, {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n })\n}\n\ntype CategoryDescription = {\n description?: string\n descriptionHtml?: string\n imagePublicEmbedBaseUrl?: string\n}\n\nexport function createMailooBlogClient(\n config: MailooBlogConfig,\n clientOptions?: CreateMailooBlogClientOptions\n) {\n const proxyPrefix = imageProxyPrefix(clientOptions)\n return {\n async listPosts(\n query: BlogListQuery = {},\n options?: MailooBlogFetchOptions\n ): Promise<\n | {\n ok: true\n data: BlogPostApiData[]\n pagination: BlogListPagination\n imagePublicEmbedBaseUrl: string\n status: number\n }\n | { ok: false; status: number }\n > {\n const fetched = await blogFetchJson(\n config,\n '',\n queryRecord(query),\n options\n )\n if (!fetched.ok) return fetched\n const json = fetched.json\n if (json === null || typeof json !== 'object') {\n return { ok: false, status: 500 }\n }\n const body = json as {\n data?: unknown\n pagination?: unknown\n imagePublicEmbedBaseUrl?: unknown\n }\n const imagePublicEmbedBaseUrl =\n typeof body.imagePublicEmbedBaseUrl === 'string'\n ? body.imagePublicEmbedBaseUrl.trim()\n : ''\n if (\n !Array.isArray(body.data) ||\n !isBlogListPagination(body.pagination) ||\n !imagePublicEmbedBaseUrl\n ) {\n return { ok: false, status: 500 }\n }\n const posts = body.data as BlogPostApiData[]\n return {\n ok: true,\n data: proxyPrefix\n ? posts.map((post) =>\n rewritePost(post, imagePublicEmbedBaseUrl, proxyPrefix)\n )\n : posts,\n pagination: body.pagination,\n imagePublicEmbedBaseUrl,\n status: fetched.status,\n }\n },\n\n async getPostBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<BlogPostApiData>(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewritePost(result.data, canonical, proxyPrefix),\n }\n },\n\n async getPostJsonLd(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<{ jsonLd: Record<string, unknown> }>(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const rewritten = rewriteMailooPublicImageUrlsAnyHost(\n JSON.stringify(result.data.jsonLd),\n proxyPrefix\n )\n try {\n return {\n ...result,\n data: { jsonLd: JSON.parse(rewritten) as Record<string, unknown> },\n }\n } catch {\n return { ok: false as const, status: 500 }\n }\n },\n\n async listCategories(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogCategoryApi[]>(\n config,\n '/categories',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async listTags(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogTagApi[]>(\n config,\n '/tags',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getTagBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n return blogGet<BlogTagApi>(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getCategoryDescription(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<CategoryDescription>(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n const opts = {\n canonicalPrefixWithIdEquals: canonical,\n proxyPrefixWithIdEquals: proxyPrefix,\n }\n return {\n ...result,\n data: {\n ...result.data,\n description:\n typeof result.data.description === 'string'\n ? rewriteMailooPublicImageUrls(result.data.description, opts)\n : result.data.description,\n descriptionHtml:\n typeof result.data.descriptionHtml === 'string'\n ? rewriteMailooPublicImageUrls(result.data.descriptionHtml, opts)\n : result.data.descriptionHtml,\n },\n }\n },\n\n async setReaction(slug: string, input: SetBlogReactionInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/reaction`,\n input\n )\n },\n\n async setFeedback(slug: string, input: SetBlogFeedbackInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/feedback`,\n input\n )\n },\n }\n}\n\nexport type MailooBlogClient = ReturnType<typeof createMailooBlogClient>\n"],"mappings":"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","names":["import_next_core"]}
1
+ {"version":3,"sources":["../../src/routes/index.ts","../../src/server/client.ts","../../src/server/image-rewrite.ts"],"sourcesContent":["import type { NextRequest } from 'next/server'\nimport {\n defaultMailooLogger,\n jsonError,\n proxyMailooWebhookJsonPost,\n type MailooIntegrationConfig,\n type MailooLogger,\n} from '@mailoo/next-core'\nimport { getMailooBlogConfigFromEnv } from '../server/client.js'\nimport {\n rewriteBlogCategoryDescriptionEnvelope,\n rewriteBlogJsonLdEnvelope,\n rewriteBlogListEnvelope,\n rewriteBlogPostEnvelope,\n type BlogImageRewriteResult,\n} from '../server/image-rewrite.js'\n\nexport type CreateBlogRoutesOptions = {\n /** Explicit config; defaults to MAILOO_BLOG_* env */\n getConfig?: () => MailooIntegrationConfig | null\n logger?: MailooLogger\n /**\n * Integrator prefix ending with `?id=`. When set, list, slug, category\n * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.\n * Example: `/api/v1/images/public/content?id=`\n */\n imageProxyPrefixWithIdEquals?: string\n /**\n * Used only when the query has no `locale`. Host maps Accept-Language\n * to its own locale list.\n */\n resolveLocale?: (request: NextRequest) => string | undefined\n}\n\nconst EMBED_BASE_ERROR =\n 'Blog API contract violation: imagePublicEmbedBaseUrl missing'\n\ntype BlogGetKind =\n 'list' | 'post' | 'category-description' | 'json-ld' | 'passthrough'\n\nfunction resolveConfig(\n options?: CreateBlogRoutesOptions\n): MailooIntegrationConfig | null {\n return options?.getConfig?.() ?? getMailooBlogConfigFromEnv()\n}\n\nfunction notConfigured(logger: MailooLogger): Response {\n logger.error(\n 'MAILOO_BLOG_PROJECT_UID, MAILOO_BLOG_INTEGRATION_ID, MAILOO_BLOG_API and MAILOO_BLOG_API_KEY must be set'\n )\n return jsonError({ error: 'Blog API not configured' }, 503)\n}\n\nfunction imageProxyPrefix(\n options?: CreateBlogRoutesOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction localeSearchParams(\n request: NextRequest,\n options: CreateBlogRoutesOptions | undefined,\n mode: 'all' | 'locale-only'\n): URLSearchParams {\n const source = request.nextUrl.searchParams\n const params =\n mode === 'all' ? new URLSearchParams(source) : new URLSearchParams()\n if (mode === 'locale-only') {\n const fromQuery = source.get('locale')\n if (fromQuery) params.set('locale', fromQuery)\n }\n if (!params.get('locale')) {\n const resolved = options?.resolveLocale?.(request)\n const locale = typeof resolved === 'string' ? resolved.trim() : ''\n if (locale) params.set('locale', locale)\n }\n return params\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction rewriteProxiedBody(\n kind: BlogGetKind,\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n switch (kind) {\n case 'list':\n return rewriteBlogListEnvelope(body, proxyPrefix)\n case 'post':\n return rewriteBlogPostEnvelope(body, proxyPrefix)\n case 'category-description':\n return rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix)\n case 'json-ld':\n return rewriteBlogJsonLdEnvelope(body, proxyPrefix)\n case 'passthrough':\n return { ok: true, body }\n }\n}\n\nasync function proxyGet(\n config: MailooIntegrationConfig,\n pathSuffix: string,\n searchParams: URLSearchParams | undefined,\n options: CreateBlogRoutesOptions | undefined,\n kind: BlogGetKind\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const url = new URL(\n `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}${pathSuffix}`\n )\n if (searchParams) {\n searchParams.forEach((value, key) => {\n url.searchParams.append(key, value)\n })\n }\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n })\n const data: unknown = await response.json()\n const prefix = imageProxyPrefix(options)\n if (!response.ok || !prefix || kind === 'passthrough') {\n return jsonResponse(data, response.status)\n }\n const rewritten = rewriteProxiedBody(kind, data, prefix)\n if (!rewritten.ok) {\n if (rewritten.reason === 'missing-embed-base') {\n logger.error(EMBED_BASE_ERROR)\n return jsonError({ error: EMBED_BASE_ERROR }, 502)\n }\n logger.error('Blog API JSON-LD rewrite failed')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n return jsonResponse(rewritten.body, response.status)\n}\n\n/** Factory: GET list posts — mount at `app/api/v1/blog/route.ts` */\nexport function createBlogListHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '',\n localeSearchParams(request, options, 'all'),\n options,\n 'list'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET post by slug — mount at `app/api/v1/blog/slug/[slug]/route.ts` */\nexport function createBlogSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'post'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET categories */\nexport function createBlogCategoriesHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '/categories',\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog categories API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tags */\nexport function createBlogTagsHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '/tags',\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tags API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tag by slug */\nexport function createBlogTagSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tag slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET category description by slug */\nexport function createBlogCategoryDescriptionHandler(\n options?: CreateBlogRoutesOptions\n) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'category-description'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog category description API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET JSON-LD by slug — mount at `app/api/v1/blog/slug/[slug]/json-ld/route.ts` */\nexport function createBlogJsonLdHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'json-ld'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog JSON-LD API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\nasync function proxyMarkPost(\n options: CreateBlogRoutesOptions | undefined,\n request: NextRequest,\n slug: string,\n pathKind: 'reaction' | 'feedback',\n logLabel: string\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonError({ error: 'Invalid JSON body' }, 400)\n }\n const url = `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}/slug/${encodeURIComponent(slug)}/${pathKind}`\n try {\n return await proxyMailooWebhookJsonPost({\n request,\n url,\n apiKey: config.apiKey,\n body,\n })\n } catch (error) {\n logger.logException?.(error, logLabel)\n return jsonError({ error: 'Internal server error' }, 500)\n }\n}\n\n/** Factory: POST reaction — mount at `app/api/v1/blog/slug/[slug]/reaction/route.ts` */\nexport function createBlogReactionHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'reaction',\n 'Blog reaction proxy error'\n )\n }\n}\n\n/** Factory: POST feedback — mount at `app/api/v1/blog/slug/[slug]/feedback/route.ts` */\nexport function createBlogFeedbackHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'feedback',\n 'Blog feedback proxy error'\n )\n }\n}\n\nexport function createMailooBlogRoutes(options?: CreateBlogRoutesOptions) {\n return {\n list: createBlogListHandler(options),\n slug: createBlogSlugHandler(options),\n jsonLd: createBlogJsonLdHandler(options),\n categories: createBlogCategoriesHandler(options),\n tags: createBlogTagsHandler(options),\n tagSlug: createBlogTagSlugHandler(options),\n categoryDescription: createBlogCategoryDescriptionHandler(options),\n reaction: createBlogReactionHandler(options),\n feedback: createBlogFeedbackHandler(options),\n }\n}\n","import { unstable_rethrow } from 'next/navigation'\nimport type { MailooIntegrationConfig } from '@mailoo/next-core'\nimport { readMailooIntegrationConfig } from '@mailoo/next-core'\nimport {\n rewriteBlogPostMedia,\n rewriteCategoryDescriptionMedia,\n rewriteEmbedUrlsAnyHost,\n} from './image-rewrite.js'\nimport type {\n BlogArticleMarkState,\n BlogCategoryApi,\n BlogTagApi,\n BlogListPagination,\n BlogListQuery,\n BlogPostApiData,\n MailooBlogFetchOptions,\n SetBlogFeedbackInput,\n SetBlogReactionInput,\n} from '../types.js'\n\nexport type MailooBlogConfig = MailooIntegrationConfig\nexport type { MailooBlogFetchOptions }\n\n/** Host image proxy. Omit to keep canonical Mailoo embed URLs. */\nexport type CreateMailooBlogClientOptions = {\n /**\n * Integrator prefix ending with `?id=` (absolute or same-origin path).\n * Example: `/api/v1/images/public/content?id=`\n */\n imageProxyPrefixWithIdEquals?: string\n}\n\nconst DEFAULT_REVALIDATE = 60\n\n/** Read MAILOO_BLOG_* env (API, API_KEY, PROJECT_UID, INTEGRATION_ID). */\nexport function getMailooBlogConfigFromEnv(): MailooBlogConfig | null {\n return readMailooIntegrationConfig({ prefix: 'MAILOO_BLOG' })\n}\n\nfunction blogBasePath(config: MailooBlogConfig): string {\n return `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}`\n}\n\nfunction resolveRevalidate(options?: MailooBlogFetchOptions): number {\n return options?.revalidate ?? DEFAULT_REVALIDATE\n}\n\nasync function blogFetchJson(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; json: unknown; status: number } | { ok: false; status: number }\n> {\n const url = new URL(`${blogBasePath(config)}${path}`)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== '') url.searchParams.set(k, v)\n }\n }\n try {\n const revalidate = resolveRevalidate(options)\n const res = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n next: { revalidate },\n } as RequestInit)\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n return { ok: true, json: await res.json(), status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogPostJson(\n config: MailooBlogConfig,\n path: string,\n body: unknown\n): Promise<\n | { ok: true; data: BlogArticleMarkState; status: number }\n | { ok: false; status: number }\n> {\n try {\n const res = await fetch(`${blogBasePath(config)}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n body: JSON.stringify(body),\n cache: 'no-store',\n })\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n const json = (await res.json()) as { data?: BlogArticleMarkState }\n if (!json.data || typeof json.data !== 'object') {\n return { ok: false, status: 500 }\n }\n return { ok: true, data: json.data, status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogGet<T>(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; data: T; status: number } | { ok: false; status: number }\n> {\n const fetched = await blogFetchJson(config, path, query, options)\n if (!fetched.ok) return fetched\n const json = fetched.json as { data?: T } & T\n const data =\n (json as { data?: T }).data !== undefined\n ? (json as { data: T }).data\n : (json as T)\n return { ok: true, data, status: fetched.status }\n}\n\nfunction isBlogListPagination(value: unknown): value is BlogListPagination {\n if (value === null || typeof value !== 'object') return false\n const p = value as Record<string, unknown>\n return (\n typeof p.page === 'number' &&\n typeof p.limit === 'number' &&\n typeof p.total === 'number' &&\n typeof p.totalPages === 'number' &&\n typeof p.hasNextPage === 'boolean' &&\n typeof p.hasPrevPage === 'boolean'\n )\n}\n\nfunction queryRecord(query: BlogListQuery): Record<string, string | undefined> {\n const q: Record<string, string | undefined> = {}\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue\n q[k] = String(v)\n }\n return q\n}\n\nfunction imageProxyPrefix(\n options?: CreateMailooBlogClientOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction rewritePost(\n post: BlogPostApiData,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): BlogPostApiData {\n return rewriteBlogPostMedia(\n post,\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals\n )\n}\n\ntype CategoryDescription = {\n description?: string\n descriptionHtml?: string\n imagePublicEmbedBaseUrl?: string\n}\n\nexport function createMailooBlogClient(\n config: MailooBlogConfig,\n clientOptions?: CreateMailooBlogClientOptions\n) {\n const proxyPrefix = imageProxyPrefix(clientOptions)\n return {\n async listPosts(\n query: BlogListQuery = {},\n options?: MailooBlogFetchOptions\n ): Promise<\n | {\n ok: true\n data: BlogPostApiData[]\n pagination: BlogListPagination\n imagePublicEmbedBaseUrl: string\n status: number\n }\n | { ok: false; status: number }\n > {\n const fetched = await blogFetchJson(\n config,\n '',\n queryRecord(query),\n options\n )\n if (!fetched.ok) return fetched\n const json = fetched.json\n if (json === null || typeof json !== 'object') {\n return { ok: false, status: 500 }\n }\n const body = json as {\n data?: unknown\n pagination?: unknown\n imagePublicEmbedBaseUrl?: unknown\n }\n const imagePublicEmbedBaseUrl =\n typeof body.imagePublicEmbedBaseUrl === 'string'\n ? body.imagePublicEmbedBaseUrl.trim()\n : ''\n if (\n !Array.isArray(body.data) ||\n !isBlogListPagination(body.pagination) ||\n !imagePublicEmbedBaseUrl\n ) {\n return { ok: false, status: 500 }\n }\n const posts = body.data as BlogPostApiData[]\n return {\n ok: true,\n data: proxyPrefix\n ? posts.map((post) =>\n rewritePost(post, imagePublicEmbedBaseUrl, proxyPrefix)\n )\n : posts,\n pagination: body.pagination,\n imagePublicEmbedBaseUrl,\n status: fetched.status,\n }\n },\n\n async getPostBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<BlogPostApiData>(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewritePost(result.data, canonical, proxyPrefix),\n }\n },\n\n async getPostJsonLd(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<{ jsonLd: Record<string, unknown> }>(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n try {\n return {\n ...result,\n data: {\n jsonLd: rewriteEmbedUrlsAnyHost(result.data.jsonLd, proxyPrefix),\n },\n }\n } catch {\n return { ok: false as const, status: 500 }\n }\n },\n\n async listCategories(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogCategoryApi[]>(\n config,\n '/categories',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async listTags(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogTagApi[]>(\n config,\n '/tags',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getTagBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n return blogGet<BlogTagApi>(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getCategoryDescription(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<CategoryDescription>(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewriteCategoryDescriptionMedia(\n result.data,\n canonical,\n proxyPrefix\n ),\n }\n },\n\n async setReaction(slug: string, input: SetBlogReactionInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/reaction`,\n input\n )\n },\n\n async setFeedback(slug: string, input: SetBlogFeedbackInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/feedback`,\n input\n )\n },\n }\n}\n\nexport type MailooBlogClient = ReturnType<typeof createMailooBlogClient>\n","import {\n rewriteMailooBlogMediaUrls,\n rewriteMailooPublicImageUrls,\n rewriteMailooPublicImageUrlsAnyHost,\n} from '@mailoo/images'\nimport type { BlogPostApiData } from '../types.js'\n\nexport type BlogImageRewriteFailure = 'missing-embed-base' | 'invalid-json'\n\nexport type BlogImageRewriteResult =\n { ok: true; body: unknown } | { ok: false; reason: BlogImageRewriteFailure }\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return null\n }\n return value as Record<string, unknown>\n}\n\nexport function readEmbedBase(...candidates: unknown[]): string | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return undefined\n}\n\nexport function rewriteBlogPostMedia<T extends BlogPostApiData>(\n post: T,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): T {\n return rewriteMailooBlogMediaUrls(post, {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n })\n}\n\nexport function rewriteCategoryDescriptionMedia<\n T extends {\n description?: string | null\n descriptionHtml?: string | null\n },\n>(\n data: T,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): T {\n const opts = {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n }\n return {\n ...data,\n description:\n typeof data.description === 'string'\n ? rewriteMailooPublicImageUrls(data.description, opts)\n : data.description,\n descriptionHtml:\n typeof data.descriptionHtml === 'string'\n ? rewriteMailooPublicImageUrls(data.descriptionHtml, opts)\n : data.descriptionHtml,\n }\n}\n\nexport function rewriteEmbedUrlsAnyHost<T>(value: T, proxyPrefix: string): T {\n const rewritten = rewriteMailooPublicImageUrlsAnyHost(\n JSON.stringify(value),\n proxyPrefix\n )\n return JSON.parse(rewritten) as T\n}\n\nfunction rewritePostRecord(\n post: Record<string, unknown>,\n canonical: string,\n proxy: string\n): Record<string, unknown> {\n return rewriteBlogPostMedia(\n post as unknown as BlogPostApiData,\n canonical,\n proxy\n ) as unknown as Record<string, unknown>\n}\n\n/** List envelope: one `imagePublicEmbedBaseUrl` rewrites every post. */\nexport function rewriteBlogListEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const canonical = readEmbedBase(record?.imagePublicEmbedBaseUrl)\n if (!record || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n if (!Array.isArray(record.data)) {\n return { ok: true, body }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: record.data.map((item) => {\n const post = asRecord(item)\n if (!post) return item\n return rewritePostRecord(post, canonical, proxyPrefix)\n }),\n },\n }\n}\n\n/** Slug envelope: embed base on the article, or on the response root. */\nexport function rewriteBlogPostEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const data = asRecord(record?.data)\n const canonical = readEmbedBase(\n data?.imagePublicEmbedBaseUrl,\n record?.imagePublicEmbedBaseUrl\n )\n if (!record || !data || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: rewritePostRecord(data, canonical, proxyPrefix),\n },\n }\n}\n\n/** Category description envelope: embed base on `data` or the response root. */\nexport function rewriteBlogCategoryDescriptionEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const data = asRecord(record?.data)\n const canonical = readEmbedBase(\n data?.imagePublicEmbedBaseUrl,\n record?.imagePublicEmbedBaseUrl\n )\n if (!record || !data || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: rewriteCategoryDescriptionMedia(\n data as {\n description?: string | null\n descriptionHtml?: string | null\n },\n canonical,\n proxyPrefix\n ),\n },\n }\n}\n\nexport function rewriteBlogJsonLdEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n try {\n return {\n ok: true,\n body: rewriteEmbedUrlsAnyHost(body, proxyPrefix),\n }\n } catch {\n return { ok: false, reason: 'invalid-json' }\n }\n}\n"],"mappings":"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","names":["import_next_core"]}
@@ -5,6 +5,17 @@ type CreateBlogRoutesOptions = {
5
5
  /** Explicit config; defaults to MAILOO_BLOG_* env */
6
6
  getConfig?: () => MailooIntegrationConfig | null;
7
7
  logger?: MailooLogger;
8
+ /**
9
+ * Integrator prefix ending with `?id=`. When set, list, slug, category
10
+ * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.
11
+ * Example: `/api/v1/images/public/content?id=`
12
+ */
13
+ imageProxyPrefixWithIdEquals?: string;
14
+ /**
15
+ * Used only when the query has no `locale`. Host maps Accept-Language
16
+ * to its own locale list.
17
+ */
18
+ resolveLocale?: (request: NextRequest) => string | undefined;
8
19
  };
9
20
  /** Factory: GET list posts — mount at `app/api/v1/blog/route.ts` */
10
21
  declare function createBlogListHandler(options?: CreateBlogRoutesOptions): (request: NextRequest) => Promise<Response>;
@@ -5,6 +5,17 @@ type CreateBlogRoutesOptions = {
5
5
  /** Explicit config; defaults to MAILOO_BLOG_* env */
6
6
  getConfig?: () => MailooIntegrationConfig | null;
7
7
  logger?: MailooLogger;
8
+ /**
9
+ * Integrator prefix ending with `?id=`. When set, list, slug, category
10
+ * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.
11
+ * Example: `/api/v1/images/public/content?id=`
12
+ */
13
+ imageProxyPrefixWithIdEquals?: string;
14
+ /**
15
+ * Used only when the query has no `locale`. Host maps Accept-Language
16
+ * to its own locale list.
17
+ */
18
+ resolveLocale?: (request: NextRequest) => string | undefined;
8
19
  };
9
20
  /** Factory: GET list posts — mount at `app/api/v1/blog/route.ts` */
10
21
  declare function createBlogListHandler(options?: CreateBlogRoutesOptions): (request: NextRequest) => Promise<Response>;
@@ -8,16 +8,137 @@ import {
8
8
  // src/server/client.ts
9
9
  import { unstable_rethrow } from "next/navigation";
10
10
  import { readMailooIntegrationConfig } from "@mailoo/next-core";
11
+
12
+ // src/server/image-rewrite.ts
11
13
  import {
12
14
  rewriteMailooBlogMediaUrls,
13
15
  rewriteMailooPublicImageUrls,
14
16
  rewriteMailooPublicImageUrlsAnyHost
15
17
  } from "@mailoo/images";
18
+ function asRecord(value) {
19
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
20
+ return null;
21
+ }
22
+ return value;
23
+ }
24
+ function readEmbedBase(...candidates) {
25
+ for (const candidate of candidates) {
26
+ if (typeof candidate === "string" && candidate.trim()) {
27
+ return candidate.trim();
28
+ }
29
+ }
30
+ return void 0;
31
+ }
32
+ function rewriteBlogPostMedia(post, canonicalPrefixWithIdEquals, proxyPrefixWithIdEquals) {
33
+ return rewriteMailooBlogMediaUrls(post, {
34
+ canonicalPrefixWithIdEquals,
35
+ proxyPrefixWithIdEquals
36
+ });
37
+ }
38
+ function rewriteCategoryDescriptionMedia(data, canonicalPrefixWithIdEquals, proxyPrefixWithIdEquals) {
39
+ const opts = {
40
+ canonicalPrefixWithIdEquals,
41
+ proxyPrefixWithIdEquals
42
+ };
43
+ return {
44
+ ...data,
45
+ description: typeof data.description === "string" ? rewriteMailooPublicImageUrls(data.description, opts) : data.description,
46
+ descriptionHtml: typeof data.descriptionHtml === "string" ? rewriteMailooPublicImageUrls(data.descriptionHtml, opts) : data.descriptionHtml
47
+ };
48
+ }
49
+ function rewriteEmbedUrlsAnyHost(value, proxyPrefix) {
50
+ const rewritten = rewriteMailooPublicImageUrlsAnyHost(
51
+ JSON.stringify(value),
52
+ proxyPrefix
53
+ );
54
+ return JSON.parse(rewritten);
55
+ }
56
+ function rewritePostRecord(post, canonical, proxy) {
57
+ return rewriteBlogPostMedia(
58
+ post,
59
+ canonical,
60
+ proxy
61
+ );
62
+ }
63
+ function rewriteBlogListEnvelope(body, proxyPrefix) {
64
+ const record = asRecord(body);
65
+ const canonical = readEmbedBase(record?.imagePublicEmbedBaseUrl);
66
+ if (!record || !canonical) {
67
+ return { ok: false, reason: "missing-embed-base" };
68
+ }
69
+ if (!Array.isArray(record.data)) {
70
+ return { ok: true, body };
71
+ }
72
+ return {
73
+ ok: true,
74
+ body: {
75
+ ...record,
76
+ data: record.data.map((item) => {
77
+ const post = asRecord(item);
78
+ if (!post) return item;
79
+ return rewritePostRecord(post, canonical, proxyPrefix);
80
+ })
81
+ }
82
+ };
83
+ }
84
+ function rewriteBlogPostEnvelope(body, proxyPrefix) {
85
+ const record = asRecord(body);
86
+ const data = asRecord(record?.data);
87
+ const canonical = readEmbedBase(
88
+ data?.imagePublicEmbedBaseUrl,
89
+ record?.imagePublicEmbedBaseUrl
90
+ );
91
+ if (!record || !data || !canonical) {
92
+ return { ok: false, reason: "missing-embed-base" };
93
+ }
94
+ return {
95
+ ok: true,
96
+ body: {
97
+ ...record,
98
+ data: rewritePostRecord(data, canonical, proxyPrefix)
99
+ }
100
+ };
101
+ }
102
+ function rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix) {
103
+ const record = asRecord(body);
104
+ const data = asRecord(record?.data);
105
+ const canonical = readEmbedBase(
106
+ data?.imagePublicEmbedBaseUrl,
107
+ record?.imagePublicEmbedBaseUrl
108
+ );
109
+ if (!record || !data || !canonical) {
110
+ return { ok: false, reason: "missing-embed-base" };
111
+ }
112
+ return {
113
+ ok: true,
114
+ body: {
115
+ ...record,
116
+ data: rewriteCategoryDescriptionMedia(
117
+ data,
118
+ canonical,
119
+ proxyPrefix
120
+ )
121
+ }
122
+ };
123
+ }
124
+ function rewriteBlogJsonLdEnvelope(body, proxyPrefix) {
125
+ try {
126
+ return {
127
+ ok: true,
128
+ body: rewriteEmbedUrlsAnyHost(body, proxyPrefix)
129
+ };
130
+ } catch {
131
+ return { ok: false, reason: "invalid-json" };
132
+ }
133
+ }
134
+
135
+ // src/server/client.ts
16
136
  function getMailooBlogConfigFromEnv() {
17
137
  return readMailooIntegrationConfig({ prefix: "MAILOO_BLOG" });
18
138
  }
19
139
 
20
140
  // src/routes/index.ts
141
+ var EMBED_BASE_ERROR = "Blog API contract violation: imagePublicEmbedBaseUrl missing";
21
142
  function resolveConfig(options) {
22
143
  return options?.getConfig?.() ?? getMailooBlogConfigFromEnv();
23
144
  }
@@ -27,7 +148,46 @@ function notConfigured(logger) {
27
148
  );
28
149
  return jsonError({ error: "Blog API not configured" }, 503);
29
150
  }
30
- async function proxyGet(config, pathSuffix, searchParams) {
151
+ function imageProxyPrefix(options) {
152
+ const raw = options?.imageProxyPrefixWithIdEquals?.trim();
153
+ return raw || void 0;
154
+ }
155
+ function localeSearchParams(request, options, mode) {
156
+ const source = request.nextUrl.searchParams;
157
+ const params = mode === "all" ? new URLSearchParams(source) : new URLSearchParams();
158
+ if (mode === "locale-only") {
159
+ const fromQuery = source.get("locale");
160
+ if (fromQuery) params.set("locale", fromQuery);
161
+ }
162
+ if (!params.get("locale")) {
163
+ const resolved = options?.resolveLocale?.(request);
164
+ const locale = typeof resolved === "string" ? resolved.trim() : "";
165
+ if (locale) params.set("locale", locale);
166
+ }
167
+ return params;
168
+ }
169
+ function jsonResponse(body, status) {
170
+ return new Response(JSON.stringify(body), {
171
+ status,
172
+ headers: { "Content-Type": "application/json" }
173
+ });
174
+ }
175
+ function rewriteProxiedBody(kind, body, proxyPrefix) {
176
+ switch (kind) {
177
+ case "list":
178
+ return rewriteBlogListEnvelope(body, proxyPrefix);
179
+ case "post":
180
+ return rewriteBlogPostEnvelope(body, proxyPrefix);
181
+ case "category-description":
182
+ return rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix);
183
+ case "json-ld":
184
+ return rewriteBlogJsonLdEnvelope(body, proxyPrefix);
185
+ case "passthrough":
186
+ return { ok: true, body };
187
+ }
188
+ }
189
+ async function proxyGet(config, pathSuffix, searchParams, options, kind) {
190
+ const logger = options?.logger ?? defaultMailooLogger;
31
191
  const url = new URL(
32
192
  `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}${pathSuffix}`
33
193
  );
@@ -44,10 +204,20 @@ async function proxyGet(config, pathSuffix, searchParams) {
44
204
  }
45
205
  });
46
206
  const data = await response.json();
47
- return new Response(JSON.stringify(data), {
48
- status: response.status,
49
- headers: { "Content-Type": "application/json" }
50
- });
207
+ const prefix = imageProxyPrefix(options);
208
+ if (!response.ok || !prefix || kind === "passthrough") {
209
+ return jsonResponse(data, response.status);
210
+ }
211
+ const rewritten = rewriteProxiedBody(kind, data, prefix);
212
+ if (!rewritten.ok) {
213
+ if (rewritten.reason === "missing-embed-base") {
214
+ logger.error(EMBED_BASE_ERROR);
215
+ return jsonError({ error: EMBED_BASE_ERROR }, 502);
216
+ }
217
+ logger.error("Blog API JSON-LD rewrite failed");
218
+ return jsonError({ error: "Internal server error" }, 500);
219
+ }
220
+ return jsonResponse(rewritten.body, response.status);
51
221
  }
52
222
  function createBlogListHandler(options) {
53
223
  const logger = options?.logger ?? defaultMailooLogger;
@@ -55,7 +225,13 @@ function createBlogListHandler(options) {
55
225
  const config = resolveConfig(options);
56
226
  if (!config) return notConfigured(logger);
57
227
  try {
58
- return await proxyGet(config, "", request.nextUrl.searchParams);
228
+ return await proxyGet(
229
+ config,
230
+ "",
231
+ localeSearchParams(request, options, "all"),
232
+ options,
233
+ "list"
234
+ );
59
235
  } catch (error) {
60
236
  logger.logException?.(error, "Blog API proxy error");
61
237
  return jsonError({ error: "Internal server error" }, 500);
@@ -69,10 +245,13 @@ function createBlogSlugHandler(options) {
69
245
  if (!config) return notConfigured(logger);
70
246
  const { slug } = await ctx.params;
71
247
  try {
72
- const locale = request.nextUrl.searchParams.get("locale");
73
- const qs = new URLSearchParams();
74
- if (locale) qs.set("locale", locale);
75
- return await proxyGet(config, `/slug/${encodeURIComponent(slug)}`, qs);
248
+ return await proxyGet(
249
+ config,
250
+ `/slug/${encodeURIComponent(slug)}`,
251
+ localeSearchParams(request, options, "locale-only"),
252
+ options,
253
+ "post"
254
+ );
76
255
  } catch (error) {
77
256
  logger.logException?.(error, "Blog slug API proxy error");
78
257
  return jsonError({ error: "Internal server error" }, 500);
@@ -85,7 +264,13 @@ function createBlogCategoriesHandler(options) {
85
264
  const config = resolveConfig(options);
86
265
  if (!config) return notConfigured(logger);
87
266
  try {
88
- return await proxyGet(config, "/categories", request.nextUrl.searchParams);
267
+ return await proxyGet(
268
+ config,
269
+ "/categories",
270
+ localeSearchParams(request, options, "all"),
271
+ options,
272
+ "passthrough"
273
+ );
89
274
  } catch (error) {
90
275
  logger.logException?.(error, "Blog categories API proxy error");
91
276
  return jsonError({ error: "Internal server error" }, 500);
@@ -98,7 +283,13 @@ function createBlogTagsHandler(options) {
98
283
  const config = resolveConfig(options);
99
284
  if (!config) return notConfigured(logger);
100
285
  try {
101
- return await proxyGet(config, "/tags", request.nextUrl.searchParams);
286
+ return await proxyGet(
287
+ config,
288
+ "/tags",
289
+ localeSearchParams(request, options, "all"),
290
+ options,
291
+ "passthrough"
292
+ );
102
293
  } catch (error) {
103
294
  logger.logException?.(error, "Blog tags API proxy error");
104
295
  return jsonError({ error: "Internal server error" }, 500);
@@ -115,7 +306,9 @@ function createBlogTagSlugHandler(options) {
115
306
  return await proxyGet(
116
307
  config,
117
308
  `/tags/slug/${encodeURIComponent(slug)}`,
118
- request.nextUrl.searchParams
309
+ localeSearchParams(request, options, "all"),
310
+ options,
311
+ "passthrough"
119
312
  );
120
313
  } catch (error) {
121
314
  logger.logException?.(error, "Blog tag slug API proxy error");
@@ -130,13 +323,12 @@ function createBlogCategoryDescriptionHandler(options) {
130
323
  if (!config) return notConfigured(logger);
131
324
  const { slug } = await ctx.params;
132
325
  try {
133
- const locale = request.nextUrl.searchParams.get("locale");
134
- const qs = new URLSearchParams();
135
- if (locale) qs.set("locale", locale);
136
326
  return await proxyGet(
137
327
  config,
138
328
  `/categories/slug/${encodeURIComponent(slug)}/description`,
139
- qs
329
+ localeSearchParams(request, options, "locale-only"),
330
+ options,
331
+ "category-description"
140
332
  );
141
333
  } catch (error) {
142
334
  logger.logException?.(error, "Blog category description API proxy error");
@@ -151,13 +343,12 @@ function createBlogJsonLdHandler(options) {
151
343
  if (!config) return notConfigured(logger);
152
344
  const { slug } = await ctx.params;
153
345
  try {
154
- const locale = request.nextUrl.searchParams.get("locale");
155
- const qs = new URLSearchParams();
156
- if (locale) qs.set("locale", locale);
157
346
  return await proxyGet(
158
347
  config,
159
348
  `/slug/${encodeURIComponent(slug)}/json-ld`,
160
- qs
349
+ localeSearchParams(request, options, "locale-only"),
350
+ options,
351
+ "json-ld"
161
352
  );
162
353
  } catch (error) {
163
354
  logger.logException?.(error, "Blog JSON-LD API proxy error");