@kensio/colophon 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -801
- package/dist/background.d.ts +7 -2
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +44 -1
- package/dist/background.js.map +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/config.d.ts +1 -1
- package/dist/cli/config.js +1 -1
- package/dist/config/defaults.d.ts +8 -7
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +8 -8
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +5 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/resolve.d.ts +7 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +16 -0
- package/dist/config/resolve.js.map +1 -1
- package/dist/config/size.d.ts +1 -1
- package/dist/config/size.d.ts.map +1 -1
- package/dist/config/size.js +3 -2
- package/dist/config/size.js.map +1 -1
- package/dist/content/props.d.ts +2 -2
- package/dist/content/props.js +2 -2
- package/dist/content/slug.d.ts +5 -5
- package/dist/content/slug.js +6 -6
- package/dist/content/slug.js.map +1 -1
- package/dist/content/walk.d.ts +1 -1
- package/dist/content/walk.js +1 -1
- package/dist/fonts/index.d.ts +3 -2
- package/dist/fonts/index.d.ts.map +1 -1
- package/dist/fonts/index.js +3 -2
- package/dist/fonts/index.js.map +1 -1
- package/dist/fonts/resolve.js +1 -1
- package/dist/generate/extra.js +4 -4
- package/dist/generate/extra.js.map +1 -1
- package/dist/generate/options.d.ts +1 -1
- package/dist/generate/outputs.d.ts +4 -4
- package/dist/generate/outputs.js +4 -4
- package/dist/highlight/language.d.ts +2 -3
- package/dist/highlight/language.d.ts.map +1 -1
- package/dist/highlight/language.js +3 -4
- package/dist/highlight/language.js.map +1 -1
- package/dist/highlight/normalise.d.ts +2 -2
- package/dist/highlight/normalise.js +2 -2
- package/dist/image/index.d.ts +18 -0
- package/dist/image/index.d.ts.map +1 -0
- package/dist/image/index.js +42 -0
- package/dist/image/index.js.map +1 -0
- package/dist/image/jpeg.d.ts +10 -0
- package/dist/image/jpeg.d.ts.map +1 -0
- package/dist/image/jpeg.js +57 -0
- package/dist/image/jpeg.js.map +1 -0
- package/dist/image/load.d.ts +4 -0
- package/dist/image/load.d.ts.map +1 -0
- package/dist/image/load.js +60 -0
- package/dist/image/load.js.map +1 -0
- package/dist/image/media.d.ts +10 -0
- package/dist/image/media.d.ts.map +1 -0
- package/dist/image/media.js +37 -0
- package/dist/image/media.js.map +1 -0
- package/dist/image/resolve.d.ts +13 -0
- package/dist/image/resolve.d.ts.map +1 -0
- package/dist/image/resolve.js +38 -0
- package/dist/image/resolve.js.map +1 -0
- package/dist/image/size.d.ts +22 -0
- package/dist/image/size.d.ts.map +1 -0
- package/dist/image/size.js +60 -0
- package/dist/image/size.js.map +1 -0
- package/dist/image/svg.d.ts +12 -0
- package/dist/image/svg.d.ts.map +1 -0
- package/dist/image/svg.js +61 -0
- package/dist/image/svg.js.map +1 -0
- package/dist/image/uri.d.ts +9 -0
- package/dist/image/uri.d.ts.map +1 -0
- package/dist/image/uri.js +22 -0
- package/dist/image/uri.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/layout/block.d.ts +37 -0
- package/dist/layout/block.d.ts.map +1 -0
- package/dist/layout/block.js +26 -0
- package/dist/layout/block.js.map +1 -0
- package/dist/layout/box.d.ts +20 -0
- package/dist/layout/box.d.ts.map +1 -0
- package/dist/layout/box.js +61 -0
- package/dist/layout/box.js.map +1 -0
- package/dist/layout/distribute.d.ts +15 -0
- package/dist/layout/distribute.d.ts.map +1 -0
- package/dist/layout/distribute.js +30 -0
- package/dist/layout/distribute.js.map +1 -0
- package/dist/layout/image.d.ts +42 -0
- package/dist/layout/image.d.ts.map +1 -0
- package/dist/layout/image.js +34 -0
- package/dist/layout/image.js.map +1 -0
- package/dist/layout/index.d.ts +40 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +30 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/lines.d.ts +45 -0
- package/dist/layout/lines.d.ts.map +1 -0
- package/dist/layout/lines.js +71 -0
- package/dist/layout/lines.js.map +1 -0
- package/dist/layout/measure.d.ts +11 -0
- package/dist/layout/measure.d.ts.map +1 -0
- package/dist/layout/measure.js +11 -0
- package/dist/layout/measure.js.map +1 -0
- package/dist/layout/panel.d.ts +20 -0
- package/dist/layout/panel.d.ts.map +1 -0
- package/dist/layout/panel.js +22 -0
- package/dist/layout/panel.js.map +1 -0
- package/dist/layout/row.d.ts +15 -0
- package/dist/layout/row.d.ts.map +1 -0
- package/dist/layout/row.js +20 -0
- package/dist/layout/row.js.map +1 -0
- package/dist/layout/scrim.d.ts +23 -0
- package/dist/layout/scrim.d.ts.map +1 -0
- package/dist/layout/scrim.js +38 -0
- package/dist/layout/scrim.js.map +1 -0
- package/dist/layout/stack.d.ts +14 -0
- package/dist/layout/stack.d.ts.map +1 -0
- package/dist/layout/stack.js +19 -0
- package/dist/layout/stack.js.map +1 -0
- package/dist/layout/types.d.ts +49 -0
- package/dist/layout/types.d.ts.map +1 -0
- package/dist/layout/types.js +2 -0
- package/dist/layout/types.js.map +1 -0
- package/dist/manifest/build.d.ts +1 -1
- package/dist/manifest/build.js +1 -1
- package/dist/manifest/index.d.ts +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/order.d.ts +1 -1
- package/dist/manifest/order.js +1 -1
- package/dist/manifest/page.d.ts +2 -2
- package/dist/manifest/page.js +3 -3
- package/dist/measure/estimate.d.ts +10 -0
- package/dist/measure/estimate.d.ts.map +1 -0
- package/dist/measure/estimate.js +58 -0
- package/dist/measure/estimate.js.map +1 -0
- package/dist/measure/faces.d.ts +15 -0
- package/dist/measure/faces.d.ts.map +1 -0
- package/dist/measure/faces.js +70 -0
- package/dist/measure/faces.js.map +1 -0
- package/dist/measure/index.d.ts +16 -0
- package/dist/measure/index.d.ts.map +1 -0
- package/dist/measure/index.js +0 -0
- package/dist/measure/index.js.map +1 -0
- package/dist/measure/runs.d.ts +17 -0
- package/dist/measure/runs.d.ts.map +1 -0
- package/dist/measure/runs.js +39 -0
- package/dist/measure/runs.js.map +1 -0
- package/dist/measure/select.d.ts +13 -0
- package/dist/measure/select.d.ts.map +1 -0
- package/dist/measure/select.js +47 -0
- package/dist/measure/select.js.map +1 -0
- package/dist/meta/tags.d.ts +6 -6
- package/dist/meta/tags.js +6 -6
- package/dist/meta/url.d.ts +4 -4
- package/dist/meta/url.js +4 -4
- package/dist/placement/check.d.ts +1 -1
- package/dist/placement/check.js +1 -1
- package/dist/placement/index.js +1 -1
- package/dist/placement/relative.d.ts +3 -3
- package/dist/placement/relative.js +3 -3
- package/dist/pool.d.ts +2 -2
- package/dist/pool.js +3 -3
- package/dist/pool.js.map +1 -1
- package/dist/props.d.ts +7 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +17 -0
- package/dist/props.js.map +1 -0
- package/dist/render/svg.d.ts +4 -2
- package/dist/render/svg.d.ts.map +1 -1
- package/dist/render/svg.js +19 -4
- package/dist/render/svg.js.map +1 -1
- package/dist/stamp/config-digest.d.ts.map +1 -1
- package/dist/stamp/config-digest.js +20 -1
- package/dist/stamp/config-digest.js.map +1 -1
- package/dist/stamp/index.js +2 -2
- package/dist/stamp/index.js.map +1 -1
- package/dist/stamp/read.d.ts +1 -1
- package/dist/stamp/read.js +4 -4
- package/dist/templates/attribution.d.ts +17 -0
- package/dist/templates/attribution.d.ts.map +1 -0
- package/dist/templates/attribution.js +39 -0
- package/dist/templates/attribution.js.map +1 -0
- package/dist/templates/banner/badge.d.ts.map +1 -1
- package/dist/templates/banner/badge.js +3 -6
- package/dist/templates/banner/badge.js.map +1 -1
- package/dist/templates/banner/index.d.ts +3 -0
- package/dist/templates/banner/index.d.ts.map +1 -1
- package/dist/templates/banner/index.js +37 -35
- package/dist/templates/banner/index.js.map +1 -1
- package/dist/templates/banner/lines.d.ts +7 -12
- package/dist/templates/banner/lines.d.ts.map +1 -1
- package/dist/templates/banner/lines.js +23 -34
- package/dist/templates/banner/lines.js.map +1 -1
- package/dist/templates/banner/version.d.ts +9 -0
- package/dist/templates/banner/version.d.ts.map +1 -0
- package/dist/templates/banner/version.js +21 -0
- package/dist/templates/banner/version.js.map +1 -0
- package/dist/templates/card/index.d.ts +4 -1
- package/dist/templates/card/index.d.ts.map +1 -1
- package/dist/templates/card/index.js +37 -25
- package/dist/templates/card/index.js.map +1 -1
- package/dist/templates/card/lines.d.ts +17 -13
- package/dist/templates/card/lines.d.ts.map +1 -1
- package/dist/templates/card/lines.js +24 -20
- package/dist/templates/card/lines.js.map +1 -1
- package/dist/templates/code/chrome.d.ts +7 -3
- package/dist/templates/code/chrome.d.ts.map +1 -1
- package/dist/templates/code/chrome.js +12 -11
- package/dist/templates/code/chrome.js.map +1 -1
- package/dist/templates/code/clip.d.ts +1 -1
- package/dist/templates/code/clip.js +1 -1
- package/dist/templates/code/fit.d.ts +2 -2
- package/dist/templates/code/fit.d.ts.map +1 -1
- package/dist/templates/code/fit.js +4 -4
- package/dist/templates/code/fit.js.map +1 -1
- package/dist/templates/code/index.d.ts.map +1 -1
- package/dist/templates/code/index.js +7 -6
- package/dist/templates/code/index.js.map +1 -1
- package/dist/templates/code/layout.d.ts +1 -1
- package/dist/templates/code/layout.d.ts.map +1 -1
- package/dist/templates/code/layout.js +6 -6
- package/dist/templates/code/layout.js.map +1 -1
- package/dist/templates/code/panel.d.ts +13 -0
- package/dist/templates/code/panel.d.ts.map +1 -1
- package/dist/templates/code/panel.js +34 -2
- package/dist/templates/code/panel.js.map +1 -1
- package/dist/templates/code/plate.d.ts +3 -1
- package/dist/templates/code/plate.d.ts.map +1 -1
- package/dist/templates/code/plate.js +11 -9
- package/dist/templates/code/plate.js.map +1 -1
- package/dist/templates/code/spans.js +1 -1
- package/dist/templates/code/spans.js.map +1 -1
- package/dist/templates/code/width.d.ts +10 -0
- package/dist/templates/code/width.d.ts.map +1 -0
- package/dist/templates/code/width.js +26 -0
- package/dist/templates/code/width.js.map +1 -0
- package/dist/templates/footer.d.ts +8 -0
- package/dist/templates/footer.d.ts.map +1 -1
- package/dist/templates/footer.js +11 -1
- package/dist/templates/footer.js.map +1 -1
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/logo.d.ts +15 -0
- package/dist/templates/logo.d.ts.map +1 -0
- package/dist/templates/logo.js +30 -0
- package/dist/templates/logo.js.map +1 -0
- package/dist/templates/measure.d.ts +11 -0
- package/dist/templates/measure.d.ts.map +1 -0
- package/dist/templates/measure.js +11 -0
- package/dist/templates/measure.js.map +1 -0
- package/dist/templates/props.d.ts +2 -2
- package/dist/templates/props.js +2 -2
- package/dist/templates/text.d.ts +37 -0
- package/dist/templates/text.d.ts.map +1 -0
- package/dist/templates/text.js +26 -0
- package/dist/templates/text.js.map +1 -0
- package/dist/text/break.d.ts +11 -0
- package/dist/text/break.d.ts.map +1 -0
- package/dist/text/break.js +30 -0
- package/dist/text/break.js.map +1 -0
- package/dist/text/fit.d.ts +30 -0
- package/dist/text/fit.d.ts.map +1 -0
- package/dist/text/fit.js +29 -0
- package/dist/text/fit.js.map +1 -0
- package/dist/text/index.d.ts +5 -3
- package/dist/text/index.d.ts.map +1 -1
- package/dist/text/index.js +3 -2
- package/dist/text/index.js.map +1 -1
- package/dist/text/wrap.d.ts +17 -7
- package/dist/text/wrap.d.ts.map +1 -1
- package/dist/text/wrap.js +25 -17
- package/dist/text/wrap.js.map +1 -1
- package/dist/types.d.ts +161 -45
- package/dist/types.d.ts.map +1 -1
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +3 -2
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/keys.d.ts +19 -2
- package/dist/validate/keys.d.ts.map +1 -1
- package/dist/validate/keys.js +40 -3
- package/dist/validate/keys.js.map +1 -1
- package/dist/validate/suggest.d.ts.map +1 -1
- package/dist/validate/suggest.js +21 -3
- package/dist/validate/suggest.js.map +1 -1
- package/dist/validate/values.d.ts +2 -2
- package/dist/validate/values.d.ts.map +1 -1
- package/dist/validate/values.js +17 -3
- package/dist/validate/values.js.map +1 -1
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
# <img src="https://colophonjs.dev/favicon.png" alt="Colophon logo" width="28" height="28"> @kensio/colophon
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@kensio/colophon)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
Generate social meta images (Open Graph and share-card images) for the posts of
|
|
11
|
+
a static website, driven by each post's frontmatter.
|
|
12
|
+
|
|
13
|
+
[https://colophonjs.dev/](https://colophonjs.dev/ "Colophon documentation website")
|
|
14
|
+
|
|
15
|
+
You describe an image in frontmatter with a title, a subtitle, a version and a
|
|
16
|
+
template name, and Colophon renders branded PNGs at the sizes you need. The name
|
|
17
|
+
comes from the printer's _colophon_, the emblem a publisher stamps on a finished
|
|
18
|
+
work.
|
|
19
|
+
|
|
20
|
+
- **Frontmatter-driven.** Props are read from a post rather than fixed by a
|
|
21
|
+
schema.
|
|
22
|
+
- **Templates.** A small registry of layouts, picked per post from frontmatter.
|
|
23
|
+
- **Syntax-highlighted code images.** The `code` template renders a snippet from
|
|
24
|
+
frontmatter with real VS Code theme colours.
|
|
25
|
+
- **Configurable branding.** Colours, gradient, fonts, footer and badge come from
|
|
26
|
+
config, not from any one site's stylesheet.
|
|
27
|
+
- **Multiple sizes from one input.** A 1:1 square and a 1.91:1 landscape by
|
|
18
28
|
default, or whatever set you configure.
|
|
19
|
-
- **Manifest and meta tags
|
|
20
|
-
|
|
21
|
-
- **Small, reusable API
|
|
29
|
+
- **Manifest and meta tags.** A JSON record of what was generated, and the Open
|
|
30
|
+
Graph and Twitter tags that go with it.
|
|
31
|
+
- **Small, reusable API.** A render core with no filesystem concerns, plus an
|
|
22
32
|
optional content walker and CLI.
|
|
23
33
|
|
|
24
34
|
## Install
|
|
@@ -27,12 +37,12 @@ finished work.
|
|
|
27
37
|
pnpm add @kensio/colophon
|
|
28
38
|
```
|
|
29
39
|
|
|
30
|
-
`@resvg/resvg-js`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
`@resvg/resvg-js` rasterises the SVG to PNG, and `shiki` provides the grammars
|
|
41
|
+
and themes for the `code` template. No headless browser is involved. Fonts can
|
|
42
|
+
be handed to the renderer as files, so a build renders the same image
|
|
43
|
+
everywhere.
|
|
34
44
|
|
|
35
|
-
## Quick start
|
|
45
|
+
## Quick start
|
|
36
46
|
|
|
37
47
|
Add image props to a post's frontmatter:
|
|
38
48
|
|
|
@@ -47,7 +57,7 @@ meta_img_props:
|
|
|
47
57
|
---
|
|
48
58
|
```
|
|
49
59
|
|
|
50
|
-
Create a config module
|
|
60
|
+
Create a config module, or omit it to use the neutral defaults:
|
|
51
61
|
|
|
52
62
|
```ts
|
|
53
63
|
// colophon.config.ts
|
|
@@ -67,767 +77,30 @@ colophon content --config colophon.config.ts
|
|
|
67
77
|
```
|
|
68
78
|
|
|
69
79
|
For every file that declares `meta_img_props`, Colophon writes one PNG per
|
|
70
|
-
output size next to it, named `<slug>-<size>.png`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
colophon [contentDir] [options]
|
|
75
|
-
|
|
76
|
-
-c, --config <path> Config module whose default export is a ColophonConfig,
|
|
77
|
-
or a function returning one
|
|
78
|
-
-f, --force Re-render every image, ignoring the stamps
|
|
79
|
-
-o, --overwrite Alias for --force
|
|
80
|
-
--concurrency <n> How many images to render at once
|
|
81
|
-
-h, --help Show help
|
|
82
|
-
|
|
83
|
-
contentDir defaults to "content"
|
|
84
|
-
--concurrency defaults to one per available CPU
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Images are rendered a few at a time rather than all at once, so a tree of a few
|
|
88
|
-
hundred posts does not start a few hundred rasterisations and thrash. The
|
|
89
|
-
default — one per CPU the process can use — suits a build machine that has
|
|
90
|
-
nothing else to do; lower it with `--concurrency` to leave room for whatever
|
|
91
|
-
else is running.
|
|
92
|
-
|
|
93
|
-
### Rebuilds
|
|
94
|
-
|
|
95
|
-
Every image Colophon writes carries a stamp — a hash of the props, the config
|
|
96
|
-
and the output size it was rendered from, stored in the PNG itself as a `tEXt`
|
|
97
|
-
chunk. On the next run an image whose stamp still matches is left alone, and
|
|
98
|
-
one whose title, colours, template or size has moved on is rendered again. So
|
|
99
|
-
correcting a single post's title re-renders that post's images and nothing
|
|
100
|
-
else.
|
|
101
|
-
|
|
102
|
-
There is no cache directory and nothing to keep in sync: delete an image and
|
|
103
|
-
its stamp goes with it. An image Colophon did not write, or one written by an
|
|
104
|
-
older version, has no stamp and is rendered over.
|
|
105
|
-
|
|
106
|
-
The stamp covers the Colophon version too, since the built-in templates ship
|
|
107
|
-
with it — upgrading the package re-renders the tree once. A custom template is
|
|
108
|
-
covered by its own source code, which means a template that reads something its
|
|
109
|
-
source does not name (a closed-over value, a file it loads itself) can change
|
|
110
|
-
without being noticed; `--force` is the way out.
|
|
111
|
-
|
|
112
|
-
## Programmatic use
|
|
113
|
-
|
|
114
|
-
### Render from props (core)
|
|
115
|
-
|
|
116
|
-
The core takes props and config and returns rendered bytes — no filesystem, no
|
|
117
|
-
content discovery:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
import { renderMetaImages } from "@kensio/colophon";
|
|
121
|
-
import { writeFile } from "node:fs/promises";
|
|
122
|
-
|
|
123
|
-
const images = await renderMetaImages(
|
|
124
|
-
{
|
|
125
|
-
template: "banner",
|
|
126
|
-
title: "@kensio/colophon",
|
|
127
|
-
subtitle: "Generate social meta images from frontmatter",
|
|
128
|
-
version: "1.2.0",
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
colors: { brand: "#2563eb" },
|
|
132
|
-
footer: "example.com",
|
|
133
|
-
badge: { text: "npm" },
|
|
134
|
-
},
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
for (const image of images) {
|
|
138
|
-
// image.name is the output-size name ("og", "square", …).
|
|
139
|
-
await writeFile(`social-${image.name}.png`, image.png);
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Walk content + generate (host helpers)
|
|
144
|
-
|
|
145
|
-
`walkContent` finds `.md` files and reads their frontmatter; `generate` ties
|
|
146
|
-
walking, rendering and writing together (this is what the CLI uses):
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
import { generate } from "@kensio/colophon";
|
|
150
|
-
|
|
151
|
-
await generate({
|
|
152
|
-
contentDir: "content",
|
|
153
|
-
config: { colors: { brand: "#2563eb" } },
|
|
154
|
-
overwrite: false,
|
|
155
|
-
concurrency: 4, // defaults to one per available CPU
|
|
156
|
-
onResult: (result) =>
|
|
157
|
-
// result.url is where it is served, when the placement knows.
|
|
158
|
-
console.log(`${result.skipped ? "skip" : "wrote"} ${result.outputPath}`),
|
|
159
|
-
});
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Import the walker on its own from the `@kensio/colophon/content` subpath if you
|
|
163
|
-
only want frontmatter discovery.
|
|
164
|
-
|
|
165
|
-
## Templates
|
|
166
|
-
|
|
167
|
-
| Name | Layout |
|
|
168
|
-
| -------- | ---------------------------------------------------------------------------- |
|
|
169
|
-
| `banner` | Left-aligned title with optional version, subtitle, corner badge and footer. |
|
|
170
|
-
| `card` | Minimal centred title with an optional subtitle. |
|
|
171
|
-
| `code` | Syntax-highlighted snippet on a rounded panel over the background. |
|
|
172
|
-
|
|
173
|
-
Register your own by passing `templates` in config — a template is `{ name,
|
|
174
|
-
render(context) }` returning SVG foreground content, either directly or as a
|
|
175
|
-
promise. Anything you add merges over (and can override) the built-ins.
|
|
176
|
-
|
|
177
|
-
### The `code` template
|
|
178
|
-
|
|
179
|
-
Put the snippet in frontmatter and name its language:
|
|
180
|
-
|
|
181
|
-
```yaml
|
|
182
|
-
---
|
|
183
|
-
title: eslint changed TypeScript files only
|
|
184
|
-
slug: eslint-changed-ts-files-only
|
|
185
|
-
meta_img_props:
|
|
186
|
-
template: code
|
|
187
|
-
language: bash
|
|
188
|
-
code: |
|
|
189
|
-
mapfile -t CHANGED_TS < <(
|
|
190
|
-
git diff origin/main --name-only \
|
|
191
|
-
| grep '\.ts'
|
|
192
|
-
)
|
|
193
|
-
---
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
| Prop | Notes |
|
|
197
|
-
| ---------- | ---------------------------------------------------------------- |
|
|
198
|
-
| `code` | The snippet. Trimmed, tabs expanded, common indentation removed. |
|
|
199
|
-
| `language` | Any [Shiki language]; unknown names fall back to plain text. |
|
|
200
|
-
| `title` | Optional heading above the panel. Omit for a bare code image. |
|
|
201
|
-
| `theme` | Optional per-post override of `config.code.theme`. |
|
|
202
|
-
|
|
203
|
-
[Shiki language]: https://shiki.style/languages
|
|
204
|
-
|
|
205
|
-
Pygments-style names carried over from an older pipeline (`text`, `console`,
|
|
206
|
-
`html+handlebars`, …) are mapped onto their Shiki equivalents, so existing
|
|
207
|
-
frontmatter usually needs no changes.
|
|
208
|
-
|
|
209
|
-
The font size is fitted to the snippet: Colophon measures the longest line and
|
|
210
|
-
the line count against a monospace grid and picks the largest size that fits on
|
|
211
|
-
both axes, within `minFontScale`/`maxFontScale`. Those bounds are fractions of
|
|
212
|
-
the image _width_, because that is what a feed scales a share image to — a
|
|
213
|
-
landscape image would otherwise render the same snippet at half the size of its
|
|
214
|
-
square counterpart. Code too long to fit at the floor is truncated with an
|
|
215
|
-
ellipsis rather than shrunk into unreadability, and the panel then shrinks onto
|
|
216
|
-
what's left so the code isn't marooned in a larger box.
|
|
217
|
-
|
|
218
|
-
That trade matters most on the landscape sizes, which have around half the
|
|
219
|
-
vertical room of the square: at the default floor an Open Graph image fits
|
|
220
|
-
roughly nine lines of about sixty characters. Snippets written to that budget
|
|
221
|
-
render identically at every size; longer ones keep their opening lines and lose
|
|
222
|
-
the tail. Lower `minFontScale` if you would rather show the whole snippet small.
|
|
223
|
-
|
|
224
|
-
Nothing in a finished image says the sample continued, so Colophon says it for
|
|
225
|
-
you — a snippet that had to lose lines is reported through `onWarning`:
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
colophon: content/post/index.md: code snippet does not fit the 1200x630 image at
|
|
229
|
-
a legible size: 4 of 13 lines dropped. Shorten the sample, or lower
|
|
230
|
-
code.minFontScale to fit it in smaller.
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
A snippet's leading indentation is dropped before any of this, so lifting a
|
|
234
|
-
sample out of a nested block costs you no width.
|
|
235
|
-
|
|
236
|
-
Styling comes from `config.code`:
|
|
237
|
-
|
|
238
|
-
```ts
|
|
239
|
-
export default defineConfig({
|
|
240
|
-
colors: { brand: "#2563eb" },
|
|
241
|
-
footer: "example.com",
|
|
242
|
-
code: {
|
|
243
|
-
theme: "night-owl", // any bundled Shiki theme
|
|
244
|
-
fontFamily: '"JetBrains Mono", monospace',
|
|
245
|
-
charWidthRatio: 0.6, // glyph advance ÷ font size, for your monospace face
|
|
246
|
-
lineHeight: 1.55,
|
|
247
|
-
tabSize: 2,
|
|
248
|
-
cornerScale: 0.025,
|
|
249
|
-
maxFontScale: 0.075, // fractions of the image width
|
|
250
|
-
minFontScale: 0.025,
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
`charWidthRatio` is how the layout knows where each token sits, so it must match
|
|
256
|
-
the font actually used — `0.6` suits most monospace faces (Source Code Pro,
|
|
257
|
-
Menlo, DejaVu Sans Mono); Consolas wants about `0.55`. Give the monospace face
|
|
258
|
-
as a file under `fonts` so the ratio you set is the ratio you get; the default
|
|
259
|
-
stack ends in the generic `monospace` family so it resolves to something
|
|
260
|
-
whatever the machine has.
|
|
261
|
-
|
|
262
|
-
## Configuration
|
|
263
|
-
|
|
264
|
-
All fields are optional; sensible defaults apply.
|
|
265
|
-
|
|
266
|
-
| Option | Default | Notes |
|
|
267
|
-
| ------------- | ------------------------------ | --------------------------------------------------- |
|
|
268
|
-
| `colors` | neutral indigo/pink | `brand`, `brandDark`, `brandWarm`, `foreground`. |
|
|
269
|
-
| `background` | gradient derived from `colors` | Or a `{ type: "solid" }` / custom gradient. |
|
|
270
|
-
| `fonts` | none | Font files to render with (see below). |
|
|
271
|
-
| `systemFonts` | `true` until `fonts` is set | Whether installed fonts are loaded too. |
|
|
272
|
-
| `fontFamily` | first font, else `Arial, …` | Font stack for template text. |
|
|
273
|
-
| `footer` | none | Footer text; omit the field for none. |
|
|
274
|
-
| `badge` | none | Corner badge for `banner`; omit the field for none. |
|
|
275
|
-
| `code` | `github-dark`, monospace stack | Styling for the `code` template (see above). |
|
|
276
|
-
| `onWarning` | `console.warn` | Where compromises are reported (see below). |
|
|
277
|
-
| `sizes` | `og` + `square` | Named output sizes, each able to override config. |
|
|
278
|
-
| `templates` | `banner`, `card`, `code` | Merged over the built-ins. |
|
|
279
|
-
| `content` | `meta_img_props`, `.md` files | How props are read from the tree (see below). |
|
|
280
|
-
| `placement` | `beside-content` | Where images go and what URL they get (see below). |
|
|
281
|
-
| `manifest` | none | Path to write a JSON manifest to (see below). |
|
|
282
|
-
| `extra` | none | One-off images not tied to a post (see below). |
|
|
283
|
-
|
|
284
|
-
### A config that computes something
|
|
285
|
-
|
|
286
|
-
The default export can also be a function returning the config, async or not.
|
|
287
|
-
Some configs cannot be written as a literal — brand colours read out of the
|
|
288
|
-
site's own stylesheet, a version pulled from `package.json`, a footer that
|
|
289
|
-
names the current build — and without this such a project has to give up the
|
|
290
|
-
CLI and drive `generate` from a script of its own.
|
|
291
|
-
|
|
292
|
-
```ts
|
|
293
|
-
// colophon.config.ts
|
|
294
|
-
import { readFile } from "node:fs/promises";
|
|
295
|
-
|
|
296
|
-
import { defineConfig } from "@kensio/colophon";
|
|
297
|
-
|
|
298
|
-
export default defineConfig(async () => {
|
|
299
|
-
const theme = JSON.parse(await readFile("src/theme.json", "utf8"));
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
colors: { brand: theme.primary, brandDark: theme.primaryDark },
|
|
303
|
-
footer: "example.com",
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
The function takes no arguments: everything it might be handed is either
|
|
309
|
-
already on the command line or a decision the config itself is making. It is
|
|
310
|
-
called once per run, before anything is walked or rendered, and what it returns
|
|
311
|
-
is the config — validated and stamped exactly as a literal one would be.
|
|
312
|
-
|
|
313
|
-
A module that exports neither is an error rather than a run with the defaults:
|
|
314
|
-
the command line asked for that file, so quietly rendering the whole tree
|
|
315
|
-
without it would be the worst of both.
|
|
316
|
-
|
|
317
|
-
### Unknown options
|
|
318
|
-
|
|
319
|
-
The config is closed: an option Colophon does not recognise stops the build
|
|
320
|
-
rather than being ignored. A key nobody reads is otherwise a build that
|
|
321
|
-
succeeds and images that are wrong — the default sizes, the default colours,
|
|
322
|
-
and nothing in the log to say why.
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
Unknown option "dimensions". Did you mean "sizes"?
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Where there is an obvious near miss it is named, including options that have
|
|
329
|
-
been renamed between versions; where there is not, the message lists what is
|
|
330
|
-
valid at that point in the config. Nested objects are checked too and named by
|
|
331
|
-
their path — `code.tabsize`, `sizes[1].heigth`, `background.stops[0].ofset` —
|
|
332
|
-
and everything wrong with a config is reported in one go rather than one run
|
|
333
|
-
at a time.
|
|
334
|
-
|
|
335
|
-
Two parts stay open on purpose: the names under `templates` are your own, and a
|
|
336
|
-
post's props are read by whichever template understands them.
|
|
337
|
-
|
|
338
|
-
### Fonts
|
|
339
|
-
|
|
340
|
-
By default Colophon names font families and hopes the machine has them, which
|
|
341
|
-
is how the same post ends up rendering differently on a laptop, in CI and in a
|
|
342
|
-
container. Point `fonts` at font files instead and the output stops depending
|
|
343
|
-
on the machine:
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
export default defineConfig({
|
|
347
|
-
fonts: [
|
|
348
|
-
{ family: "Inter", path: "./fonts/Inter-Regular.ttf" },
|
|
349
|
-
{ path: "./fonts/Inter-Bold.ttf" },
|
|
350
|
-
{ path: "./fonts/JetBrainsMono-Regular.ttf" },
|
|
351
|
-
],
|
|
352
|
-
code: { fontFamily: "JetBrains Mono" },
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
- **One entry per file.** Weight and style are read from the font itself, so a
|
|
357
|
-
regular and a bold face are two entries and the template's `font-weight`
|
|
358
|
-
picks between them. Supply the bold face: templates ask for weights up to
|
|
359
|
-
`900` for titles and badges, and a missing weight is drawn with the face you
|
|
360
|
-
did supply rather than being synthesised into a fake bold.
|
|
361
|
-
- **`family` is optional** and doesn't affect matching — the family name inside
|
|
362
|
-
the file does that. Naming it on the first font saves setting `fontFamily`,
|
|
363
|
-
which otherwise stays on the default stack.
|
|
364
|
-
- **Paths are files**, `.ttf`, `.otf`, `.ttc` or `.otc`, resolved from the
|
|
365
|
-
working directory when relative. A path that isn't there is an error rather
|
|
366
|
-
than a silently blank image. To load a font you already have in memory —
|
|
367
|
-
fetched at build time, or bundled — pass `{ data }` with its bytes instead.
|
|
368
|
-
- **System fonts switch off** as soon as you configure any font, so a family
|
|
369
|
-
you didn't supply can't quietly resolve to something installed. Set
|
|
370
|
-
`systemFonts: true` to have both, at the cost of the determinism you came
|
|
371
|
-
for.
|
|
372
|
-
|
|
373
|
-
An unknown family falls back to a configured font rather than rendering
|
|
374
|
-
nothing, so a mismatched name shows up as the wrong typeface, not a blank
|
|
375
|
-
image.
|
|
376
|
-
|
|
377
|
-
### Warnings
|
|
378
|
-
|
|
379
|
-
Some inputs can't be honoured exactly — code too long to render legibly, so far.
|
|
380
|
-
Colophon renders anyway and reports the compromise through `onWarning`, which
|
|
381
|
-
defaults to `console.warn`. Pass your build's logger to route them, or a no-op
|
|
382
|
-
to silence them:
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
export default defineConfig({
|
|
386
|
-
onWarning: () => {},
|
|
387
|
-
});
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
`generate` prefixes each message with the content file it came from, so a build
|
|
391
|
-
over a whole tree still names the post to fix.
|
|
80
|
+
output size next to it, named `<slug>-<size>.png`, so `post/index.md` produces
|
|
81
|
+
`post/post-og.png` and `post/post-square.png`.
|
|
392
82
|
|
|
393
|
-
|
|
83
|
+
There is also a programmatic API. `renderMetaImages` takes props and config and
|
|
84
|
+
returns rendered bytes, and `generate` ties walking, rendering and writing
|
|
85
|
+
together.
|
|
394
86
|
|
|
395
|
-
|
|
396
|
-
filename suffix, so every image is distinct: `my-post-og.png`,
|
|
397
|
-
`my-post-square.png`. The default set is one 1.91:1 Open Graph landscape and one
|
|
398
|
-
1:1 square, which between them satisfy `og:image` and both `twitter:image` card
|
|
399
|
-
types (`summary_large_image` reuses the landscape; `summary` uses the square).
|
|
87
|
+
## Documentation
|
|
400
88
|
|
|
401
|
-
|
|
89
|
+
Full documentation is in [`docs/`](./docs/).
|
|
402
90
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{ name: "hero", width: 1600, height: 900 }, // or anything custom
|
|
413
|
-
],
|
|
414
|
-
});
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
The base filename is the **post slug**: Colophon reads a top-level `slug` from
|
|
418
|
-
frontmatter (SEO-friendly, keyword-rich), falling back to the file name — or the
|
|
419
|
-
parent directory for `index.*` files. Point `slugField` at a different key, or
|
|
420
|
-
override naming entirely with `generate`'s `outputPath` callback.
|
|
421
|
-
|
|
422
|
-
#### Slug strategies
|
|
423
|
-
|
|
424
|
-
Two ways to derive a slug from a path, for sites that address content
|
|
425
|
-
differently. `basename` is the default and unchanged:
|
|
426
|
-
|
|
427
|
-
| Path | `basename` | `route` |
|
|
428
|
-
| ----------------------- | ---------- | -------------- |
|
|
429
|
-
| `index.md` | `index` | `index` |
|
|
430
|
-
| `blog/my-post.md` | `my-post` | `blog/my-post` |
|
|
431
|
-
| `services/iam/index.md` | `iam` | `services/iam` |
|
|
432
|
-
|
|
433
|
-
`basename` suits Hugo-style page bundles, where the image belongs beside its
|
|
434
|
-
post. `route` suits a site addressed by route — a docs tree where
|
|
435
|
-
`services/iam/index.md` is served at `/services/iam` and wants an image named to
|
|
436
|
-
match:
|
|
437
|
-
|
|
438
|
-
```ts
|
|
439
|
-
export default defineConfig({
|
|
440
|
-
content: { slugStrategy: "route" },
|
|
441
|
-
});
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
A slug carrying directories is written from the **content root** rather than
|
|
445
|
-
beside the file, so `services/iam` becomes `content/services/iam-og.png`.
|
|
446
|
-
Resolving it beside the file would repeat the directories already in the slug.
|
|
447
|
-
A frontmatter `slug` still wins over either strategy.
|
|
448
|
-
|
|
449
|
-
### Placement
|
|
450
|
-
|
|
451
|
-
`outputPath` says where the bytes go and nothing about how anyone reaches them,
|
|
452
|
-
so every site rebuilds that mapping in its own templates — from information
|
|
453
|
-
Colophon had while generating and threw away. A placement says both:
|
|
454
|
-
|
|
455
|
-
```ts
|
|
456
|
-
export default defineConfig({
|
|
457
|
-
// Astro, Eleventy, Vite: one directory, served under one prefix.
|
|
458
|
-
placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og" },
|
|
459
|
-
});
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
```text
|
|
463
|
-
wrote public/og/my-post-og.png -> /og/my-post-og.png
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
| Strategy | Writes | Suits |
|
|
467
|
-
| ---------------- | ------------------------------------- | ----------------------- |
|
|
468
|
-
| `beside-content` | Next to the post, as it always has | Hugo-style page bundles |
|
|
469
|
-
| `public-dir` | Into `dir`, one directory for the lot | Astro, Eleventy, Vite |
|
|
470
|
-
| `custom` | Wherever `path` says | Anything else |
|
|
471
|
-
|
|
472
|
-
The URL comes from `urlBase`, prefixed to the image's path under whatever root
|
|
473
|
-
placed it. **No `urlBase`, no URL** — a directory on disk does not say how, or
|
|
474
|
-
whether, it is served, and a URL Colophon invented would be worse than the gap
|
|
475
|
-
it fills. It can be site-relative (`/og`) or absolute, for images on a CDN.
|
|
476
|
-
|
|
477
|
-
`custom` works both halves out itself, for a mapping that is nobody else's —
|
|
478
|
-
images under a dated directory, say:
|
|
479
|
-
|
|
480
|
-
```ts
|
|
481
|
-
placement: {
|
|
482
|
-
strategy: "custom",
|
|
483
|
-
path: (file, size) => `public/og/2026/${file.slug}-${size.name}.png`,
|
|
484
|
-
url: (file, size) => `/og/2026/${file.slug}-${size.name}.png`,
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
Each result carries the URL as `result.url`, `undefined` where nothing says:
|
|
489
|
-
no `urlBase`, an image placed by `generate`'s `outputPath` callback (which
|
|
490
|
-
still wins, and then the placement no longer describes where the file went), or
|
|
491
|
-
an `extra` that named its own path.
|
|
492
|
-
|
|
493
|
-
#### Content hashed filenames
|
|
494
|
-
|
|
495
|
-
Social platforms cache share images hard, and they cache by **URL**. Correct a
|
|
496
|
-
post's image and the old one can keep turning up in feeds for a long time
|
|
497
|
-
afterwards. A hash in the filename is the reliable way round it:
|
|
498
|
-
|
|
499
|
-
```ts
|
|
500
|
-
placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og", hash: true }
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
```text
|
|
504
|
-
wrote public/og/my-post-og.ecd0aab2.png -> /og/my-post-og.ecd0aab2.png
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
Correct the post and the name moves with it, so the URL is one nothing has
|
|
508
|
-
cached:
|
|
509
|
-
|
|
510
|
-
```text
|
|
511
|
-
wrote public/og/my-post-og.2e7bd5a9.png -> /og/my-post-og.2e7bd5a9.png
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
The hash is the image's [rebuild stamp](#rebuilds) — its props, config and
|
|
515
|
-
size. Hashing the rendered bytes would be a truer name, but they are not known
|
|
516
|
-
until the image has been rendered, and not rendering the unchanged ones is the
|
|
517
|
-
point of the stamp. It follows that anything the stamp covers moves the name,
|
|
518
|
-
including a Colophon upgrade: the images are re-rendered by that anyway, and a
|
|
519
|
-
fresh URL is the correct answer for an image that may have changed.
|
|
520
|
-
|
|
521
|
-
It is opt-in because the filename then moves whenever the image does, which not
|
|
522
|
-
every setup wants, and because it leaves the old files behind. **Nothing deletes
|
|
523
|
-
them** — which is the point under a `public-dir` you can rebuild from scratch,
|
|
524
|
-
since a crawler holding the old URL still gets an image, but means a
|
|
525
|
-
`beside-content` tree slowly accumulates them in your content directory. The
|
|
526
|
-
manifest always names the current one, so a site never has to work out which is
|
|
527
|
-
which.
|
|
528
|
-
|
|
529
|
-
`custom` has no `hash` option: a placement naming its own paths is the one that
|
|
530
|
-
can hash them itself.
|
|
531
|
-
|
|
532
|
-
A flat placement makes filename collisions much easier to hit — two posts named
|
|
533
|
-
`intro.md` in different sections both want `public/og/intro-og.png`. Colophon
|
|
534
|
-
refuses the build and names both posts rather than letting one overwrite the
|
|
535
|
-
other, which would also leave the pair re-rendering on every build. Pair
|
|
536
|
-
`public-dir` with `slugStrategy: "route"` and each post keeps its section:
|
|
537
|
-
|
|
538
|
-
```ts
|
|
539
|
-
export default defineConfig({
|
|
540
|
-
content: { slugStrategy: "route" },
|
|
541
|
-
placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og" },
|
|
542
|
-
});
|
|
543
|
-
// public/og/blog/intro-og.png -> /og/blog/intro-og.png
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### Manifest
|
|
547
|
-
|
|
548
|
-
Colophon writes PNGs and then goes quiet, so the site works out for itself what
|
|
549
|
-
was generated, where it lives and how big it is — globbing for `*-og.png` to
|
|
550
|
-
find the landscape variant, or hardcoding 1200 and 630 into its meta tags. All
|
|
551
|
-
of that is known while generating. Point `manifest` at a file and it is written
|
|
552
|
-
down:
|
|
553
|
-
|
|
554
|
-
```ts
|
|
555
|
-
export default defineConfig({
|
|
556
|
-
placement: { strategy: "public-dir", dir: "public/og", urlBase: "/og" },
|
|
557
|
-
manifest: "data/colophon.json", // src/data/ for Astro, _data/ for Eleventy
|
|
558
|
-
});
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
```json
|
|
562
|
-
{
|
|
563
|
-
"version": 1,
|
|
564
|
-
"pages": {
|
|
565
|
-
"blog/my-post": {
|
|
566
|
-
"images": {
|
|
567
|
-
"og": {
|
|
568
|
-
"url": "/og/blog/my-post-og.png",
|
|
569
|
-
"width": 1200,
|
|
570
|
-
"height": 630
|
|
571
|
-
},
|
|
572
|
-
"square": {
|
|
573
|
-
"url": "/og/blog/my-post-square.png",
|
|
574
|
-
"width": 1200,
|
|
575
|
-
"height": 1200
|
|
576
|
-
}
|
|
577
|
-
},
|
|
578
|
-
"widest": "og",
|
|
579
|
-
"alt": "My post"
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
Every generator in scope reads JSON as native data: Hugo picks it up from
|
|
586
|
-
`data/`, Astro imports it, Eleventy and Jekyll read `_data/`, Zola has
|
|
587
|
-
`load_data`. Which makes the meta tags a lookup rather than a convention:
|
|
588
|
-
|
|
589
|
-
```ts
|
|
590
|
-
const page = manifest.pages["blog/my-post"];
|
|
591
|
-
const image = page.images[page.widest];
|
|
592
|
-
// <meta property="og:image" content={image.url}>
|
|
593
|
-
// <meta property="og:image:width" content={image.width}>
|
|
594
|
-
// <meta name="twitter:card" content={image.width / image.height > 1.5
|
|
595
|
-
// ? "summary_large_image" : "summary"}>
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
- **Pages are keyed by slug** — what the site addresses a page by, which under
|
|
599
|
-
`slugStrategy: "route"` is the route itself. Two pages cannot share one, and
|
|
600
|
-
a build that would need them to fails saying so, since a lookup that returned
|
|
601
|
-
the wrong post's image is worse than no manifest at all.
|
|
602
|
-
- **`widest`** names the most landscape image by aspect ratio, ties going to
|
|
603
|
-
the size configured first. It is what a `summary_large_image` card wants, and
|
|
604
|
-
the check every site currently writes for itself. Note that og (1200×630) and
|
|
605
|
-
square (1200×1200) are equally _wide_, so comparing widths would pick either.
|
|
606
|
-
- **`url`** is absent where the placement knows none — see
|
|
607
|
-
[Placement](#placement). The dimensions are always there.
|
|
608
|
-
- **`alt`** comes from the props' title, and is absent for a page without one.
|
|
609
|
-
- **`extra` images are not pages**, so they are not listed. A project that named
|
|
610
|
-
the output path of one already knows where it is.
|
|
611
|
-
|
|
612
|
-
The manifest describes what exists, not what a given run did: a rebuild that
|
|
613
|
-
skips every image still writes the whole thing. Pages and sizes are sorted, so
|
|
614
|
-
a manifest committed to a repository changes only when the build does.
|
|
615
|
-
|
|
616
|
-
### Social meta tags
|
|
617
|
-
|
|
618
|
-
Generating the image is half the job — the site still has to write the tags,
|
|
619
|
-
and every site ends up with slightly different results. Given the manifest,
|
|
620
|
-
that is a lookup:
|
|
621
|
-
|
|
622
|
-
```ts
|
|
623
|
-
import { metaTags, metaTagsHtml } from "@kensio/colophon/meta";
|
|
624
|
-
import manifest from "./data/colophon.json";
|
|
625
|
-
|
|
626
|
-
const site = { baseUrl: "https://example.com" };
|
|
627
|
-
|
|
628
|
-
metaTagsHtml(manifest, "blog/my-post", site);
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
```html
|
|
632
|
-
<meta
|
|
633
|
-
property="og:image"
|
|
634
|
-
content="https://example.com/og/blog/my-post-og.png"
|
|
635
|
-
/>
|
|
636
|
-
<meta property="og:image:width" content="1200" />
|
|
637
|
-
<meta property="og:image:height" content="630" />
|
|
638
|
-
<meta property="og:image:alt" content="My post" />
|
|
639
|
-
<meta name="twitter:card" content="summary_large_image" />
|
|
640
|
-
<meta
|
|
641
|
-
name="twitter:image"
|
|
642
|
-
content="https://example.com/og/blog/my-post-og.png"
|
|
643
|
-
/>
|
|
644
|
-
<meta name="twitter:image:alt" content="My post" />
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
`metaTags` returns the same set as objects, for a component that spreads them
|
|
648
|
-
— Open Graph names its tags with `property` and Twitter with `name`, so the
|
|
649
|
-
type keeps them apart and `<meta {...tag} />` is right either way:
|
|
650
|
-
|
|
651
|
-
```jsx
|
|
652
|
-
{
|
|
653
|
-
metaTags(manifest, slug, site).map((tag) => <meta {...tag} />);
|
|
654
|
-
}
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
- **The card type follows the image.** `summary_large_image` when the page's
|
|
658
|
-
widest image is at least 1.5:1 — which the 1.91:1 Open Graph landscape and
|
|
659
|
-
the 2:1 Twitter size both clear — and `summary` otherwise, since a square
|
|
660
|
-
shown as a large card is cropped. Sites usually hardcode whichever answer
|
|
661
|
-
suited the image they had.
|
|
662
|
-
- **`baseUrl` makes the URL absolute**, which Open Graph needs: a crawler reads
|
|
663
|
-
the tag out of the page and has nothing to resolve a relative URL against. A
|
|
664
|
-
URL that is already absolute, from a CDN `urlBase`, is left alone.
|
|
665
|
-
- **Alt text goes to both platforms.** Twitter reads `twitter:image:alt` rather
|
|
666
|
-
than falling back to the Open Graph one, so emitting only the latter means no
|
|
667
|
-
alt text where most of the sharing happens.
|
|
668
|
-
- **A page with no share image gets no tags** — an empty array, an empty string
|
|
669
|
-
— rather than an error. Not every page has one, and a template asking should
|
|
670
|
-
not have to know which in advance.
|
|
671
|
-
|
|
672
|
-
Import it from `@kensio/colophon/meta` rather than the package root: emitting
|
|
673
|
-
tags reads a JSON file, and a site's templates should not load a rasteriser and
|
|
674
|
-
a syntax highlighter to write a `<head>`.
|
|
675
|
-
|
|
676
|
-
### Per-size config
|
|
677
|
-
|
|
678
|
-
Some settings only make sense per size. `code.minFontScale` is the clearest
|
|
679
|
-
case: a 1:1 square and a 1.91:1 landscape have very different amounts of
|
|
680
|
-
vertical room, so a snippet that fits one gets truncated in the other. A size
|
|
681
|
-
can carry its own overrides, applied only when rendering it:
|
|
682
|
-
|
|
683
|
-
```ts
|
|
684
|
-
export default defineConfig({
|
|
685
|
-
colors: { brand: "#2563eb" },
|
|
686
|
-
code: { theme: "github-dark" },
|
|
687
|
-
sizes: [
|
|
688
|
-
SIZE_PRESETS.square,
|
|
689
|
-
{ ...SIZE_PRESETS.og, code: { minFontScale: 0.013 } },
|
|
690
|
-
],
|
|
691
|
-
});
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
One pass over the content tree, one config file. The alternative is running
|
|
695
|
-
`generate` once per size with a different config each time, which re-walks and
|
|
696
|
-
re-parses everything for each.
|
|
697
|
-
|
|
698
|
-
Overridable: `colors`, `background`, `fontFamily`, `footer`, `badge`, `code` —
|
|
699
|
-
what a template reads while drawing. Not overridable: `fonts`, `systemFonts` and
|
|
700
|
-
`templates`, which are shared build inputs rather than part of the picture, and
|
|
701
|
-
`onWarning`, which is where messages go rather than what they say. A size naming
|
|
702
|
-
one of those is an unknown-option error, not a setting that quietly does
|
|
703
|
-
nothing. `fontFamily` is overridable because it picks from the fonts already
|
|
704
|
-
loaded; supplying different font _files_ per size is not the same thing.
|
|
705
|
-
|
|
706
|
-
`colors` and `code` **merge** over their config-level counterparts, so the
|
|
707
|
-
example above keeps `github-dark` and changes only the minimum font size. Any
|
|
708
|
-
single shade can be overridden on its own — `colors: { foreground: "#111827" }`
|
|
709
|
-
on one size keeps the brand palette and changes just the text colour. The
|
|
710
|
-
rest **replace**: a `background` is a union whose variants have different keys,
|
|
711
|
-
so merging half of one onto half of another would produce a background that is
|
|
712
|
-
neither, and `badge` carries a required `text` a partial override could not
|
|
713
|
-
supply.
|
|
714
|
-
|
|
715
|
-
Overrides are part of an image's rebuild stamp, so changing one re-renders that
|
|
716
|
-
size and leaves the others alone.
|
|
717
|
-
|
|
718
|
-
### One-off images
|
|
719
|
-
|
|
720
|
-
Not every image belongs to a post. A package card, a repository social preview
|
|
721
|
-
and a home page share image all want the same brand and the same templates, and
|
|
722
|
-
none of them has a markdown file behind it. List them under `extra` and the
|
|
723
|
-
build renders them alongside the tree:
|
|
724
|
-
|
|
725
|
-
```ts
|
|
726
|
-
export default defineConfig({
|
|
727
|
-
colors: { brand: "#2563eb" },
|
|
728
|
-
footer: "example.com",
|
|
729
|
-
extra: [
|
|
730
|
-
{
|
|
731
|
-
props: {
|
|
732
|
-
template: "banner",
|
|
733
|
-
title: "@kensio/colophon",
|
|
734
|
-
version: "2.0.0",
|
|
735
|
-
},
|
|
736
|
-
output: "public/npm-card.png",
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
props: {
|
|
740
|
-
template: "card",
|
|
741
|
-
title: "colophon",
|
|
742
|
-
subtitle: "social meta images",
|
|
743
|
-
},
|
|
744
|
-
output: "public/repo-preview.png",
|
|
745
|
-
size: {
|
|
746
|
-
name: "repo",
|
|
747
|
-
width: 1280,
|
|
748
|
-
height: 640,
|
|
749
|
-
footer: "github.com/KensioSoftware/colophon",
|
|
750
|
-
},
|
|
751
|
-
},
|
|
752
|
-
],
|
|
753
|
-
});
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
`output` is the path to write, relative to the working directory, and any
|
|
757
|
-
directories it names are created. It is the whole path: an extra image has no
|
|
758
|
-
post to sit beside, so `generate`'s `outputPath` callback is not consulted and
|
|
759
|
-
nothing is appended to the filename. An extra that would land on another image
|
|
760
|
-
in the same build stops it before anything is written — two images sharing a
|
|
761
|
-
path do not merely lose one of themselves, they each stamp the file and so
|
|
762
|
-
re-render on every build afterwards.
|
|
763
|
-
|
|
764
|
-
`size` is an output size like any other, [overrides](#per-size-config) and all —
|
|
765
|
-
that is how the preview above gets its own footer without adding an entry to
|
|
766
|
-
`sizes` that every post would then be rendered at. Leave it out and the image
|
|
767
|
-
takes the first configured size, which for the card above is the default `og`
|
|
768
|
-
1200×630.
|
|
769
|
-
|
|
770
|
-
Extras are stamped and skipped exactly as content images are, so editing one
|
|
771
|
-
card's title re-renders that card and leaves the rest of the build alone. They
|
|
772
|
-
are reported by `onResult` too, with `contentPath` left `undefined`: there is no
|
|
773
|
-
post behind them to name.
|
|
774
|
-
|
|
775
|
-
### Frontmatter shape
|
|
776
|
-
|
|
777
|
-
By default Colophon reads a `meta_img_props` object and a `template` field
|
|
778
|
-
within it, plus a top-level `slug`. All are configurable under `content`
|
|
779
|
-
(`propsKey`, `templateField`, `defaultTemplate`, `slugField`, `slugStrategy`,
|
|
780
|
-
`extensions`) so you can match an existing convention.
|
|
781
|
-
|
|
782
|
-
### Using the frontmatter you already have
|
|
783
|
-
|
|
784
|
-
A site with 200 existing posts gets no images until someone adds a props block
|
|
785
|
-
to 200 files. Most posts already carry the fields an image needs, just under
|
|
786
|
-
different names — so map them instead:
|
|
787
|
-
|
|
788
|
-
```ts
|
|
789
|
-
export default defineConfig({
|
|
790
|
-
content: {
|
|
791
|
-
defaultTemplate: "banner",
|
|
792
|
-
props: (frontmatter) =>
|
|
793
|
-
frontmatter.draft === true
|
|
794
|
-
? undefined
|
|
795
|
-
: { title: frontmatter.title, subtitle: frontmatter.description },
|
|
796
|
-
},
|
|
797
|
-
});
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
Point it at your content directory and the site gets its images, without
|
|
801
|
-
editing a single post.
|
|
802
|
-
|
|
803
|
-
**Returning `undefined` skips a post.** That is the filter for drafts, section
|
|
804
|
-
indexes and anything else in the tree that is not a page worth sharing —
|
|
805
|
-
without it, mapping frontmatter means an image for every markdown file there
|
|
806
|
-
is.
|
|
807
|
-
|
|
808
|
-
**An explicit props block still wins, field by field.** A post that wants a
|
|
809
|
-
different subtitle writes just that:
|
|
810
|
-
|
|
811
|
-
```yaml
|
|
812
|
-
---
|
|
813
|
-
title: Colophon 2.3.0
|
|
814
|
-
description: Autogenerated release notes
|
|
815
|
-
meta_img_props:
|
|
816
|
-
subtitle: Per-size config, frontmatter mapping
|
|
817
|
-
---
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
The title still comes from the mapper; only the subtitle is overridden. A post
|
|
821
|
-
declaring a block is never skipped, even if the mapper would have skipped it —
|
|
822
|
-
asking for an image outright is the stronger signal.
|
|
823
|
-
|
|
824
|
-
`content` lives in the config module because a `props` mapper is a function and
|
|
825
|
-
cannot be passed as a CLI flag. `generate`'s `walk` option is the programmatic
|
|
826
|
-
equivalent and takes precedence where both are given.
|
|
91
|
+
- [Getting started](./docs/getting-started/ "Install, frontmatter, config and the CLI")
|
|
92
|
+
- [Templates](./docs/templates/ "The built-in layouts and how to register your own")
|
|
93
|
+
- [The code template](./docs/code-template/ "Syntax-highlighted code images")
|
|
94
|
+
- [The layout toolkit](./docs/layout/ "The primitives templates are built from")
|
|
95
|
+
- [Logos and photographs](./docs/configuration/images/ "Branding an image with a logo, avatar or background photo")
|
|
96
|
+
- [Configuration](./docs/configuration/ "Every option, and what happens to an unknown one")
|
|
97
|
+
- [Rebuilds](./docs/rebuilds/ "How Colophon decides what to render again")
|
|
98
|
+
- [Programmatic use](./docs/programmatic-use/ "The API behind the CLI")
|
|
99
|
+
- [Upgrading](./docs/upgrading/ "The breaking changes in 2.0 and 3.0")
|
|
827
100
|
|
|
828
101
|
## Sample output
|
|
829
102
|
|
|
830
|
-
These are generated by [`scripts/gen-samples.ts`](scripts/gen-samples.ts)
|
|
103
|
+
These are generated by [`scripts/gen-samples.ts`](scripts/gen-samples.ts). Run
|
|
831
104
|
`pnpm samples` to regenerate them after changing a template, then commit the
|
|
832
105
|
updated PNGs so this gallery stays in sync.
|
|
833
106
|
|
|
@@ -864,29 +137,17 @@ updated PNGs so this gallery stays in sync.
|
|
|
864
137
|
</tr>
|
|
865
138
|
</table>
|
|
866
139
|
|
|
867
|
-
## Upgrading from 1.x
|
|
868
|
-
|
|
869
|
-
Adding the `code` template made two small breaking changes:
|
|
870
|
-
|
|
871
|
-
- `Template.render` may now return `string | Promise<string>`, and `buildSvg`
|
|
872
|
-
is `async`. Custom templates that return a string still work unchanged; call
|
|
873
|
-
sites of `buildSvg` need an `await`. `renderMetaImages` and `generate` were
|
|
874
|
-
already async and are unaffected.
|
|
875
|
-
- `MetaImageProps.title` is optional, and `walkContent`/`extractProps` no
|
|
876
|
-
longer skip a file that declares props without a title — a `code` post
|
|
877
|
-
describes its image entirely through `code` and `language`.
|
|
878
|
-
|
|
879
140
|
## Development
|
|
880
141
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
142
|
+
| Script | What it does |
|
|
143
|
+
| --------------------------------- | -------------------------------------------------------------------------- |
|
|
144
|
+
| `pnpm build` | Compile to `dist/`. |
|
|
145
|
+
| `pnpm test`, `pnpm test:coverage` | Run Vitest. |
|
|
146
|
+
| `pnpm lint` | ESLint and Prettier check. |
|
|
147
|
+
| `pnpm fmt` | Auto-fix. |
|
|
148
|
+
| `pnpm samples` | Regenerate the sample images into `docs/samples/`. |
|
|
149
|
+
| `pnpm fta` | [FTA](https://ftaproject.dev) scores, failing on any file 50 or above. |
|
|
150
|
+
| `pnpm check` | Format, FTA, typecheck and test with coverage. Run this before committing. |
|
|
890
151
|
|
|
891
152
|
## License
|
|
892
153
|
|