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

Open
wants to merge 58 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7babd4e
Add new `crates_io_og_image` crate
Turbo87 Jun 25, 2025
b6d8282
og_image: Add `OgImageGenerator` struct
Turbo87 Jun 25, 2025
2d09227
og_image: Add `OgImageGenerator::new()` fn
Turbo87 Jun 25, 2025
0e7095d
og_image: Implement `Default` for `OgImageGenerator` struct
Turbo87 Jun 25, 2025
1c820f3
og_image: Add `OgImageGenerator::from_environment()` fn
Turbo87 Jun 25, 2025
ba2abdc
og_image: Add `OgImageGenerator::generate()` fn
Turbo87 Jun 25, 2025
7b9b852
og_image: Add `test_generator` example binary
Turbo87 Jun 25, 2025
397ae4d
og_image: Add PNG snapshot test
Turbo87 Jun 25, 2025
cdc81e6
og_image: Add fields to `OgImageData` struct
Turbo87 Jun 25, 2025
534dcbe
og_image: Use `OgImageData` data in generated image
Turbo87 Jun 25, 2025
40a3af2
og_image: Use `minijinja` to generate Typst file
Turbo87 Jun 25, 2025
1bfd48a
og_image: Adjust page size to OpenGraph image standard
Turbo87 Jun 25, 2025
af81898
og_image: Add doc header to the template
Turbo87 Jun 25, 2025
75e77ab
og_image: Add "crates.io" header and a bit more styling
Turbo87 Jun 25, 2025
f51df4e
og_image: Add crates.io logo to the header
Turbo87 Jun 25, 2025
c3eb2dc
og_image: Improve tag styling
Turbo87 Jun 25, 2025
ac9ba83
og_image: Extract `generate_template()` fn with corresponding test
Turbo87 Jun 25, 2025
7912751
og_image: Remove extra whitespace from rendered template
Turbo87 Jun 25, 2025
50eba9a
og_image: Add second `generate_template()` test
Turbo87 Jun 25, 2025
498b994
og_image: Move "Tags" comment into condition
Turbo87 Jun 25, 2025
547bc59
og_image: Improve metadata rendering
Turbo87 Jun 25, 2025
22d3ab0
og_image: Add second `generate()` test
Turbo87 Jun 25, 2025
62cb578
og_image: Remove redundant "Version" display
Turbo87 Jun 25, 2025
6751441
og_image: Truncate the crate name if it exceeds the available width
Turbo87 Jun 25, 2025
7088754
og_image: Truncate the description if it exceeds three lines
Turbo87 Jun 25, 2025
abdda33
og_image: Fix escaping of injected strings
Turbo87 Jun 25, 2025
b0a39ed
og_image: Reduce test code duplication
Turbo87 Jun 25, 2025
bae6ab3
og_image: Truncate metadata if necessary
Turbo87 Jun 25, 2025
dfd09b4
og_image: Add icons to metadata fields
Turbo87 Jun 25, 2025
95d9121
og_image: Add footer element and Rust logo background
Turbo87 Jun 25, 2025
2228889
og_image: Improve author rendering
Turbo87 Jun 25, 2025
d79d9c5
og_image: Extract `author()` helper fn
Turbo87 Jun 25, 2025
b872ea4
og_image: Implement author avatar rendering
Turbo87 Jun 26, 2025
0d40450
og_image: Implement author avatar downloading
Turbo87 Jun 26, 2025
fd6687b
og_image: Use async file writing API
Turbo87 Jun 26, 2025
0240ffd
og_image: Avoid unnecessary allocations
Turbo87 Jun 26, 2025
fb71c17
og_image: Improve crate size formatting
Turbo87 Jun 26, 2025
7d9697a
og_image: Improve releases and lines of code formatting
Turbo87 Jun 26, 2025
d0d37fe
og_image: Adjust metadata value color
Turbo87 Jun 26, 2025
a2246e4
og_image: Add minimal data PNG snapshot
Turbo87 Jun 26, 2025
a5bc559
og_image: Extract `generate_image()` test helper fn
Turbo87 Jun 26, 2025
5be99ba
og_image: Extract `generate_template()` test helper fn
Turbo87 Jun 26, 2025
5c014a3
og_image: Add README file
Turbo87 Jun 26, 2025
ada3b98
og_image: Use README file as crate documentation
Turbo87 Jun 26, 2025
671a7de
og_image: Extract `OgImageError` enum
Turbo87 Jun 26, 2025
fd4e012
og_image: Use Typst's `--input` instead of `minijinja` to pass in data
Turbo87 Jun 26, 2025
c67f35d
og_image: Move assets into `template` folder
Turbo87 Jun 26, 2025
a63fc67
CI: Install Typst CLI to run full `crates_io_og_image` test suite
Turbo87 Jun 26, 2025
a6261c4
CI: Install Fira Sans font for consistent PNG snapshot rendering
Turbo87 Jun 26, 2025
b7e52cc
og_image: Use `new()` method in `Default` implementation
Turbo87 Jun 27, 2025
8b1a108
og_image: Use `from_environment()` for tests
Turbo87 Jun 27, 2025
38b7cb8
og_image: Fix clippy warnings
Turbo87 Jun 27, 2025
fdfd2b7
og_image: Add comments to `Command` arguments
Turbo87 Jun 27, 2025
350dca3
og_image: Clear environment variables for typst subprocess
Turbo87 Jun 27, 2025
dcd2d7b
og_image: Support custom font paths via `TYPST_FONT_PATH` environment…
Turbo87 Jun 27, 2025
7efa959
CI: Simplify font handling by using Typst font path instead of system…
Turbo87 Jun 27, 2025
c9678fc
og_image: Improve template rendering with proper block layout for tag…
Turbo87 Jun 28, 2025
00e972e
og_image: Force test execution in CI instead of skipping when Typst u…
Turbo87 Jun 28, 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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ env:
PNPM_VERSION: 10.12.3
# 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.10.0

Expand Down Expand Up @@ -167,6 +169,19 @@ 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: 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 @@ -178,6 +193,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
16 changes: 16 additions & 0 deletions Cargo.lock

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

24 changes: 24 additions & 0 deletions crates/crates_io_og_image/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[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.20"
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"] }

[dev-dependencies]
insta = "=1.43.1"
tokio = { version = "=1.45.1", features = ["macros", "rt-multi-thread"] }
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: "v1.2.3",
description: "An example crate for testing OpenGraph image generation",
license: "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.
45 changes: 45 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,45 @@
use crates_io_og_image::{OgImageAuthorData, OgImageData, OgImageGenerator};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Testing OgImageGenerator...");

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

// Test generating an image
let data = OgImageData {
name: "example-crate",
version: "v1.2.3",
description: "An example crate for testing OpenGraph image generation",
license: "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(e) => {
println!("Failed to generate image: {e}");
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