diff --git a/.cache/pages.json b/.cache/pages.json index 2e68fb73..7539e6a7 100644 --- a/.cache/pages.json +++ b/.cache/pages.json @@ -2,6 +2,29 @@ "about", { "articles": [ + "kubernetes-pod-disruption-budget-autoscaler-node-rotation" + ] + }, + { + "case-studies": [ + "cicd-pipeline-overhaul", + "cloud-migration-logistics-platform", + "compliance-automation-hipaa", + "cost-optimization-saas-startup", + "event-driven-architecture-migration", + "incident-response-modernization", + "infrastructure-as-code-transformation", + "internal-developer-platform-backstage", + "kubernetes-adoption-fintech", + "observability-stack-implementation", + "self-service-infrastructure", + "zero-trust-network-architecture" + ] + }, + "consent", + "contact", + { + "deep-dive": [ "alert-fatigue-reduction-triage-actionable-alerts", "api-deprecation-sunset-headers-consumer-migration", "api-gateway-metrics-traces-logs-debugging", @@ -37,7 +60,6 @@ "kubernetes-hpa-autoscaling-metrics-tuning-latency", "kubernetes-ingress-gateway-api-comparison-migration", "kubernetes-multi-cluster-fleet-management-configuration", - "kubernetes-pod-disruption-budget-autoscaler-node-rotation", "kubernetes-pod-resource-requests-limits-qos-classes", "kubernetes-secrets-external-secrets-operator-csi-vault", "legacy-code-testing-characterization-tests-seams", @@ -70,71 +92,13 @@ "workload-identity-federation-keyless-cloud-authentication" ] }, - { - "case-studies": [ - "cicd-pipeline-overhaul", - "cloud-migration-logistics-platform", - "compliance-automation-hipaa", - "cost-optimization-saas-startup", - "event-driven-architecture-migration", - "incident-response-modernization", - "infrastructure-as-code-transformation", - "internal-developer-platform-backstage", - "kubernetes-adoption-fintech", - "observability-stack-implementation", - "self-service-infrastructure", - "zero-trust-network-architecture" - ] - }, - "consent", - "contact", "newsletter", - "offline", { "privacy": [ "my-data" ] }, "resume", - "search", "services", - { - "tags": [ - "apis-and-gateways", - "argo-cd", - "aws", - "aws/2", - "aws/3", - "azure", - "backstage-idp", - "build-and-deploy", - "cloud-platforms", - "cloud-platforms/2", - "crossplane", - "docker", - "dotnet", - "go", - "grafana", - "helm", - "kubernetes", - "kubernetes/2", - "kubernetes/3", - "observability-and-telemetry", - "openstack", - "platform-engineering", - "prometheus", - "prometheus/2", - "python", - "python/2", - "react", - "reliability-and-testing", - "ruby", - "system-modernization", - "systems-and-development", - "terraform", - "typescript", - "typescript/2" - ] - }, "terms" ] \ No newline at end of file diff --git a/src/pages/api/social-card/_lib/assetLoaders.ts b/src/pages/api/social-card/_lib/assetLoaders.ts index bb487130..13fef6d3 100644 --- a/src/pages/api/social-card/_lib/assetLoaders.ts +++ b/src/pages/api/social-card/_lib/assetLoaders.ts @@ -66,16 +66,12 @@ class FontManager { hasNew = true try { - const fontFile = await fs.readFile(fontPath) - const fontData = fontFile.buffer.slice( - fontFile.byteOffset, - fontFile.byteOffset + fontFile.byteLength - ) + const fontData = await loadFont(fontPath) this.#cache.set(fontPath, fontData) } catch (cause) { throw new SocialCardGenerationError( 'load-fonts', - 'Failed to load a local font file for social card generation', + 'Failed to load a font asset for social card generation', { fontPath, }, @@ -92,6 +88,27 @@ export const fontManager = new FontManager() const imageCache = new Map() +const isHttpUrl = (value: string): boolean => /^https?:\/\//.test(value) + +const loadFont = async (fontPath: string): Promise => { + if (isHttpUrl(fontPath)) { + const response = await fetch(fontPath) + + if (!response.ok) { + throw new SocialCardGenerationError('load-fonts', 'Failed to fetch a social card font asset', { + fontPath, + status: response.status, + statusText: response.statusText, + }) + } + + return await response.arrayBuffer() + } + + const fontFile = await fs.readFile(fontPath) + return fontFile.buffer.slice(fontFile.byteOffset, fontFile.byteOffset + fontFile.byteLength) +} + export const loadImage = async (imageUrl: string): Promise => { const cachedImage = imageCache.get(imageUrl) if (cachedImage) { diff --git a/src/pages/api/social-card/_lib/generateOpenGraphImage.ts b/src/pages/api/social-card/_lib/generateOpenGraphImage.ts index 09aac800..e9689a40 100644 --- a/src/pages/api/social-card/_lib/generateOpenGraphImage.ts +++ b/src/pages/api/social-card/_lib/generateOpenGraphImage.ts @@ -1,6 +1,4 @@ import { Buffer } from 'node:buffer' -import { existsSync } from 'node:fs' -import { fileURLToPath } from 'node:url' import { decodeHTMLStrict } from 'entities' import { getCanvasKit, fontManager, loadImage } from './assetLoaders' import { SocialCardGenerationError } from './SocialCardGenerationError' @@ -35,8 +33,8 @@ export async function generateOpenGraphImage({ bgGradient = defaultGradient, avatarUrl, }: SocialCardImageOptions): Promise { - const { titleFontPath, descriptionFontPath } = resolveFontPaths() - const fontMgr = await fontManager.get([titleFontPath, descriptionFontPath]) + const { titleFontUrl, descriptionFontUrl } = resolveFontUrls(avatarUrl) + const fontMgr = await fontManager.get([titleFontUrl, descriptionFontUrl]) const avatarBuffer = await loadImage(avatarUrl) const CanvasKit = await getCanvasKit() const surface = CanvasKit.MakeSurface(width, height) @@ -185,40 +183,51 @@ const buildTextStyle = ( heightMultiplier: style.lineHeight, }) -const resolveFontPaths = (): { descriptionFontPath: string; titleFontPath: string } => ({ - titleFontPath: resolveLocalAssetPath( +const resolveFontUrls = ( + assetBaseUrl: string +): { descriptionFontUrl: string; titleFontUrl: string } => ({ + titleFontUrl: resolvePublicAssetUrl( 'resolve-title-font', - [ - new URL('../../../../../public/fonts/Serif-Lora/Lora-Bold.ttf', import.meta.url), - new URL('../../../../../public/fonts/Lora-Bold.ttf', import.meta.url), - ], + ['/fonts/Serif-Lora/Lora-Bold.ttf'], + assetBaseUrl, 'title font' ), - descriptionFontPath: resolveLocalAssetPath( + descriptionFontUrl: resolvePublicAssetUrl( 'resolve-description-font', - [ - new URL('../../../../../public/fonts/Serif-Lora/Lora-Regular.ttf', import.meta.url), - new URL('../../../../../public/fonts/Lora-Regular.ttf', import.meta.url), - ], + ['/fonts/Serif-Lora/Lora-Regular.ttf'], + assetBaseUrl, 'description font' ), }) -const resolveLocalAssetPath = ( +const resolvePublicAssetUrl = ( stage: 'resolve-description-font' | 'resolve-title-font', - candidateUrls: URL[], + candidatePaths: string[], + assetBaseUrl: string, label: string ): string => { - const candidatePaths = candidateUrls.map(candidateUrl => fileURLToPath(candidateUrl)) + try { + const baseUrl = new URL(assetBaseUrl) + const candidateUrls = candidatePaths.map(candidatePath => new URL(candidatePath, baseUrl).toString()) + const [resolvedUrl] = candidateUrls + + if (!resolvedUrl) { + throw new SocialCardGenerationError(stage, `Social card ${label} could not be resolved`, { + candidatePaths, + label, + }) + } - for (const candidatePath of candidatePaths) { - if (existsSync(candidatePath)) { - return candidatePath + return resolvedUrl + } catch (cause) { + if (cause instanceof SocialCardGenerationError) { + throw cause } - } - throw new SocialCardGenerationError(stage, `Social card ${label} could not be resolved`, { - candidatePaths, - label, - }) + throw new SocialCardGenerationError(stage, `Social card ${label} could not be resolved`, { + candidatePaths, + label, + assetBaseUrl, + }, { cause }) + } }