Skip to content

Commit

Permalink
docs: add details + common example config file
Browse files Browse the repository at this point in the history
  • Loading branch information
JulianCataldo committed Feb 9, 2024
1 parent 61f3f0c commit 36e5ab8
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 34 deletions.
100 changes: 72 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# OpenGraph Images Generator <!-- omit in toc -->
# Open Graph Images Generator <!-- omit in toc -->

[![NPM](https://img.shields.io/npm/v/og-images-generator)](https://www.npmjs.com/package/og-images-generator)
![Downloads](https://img.shields.io/npm/dt/og-images-generator)
Expand All @@ -9,9 +9,18 @@
[![Prettier](https://img.shields.io/badge/Prettier-333333?logo=prettier)](https://prettier.io)
[![EditorConfig](https://img.shields.io/badge/EditorConfig-333333?logo=editorconfig)](https://editorconfig.org)

Generate OG images from a static folder and / or a middleware.
Extract metadata from HTML pages. No headless browser involved.
Comes as a CLI, API or plugins.
Generate social sharing thumbnails for your websites, with plain **HTML** + **CSS**.
Extract metadata from pages, on-the-fly (middleware) or from distributables (static folder).

**No headless browser** involved = fast cold boot, much less MBs.
Exposes all underlying APIs for full output customization.

Usable as a **CLI**, an **API** or via **plugins** for **Astro**, **Express**, **Rollup** and **Vite**.

Moreover, a handful of helpers are here to ease poster images authoring.

Under the hood, it will transform your HTML / CSS to **SVG**, while **retaining layout and typography calculations**, then it's converted to **PNG**.
You can use gradients, borders, flexboxes, inline SVGs, and [more](https://github.com/vercel/satori)

---

Expand All @@ -33,6 +42,11 @@ Comes as a CLI, API or plugins.

---

**Additional ressources**

- [Demo projects](./demos)
- [API documentation](https://juliancataldo.github.io/og-images-generator/)

## Installation

```
Expand Down Expand Up @@ -75,12 +89,13 @@ export const renderOptions = {
};
```
**You need to export** `renderOptions` and `template` from your `og-images-generator` configuration file.
**At the minimum**, you need to export `renderOptions` (with **size** and **font**) and `template` from your `og-images-generator` configuration file.
`paths` is optional.
> [!NOTE]
> Helpers
> `styled.div` is a dummy strings concatenation literal (to get syntax highlighting).
> `div` is the only needed (and available) tag, as it makes no difference anyway.
> **Helpers**
> `styled.div` is a dummy strings concatenation literal (bringing syntax highlighting and formatting).
> `div` is the only needed (and available) tag, as it makes no difference anyway for this sugar.
>
> Also, you don't need to wrap interpolated HTML attributes with quotes (e.g. `style="${foo}"`).
> `<foo-bar style=${styles.baz}></foo-bar>` just works.
Expand All @@ -90,6 +105,10 @@ export const renderOptions = {
**As a preamble**, don't forget to add the appropriate meta for your OGs, there is plenty
on [ressources](https://code.juliancataldo.com/component/astro-seo-metadata) on the web on how to setup your SEO with your favorite environment.
That way, `og-images-generator` will crawl them back to your template.
It will parse all the **meta tags** (in head) and **JSON LDs** script tags content (in head and body).
---
By default:
Expand All @@ -99,7 +118,7 @@ By default:
> [!WARNING]
> `/``index.png` is an exception.
> We don't want `https://example.com/og.png`, as to keep this library output well segregated from the rest of your `dist`.
> We don't want `https://example.com/og.png`, as to keep this library output well segregated from the rest of your `dist` folder.
> That's why so we need to disambiguate the root path.
For `https://example.com`:
Expand All @@ -112,15 +131,16 @@ For `https://example.com`:
<meta property="og:image" content="https://example.com/og/nested/my-page.png" />
```
It's a contrived example. Fine-tuning SEO tags is an ancient, dark art.
You'll need the `twitter:` stuff and other massaging,
but that's really out of the scope of this library, which does not mess with your HTML.
It's a contrived example. Fine-tuning SEO tags is an dark, ancient art.
You'll need the `twitter:` stuff and other massaging, so you're sure it looks great everywhere.
But that's really out of the scope of this library, which does not mess with your HTML in the first place.
> [!NOTE]
> Additional ressources
>
> - [Demo projects](./demos)
> - [API documentation](https://juliancataldo.github.io/og-images-generator/)
Alongside meta tag, JSON LD blocks are also extracted and made available for your template to consume.
**_What if I need to attribute different templates depending on the page route?_**
To achieve per URL template variations, add your branching logic in the root template.
You can split and import full or partial templates accordingly if it grows too much, or to organize styles separately.
Also, `page.url` is provided, alongside metadata (which should hold those info too, like `og:url`).
---
Expand All @@ -139,22 +159,46 @@ npx generate-og
npx generate-og --base dist --out dist/og --json dist/og/index.json
```
It will parse all the meta tags (in head) and JSON LDs script content (in head and body).
### Programmatic (JS API)
Use this API if you want to build your custom workflow, or create a plugin for an unsupported dev/build tools or JS runtimes (e.g. "serverless" functions).
```js
import { generateOgImages } from 'og-images-generator/api';
import * as api from 'og-images-generator/api';
await api.generateOgImages(/* options */);
await generateOgImages(/* options */);
await api.renderOgImage(/* options */);
```
See also the [tests folder](./test) for more minimal insights.
### Express / Connect middleware
```js
import express from 'express';
import { connectOgImagesGenerator } from 'og-images-generator/connect';
app.use(await connectOgImagesGenerator(/* pathPrefix: string */));
const app = express();
app.use(await connectOgImagesGenerator());
app.get('/', (_, res) => {
res.send(`
<html>
<head>
<meta property="og:title" content="Express / Connect demo" />
<meta property="og:description" content="Welcome to my website!" />
</head>
<body>
<img src="/og/index.png"/>
</body>
</html>
`);
});
app.listen(1234);
```
### Rollup plugin
Expand Down Expand Up @@ -210,16 +254,16 @@ export default defineConfig({
## Notes on image optimization
You could use a CDN proxy to handle on the fly image optimizations.
If you're running this on a server, you should use a CDN or any kind of proxying + caching, to handle on the fly image optimizations, with the rest of your assets.
Also AFAIK, all major social networks crawlers are transforming and caching assets themselves.
It's their job to normalize optimizations in order to serve assets to their users.
It's their job to normalize optimizations in order to serve images to their users efficiently.
## References
- [vercel/satori](https://github.com/vercel/satori)
- [natemoo-re/satori-html](https://github.com/natemoo-re/satori-html)
- [yisibl/resvg-js](https://github.com/yisibl/resvg-js)
- [lit/ssr](https://github.com/lit/lit/tree/d68f5c705484b9f6ea1f553d4851a9aa6a440db0/packages/labs/ssr)
- Vercel's Satori: [vercel/satori](https://github.com/vercel/satori)
- Nate Moore HTML to Satori AST adapter: [natemoo-re/satori-html](https://github.com/natemoo-re/satori-html)
- SVG to PNG conversion with resvg: [yisibl/resvg-js](https://github.com/yisibl/resvg-js)
- Static HTML template literal authoring / rendering with Lit SSR: [lit/ssr](https://github.com/lit/lit/tree/d68f5c705484b9f6ea1f553d4851a9aa6a440db0/packages/labs/ssr)
---
Expand Down
187 changes: 187 additions & 0 deletions demos/__common/og-images.example-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { html, styled, OG_SIZE, FONTS } from 'og-images-generator';

/** @type {import('og-images-generator').PathsOptions} */
export const paths = {
// DEFAULTS
// base: './dist',
// out: './dist/og',
// json: './dist/og/index.json',
};

/** @type {import('og-images-generator').RenderOptions} */
export const renderOptions = {
satori: {
fonts: [await FONTS.sourceSans()],
...OG_SIZE,
},
};

/** @type {import('og-images-generator').Template} */
export const template = ({ page }) => {
console.info('OG Template for: ', page.path);

if ('og:title' in page.meta.tags === false) throw Error('Missing title!');
if ('og:description' in page.meta.tags === false)
throw Error('Missing description!');

const title = page.meta.tags['og:title'] ?? 'untitled';
const description = page.meta.tags['og:description'] ?? 'nodesc';

return html` <!-- -->
<div style=${styles.container}>
<div style=${styles.wrap}>
<header style=${styles.header}>
<span style=${styles.breadcrumbs}>
${['Bread', 'Crumbs', 'Baguette'].map(
(b) => html` ${b} <span style="margin: 0 1rem"> / </span>`,
)}
</span>
<span style=${styles.mainTitle}>${title}</span>
</header>
<div style=${styles.description}>${description.trim()}</div>
<footer style=${styles.footer}>
<div style=${styles.logo1}>
${icons.main}${globalContent.siteTitle}
</div>
<em>Nice</em>
<strong>Weather</strong>
</footer>
</div>
</div>`;
};

const globalContent = {
siteTitle: 'My Site',
};

const tokens = {
/* Shoelace "Sky" palette (https://shoelace.style/tokens/color) */
slColorSky_50: `rgb(19, 61, 87)`,
slColorSky_100: `rgb(21, 82, 122)`,
slColorSky_200: `rgb(19, 93, 138)`,
slColorSky_300: `rgb(18, 109, 166)`,
slColorSky_400: `rgb(22, 137, 204)`,
slColorSky_500: `rgb(17, 158, 226)`,
slColorSky_600: `rgb(39, 186, 253)`,
slColorSky_700: `rgb(105, 208, 255)`,
slColorSky_800: `rgb(166, 227, 255)`,
slColorSky_900: `rgb(203, 239, 255)`,
slColorSky_950: `rgb(232, 253, 255)`,
};

const icons = {
main: html`
<!-- -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
width="4rem"
height="4rem"
fill="currentColor"
>
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<path
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM388.1 312.8c12.3-3.8 24.3 6.9 19.3 18.7C382.4 390.6 324.2 432 256.3 432s-126.2-41.4-151.1-100.5c-5-11.8 7-22.5 19.3-18.7c39.7 12.2 84.5 19 131.8 19s92.1-6.8 131.8-19zm-16.9-79.2c-17.6-23.5-52.8-23.5-70.4 0c-5.3 7.1-15.3 8.5-22.4 3.2s-8.5-15.3-3.2-22.4c30.4-40.5 91.2-40.5 121.6 0c5.3 7.1 3.9 17.1-3.2 22.4s-17.1 3.9-22.4-3.2zM176.4 176a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
/>
</svg>
`,
};

const styles = {
header: styled.div`
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 0.5rem;
`,

breadcrumbs: styled.div`
align-items: center;
font-size: 40px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
`,

container: styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
background-image: radial-gradient(
at 15% 100%,
${tokens.slColorSky_50} 0px,
${tokens.slColorSky_100} 100%
),
linear-gradient(
90deg,
${tokens.slColorSky_50} 0px,
${tokens.slColorSky_900} 50%,
${tokens.slColorSky_50} 100%
);
`,

title: styled.div`
font-weight: 700;
font-size: 70px;
color: white;
`,

mainTitle: styled.div`
text-shadow: 0.15em 0.15em 0.5em #00111ad9;
color: ${tokens.slColorSky_800};
font-size: 60px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
`,

wrap: styled.div`
padding: 50px 75px 50px 75px;
height: 100%;
width: 100%;
display: flex;
justify-content: space-between;
flex-direction: column;
color: ${tokens.slColorSky_900};
`,

description: styled.div`
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: pre-wrap;
text-overflow: ellipsis;
font-size: 40px;
margin: 1.5rem 0 2.5rem 0;
flex-direction: column;
width: 100%;
overflow: hidden;
border-left: 5px solid ${tokens.slColorSky_950};
padding: 0 40px 0 40px;
padding-bottom: 10px;
`,

logo1: styled.div`
display: flex;
align-items: center;
gap: 2rem;
`,

footer: styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 30px;
font-size: 40px;
`,
};
5 changes: 1 addition & 4 deletions demos/express-connect/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import express from 'express';
import { connectOgImagesGenerator } from 'og-images-generator/connect';

const app = express();
const port = 3000;

app.use(await connectOgImagesGenerator());

Expand Down Expand Up @@ -45,6 +44,4 @@ app.get('/nested/foo', (_, res) => {
`);
});

app.listen(port, () => {
console.log(`Example app listening on http://localhost:${port}`);
});
app.listen(3000);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "og-images-generator",
"version": "0.0.3",
"version": "0.0.4",
"description": "Generate OG images from a static folder and / or a middleware.\nExtract metadata from HTML pages. No headless browser involved.\nComes as a CLI, API or plugins.",
"keywords": [
"og-images",
Expand Down
Loading

0 comments on commit 36e5ab8

Please sign in to comment.