PHPackages                             pfefferle/wordpress-blockroll - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. pfefferle/wordpress-blockroll

ActiveWordpress-plugin

pfefferle/wordpress-blockroll
=============================

A blogroll block with XFN, h-cards, OPML import/export, and blogroll discovery.

010↑2600%[1 PRs](https://github.com/pfefferle/wordpress-blockroll/pulls)PHPCI failing

Since Aug 1Pushed todayCompare

[ Source](https://github.com/pfefferle/wordpress-blockroll)[ Packagist](https://packagist.org/packages/pfefferle/wordpress-blockroll)[ RSS](/packages/pfefferle-wordpress-blockroll/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

Blockroll
=========

[](#blockroll)

A blogroll block for WordPress. You add a list of sites you read or people you know, and it renders them as [h-cards](https://microformats.org/wiki/h-card) with [XFN](https://gmpg.org/xfn/) relationships, lets visitors sort and page through them, and publishes the whole thing as an OPML feed so other people can subscribe to your list.

The name is a bad pun: a Gutenberg *block* that renders a *blogroll*.

What it does
------------

[](#what-it-does)

- **One block, `blockroll/blogroll`.** No options page, no global config. Everything lives in the block.
- **Add a link, get the rest for free.** Paste a URL and it tries to find the site's feed, name, description, and avatar for you. You can edit any of it afterwards.
- **XFN and h-cards.** Each entry is marked up as an h-card, and the relationships you pick (friend, met, colleague, and so on) are written as `rel` values on the link.
- **Sorting and paging on the frontend.** Both are plain links with query parameters, rendered and handled entirely in PHP. There is no JavaScript on the frontend. You can turn the visitor-facing sorting off per block.
- **OPML out.** The block exposes its links as OPML at `{page}/feed/opml`. The site root, `/feed/opml`, is a directory that lists all of those per-page OPMLs.
- **Blogroll discovery.** Pages with a blogroll advertise their own OPML in the head with ``, following [Dave Winer's proposal](https://danq.me/2024/05/03/23615/), and the front page advertises them too, so readers can find the blogroll from the homepage. The site-root directory is never advertised this way, since it is a list of OPMLs rather than a blogroll.
- **OPML in.** You can seed a block by importing an OPML file, for example the export from your feed reader.

How it works
------------

[](#how-it-works)

### The block stores everything itself

[](#the-block-stores-everything-itself)

There is no settings page and no database table. All the link data is saved in the block's attributes, inside the post content:

```

```

Each link holds `url`, `name`, `description`, `feedUrl`, `photo`, an `xfn` array, and the date it was added. The block also keeps a default sort order, a page size, whether to show site icons, and whether visitors may re-sort the list.

### Auto-fill and import happen on the server

[](#auto-fill-and-import-happen-on-the-server)

A browser cannot fetch other people's sites directly, so the editor asks the plugin to do it. There are two REST routes:

- `POST blockroll/v1/discover` takes a URL, fetches it, and pulls out the feed (from ``), the name and description (from an h-card, then the feed, then the page title), and an avatar (from an h-card photo, then the favicon).
- `POST blockroll/v1/import` takes an OPML file, paste, or URL and turns its `` entries into links. Fetching extra details for each imported link is opt-in, so importing a 200-feed OPML does not fire 200 requests unless you ask it to.

### The frontend is PHP only

[](#the-frontend-is-php-only)

The block is rendered by `render.php`. Every link becomes an h-card list item with the XFN relationships as `rel`:

```

  Example
  A blog about examples
  …
  feed

```

The feed icon links with the `feed:` protocol, so a click lands in the visitor's feed reader; the "feed" text links to the plain URL. The chosen relationships are also shown as a small list under each entry.

Sorting and paging are links with the query parameters `blockroll-sort` and `blockroll-page`, so each click is a normal page load and only the current page of links is in the HTML. Sort options only show up when they make sense: "Newest first" needs links with dates, and the manual order is only offered (as "Default") when it is the block's own default. There is no frontend JavaScript at all.

### OPML and discovery

[](#opml-and-discovery)

The plugin registers an `opml` feed. On a single post or page it emits the OPML for that page's blogroll. At the site root it emits a directory OPML that lists each per-page OPML with ``, so a reader gets one entry per blogroll page rather than one big merged list.

To find those pages cheaply, the plugin keeps a private taxonomy up to date on save: a post gets the term when it contains the block and loses it when it does not. The link data still lives in the block, the taxonomy is only an index.

Pages that have a blogroll add the discovery link to their head, pointing at their own OPML, and the front page repeats those links. The root directory URL never appears in a head:

```

```

File layout
-----------

[](#file-layout)

```
wordpress-blockroll/
├── blockroll.php              # plugin bootstrap
├── includes/
│   ├── class-discovery.php    # extracts feed, name, description, photo from HTML
│   ├── class-import.php       # OPML parsing
│   ├── class-links.php        # link normalizing and sorting
│   ├── class-opml.php         # opml feed + head discovery links
│   ├── class-index.php        # private taxonomy, kept in sync on save
│   ├── class-xfn.php          # XFN vocabulary and rel helper
│   └── rest/
│       ├── class-discovery-controller.php   # POST blockroll/v1/discover
│       └── class-import-controller.php      # POST blockroll/v1/import
├── templates/
│   ├── opml.php               # OPML of one blogroll page
│   └── opml-directory.php     # directory of all blogroll pages
├── src/blogroll/
│   ├── block.json             # block metadata and attributes
│   ├── render.php             # PHP frontend rendering
│   ├── index.js               # registerBlockType
│   ├── edit.js                # editor UI
│   ├── components/            # link form, import modal, XFN control
│   ├── editor.scss
│   └── style.scss
├── build/                     # compiled assets (committed)
├── tests/                     # PHPUnit and Jest tests
├── package.json
├── composer.json
└── README.md

```

Development
-----------

[](#development)

```
composer install
npm install

npm run build         # compile the block assets (build/ is committed)
npm run dev           # watch mode
npm run env-start     # wp-env on ports 8833/8834
composer test:wp-env  # PHPUnit inside wp-env
npm run test:unit     # Jest
npm run lint:js
npm run lint:css
composer lint         # PHP CodeSniffer (WPCS)
```

Status
------

[](#status)

Early.

License
-------

[](#license)

GPL-2.0-or-later.

###  Health Score

23

↑

LowBetter than 25% of packages

Maintenance65

Regular maintenance activity

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/2e3d56273f319d19d7492f8ba06a01fd64a5724e3553b2d1f2ac0f4e399b2cfa?d=identicon)[pfefferle](/maintainers/pfefferle)

---

Top Contributors

[![pfefferle](https://avatars.githubusercontent.com/u/2373?v=4)](https://github.com/pfefferle "pfefferle (61 commits)")

### Embed Badge

![Health badge](/badges/pfefferle-wordpress-blockroll/health.svg)

```
[![Health](https://phpackages.com/badges/pfefferle-wordpress-blockroll/health.svg)](https://phpackages.com/packages/pfefferle-wordpress-blockroll)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
