Skip to content

Add OpenGraph image generation crate #11436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 61 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
1fdf3ec
Add new `crates_io_og_image` crate
Turbo87 Jun 25, 2025
a1141c4
og_image: Add `OgImageGenerator` struct
Turbo87 Jun 25, 2025
1fdfe0e
og_image: Add `OgImageGenerator::new()` fn
Turbo87 Jun 25, 2025
ee1b30a
og_image: Implement `Default` for `OgImageGenerator` struct
Turbo87 Jun 25, 2025
42d35f6
og_image: Add `OgImageGenerator::from_environment()` fn
Turbo87 Jun 25, 2025
b0f1904
og_image: Add `OgImageGenerator::generate()` fn
Turbo87 Jun 25, 2025
695f4cd
og_image: Add `test_generator` example binary
Turbo87 Jun 25, 2025
b7dd06f
og_image: Add PNG snapshot test
Turbo87 Jun 25, 2025
85bd6f4
og_image: Add fields to `OgImageData` struct
Turbo87 Jun 25, 2025
262afa9
og_image: Use `OgImageData` data in generated image
Turbo87 Jun 25, 2025
54d5b60
og_image: Use `minijinja` to generate Typst file
Turbo87 Jun 25, 2025
e7c63ca
og_image: Adjust page size to OpenGraph image standard
Turbo87 Jun 25, 2025
8454abb
og_image: Add doc header to the template
Turbo87 Jun 25, 2025
5663918
og_image: Add "crates.io" header and a bit more styling
Turbo87 Jun 25, 2025
04e19af
og_image: Add crates.io logo to the header
Turbo87 Jun 25, 2025
2674bcb
og_image: Improve tag styling
Turbo87 Jun 25, 2025
7683fd3
og_image: Extract `generate_template()` fn with corresponding test
Turbo87 Jun 25, 2025
06b2230
og_image: Remove extra whitespace from rendered template
Turbo87 Jun 25, 2025
1c1ec5d
og_image: Add second `generate_template()` test
Turbo87 Jun 25, 2025
26f99c7
og_image: Move "Tags" comment into condition
Turbo87 Jun 25, 2025
e4e09a9
og_image: Improve metadata rendering
Turbo87 Jun 25, 2025
9378973
og_image: Add second `generate()` test
Turbo87 Jun 25, 2025
f0bfc29
og_image: Remove redundant "Version" display
Turbo87 Jun 25, 2025
ec6fc89
og_image: Truncate the crate name if it exceeds the available width
Turbo87 Jun 25, 2025
c1d589f
og_image: Truncate the description if it exceeds three lines
Turbo87 Jun 25, 2025
b304f57
og_image: Fix escaping of injected strings
Turbo87 Jun 25, 2025
53c6604
og_image: Reduce test code duplication
Turbo87 Jun 25, 2025
bd16c98
og_image: Truncate metadata if necessary
Turbo87 Jun 25, 2025
c7072cb
og_image: Add icons to metadata fields
Turbo87 Jun 25, 2025
73b8c8a
og_image: Add footer element and Rust logo background
Turbo87 Jun 25, 2025
988e569
og_image: Improve author rendering
Turbo87 Jun 25, 2025
14bf818
og_image: Extract `author()` helper fn
Turbo87 Jun 25, 2025
ec707f1
og_image: Implement author avatar rendering
Turbo87 Jun 26, 2025
ffae242
og_image: Implement author avatar downloading
Turbo87 Jun 26, 2025
8e6e834
og_image: Use async file writing API
Turbo87 Jun 26, 2025
915a6cd
og_image: Avoid unnecessary allocations
Turbo87 Jun 26, 2025
36bc9c4
og_image: Improve crate size formatting
Turbo87 Jun 26, 2025
a4c5e9c
og_image: Improve releases and lines of code formatting
Turbo87 Jun 26, 2025
9c663da
og_image: Adjust metadata value color
Turbo87 Jun 26, 2025
2c7b552
og_image: Add minimal data PNG snapshot
Turbo87 Jun 26, 2025
6c6dd49
og_image: Extract `generate_image()` test helper fn
Turbo87 Jun 26, 2025
5678602
og_image: Extract `generate_template()` test helper fn
Turbo87 Jun 26, 2025
6aa2353
og_image: Add README file
Turbo87 Jun 26, 2025
b84b652
og_image: Use README file as crate documentation
Turbo87 Jun 26, 2025
f4228ab
og_image: Extract `OgImageError` enum
Turbo87 Jun 26, 2025
e9df275
og_image: Use Typst's `--input` instead of `minijinja` to pass in data
Turbo87 Jun 26, 2025
00d647f
og_image: Move assets into `template` folder
Turbo87 Jun 26, 2025
c35c740
CI: Install Typst CLI to run full `crates_io_og_image` test suite
Turbo87 Jun 26, 2025
4f09c95
CI: Install Fira Sans font for consistent PNG snapshot rendering
Turbo87 Jun 26, 2025
c771e0a
og_image: Use `new()` method in `Default` implementation
Turbo87 Jun 27, 2025
9712805
og_image: Use `from_environment()` for tests
Turbo87 Jun 27, 2025
93745f1
og_image: Add comments to `Command` arguments
Turbo87 Jun 27, 2025
55ee8a6
og_image: Clear environment variables for typst subprocess
Turbo87 Jun 27, 2025
ee515ae
og_image: Support custom font paths via `TYPST_FONT_PATH` environment…
Turbo87 Jun 27, 2025
316c45b
CI: Simplify font handling by using Typst font path instead of system…
Turbo87 Jun 27, 2025
05ec8ed
og_image: Improve template rendering with proper block layout for tag…
Turbo87 Jun 28, 2025
419c96d
og_image: Force test execution in CI instead of skipping when Typst u…
Turbo87 Jun 28, 2025
d129157
og_image: Add structured tracing for image generation process with pe…
Turbo87 Jun 29, 2025
f3a0fbf
og_image: Add oxipng-based PNG compression to reduce file sizes
Turbo87 Jun 30, 2025
ec8cb2b
og_image: Refactor `OgImageGenerator` constructors and add `with_typs…
Turbo87 Jun 30, 2025
820445f
og_image: Make description and license optional and add version prefi…
Turbo87 Jun 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ env:
CARGO_DENY_VERSION: 0.18.3
# renovate: datasource=crate depName=cargo-machete versioning=semver
CARGO_MACHETE_VERSION: 0.8.0
# renovate: datasource=github-releases depName=shssoichiro/oxipng versioning=semver
OXIPNG_VERSION: 9.1.5
# renovate: datasource=npm depName=pnpm
PNPM_VERSION: 10.12.4
# renovate: datasource=docker depName=postgres
POSTGRES_VERSION: 16
# renovate: datasource=github-releases depName=typst/typst versioning=semver
TYPST_VERSION: 0.13.1
# renovate: datasource=pypi depName=zizmor
ZIZMOR_VERSION: 1.11.0

Expand Down Expand Up @@ -172,6 +176,27 @@ jobs:
# Remove the Android SDK to free up space
- run: sudo rm -rf /usr/local/lib/android

- name: Install Typst
run: |
wget -q "https://github.com/typst/typst/releases/download/v${TYPST_VERSION}/typst-x86_64-unknown-linux-musl.tar.xz"
tar -xf "typst-x86_64-unknown-linux-musl.tar.xz"
sudo mv "typst-x86_64-unknown-linux-musl/typst" /usr/local/bin/
rm -rf "typst-x86_64-unknown-linux-musl" "typst-x86_64-unknown-linux-musl.tar.xz"
typst --version

- name: Install oxipng
run: |
wget -q "https://github.com/shssoichiro/oxipng/releases/download/v${OXIPNG_VERSION}/oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-musl.tar.gz"
tar -xf "oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-musl.tar.gz"
sudo mv "oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-musl/oxipng" /usr/local/bin/
rm -rf "oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-musl" "oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-musl.tar.gz"
oxipng --version

- name: Download Fira Sans font
run: |
wget -q "https://github.com/mozilla/Fira/archive/4.202.zip"
unzip -q "4.202.zip"

- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
Expand All @@ -183,6 +208,10 @@ jobs:

- run: cargo build --tests --workspace
- run: cargo test --workspace
env:
# Set the path to the Fira Sans font for Typst.
# The path is relative to the `crates_io_og_image` crate root.
TYPST_FONT_PATH: ../../Fira-4.202/otf

frontend-lint:
name: Frontend / Lint
Expand Down
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions crates/crates_io_og_image/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "crates_io_og_image"
version = "0.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"
description = "OpenGraph image generation for crates.io"

[lints]
workspace = true

[dependencies]
anyhow = "=1.0.98"
bytes = "=1.10.1"
crates_io_env_vars = { path = "../crates_io_env_vars" }
reqwest = "=0.12.21"
serde = { version = "=1.0.219", features = ["derive"] }
serde_json = "=1.0.140"
tempfile = "=3.20.0"
thiserror = "=2.0.12"
tokio = { version = "=1.45.1", features = ["process", "fs"] }
tracing = "=0.1.41"

[dev-dependencies]
insta = "=1.43.1"
tokio = { version = "=1.45.1", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "fmt"] }
94 changes: 94 additions & 0 deletions crates/crates_io_og_image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# crates_io_og_image

A Rust crate for generating Open Graph images for crates.io packages.

![Example OG Image](src/snapshots/crates_io_og_image__tests__generated_og_image.snap.png)

## Overview

`crates_io_og_image` is a specialized library for generating visually appealing Open Graph images for Rust crates. These images are designed to be displayed when crates.io links are shared on social media platforms, providing rich visual context about the crate including its name, description, authors, and key metrics.

The generated images include:

- Crate name and description
- Tags/keywords
- Author information with avatars (when available)
- Key metrics (releases, latest version, license, lines of code, size)
- Consistent crates.io branding

## Requirements

- The [Typst](https://typst.app/) CLI must be installed and available in your `PATH`

## Usage

### Basic Example

```rust
use crates_io_og_image::{OgImageData, OgImageGenerator, OgImageAuthorData, OgImageError};

#[tokio::main]
async fn main() -> Result<(), OgImageError> {
// Create a generator instance
let generator = OgImageGenerator::default();

// Define the crate data
let data = OgImageData {
name: "example-crate",
version: "1.2.3",
description: Some("An example crate for testing OpenGraph image generation"),
license: Some("MIT/Apache-2.0"),
tags: &["example", "testing", "og-image"],
authors: &[
OgImageAuthorData::with_url(
"Turbo87",
"https://avatars.githubusercontent.com/u/141300",
),
],
lines_of_code: Some(2000),
crate_size: 75,
releases: 5,
};

// Generate the image
let temp_file = generator.generate(data).await?;

// The temp_file contains the path to the generated PNG image
println!("Image generated at: {}", temp_file.path().display());

Ok(())
}
```

## Configuration

The path to the Typst CLI can be configured through the `TYPST_PATH` environment variables.

## Development

### Running Tests

```bash
cargo test
```

Note that some tests require Typst to be installed and will be skipped if it's not available.

### Example

The crate includes an example that demonstrates how to generate an image:

```bash
cargo run --example test_generator
```

This will generate a test image in the current directory. This will also test the avatar fetching functionality, which requires network access and isn't run as part of the automated tests.

## License

Licensed under either of:

- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)

at your option.
57 changes: 57 additions & 0 deletions crates/crates_io_og_image/examples/test_generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crates_io_og_image::{OgImageAuthorData, OgImageData, OgImageGenerator};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{EnvFilter, fmt};

fn init_tracing() {
let env_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy();

fmt().compact().with_env_filter(env_filter).init();
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
init_tracing();

println!("Testing OgImageGenerator...");

let generator = OgImageGenerator::from_environment()?;
println!("Created generator from environment");

// Test generating an image
let data = OgImageData {
name: "example-crate",
version: "1.2.3",
description: Some("An example crate for testing OpenGraph image generation"),
license: Some("MIT/Apache-2.0"),
tags: &["example", "testing", "og-image"],
authors: &[
OgImageAuthorData::new("example-user", None),
OgImageAuthorData::with_url(
"Turbo87",
"https://avatars.githubusercontent.com/u/141300",
),
],
lines_of_code: Some(2000),
crate_size: 75,
releases: 5,
};
match generator.generate(data).await {
Ok(temp_file) => {
let output_path = "test_og_image.png";
std::fs::copy(temp_file.path(), output_path)?;
println!("Successfully generated image at: {output_path}");
println!(
"Image file size: {} bytes",
std::fs::metadata(output_path)?.len()
);
}
Err(error) => {
println!("Failed to generate image: {error}");
println!("Make sure typst is installed and available in PATH");
}
}

Ok(())
}
56 changes: 56 additions & 0 deletions crates/crates_io_og_image/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Error types for the crates_io_og_image crate.

use std::path::PathBuf;
use thiserror::Error;

/// Errors that can occur when generating OpenGraph images.
#[derive(Debug, Error)]
pub enum OgImageError {
/// Failed to find or execute the Typst binary.
#[error("Failed to find or execute Typst binary: {0}")]
TypstNotFound(#[source] std::io::Error),

/// Environment variable error.
#[error("Environment variable error: {0}")]
EnvVarError(anyhow::Error),

/// Failed to download avatar from URL.
#[error("Failed to download avatar from URL '{url}': {source}")]
AvatarDownloadError {
url: String,
#[source]
source: reqwest::Error,
},

/// Failed to write avatar to file.
#[error("Failed to write avatar to file at {path:?}: {source}")]
AvatarWriteError {
path: PathBuf,
#[source]
source: std::io::Error,
},

/// JSON serialization error.
#[error("JSON serialization error: {0}")]
JsonSerializationError(#[source] serde_json::Error),

/// Typst compilation failed.
#[error("Typst compilation failed: {stderr}")]
TypstCompilationError {
stderr: String,
stdout: String,
exit_code: Option<i32>,
},

/// I/O error.
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),

/// Temporary file creation error.
#[error("Failed to create temporary file: {0}")]
TempFileError(std::io::Error),

/// Temporary directory creation error.
#[error("Failed to create temporary directory: {0}")]
TempDirError(std::io::Error),
}
Loading
Loading