{"_id":"@davidsneighbour/postshot","name":"@davidsneighbour/postshot","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@davidsneighbour/postshot","version":"0.2.0","description":"Create styled screenshots of social media posts using network adapters and reusable HTML/CSS themes.","type":"module","bin":{"postshot":"dist/cli.js"},"engines":{"node":">=22"},"scripts":{"build":"tsc -p tsconfig.json && node ./scripts/copy-assets.mjs","dev":"tsx src/cli.ts --help","check":"tsc -p tsconfig.json --noEmit","publish:local":"node ./scripts/publish.mjs"},"dependencies":{"commander":"14.0.3","handlebars":"4.7.8","moment":"^2.29.4","playwright":"1.58.2","png-chunk-text":"1.0.0","png-chunks-encode":"1.0.0","png-chunks-extract":"1.0.0","sharp":"0.34.5"},"devDependencies":{"@types/node":"25.4.0","tsx":"4.21.0","typescript":"5.9.3"},"gitHead":"1b2198fc23bea3860defb08524cc0feb0128d9b8","_id":"@davidsneighbour/postshot@0.2.0","_nodeVersion":"25.6.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-wRis8krDCHW2aXubtEOHZrAjEd+5e4DEXaNhN1GvP0MflWSdYwvORr2jJSP0DxFULPofiztL08+svxkT5ExlIw==","shasum":"3833d62710751b1be928b58f63f9a05212bf31c8","tarball":"https://registry.npmjs.org/@davidsneighbour/postshot/-/postshot-0.2.0.tgz","fileCount":72,"unpackedSize":120943,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDAc4YBKM2bq7ggfT3wfxeD+UNsSNSooElCHgCX02VynQIhALGiHcV2o863fnjCn4fRDF4edhUxAQVDL/ihS1ZKleVY"}]},"_npmUser":{"name":"davidsneighbour","email":"pkollitsch@gmail.com"},"directories":{},"maintainers":[{"name":"davidsneighbour","email":"pkollitsch@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/postshot_0.2.0_1773187035139_0.38544207229074035"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-10T23:57:15.046Z","0.2.0":"2026-03-10T23:57:15.291Z","modified":"2026-03-10T23:57:15.484Z"},"maintainers":[{"name":"davidsneighbour","email":"pkollitsch@gmail.com"}],"description":"Create styled screenshots of social media posts using network adapters and reusable HTML/CSS themes.","readme":"# postshot\n\nA TypeScript CLI that turns social media post URLs into branded image cards.\n\nThe 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.\n\n## Features\n\n* Takes a Mastodon post URL and fetches the post via the public Mastodon API.\n* Uses a reusable adapter abstraction so other networks can be added without changing the renderer.\n* Renders output through reusable HTML/CSS themes with Handlebars templates.\n* Supports multiple themes under `themes/THEMENAME/`.\n* Supports a JSON config file plus CLI flags, with CLI values overriding config values.\n* Supports configurable output format: `png`, `jpg`, `webp`.\n* Supports configurable dimensions and aspect ratios.\n* Supports configurable background styles: solid colour or gradient.\n* Generates ALT text from the post content.\n* Writes ALT text sidecar output by default.\n* Embeds ALT text into PNG files via PNG text chunks.\n* Embeds ALT text into JPEG files via XMP APP1 metadata.\n* Attempts ALT text embedding for WEBP via an XMP chunk.\n* Uses Playwright for deterministic rendering.\n\n## Architecture\n\nThe tool is split into clear layers:\n\n* *Adapter layer*: fetches and normalises a post from a network into `SocialPostData`.\n* *Theme layer*: provides HTML/CSS templates and theme defaults.\n* *Renderer layer*: renders the normalised post object to HTML and then to an image.\n* *Metadata layer*: generates ALT text and embeds it where supported.\n* *Config layer*: loads defaults from JSON and merges CLI overrides on top.\n\nThat means a future `XAdapter`, `ThreadsAdapter`, `LinkedInAdapter`, or `RedditAdapter` only needs to return the same internal data shape. Rendering and metadata can remain unchanged.\n\n## Project structure\n\n```text\npostshot/\n├── examples/\n│   └── postshot.config.json\n├── src/\n│   ├── adapters/\n│   │   └── mastodon-adapter.ts\n│   ├── config/\n│   │   └── load-config.ts\n│   ├── core/\n│   │   ├── adapters.ts\n│   │   ├── alt-text.ts\n│   │   ├── render.ts\n│   │   ├── template-engine.ts\n│   │   ├── theme.ts\n│   │   └── types.ts\n│   └── cli.ts\n├── themes/\n│   ├── default/\n│   │   ├── post.css\n│   │   ├── post.hbs\n│   │   └── theme.json\n│   └── quote/\n│       ├── post.css\n│       ├── post.hbs\n│       └── theme.json\n└── README.md\n```\n\n## Installation\n\n```bash\nnpm install\nnpx playwright install chromium\nnpm run build\n```\n\n## Publishing\n\n### Local CLI publish\n\nUse the built-in publish script when you want to publish manually from your machine:\n\n```bash\nNPM_TOKEN=your_npm_token npm run publish:local\n```\n\nNotes:\n\n* `NPM_TOKEN` (or `NODE_AUTH_TOKEN`) is required.\n* The script verifies auth with `npm whoami`, runs `npm run build`, then publishes with `npm publish --access public`.\n\n### GitHub Actions publish on tags\n\nThis repository includes `.github/workflows/publish.yml` to automatically publish on tag pushes matching `v*` (for example `v0.3.0`).\n\nSetup required:\n\n1. Add an npm automation token as repository secret: `NPM_TOKEN`.\n2. Push a version tag:\n\n```bash\ngit tag v0.3.0\ngit push origin v0.3.0\n```\n\nThe workflow installs dependencies, builds, and publishes to npm with provenance enabled.\n\n## Config file support\n\nThe tool looks for a config file in this order:\n\n* explicit `--config <file>`\n* `./postshot.config.json`\n* `./.postshot.json`\n* legacy `./social-post-shot.config.json`\n* legacy `./.social-post-shot.json`\n\nCLI options override config values.\n\nExample config:\n\n```json\n{\n  \"defaults\": {\n    \"outputFormat\": \"png\",\n    \"width\": 1600,\n    \"aspectRatio\": \"4:5\",\n    \"locale\": \"en-GB\",\n    \"timezone\": \"UTC\",\n    \"embedAltText\": true,\n    \"writeAltTextSidecar\": true,\n    \"dryRun\": false\n  },\n  \"theme\": {\n    \"name\": \"default\",\n    \"backgroundType\": \"gradient\",\n    \"gradientFrom\": \"#101418\",\n    \"gradientTo\": \"#1e293b\",\n    \"gradientAngle\": 145,\n    \"cardMaxWidth\": 920,\n    \"padding\": 72\n  }\n}\n```\n\n## Usage\n\n### Basic example\n\n```bash\nnode ./dist/cli.js   --url \"https://mas.to/@Daojoan@mastodon.social/116181703584630659\"\n```\n\n### Use a config file with CLI overrides\n\n```bash\nnode ./dist/cli.js   --config ./examples/postshot.config.json   --url \"https://mas.to/@Daojoan@mastodon.social/116181703584630659\"   --theme quote   --format webp   --aspect-ratio 1:1\n```\n\n### Explicit output path and solid background\n\n```bash\nnode ./dist/cli.js   --url \"https://mas.to/@Daojoan@mastodon.social/116181703584630659\"   --output \"./output/mastodon-post.png\"   --format png   --width 1600   --background-type solid   --background-color \"#111827\"\n```\n\n### Inspect fetched data without rendering\n\n```bash\nnode ./dist/cli.js   --url \"https://mas.to/@Daojoan@mastodon.social/116181703584630659\"   --dry-run\n```\n\n## CLI options\n\n| Option                                                     | Purpose                                                  | Default source      |\n| ---------------------------------------------------------- | -------------------------------------------------------- | ------------------- |\n| `--url <url>`                                              | Post URL to render                                       | required            |\n| `--config <file>`                                          | Path to a JSON config file                               | auto-detect         |\n| `--output <file>`                                          | Output image path                                        | derived from URL    |\n| `--outputPath <file>`                                      | Output folder                                            | ourput              |\n| `--format <png\\|jpg\\|webp>`                                | Image format                                             | config or `jpg`     |\n| `--width <pixels>`                                         | Output width                                             | config or `1600`    |\n| `--height <pixels>`                                        | Output height                                            | config or `0`       |\n| `--aspect-ratio <preset>`                                  | Ratio preset: `1:1`, `4:5`, `16:9`, `9:16`, `3:2`, `2:3` | config or unset     |\n| `--theme <name>`                                           | Theme identifier                                         | config or `default` |\n| `--background-type <solid\\|gradient>`                      | Background strategy                                      | config or theme     |\n| `--background-color <color>`                               | Solid colour fallback                                    | config or theme     |\n| `--gradient-from <color>`                                  | Gradient start                                           | config or theme     |\n| `--gradient-to <color>`                                    | Gradient end                                             | config or theme     |\n| `--gradient-angle <degrees>`                               | Gradient angle                                           | config or theme     |\n| `--card-max-width <pixels>`                                | Maximum card width                                       | config or theme     |\n| `--padding <pixels>`                                       | Outer canvas padding                                     | config or theme     |\n| `--locale <locale>`                                        | Date formatting locale                                   | config or `en-GB`   |\n| `--timezone <timezone>`                                    | Date formatting timezone                                 | config or `UTC`     |\n| `--embed-alt-text` / `--no-embed-alt-text`                 | Enable or disable metadata embedding                     | config or enabled   |\n| `--write-alt-text-sidecar` / `--no-write-alt-text-sidecar` | Enable or disable `file.alt.txt` output                  | config or enabled   |\n| `--dry-run`                                                | Fetch and print normalised data only                     | config or disabled  |\n\n## Theme system\n\nThemes live side by side under `themes/THEMENAME/`.\n\nEach 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.\n\nMinimal recommended structure:\n\n```text\nthemes/\n  my-theme/\n    theme.json\n    post.hbs\n    post.css\n```\n\nOptional structure with assets:\n\n```text\nthemes/\n  my-theme/\n    theme.json\n    post.hbs\n    post.css\n    assets/\n      my-font.woff2\n      texture.png\n```\n\n### `theme.json`\n\nA theme can define:\n\n* `templateFile`\n* `stylesheetFile`\n* `assetsDirectory`\n* `tailwind` (optional, `true` to enable Tailwind utility classes in your `post.hbs` layout)\n* `postClassName`\n* `bodyClassName`\n* `background`\n* `cardMaxWidth`\n* `padding`\n* `shadow`\n* `borderRadius`\n* `fontFamily`\n* `fonts`\n* `variables`\n\nExample:\n\n```json\n{\n  \"templateFile\": \"post.hbs\",\n  \"stylesheetFile\": \"post.css\",\n  \"assetsDirectory\": \"assets\",\n  \"tailwind\": true,\n  \"background\": {\n    \"type\": \"gradient\",\n    \"gradientFrom\": \"#101418\",\n    \"gradientTo\": \"#1e293b\",\n    \"gradientAngle\": 145\n  },\n  \"cardMaxWidth\": 920,\n  \"padding\": 72,\n  \"fontFamily\": \"Inter, system-ui, sans-serif\",\n  \"fonts\": [\n    {\n      \"family\": \"Inter\",\n      \"importUrl\": \"https://rsms.me/inter/inter.css\"\n    }\n  ],\n  \"variables\": {\n    \"postshot-accent\": \"#8b5cf6\",\n    \"postshot-muted\": \"#94a3b8\"\n  }\n}\n```\n\n### Template capabilities\n\nThe Handlebars template receives:\n\n* `post`\n* `config`\n* `backgroundCss`\n* `inlineCss`\n* `themeFontCss`\n* `themeVariableCss`\n\nThat allows a theme to:\n\n* define its own HTML structure\n* define its own CSS layout rules\n* load theme-specific fonts\n* inject reusable CSS custom properties\n* control the visual look without modifying the adapter or renderer\n\n### Current themes\n\n* `default`: full post card with metrics, media, and link cards\n* `quote`: simplified quote-style card for embedding into articles or site designs\n\n## Current Mastodon behaviour\n\nThe Mastodon adapter currently uses the instance's public API endpoint:\n\n* `https://INSTANCE/api/v1/statuses/STATUS_ID`\n\nIt maps the following into the internal post model:\n\n* author information\n* HTML post body\n* plain text post body\n* content warning / spoiler text\n* media attachments\n* preview card\n* reply / boost / favourite / bookmark counts\n* reblogged status when present\n\nThis is the most robust approach for Mastodon because it avoids brittle DOM scraping for the primary content model.\n\n## ALT text and metadata\n\n`postshot` generates ALT text from the normalised post data.\n\nCurrent behaviour:\n\n* PNG: embeds a `Description` text chunk\n* JPEG: embeds XMP metadata in an APP1 segment\n* WEBP: appends an XMP chunk where feasible\n* all formats: can write a `file.alt.txt` sidecar\n\nMetadata support differs by viewer and platform, so the sidecar file remains useful even when an application ignores embedded metadata.\n\n## ToDo\n\n* [ ] Add authenticated/private post retrieval support where platform APIs and user tokens allow it.\n* [ ] Implement a Bluesky fetcher via AT Protocol APIs.\n* [ ] Add theme listing and preview subcommands.\n* [ ] Add optional config initialisation command.\n* [ ] Improve metadata verification across image viewers and publishing pipelines.\n* [ ] Tailwind integration for themes\n","readmeFilename":"README.md","_rev":"1-9e756f2a214e570887d77cd576d82c97"}