Skip to content

Add stats block with shareable image generation#3126

Open
pfefferle wants to merge 33 commits intotrunkfrom
add/sharepic
Open

Add stats block with shareable image generation#3126
pfefferle wants to merge 33 commits intotrunkfrom
add/sharepic

Conversation

@pfefferle
Copy link
Copy Markdown
Member

@pfefferle pfefferle commented Apr 1, 2026

Fixes #3074

Proposed changes:

  • Add activitypub/stats block that displays annual Fediverse statistics as a styled card on the site, showing posts federated, engagements, follower growth, top supporter, and top posts.
Screenshot 2026-04-01 at 12 53 39
  • When shared via ActivityPub, the block HTML is stripped from the content and replaced with a server-generated 1200x630 image attached to the activity, so it appears as a shareable image on Mastodon, Pixelfed, and other platforms.
Screenshot 2026-04-01 at 12 53 50
  • Generated images are cached in uploads/activitypub/stats/{user_id}/ using WP_Image_Editor for WebP optimization. Cached images are served as direct file URLs, bypassing PHP on subsequent requests.
Screenshot 2026-04-01 at 12 54 20
  • Two REST endpoints: /stats/image/{user_id}/{year} serves the image binary (generates and caches on first hit), /stats/image-url/{user_id}/{year} returns the resolved URL as JSON (cached file URL or REST endpoint).
  • The share image automatically adopts colors from the site's Global Styles palette and resolves fonts from the active theme or Font Library.
  • Editor sidebar includes a "Share Image" panel that fetches and displays the resolved image URL with a copy button and preview link. Hidden when GD is unavailable.
Screenshot 2026-04-01 at 12 54 11
  • Block supports native color, border, spacing, and typography settings. Border color is passed through to inner elements. Default colors are derived from the site's theme via CSS custom properties.
  • Uses the actor's webfinger identifier in both the block and the share image.
  • Image caching can be disabled via the activitypub_cache_stats_image_enabled filter. The image URL can be routed through a CDN or image proxy like Photon via the activitypub_stats_image_url filter.
  • Graceful degradation when GD is unavailable: endpoints return 501, sidebar panel is hidden, no broken attachments.
  • All URLs work with both pretty and plain permalinks.

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Go to WP-CLI and run wp activitypub stats collect --year=2025 --force && wp activitypub stats compile --year=2025 to seed stats data.
  • Go to the block editor and insert the "ActivityPub Stats" block into a post.
  • Verify the card renders with stats, engagement breakdown, details, and top posts.
  • Go to the block sidebar, open the "Share Image" panel, verify the URL points to a cached file in uploads, and the copy button and preview link work.
  • Go to the cached image URL directly in the browser and confirm it loads without hitting PHP.
  • Go to the REST image endpoint directly and confirm it serves the cached image.
  • Go to the post URL with ?activitypub appended and verify the image is attached (as a direct uploads URL) and the block HTML is stripped.
  • Go to Settings > Permalinks, switch to "Plain", and verify the share image URL and block still work.
  • Go to Appearance > Themes, switch themes, and verify the share image adapts colors and fonts.
  • Go to the block editor, select a user with no stats, and verify saving does not fail.
  • Verify the Share Image panel is hidden when GD is unavailable (can test by filtering activitypub_cache_stats_image_enabled to false).

Changelog entry

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Added - for new features
  • Changed - for changes in existing functionality
  • Deprecated - for soon-to-be removed features
  • Removed - for now removed features
  • Fixed - for any bug fixes
  • Security - in case of vulnerabilities

Message

Add a stats block that displays annual Fediverse statistics as a card on the site and as a shareable image on the Fediverse, with automatic color and font adoption from the site's theme.

@pfefferle pfefferle added the Enhancement New feature or request label Apr 1, 2026
@pfefferle pfefferle self-assigned this Apr 1, 2026
@pfefferle pfefferle requested a review from a team April 1, 2026 07:39
@github-actions github-actions bot added [Feature] REST API [Focus] Editor Changes to the ActivityPub experience in the block editor [Status] In Progress labels Apr 1, 2026
Introduces an `activitypub/stats` block that displays annual Fediverse
statistics as a card or shareable image. The image mode uses a REST
endpoint (`/activitypub/v1/stats/image/{user_id}/{year}`) to generate
a 1200x630 PNG server-side via GD, making it accessible to social
media platforms for OG/link preview cards.

Based on the wrapped block from the fediverse-wrapped-block branch,
renamed from "wrapped" to "stats" for clarity as a standalone feature.
Seeds realistic-looking annual statistics into wp_options so the stats
block and image endpoint can be tested without real ActivityPub activity.

Usage: wp eval-file bin/seed-demo-stats.php
- Use theme colors and fonts from Global Styles for the share image.
- Add bg/fg color query params to image endpoint for block overrides.
- Add Share Image panel in editor sidebar with URL input and copy button.
- Strip stats block from federated content, attach image instead.
- Use webfinger instead of display name in block and image subtitle.
- Add border and spacing block settings, remove hardcoded styles.
- Fix user selection persistence and auto-correction on load.
- Remove image display mode, block always renders as card.
- Fix float-to-int deprecation warnings in GD image rendering.
@pfefferle
Copy link
Copy Markdown
Member Author

/cc @kaffeeringe

- Add REST controller tests for the stats image endpoint (route
  registration, 404 on missing stats, public access, color params).
- Add block tests for image attachment (with/without stats block,
  preserving existing attachments, user ID handling, URL generation
  with plain and pretty permalinks).
- Replace custom strip_stats_block method with __return_empty_string.
- Fix PHPCS issues: missing param docs, formatting, unused params.
- Move image generation, caching, color/font resolution from the REST
  controller into a dedicated Cache\Stats_Image class.
- Controller is now a thin layer with two endpoints:
  /stats/image/{user_id}/{year} serves the image binary,
  /stats/image-url/{user_id}/{year} returns the resolved URL as JSON.
- Cache stores images in uploads/activitypub/stats/{user_id}/ using
  WP_Image_Editor for WebP optimization.
- Add activitypub_stats_image_url filter for CDN/Photon integration.
- Add activitypub_cache_stats_image_enabled filter to disable caching.
- Editor sidebar fetches the resolved URL via the image-url endpoint.
- ActivityPub attachment uses the direct cached file URL.
- Add Stats_Image::is_available() central check for GD.
- Skip image attachment when GD is unavailable.
- Hide Share Image sidebar panel when GD is unavailable.
- Return empty statsImageUrlEndpoint so JS doesn't fetch.
- Include build files.
- Only call imagedestroy() on PHP < 8.0 where GD images are resources.
- Fix URL assertion tests to work with both cached file URLs and REST
  endpoint URLs.
@pfefferle pfefferle marked this pull request as ready for review April 1, 2026 12:15
Copilot AI review requested due to automatic review settings April 1, 2026 12:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new activitypub/stats block and REST-backed share-image feature to surface annual Fediverse stats as both on-site card UI and a shareable 1200×630 image for ActivityPub.

Changes:

  • Introduces a server-rendered Stats block with editor UI (user/year selection + “Share Image” panel).
  • Adds REST endpoints to generate/resolve the stats image URL and to serve the image binary.
  • Implements image generation + caching in uploads (with WebP optimization) and integrates as ActivityPub attachments; adds PHPUnit/Jest coverage.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/phpunit/tests/includes/rest/class-test-stats-image-controller.php Adds REST route tests for the stats image controller (route registration + error cases).
tests/phpunit/tests/includes/class-test-blocks.php Adds PHPUnit coverage for stats image attachment behavior and URL generation.
src/stats/style.scss Adds frontend styles for the stats card block.
src/stats/render.php Implements SSR output for the stats card (summary lookup + markup + border style passthrough).
src/stats/index.js Registers the activitypub/stats block in the editor.
src/stats/edit.js Adds editor controls, SSR preview, and Share Image panel fetching the resolved image URL.
src/stats/block.json Declares block metadata and support flags (color/spacing/typography/border).
src/stats/tests/edit.test.js Adds Jest tests for editor helper logic (year options + endpoint templating).
includes/rest/class-stats-image-controller.php Adds REST endpoints /stats/image/... and /stats/image-url/....
includes/class-blocks.php Registers the block, strips block output for ActivityPub, and appends generated image as ActivityPub attachment.
includes/cache/class-stats-image.php Implements image rendering, caching, WebP optimization, palette/font resolution, and serving logic.
build/stats/style-index.css Compiled CSS output for the stats block.
build/stats/style-index-rtl.css Compiled RTL CSS output for the stats block.
build/stats/render.php Built SSR render file for the block.
build/stats/index.js Built editor script bundle for the block.
build/stats/index.asset.php Build asset metadata (deps + version hash).
build/stats/block.json Built block.json output for distribution.
activitypub.php Registers the new REST controller during REST init.
.github/changelog/3126-from-description Adds changelog entry for the new stats block + share image feature.

- Sanitize border attribute values individually with regex validation
  before writing to inline styles. Prevents CSS injection from imported
  or migrated post content.
- Guard against null user_id from get_user_id() when selectedUser is
  'inherit' outside a loop context.
- Add tests for image-url endpoint (get_url JSON response, 404 case).
- Derive image-url route from rest_base instead of hardcoding.
- Remove dead Blocks::get_stats_image_url(), update tests to use
  Stats_Image::get_url() directly.
- Add comment explaining stats image intentionally bypasses the max
  attachments limit since it replaces the stripped block content.
- Wrap require_once for wp-admin/includes/file.php in function_exists
  check for wp_tempnam.
- Extend Cache\File for storage paths, optimization, glob lookup,
  filesystem access, and is_enabled() filter.
- Reuse File::optimize_image() for WebP conversion instead of custom
  optimize_and_store().
- Reuse File::get_storage_paths() for basedir/baseurl resolution
  instead of custom path_to_url().
- Reuse File::get_file_mime_type() for MIME detection.
- Reuse File::escape_glob_pattern() + glob for cached file lookup.
- Reuse File::get_filesystem()->move() for temp file handling.
- Merge draw_text_centered/draw_text_at into single draw_text method.
- Simplify parse_hex with sscanf.
- Extract find_ttf_in_families/find_ttf_in_font_library helpers.
- Add image-url endpoint test and route registration test.
- Remove dead Blocks::get_stats_image_url(), update tests.
- Add comment about max attachments bypass.
@pfefferle pfefferle requested review from jeherve and robertbpugh April 1, 2026 12:50
- Include theme stylesheet and version in cache hash so images are
  regenerated on theme switch.
- Append color override params to REST URL when caching is disabled.
- Recursively search inner blocks for stats block (supports Group,
  Columns, etc.).
- Handle wp_tempnam and imagepng failures with proper WP_Error.
- Restore original permalink_structure in test instead of hardcoding.
- Remove color override params (bg/fg) from REST endpoints to prevent
  cache-flooding DoS via unlimited color combinations.
- Validate font paths against the theme directory using realpath() to
  prevent arbitrary file reads via crafted theme.json.
- Add year bounds (2000 to current year) to REST schema.
- Add user_id validation against existing users.
- Add X-Content-Type-Options: nosniff header to image responses.
- Simplify color resolution to use theme colors only.
- Cache key now based on theme identity only (no color variants).
Replace manual regex/allowlist border validation with
wp_style_engine_get_styles() which handles sanitization,
camelCase-to-CSS conversion, and per-corner radius natively.
Copy link
Copy Markdown
Member

@jeherve jeherve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! If we can remind folks that the block exists when the end of the year approaches, I'm sure it will be super popular!

Here are a few notes below.


When loading the block in the editor, I see multiple requests to /undefined/actors/{user_id} that all 404. This seems from src/shared/use-user-options.js:49 — the useEffect that checks if the current user has ActivityPub capability fires before window._activityPubOptions is loaded, so namespace is undefined.


I'm not super clear on the stat collection part, so correct me if I'm wrong.

Right now, should we add a one-time backfill on upgrade (via the migration class) so past months in the current year, or the whole last year gets populated? Otherwise everyone will get an empty stats blockk when they try it out.

On my end, I used the CLI tool to collect data, but I think that when I specify --year, e.g. wp activitypub stats collect --year=2025, only the current month (i.e. April 2025) is collected, vs. all 12 months of 2025.

I'm think it would make sense that when --year is provided without --month, it should loop through all months of that year (or at least up to the current month if it's the current year).


The stats image is rendered as PNG via imagepng(), then converted to WebP by the inherited optimize_image(). That pipeline makes sense for caching remote images, but I'm not sure we should use WebP for an image that will be shared on different social networks / apps / clients. WebP doesn't yet have the wide support png has. I'm thinking we could skip that conversion for stats images meant to be shared.


When no stats exist, the block outputs nothing on the front end. The editor shows a placeholder message, but a published page just has a blank gap where the block should be. I'm wondering if we should either hide the block wrapper entirely (so no gap), or show a graceful fallback message.


Something seems off with the image generation and the background color. Check this small screencast (also note the padding issues in the editor):

Screen.Recording.2026-04-02.at.11.03.09.mov

@pfefferle
Copy link
Copy Markdown
Member Author

@jeherve

Nice work! If we can remind folks that the block exists when the end of the year approaches, I'm sure it will be super popular!

I thought about creating a post-template that shows up on january (or on december).

Like I did by accident in #3026

- Prepend @ to webfinger display in stats block.
- Change branding text to "Powered by the ActivityPub plugin".
- Remove leftover duplicate doc comment opener.
- Include compiled_at timestamp in image cache hash so recompiled
  stats invalidate the cached image.
- Skip image URL fetch when selectedUser is not yet set to prevent
  double request on block load.
Guard the capability-check useEffect against a missing namespace
so it does not fire before window._activityPubOptions is available.
@pfefferle
Copy link
Copy Markdown
Member Author

The backfill is already implemented and wired into the migration system. But it only triggers for upgrades from < 7.9.0. Since this branch hasn't been released yet, anyone installing the plugin for the first time (or upgrading from a version >= 7.9.0) won't get the backfill.

Existing 8.x users have historical data but the backfill was gated
behind the 7.9.0 version check. Move it to the unreleased block so
it runs on upgrade.
Previously, `wp activitypub stats collect --year=2024` only collected
the current month. Now it loops through all months of the specified
year (up to the current month for the current year).
Skip WebP conversion for stats images since they are meant to be
shared on social networks where PNG has wider support.
@pfefferle
Copy link
Copy Markdown
Member Author

pfefferle commented Apr 2, 2026

@jeherve you can change the padding/margin in the block settings.

have you changed the colors of the block? is the issue that the block does not use the background of the theme, or is it that the image does not adapt to the changes of the block!?

The idea was, that both should use the colors of the theme if possible.

@jeherve
Copy link
Copy Markdown
Member

jeherve commented Apr 2, 2026

have you changed the colors of the block? is the issue that the block does not use the background of the theme, or is it that the image does not adapt to the changes of the block!?

I didn't change the colors of the block now. I think it's a good idea to try to use the theme's colors as much as possible, but as you can see that doesn't quite happen on my site, because the variables are not present in my theme. In the editor it falls back to the default color you set for the background (white), but the generated image does not inherit that color.

@pfefferle
Copy link
Copy Markdown
Member Author

But why do they work on the image then!?

The stats image now respects background and text color overrides
set on the block, falling back to theme global styles when not set.
Color overrides are included in the cache hash so different color
combinations produce separate cached images.

Also fix the block CSS to fall back to inherit instead of hardcoded
white when theme preset color variables are not defined.
Remove color and typography block supports to avoid mismatch
between block and generated image. The block now inherits theme
colors, matching the image which uses theme global styles.

Clean up old cached images for the same year before generating
a new one to prevent stale files from accumulating.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request [Feature] CLI [Feature] REST API [Focus] Editor Changes to the ActivityPub experience in the block editor [Status] In Progress [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fediverse Wrapped: Generate a shareable image (sharepic) for year-end stats

4 participants