Skip to content
Corex

User guides

Corex Guides puts a Guides screen in the admin. Corex registers its own guides through it — reading and answering messages, publishing a post, checking whether an email was sent — and your site registers its own through exactly the same public API.

This matters because a client’s guide is about the client’s site. It documents their post types and their flows, it ships with their plugin, and it must survive a Corex upgrade without anybody editing Corex.

Terminal window
wp corex make:guide Projects

That writes Guides/ProjectsGuide.php in your plugin, with a topic, two steps, and a warning — the parts authors skip when starting from an empty file.

From your service provider’s boot():

use Corex\Guides\GuideRegistry;
use Corex\Support\Facades\Corex;
Corex::onReady( static function (): void {
Corex::make( GuideRegistry::class )->registerDeferred(
static fn (): array => [ ( new ProjectsGuide() )->guide() ]
);
} );

Both exist, and only one is safe from a plugin.

Corex boots on plugins_loaded at priority 10. Your plugin very likely boots on plugins_loaded at priority 10 too — the generated starter does. Which of you WordPress loads first depends on your plugin’s directory name, so register() works or silently does nothing depending on what you called your folder.

registerDeferred() takes a factory and runs it the first time anything reads the registry, which is an admin request — long after every plugin has loaded. There is no race left to lose.

use Corex\Guides\Guide;
use Corex\Guides\GuideStep;
use Corex\Guides\GuideTopic;
Guide::for( 'projects', __( 'Managing projects', 'perego' ) )
->withSummary( __( 'Add a project, choose where it appears, and publish it.', 'perego' ) )
->inSection( 'content' )
->onScreen( 'edit.php?post_type=perego_project' )
->requiring( 'edit_posts' )
->withTopic( GuideTopic::for(
'add',
__( 'Add a project', 'perego' ),
'',
[
new GuideStep(
__( 'Choose Projects, then Add Project.', 'perego' ),
__( 'The editor opens with an empty project.', 'perego' ),
),
new GuideStep(
__( 'Choose Publish.', 'perego' ),
__( 'The project appears on the Projects page.', 'perego' ),
warning: __( 'Publishing makes it public immediately.', 'perego' ),
),
],
) );
MethodEffect
inSection()Groups the guide on the Guides screen. Sections appear in key order, alphabetically — that is the only lever you have over where your section lands.
onScreen()The admin address the guide describes. The Guides screen renders it as a link straight to that screen, so a reader who has finished the steps is one click from doing them. Use the address WordPress itself uses: edit.php for posts, edit.php?post_type=… for anything else.
requiring()The capability a reader needs. Anyone without it never sees the guide — they could not follow it. Omit it and the guide is shown to anyone who can open the admin.
ordered()Position within the section. Lower first; the default is 50.

A step is an instruction and an expected result, not a paragraph. The pair is what makes a step checkable: somebody does the first and can tell from the second whether it worked. A guide written as prose reads fine and leaves people stuck with no way to know they are stuck.

Add warning: to anything hard to undo. It renders before the instruction — a caution somebody meets after the fact is decoration.

Guides are keyed by id, so registering your own under a Corex id replaces it:

Guide::for( 'corex-publishing', __( 'How we publish here', 'perego' ) )

If you cannot reach the container, use the filter. Anything returned that is not a Guide is discarded rather than rendered — this screen is what somebody opens when they are already stuck.

add_filter( 'corex_guides', static function ( array $guides ): array {
$guides[] = ( new ProjectsGuide() )->guide();
return $guides;
} );

A step can carry one:

use Corex\Guides\GuideScreenshot;
new GuideStep(
__( 'Choose Projects.', 'perego' ),
__( 'The list opens.', 'perego' ),
screenshot: new GuideScreenshot( 'projects-list', __( 'The Projects list.', 'perego' ) ),
);

The alt text is required, not optional — a screenshot with no text alternative is a step a screen-reader user simply does not get.

The capture id links the image to the script that produces it in both directions: the script fails loudly for an id it was asked for and could not capture, and a reviewer can find the code behind any image in the tree. Corex captures its own with node tests/e2e/capture-guide-screenshots.mjs; add your ids to your own equivalent so your images cannot quietly stop matching your product.

There is nothing to do. Register your guides from your own service provider, and a deactivated plugin registers nothing — so its guides are simply absent. No is_active() check, and no list of plugin slugs to keep in step with reality.

A reader who did not find their answer needs a person, not another page. The Guides screen ends with a Still stuck? panel: a category, a message, and their email, sent to the address the site owner configured.

Two ways, and the second wins:

  1. In code. addons/corex-guides/config/guides.php — one line.

    return [
    'support' => [
    'email' => 'support@example.com',
    'enabled' => true,
    ],
    ];
  2. In the admin. CoreX Settings → Guides → Support email. This writes the corex_guides_support_email option, which layers over the file, so a site owner can change it without a deploy. Leave it blank to fall back to the shipped address.

Setting Offer the support form to off removes the panel entirely. That is different from leaving the address blank: with the panel on and no deliverable address, the screen says support is not set up here rather than drawing a form that would discard what somebody typed.

Through Corex Mail when addons/corex-email is active, and through wp_mail() when it is not. Neither is required — a help form must not stop working because a site does not use the mail add-on. The reader’s own address becomes the Reply-To, if it validates; if it does not, the header is dropped rather than passed through.

The outcome is reported honestly. If the transport refuses the message, the panel says it did not send and gives the text back, rather than showing a confirmation for something nobody received.

  • It does not retry. One message per user per minute, enforced server-side, so a double-click or a replayed POST does not arrive twice.
  • It does not store anything. The message is an email. If you want a record, route the address to a shared inbox.
  • It carries no attachments — text, the sender, and the site URL.