Better Disqus Comments has a stable extension surface — PHP actions and filters, JavaScript slots in the settings screen, REST endpoints, and an option schema that hasn't changed since version 10. Everything on this page is public API.
PHP examples can go in your theme's functions.php or a small plugin.
The plugin's slug, text domain, option names and hook prefix (dcl_) date from its old name, Disqus Conditional Load, and are kept for compatibility. The PHP namespace is FoxeLabs\DCL.
FoxeLabs\DCL\├── Core Boot orchestrator — wires every module, fires dcl_running├── Plugin Slug, settings page slug and URL, admin screen ID├── Settings dcl_gnrl_options: defaults, access, sanitising, REST schema├── Account dcl_disqus_account: shortname and API credentials├── Front\│ ├── Controller Boots the front-end classes, registers the shortcodes│ ├── Detector "Can Disqus load here, and how?" — one answer per request│ ├── Renderer Thread markup, button, shortcode│ ├── TemplateReplacer Swaps the classic comments template│ ├── BlockReplacer Swaps the core/comments block│ ├── Assets Front-end script and inline styles│ ├── Count Comment-count markers│ └── LinkRewriter Points comment links at #disqus_thread├── Embed\Config Identifier, URL, title and embed config per post├── Sync\{Webhook, Manager,│ Mapper, Exporter,│ ApiService, Log} Comment sync, manual sync and export├── Addons\{Addons, Catalog,│ Bundle} Freemius wiring, add-on catalogue, Pro Bundle license├── Admin\… Menu, settings screen, notices, toolbar menu├── Api\… REST controllers under dcl/v1├── Compat\Manager Official Disqus plugin detection, WooCommerce reviews├── Setup\… Activation, deactivation, data migrations└── Utils\… Permission check, caches, asset helpers
Front-end classes load only outside the admin, and the Admin classes, apart from the toolbar menu, only inside it. The rest loads on every request.
The recommended way to extend the plugin:
Declare your add-on with dcl_addons, or hook into dcl_running, so your code runs after the plugin has booted.
Use the documented filters and actions.
For settings, declare cards and fields with the dcl_settings_schema PHP filter and keep the values in your own show_in_rest option. No JavaScript needed.
Filter. Whether the Disqus embed loads on this request. Computed once per post per request.
The built-in rules, in order: a shortname is set; not a feed; not an AMP page; dsq_can_load doesn't return false; a singular view of a post; comments open; the post isn't waiting for its password; the post isn't a draft, pending, scheduled, auto-draft or trashed; the post type isn't excluded; and the visitor isn't a bot (a visitor with no user agent counts as one), unless Load comments for search engine bots is on.
php
// Never load Disqus on posts in the "announcements" category.add_filter( 'dcl_can_load', function ( $can_load ) { return is_singular( 'post' ) && has_category( 'announcements' ) ? false : $can_load;} );
Parameter
Type
Description
$can_load
bool
Result of the built-in rules.
$post
WP_Post|null
The post being checked: the one passed to the check, or else the global post. null when there's none.
When it returns false, the theme's normal WordPress comments show instead.
Filter. Default true: WooCommerce products keep their own reviews template and are excluded from Disqus. Return false to let Disqus replace product reviews. The Comments for WooCommerce add-on uses this.
Filter kept from the official Disqus plugin, so existing snippets keep working. Called with 'embed' for the thread and 'count' for comment counts; returning exactly false blocks that part.
Filter. The resolved loading method for this visitor, after the mobile method and the fallback for inactive add-ons: scroll, click, normal or an add-on's method.
php
// Button on long reads only.add_filter( 'dcl_load_method', function ( $method ) { $words = str_word_count( wp_strip_all_tags( get_post_field( 'post_content' ) ) ); return $words > 3000 ? 'click' : $method;} );
Filter. The methods offered in the settings, as slug => array( 'label' => …, 'description' => … ). A stored method missing from this list falls back to scroll. See Adding a load method.
Filter. The Disqus shortname. Useful on multisite or staging:
php
// Keep staging comments out of the live forum.add_filter( 'dcl_shortname', function ( $shortname ) { return 'staging' === wp_get_environment_type() ? 'example-staging' : $shortname;} );
Filter. Registered button styles, as class => label. Empty in the free plugin; the Advanced Buttons add-on registers its six here. A stored style only takes effect while it's registered.
The thread is printed from templates/disqus-comments.php; a blanked comments area uses templates/empty-comments.php. Themes can't override these files — use the hooks above, or filter comments_template at a priority above 100 (the plugin's own swap runs at 100).
[dcl-comments] shows the Disqus thread where you put it, and blanks the theme's comments area further down the page so the thread shows once. [js-disqus] is an older alias.
It shows nothing where Disqus can't load — archives, closed comments, excluded post types.
Filter. Return true to skip the shortcode's checks. The shortcode then prints the Disqus thread wherever it's used — off single posts too, and where dcl_can_load says no — as long as there's a post to build the embed for and a shortname is set.
Filter. The per-post embed configuration handed to the front-end script.
php
// Force the Disqus interface language.add_filter( 'dcl_embed_vars', function ( $vars, $post ) { $vars['disqusConfig']['language'] = 'de'; return $vars;}, 10, 2 );
Key
Description
disqusShortname
The shortname
disqusIdentifier
The thread identifier
disqusUrl
The post URL
disqusTitle
The post title
disqusConfig
integration. Add language to set the Disqus interface language.
postId
The post ID
Changing the identifier
Changing disqusIdentifier detaches existing threads, and comment counts and sync keep using the original identifier. Don't change it on an existing site.
A window.disqus_config function your site defines is kept. The plugin sets the page URL, identifier, title, integration and language, then calls your function, so your values win. Define it before the footer:
html
<script>window.disqus_config = function () { this.callbacks.onNewComment = [ function (comment) { console.log('New comment', comment.id) }, ]}</script>
Filter. Whether comment-count markers and Disqus's count.js are output on this request. Default: a shortname is set, counts are on, not a feed, a singular view, the posts page, an archive or search results, and dsq_can_load allows 'count'.
For posts that would show Disqus on their own page, the plugin wraps the output of WordPress's comments_number in <span class="dsq-postid" data-dsqidentifier="…">; the script turns links around those markers into Disqus count links. Other posts keep WordPress's own count.
Trigger it. For a method it doesn't know, the front-end script loads nothing and exposes window.dclEmbed = { method, load }. Enqueue a script that depends on the dcl-comments handle and call load():
js
if (window.dclEmbed && window.dclEmbed.method === 'on_idle') { requestIdleCallback(() => window.dclEmbed.load())}
Enqueue it on wp_footer at an early priority, so it also works when the shortcode enqueued the main script late. A script this short can go inline after the main one instead, as Scroll Load does:
Filter. Hosts that count as this site when the plugin looks for its webhook subscription among the Disqus forum's subscriptions. A subscription matches when its URL has this site's webhook path and one of these hosts. Default: the hosts of the REST URL and the home URL, lower-case and without www..
Sites served under more than one domain can add theirs, in the same form:
The settings screen is a React app under Disqus in the admin menu, at admin.php?page=dcl-settings (hook suffix toplevel_page_dcl-settings). A sidebar lists its pages in three groups. The tab query argument picks the page — admin.php?page=dcl-settings&tab=display — so a reload or a shared link opens the same page. An unknown tab opens General.
Group
Pages (tab)
Settings
General (general), Display (display), Comment sync (sync), Advanced (advanced)
Manage
Import & export (tools)
More
Add-ons (addons), Help (help)
Every page edits one shared draft: the core settings, the Disqus account and any add-on option. Save and Discard sit in the page header. Save shows on the four settings pages, and on any other page while there are unsaved changes; Discard shows while there are changes. Changes made on several pages save together.
Add-ons extend the screen in two ways:
In PHP, with dcl_settings_schema. Declare cards and fields, and the screen draws them with its own controls. This is the recommended way, and the add-ons with settings use it.
In JavaScript, with @wordpress/hooks filters. Add a page, a panel or a field drawn by your own components.
The settings pages are built from panels. These are the core panels, and the IDs that after refers to:
Panel
Page
Cards
disqus
General
Disqus site
loading
General
Comment loading, Load comments button
display
Display
Comment counts, Comment section
sync
Comment sync
Disqus API, Sync status
advanced
Advanced
Compatibility
Add-on cards and panels are placed the same way:
after puts it right after the named panel — a core panel or an add-on's — on that panel's page.
tab moves it to another settings page: general, display, sync or advanced.
Without after, or with an after that matches nothing, it goes just before the core advanced panel: at the end of the page tab names, or at the top of Advanced when there's no tab either.
Schema cards are placed before JavaScript panels, so a panel's after can name a schema card.
PHP filter. Settings that add-ons declare in PHP. The screen draws them with the same controls as the core settings, and the header Save saves them. Receives and returns array( 'cards' => array(), 'fields' => array() ).
Keep the values in your own option, registered with show_in_rest and a schema that lists every key:
The card shows on Display, below the core cards. Comments for WooCommerce declares its card the same way.
Cards:
Key
Description
id
Unique card ID. Fields name it in card.
title
The card title.
description
Optional. One sentence under the title.
after
Optional. The panel or card to follow. See Panels.
tab
Optional. The settings page to show the card on. See Panels.
Fields:
Key
Description
card
A card ID, or loading-button for the core Load comments button card on General, which shows while the click method is in use.
key
The key in the option.
type
toggle, select, text, number or button_preview. Any other type draws a text field.
label
The field label.
help
Optional. A line under the field.
option
Optional. The site option holding key. Default: dcl_gnrl_options.
options
select only. A list of array( 'value' => …, 'label' => …, 'help' => … ). The chosen option's help replaces the field's. Before a value is saved, the first option shows.
min, max
number only. The allowed range.
show_if
Optional. array( 'key' => …, 'equals' => … ): show the field only while that key, in the same option, equals the value. The check is strict, so match the stored type.
cast
Optional. int stores the value as a number.
What each type stores:
Type
Stores
toggle
1 or 0.
select
The chosen value, as a string, or as a number with 'cast' => 'int'.
text
A string.
number
A number, or '' while the field is empty.
button_preview
Nothing. Shows the load button as visitors see it, from the core button settings.
Keep new keys in your own option
The settings endpoint, /wp/v2/settings, only accepts the keys listed in an option's REST schema, and rejects the whole save over any other. Leave option out only for keys the core already stores — Advanced Buttons does this for dcl_btn_style and dcl_btn_count.
The JavaScript filters go through @wordpress/hooks. Add them in a script built with @wordpress/scripts that depends on the dcl-settings handle, and enqueue it on the settings screen only:
The app mounts once the DOM is ready, after such scripts have run, and reads each filter once. Its own controls aren't exposed to other scripts, so your components draw their own markup. For the core look, use dcl_settings_schema.
The page opens at admin.php?page=dcl-settings&tab=my-reports.
Key
Description
id
Unique page ID, and its tab value. Reusing a core ID replaces that page.
group
settings, manage or more. A page with any other group is left out.
title
The sidebar label and page heading.
render
The page component, rendered without props once the settings have loaded.
icon
Optional. An icon from the plugin's set, such as download, mail or plugin. Other names show a dot.
saves
Optional. true shows Save in the header before anything has changed, for pages that edit settings.
wide
Optional. true uses the wider content column.
A new page goes last in its group; in More, above Help, which stays last. A page that edits settings uses core-data, as a panel does, and the header Save saves it.
The filter from before 13.0, still read so older add-ons keep working. Each tab, keyed by its ID — { label, component, footer?, saves?, wide? } — becomes a page:
label is the title and component the page.
A tab with footer or saves joins the Settings group and shows Save. Any other tab joins More.
It gets a dot icon and goes after every other page in its group.
A tab whose key is already a page ID, or that has no component, is skipped.
Unique panel ID. Reusing a core panel ID replaces that panel, on the same page unless tab says otherwise.
Component
The panel component, rendered without props.
after
Optional. The panel or card to follow. See Panels.
tab
Optional. The settings page to show it on. See Panels.
Edit your own option with useEntityProp('root', 'site', 'my_addon_options'), registered with show_in_rest as in the schema example. The header Save then saves it with everything else, and Discard reverts it.
PHP filter. The window.dcl object passed to the settings screen: the version, option key, REST nonce, links, loadMethods, activeAddons (slugs of the installed add-ons) and schema (the settings schema), among others.
The settings page and every dcl/v1 REST route need the manage_options capability. Change it with the DCL_ACCESS constant, in wp-config.php:
php
define( 'DCL_ACCESS', 'edit_others_posts' );
or with the dcl_capability filter. dcl_has_access filters the check the dcl/v1 routes run; the menu page only checks the capability.
General settings still need manage_options
The general settings save through WordPress's own settings endpoint, which always requires manage_options. Lower DCL_ACCESS and other roles can open the page and manage the Disqus account and sync, but not save the other settings.
Filter. The default settings, filled in under the stored values. Keys added here are also kept when settings are saved. The settings endpoint only accepts the core keys, though, so the settings screen can't edit them — keep add-on settings in their own option.
Action. Fires after the plugin adds its Settings link to its row on the Plugins screen, with the row's action links. To change the links, use WordPress's plugin_action_links_{$plugin_file} filter.
Action. Fires after the plugin adds its Docs and Add-ons links to its row on the Plugins screen, with the row's meta links. To change the links, use WordPress's plugin_row_meta filter.
Filter. The one hook an add-on needs to plug in. Its main file adds a declaration; the plugin registers it for licensing and calls its boot callback just before dcl_running, so the add-on needs no version or class_exists() checks. Add it at file load, not on a hook:
Filter. The lower-level registration that dcl_addons builds on: Freemius arguments keyed by product ID, with no boot callback. Also added at file load:
All need the plugin's capability and a REST nonce.
Route
Method
Does
dcl/v1/account
GET
The shortname, public key and has_secret_key, has_access_token, has_sync_token flags. Secrets are never returned.
dcl/v1/account
POST
Update shortname, public_key, secret_key, access_token, sync_token. An empty string keeps a stored secret; null clears it.
dcl/v1/sync
GET
Sync status: configured, allowed, subscribed, enabled, requires_update, subscription_id, webhook_url, last.
dcl/v1/sync/enable
POST
Create or repair the Disqus webhook subscription.
dcl/v1/sync/disable
POST
Stop Disqus sending comments.
dcl/v1/sync/manual
POST
Import past comments. start, end (date or Unix timestamp; a date without a time zone is UTC), cursor. Returns synced, failed, next.
dcl/v1/export
POST
Export comments to Disqus, 10 posts per batch. after: the last post ID of the previous batch, 0 or left out for the first. Returns log, after, remaining, done.
dcl/v1/addons
GET
The add-on catalogue and bundle: items, bundle.
dcl/v1/addons/refresh
POST
Reload the catalogue from Freemius.
dcl/v1/addons/<id>/license
POST, DELETE
Activate (key) or deactivate an add-on's license. Refused for an add-on licensed through the Pro Bundle.
dcl/v1/addons/bundle/license
POST, DELETE
Activate (key) or deactivate the Pro Bundle.
General settings use WordPress's settings endpoint, /wp/v2/settings, under the dcl_gnrl_options key. It only accepts the keys listed under Options.
Disqus sends comments here. The routes use the official plugin's namespace so existing subscriptions keep working, and are only registered while the official plugin is inactive.
Requests must carry an X-Hub-Signature header — sha512= followed by the HMAC-SHA512 of the request body, keyed with the sync token (at least 32 characters) — or come from a logged-in user with manage_options. Anything else gets 401, and a request without a verb gets 400. Handled verbs are verify (the subscription handshake), and create, update and force_sync for comments; others get 204.
Deleting the plugin removes dcl_gnrl_options, dcl_disqus_account, dcl_db_version, dcl_sso_notice, dcl_sync_last_message, dcl_bundle_license, the dcl_bundle_sync_retry transient, and the dcl_version_no and dcl_do_activation_redirect options older versions left behind. Synced comments, their meta, add-on license activations and the official Disqus plugin's own settings are left in place.
Nothing is removed while the 11.x Pro plugin is active, as it shares these options.
Developer Docs
Better Disqus Comments has a stable extension surface — PHP actions and filters, JavaScript slots in the settings screen, REST endpoints, and an option schema that hasn't changed since version 10. Everything on this page is public API.
PHP examples can go in your theme's
functions.phpor a small plugin.Architecture overview
The plugin's slug, text domain, option names and hook prefix (
dcl_) date from its old name, Disqus Conditional Load, and are kept for compatibility. The PHP namespace isFoxeLabs\DCL.Front-end classes load only outside the admin, and the
Adminclasses, apart from the toolbar menu, only inside it. The rest loads on every request.The recommended way to extend the plugin:
dcl_addons, or hook intodcl_running, so your code runs after the plugin has booted.dcl_settings_schemaPHP filter and keep the values in your ownshow_in_restoption. No JavaScript needed.The
dcl()helper dcl()returns theCoreinstance, with accessors for the main services:dcl()->settings()Settings—all(),get( $key, $fallback ),set( $key, $value ),update( $values ),defaults()dcl()->account()Account—shortname(),is_connected(),has_api_credentials(),get( $key )dcl()->compat()Compat\Manager—is_disqus_active(),sync_allowed(),woocommerce_review_support()dcl()->front()Front\Controller—detector(),renderer(). Front-end requests only.Lifecycle hooks
dcl_runningAction. Fires on
plugins_loaded, once every module is wired and the add-ons declared throughdcl_addonshave booted.$coreFoxeLabs\DCL\Coredcl().dcl_deactivatedAction. Fires when the plugin is deactivated.
Loading and detection
The plugin answers one question per request — can Disqus load here, and how? — and every part of it uses the same answer.
dcl_can_loadFilter. Whether the Disqus embed loads on this request. Computed once per post per request.
The built-in rules, in order: a shortname is set; not a feed; not an AMP page;
dsq_can_loaddoesn't returnfalse; a singular view of a post; comments open; the post isn't waiting for its password; the post isn't a draft, pending, scheduled, auto-draft or trashed; the post type isn't excluded; and the visitor isn't a bot (a visitor with no user agent counts as one), unless Load comments for search engine bots is on.$can_loadbool$postWP_Post|nullnullwhen there's none.When it returns
false, the theme's normal WordPress comments show instead.dcl_excluded_cptsFilter. Post types that never show Disqus: the Exclude post types setting, plus
productwhile WooCommerce review support is on.dcl_woocommerce_review_supportFilter. Default
true: WooCommerce products keep their own reviews template and are excluded from Disqus. Returnfalseto let Disqus replace product reviews. The Comments for WooCommerce add-on uses this.dsq_can_loadFilter kept from the official Disqus plugin, so existing snippets keep working. Called with
'embed'for the thread and'count'for comment counts; returning exactlyfalseblocks that part.dcl_load_methodFilter. The resolved loading method for this visitor, after the mobile method and the fallback for inactive add-ons:
scroll,click,normalor an add-on's method.dcl_is_mobileFilter. Whether the visitor gets the mobile method. Default:
wp_is_mobile().dcl_is_lazyFilter. Whether the resolved method is lazy. Parameters:
bool $is_lazy,string $method.dcl_load_method_optionsFilter. The methods offered in the settings, as
slug => array( 'label' => …, 'description' => … ). A stored method missing from this list falls back toscroll. See Adding a load method.dcl_lazy_load_methodsFilter. Slugs that count as lazy. Default
array( 'scroll', 'click' ).dcl_shortnameFilter. The Disqus shortname. Useful on multisite or staging:
Rendering
The comments area becomes:
The button container only appears for the click method.
dcl_inside_disqus_threadAction. Fires inside
#disqus_thread, before the button. Anything printed here is replaced when Disqus loads — a good place for a placeholder.dcl_button_textFilter. The button label. Escaped on output — plain text only.
dcl_button_classFilter. The button's CSS classes: the Button CSS classes setting plus the selected style.
dcl_button_stylesFilter. Registered button styles, as
class => label. Empty in the free plugin; the Advanced Buttons add-on registers its six here. A stored style only takes effect while it's registered.dcl_empty_commentsAction. Fires where the theme's comments area was blanked — when the shortcode or the Comments Widget shows the thread elsewhere.
Templates
The thread is printed from
templates/disqus-comments.php; a blanked comments area usestemplates/empty-comments.php. Themes can't override these files — use the hooks above, or filtercomments_templateat a priority above100(the plugin's own swap runs at100).Shortcodes
[dcl-comments]shows the Disqus thread where you put it, and blanks the theme's comments area further down the page so the thread shows once.[js-disqus]is an older alias.It shows nothing where Disqus can't load — archives, closed comments, excluded post types.
dcl_force_shortcodeFilter. Return
trueto skip the shortcode's checks. The shortcode then prints the Disqus thread wherever it's used — off single posts too, and wheredcl_can_loadsays no — as long as there's a post to build the embed for and a shortname is set.Block themes
In block themes the shortcode doesn't hide the Comments block. Remove that block from the template if you place the thread with the shortcode.
Embed configuration
For each post the plugin builds the same values the official Disqus plugin used, so threads stay attached to their posts:
"<post ID> <guid>", e.g.42 https://example.com/?p=42get_permalink()dcl_embed_varsFilter. The per-post embed configuration handed to the front-end script.
disqusShortnamedisqusIdentifierdisqusUrldisqusTitledisqusConfigintegration. Addlanguageto set the Disqus interface language.postIdChanging the identifier
Changing
disqusIdentifierdetaches existing threads, and comment counts and sync keep using the original identifier. Don't change it on an existing site.disqus_configA
window.disqus_configfunction your site defines is kept. The plugin sets the page URL, identifier, title, integration and language, then calls your function, so your values win. Define it before the footer:dcl_localized_dataFilter. The whole
window.dclDataobject passed to the front-end script:Comment counts
dcl_can_countFilter. Whether comment-count markers and Disqus's
count.jsare output on this request. Default: a shortname is set, counts are on, not a feed, a singular view, the posts page, an archive or search results, anddsq_can_loadallows'count'.For posts that would show Disqus on their own page, the plugin wraps the output of WordPress's
comments_numberin<span class="dsq-postid" data-dsqidentifier="…">; the script turns links around those markers into Disqus count links. Other posts keep WordPress's own count.Adding a load method
Add-ons can add a loading method in three steps — this is how Scroll Load works.
Offer it in the settings:
Mark it lazy, so the plugin holds the embed back:
Trigger it. For a method it doesn't know, the front-end script loads nothing and exposes
window.dclEmbed = { method, load }. Enqueue a script that depends on thedcl-commentshandle and callload():Enqueue it on
wp_footerat an early priority, so it also works when the shortcode enqueued the main script late. A script this short can go inline after the main one instead, as Scroll Load does:load()is safe to call more than once.Sync
dcl_comment_syncedAction. Fires after Disqus sends a comment and it's saved in WordPress.
$comment_idint$dataarray$verbstringcreate,updateorforce_sync.It doesn't fire for Import past comments.
dcl_export_post_typesFilter. Post types whose comments are exported to Disqus. Default: public post types that support comments.
dcl_webhook_hostsFilter. Hosts that count as this site when the plugin looks for its webhook subscription among the Disqus forum's subscriptions. A subscription matches when its URL has this site's webhook path and one of these hosts. Default: the hosts of the REST URL and the home URL, lower-case and without
www..Sites served under more than one domain can add theirs, in the same form:
Settings screen
The settings screen is a React app under Disqus in the admin menu, at
admin.php?page=dcl-settings(hook suffixtoplevel_page_dcl-settings). A sidebar lists its pages in three groups. Thetabquery argument picks the page —admin.php?page=dcl-settings&tab=display— so a reload or a shared link opens the same page. An unknowntabopens General.tab)general), Display (display), Comment sync (sync), Advanced (advanced)tools)addons), Help (help)Every page edits one shared draft: the core settings, the Disqus account and any add-on option. Save and Discard sit in the page header. Save shows on the four settings pages, and on any other page while there are unsaved changes; Discard shows while there are changes. Changes made on several pages save together.
Add-ons extend the screen in two ways:
dcl_settings_schema. Declare cards and fields, and the screen draws them with its own controls. This is the recommended way, and the add-ons with settings use it.@wordpress/hooksfilters. Add a page, a panel or a field drawn by your own components.Panels
The settings pages are built from panels. These are the core panels, and the IDs that
afterrefers to:disqusloadingdisplaysyncadvancedAdd-on cards and panels are placed the same way:
afterputs it right after the named panel — a core panel or an add-on's — on that panel's page.tabmoves it to another settings page:general,display,syncoradvanced.after, or with anafterthat matches nothing, it goes just before the coreadvancedpanel: at the end of the pagetabnames, or at the top of Advanced when there's notabeither.Schema cards are placed before JavaScript panels, so a panel's
aftercan name a schema card.dcl_settings_schemaPHP filter. Settings that add-ons declare in PHP. The screen draws them with the same controls as the core settings, and the header Save saves them. Receives and returns
array( 'cards' => array(), 'fields' => array() ).Keep the values in your own option, registered with
show_in_restand a schema that lists every key:Then declare a card and its fields:
The card shows on Display, below the core cards. Comments for WooCommerce declares its card the same way.
Cards:
idcard.titledescriptionaftertabFields:
cardloading-buttonfor the core Load comments button card on General, which shows while the click method is in use.keytypetoggle,select,text,numberorbutton_preview. Any other type draws a text field.labelhelpoptionkey. Default:dcl_gnrl_options.optionsselectonly. A list ofarray( 'value' => …, 'label' => …, 'help' => … ). The chosen option'shelpreplaces the field's. Before a value is saved, the first option shows.min,maxnumberonly. The allowed range.show_ifarray( 'key' => …, 'equals' => … ): show the field only while that key, in the same option, equals the value. The check is strict, so match the stored type.castintstores the value as a number.What each type stores:
toggle1or0.selectvalue, as a string, or as a number with'cast' => 'int'.textnumber''while the field is empty.button_previewKeep new keys in your own option
The settings endpoint,
/wp/v2/settings, only accepts the keys listed in an option's REST schema, and rejects the whole save over any other. Leaveoptionout only for keys the core already stores — Advanced Buttons does this fordcl_btn_styleanddcl_btn_count.JavaScript filters
The JavaScript filters go through
@wordpress/hooks. Add them in a script built with@wordpress/scriptsthat depends on thedcl-settingshandle, and enqueue it on the settings screen only:The app mounts once the DOM is ready, after such scripts have run, and reads each filter once. Its own controls aren't exposed to other scripts, so your components draw their own markup. For the core look, use
dcl_settings_schema.dcl.admin.pagesAdds pages to the sidebar.
The page opens at
admin.php?page=dcl-settings&tab=my-reports.idtabvalue. Reusing a core ID replaces that page.groupsettings,manageormore. A page with any other group is left out.titlerendericondownload,mailorplugin. Other names show a dot.savestrueshows Save in the header before anything has changed, for pages that edit settings.widetrueuses the wider content column.A new page goes last in its group; in More, above Help, which stays last. A page that edits settings uses core-data, as a panel does, and the header Save saves it.
dcl.admin.tabsThe filter from before 13.0, still read so older add-ons keep working. Each tab, keyed by its ID —
{ label, component, footer?, saves?, wide? }— becomes a page:labelis the title andcomponentthe page.footerorsavesjoins the Settings group and shows Save. Any other tab joins More.component, is skipped.Use
dcl.admin.pagesin new code.dcl.settings.panelsAdds a panel to a settings page, for settings that need more than the schema's fields.
idtabsays otherwise.ComponentaftertabEdit your own option with
useEntityProp('root', 'site', 'my_addon_options'), registered withshow_in_restas in the schema example. The header Save then saves it with everything else, and Discard reverts it.dcl.settings.loading.fieldsAdds fields to the Comment loading card on General, below On mobile devices. Each entry is
{ id, Component }.The component receives:
getSetting(key, fallback)dcl_gnrl_optionsvalue.fallbackdefaults to''.setSetting(key, value)dcl_gnrl_optionsvalue in the draft. Only for keys the core stores — see the warning above.usesButtontruewhile desktop or mobile uses the click method.To add PHP fields to the Load comments button card instead, use the schema's
loading-buttoncard.dcl_admin_script_varsPHP filter. The
window.dclobject passed to the settings screen: the version, option key, REST nonce, links,loadMethods,activeAddons(slugs of the installed add-ons) andschema(the settings schema), among others.Admin
Capability
The settings page and every
dcl/v1REST route need themanage_optionscapability. Change it with theDCL_ACCESSconstant, inwp-config.php:or with the
dcl_capabilityfilter.dcl_has_accessfilters the check thedcl/v1routes run; the menu page only checks the capability.General settings still need manage_options
The general settings save through WordPress's own settings endpoint, which always requires
manage_options. LowerDCL_ACCESSand other roles can open the page and manage the Disqus account and sync, but not save the other settings.dcl_show_admin_barFilter. Whether the toolbar Disqus menu shows. Default:
current_user_can( 'moderate_comments' ).dcl_admin_menuAction. Fires after the Disqus menu page is added, with the page's hook suffix.
dcl_disqus_conflict_alert_textFilter. The full notice markup of the warning shown while the official Disqus plugin is active.
dcl_disqus_fileFilter. The plugin file used to detect the official Disqus plugin. Default
disqus-comment-system/disqus.php.dcl_default_settingsFilter. The default settings, filled in under the stored values. Keys added here are also kept when settings are saved. The settings endpoint only accepts the core keys, though, so the settings screen can't edit them — keep add-on settings in their own option.
dcl_admin_action_linksAction. Fires after the plugin adds its Settings link to its row on the Plugins screen, with the row's action links. To change the links, use WordPress's
plugin_action_links_{$plugin_file}filter.dcl_plugin_row_metaAction. Fires after the plugin adds its Docs and Add-ons links to its row on the Plugins screen, with the row's meta links. To change the links, use WordPress's
plugin_row_metafilter.Add-ons
dcl_addonsFilter. The one hook an add-on needs to plug in. Its main file adds a declaration; the plugin registers it for licensing and calls its
bootcallback just beforedcl_running, so the add-on needs no version orclass_exists()checks. Add it at file load, not on a hook:idslugpublic_keyfilebootis_premiumtrue.A declaration missing
id,slug,public_keyorfileis skipped. Premium add-ons in the Pro Bundle are licensed by it automatically.dcl_register_addonFilter. The lower-level registration that
dcl_addonsbuilds on: Freemius arguments keyed by product ID, with nobootcallback. Also added at file load:dcl_addons_catalogFilter. The add-on rows shown on the Add-ons page. Parameters:
array $items, the rows;array $catalogue, the raw rows from Freemius.dcl_addons_bundleFilter. The data of the Pro Bundle card on the Add-ons page. Return an empty array to hide the card.
REST API
Admin routes
All need the plugin's capability and a REST nonce.
dcl/v1/accountGEThas_secret_key,has_access_token,has_sync_tokenflags. Secrets are never returned.dcl/v1/accountPOSTshortname,public_key,secret_key,access_token,sync_token. An empty string keeps a stored secret;nullclears it.dcl/v1/syncGETconfigured,allowed,subscribed,enabled,requires_update,subscription_id,webhook_url,last.dcl/v1/sync/enablePOSTdcl/v1/sync/disablePOSTdcl/v1/sync/manualPOSTstart,end(date or Unix timestamp; a date without a time zone is UTC),cursor. Returnssynced,failed,next.dcl/v1/exportPOSTafter: the last post ID of the previous batch,0or left out for the first. Returnslog,after,remaining,done.dcl/v1/addonsGETitems,bundle.dcl/v1/addons/refreshPOSTdcl/v1/addons/<id>/licensePOST,DELETEkey) or deactivate an add-on's license. Refused for an add-on licensed through the Pro Bundle.dcl/v1/addons/bundle/licensePOST,DELETEkey) or deactivate the Pro Bundle.General settings use WordPress's settings endpoint,
/wp/v2/settings, under thedcl_gnrl_optionskey. It only accepts the keys listed under Options.Webhook
disqus/v1/sync/webhookPOSTdisqus/v1/sync/commentPOST(alias)Disqus sends comments here. The routes use the official plugin's namespace so existing subscriptions keep working, and are only registered while the official plugin is inactive.
Requests must carry an
X-Hub-Signatureheader —sha512=followed by the HMAC-SHA512 of the request body, keyed with the sync token (at least 32 characters) — or come from a logged-in user withmanage_options. Anything else gets401, and a request without averbgets400. Handled verbs areverify(the subscription handshake), andcreate,updateandforce_syncfor comments; others get204.Stored data
Options
dcl_gnrl_optionsdcl_disqus_accountshortname,public_key,secret_key,access_token,sync_token. Not exposed through/wp/v2/settings.dcl_db_versiondcl_sso_noticedcl_sync_last_messagemessage,error,time.dcl_bundle_licensedcl_bundle_sync_retryfoxelabs_freemius_activation_datadcl_gnrl_optionskeys:dcl_typescrolldcl_type_mob''dcl_btn_txtLoad Commentsdcl_btn_class''dcl_messageLoading...dcl_btn_style''dcl_btn_count0dcl_count_disable11means ondcl_cpt_exclude''dcl_div_width''dcl_div_width_typepxdcl_caching0dcl_cfasync0dcl_render_inline0Switches are stored as
0or1.Meta
dsq_post_iddsq_thread_iddsq_parent_post_idSynced comments also have the user agent
Disqus Sync Host.dsq_post_idanddsq_thread_idmatch the official Disqus plugin's keys.Uninstalling
Deleting the plugin removes
dcl_gnrl_options,dcl_disqus_account,dcl_db_version,dcl_sso_notice,dcl_sync_last_message,dcl_bundle_license, thedcl_bundle_sync_retrytransient, and thedcl_version_noanddcl_do_activation_redirectoptions older versions left behind. Synced comments, their meta, add-on license activations and the official Disqus plugin's own settings are left in place.Nothing is removed while the 11.x Pro plugin is active, as it shares these options.