Add stats block with shareable image generation#3126
Add stats block with shareable image generation#3126
Conversation
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.
|
/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.
There was a problem hiding this comment.
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.
- 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.
jeherve
left a comment
There was a problem hiding this comment.
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
- 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.
|
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.
|
@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. |
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. |
|
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.
This reverts commit de7f26a.
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.
Fixes #3074
Proposed changes:
activitypub/statsblock that displays annual Fediverse statistics as a styled card on the site, showing posts federated, engagements, follower growth, top supporter, and top posts.uploads/activitypub/stats/{user_id}/usingWP_Image_Editorfor WebP optimization. Cached images are served as direct file URLs, bypassing PHP on subsequent requests./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).activitypub_cache_stats_image_enabledfilter. The image URL can be routed through a CDN or image proxy like Photon via theactivitypub_stats_image_urlfilter.Other information:
Testing instructions:
wp activitypub stats collect --year=2025 --force && wp activitypub stats compile --year=2025to seed stats data.?activitypubappended and verify the image is attached (as a direct uploads URL) and the block HTML is stripped.activitypub_cache_stats_image_enabledto false).Changelog entry
Changelog Entry Details
Significance
Type
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.