Skip to content

Commit 1f2fde9

Browse files
authored
Merge pull request #19 from marcop135/develop
release: v2.9.15 dedicated 1200x630 og:image with CTA, tightened meta
2 parents ce1361e + eac32bf commit 1f2fde9

7 files changed

Lines changed: 154 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
**Labels:** **Build**, **Chore**, **CI**, **Docs**, **Enhance**, **Feat**, **Fix**, **Perf**, **Revert**, **Sec**, **Style**; add **(WIP)** only for incomplete work.
44

5+
## [2.9.15] - 2026-05-05
6+
7+
- **Enhance:** Render dedicated 1200x630 og:image from `docs/og-img.svg` with a "Try it now" CTA, under 600 KB.
8+
- **Enhance:** Add `og:image:width`, `og:image:height`, `og:image:type`, and `og:image:alt` for better unfurl rendering.
9+
- **Style:** Tighten og:title to 55 chars and og:description to 134 chars to match social-preview optimums.
10+
- **Revert:** Restore `docs/readme-hero.png` to the un-stripped screenshot; og:image is now its own asset.
11+
512
## [2.9.14] - 2026-05-04
613

714
- **Enhance:** Generate 192/512 PNG and maskable PWA icons so Android installs render the proper logo.

CLAUDE.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ If `yarn start` fails because **5173 is already bound**, a previous Vite session
3131
- `src/App/Container/` — state via `nonaction` and hooks like `useIsMobile`, `useDrop`.
3232
- `src/App/Lib/` — small utilities (e.g. upload helper).
3333
- `public/.htaccess` — Apache security/caching headers used in production deploys.
34-
- `public/static/og-img.png`must mirror `docs/readme-hero.png` so README, social meta, and deploy stay in sync.
34+
- `public/static/og-img.png`generated 1200x630 social-preview image; rendered from `docs/og-img.svg` by `scripts/sync-hero.mjs`. Separate from the README hero.
3535
- `scripts/changelog-lint.mjs` — enforces the changelog format; do not bypass.
3636

3737
## Header toolbar conventions
@@ -61,9 +61,18 @@ The app uses **`system-ui, sans-serif`** globally (set in [`src/App/index.js`](s
6161

6262
The hero uses plain markdown image+link form (`[![alt](src)](url)`), with **no surrounding `<div>` or `<p>` wrapper**. Markdown-inside-HTML rendering is inconsistent across local previewers (VS Code, JetBrains, etc.) even when GitHub handles it, so we keep it pure markdown.
6363

64-
`docs/readme-hero.png` is the single source of truth. `scripts/sync-hero.mjs` (run automatically on `yarn start` / `yarn dev` / `yarn build`, or manually via `yarn hero:sync`) copies it to `public/static/og-img.png` for social meta and the PWA. The destination is gitignored — to update, just replace the source and re-run dev/build.
64+
`docs/readme-hero.png` is the README screenshot. It is referenced directly from the README and is **not** the social-preview image — those are separate assets with different aspect ratios and design constraints.
6565

66-
The hero must **not** show the toolbar version chip — semvers rot. The chip stays visible in the live app; if you re-shoot the hero, edit the chip out before saving over `docs/readme-hero.png`.
66+
## Social preview (og:image)
67+
68+
`docs/og-img.svg` is the source for the social-preview image. `scripts/sync-hero.mjs` (run on `yarn start` / `yarn dev` / `yarn build`, or manually via `yarn hero:sync`) renders it to `public/static/og-img.png` at **1200x630** (the Open Graph standard) so unfurls render correctly across WhatsApp, Slack, Twitter, LinkedIn, Discord, etc.
69+
70+
Constraints:
71+
72+
- **Dimensions:** 1200x630 (16:8.4 ratio that all major unfurl bots target).
73+
- **File size:** keep under **600 KB** — WhatsApp drops larger images.
74+
- **Must include a CTA** ("Try it now →") and the live URL.
75+
- The destination is gitignored; to update, edit `docs/og-img.svg` and re-run dev/build.
6776

6877
## PWA icons
6978

docs/og-img.svg

Lines changed: 92 additions & 0 deletions
Loading

docs/readme-hero.png

123 KB
Loading

index.html

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@
1818
<meta name="ClaudeBot" content="noindex, nofollow">
1919
<meta name="PerplexityBot" content="noindex, nofollow">
2020
<meta name="CCBot" content="noindex, nofollow">
21-
<title>Markdown to PDF: mobile-friendly, offline in the browser</title>
21+
<title>Markdown to PDF offline browser converter, no uploads</title>
2222
<meta name="description"
23-
content="Mobile-friendly Markdown to PDF in your browser with GFM, syntax highlighting, and Mermaid. Works offline; nothing is uploaded for conversion. Fork of realdennis/md2pdf (MIT).">
24-
<meta property="og:title" content="Markdown to PDF">
23+
content="Mobile-friendly Markdown to PDF in your browser. GFM, syntax highlighting, Mermaid. Works offline; nothing is uploaded for conversion.">
24+
<meta property="og:title" content="Markdown to PDF — offline browser converter, no uploads">
2525
<meta property="og:description"
26-
content="Mobile-friendly Markdown to PDF in your browser with GFM, syntax highlighting, and Mermaid. Works offline; nothing is uploaded for conversion. Fork of realdennis/md2pdf (MIT).">
26+
content="Mobile-friendly Markdown to PDF in your browser. GFM, syntax highlighting, Mermaid. Works offline; nothing is uploaded for conversion.">
2727
<meta property="og:image" content="https://md2pdf.marcopontili.com/static/og-img.png">
28+
<meta property="og:image:width" content="1200">
29+
<meta property="og:image:height" content="630">
30+
<meta property="og:image:type" content="image/png">
31+
<meta property="og:image:alt" content="Markdown to PDF — offline browser converter. Try it now at md2pdf.marcopontili.com.">
2832
<meta name="twitter:card" content="summary_large_image">
29-
<meta name="twitter:title" content="Markdown to PDF">
33+
<meta name="twitter:title" content="Markdown to PDF — offline browser converter, no uploads">
3034
<meta name="twitter:description"
31-
content="Mobile-friendly Markdown to PDF in your browser with GFM, syntax highlighting, and Mermaid. Works offline; nothing is uploaded for conversion. Fork of realdennis/md2pdf (MIT).">
35+
content="Mobile-friendly Markdown to PDF in your browser. GFM, syntax highlighting, Mermaid. Works offline; nothing is uploaded for conversion.">
3236
<meta name="twitter:image" content="https://md2pdf.marcopontili.com/static/og-img.png">
3337
<link rel="manifest" href="/manifest.json">
3438
<link rel="icon" type="image/svg+xml" href="/favicon.svg">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "md2pdf",
3-
"version": "2.9.14",
3+
"version": "2.9.15",
44
"description": "Mobile-friendly Markdown to PDF in your browser with GFM, syntax highlighting, and Mermaid. Works offline; nothing is uploaded for conversion. Fork of realdennis/md2pdf (MIT).",
55
"keywords": [
66
"markdown",

scripts/sync-hero.mjs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
1-
import { copyFileSync, existsSync, mkdirSync, statSync } from "node:fs";
2-
import { fileURLToPath } from "node:url";
3-
import { dirname, join } from "node:path";
1+
// Renders docs/og-img.svg -> public/static/og-img.png at 1200x630 (the
2+
// Open Graph standard) so social link previews unfurl with the proper
3+
// dimensions and stay under WhatsApp's ~600KB unfurl ceiling.
4+
//
5+
// docs/readme-hero.png is the README screenshot (different aspect ratio,
6+
// version chip baked in). It is intentionally NOT used for og:image
7+
// anymore — they have separate purposes.
8+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
9+
import { fileURLToPath } from 'node:url';
10+
import { dirname, join } from 'node:path';
11+
import { Resvg } from '@resvg/resvg-js';
412

513
const __dirname = dirname(fileURLToPath(import.meta.url));
6-
const root = join(__dirname, "..");
7-
const src = join(root, "docs/readme-hero.png");
8-
const dst = join(root, "public/static/og-img.png");
14+
const root = join(__dirname, '..');
15+
const srcPath = join(root, 'docs/og-img.svg');
16+
const outPath = join(root, 'public/static/og-img.png');
917

10-
if (!existsSync(src)) {
11-
console.error(`sync-hero: source missing: ${src}`);
18+
if (!existsSync(srcPath)) {
19+
console.error(`sync-hero: source missing: ${srcPath}`);
1220
process.exit(1);
1321
}
1422

15-
const srcMtime = statSync(src).mtimeMs;
16-
const dstMtime = existsSync(dst) ? statSync(dst).mtimeMs : 0;
17-
18-
if (srcMtime > dstMtime) {
19-
mkdirSync(dirname(dst), { recursive: true });
20-
copyFileSync(src, dst);
21-
console.log("sync-hero: copied docs/readme-hero.png -> public/static/og-img.png");
22-
} else {
23-
console.log("sync-hero: og-img already up to date");
23+
const srcMtime = statSync(srcPath).mtimeMs;
24+
const outMtime = existsSync(outPath) ? statSync(outPath).mtimeMs : 0;
25+
if (outMtime >= srcMtime) {
26+
console.log('sync-hero: og-img already up to date');
27+
process.exit(0);
2428
}
29+
30+
mkdirSync(dirname(outPath), { recursive: true });
31+
const svg = readFileSync(srcPath);
32+
const png = new Resvg(svg, {
33+
fitTo: { mode: 'width', value: 1200 },
34+
background: '#0d6efd',
35+
}).render().asPng();
36+
37+
writeFileSync(outPath, png);
38+
const kb = (png.length / 1024).toFixed(1);
39+
console.log(`sync-hero: rendered docs/og-img.svg -> public/static/og-img.png (${kb} KB)`);

0 commit comments

Comments
 (0)