Skip to content

Commit b704d66

Browse files
committed
fixing social cards
1 parent fe57c47 commit b704d66

39 files changed

Lines changed: 622 additions & 160 deletions

‎.github/workflows/deploy-documents.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ jobs:
101101
- name: Build
102102
run: pnpm build
103103

104+
- name: Render the social cards
105+
# dist/og.png and one card for each page name (dist/og/<slug>.png), in
106+
# the layout of the social cards of phalcon/assets
107+
# (scripts/render-og-cards.mjs, resources/og/card.html). The Puppeteer
108+
# image is the engine of the assets cards; Chrome keeps its profile in
109+
# node_modules/.cache/og.
110+
run: >-
111+
docker run --rm --shm-size=2g -u "$(id -u):$(id -g)"
112+
-e HOME=/docs/node_modules/.cache/og -e TMPDIR=/docs/node_modules/.cache/og
113+
-e PUPPETEER_CACHE_DIR=/home/pptruser/.cache/puppeteer
114+
-v "$PWD:/docs" -w /docs ghcr.io/puppeteer/puppeteer:25.12.0 node scripts/render-og-cards.mjs
115+
116+
- name: Check the social cards
117+
# Every og:image and twitter:image of every page is a file of dist/.
118+
run: node scripts/check-og.mjs
119+
104120
- name: Publish to the deploy branch
105121
# dist/_redirects (written by the cloudflare-redirects hook in
106122
# astro.config.ts) carries /latest -> the stable version, so the

‎AGENT.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ src/
4646
│ ├── <version>/[...slug]/index.md.ts # GENERATED markdown twin, /<version>/<slug>/index.md
4747
│ ├── [...slug].astro, [...slug]/ # the empty primary collection
4848
│ ├── [section]/llms.txt.ts # per-version agent index, /<version>/llms.txt
49-
│ ├── llms.txt.ts, llms-full.txt.ts, robots.txt.ts, og.png.ts, og/, 404.astro
49+
│ ├── llms.txt.ts, llms-full.txt.ts, robots.txt.ts, 404.astro
5050
├── layouts/ # BaseLayout (head, fan art, AgentDirective), DocsLayout
5151
├── lib/
5252
│ ├── site.mjs # STABLE_VERSIONS, PRERELEASES, DEPRECATED, versionBanner, analytics
@@ -60,7 +60,8 @@ src/
6060
6161
public/assets/images/ # images, shared by all versions, referenced as /assets/images/...
6262
resources/nimbus/ # convert.py, its tests, templates, per-version overrides, parity.sh
63-
scripts/ # new-version.sh, update-nfr.mjs, update-tokens.mjs (the design files), token-sources.mjs (the tokens that src/ uses)
63+
resources/og/card.html # the layout of the social cards (the cards of phalcon/assets), see README.md
64+
scripts/ # new-version.sh, update-nfr.mjs, update-tokens.mjs (the design files), token-sources.mjs (the tokens that src/ uses), og-cards.mjs, render-og-cards.mjs, check-og.mjs (the social cards)
6465
patches/ # nimbus patch that allows the dot in `docs-5.20`
6566
```
6667

‎README.md‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,20 @@ docker run --rm -v "$PWD":/docs phalcon-docs-converter resources/nimbus/convert.
8989

9090
### Deployment
9191

92-
A push to `master` builds the site and publishes `dist/` to the branch named by `DEPLOY_BRANCH` in the workflow (`production`), which Cloudflare Pages serves. The `gh-pages` branch holds the last MkDocs deployment as the rollback: to roll back, point the Cloudflare Pages production branch at `gh-pages`. The workflow also points the `/latest/` redirect rule at the stable version.
92+
A push to `master` builds the site and publishes `dist/` to the branch named by `DEPLOY_BRANCH` in the workflow (`production`), which Cloudflare Pages serves. The `gh-pages` branch holds the last MkDocs deployment as the rollback: to roll back, point the Cloudflare Pages production branch at `gh-pages`. The build writes `dist/_redirects` (the `cloudflare-redirects` hook in `astro.config.ts`): `/`, `/latest` and `/latest/*` lead to the first entry of `STABLE_VERSIONS`, so a deploy moves them with no Cloudflare rule and no secret. The workflow needs no secret other than the `GITHUB_TOKEN` of the run.
93+
94+
### Social cards
95+
96+
Every page has a social card (`og:image`, `twitter:image`): one card for each page name, the same for every version, at `/og/<slug>.png`, and the site card at `/og.png`. The layout is the one of the social cards of [phalcon/assets](https://github.com/phalcon/assets) (`resources/og/card.html`, on the design tokens and the falcon). The title of a card comes from the stable version, or from the newest version that has the page. After `pnpm build`, the deploy renders the cards into `dist/` with `scripts/render-og-cards.mjs` in the Puppeteer image, then `scripts/check-og.mjs` checks that the card of every page is a file of `dist/`. Locally, after a build:
97+
98+
```bash
99+
docker run --rm --shm-size=2g -u "$(id -u):$(id -g)" -e HOME=/docs/node_modules/.cache/og \
100+
-e TMPDIR=/docs/node_modules/.cache/og -e PUPPETEER_CACHE_DIR=/home/pptruser/.cache/puppeteer \
101+
-v "$PWD":/docs -w /docs ghcr.io/puppeteer/puppeteer:25.12.0 node scripts/render-og-cards.mjs
102+
docker run --rm -v "$PWD":/docs phalcon-docs node scripts/check-og.mjs
103+
```
104+
105+
A card keeps its address from build to build: a platform (Discord, X, LinkedIn) keeps a preview image by its address, so a new layout, a new title or a new token shows there only for the pages that it did not see before. To make the platforms fetch the new cards, add a version to the card address in `resources/nimbus/templates/slug.astro.tpl` (for example `/og/${entry.id}.png?v=2`) and run `convert.py --register`; the check accepts a query.
93106

94107
### Colors and fonts
95108

@@ -98,7 +111,7 @@ The colors and the fonts come from `phalcon/css/tokens.css` in [phalcon/assets](
98111
- To change a color, change `tokens.css` in phalcon/assets. The docs get it on their next deploy.
99112
- To get the new files now, for a local preview or to commit them: `docker run --rm -v "$PWD":/docs phalcon-docs node scripts/update-tokens.mjs`.
100113
- The checks and the refresh are the shared design tools of phalcon/assets: `src/lib/design-checks.mjs` and `src/lib/design-refresh.mjs` are copies of `phalcon/tools/` there, and every deploy gets them again first. Change them in phalcon/assets, not here.
101-
- `src/styles/globals.css` maps the nimbus tokens (`--nb-*`) to the design tokens: `--ph-light-*` in the light theme, `--ph-dark-*` in the dark theme. Use a color through a token: `var(--nb-…)` or `var(--ph-…)`. `pnpm test` fails on a typed color (`#…`, `rgb(…)`, `oklch(…)` or a color of Tailwind's palette) in `src/`, except in `src/scripts/mermaid.ts` and `src/pages/og/_og-card-config.ts`.
114+
- `src/styles/globals.css` maps the nimbus tokens (`--nb-*`) to the design tokens: `--ph-light-*` in the light theme, `--ph-dark-*` in the dark theme. Use a color through a token: `var(--nb-…)` or `var(--ph-…)`. `pnpm test` fails on a typed color (`#…`, `rgb(…)`, `oklch(…)` or a color of Tailwind's palette) in `src/`, except in `src/scripts/mermaid.ts`.
102115
- Code blocks use the code theme (set in `astro.config.ts`): the rules of GitHub's dark theme with `--code-<role>` variables. `globals.css` gives them the syntax tokens of each theme.
103116

104117
## Community

‎package.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
"@cloudflare/nimbus-docs": "0.11.0",
1919
"@iconify-json/ph": "^1.2.0",
2020
"astro": "~7.0.9",
21-
"astro-og-canvas": "^0.13.0",
22-
"canvaskit-wasm": "^0.41.1",
2321
"clsx": "^2.1.1",
2422
"mermaid": "^11.17.2",
2523
"tailwind-merge": "^3.5.0"

‎pnpm-lock.yaml‎

Lines changed: 0 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎public/fonts/Inter-Bold.ttf‎

-411 KB
Binary file not shown.

‎resources/nimbus/templates/slug.astro.tpl‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const tocConfig = entry.data.tableOfContents;
4545
const toc = tocOn && tocConfig !== false ? getTOC(headings, tocConfig) : false;
4646
const markdownPath = `/${VERSION}/${entry.id}/index.md`;
4747
const markdownUrl = Astro.site ? new URL(markdownPath, Astro.site).href : markdownPath;
48-
const socialImage = entry.data.socialImage ?? `/og/${VERSION}/${entry.id}.png`;
48+
const socialImage = entry.data.socialImage ?? `/og/${entry.id}.png`;
4949
// A page of a non-stable version carries the version banner unless the
5050
// page sets its own (STABLE_VERSIONS and PRERELEASES in src/lib/site.mjs).
5151
const banner = entry.data.banner ?? versionBanner(VERSION) ?? undefined;

‎resources/og/card.html‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<!doctype html>
2+
<!--
3+
A social card of the docs (1200 x 630, the og:image of the pages). The layout of
4+
the social cards of phalcon/assets (scripts/social/template.html there).
5+
scripts/render-og-cards.mjs fills the fields, puts in the design tokens
6+
(src/styles/tokens.css) and the falcon (public/assets/images/falcon.svg), and
7+
writes the PNG.
8+
-->
9+
<html lang="en">
10+
<head>
11+
<meta charset="utf-8">
12+
<style>{{tokens}}</style>
13+
<style>
14+
html, body { margin: 0; }
15+
.card {
16+
background:
17+
radial-gradient(circle at 78% 52%, color-mix(in srgb, var(--ph-brand-400) 16%, transparent) 0, transparent 40%),
18+
var(--ph-night-950);
19+
box-sizing: border-box;
20+
color: var(--ph-mist-100);
21+
font-family: var(--ph-font-sans);
22+
height: 630px;
23+
overflow: hidden;
24+
padding: 68px 76px;
25+
position: relative;
26+
width: 1200px;
27+
}
28+
.kicker {
29+
color: var(--ph-dark-kicker);
30+
font-family: var(--ph-font-mono);
31+
font-size: 19px;
32+
font-weight: 600;
33+
letter-spacing: 0.24em;
34+
text-transform: uppercase;
35+
}
36+
h1 {
37+
font-size: 96px;
38+
font-weight: 700;
39+
letter-spacing: -0.04em;
40+
line-height: 1.05;
41+
margin: 24px 0 0;
42+
max-width: 600px;
43+
}
44+
h1 .name { display: block; }
45+
h1 .tagline { color: var(--ph-dark-muted); display: block; font-size: 50px; letter-spacing: -0.03em; margin-top: 12px; }
46+
h1 .tagline:empty { display: none; }
47+
p { color: var(--ph-mist-300); font-size: 26px; line-height: 1.4; margin: 28px 0 0; max-width: 600px; }
48+
.pills { display: flex; gap: 13px; margin-top: 30px; }
49+
.pill {
50+
border: 1px solid var(--ph-dark-border);
51+
border-radius: 4px;
52+
color: var(--ph-dark-accent);
53+
font-family: var(--ph-font-mono);
54+
font-size: 19px;
55+
font-weight: 600;
56+
padding: 9px 15px;
57+
}
58+
.pill + .pill { color: var(--ph-dark-muted); }
59+
.address { bottom: 52px; color: var(--ph-dark-kicker); font-family: var(--ph-font-mono); font-size: 20px; left: 76px; position: absolute; }
60+
.falcon { height: 440px; position: absolute; right: 56px; top: 82px; }
61+
</style>
62+
</head>
63+
<body>
64+
<div class="card">
65+
<div class="kicker">{{kicker}}</div>
66+
<h1><span class="name">{{name}}</span><span class="tagline">{{tagline}}</span></h1>
67+
<p>{{text}}</p>
68+
<div class="pills">{{pills}}</div>
69+
<div class="address">{{address}}</div>
70+
<img class="falcon" src="{{falcon}}" alt="">
71+
</div>
72+
<script>
73+
// A long name gets smaller until it fits beside the falcon: on two lines, or on
74+
// three lines at 56px and less (a long class name of the API). In a block:
75+
// render-og-cards.mjs gives each card to the same page.
76+
{
77+
const name = document.querySelector('h1 .name');
78+
const fits = (size) => name.scrollWidth <= 600 && name.offsetHeight <= Math.ceil(size * 1.05 * (size > 56 ? 2 : 3));
79+
80+
for (let size = 96; !fits(size) && size > 40; size -= 2) {
81+
name.style.fontSize = `${size - 2}px`;
82+
}
83+
}
84+
</script>
85+
</body>
86+
</html>

‎scripts/check-og.mjs‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Checks that every page of the build has its social card: each og:image and
3+
* twitter:image of each HTML page of dist/ is a file of dist/. Prints the count,
4+
* then each missing card, and exits with 1 when a card is missing. The deploy
5+
* workflow runs it after "Render the social cards".
6+
*
7+
* node scripts/check-og.mjs [dist]
8+
*/
9+
import { existsSync, readdirSync, readFileSync } from "node:fs";
10+
import { join, relative } from "node:path";
11+
import { pathToFileURL } from "node:url";
12+
13+
/** The address of the site: an image there is a file of dist/. */
14+
export const SITE = "https://docs.phalcon.io";
15+
16+
/**
17+
* The image addresses of a page (og:image, twitter:image) as paths of the site,
18+
* each once, with no query and no fragment (a card can have a version, ?v=2).
19+
* The attributes of a meta tag can come in any order. An image on another host
20+
* is left out.
21+
*
22+
* @param {string} html
23+
* @returns {string[]}
24+
*/
25+
export function cardPaths(html) {
26+
const paths = new Set();
27+
28+
for (const [tag] of html.matchAll(/<meta\b[^>]*>/g)) {
29+
const image = /\b(?:property|name)="(?:og:image|twitter:image)"/.test(tag);
30+
const address = /\bcontent="([^"]*)"/.exec(tag)?.[1] ?? "";
31+
const path = address.startsWith(`${SITE}/`) ? address.slice(SITE.length) : address;
32+
33+
if (image && path.startsWith("/")) {
34+
paths.add(path.replace(/[?#].*$/, ""));
35+
}
36+
}
37+
38+
return [...paths];
39+
}
40+
41+
/**
42+
* The cards of the pages that are not files.
43+
*
44+
* @param {{ html: string, page: string }[]} pages
45+
* @param {(path: string) => boolean} exists
46+
* @returns {{ page: string, path: string }[]}
47+
*/
48+
export function missingCards(pages, exists) {
49+
return pages.flatMap(({ html, page }) => cardPaths(html).filter((path) => !exists(path)).map((path) => ({ page, path })));
50+
}
51+
52+
/**
53+
* The result of the check: the count of the different cards, the missing ones,
54+
* and whether the build passes. A build with no card at all fails: then the
55+
* check read nothing (for example, a new form of the meta tags), and must not
56+
* pass in silence.
57+
*
58+
* @param {{ html: string, page: string }[]} pages
59+
* @param {(path: string) => boolean} exists
60+
* @returns {{ cards: number, missing: { page: string, path: string }[], ok: boolean }}
61+
*/
62+
export function checkSummary(pages, exists) {
63+
const cards = new Set(pages.flatMap(({ html }) => cardPaths(html))).size;
64+
const missing = missingCards(pages, exists);
65+
66+
return { cards, missing, ok: cards > 0 && missing.length === 0 };
67+
}
68+
69+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
70+
const dist = process.argv[2] ?? "dist";
71+
const files = readdirSync(dist, { recursive: true }).filter((file) => file.endsWith(".html"));
72+
const pages = files.map((file) => ({ html: readFileSync(join(dist, file), "utf8"), page: relative(dist, join(dist, file)) }));
73+
const { cards, missing, ok } = checkSummary(pages, (path) => existsSync(join(dist, decodeURIComponent(path))));
74+
75+
console.log(`pages ${pages.length}, cards ${cards}, missing ${missing.length}`);
76+
77+
if (cards === 0) {
78+
console.log("no card: no page has an og:image or a twitter:image that the check can read");
79+
}
80+
81+
for (const { page, path } of missing.slice(0, 20)) {
82+
console.log(`missing ${path} (${page})`);
83+
}
84+
85+
process.exitCode = ok ? 0 : 1;
86+
}

0 commit comments

Comments
 (0)