{"id":25,"date":"2026-10-29T14:00:00","date_gmt":"2026-10-29T08:00:00","guid":{"rendered":"https:\/\/nazmulrobin.com\/blog\/?p=25"},"modified":"2026-10-26T00:46:20","modified_gmt":"2026-10-25T18:46:20","slug":"how-i-built-nhr-core-contributions","status":"publish","type":"post","link":"https:\/\/nazmulrobin.com\/blog\/how-i-built-nhr-core-contributions\/","title":{"rendered":"How I built NHR Core Contributions"},"content":{"rendered":"\n<p>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 <a href=\"https:\/\/wordpress.org\/plugins\/nhrrob-core-contributions\/\">NHR Core Contributions<\/a>, a plugin where you enter a WordPress.org username and add blocks.<\/p>\n<p>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.<\/p>\n<h2>What it shows<\/h2>\n<ul>\n<li><strong>A list of core contributions:<\/strong> every core changeset that gives the username props, with component, ticket, changeset and date. It can list Gutenberg commits too.<\/li>\n<li><strong>A contributor card and headline stats:<\/strong> totals, first and latest contribution, top components.<\/li>\n<li><strong>Activity per year<\/strong> as a bar chart.<\/li>\n<li><strong>Release badges:<\/strong> every WordPress release whose credits include the username.<\/li>\n<li><strong>WordPress.org badges<\/strong> from the public profile.<\/li>\n<li><strong>Plugins and themes<\/strong> the user has published.<\/li>\n<\/ul>\n<h2>Where the data comes from<\/h2>\n<p>There is no single API for &#8220;what did this person contribute&#8221;. The plugin puts it together from four public sources.<\/p>\n<p><strong>Core commits, from GitHub.<\/strong> Core development happens on Trac and SVN, but every commit is mirrored to <code>WordPress\/wordpress-develop<\/code> on GitHub. A core commit message names its contributors in a <code>Props<\/code> line, so GitHub&#8217;s commit search can find them:<\/p>\n<pre><code class=\"language-php hljs\">$query = <span class=\"hljs-string\">'repo:WordPress\/wordpress-develop props \"'<\/span> . $username . <span class=\"hljs-string\">'\"'<\/span>;<\/code><\/pre>\n<p><strong>Gutenberg commits, also from GitHub.<\/strong> Gutenberg credits people differently. Each contributor gets a <code>Co-authored-by<\/code> trailer with an address made from their WordPress.org username, so the search is for <code>\"username@git.wordpress.org\"<\/code> in <code>WordPress\/gutenberg<\/code>.<\/p>\n<p><strong>Release credits and published plugins and themes,<\/strong> from the <code>api.wordpress.org<\/code> endpoints.<\/p>\n<p><strong>Name, avatar and badges,<\/strong> 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.<\/p>\n<h2>The day the data source broke<\/h2>\n<p>Until this month the contribution list came from Core Trac directly. Then Trac started blocking automated requests, and every username showed zero contributions.<\/p>\n<p>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.<\/p>\n<p>The lesson is an old one. If your plugin depends on reading someone else&#8217;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.<\/p>\n<h2>One door for every request<\/h2>\n<p>All HTTP requests go through a single method, <code>Remote::get( $url, $ttl, $parse )<\/code>. It does four things:<\/p>\n<ul>\n<li><strong>It caches the parsed result,<\/strong> not the raw response, in a transient.<\/li>\n<li><strong>It caches failures too,<\/strong> 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.<\/li>\n<li><strong>It uses short timeouts:<\/strong> five seconds when a visitor is waiting, ten for background work.<\/li>\n<li><strong>It sends the optional GitHub token to GitHub only.<\/strong><\/li>\n<\/ul>\n<pre><code class=\"language-php hljs\"><span class=\"hljs-keyword\">if<\/span> ( ! is_array( $data ) ) {\n    set_transient( $key, [ <span class=\"hljs-string\">'nhrcc_error'<\/span> =&gt; <span class=\"hljs-number\">1<\/span> ], <span class=\"hljs-keyword\">self<\/span>::FAILURE_TTL );\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">self<\/span>::error();\n}<\/code><\/pre>\n<p>Each source is a small class that builds a URL and parses the answer. None of them makes a request by itself.<\/p>\n<h2>Never make a visitor wait<\/h2>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<h2>Blocks without front-end JavaScript<\/h2>\n<p>Earlier versions shipped Tailwind CSS and jQuery scripts to the front end. Version 2.0 removed both.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>One detail for anyone maintaining a block plugin: the original block&#8217;s name dates from the first version, and I cannot change it. Renaming a block breaks every post that already uses it. The old <code>preset<\/code> attribute is still read and mapped to the new block styles at render time, without rewriting saved posts.<\/p>\n<h2>Ready for AI agents<\/h2>\n<p>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 <code>nhrcc\/get-core-contributions<\/code> and <code>nhrcc\/get-contribution-summary<\/code>.<\/p>\n<p>Each one declares its input and output and is marked as read-only:<\/p>\n<pre><code class=\"language-php hljs\"><span class=\"hljs-string\">'permission_callback'<\/span> =&gt; [ <span class=\"hljs-keyword\">$this<\/span>, <span class=\"hljs-string\">'can_read'<\/span> ],\n<span class=\"hljs-string\">'meta'<\/span>                =&gt; [\n    <span class=\"hljs-string\">'annotations'<\/span> =&gt; [\n        <span class=\"hljs-string\">'readonly'<\/span>    =&gt; <span class=\"hljs-keyword\">true<\/span>,\n        <span class=\"hljs-string\">'destructive'<\/span> =&gt; <span class=\"hljs-keyword\">false<\/span>,\n        <span class=\"hljs-string\">'idempotent'<\/span>  =&gt; <span class=\"hljs-keyword\">true<\/span>,\n    ],\n],<\/code><\/pre>\n<p>They return the same data as the blocks and need a signed-in user who can edit posts. The registration is wrapped in a <code>function_exists( 'wp_register_ability' )<\/code> check, so the plugin still runs on WordPress versions before 6.9.<\/p>\n<h2>Keeping it safe<\/h2>\n<p>The plugin takes a username from a user and data from other websites, so both are treated as untrusted.<\/p>\n<ul>\n<li><strong>The username<\/strong> is reduced to lowercase letters, digits, dot, underscore and hyphen before it goes anywhere near a URL. The hosts are fixed strings.<\/li>\n<li><strong>Remote data<\/strong> has its tags stripped when parsed, and every value is escaped on output.<\/li>\n<li><strong>Every REST route<\/strong> needs <code>manage_options<\/code>, and the GitHub token is never returned by the API, only whether one is saved.<\/li>\n<\/ul>\n<h2>What checks every change<\/h2>\n<ul>\n<li><strong>WordPress Coding Standards and PHP compatibility checks,<\/strong> with the plugin tested on PHP 7.4 through 8.6.<\/li>\n<li><strong>Unit tests<\/strong> that need no database and no network.<\/li>\n<li><strong>Plugin Check,<\/strong> the same tool the WordPress.org review team uses.<\/li>\n<li><strong>A security workflow<\/strong> with static analysis and a probe that checks every endpoint asks for the right permission.<\/li>\n<\/ul>\n<p>A pre-commit hook runs the linters and tests locally, so most problems never reach a pull request.<\/p>\n<h2>What I would do the same, and differently<\/h2>\n<ul>\n<li><strong>Same: one class for all HTTP requests.<\/strong> Replacing Trac with GitHub touched very little code because of it.<\/li>\n<li><strong>Same: background work for anything slow.<\/strong> It removed a whole class of &#8220;the page is slow&#8221; problems.<\/li>\n<li><strong>Different: start with dynamic blocks and no front-end framework.<\/strong> I added Tailwind and jQuery early and spent a release taking them out.<\/li>\n<li><strong>Different: pick block names carefully the first time.<\/strong> They are permanent.<\/li>\n<li><strong>Different: add tests and coding standards on day one.<\/strong> Adding them to an existing codebase is more work than writing them along the way.<\/li>\n<\/ul>\n<p>The plugin is free on <a href=\"https:\/\/wordpress.org\/plugins\/nhrrob-core-contributions\/\">WordPress.org<\/a> and the code is on <a href=\"https:\/\/github.com\/nhrrob\/nhrrob-core-contributions\">GitHub<\/a>. Thanks to the people who sent fixes and presets along the way.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>NHR Core Contributions is my plugin that shows a contributor&#8217;s WordPress core work on their own site. This is how it works inside, from where the data comes from to how it stays fast.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[20,23,16],"class_list":["post-25","post","type-post","status-publish","format-standard","hentry","category-wordpress","tag-core-contribution","tag-gutenberg-blocks","tag-plugin-development"],"_links":{"self":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts\/25","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/comments?post=25"}],"version-history":[{"count":1,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts\/25\/revisions"}],"predecessor-version":[{"id":38,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/posts\/25\/revisions\/38"}],"wp:attachment":[{"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/media?parent=25"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/categories?post=25"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nazmulrobin.com\/blog\/wp-json\/wp\/v2\/tags?post=25"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}