ogcards 0.1.0__tar.gz
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.
- ogcards-0.1.0/.github/workflows/ci.yml +22 -0
- ogcards-0.1.0/.github/workflows/publish.yml +33 -0
- ogcards-0.1.0/.gitignore +17 -0
- ogcards-0.1.0/CHANGELOG.md +19 -0
- ogcards-0.1.0/LICENSE +21 -0
- ogcards-0.1.0/PKG-INFO +95 -0
- ogcards-0.1.0/README.md +68 -0
- ogcards-0.1.0/examples/jekyll/og-manifest.json +24 -0
- ogcards-0.1.0/examples/og-manifest.json +22 -0
- ogcards-0.1.0/examples/ogcards.toml +13 -0
- ogcards-0.1.0/pyproject.toml +53 -0
- ogcards-0.1.0/src/ogcards/__init__.py +17 -0
- ogcards-0.1.0/src/ogcards/cache.py +42 -0
- ogcards-0.1.0/src/ogcards/cli.py +144 -0
- ogcards-0.1.0/src/ogcards/config.py +138 -0
- ogcards-0.1.0/src/ogcards/errors.py +19 -0
- ogcards-0.1.0/src/ogcards/fonts/Inter-Bold.ttf +0 -0
- ogcards-0.1.0/src/ogcards/fonts/Inter-Regular.ttf +0 -0
- ogcards-0.1.0/src/ogcards/fonts/OFL.txt +92 -0
- ogcards-0.1.0/src/ogcards/fonts/README.md +14 -0
- ogcards-0.1.0/src/ogcards/manifest.py +66 -0
- ogcards-0.1.0/src/ogcards/py.typed +0 -0
- ogcards-0.1.0/src/ogcards/render.py +141 -0
- ogcards-0.1.0/src/ogcards/templates.py +38 -0
- ogcards-0.1.0/tests/test_cli.py +38 -0
- ogcards-0.1.0/tests/test_config.py +42 -0
- ogcards-0.1.0/tests/test_manifest.py +31 -0
- ogcards-0.1.0/tests/test_render.py +31 -0
- ogcards-0.1.0/uv.lock +396 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run mypy
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI on a version tag via Trusted Publishing (OIDC).
|
|
4
|
+
# No API token is stored: register this repo + workflow as a trusted
|
|
5
|
+
# publisher on the PyPI project, and set a "pypi" environment.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- run: uv build
|
|
17
|
+
- uses: actions/upload-artifact@v4
|
|
18
|
+
with:
|
|
19
|
+
name: dist
|
|
20
|
+
path: dist/
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
needs: build
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/download-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
ogcards-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project adheres to [Common Changelog](https://common-changelog.org) and
|
|
4
|
+
[Semantic Versioning](https://semver.org).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-06-14
|
|
7
|
+
|
|
8
|
+
_Initial release._
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add `build`, `preview`, and `init` CLI commands
|
|
13
|
+
- Add a TOML theme with named templates and a JSON card manifest
|
|
14
|
+
- Add a Pillow `stack` layout with title auto-fit, wrapping, and ellipsis
|
|
15
|
+
- Add incremental rendering via a content-fingerprint cache
|
|
16
|
+
- Add custom layout registration through `ogcards.templates.register`
|
|
17
|
+
- Bundle the Inter typeface (SIL OFL 1.1) with a system-font fallback
|
|
18
|
+
|
|
19
|
+
[0.1.0]: https://github.com/laplacef/ogcards/releases/tag/v0.1.0
|
ogcards-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francisco Laplace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ogcards-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ogcards
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build-time Open Graph card generator for any static site. Manifest in, PNGs out, one dependency.
|
|
5
|
+
Project-URL: Homepage, https://github.com/laplacef/ogcards
|
|
6
|
+
Project-URL: Source, https://github.com/laplacef/ogcards
|
|
7
|
+
Project-URL: Issues, https://github.com/laplacef/ogcards/issues
|
|
8
|
+
Author: Francisco Laplace
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: src/ogcards/fonts/OFL.txt
|
|
12
|
+
Keywords: og-image,open-graph,pillow,social-card,ssg,static-site
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: pillow>=10.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ogcards
|
|
29
|
+
|
|
30
|
+
Generate Open Graph cards for a static site at build time. Give it a JSON
|
|
31
|
+
manifest and a TOML theme, and it writes one PNG per card with Pillow. No
|
|
32
|
+
headless browser, no Node, no system libraries, just Pillow.
|
|
33
|
+
|
|
34
|
+
It's framework-agnostic, so any generator that can write a JSON file can drive it.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
uv tool install ogcards
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
ogcards init # writes a sample ogcards.toml and og-manifest.json
|
|
46
|
+
ogcards build --config ogcards.toml --manifest og-manifest.json --out-dir _site
|
|
47
|
+
ogcards preview --title "Hello" --subtitle "June 2026" --out card.png
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`build` is incremental, so a card re-renders only when its title, subtitle,
|
|
51
|
+
template, or theme changes. Pass `--force` to rebuild everything.
|
|
52
|
+
|
|
53
|
+
## Inputs
|
|
54
|
+
|
|
55
|
+
Two files. The theme (`ogcards.toml`) is your branding. The manifest
|
|
56
|
+
(`og-manifest.json`) is the per-build content your generator emits.
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[canvas]
|
|
60
|
+
width = 1200
|
|
61
|
+
height = 630
|
|
62
|
+
background = "#ffffff"
|
|
63
|
+
|
|
64
|
+
[templates.post]
|
|
65
|
+
title_size = 64 # shrinks automatically past max_title_lines
|
|
66
|
+
title_color = "#1a1a1a"
|
|
67
|
+
meta_color = "#535358"
|
|
68
|
+
accent = "#0645ad"
|
|
69
|
+
padding = 80
|
|
70
|
+
max_title_lines = 3
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"cards": [
|
|
76
|
+
{
|
|
77
|
+
"title": "Your Post Title",
|
|
78
|
+
"subtitle": "June 11, 2026",
|
|
79
|
+
"out": "assets/og/posts/your-post.png"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`out` is relative to `--out-dir`, and a card's `template` defaults to `post`.
|
|
86
|
+
|
|
87
|
+
## Static sites
|
|
88
|
+
|
|
89
|
+
Have your generator write `og-manifest.json` at build time, run `ogcards build`,
|
|
90
|
+
then point each `og:image` at the `out` path. See [`examples/jekyll/`](examples/jekyll/)
|
|
91
|
+
for a Jekyll manifest; the pattern is the same for Hugo or Eleventy.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT. See [`LICENSE`](LICENSE).
|
ogcards-0.1.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ogcards
|
|
2
|
+
|
|
3
|
+
Generate Open Graph cards for a static site at build time. Give it a JSON
|
|
4
|
+
manifest and a TOML theme, and it writes one PNG per card with Pillow. No
|
|
5
|
+
headless browser, no Node, no system libraries, just Pillow.
|
|
6
|
+
|
|
7
|
+
It's framework-agnostic, so any generator that can write a JSON file can drive it.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv tool install ogcards
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
ogcards init # writes a sample ogcards.toml and og-manifest.json
|
|
19
|
+
ogcards build --config ogcards.toml --manifest og-manifest.json --out-dir _site
|
|
20
|
+
ogcards preview --title "Hello" --subtitle "June 2026" --out card.png
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`build` is incremental, so a card re-renders only when its title, subtitle,
|
|
24
|
+
template, or theme changes. Pass `--force` to rebuild everything.
|
|
25
|
+
|
|
26
|
+
## Inputs
|
|
27
|
+
|
|
28
|
+
Two files. The theme (`ogcards.toml`) is your branding. The manifest
|
|
29
|
+
(`og-manifest.json`) is the per-build content your generator emits.
|
|
30
|
+
|
|
31
|
+
```toml
|
|
32
|
+
[canvas]
|
|
33
|
+
width = 1200
|
|
34
|
+
height = 630
|
|
35
|
+
background = "#ffffff"
|
|
36
|
+
|
|
37
|
+
[templates.post]
|
|
38
|
+
title_size = 64 # shrinks automatically past max_title_lines
|
|
39
|
+
title_color = "#1a1a1a"
|
|
40
|
+
meta_color = "#535358"
|
|
41
|
+
accent = "#0645ad"
|
|
42
|
+
padding = 80
|
|
43
|
+
max_title_lines = 3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"cards": [
|
|
49
|
+
{
|
|
50
|
+
"title": "Your Post Title",
|
|
51
|
+
"subtitle": "June 11, 2026",
|
|
52
|
+
"out": "assets/og/posts/your-post.png"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`out` is relative to `--out-dir`, and a card's `template` defaults to `post`.
|
|
59
|
+
|
|
60
|
+
## Static sites
|
|
61
|
+
|
|
62
|
+
Have your generator write `og-manifest.json` at build time, run `ogcards build`,
|
|
63
|
+
then point each `og:image` at the `out` path. See [`examples/jekyll/`](examples/jekyll/)
|
|
64
|
+
for a Jekyll manifest; the pattern is the same for Hugo or Eleventy.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Place this file at your Jekyll site root. Jekyll renders it to
|
|
3
|
+
# _site/og-manifest.json at build time (the front-matter block below is what
|
|
4
|
+
# makes Jekyll process it). Then, as a post-build CI step:
|
|
5
|
+
#
|
|
6
|
+
# ogcards build --config ogcards.toml \
|
|
7
|
+
# --manifest _site/og-manifest.json \
|
|
8
|
+
# --out-dir _site
|
|
9
|
+
#
|
|
10
|
+
# Point your og:image meta tag at the same path, e.g.
|
|
11
|
+
# /assets/og/posts/{{ page.slug }}.png
|
|
12
|
+
---
|
|
13
|
+
{
|
|
14
|
+
"cards": [
|
|
15
|
+
{%- for post in site.posts %}
|
|
16
|
+
{
|
|
17
|
+
"title": {{ post.title | jsonify }},
|
|
18
|
+
"subtitle": "{{ post.date | date: '%B %-d, %Y' }}",
|
|
19
|
+
"template": "post",
|
|
20
|
+
"out": "assets/og/posts/{{ post.slug }}.png"
|
|
21
|
+
}{%- unless forloop.last %},{%- endunless %}
|
|
22
|
+
{%- endfor %}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cards": [
|
|
3
|
+
{
|
|
4
|
+
"title": "Effectively Communicating with Non-Technical Stakeholders",
|
|
5
|
+
"subtitle": "June 11, 2026",
|
|
6
|
+
"template": "post",
|
|
7
|
+
"out": "assets/og/posts/communicating-with-stakeholders.png"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"title": "Short Title",
|
|
11
|
+
"subtitle": "June 13, 2026",
|
|
12
|
+
"template": "post",
|
|
13
|
+
"out": "assets/og/posts/short.png"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "A Very Long Title That Should Exercise the Auto-Fit and Wrapping Logic Across Several Lines and Eventually Force an Ellipsis When It Runs Out of Room",
|
|
17
|
+
"subtitle": "June 1, 2026",
|
|
18
|
+
"template": "post",
|
|
19
|
+
"out": "assets/og/posts/long.png"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ogcards"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Build-time Open Graph card generator for any static site. Manifest in, PNGs out, one dependency."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "src/ogcards/fonts/OFL.txt"]
|
|
13
|
+
authors = [{ name = "Francisco Laplace" }]
|
|
14
|
+
keywords = ["open-graph", "og-image", "social-card", "static-site", "pillow", "ssg"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Multimedia :: Graphics",
|
|
25
|
+
"Topic :: Software Development :: Build Tools",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = ["pillow>=10.0"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/laplacef/ogcards"
|
|
32
|
+
Source = "https://github.com/laplacef/ogcards"
|
|
33
|
+
Issues = "https://github.com/laplacef/ogcards/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
ogcards = "ogcards.cli:main"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/ogcards"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
src = ["src", "tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
strict = true
|
|
53
|
+
files = ["src", "tests"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""ogcards — build-time Open Graph card generator for any static site."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ogcards.config import Theme, load_theme
|
|
6
|
+
from ogcards.manifest import Card, Manifest, load_manifest
|
|
7
|
+
from ogcards.render import render_card
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Card",
|
|
11
|
+
"Manifest",
|
|
12
|
+
"Theme",
|
|
13
|
+
"load_manifest",
|
|
14
|
+
"load_theme",
|
|
15
|
+
"render_card",
|
|
16
|
+
]
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Incremental rendering: a card is re-rendered only when its inputs change.
|
|
2
|
+
|
|
3
|
+
The fingerprint folds in the card, its resolved template, and a theme token so
|
|
4
|
+
that a branding tweak (canvas size, fonts) invalidates every card.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import asdict
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ogcards.config import Template
|
|
15
|
+
from ogcards.manifest import Card
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fingerprint(card: Card, template: Template, theme_token: str) -> str:
|
|
19
|
+
"""Return a stable hash of everything that affects a card's pixels."""
|
|
20
|
+
payload = json.dumps(
|
|
21
|
+
{"card": asdict(card), "template": asdict(template), "theme": theme_token},
|
|
22
|
+
sort_keys=True,
|
|
23
|
+
)
|
|
24
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_cache(path: Path) -> dict[str, str]:
|
|
28
|
+
"""Read the out-path -> fingerprint map, or an empty map if absent/corrupt."""
|
|
29
|
+
if not path.is_file():
|
|
30
|
+
return {}
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except json.JSONDecodeError:
|
|
34
|
+
return {}
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return {}
|
|
37
|
+
return {str(k): str(v) for k, v in data.items()}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_cache(path: Path, cache: dict[str, str]) -> None:
|
|
41
|
+
"""Persist the fingerprint map next to the rendered output."""
|
|
42
|
+
path.write_text(json.dumps(cache, indent=2, sort_keys=True), encoding="utf-8")
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Command-line interface: ``ogcards build | preview | init``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ogcards import cache as cache_mod
|
|
12
|
+
from ogcards.config import Theme, load_theme
|
|
13
|
+
from ogcards.errors import OgcardsError
|
|
14
|
+
from ogcards.manifest import Card, load_manifest
|
|
15
|
+
from ogcards.render import render_card
|
|
16
|
+
|
|
17
|
+
_SAMPLE_TOML = """\
|
|
18
|
+
[canvas]
|
|
19
|
+
width = 1200
|
|
20
|
+
height = 630
|
|
21
|
+
background = "#ffffff"
|
|
22
|
+
|
|
23
|
+
[templates.post]
|
|
24
|
+
title_size = 64
|
|
25
|
+
title_color = "#1a1a1a"
|
|
26
|
+
meta_size = 28
|
|
27
|
+
meta_color = "#535358"
|
|
28
|
+
accent = "#0645ad"
|
|
29
|
+
padding = 80
|
|
30
|
+
max_title_lines = 3
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_SAMPLE_MANIFEST = {
|
|
34
|
+
"cards": [
|
|
35
|
+
{
|
|
36
|
+
"title": "Your Post Title Goes Here",
|
|
37
|
+
"subtitle": "June 13, 2026",
|
|
38
|
+
"template": "post",
|
|
39
|
+
"out": "assets/og/posts/your-post.png",
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build(args: argparse.Namespace) -> int:
|
|
46
|
+
theme = load_theme(Path(args.config))
|
|
47
|
+
manifest = load_manifest(Path(args.manifest))
|
|
48
|
+
out_dir = Path(args.out_dir)
|
|
49
|
+
cache_path = out_dir / ".ogcards-cache.json"
|
|
50
|
+
cache = {} if args.force else cache_mod.load_cache(cache_path)
|
|
51
|
+
token = _theme_token(theme)
|
|
52
|
+
|
|
53
|
+
rendered = skipped = 0
|
|
54
|
+
for card in manifest.cards:
|
|
55
|
+
template = theme.template(card.template)
|
|
56
|
+
fp = cache_mod.fingerprint(card, template, token)
|
|
57
|
+
dest = out_dir / card.out
|
|
58
|
+
if not args.force and dest.is_file() and cache.get(card.out) == fp:
|
|
59
|
+
skipped += 1
|
|
60
|
+
continue
|
|
61
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
render_card(card, template, theme).save(dest, "PNG", optimize=True)
|
|
63
|
+
cache[card.out] = fp
|
|
64
|
+
rendered += 1
|
|
65
|
+
if args.verbose:
|
|
66
|
+
print(f"rendered {dest}")
|
|
67
|
+
|
|
68
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
cache_mod.save_cache(cache_path, cache)
|
|
70
|
+
print(f"ogcards: {rendered} rendered, {skipped} unchanged")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _preview(args: argparse.Namespace) -> int:
|
|
75
|
+
theme = load_theme(Path(args.config)) if args.config else Theme()
|
|
76
|
+
card = Card(title=args.title, out=args.out, subtitle=args.subtitle, template=args.template)
|
|
77
|
+
dest = Path(args.out)
|
|
78
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
render_card(card, theme.template(card.template), theme).save(dest, "PNG", optimize=True)
|
|
80
|
+
print(f"ogcards: wrote {dest}")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _init(args: argparse.Namespace) -> int:
|
|
85
|
+
target = Path(args.dir)
|
|
86
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
config = target / "ogcards.toml"
|
|
88
|
+
manifest = target / "og-manifest.json"
|
|
89
|
+
for path, contents in (
|
|
90
|
+
(config, _SAMPLE_TOML),
|
|
91
|
+
(manifest, json.dumps(_SAMPLE_MANIFEST, indent=2) + "\n"),
|
|
92
|
+
):
|
|
93
|
+
if path.exists():
|
|
94
|
+
print(f"ogcards: {path} exists, leaving it alone")
|
|
95
|
+
else:
|
|
96
|
+
path.write_text(contents, encoding="utf-8")
|
|
97
|
+
print(f"ogcards: wrote {path}")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _theme_token(theme: Theme) -> str:
|
|
102
|
+
return json.dumps(
|
|
103
|
+
{"canvas": asdict(theme.canvas), "fonts": asdict(theme.fonts)},
|
|
104
|
+
sort_keys=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main(argv: list[str] | None = None) -> int:
|
|
109
|
+
parser = argparse.ArgumentParser(
|
|
110
|
+
prog="ogcards",
|
|
111
|
+
description="Build-time Open Graph card generator for any static site.",
|
|
112
|
+
)
|
|
113
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
114
|
+
|
|
115
|
+
build = sub.add_parser("build", help="render every card in a manifest")
|
|
116
|
+
build.add_argument("--config", required=True, help="path to ogcards.toml")
|
|
117
|
+
build.add_argument("--manifest", required=True, help="path to og-manifest.json")
|
|
118
|
+
build.add_argument("--out-dir", required=True, help="base directory for outputs")
|
|
119
|
+
build.add_argument("--force", action="store_true", help="ignore the incremental cache")
|
|
120
|
+
build.add_argument("-v", "--verbose", action="store_true", help="print each rendered path")
|
|
121
|
+
build.set_defaults(func=_build)
|
|
122
|
+
|
|
123
|
+
preview = sub.add_parser("preview", help="render a single card for design iteration")
|
|
124
|
+
preview.add_argument("--title", required=True)
|
|
125
|
+
preview.add_argument("--subtitle")
|
|
126
|
+
preview.add_argument("--template", default="post")
|
|
127
|
+
preview.add_argument("--config", help="optional ogcards.toml; omit for defaults")
|
|
128
|
+
preview.add_argument("--out", default="card.png")
|
|
129
|
+
preview.set_defaults(func=_preview)
|
|
130
|
+
|
|
131
|
+
init = sub.add_parser("init", help="write a sample config and manifest")
|
|
132
|
+
init.add_argument("--dir", default=".")
|
|
133
|
+
init.set_defaults(func=_init)
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args(argv)
|
|
136
|
+
try:
|
|
137
|
+
return int(args.func(args))
|
|
138
|
+
except OgcardsError as exc:
|
|
139
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
raise SystemExit(main())
|