How I built NHR Core Contributions

Nazmul Hasan Robin Avatar

7 min read

If you contribute to WordPress core, your work is spread over Trac tickets, changesets and release credit pages. I wanted one place on my own site that shows it. That became NHR Core Contributions, a plugin where you enter a WordPress.org username and add blocks.

I started it in May 2024 as a count and a list. Version 2.0 came out this month with seven blocks. This post is how it works inside and what I learned on the way.

What it shows

  • A list of core contributions: every core changeset that gives the username props, with component, ticket, changeset and date. It can list Gutenberg commits too.
  • A contributor card and headline stats: totals, first and latest contribution, top components.
  • Activity per year as a bar chart.
  • Release badges: every WordPress release whose credits include the username.
  • WordPress.org badges from the public profile.
  • Plugins and themes the user has published.

Where the data comes from

There is no single API for “what did this person contribute”. The plugin puts it together from four public sources.

Core commits, from GitHub. Core development happens on Trac and SVN, but every commit is mirrored to WordPress/wordpress-develop on GitHub. A core commit message names its contributors in a Props line, so GitHub’s commit search can find them:

$query = 'repo:WordPress/wordpress-develop props "' . $username . '"';

Gutenberg commits, also from GitHub. Gutenberg credits people differently. Each contributor gets a Co-authored-by trailer with an address made from their WordPress.org username, so the search is for "username@git.wordpress.org" in WordPress/gutenberg.

Release credits and published plugins and themes, from the api.wordpress.org endpoints.

Name, avatar and badges, from the public profile page. WordPress.org has no profile API, so the plugin reads the page. Anything the page stops providing is simply left out.

The day the data source broke

Until this month the contribution list came from Core Trac directly. Then Trac started blocking automated requests, and every username showed zero contributions.

Version 1.4.1 moved the list to the GitHub mirror. That turned out to be the better source anyway: it is a real API, it returns structured data, and it is meant to be queried.

The lesson is an old one. If your plugin depends on reading someone else’s web page, expect it to stop working one day, and keep the code that talks to each source in one small place so it can be replaced.

One door for every request

All HTTP requests go through a single method, Remote::get( $url, $ttl, $parse ). It does four things:

  • It caches the parsed result, not the raw response, in a transient.
  • It caches failures too, for five minutes. This matters more than it sounds. Without it, a rate-limited or broken service would be requested again on every page view, which makes the site slow and the rate limit worse.
  • It uses short timeouts: five seconds when a visitor is waiting, ten for background work.
  • It sends the optional GitHub token to GitHub only.
if ( ! is_array( $data ) ) {
    set_transient( $key, [ 'nhrcc_error' => 1 ], self::FAILURE_TTL );
    return self::error();
}

Each source is a small class that builds a URL and parses the answer. None of them makes a request by itself.

Never make a visitor wait

Some numbers need many requests. Totals per year, top components and credited releases cannot be read in one call, and GitHub allows only ten unauthenticated searches a minute.

So a page view never gathers them. A background task run by WP-Cron does it a few requests at a time, with at most five GitHub requests per run, and stores the result in an option. A block only reads what is stored.

Right after you add a block, some parts are empty for a few minutes. A visitor sees nothing there, and a logged-in editor sees a short note that the data is on its way. I prefer that to a page that takes twenty seconds to load.

One limit comes from GitHub itself: a search never returns more than its first 1,000 results. The list pages through the latest 1,000 contributions, and the per-year totals for bigger contributors are counted with one date-ranged search per year, so the total stays complete.

Blocks without front-end JavaScript

Earlier versions shipped Tailwind CSS and jQuery scripts to the front end. Version 2.0 removed both.

All seven blocks are dynamic blocks rendered in PHP. The front end loads no JavaScript and one small stylesheet. In the editor, one shared component shows each block through server-side rendering, so what you see while editing is the same markup visitors get.

The blocks take their colors and typography from the theme and use one accent color, which is why they fit light and dark designs without a set of color settings for every part.

One detail for anyone maintaining a block plugin: the original block’s name dates from the first version, and I cannot change it. Renaming a block breaks every post that already uses it. The old preset attribute is still read and mapped to the new block styles at render time, without rewriting saved posts.

Ready for AI agents

WordPress 6.9 added the Abilities API, a way for a plugin to describe what it can do so that AI agents and MCP clients can call it. The plugin registers five read-only abilities, such as nhrcc/get-core-contributions and nhrcc/get-contribution-summary.

Each one declares its input and output and is marked as read-only:

'permission_callback' => [ $this, 'can_read' ],
'meta'                => [
    'annotations' => [
        'readonly'    => true,
        'destructive' => false,
        'idempotent'  => true,
    ],
],

They return the same data as the blocks and need a signed-in user who can edit posts. The registration is wrapped in a function_exists( 'wp_register_ability' ) check, so the plugin still runs on WordPress versions before 6.9.

Keeping it safe

The plugin takes a username from a user and data from other websites, so both are treated as untrusted.

  • The username is reduced to lowercase letters, digits, dot, underscore and hyphen before it goes anywhere near a URL. The hosts are fixed strings.
  • Remote data has its tags stripped when parsed, and every value is escaped on output.
  • Every REST route needs manage_options, and the GitHub token is never returned by the API, only whether one is saved.

What checks every change

  • WordPress Coding Standards and PHP compatibility checks, with the plugin tested on PHP 7.4 through 8.6.
  • Unit tests that need no database and no network.
  • Plugin Check, the same tool the WordPress.org review team uses.
  • A security workflow with static analysis and a probe that checks every endpoint asks for the right permission.

A pre-commit hook runs the linters and tests locally, so most problems never reach a pull request.

What I would do the same, and differently

  • Same: one class for all HTTP requests. Replacing Trac with GitHub touched very little code because of it.
  • Same: background work for anything slow. It removed a whole class of “the page is slow” problems.
  • Different: start with dynamic blocks and no front-end framework. I added Tailwind and jQuery early and spent a release taking them out.
  • Different: pick block names carefully the first time. They are permanent.
  • Different: add tests and coding standards on day one. Adding them to an existing codebase is more work than writing them along the way.

The plugin is free on WordPress.org and the code is on GitHub. Thanks to the people who sent fixes and presets along the way.

Was this useful?
Nazmul Hasan Robin Avatar

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Your first comment is reviewed before it appears. After that, your comments appear straight away.