Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 23 additions & 59 deletions .cache/pages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
]
29 changes: 23 additions & 6 deletions src/pages/api/social-card/_lib/assetLoaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -92,6 +88,27 @@ export const fontManager = new FontManager()

const imageCache = new Map<string, Buffer>()

const isHttpUrl = (value: string): boolean => /^https?:\/\//.test(value)

const loadFont = async (fontPath: string): Promise<ArrayBuffer> => {
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<Buffer> => {
const cachedImage = imageCache.get(imageUrl)
if (cachedImage) {
Expand Down
61 changes: 35 additions & 26 deletions src/pages/api/social-card/_lib/generateOpenGraphImage.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -35,8 +33,8 @@ export async function generateOpenGraphImage({
bgGradient = defaultGradient,
avatarUrl,
}: SocialCardImageOptions): Promise<Buffer> {
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)
Expand Down Expand Up @@ -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 })
}
}
Loading