Skip to content

Commit

Permalink
feat: add dynamic og images
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-R44 committed Jan 20, 2024
1 parent b5da5b4 commit 48d9964
Show file tree
Hide file tree
Showing 11 changed files with 906 additions and 452 deletions.
19 changes: 19 additions & 0 deletions app/controllers/og_images_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'

import { PackagesFetcher } from '#services/packages_fetcher'
import { OgImageGenerator } from '#services/og_image/og_image_renderer'

export default class OgImagesController {
@inject()
async handle(
{ params, response }: HttpContext,
pkgFetcher: PackagesFetcher,
ogImageGenerator: OgImageGenerator,
) {
const { package: pkg } = await pkgFetcher.fetchPackage(params.name)
const img = await ogImageGenerator.generate(pkg.name, pkg.description)

return response.type('image/png').send(img)
}
}
7 changes: 6 additions & 1 deletion app/controllers/packages_controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import router from '@adonisjs/core/services/router'

import { getHomeValidator } from '#validators/main'
import { PackagesFetcher } from '#services/packages_fetcher'
Expand Down Expand Up @@ -27,7 +28,11 @@ export default class PackagesController {
const result = await statsFetcher.fetchPackage(ctx.params.name)

return ctx.inertia.render<GetPackageResponse>('package/main', result, {
meta: { title: result.package.name, description: result.package.description },
meta: {
title: result.package.name,
description: result.package.description,
image: router.builder().params({ name: result.package.name }).make('og_image'),
},
})
}
}
2 changes: 2 additions & 0 deletions app/services/og_image/adonis_logo.ts

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions app/services/og_image/og_image_renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import satori from 'satori'
import { html } from 'satori-html'
import { Resvg } from '@resvg/resvg-js'
import { readFile } from 'node:fs/promises'
import app from '@adonisjs/core/services/app'
import cache from '@adonisjs/cache/services/main'

import { adonisLogo } from './adonis_logo.js'

export class OgImageGenerator {
#buildMarkup(name: string, description: string) {
return html`
<div
style="
display: flex;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #212121;
font-size: 50;
padding: 50px 80px;
letter-spacing: -2;
font-weight: 800;
text-align: left;
"
>
<!-- Radial gradient at right -->
<div
style="
background: radial-gradient(circle, rgba(88,82,213,0.2) 0%, rgba(28,28,28,0) 45%);
width: 3000;
height: 1200;
top: -200;
right: -100;
position: absolute;
display: flex;
"
/>
<!-- Radial gradient at left -->
<div
style="
background: radial-gradient(circle, rgba(120,82,213,0.5) 0%, rgba(28,28,28,0) 45%);
width: 3000;
height: 1200;
top: -600;
left: -100;
position: absolute;
display: flex;
"
/>
<!-- Adonis logo at top right -->
<img
style="
position: absolute;
right: -30;
top: -20;
transform: rotate(-25deg);
"
src="${adonisLogo}"
width="400"
/>
<!-- Adonis logo at bottom left -->
<img
style="
position: absolute;
left: -90;
bottom: -20;
transform: rotate(20deg);
"
src="${adonisLogo}"
width="400"
/>
<!-- Package name -->
<div
style="
margin-top: 0;
color: white;
font-size: 60;
font-family: 'PolySans', sans-serif;
"
>
${name}
</div>
<!-- Package description -->
<div
style="
marginTop: 20;
color: #A0A0A0;
fontWeight: 100;
font-size: 30;
text-align: center;
line-height: 1.4;
letter-spacing: -1.2;
max-width: 60%;
font-family: 'Graphik', sans-serif;
"
>
${description}
</div>
</div>
`
}

async #generateSvg(name: string, description: string) {
const markup = this.#buildMarkup(name, description)

const fontsPath = app.makePath('resources/assets/fonts')

const fontPath1 = `${fontsPath}/Graphik-Regular.ttf`
const fontPath2 = `${fontsPath}/PolySans-Median.ttf`
const svg = await satori(markup, {
width: 1200,
height: 630,
fonts: [
{ name: 'Graphik', data: await readFile(fontPath1), weight: 700, style: 'normal' },
{ name: 'PolySans', data: await readFile(fontPath2), weight: 400, style: 'normal' },
],
})

return svg
}

/**
* Generate a new OpenGraph image for the given package name and description
*
* - First, convert HTML to SVG markup using Satori
* - Then, convert SVG to PNG using Resvg
* - Finally, cache the result so that we don't have to generate it again and again
*/
async generate(name: string, description: string) {
const base64Og = await cache.use('ogImage').getOrSet(`og-image:${name}`, async () => {
console.log('Generating new OG image for %s', name)
const svg = await this.#generateSvg(name, description)
return new Resvg(svg).render().asPng().toString('base64')
})

return Buffer.from(base64Og, 'base64')
}
}
3 changes: 3 additions & 0 deletions config/cache.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import app from '@adonisjs/core/services/app'
import { defineConfig, store, drivers } from '@adonisjs/cache'

import env from '#start/env'
Expand Down Expand Up @@ -41,6 +42,8 @@ const cacheConfig = defineConfig({
.useL1Layer(drivers.memory({ maxSize: 50 * 1024 * 1024 }))
.useL2Layer(drivers.database({ connectionName: 'sqlite' })),

ogImage: store({ ttl: null }).useL2Layer(drivers.file({ directory: app.tmpPath('og_images') })),

test: store().useL1Layer(drivers.memory({})),
},
})
Expand Down
23 changes: 13 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,22 @@
"dependencies": {
"@adonisjs/cache": "1.0.0-1",
"@adonisjs/core": "6.2.0",
"@adonisjs/inertia": "1.0.0-6",
"@adonisjs/lucid": "19.0.0-8",
"@adonisjs/inertia": "1.0.0-7",
"@adonisjs/lucid": "19.0.0",
"@adonisjs/session": "7.0.0",
"@adonisjs/shield": "8.0.1",
"@adonisjs/static": "^1.1.1",
"@adonisjs/vite": "2.0.2",
"@headlessui/vue": "^1.7.17",
"@inertiajs/vue3": "^1.0.14",
"@poppinss/utils": "^6.7.0",
"@poppinss/utils": "^6.7.1",
"@resvg/resvg-js": "^2.6.0",
"@sindresorhus/slugify": "^2.2.1",
"@vinejs/vine": "^1.7.0",
"@vueuse/core": "^10.7.1",
"@vueuse/core": "^10.7.2",
"cron": "^3.1.6",
"edge.js": "^6.0.1",
"floating-vue": "5.0.2",
"floating-vue": "5.2.0",
"github-markdown-css": "^5.5.0",
"globby": "^14.0.0",
"gsap": "^3.12.4",
Expand All @@ -63,6 +64,8 @@
"p-limit": "^4.0.0",
"reflect-metadata": "^0.2.1",
"sanitize-html": "^2.11.0",
"satori": "^0.10.11",
"satori-html": "^0.3.2",
"sqlite3": "^5.1.7",
"vue": "^3.4.7"
},
Expand All @@ -76,23 +79,23 @@
"@japa/assert": "^2.1.0",
"@japa/browser-client": "^2.0.2",
"@japa/file-system": "^2.1.1",
"@japa/plugin-adonisjs": "^2.0.3",
"@japa/plugin-adonisjs": "^3.0.0",
"@japa/runner": "^3.1.1",
"@julr/tooling-configs": "2.1.0",
"@julr/unocss-preset-forms": "^0.1.0",
"@swc/core": "^1.3.102",
"@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.3.7",
"@types/luxon": "^3.4.1",
"@types/markdown-it": "^13.0.7",
"@types/node": "^20.10.7",
"@types/node": "^20.11.5",
"@types/sanitize-html": "^2.9.5",
"@unocss/reset": "^0.58.3",
"@vitejs/plugin-vue": "^5.0.2",
"@vitejs/plugin-vue": "^5.0.3",
"eslint": "^8.56.0",
"pino-pretty": "^10.3.1",
"playwright": "^1.40.1",
"postcss-nested": "^6.0.1",
"prettier": "^3.1.1",
"prettier": "^3.2.4",
"ts-node": "^10.9.2",
"typescript": "~5.2.2",
"unocss": "^0.58.3",
Expand Down
Loading

0 comments on commit 48d9964

Please sign in to comment.