# Custom Guest Authors — Upgrade Planning

Future features, architectural changes, and development notes for upcoming
versions of the Custom Guest Authors plugin. Items are ordered by release
milestone. Each entry documents the rationale, affected files, and
implementation detail needed to resume development without losing design
decisions made during earlier sessions.

When a milestone ships, move its entries to `changelog.md` and remove them
here. Update the version bump checklist (five locations) before tagging.

---

## Release schedule

| Version | Theme                                        | Status   |
|---------|----------------------------------------------|----------|
| 2.3.1   | Plugin Check audit — PHPCS inline suppressions     | Current  |
| 2.3.0   | Stability, code hygiene, top-level menu            | Released |
| 2.4.0   | Byline card, author URL, feeds, UI refresh   | Planned  |
| 2.5.0   | Gutenberg block, onboarding, audit log       | Planned  |
| 2.6.0   | REST endpoint, schema modes, WP-CLI          | Planned  |
| 2.7.0+  | Build tooling, meta migration, multisite     | Future   |

---

## 2.3.0 — Stability and code hygiene

### Feature 1 — MENJ Plugin Suite top-level menu

**Scope:** `custom-guest-authors.php`, `readme.txt`, `changelog.md`

Group all suite plugins (Auto Justify Content, Abstract Box, Cite, Endmark,
Custom Guest Authors) under a shared `MENJ Plugins` top-level admin menu
rather than separate entries under Settings.

**Implementation notes:**
- Top-level slug: `menj-plugin-suite` — dashicon: `dashicons-admin-plugins`.
- Only one plugin should call `add_menu_page()`; all others attach via
  `add_submenu_page()`. Use a `did_action('menj_suite_menu_registered')`
  check, or a shared constant, to prevent duplicates regardless of load order.
- Replace the existing `Settings › Guest Authors` entry with
  `MENJ Plugins › Guest Authors`. Add a one-version redirect shim from the
  old URL so existing bookmarks do not break.
- When both Endmark and CGA are active, only one plugin's byline card should
  auto-append to `the_content`. Use `function_exists()` detection to gate
  the CGA auto-append behind Endmark's absence.

### Issue D — Narrow `register_post_meta()` REST scope

**Scope:** `includes/post-meta.php`

`register_post_meta( '', 'guest-author', ... )` registers the meta globally
across all post types. This exposes the meta in REST responses for post types
where CGA is not enabled.

**Implementation notes:**
- Replace the single call with a loop over `cga_get_option('cga_enabled_post_types')`.
- Call `register_post_meta( $post_type, 'guest-author', ... )` for each
  enabled type individually.
- Run after the `add_post_type_support()` call at `init` priority 9 (priority
  10 unchanged).

### Issue E — Move version-based cache flush to `upgrader_process_complete`

**Scope:** `includes/cache.php`

The version-based transient flush currently runs on every `init`. This costs
one `get_option()` call per request on sites that are never updated.

**Implementation notes:**
- Remove the `init` hook for the version flush.
- Add a hook on `upgrader_process_complete`. Check `$hook_extra['plugin']`
  matches `custom-guest-authors/custom-guest-authors.php` before flushing.
- Replace the `init` fallback (manual file-drop upgrades) with a
  `register_activation_hook()` flush instead.

### Issue J — Unify `--wpcp-primary` across CSS files

**Scope:** `assets/css/settings.css`, `assets/css/meta-box.css`,
`assets/css/gutenberg-sidebar.css`

`settings.css` defines `--wpcp-primary: #1B3C53` (navy). `meta-box.css`
and `gutenberg-sidebar.css` define `--wpcp-primary: #64748b` (slate). The
same token name resolves to visually different colours depending on context.

**Implementation notes:**
- Canonical value for `--wpcp-primary` suite-wide: `#1B3C53` (navy).
- In `meta-box.css` and `gutenberg-sidebar.css`, rename the slate token to
  `--wpcp-slate` and update all internal references.
- Update the palette table in Architecture Notes once done.

### Issue L — Convert `$tab_url` closure to named function

**Scope:** `admin/admin.php`

The `$tab_url` closure is not reusable across files and cannot be unit-tested.

**Implementation notes:**
- Extract into a named function `cga_tab_url( $tab )` in `admin/admin.php`.
- Replace all `$tab_url( '...' )` call sites with `cga_tab_url( '...' )`.
- Wrap with `if ( ! function_exists( 'cga_tab_url' ) )`.

---

## 2.4.0 — Byline card, author URL, feeds, admin UI refresh

### Feature 2 — Guest author byline card

**Scope:** `custom-guest-authors.php`, `includes/front-end.php`,
`admin/admin.php`, `assets/js/gutenberg-sidebar.js`,
`assets/css/meta-box.css`, `assets/css/gutenberg-sidebar.css`,
new `assets/css/byline-card.css`

A shortcode `[guest_author_card]` and optional auto-append toggle to render
a styled byline card beneath post content, displaying guest author name(s)
and an optional per-post bio line.

**Implementation notes:**
- New meta key: `guest-author-bio` (textarea). Register with
  `show_in_rest => true`, `type => 'string'`,
  `sanitize_callback => 'wp_kses_post'`.
- Classic editor meta box: add a `<textarea>` for the bio below the existing
  name input in `admin/admin.php`.
- Gutenberg sidebar: add a `TextareaControl` for `guest-author-bio`.
- Auto-append toggle on the Display tab: new option `cga_auto_append_card`
  (boolean, default `false`). Register under `cga_display` group. Add to
  `uninstall.php`.
- Byline card output via `add_filter( 'the_content', ..., 20 )`. Gate on
  `is_singular()`. Respect Endmark conflict guard (Feature 1).
- New file `assets/css/byline-card.css`. Front-end CSS conventions:
  - **FE-1:** CSS vars injected via `wp_add_inline_style()` on the enqueued
    handle. Static file contains only structural rules with fallback values.
  - **FE-2:** Hover states gated by `@media (hover: hover)` AND an opt-in
    class (`cga_byline_hover_effect`, default off) to prevent stuck hover on
    touch devices.
  - **FE-3:** `transition` declared on the base container selector only.
  - **FE-4:** Scoped list reset inside the bio content area to resist theme
    CSS resets.
- Enqueue `byline-card.css` unconditionally on the front end.

### Feature 3 — Per-post author URL override

**Scope:** `includes/front-end.php`, `includes/post-meta.php`,
`admin/admin.php`, `assets/js/gutenberg-sidebar.js`

Allow an optional per-post URL field instead of always suppressing the
author archive URL when a guest author is set.

**Implementation notes:**
- New meta key: `guest-author-url`. Register with `show_in_rest => true`,
  `type => 'string'`, `sanitize_callback => 'esc_url_raw'`.
- Gutenberg sidebar: `TextControl` for the URL below the name input.
- Classic editor meta box: `<input type="url">` below the name input.
- Update `custom_guest_authors_suppress_url()`:
  - Return custom URL when `guest-author-url` meta is set and guest author
    is active.
  - Return `''` (suppress) when guest author is set but no URL provided.
  - Return original WP author URL when no guest author is set.
- Add `guest-author-url` to `uninstall.php` meta cleanup list.

### Feature 4 — RSS feed toggle

**Scope:** `includes/front-end.php`, `admin/admin.php`

A toggle to control whether the guest author override applies inside RSS/Atom
feeds independently from on-site display.

**Implementation notes:**
- New option: `cga_apply_in_feeds` (boolean, default `true`). Register under
  `cga_display` group. Add to `uninstall.php`.
- Display tab: toggle row after "Show Override On" radio group.
- Add `is_feed()` check at the top of both filter functions:
  `if ( is_feed() && ! cga_get_option( 'cga_apply_in_feeds', true ) ) { return $name; }`

### Admin UI refresh — gaps 2, 3, 4, 6

**Scope:** `assets/css/settings.css`

**Gap 2 — Gradient hero header:**
Replace `background: #111d27` on `.wpcp-hero` with
`background: linear-gradient(135deg, #1B3C53 0%, #2E6A8E 100%)`.
Keep `border-radius: var(--wpcp-radius-lg) var(--wpcp-radius-lg) 0 0`.

**Gap 3 — Tab strip on white surface:**
Set `.wpcp-tabs` to `background: #ffffff`,
`border-bottom: 1px solid #e2e8f0`. Active tab: `color: #1B3C53`,
`border-bottom: 2px solid #2E6A8E`. Inactive: `color: #64748b`.

**Gap 4 — Per-field-row bordered cards:**
Add `border: 1px solid #e2e8f0` and a
`border-color: var(--wpcp-secondary)` hover transition to each field row
wrapper. Transition: `border-color 0.16s ease`.

**Gap 6 — Branded `.button-primary` override:**
Inside `.cga-wrap`, override WP core's `.button-primary`:
`background: linear-gradient(135deg, #1B3C53 0%, #2E6A8E 100%)`,
`border: none`, `box-shadow: 0 1px 3px rgba(27,60,83,.25)`,
hover: `filter: brightness(1.08)`.

---

## 2.5.0 — Gutenberg block, onboarding, audit log

### Feature 5 — Guest author byline block

**Priority:** Depends on Feature 2.
**Scope:** `custom-guest-authors.php`, new `assets/js/byline-block.js`,
new `assets/css/byline-block.css`

A Gutenberg block (`custom-guest-authors/byline`) rendering the byline card
inline in the editor with live preview.

**Implementation notes:**
- `register_block_type()` with PHP `render_callback` producing identical HTML
  to the `[guest_author_card]` shortcode.
- Block category: `text`.
- Editor preview via `useEntityProp( 'postType', postType, 'meta' )`.
- Block attribute `show_bio` (boolean, default `true`) in the inspector.
- Block script registered in `cga_enqueue_block_editor_assets()`, loaded only
  in the block editor context.
- `byline-block.css` shares token and structural conventions with
  `byline-card.css` (FE-1 through FE-4). Do not duplicate base styles.

### Feature 9 — Activation onboarding notice

**Scope:** `custom-guest-authors.php`, `admin/admin.php`

A one-time admin notice on first activation linking to the settings page.

**Implementation notes:**
- `register_activation_hook()` sets a transient `cga_show_welcome`.
- `admin_notices` hook in `admin/admin.php`: if transient exists, render
  notice using `.cga-notice` and delete the transient.
- Dismissal via a nonce-signed URL, not relying solely on WP core's
  client-side `.notice-dismiss` JS.

### Feature 10 — Per-post override audit log on Debug tab

**Scope:** `admin/views/tab-debug.php`, `admin/views/settings-page.php`

A read-only table on the Debug tab listing the 20 most recent posts where
`guest-author` meta is set.

**Implementation notes:**
- Query via `get_posts()`: `meta_key = 'guest-author'`,
  `posts_per_page = 20`, `post_status = 'any'`.
- Cache with `wp_cache_set()` for 30 seconds under `cga_audit_log` in the
  `custom-guest-authors` group.
- Bust cache in `custom_guest_authors_invalidate_cache()` alongside the
  per-post transient.
- Render as `<table class="widefat">`. No sorting or pagination in v2.5.0.

---

## 2.6.0 — REST endpoint, schema modes, WP-CLI

### Feature 6 — REST autocomplete endpoint

**Scope:** `custom-guest-authors.php`

`GET /wp-json/cga/v1/authors` returning a deduplicated list of recently used
guest author names for autocomplete in the Gutenberg sidebar.

**Implementation notes:**
- `register_rest_route( 'cga/v1', '/authors', ... )` on `rest_api_init`.
- Query: `get_posts()` with `meta_key = 'guest-author'`,
  `posts_per_page = 200`, `fields = 'ids'`, then explode by comma,
  `array_unique()`, `array_filter()`.
- Permission callback: `current_user_can( 'edit_posts' )`.
- Cache with `wp_cache_set()` for 60 seconds.
- Gutenberg sidebar: replace `TextControl` with `ComboboxControl` reading
  suggestions via `apiFetch( { path: '/cga/v1/authors' } )`.

### Feature 11 — Schema substitution mode

**Scope:** `admin/admin.php`, `includes/front-end.php`

Extend `cga_suppress_schema` to a three-state `cga_schema_mode`:
`none` | `suppress` | `replace`.

**Implementation notes:**
- Migrate old boolean `cga_suppress_schema` on first load: if value is `1`,
  write `cga_schema_mode = suppress`.
- `none`: no schema filters registered.
- `suppress`: current behaviour — remove author node.
- `replace`: build a minimal `Person` node from the guest author name and
  inject it into `@graph`.
- Advanced tab: replace the toggle with a three-option radio card group.

### Feature 12 — WP-CLI command group

**Scope:** `custom-guest-authors.php`, new `includes/cli.php`

Load `includes/cli.php` only when `defined( 'WP_CLI' ) && WP_CLI`.

**Commands:**
- `wp cga list` — table of posts with `guest-author` meta.
- `wp cga set <post-id> <name>` — write meta and bust transient cache.
- `wp cga clear-cache [--all]` — bust all `cga_*` transients; `--all`
  adds network-wide sweep on multisite.
- `wp cga flush-option-cache` — call `cga_flush_option_cache()`.

All commands respect `cga_enabled_post_types` and warn on mismatches.

---

## 2.7.0+ — Build tooling, meta migration, multisite

### Feature 7 — Meta key migration tool

**Scope:** `admin/views/tab-debug.php`

A Debug tab tool that copies post meta from a user-specified source key into
`guest-author`, with dry-run mode showing affected post counts before commit.

**Implementation notes:**
- Form: `<input type="text">` for the source key, nonce field, Dry Run /
  Migrate button pair.
- Dry-run: `$wpdb->get_var()` count with `WHERE meta_key = %s AND meta_value != ''`.
- Migration: `$wpdb->query()` INSERT/SELECT with `ON DUPLICATE KEY UPDATE`
  or prior delete pass.
- Nonce action: `cga_migrate_meta`.
- Post-migration: bulk-delete all `cga_*` transients.
- Add `DirectDatabaseQuery` suppression to `phpcs.xml` with explanation.

### Feature 8 — `@wordpress/scripts` build step

**Scope:** `assets/js/gutenberg-sidebar.js`, `assets/js/byline-block.js`,
`package.json`, `.gitignore`

Adopt `@wordpress/scripts` for JSX authoring in Gutenberg files.

**Implementation notes:**
- Move source files to `src/gutenberg-sidebar.jsx` and `src/byline-block.jsx`.
- `npm run build` outputs to existing `assets/js/` paths (enqueue handles
  unchanged).
- Add `/node_modules/` and `/src/*.js.map` to `.gitignore`.
- Update `phpcs.xml` `<exclude-pattern>` to exclude `node_modules`.
- Implement last — this is a pure DX improvement with no functional change.

### Issue G — Multisite network-wide Clear Cache

**Scope:** `admin/views/tab-debug.php`, `admin/views/settings-page.php`

The current Clear Cache only deletes transients on the current site.

**Implementation notes:**
- Add a "Clear cache network-wide" button, visible only when `is_multisite()`.
- Use `switch_to_blog()` / `restore_current_blog()` to iterate all network
  sites and run the DELETE query per site.
- Gate behind `current_user_can( 'manage_network' )`.
- Also exposed as `wp cga clear-cache --all` (Feature 12).

---

## Architecture notes

### Asset organisation

All CSS and JS assets live under `assets/css/` and `assets/js/`. New assets
must follow this layout. Do not place CSS or JS in the plugin root or in
`admin/`.

### CSS token scoping

CSS custom properties must be declared on a scoped wrapper selector, not on
`:root`. Correct scopes:

| File                               | Wrapper selector      |
|------------------------------------|-----------------------|
| `assets/css/settings.css`          | `.cga-wrap`           |
| `assets/css/meta-box.css`          | `#cga-meta-box`       |
| `assets/css/gutenberg-sidebar.css` | `.cga-sidebar-panel`  |
| `assets/css/byline-card.css`       | `.cga-byline-card`    |

`:root` declarations bleed into the host page and conflict with other plugins
sharing the same token names.

### Palette

| Token               | Value     | Role                           |
|---------------------|-----------|--------------------------------|
| `--wpcp-primary`    | `#1B3C53` | Navy — headers, borders        |
| `--wpcp-secondary`  | `#2E6A8E` | Teal — accents, active tabs    |
| `--wpcp-accent`     | `#4F7FA0` | Steel — secondary accents      |
| `--wpcp-stone`      | `#C8BAB0` | Stone — muted surfaces         |
| `--wpcp-slate`      | `#64748b` | Slate — meta-box / sidebar (pending Issue J rename) |

Note: `meta-box.css` and `gutenberg-sidebar.css` currently use `#64748b`
under the name `--wpcp-primary`. Issue J (v2.3.0) renames that token to
`--wpcp-slate` in those two files and aligns `--wpcp-primary` to navy.

### CSS class prefix

Utility and component classes use the `wpcp-` prefix. Plugin-specific
structural classes use the `cga-` prefix. Reuse existing toggle, radio card,
and checkbox card components from `settings.css` rather than duplicating.

### Option naming

All WordPress options use the `cga_` prefix. New options must be registered
in `cga_register_settings()` in `admin/admin.php` and added to the deletion
array in `uninstall.php`.

### Per-request option cache

`cga_get_option( $option, $default )` in `includes/front-end.php` wraps
`get_option()` with a static array cache. All new option reads in
`includes/front-end.php` must use `cga_get_option()`. Admin-only files may
call `get_option()` directly. Call `cga_flush_option_cache()` after any save
that could alter option values mid-request.

### Transient cache

Guest author names are cached per post under the `cga_{post_id}` transient
key. The sentinel `CGA_NO_META` (`'__cga_none__'`) caches confirmed DB misses.
Any new per-post meta key requiring caching must add invalidation to
`custom_guest_authors_invalidate_cache()` in `includes/cache.php`.

### phpcs.xml suppressions

New `$wpdb->query()` or `$wpdb->get_var()` calls must use a per-line
`// phpcs:ignore WordPress.DB.DirectDatabaseQuery` annotation with a comment
explaining why no WP API equivalent exists. File-level `<exclude-pattern>`
blocks for escape or query rules are not permitted.

### Version bump checklist

Update the version string in all five locations when releasing:

1. `Version:` header in `custom-guest-authors.php`
2. `define( 'CGA_VERSION', '...' )` in `custom-guest-authors.php`
3. `Stable tag:` in `readme.txt`
4. New entry at the top of `changelog.md`
5. Move shipped features from `upgrading.md` to `changelog.md` and remove
   or update their entries here.
