What's with ActivityPub + WordPress + Cloudflare not working?

What's with ActivityPub + WordPress + Cloudflare not working?

I’ve been on a journey to get WPwatercooler.com to be on the fediverse and it hasn’t been as easy as I was thinking it was going to be. Folks that run a website and want to use Mastodon to build community around it have a few options for being presented on the Fediverse. This post isn’t going to help you solve this yet but I hope we can work out what needs to be done in order to do so.

Some new developments

Since writing this post, others have discovered solutions to the issues I discuss below. One such solution is provided by Dustin Rue in a blog post Caching WordPress author pages when using ActivityPub, which was extremely helpful in attempting to resolve the Cloudflare problem. However, it should be noted that implementing this solution requires access to the Nginx configuration file. If you are hosting with SpinupWP, which provides such access, Dustin’s post may be useful in resolving this issue.

  1. Make an account on an existing Mastodon instance, share your links to your content there
  2. Build your own Mastodon instance and make an account there and share your content
  3. Use ActivityPub and allow people to follow your website on any Fediverse property

There is most likely more but for the most part its those 3 that you can select from.

I started out with WPwatercooler on Mastodon.online which can be found at https://mastodon.online/@wpwatercooler this works fine and people can follow it but I wanted to see if I could take it a step further and add WPwatercooler.com to the Fediverse directly and this is where ActivityPub comes in.

I installed these three plugins so that I could offer up ActivityPub via WebFinger and people on Mastodon will be able to find the website on there. I created 2 accounts on WPwatercooler.com one for each show wpwatercooler and devbranch and then I did a search for @wpwatercooler@wpwatercooler.com and @devbranch@wpwatercooler.com on my Mastodon instance and was able to find them.

The Problem

The thing I and other folks in our community ran into was that we could follow the accounts and if we tried a bunch sometimes it should register. Seems to be a caching issues I think. Let’s check and see.

WebFinger

In part WebFinger is a protocol that allows for discovery of information about people and things identified by a URI. Information about a person might be discovered via an acct: URI, for example, which is a URI that looks like an email address.

Both of these accounts can be found by going to https://wpwatercooler.com/author/devbranch/ and https://wpwatercooler.com/author/wpwatercooler/ as you can see both of these are authors on the WPwatercooler website like I spoke about before. When someone tries and searches for @wpwatercooler@wpwatercooler.com on Mastodon it accesses https://wpwatercooler.com/.well-known/webfinger?resource=acct:wpwatercooler@wpwatercooler.com and as you can see from the url it looks at the domain name in the .well-known/webfinder and looks for the resource with the account name of wpwatercooler@wpwatercooler.com

Webfinger offers up a json output that looks like the following:

{
“subject”: “acct:wpwatercooler@wpwatercooler.com”,
“aliases”: [
“acct:wpwatercooler@wpwatercooler.com”,
“https://wpwatercooler.com/author/wpwatercooler/”,
“mailto:jason+wpwatercooler@wpwatercooler.com”
],
“links”: [
{
“rel”: “http://webfinger.net/rel/profile-page”,
“href”: “https://wpwatercooler.com/author/wpwatercooler/”,
“type”: “text/html”
},
{
“rel”: “http://webfinger.net/rel/avatar”,
“href”: “https://wpwatercooler.com/wp-content/uploads/2015/06/cropped-WPwatercooler-Avatar-2023-2-96x96.png”
},
{
“rel”: “http://webfinger.net/rel/profile-page”,
“href”: “https://www.wpwatercooler.com”,
“type”: “text/html”
},
{
“rel”: “self”,
“type”: “application/activity+json”,
“href”: “https://wpwatercooler.com/author/wpwatercooler/”
},
{
“rel”: “http://nodeinfo.diaspora.software/ns/schema/2.0”,
“href”: “https://wpwatercooler.com/wp-json/nodeinfo/2.0”
},
{
“rel”: “http://nodeinfo.diaspora.software/ns/schema/1.1”,
“href”: “https://wpwatercooler.com/wp-json/nodeinfo/1.1”
},
{
“rel”: “http://nodeinfo.diaspora.software/ns/schema/1.0”,
“href”: “https://wpwatercooler.com/wp-json/nodeinfo/1.0”
},
{
“rel”: “https://feneas.org/ns/serviceinfo”,
“type”: “application/ld+json”,
“href”: “https://wpwatercooler.com/wp-json/serviceinfo/1.0”,
“properties”: {
“https://feneas.org/ns/serviceinfo#software.name”: “WPwatercooler”
}
},
{
“rel”: “http://schemas.google.com/g/2010#updates-from”,
“href”: “https://wpwatercooler.com/author/wpwatercooler/feed/ostatus/”,
“type”: “application/atom+xml”
},
{
“rel”: “http://ostatus.org/schema/1.0/subscribe”,
“template”: “https://wpwatercooler.com/?profile={uri}”
},
{
“rel”: “feed”,
“type”: “application/stream+json”,
“title”: “Activity-Streams 1.0 Feed”,
“href”: “https://wpwatercooler.com/author/wpwatercooler/feed/as1/”
},
{
“rel”: “feed”,
“type”: “application/activity+json”,
“title”: “Activity-Streams 2.0 Feed”,
“href”: “https://wpwatercooler.com/author/wpwatercooler/feed/as2/”
}
]
}

the section we’re interested in is

{
“rel”: “self”,
“type”: “application/activity+json”,
“href”: “https://wpwatercooler.com/author/wpwatercooler/”
},

This section has the author url and you can see that its using the type of application/activity+json instead of something like text/html so when it queries that url it passes along in the header Accept: application/activity+json and it should return it in that format.

Running the following command

$ url -i -H “Accept: application/activity+json” “https://wpwatercooler.com/author/wpwatercooler”

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 —:—:— —:—:— —:—:—     0HTTP/2 200
date: Tue, 17 Jan 2023 17:42:34 GMT
content-type: text/html; charset=UTF-8
last-modified: Tue, 17 Jan 2023 16:11:53 GMT
cache-control: max-age=0
expires: Tue, 17 Jan 2023 16:40:07 GMT
vary: Accept-Encoding
age: 3746
x-cache: HIT
cf-cache-status: DYNAMIC
server-timing: cf-q-config;dur=9.0000003183377e-06
report-to: {“endpoints”:[{“url”:“https:\/\/a.nel.cloudflare.com\/report\/v3?s=hTOA5wCRdqy4Lkoey48CmI2UIbYym8HWAvtsssC4lRsXgli7%2BchgWImTY3ol%2FdS8gTcodIYZQ6cbSDMVZ1co5brwCJxEpkIdqJ%2FCEjwKcxOWhomHgfWISN9qzmkfkMd7tNo8fQORQ%3D%3D”}],“group”:“cf-nel”,“max_age”:604800}
nel: {“success_fraction”:0,“report_to”:“cf-nel”,“max_age”:604800}
server: cloudflare
cf-ray: 78b0e3631e862ea3-LAX
alt-svc: h3=“:443”; ma=86400, h3-29=“:443”; ma=86400

Cloudflare returns back text/html and outputs the author page and not the json we wanted. Talking with a few folks on Mastodon we found that people are having this issue with using Cloudflare and there isn’t anything we can do to resolve it sadly. I tried excluding the caching for the specific paths /author/(.*) and other such tricks and it keeps changing the content-type to text/html and not application/activity+json as requested.

Looking online I’ve found a bunch of people using Cloudflare + ActivityPub running into this issue and other technologies that also use Accept: application/activity+json and finding that it only returns text/html. Bummer.

The code that makes all that work can be found in the WordPress ActivityPub plugin class-activity-pub.php

/**
	 * Return a AS2 JSON version of an author, post or page.
	 *
	 * @param  string $template The path to the template object.
	 *
	 * @return string The new path to the JSON template.
	 */
	public static function render_json_template( $template ) {
		if ( ! \is_author() && ! \is_singular() && ! \is_home() ) {
			return $template;
		}

		// check if user can publish posts
		if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) {
			return $template;
		}

		if ( \is_author() ) {
			$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
		} elseif ( \is_singular() ) {
			$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
		} elseif ( \is_home() ) {
			$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
		}

		global $wp_query;

		if ( isset( $wp_query->query_vars['activitypub'] ) ) {
			return $json_template;
		}

		if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) {
			return $template;
		}

		$accept_header = $_SERVER['HTTP_ACCEPT'];

		if (
			\stristr( $accept_header, 'application/activity+json' ) ||
			\stristr( $accept_header, 'application/ld+json' )
		) {
			return $json_template;
		}

		// Accept header as an array.
		$accept = \explode( ',', \trim( $accept_header ) );

		if (
			\in_array( 'application/ld+json; profile=“https://www.w3.org/ns/activitystreams”', $accept, true ) ||
			\in_array( 'application/activity+json', $accept, true ) ||
			\in_array( 'application/ld+json', $accept, true ) ||
			\in_array( 'application/json', $accept, true )
		) {
			return $json_template;
		}

		return $template;
	}

Can this be done?

After discussing this with @tomfinley@wptoots.social and @tim@mastodon.timnolte.com on mastodon in this tread https://simian.rodeo/@jasontucker/109701436056062907 we found that there is an endpoint that will return json https://wpwatercooler.com/author/wpwatercooler/activitypub

I’m wondering if instead we can force Webfinger to return that url with /activitypub at the end instead of expecting Cloudflare to return a non text/html content type. You can see this JSON output here https://wpwatercooler.com/author/wpwatercooler/activitypub/

“id”: “https://wpwatercooler.com/author/wpwatercooler/”,
“type”: “Person”,
“name”: “WPwatercooler Show”,
“summary”: “The world’s most influential WordPress podcast brought to you by Jason Tucker, Sé Reed & Jason Cosper”,
“preferredUsername”: “wpwatercooler”,
“url”: “https://wpwatercooler.com/author/wpwatercooler/”,
“icon”: {
“type”: “Image”,
“url”: “https://wpwatercooler.com/wp-content/uploads/2015/06/cropped-WPwatercooler-Avatar-2023-2-120x120.png”
},
“inbox”: “https://wpwatercooler.com/wp-json/activitypub/1.0/users/6260/inbox”,
“outbox”: “https://wpwatercooler.com/wp-json/activitypub/1.0/users/6260/outbox”,
“followers”: “https://wpwatercooler.com/wp-json/activitypub/1.0/users/6260/followers”,
“following”: “https://wpwatercooler.com/wp-json/activitypub/1.0/users/6260/following”,
“manuallyApprovesFollowers”: false,
“publicKey”: {
“id”: “https://wpwatercooler.com/author/wpwatercooler/#main-key”,
“owner”: “https://wpwatercooler.com/author/wpwatercooler/”,

What’s involved to make this work on Cloudflare? I’d love to hear in the comments and we can see about getting this combo of WordPress + CloudFlare + ActivityPub working, I’ve seen so many people complain about this not working with this very popular combo of technologies in the WordPress support forums.