@davidsneighbour/postshot
v0.2.0
Published
Create styled screenshots of social media posts using network adapters and reusable HTML/CSS themes.
Readme
postshot
A TypeScript CLI that turns social media post URLs into branded image cards.
The current implementation supports Mastodon. The architecture is designed so that future adapters can plug into the same normalised post model, theming system, config loader, and metadata pipeline.
Features
- Takes a Mastodon post URL and fetches the post via the public Mastodon API.
- Uses a reusable adapter abstraction so other networks can be added without changing the renderer.
- Renders output through reusable HTML/CSS themes with Handlebars templates.
- Supports multiple themes under
themes/THEMENAME/. - Supports a JSON config file plus CLI flags, with CLI values overriding config values.
- Supports configurable output format:
png,jpg,webp. - Supports configurable dimensions and aspect ratios.
- Supports configurable background styles: solid colour or gradient.
- Generates ALT text from the post content.
- Writes ALT text sidecar output by default.
- Embeds ALT text into PNG files via PNG text chunks.
- Embeds ALT text into JPEG files via XMP APP1 metadata.
- Attempts ALT text embedding for WEBP via an XMP chunk.
- Uses Playwright for deterministic rendering.
Architecture
The tool is split into clear layers:
- Adapter layer: fetches and normalises a post from a network into
SocialPostData. - Theme layer: provides HTML/CSS templates and theme defaults.
- Renderer layer: renders the normalised post object to HTML and then to an image.
- Metadata layer: generates ALT text and embeds it where supported.
- Config layer: loads defaults from JSON and merges CLI overrides on top.
That means a future XAdapter, ThreadsAdapter, LinkedInAdapter, or RedditAdapter only needs to return the same internal data shape. Rendering and metadata can remain unchanged.
Project structure
postshot/
├── examples/
│ └── postshot.config.json
├── src/
│ ├── adapters/
│ │ └── mastodon-adapter.ts
│ ├── config/
│ │ └── load-config.ts
│ ├── core/
│ │ ├── adapters.ts
│ │ ├── alt-text.ts
│ │ ├── render.ts
│ │ ├── template-engine.ts
│ │ ├── theme.ts
│ │ └── types.ts
│ └── cli.ts
├── themes/
│ ├── default/
│ │ ├── post.css
│ │ ├── post.hbs
│ │ └── theme.json
│ └── quote/
│ ├── post.css
│ ├── post.hbs
│ └── theme.json
└── README.mdInstallation
npm install
npx playwright install chromium
npm run buildPublishing
Local CLI publish
Use the built-in publish script when you want to publish manually from your machine:
NPM_TOKEN=your_npm_token npm run publish:localNotes:
NPM_TOKEN(orNODE_AUTH_TOKEN) is required.- The script verifies auth with
npm whoami, runsnpm run build, then publishes withnpm publish --access public.
GitHub Actions publish on tags
This repository includes .github/workflows/publish.yml to automatically publish on tag pushes matching v* (for example v0.3.0).
Setup required:
- Add an npm automation token as repository secret:
NPM_TOKEN. - Push a version tag:
git tag v0.3.0
git push origin v0.3.0The workflow installs dependencies, builds, and publishes to npm with provenance enabled.
Config file support
The tool looks for a config file in this order:
- explicit
--config <file> ./postshot.config.json./.postshot.json- legacy
./social-post-shot.config.json - legacy
./.social-post-shot.json
CLI options override config values.
Example config:
{
"defaults": {
"outputFormat": "png",
"width": 1600,
"aspectRatio": "4:5",
"locale": "en-GB",
"timezone": "UTC",
"embedAltText": true,
"writeAltTextSidecar": true,
"dryRun": false
},
"theme": {
"name": "default",
"backgroundType": "gradient",
"gradientFrom": "#101418",
"gradientTo": "#1e293b",
"gradientAngle": 145,
"cardMaxWidth": 920,
"padding": 72
}
}Usage
Basic example
node ./dist/cli.js --url "https://mas.to/@[email protected]/116181703584630659"Use a config file with CLI overrides
node ./dist/cli.js --config ./examples/postshot.config.json --url "https://mas.to/@[email protected]/116181703584630659" --theme quote --format webp --aspect-ratio 1:1Explicit output path and solid background
node ./dist/cli.js --url "https://mas.to/@[email protected]/116181703584630659" --output "./output/mastodon-post.png" --format png --width 1600 --background-type solid --background-color "#111827"Inspect fetched data without rendering
node ./dist/cli.js --url "https://mas.to/@[email protected]/116181703584630659" --dry-runCLI options
| Option | Purpose | Default source |
| ---------------------------------------------------------- | -------------------------------------------------------- | ------------------- |
| --url <url> | Post URL to render | required |
| --config <file> | Path to a JSON config file | auto-detect |
| --output <file> | Output image path | derived from URL |
| --outputPath <file> | Output folder | ourput |
| --format <png\|jpg\|webp> | Image format | config or jpg |
| --width <pixels> | Output width | config or 1600 |
| --height <pixels> | Output height | config or 0 |
| --aspect-ratio <preset> | Ratio preset: 1:1, 4:5, 16:9, 9:16, 3:2, 2:3 | config or unset |
| --theme <name> | Theme identifier | config or default |
| --background-type <solid\|gradient> | Background strategy | config or theme |
| --background-color <color> | Solid colour fallback | config or theme |
| --gradient-from <color> | Gradient start | config or theme |
| --gradient-to <color> | Gradient end | config or theme |
| --gradient-angle <degrees> | Gradient angle | config or theme |
| --card-max-width <pixels> | Maximum card width | config or theme |
| --padding <pixels> | Outer canvas padding | config or theme |
| --locale <locale> | Date formatting locale | config or en-GB |
| --timezone <timezone> | Date formatting timezone | config or UTC |
| --embed-alt-text / --no-embed-alt-text | Enable or disable metadata embedding | config or enabled |
| --write-alt-text-sidecar / --no-write-alt-text-sidecar | Enable or disable file.alt.txt output | config or enabled |
| --dry-run | Fetch and print normalised data only | config or disabled |
Theme system
Themes live side by side under themes/THEMENAME/.
Each theme contains its own HTML, CSS, and defaults. That means a theme can change layout, spacing, card shape, fonts, colours, background treatment, and reusable CSS variables without touching core renderer code.
Minimal recommended structure:
themes/
my-theme/
theme.json
post.hbs
post.cssOptional structure with assets:
themes/
my-theme/
theme.json
post.hbs
post.css
assets/
my-font.woff2
texture.pngtheme.json
A theme can define:
templateFilestylesheetFileassetsDirectorytailwind(optional,trueto enable Tailwind utility classes in yourpost.hbslayout)postClassNamebodyClassNamebackgroundcardMaxWidthpaddingshadowborderRadiusfontFamilyfontsvariables
Example:
{
"templateFile": "post.hbs",
"stylesheetFile": "post.css",
"assetsDirectory": "assets",
"tailwind": true,
"background": {
"type": "gradient",
"gradientFrom": "#101418",
"gradientTo": "#1e293b",
"gradientAngle": 145
},
"cardMaxWidth": 920,
"padding": 72,
"fontFamily": "Inter, system-ui, sans-serif",
"fonts": [
{
"family": "Inter",
"importUrl": "https://rsms.me/inter/inter.css"
}
],
"variables": {
"postshot-accent": "#8b5cf6",
"postshot-muted": "#94a3b8"
}
}Template capabilities
The Handlebars template receives:
postconfigbackgroundCssinlineCssthemeFontCssthemeVariableCss
That allows a theme to:
- define its own HTML structure
- define its own CSS layout rules
- load theme-specific fonts
- inject reusable CSS custom properties
- control the visual look without modifying the adapter or renderer
Current themes
default: full post card with metrics, media, and link cardsquote: simplified quote-style card for embedding into articles or site designs
Current Mastodon behaviour
The Mastodon adapter currently uses the instance's public API endpoint:
https://INSTANCE/api/v1/statuses/STATUS_ID
It maps the following into the internal post model:
- author information
- HTML post body
- plain text post body
- content warning / spoiler text
- media attachments
- preview card
- reply / boost / favourite / bookmark counts
- reblogged status when present
This is the most robust approach for Mastodon because it avoids brittle DOM scraping for the primary content model.
ALT text and metadata
postshot generates ALT text from the normalised post data.
Current behaviour:
- PNG: embeds a
Descriptiontext chunk - JPEG: embeds XMP metadata in an APP1 segment
- WEBP: appends an XMP chunk where feasible
- all formats: can write a
file.alt.txtsidecar
Metadata support differs by viewer and platform, so the sidecar file remains useful even when an application ignores embedded metadata.
ToDo
- [ ] Add authenticated/private post retrieval support where platform APIs and user tokens allow it.
- [ ] Implement a Bluesky fetcher via AT Protocol APIs.
- [ ] Add theme listing and preview subcommands.
- [ ] Add optional config initialisation command.
- [ ] Improve metadata verification across image viewers and publishing pipelines.
- [ ] Tailwind integration for themes
