Contents
- Scope & conventions
- Bootstrap & load order
- Class map
- Lifecycle
- Permission model
- Hooks consumed
- Extension points
- Shortcodes
- AJAX endpoints
- admin-post endpoints
- Data model
- Settings architecture
- CSS variable bridge
- Asset loading
- Email & notifications
- Query-argument vocabulary
- Constraints & known issues
Blogano Technical Reference
| Audience | Maintainers and developers |
|---|---|
| Licence | GPL-2.0-or-later |
| Requires | WordPress 5.8+, PHP 7.4+ |
| Covers | 19 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
| Constant | Value | Notes |
|---|---|---|
PSW_VERSION | 1.0.0 | Also used as the asset cache-busting version. |
PSW_PLUGIN_FILE | __FILE__ | Passed to the activation hook. |
PSW_PLUGIN_DIR | plugin_dir_path() | Base for every require_once. |
PSW_PLUGIN_URL | plugin_dir_url() | Base for asset enqueues. |
PSW_META_FLAG | _psw_submission | The 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
| File | Class | Responsibility |
|---|---|---|
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.php | PSW_Roles | Registers the two custom roles, hides WordPress’s native roles from the role selector. |
class-psw-plugin.php | PSW_Plugin | Asset enqueue and its gating, the full-bleed shortcode wrapper. |
class-psw-ajax.php | PSW_Ajax | All nine AJAX endpoints: URL fetch, submit, image upload and sideload, avatar, pins, reports. |
class-psw-shortcodes.php | PSW_Shortcodes | The submission wizard markup and its Summernote editor. |
class-psw-user-dashboard.php | PSW_User_Dashboard | My Posts — a member’s own submissions, with filters and member pins. |
class-psw-admin.php | PSW_Admin | wp-admin menus, the Users screen, admin notices. |
class-psw-admin-list-table.php | PSW_Admin_List_Table | The wp-admin submissions list table. |
class-psw-auth.php | PSW_Auth | Front-end register, login, logout, email verification and its token flow. |
class-psw-user-profile.php | PSW_User_Profile | The member’s own profile editor and avatar handling. |
class-psw-live-feed.php | PSW_Live_Feed | The feed, all card styles, the featured post, and the 22 shortcode options. |
class-psw-frontend-admin-dashboard.php | PSW_Frontend_Admin_Dashboard | The Editorial Desk. |
class-psw-nav.php | PSW_Nav | The account bar, its burger mode, and prepending it to shortcode pages. |
class-psw-settings.php | PSW_Settings | Six settings tabs, three save paths, the generated CSS variable block, the Settings Manual, CSV import and export. |
class-psw-bio-page.php | PSW_Bio_Page | Takes over author URLs and renders the member bio page. |
class-psw-user-directory.php | PSW_User_Directory | The contributor directory. |
class-psw-notifications.php | PSW_Notifications | Composes and sends the five notification emails. |
class-psw-email-log.php | PSW_Email_Log | Records sends, the log screen, retention cleanup. |
class-psw-social-meta.php | PSW_Social_Meta | Open 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:
| Step | What it does | Idempotent? |
|---|---|---|
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
| Role | Slug | Capabilities |
|---|---|---|
| Publisher | psw_publisher | read, edit_posts, edit_published_posts, delete_posts, delete_published_posts, upload_files |
| Editor (Submissions) | psw_editor | The above plus edit_others_posts, publish_posts, and the delete-others and read-private equivalents |
| Administrator | administrator | WordPress 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
| Hook | Kind | Consumer | Purpose |
|---|---|---|---|
plugins_loaded | action | blogano.php | Bootstrap. |
init | action | PSW_Email_Log | Registers the log post type. |
wp_enqueue_scripts | action | PSW_Plugin | Conditional front-end assets — see section 14. |
the_content | filter | PSW_Nav | Prepends the account bar to pages holding a Blogano shortcode. |
the_content | filter | PSW_Live_Feed | Appends the Report Post control to a single submission. |
template_redirect | action | PSW_Bio_Page | Takes over author URLs entirely. |
wp_head | action | PSW_Settings | Emits the generated CSS variable block, priority 10. |
wp_head | action | PSW_Social_Meta | Emits sharing tags, priority 5. |
wp_robots | filter | PSW_Social_Meta | Adds noindex to link posts when enabled. |
wp_ajax_* / wp_ajax_nopriv_* | action | PSW_Ajax | Nine endpoints — section 09. |
admin_post_* | action | PSW_Admin, PSW_Frontend_Admin_Dashboard | Row and bulk actions — section 10. |
editable_roles | filter | PSW_Roles | Hides WordPress’s native roles. |
show_admin_bar | filter | PSW_Nav, PSW_Bio_Page | Hides the admin bar for members, and unconditionally on bio pages. |
save_post | action | helpers.php | Clears the cached page-URL transients. |
register_activation_hook | action | blogano.php | Section 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.
| Filter | Signature | Purpose |
|---|---|---|
psw_emit_social_meta | bool $emit | Forces 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
| Tag | Class | Capability required |
|---|---|---|
psw_live_feed | PSW_Live_Feed | None |
psw_login | PSW_Auth | None |
psw_register | PSW_Auth | None |
psw_resend_verification | PSW_Auth | None |
psw_user_directory | PSW_User_Directory | None |
psw_submission_form | PSW_Shortcodes | edit_posts |
psw_user_dashboard | PSW_User_Dashboard | edit_posts |
psw_user_profile | PSW_User_Profile | Logged in |
psw_admin_dashboard | PSW_Frontend_Admin_Dashboard | edit_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.
| Option | Type | Effect |
|---|---|---|
limit | int | Overrides feed_posts_per_page. |
category | csv | Scoped categories. Suppresses the visitor breadcrumb for those terms and overrides URL filtering. |
columns / columns_mobile | int | Grid columns; emitted inline and as --psw-columns-mobile. |
style | enum | standard / overlay / full-image. |
featured / grid | bool | Featured card and card grid respectively. grid="0" also suppresses pagination. |
search / pagination / cta | bool | Chrome. The first two are forced false when static is set. |
static | bool | Suppresses every $_GET read for this instance. |
show | csv | Whitelist over the ten feed_show_* settings. |
square / image_height | bool / int | Image sizing; the latter emitted as --psw-card-image-height. |
width / padding_mobile / gap / gap_mobile | css / int | Emitted as inline CSS custom properties on the feed wrapper. |
orderby / order | enum | Whitelisted to date, title, rand, menu_order and asc / desc. |
class | string | Passed through sanitize_html_class() per token. |
nav | bool | Read 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
| Action | Auth | Purpose |
|---|---|---|
psw_fetch_og | Logged in | Fetches Open Graph data from a pasted URL and runs the duplicate check. |
psw_submit_post | Logged in | Creates or updates a submission. |
psw_get_submission | Logged in | Loads an existing submission into the wizard for editing. |
psw_upload_image | Logged in | Handles a direct file upload. |
psw_sideload_image | Logged in | Pulls a remote image into the media library. |
psw_save_avatar | Logged in | Stores a member avatar. |
psw_remove_avatar | Logged in | Clears it. |
psw_set_pin_position | Logged in | Sets a site or member pin slot. |
psw_report_post | Both | The 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
| Action | Consumer | Purpose |
|---|---|---|
psw_user_action | PSW_Admin | Role changes from the wp-admin Users screen. |
psw_frontend_admin_action | PSW_Frontend_Admin_Dashboard | Approve, 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
| Key | On | Meaning |
|---|---|---|
_psw_submission | Every submission | The flag. Absent means not Blogano’s. |
_psw_source_url | Link submissions | The URL as submitted. Used for the outbound link, so never normalised. |
_psw_source_url_key | Link submissions | Normalised form of the above, used only for duplicate matching. |
_psw_og_type | Link submissions | Open Graph type read from the source. |
_psw_post_type | All | Blogano’s own type distinction: link or article. |
_psw_categories | Optional | Serialized array of category names. Not a taxonomy. |
_psw_rtl | Optional | Per-post direction override. |
_psw_pinned_admin_order | Optional | Site pin slot, 1–3. |
_psw_pinned_user_order | Optional | Member pin slot, 1–3. |
_psw_reported / _psw_rejected | Optional | Moderation state. |
_psw_published_by | On publish | Which editor approved it. |
_psw_log_type / _psw_log_to / _psw_log_success | Email log entries | Type, 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
| Option | Purpose |
|---|---|
psw_settings | The entire settings array — one option, 172 keys. |
psw_blogano_category_id | Cached ID of the WordPress blogano category. |
psw_pages_created | First-activation flag for page creation. |
psw_source_url_key_backfill | Migration flag for the URL comparison key. |
psw_email_log_last_viewed | Drives 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
| Path | Nonce | Covers |
|---|---|---|
save_settings_page() | Standard settings nonce | Layout, Buttons & Borders, Cards & Feed — branched on the posted psw_settings_tab. |
save_shortcodes_page() | Own nonce | Page assignments, reCAPTCHA keys, Search Engines. |
save_communication_page() | psw_communication_save | The 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
| Type | Setting | Recipient |
|---|---|---|
| New submission | email_notify_new_submission | All editors and administrators |
| Published | email_notify_published | The author |
| Unpublished | email_notify_unpublished | The author |
| Reported | email_notify_reported | Administrator, publishing editor, author |
| Verification | email_verification_enabled | The 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
| Prefix | Used by | Arguments |
|---|---|---|
psw_feed_* | Live Feed | s, author, date, type, category, pinned, paged |
psw_bio_* | Bio page | s, paged |
psw_dir_* | Directory | s, role, sort, paged |
psw_* (bare) | Dashboards | s, paged, orderby, order, edit, uid |
psw_*_notice | All | Post-redirect notice keys. |
psw_verify / psw_verify_error | Auth | Email 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_querywith anEXISTS/NOT EXISTSpair underrelation => OR; every filter appended afterwards joined that group, which matches everything. The sort group is now nested under anAND. - 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.