Trigger Indexing API for Contact on Linked User Update

The Content - Indexing API plugin is designed to issue an indexing request to Google and/or Bing (via IndexNow) on content updates.  It was built to initiate indexing on the built-in Joomla content types, but there are many 3rd party components that can generate content.  In this example, we'll examine a unique combination in core Joomla components that I chose NOT to implement in Content - Indexing API specifically to allow me to use it as an example for 3rd party triggers.  This way, we can go through all of the parts of this plugin using features present in ALL Joomla installations.

But Why Would You Want To Do That?

For those reading and asking themselves "why would I need to update a contact after saving a user?" it is for this reason.  User-linked contacts can display user custom fields.  That means, if a user updates a field, the contact page will display the new information.  Post-update, we want the search engines to re-index these update contact pages.

The actual trigger for the Content - Indexing API plugin is like 3 lines of code - it's super easy, barely an inconvenience.  So the bulk of this document will describe WHY we're doing the things we're doing, to give you an idea of what you might need to do with your plugin for your custom content.

A Indexing API User Plugin

  User - Indexing API User/Contact 5.1.130

Joomla\Plugin\User\IndexingAPIUser

I'm not going through ever line of the code in this document, but I will focus on key features that your plugin MUST have in order to activate the trigger. The plugin is named IndexingAPIUser - the name is not important to the function, name yours whatever you like.  I will probably follow this naming convention IndexingAPI+PluginType+Whatever-else-I-need-to-make-it-distinct-and-self-explanatory.  Probably not that long though.

First, a description of the basic plugin setup and features.

indexingapiuser.xml

The plugin XML has only one config field - a usergrouplist field named validgroups.  This field allows me to filter only users who I want to process.  This might not be important to you, it's there because I thought it would be useful.

Your configuration file may or may not require any config fields.  Remember, this is just a reference example that happens to be a practical application.

indexingapi/src/Extension/IndexingAPIUser.php

The plugin itself is constructed like any User plugin.  It extends CMSPlugin, it has a namespace, it has use statements.  Specifically, you will need one key use statement to trigger indexing:

use Joomla\CMS\Event\AbstractEvent;

This particular plugin also requires the com_contact RouteHelper class, which we load as ContactRouteHelper

use Joomla\Component\Contact\Site\Helper\RouteHelper as ContactRouteHelper;

public function onUserAfterSave

The function begins as you might expect, with tests to determine if it should exit prior to indexing.  It tests that the user id is present, that the save was successful and that the user is not new (because a new user won't be associated with a contact yet.)  It then goes on to test that the user id has a valid Joomla user, and that the Joomla user is a member of one of the valid groups selected in the config.

Next, the plugin does a database query to retrieve any contacts that are linked to this user.  This is the last test.  If there are no linked contacts, the plugin exits.

Testing the Contacts - the Last Step Before Indexing

Each of the contact records selected is then tested to ensure that they are:

  1. Published
  2. In the Public access level

If either of these tests fail - the contact is not sent to the index trigger.

private function sendToIndexingAPI

Before someone asks - this function name is arbitrary, you could all it HaroldDoesStuff if you wanted. Indexing API has no opinion.

The DB contact object is sent to the indexer function.  The contents of the object are simple, just enough to test the access level and published state, and enough to get a route from ContactRouteHelper (described above).  For the Contact RouteHelper::getContactRoute function, we only selected from the database the needed fields - contact ID, Category ID, Language, published, and access, so our object looks like this:

$contact = (obj)[
  "id"=>1,
  "catid"=>1,
  "language"=>"*",
  "published"=>1,
  "access"=>1
];

RouteHelper::getContactRoute actually only needs the first 3 properties to run.

With this object, the plugin constructs another object containing the only property that the indexer cares about - the link property:

$data = (object)[
    'link' => ContactRouteHelper::getContactRoute($contact->id, $contact->catid, $contact->language)
];

Next, we configure the arguments for the Content plugin event, (which is onContentAfterSave).  This requires 3 arguments, one of which is the $data variable we just created.

$contentEventArguments = [
    'context' => 'IndexingAPIExternalTrigger',
    'subject' => $data,
    'isNew' => false  
];

Notice the context value.  This is critical to the trigger.  The Content - Indexing API plugin listens to various normal contexts, and this one very specific context that is used ONLY for external triggers.  It's this context, or my plugin ignores it!  Got it? Good!

Now the fun part - the star of the show, triggering the indexer.  These are the 3 lines of code that separate your plugin that triggers the indexer from those who don't.

This particular implementation triggers ONLY the Content - Indexing API plugin event.  No other onContentAfterSave Content plugin events are fired - so we can do whatever we want here.  This is why I can safely send this very sparse data element to a Content plugin event without worrying that other Content plugins will puke because it's missing a key property.  Here we go, I'll explain each line as necessary afterward.

$dispatcher = $this->app->getDispatcher();
PluginHelper::importPlugin('content','indexingapi',true, $dispatcher);
$dispatcher->dispatch('onContentAfterSave', AbstractEvent::create('onContentAfterSave',$contentEventArguments));

First, it's the dispatcher that loads and triggers events, so we get it from the application.

Next, we import ONLY the plugin we want to run, in this case the content indexingapi plugin. 

Finally, we dispatch the event, with the arguments we just put together.

Like editing any regularly triggered content, your custom plugin will ALSO display the status messages (URL + Google and/or Bing statuses).

Indexing Status

This is too much!!!

That's OK, not everyone is a developer.  I see these things as puzzles to be solved, and I can be bribed to solve your puzzles for you.  My criteria for doing it is relatively simple.

  1. If the extension you want me to interface with is a commercial extension, you must purchase a license for me to use.  I will not use your licensed software.
  2. If you want exclusive rights to the resulting plugin, you're going to pay for it.
  3. If you release all rights to the resulting software, allowing me to publish it for free - you get a discount.

Closed Source (you own it): $400
Open Source (I own it): $200

Click the Contact Us link in the footer and send me a message.

Frequently Asked Questions:

If you're charging to develop plugins that interface with Content - Indexing API, then why did you make a tutorial?

Because I don't want to build these plugins. I want it to be so easy for 3rd party developers to do it, that they just do it and recommend users to come to me. The $200/$400 options are so that I can be compensated for the inconvenience of dealing with someone else's laziness.

Did you just call me lazy?

No, I called 3rd party developers lazy for not wanting to spend the hour it takes to write this plugin.

But I am a 3rd party developer, did you just call me lazy.

Oh, I supposed that I did.

If I pay $400 for exclusive rights, can I sell it on my own website?

Sure, that's what you're paying for. If you ever need updates, however, it's going to be another $400 to have me make them AND keep it closed.

What are you going to do with the $200 plugin I paid for?

I'm going to put it on my website for everyone to use, as an example of my work, and as a gift to the Joomla community that has given me so much. You'll get credit too.

Your sales pitch sucks.

Yes, I'm not a salesman - I'm a developer. I can't stand salesmen. I don't want to be a salesman. My offers are black and white - take it or don't.

Why is this software free?

I’m ditching the freemium game and giving this software to the Joomla crowd for free. It’s a nod to “Jumla”—Swahili for “all together”—because fragmentation sucks, and I’d rather focus on innovation and paid gigs. Use it, build with it, and if you need custom work, I’m super into that.

What's The Catch?

There isn’t one! I’m all about building tools that empower the Joomla community and spark creativity. This software’s free because I’d rather see it in your hands - fueling awesome projects. If you really feel like paying something, I’d appreciate a review in the Joomla Extension Directory—your feedback means a lot!