Skip to content

Commit

Permalink
feat(articles): generate og image
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainLanz committed Dec 15, 2024
1 parent d10866d commit e500fe6
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 4 deletions.
19 changes: 19 additions & 0 deletions apps/romainlanz.com/.adonisjs/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ type AdminTaxonomiesCategoriesPost = {
request: unknown
response: MakeTuyauResponse<import('../app/admin/taxonomies/controllers/store_category_controller.ts').default['execute'], false>
}
type OgComputeGetHead = {
request: unknown
response: MakeTuyauResponse<import('../app/common/controllers/compute_og_image_controllers.ts').default['execute'], false>
}
type ArticlesGetHead = {
request: unknown
response: MakeTuyauResponse<import('../app/articles/controllers/list_articles_controller.ts').default['render'], false>
Expand Down Expand Up @@ -166,6 +170,14 @@ export interface ApiDefinition {
};
};
};
'og': {
'compute': {
'$url': {
};
'$get': OgComputeGetHead;
'$head': OgComputeGetHead;
};
};
'articles': {
'$url': {
};
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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!),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Sharp> {
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();
}
}
12 changes: 10 additions & 2 deletions apps/romainlanz.com/inertia/pages/articles/show.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<script lang="ts" setup>
import { Head } from '@inertiajs/vue3';
import Headline from '@rlanz/design-system/headline';
import { client } from '@rlanz/rpc/client';
import { computed } from 'vue';
import type { ArticleViewModelSerialized } from '#articles/view_models/article_view_model';
defineProps<{
const { vm } = defineProps<{
vm: ArticleViewModelSerialized;
}>();
const ogUrl = computed(() => {
return client.$url('og.compute', { query: { title: vm.article.title, subtitle: vm.article.summary } });
});
</script>

<template>
<Head :title="vm.article.title" />
<Head :title="vm.article.title">
<meta name="og:url" :content="ogUrl" />
</Head>

<div class="mx-auto max-w-7xl px-4">
<article :class="$style.article">
Expand Down
1 change: 1 addition & 0 deletions apps/romainlanz.com/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"sharp": "^0.33.5",
"shiki": "^1.24.2",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
Expand Down
55 changes: 55 additions & 0 deletions apps/romainlanz.com/resources/og_template.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/romainlanz.com/start/routes/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { middleware } from '#start/kernel';

// region Controller's Imports
const AboutController = () => import('#pages/controllers/about_controller');
const ComputeOgImageControllers = () => import('#common/controllers/compute_og_image_controllers');
// const ConfirmEmailController = () => import('#newsletter/controllers/confirm_email_controller');
const ContactController = () => import('#pages/controllers/contact_controller');
const LandingController = () => import('#pages/controllers/landing_controller');
Expand All @@ -13,6 +14,8 @@ const LogoutController = () => import('#auth/controllers/logout_controller');
const ShowArticleController = () => import('#articles/controllers/show_article_controller');
// endregion

router.get('og/compute', [ComputeOgImageControllers, 'execute']).as('og.compute');

router.get('/', [LandingController, 'render']).as('pages.landing');
router.get('articles', [ListArticlesController, 'render']).as('articles.index');
router.get('articles/:slug', [ShowArticleController, 'render']).as('articles.show');
Expand Down
Loading

0 comments on commit e500fe6

Please sign in to comment.