diff --git a/apps/romainlanz.com/.adonisjs/api.ts b/apps/romainlanz.com/.adonisjs/api.ts index cf6a245..315aa3f 100644 --- a/apps/romainlanz.com/.adonisjs/api.ts +++ b/apps/romainlanz.com/.adonisjs/api.ts @@ -62,6 +62,10 @@ type AdminTaxonomiesCategoriesPost = { request: unknown response: MakeTuyauResponse } +type OgComputeGetHead = { + request: unknown + response: MakeTuyauResponse +} type ArticlesGetHead = { request: unknown response: MakeTuyauResponse @@ -166,6 +170,14 @@ export interface ApiDefinition { }; }; }; + 'og': { + 'compute': { + '$url': { + }; + '$get': OgComputeGetHead; + '$head': OgComputeGetHead; + }; + }; 'articles': { '$url': { }; @@ -309,6 +321,13 @@ const routes = [ method: ["POST"], types: {} as AdminTaxonomiesCategoriesPost, }, + { + params: [], + name: 'og.compute', + path: '/og/compute', + method: ["GET","HEAD"], + types: {} as OgComputeGetHead, + }, { params: [], name: 'pages.landing', diff --git a/apps/romainlanz.com/app/articles/repositories/article_repository.ts b/apps/romainlanz.com/app/articles/repositories/article_repository.ts index c3c3330..93fbffe 100644 --- a/apps/romainlanz.com/app/articles/repositories/article_repository.ts +++ b/apps/romainlanz.com/app/articles/repositories/article_repository.ts @@ -162,6 +162,7 @@ export class ArticleRepository { 'articles.id', 'articles.title', 'articles.slug', + 'articles.summary', 'articles.content_html', 'articles.published_at', 'categories.id as category_id', @@ -181,7 +182,7 @@ export class ArticleRepository { title: articleRecord.title, slug: articleRecord.slug, content: articleRecord.content_html, - summary: null, + summary: articleRecord.summary, publishedAt: DateTime.fromJSDate(articleRecord.published_at!), category: Category.create({ id: CategoryIdentifier.fromString(articleRecord.category_id!), diff --git a/apps/romainlanz.com/app/common/controllers/compute_og_image_controllers.ts b/apps/romainlanz.com/app/common/controllers/compute_og_image_controllers.ts new file mode 100644 index 0000000..9bd729c --- /dev/null +++ b/apps/romainlanz.com/app/common/controllers/compute_og_image_controllers.ts @@ -0,0 +1,24 @@ +import { inject } from '@adonisjs/core'; +import vine from '@vinejs/vine'; +import { OgImageGeneratorService } from '#common/services/og_image_generator_service'; +import type { HttpContext } from '@adonisjs/core/http'; + +@inject() +export default class ComputeOgImageControllers { + static validator = vine.compile( + vine.object({ + title: vine.string().trim(), + subtitle: vine.string().trim(), + }) + ); + + constructor(private readonly ogImageGeneratorService: OgImageGeneratorService) {} + + async execute({ request, response }: HttpContext) { + const { title, subtitle } = await request.validateUsing(ComputeOgImageControllers.validator); + + const image = await this.ogImageGeneratorService.generate(title, subtitle); + + return response.header('Content-Type', 'image/png').stream(image); + } +} diff --git a/apps/romainlanz.com/app/common/services/og_image_generator_service.ts b/apps/romainlanz.com/app/common/services/og_image_generator_service.ts new file mode 100644 index 0000000..373bdc8 --- /dev/null +++ b/apps/romainlanz.com/app/common/services/og_image_generator_service.ts @@ -0,0 +1,43 @@ +import { readFile } from 'node:fs/promises'; +import { assertExists } from '@adonisjs/core/helpers/assert'; +import app from '@adonisjs/core/services/app'; +import sharp, { type Sharp } from 'sharp'; + +export class OgImageGeneratorService { + static #template: string | null = null; + + async #loadTemplate() { + OgImageGeneratorService.#template = await readFile(app.makePath('resources/og_template.svg'), 'utf-8'); + } + + async generate(title: string, subtitle: string): Promise { + if (!OgImageGeneratorService.#template) { + await this.#loadTemplate(); + } + + assertExists(OgImageGeneratorService.#template, 'Template should be loaded'); + + console.log(title); + + const [title1, title2] = title + .trim() + .split(/(.{0,20})(?:\s|$)/g) + .filter(Boolean); + + const [subtitle1, subtitle2, subtitle3] = subtitle + .trim() + .split(/(.{0,60})(?:\s|$)/g) + .filter(Boolean); + + const generatedSvg = OgImageGeneratorService.#template + .replace('{{ title }}', title1) + .replace('{{ title2 }}', title2 || '') + .replace('{{ subtitle }}', subtitle1) + .replace('{{ subtitle2 }}', subtitle2 || '') + .replace('{{ subtitle3 }}', subtitle3 || ''); + + return sharp(Buffer.from(generatedSvg)) + .resize(1200 * 1.1, 630 * 1.1) + .png(); + } +} diff --git a/apps/romainlanz.com/inertia/pages/articles/show.vue b/apps/romainlanz.com/inertia/pages/articles/show.vue index 69d66fc..ad8df6d 100644 --- a/apps/romainlanz.com/inertia/pages/articles/show.vue +++ b/apps/romainlanz.com/inertia/pages/articles/show.vue @@ -1,15 +1,23 @@