Blogano Technical Reference

AudienceMaintainers and developers
LicenceGPL-2.0-or-later
RequiresWordPress 5.8+, PHP 7.4+
Covers19 files, 17 sections

Section 01

Scope & conventions

What this document covers, and how to check it

This is the maintainer’s document. It describes structure, not usage — for how a site owner operates Blogano, see the User Manual.

Every entity below is cited to the file and symbol that defines it, in the form class-psw-ajax.php → PSW_Ajax::guard(). Symbols are cited rather than line numbers on purpose: line numbers go stale on the first edit, and a claim in documentation that cannot be cheaply re-verified is a claim that will eventually be wrong without anyone noticing.

Four chips recur. action and filter mark which kind of WordPress hook a row describes. constraint marks behaviour that is load-bearing and not obvious from reading the code around it. core marks a WordPress core mechanism the plugin defers to rather than reimplementing.

Naming

The code prefix is psw_ / PSW_, inherited from the plugin’s original working name, post-submission-wizard. The public-facing name is Blogano and the text domain is blogano. The two diverge deliberately: the prefix appears in option keys, post meta keys, user meta keys and role slugs, so renaming it is a data migration rather than a rename, and it buys nothing a reviewer or user would ever see.

Versioning

Blogano was numbered privately during development, reaching 1.61.0. That numbering was never published. The first public release is 1.0.0, and the version is declared in three places that must always agree: the plugin header Version:, the PSW_VERSION constant, and Stable tag: in readme.txt.

Read this first. Blogano stores its submissions as ordinary WordPress posts of type post. What distinguishes them is a single meta flag, _psw_submission, defined as the constant PSW_META_FLAG. Nearly every query, guard and hook in the plugin tests that flag before acting. If you add a code path that touches posts and forget the flag test, that path will operate on the site’s unrelated editorial content.


Section 02

Bootstrap & load order

What loads, in what order, and why it matters

blogano.php → psw_run_plugin() is hooked to plugins_loaded. Everything before that point is require_once in a fixed order; everything after is singleton construction.

Constants

ConstantValueNotes
PSW_VERSION1.0.0Also used as the asset cache-busting version.
PSW_PLUGIN_FILE__FILE__Passed to the activation hook.
PSW_PLUGIN_DIRplugin_dir_path()Base for every require_once.
PSW_PLUGIN_URLplugin_dir_url()Base for asset enqueues.
PSW_META_FLAG_psw_submissionThe flag that makes a post a Blogano submission.

Load order

Twenty require_once calls, nineteen files. helpers.php is first and is the only file the others may rely on at include time; it defines the settings accessor, the defaults array, and the shared query and URL helpers.

helpers.php
class-psw-roles.php
class-psw-plugin.php
class-psw-ajax.php
class-psw-shortcodes.php
class-psw-user-dashboard.php
class-psw-admin.php
class-psw-admin-list-table.php
class-psw-auth.php
class-psw-user-profile.php
class-psw-live-feed.php
class-psw-frontend-admin-dashboard.php
class-psw-nav.php
class-psw-settings.php
class-psw-bio-page.php
class-psw-user-directory.php
class-psw-notifications.php
class-psw-email-log.php
class-psw-social-meta.php

Why helpers must be first. Several classes call psw_get_settings() in their constructors, and the shortcodes call the query and URL helpers at render time. Nothing in helpers.php may depend on a class, or the ordering becomes circular.

Construction

Sixteen singletons are constructed in psw_run_plugin(), plus PSW_Roles::init() which registers filters rather than a singleton. Construction order is not significant — every class registers hooks in its constructor and does no work until those hooks fire.


Section 03

Class map

What each file is responsible for

FileClassResponsibility
helpers.php—Settings accessor and defaults, submission queries, category filter helpers, URL normalisation, avatar and fallback images, the search bar and filter breadcrumb renderers.
class-psw-roles.phpPSW_RolesRegisters the two custom roles, hides WordPress’s native roles from the role selector.
class-psw-plugin.phpPSW_PluginAsset enqueue and its gating, the full-bleed shortcode wrapper.
class-psw-ajax.phpPSW_AjaxAll nine AJAX endpoints: URL fetch, submit, image upload and sideload, avatar, pins, reports.
class-psw-shortcodes.phpPSW_ShortcodesThe submission wizard markup and its Summernote editor.
class-psw-user-dashboard.phpPSW_User_DashboardMy Posts — a member’s own submissions, with filters and member pins.
class-psw-admin.phpPSW_Adminwp-admin menus, the Users screen, admin notices.
class-psw-admin-list-table.phpPSW_Admin_List_TableThe wp-admin submissions list table.
class-psw-auth.phpPSW_AuthFront-end register, login, logout, email verification and its token flow.
class-psw-user-profile.phpPSW_User_ProfileThe member’s own profile editor and avatar handling.
class-psw-live-feed.phpPSW_Live_FeedThe feed, all card styles, the featured post, and the 22 shortcode options.
class-psw-frontend-admin-dashboard.phpPSW_Frontend_Admin_DashboardThe Editorial Desk.
class-psw-nav.phpPSW_NavThe account bar, its burger mode, and prepending it to shortcode pages.
class-psw-settings.phpPSW_SettingsSix settings tabs, three save paths, the generated CSS variable block, the Settings Manual, CSV import and export.
class-psw-bio-page.phpPSW_Bio_PageTakes over author URLs and renders the member bio page.
class-psw-user-directory.phpPSW_User_DirectoryThe contributor directory.
class-psw-notifications.phpPSW_NotificationsComposes and sends the five notification emails.
class-psw-email-log.phpPSW_Email_LogRecords sends, the log screen, retention cleanup.
class-psw-social-meta.phpPSW_Social_MetaOpen Graph and Twitter tags, meta description, the noindex rule for link posts.

The largest file. class-psw-settings.php is by a wide margin the biggest file in the plugin. It carries the settings UI, its three separate save paths, the generated stylesheet, and the Settings Manual generator. Changes there are the most likely to have effects elsewhere — see section 12.


Section 04

Lifecycle

Activation, upgrade and the work that runs once

Activation

blogano.php → psw_activate_plugin() runs on register_activation_hook and does four things:

StepWhat it doesIdempotent?
PSW_Roles::register_roles()Adds the two custom roles if absent.Yes — guarded by get_role().
psw_get_blogano_category_id()Finds or creates the WordPress category blogano, caching its ID in an option.Yes.
psw_maybe_create_pages_on_first_activation()Creates the nine pages on first activation only, flagged by the psw_pages_created option.Yes — the flag makes it once-only.
psw_backfill_source_url_keys()Writes the URL comparison key onto submissions that predate it.Yes — guarded by the psw_source_url_key_backfill option.

Migrations run on activation, not on upgrade. There is no version_compare upgrade routine. Work that must happen once is hooked to activation and self-guards with an option flag. The consequence: updating the plugin files alone does not run a migration — the plugin must be deactivated and reactivated. If you add a migration, either follow this pattern or introduce a proper upgrade routine, but do not assume file replacement triggers anything.

Deactivation and uninstall

Neither is implemented. Roles, options, post meta and the email log post type all survive deactivation, which is deliberate for an accidental deactivation but means there is currently no clean uninstall. A reviewer will look for this.


Section 05

Permission model

Two custom roles, and one trap

RoleSlugCapabilities
Publisherpsw_publisherread, edit_posts, edit_published_posts, delete_posts, delete_published_posts, upload_files
Editor (Submissions)psw_editorThe above plus edit_others_posts, publish_posts, and the delete-others and read-private equivalents
AdministratoradministratorWordPress core, unmodified

Note what Publisher lacks: publish_posts. That single omission is the entire review workflow — a Publisher can create and edit but cannot move a post to publish, so submissions can only reach the feed through an editor.

The identically-named role trap. WordPress ships its own editor role. Blogano checks for psw_editor specifically, so a user given WordPress’s native Editor role has none of Blogano’s editor powers despite the identical display name. class-psw-roles.php → PSW_Roles::init() hides the native roles from the role selector to prevent the mistake being made in the first place. constraint

Registration always assigns psw_publisher. WordPress’s own New User Default Role setting is not consulted.


Section 06

Hooks consumed

Where Blogano attaches to WordPress

HookKindConsumerPurpose
plugins_loadedactionblogano.phpBootstrap.
initactionPSW_Email_LogRegisters the log post type.
wp_enqueue_scriptsactionPSW_PluginConditional front-end assets — see section 14.
the_contentfilterPSW_NavPrepends the account bar to pages holding a Blogano shortcode.
the_contentfilterPSW_Live_FeedAppends the Report Post control to a single submission.
template_redirectactionPSW_Bio_PageTakes over author URLs entirely.
wp_headactionPSW_SettingsEmits the generated CSS variable block, priority 10.
wp_headactionPSW_Social_MetaEmits sharing tags, priority 5.
wp_robotsfilterPSW_Social_MetaAdds noindex to link posts when enabled.
wp_ajax_* / wp_ajax_nopriv_*actionPSW_AjaxNine endpoints — section 09.
admin_post_*actionPSW_Admin, PSW_Frontend_Admin_DashboardRow and bulk actions — section 10.
editable_rolesfilterPSW_RolesHides WordPress’s native roles.
show_admin_barfilterPSW_Nav, PSW_Bio_PageHides the admin bar for members, and unconditionally on bio pages.
save_postactionhelpers.phpClears the cached page-URL transients.
register_activation_hookactionblogano.phpSection 04.

Two consumers of the_content. Both the account bar and the Report control filter the_content. The bar is prepended before shortcodes are processed, which is why the nav option has to be read out of the raw post content rather than at render time — see class-psw-nav.php → PSW_Nav::content_opts_out_of_nav(). constraint


Section 07

Extension points

What a third party can change without forking

Blogano exposes very little deliberately. What exists is listed here in full.

FilterSignaturePurpose
psw_emit_social_metabool $emitForces the sharing tags on or off. Defaults to true unless an SEO plugin is detected.

This is thin, and that is a known gap. One filter is not an extension surface. Anyone wanting to alter card markup, query arguments or email bodies currently has to fork. If the plugin gains an audience, the first requests will be a filter on the feed query arguments, one on the card render, and one on each email body before send. constraint

Detected third parties

class-psw-social-meta.php → PSW_Social_Meta::should_emit() checks for WPSEO_VERSION, RANK_MATH_VERSION, AIOSEO_VERSION, SEOPRESS_VERSION, THE_SEO_FRAMEWORK_VERSION and the class WPSEO_Frontend. Any one of them suppresses Blogano’s own og: tags and meta description. The noindex rule is deliberately not suppressed, because it is an explicit administrator instruction rather than duplicated metadata.


Section 08

Shortcodes

Nine tags, and the twenty-two options on one of them

TagClassCapability required
psw_live_feedPSW_Live_FeedNone
psw_loginPSW_AuthNone
psw_registerPSW_AuthNone
psw_resend_verificationPSW_AuthNone
psw_user_directoryPSW_User_DirectoryNone
psw_submission_formPSW_Shortcodesedit_posts
psw_user_dashboardPSW_User_Dashboardedit_posts
psw_user_profilePSW_User_ProfileLogged in
psw_admin_dashboardPSW_Frontend_Admin_Dashboardedit_others_posts

psw_live_feed options

Parsed in class-psw-live-feed.php → PSW_Live_Feed::render(). Every option defaults to the empty string, which means inherit the global setting — distinct from an explicit 0. Boolean options are read through a local $bool_att closure accepting 1/true/yes/on.

OptionTypeEffect
limitintOverrides feed_posts_per_page.
categorycsvScoped categories. Suppresses the visitor breadcrumb for those terms and overrides URL filtering.
columns / columns_mobileintGrid columns; emitted inline and as --psw-columns-mobile.
styleenumstandard / overlay / full-image.
featured / gridboolFeatured card and card grid respectively. grid="0" also suppresses pagination.
search / pagination / ctaboolChrome. The first two are forced false when static is set.
staticboolSuppresses every $_GET read for this instance.
showcsvWhitelist over the ten feed_show_* settings.
square / image_heightbool / intImage sizing; the latter emitted as --psw-card-image-height.
width / padding_mobile / gap / gap_mobilecss / intEmitted as inline CSS custom properties on the feed wrapper.
orderby / orderenumWhitelisted to date, title, rand, menu_order and asc / desc.
classstringPassed through sanitize_html_class() per token.
navboolRead from raw post content by PSW_Nav, not here.

Why per-instance sizing needs no new CSS. Every sizing option corresponds to a CSS custom property the stylesheet already reads. They are written to the feed wrapper’s inline style, so they cascade to that instance and no other. Mobile values are set unconditionally but only read inside a media query, which is why there is no mobile width option — the mobile rule forces full width with !important.


Section 09

AJAX endpoints

Nine actions, all on PSW_Ajax

ActionAuthPurpose
psw_fetch_ogLogged inFetches Open Graph data from a pasted URL and runs the duplicate check.
psw_submit_postLogged inCreates or updates a submission.
psw_get_submissionLogged inLoads an existing submission into the wizard for editing.
psw_upload_imageLogged inHandles a direct file upload.
psw_sideload_imageLogged inPulls a remote image into the media library.
psw_save_avatarLogged inStores a member avatar.
psw_remove_avatarLogged inClears it.
psw_set_pin_positionLogged inSets a site or member pin slot.
psw_report_postBothThe only endpoint registered for logged-out visitors.

Every endpoint verifies a nonce and re-checks capability server-side. A capability implied by the UI is never trusted: the wizard hides the edit control for someone else’s post, and the endpoint separately refuses it.

psw_report_post is the exposed surface. It is the one endpoint reachable without a login, so it is the one worth reviewing hardest. It flags rather than deletes, which bounds the damage of abuse to noise on the Editorial Desk rather than content loss. constraint


Section 10

admin-post endpoints

Row and bulk actions

ActionConsumerPurpose
psw_user_actionPSW_AdminRole changes from the wp-admin Users screen.
psw_frontend_admin_actionPSW_Frontend_Admin_DashboardApprove, unpublish, delete and pin from the Editorial Desk.

Both verify a nonce and redirect back with a notice argument rather than rendering. The nonce action names are class constants — psw_admin_row_action, psw_frontend_admin_action, psw_user_dashboard_action, psw_profile_action, and psw_communication_save for the Communication tab.


Section 11

Data model

Where everything is stored

Post meta

KeyOnMeaning
_psw_submissionEvery submissionThe flag. Absent means not Blogano’s.
_psw_source_urlLink submissionsThe URL as submitted. Used for the outbound link, so never normalised.
_psw_source_url_keyLink submissionsNormalised form of the above, used only for duplicate matching.
_psw_og_typeLink submissionsOpen Graph type read from the source.
_psw_post_typeAllBlogano’s own type distinction: link or article.
_psw_categoriesOptionalSerialized array of category names. Not a taxonomy.
_psw_rtlOptionalPer-post direction override.
_psw_pinned_admin_orderOptionalSite pin slot, 1–3.
_psw_pinned_user_orderOptionalMember pin slot, 1–3.
_psw_reported / _psw_rejectedOptionalModeration state.
_psw_published_byOn publishWhich editor approved it.
_psw_log_type / _psw_log_to / _psw_log_successEmail log entriesType, recipient and outcome.

User meta

_psw_verification_token, _psw_verification_sent and _psw_verification_redirect carry the email verification flow. Tokens expire after 24 hours.

Options

OptionPurpose
psw_settingsThe entire settings array — one option, 172 keys.
psw_blogano_category_idCached ID of the WordPress blogano category.
psw_pages_createdFirst-activation flag for page creation.
psw_source_url_key_backfillMigration flag for the URL comparison key.
psw_email_log_last_viewedDrives the unread count beside the menu item.

Custom post type

psw_email_log, registered by PSW_Email_Log. Not publicly queryable and given no automatic UI; the log screen is rendered by the class itself so it can sit where it belongs in the menu.

Categories are not a taxonomy. _psw_categories holds a serialized array of names, matched with a LIKE against the serialized string. That means no index, no archive pages, no interoperability with any other plugin, and a multi-category filter that has to build a nested OR group by hand. Converting to a real taxonomy is the single highest-value structural change available, and it gets more expensive with every install. constraint


Section 12

Settings architecture

One option, six tabs, three save paths

All settings live in a single option, psw_settings, with defaults in psw_default_settings(). Reads go through psw_get_settings(), which merges stored values over defaults so a missing key is never fatal.

Three save paths, not one

PathNonceCovers
save_settings_page()Standard settings nonceLayout, Buttons & Borders, Cards & Feed — branched on the posted psw_settings_tab.
save_shortcodes_page()Own noncePage assignments, reCAPTCHA keys, Search Engines.
save_communication_page()psw_communication_saveThe whole Communication tab.

The silent-reset rule. A browser posts only the fields present in the submitted form. In save_settings_page() the sanitizer is branched by tab, so a field whose markup renders on one tab but whose sanitizer sits in another tab’s branch is never posted when that tab is saved — and is silently rewritten to its default. No error appears. When adding or moving a setting, the invariant to check is: save_branch(key) == render_tab(key). This has caused real regressions twice. constraint

Adding a setting

Four places, all required:

  • A default in psw_default_settings().
  • A sanitizer, in the branch or save function matching the tab it renders on.
  • The field markup in the matching render_*_tab().
  • A row in the Settings Manual array, or the setting will work but be invisible in the generated reference.

Tab layout

Rebuilt tabs use .psw-settings-row.psw-grid — five columns of 140px, 25px gaps, 800px maximum. A fallback rule styles any row that is not gridded, and it carries explicit :not() exclusions for rows that manage their own layout. Field help text goes in <p class="description">, which JavaScript converts into an (i) popover anchored to that field’s label.

Descriptions need a label to anchor to. A description outside any .psw-settings-field has nothing to attach to. The popover code now skips those and leaves them as ordinary text; previously it inserted a bare icon belonging to nothing.

The Communication tab

Structured as accordions. Panel bodies are hidden with display:none and never removed from the DOM, so every field posts whether its panel is open or closed. Open state is kept in localStorage, because saving is a full page POST.

One inline script block. The settings screen’s JavaScript — popovers, Spectrum colour pickers, accordions — is a single inline jQuery(function($){...}). An error anywhere in it kills everything after that point in the same block. Features are ordered so the newest code runs last. If several unrelated admin features break at once, read the browser console before reading the markup. constraint


Section 13

CSS variable bridge

How settings reach the stylesheet

class-psw-settings.php → PSW_Settings::print_appearance_overrides() runs on wp_head at priority 10 and emits a :root block of 58 custom properties. The stylesheet reads them with fallbacks, so the plugin renders sensibly even if that block never prints.

Priority matters: enqueued stylesheets print at wp_head priority 8, so the generated block always follows the stylesheet. Where both declare !important, the generated block wins on source order. The navigation bar background depends on that ordering.

Per-instance overrides

The same variables can be set inline on a feed wrapper, which scopes them to that instance. This is what lets two feeds with different widths, column counts or image heights coexist on one page without colliding.

Two padding variables, deliberately separate. --psw-mobile-side-padding insets page content; --psw-nav-mobile-side-padding insets the navigation bar. They look interchangeable and are not: the nav bar is full-bleed and cancels the content inset with a negative margin, so unifying them would break one of the two.


Section 14

Asset loading

What loads, and where

class-psw-plugin.php → PSW_Plugin::maybe_enqueue_frontend_assets() gates the front-end stylesheet and script. They load when either condition holds:

  • The post content contains one of nine Blogano shortcodes.
  • The request is a singular post carrying PSW_META_FLAG, or a bio page.

Why the second condition matters. It means assets are present on a single submission and on author URLs, where no shortcode appears in the content. It also means a shortcode placed in a block template rather than post content still gets its assets on those screens.

Vendor libraries — Spectrum for colour, Summernote for the editor — are bundled under assets/ and loaded from the plugin, not a CDN. All enqueues are versioned with PSW_VERSION, so a release invalidates caches.


Section 15

Email & notifications

Five types, one log

TypeSettingRecipient
New submissionemail_notify_new_submissionAll editors and administrators
Publishedemail_notify_publishedThe author
Unpublishedemail_notify_unpublishedThe author
Reportedemail_notify_reportedAdministrator, publishing editor, author
Verificationemail_verification_enabledThe new member

A master switch, email_notifications_enabled, gates all five. Each type has an editable subject and body with placeholders, plus an optional BCC.

Verification is not merely a notification. email_verification_enabled is grouped with the four notify switches in the UI, but it changes authentication: with it on, a new account cannot log in until the token is consumed. Turning it off is not just muting an email. constraint

The log

Every send is recorded as a psw_email_log post carrying type, recipient and success. Success means wp_mail() accepted the message, not that it was delivered — a distinction worth preserving in any UI copy, because it is the difference between a plugin problem and a host problem.

Retention is a day count, default 30, enforced by a daily cleanup. Zero keeps everything. The limit exists because every entry holds an email address, so an unbounded log is an indefinite store of personal data.


Section 16

Query-argument vocabulary

The URL surface

PrefixUsed byArguments
psw_feed_*Live Feeds, author, date, type, category, pinned, paged
psw_bio_*Bio pages, paged
psw_dir_*Directorys, role, sort, paged
psw_* (bare)Dashboardss, paged, orderby, order, edit, uid
psw_*_noticeAllPost-redirect notice keys.
psw_verify / psw_verify_errorAuthEmail verification round trip.

psw_feed_category accepts repeated values and is read through psw_get_filter_categories(), which also accepts a bare scalar so links made before multi-select still work. The matching query fragment comes from psw_category_meta_clause(), which returns a plain clause for one term and a nested OR group for several.

Arguments are global to the page. They are not namespaced per instance. Every non-static feed on a page reads the same arguments, so two filterable feeds always move together. The static option exists as the practical answer; namespacing would mean threading an instance identifier through seven arguments and every URL builder in five files. constraint


Section 17

Constraints & known issues

What to know before changing things

Structural

  • Categories are meta, not a taxonomy. Section 11. The largest structural debt in the plugin, and it compounds with adoption.
  • Query arguments are global to the page. Section 16. One filterable feed per page is the supported arrangement.
  • No uninstall routine. Section 04. Roles, options and the log post type survive removal.
  • One filter as the entire extension surface. Section 07.
  • Migrations run on activation only. Section 04. Replacing files does not run them.

Performance

  • No query caching. Every feed instance runs its own uncached WP_Query, plus a pinned-IDs lookup when unfiltered. Three grids on a page is three queries per view. Acceptable at small scale; the first thing to address if it grows.
  • Avatars resolve per card. Each card showing an avatar performs an attachment lookup. Cached within a request, not across them.

Presentation

  • The settings screen has one inline script block. Section 12.
  • Fifteen descriptions sit outside a field across the settings tabs. They now render as plain text rather than orphaned icons, but they are section intros living in a field-shaped slot.
  • Sticky navigation and a transparent bar conflict by design. The bar is transparent on both breakpoints, so with sticky enabled, content scrolls visibly behind it. Nav Bar Clear is the switch that resolves it.

Fixed, recorded for context

  • Category sort disabled filters. Sorting by category replaced the whole meta_query with an EXISTS/NOT EXISTS pair under relation => OR; every filter appended afterwards joined that group, which matches everything. The sort group is now nested under an AND.
  • Duplicate detection never matched trailing slashes. The stored URL was verbatim and the search was normalised, so the two could not meet. Both sides now pass through psw_normalize_source_url(), and a backfill wrote the comparison key onto existing posts.
  • A shortcode-scoped category rendered as a removable chip that could not in fact be removed.

The general lesson from those three. Each was a normalisation or a grouping applied on one side of a pair and not the other. When adding anything that compares a stored value against a computed one, put the transformation in a single function and call it from both sides — the bug class disappears rather than being fixed once.