Skip to content

Commit

Permalink
Testing the new OG image generator system
Browse files Browse the repository at this point in the history
  • Loading branch information
conradolandia committed Oct 3, 2024
1 parent 546b397 commit ce62481
Show file tree
Hide file tree
Showing 10 changed files with 479 additions and 129 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ jobs:
- name: Install dependencies
shell: bash
run: ./ci/install.sh
- name: Install Fonts
run: |
mkdir -p ~/.fonts/silka
cp -r static/assets/fonts/silka/* ~/.fonts/silka/
fc-cache -f -v
- name: Build site
shell: bash
run: ./ci/build.sh
Expand Down
109 changes: 109 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"generate-og-images": "node scripts/generate-og-images.js",
"dev": "npm run generate-og-images && vite dev",
"build": "npm run generate-og-images && vite build",
"preview": "vite preview",
"deploy": "gh-pages -d build -t true"
},
Expand All @@ -24,6 +25,7 @@
"vite": "^5.1.6"
},
"dependencies": {
"gray-matter": "^4.0.3",
"rehype-class-names": "^2.0.0",
"rehype-rewrite": "^4.0.2",
"rehype-title-figure": "^0.1.2",
Expand Down
176 changes: 176 additions & 0 deletions scripts/generate-og-images.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import sharp from 'sharp';
import path from 'path';
import fs from 'fs';
import { promisify } from 'util';
import {
fetchMarkdownPostsMetadata,
formattedPubDate,
fetchAuthorMetadata,
} from './utils.js';

const writeFile = promisify(fs.writeFile);
const mkdir = promisify(fs.mkdir);
const access = promisify(fs.access);
const readFile = promisify(fs.readFile);

/**
* Checks if a given path exists.
* @param {string} pathToCheck - Path to verify.
* @returns {Promise<boolean>} - True if exists, false otherwise.
*/
async function exists(pathToCheck) {
try {
await access(pathToCheck, fs.constants.F_OK);
return true;
} catch {
return false;
}
}

/**
* Splits a title into multiple lines if it exceeds a maximum length.
* @param {string} title - Title to split.
* @param {number} maxLineLength - Maximum length of each line.
* @returns {Array<string>} - Array of title lines.
*/
function splitTitle(title, maxLineLength = 30) {
const words = title.split(' ');
const lines = [];
let currentLine = '';

for (const word of words) {
if ((currentLine + word).length > maxLineLength) {
lines.push(currentLine.trim());
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
}

if (currentLine.trim()) {
lines.push(currentLine.trim());
}

return lines;
}

/**
* Generates an Open Graph image for a specific post.
* @param {object} data - Data for the SVG template.
* @param {string} slug - Slug of the post.
*/
async function generateOgImage(data, slug) {
const width = 1200;
const height = 630;
const titleLines = splitTitle(data.title);
const bylineY = height - 120;

// Generate multiple <text> elements for each title line
const titleTexts = titleLines
.map((line, index) => `<text x="120" y="${150 + index * 72}" class="title">${line}</text>`)
.join('\n ');

// Read the SVG template
const templatePath = path.join(process.cwd(), 'scripts', 'templates', 'og-template.svg');
const templateContent = await readFile(templatePath, 'utf-8');

// Replace placeholders with actual data
const svg = templateContent
.replace('${width}', width)
.replace('${height}', height)
.replace('${titleTexts}', titleTexts)
.replace('${bylineY}', bylineY)
.replace('${author}', data.author)
.replace('${pubDate}', data.pubDate);

try {
// Create SVG buffer
const svgBuffer = Buffer.from(svg);

// Generate image using sharp
const image = await sharp({
create: {
width: width,
height: height,
channels: 4,
background: { r: 247, g: 247, b: 242, alpha: 1 },
},
})
.composite([
{
input: svgBuffer,
top: 0,
left: 0,
},
])
.png()
.toBuffer();

// Output directory and path
const outputDir = path.join(process.cwd(), 'static', 'assets', 'og');
const outputPath = path.join(outputDir, `${slug}.png`);

// Ensure output directory exists
if (!(await exists(outputDir))) {
await mkdir(outputDir, { recursive: true });
}

// Save the image to the filesystem
await writeFile(outputPath, image);
console.log(`OG image generated for post: ${slug}`);
} catch (error) {
console.error('Error generating OG image:', error);
}
}

/**
* Generates Open Graph images for all Markdown posts.
*/
async function generateAllOgImages() {
try {
const posts = await fetchMarkdownPostsMetadata();

for (const post of posts) {
const { meta, path: postPath } = post;
const slug = path.basename(postPath);
const imagePath = path.join('static', 'assets', 'og', `${slug}.png`);

// Check if the image already exists
if (await exists(imagePath)) {
console.log(`OG image for post "${slug}" already exists. Skipping.`);
continue;
}

// Fetch author metadata
const authorMetadata = await fetchAuthorMetadata(meta.author);

if (!authorMetadata) {
console.warn(
`Could not fetch metadata for author "${meta.author}". Skipping image generation for post "${slug}".`
);
continue;
}

// Format publication date
const pubDateFormatted = formattedPubDate(meta.pub_date);

// Prepare data for the SVG
const svgData = {
title: meta.title,
author: authorMetadata.name,
pubDate: pubDateFormatted,
};

// Generate the OG image
await generateOgImage(svgData, slug);
}

console.log('All Open Graph images have been successfully generated.');
} catch (error) {
console.error('Error during Open Graph image generation:', error);
process.exit(1); // Exit with failure
}
}

// Execute the script
generateAllOgImages();
Loading

0 comments on commit ce62481

Please sign in to comment.