This file covers what is specific to newspack-plugin. Shared conventions (Docker commands, n script, coding standards, git rules, etc.) are in the root newspack-workspace/AGENTS.md.
npm run lint # JS + SCSS only (see gotchas)
npm run lint:js # JavaScript/TypeScript linting
npm run lint:scss # SCSS linting
npm run lint:php # PHP linting (PHPCS)
npm run fix:js # Auto-fix JS issues
npm run fix:php # Auto-fix PHP issues (PHPCBF)npm run lintruns JS + SCSS only. PHP linting requires a separatenpm run lint:php.- After adding a new PHP file, run
composer dump-autoloadto update the classmap (Composer usesclassmap, not PSR-4). - Individual JS test files cannot run independently. Always run
npm testfor the full suite. - Never import
react-router-domdirectly in source code. Use the proxy:import Router from '../../packages/components/src/proxied-imports/router'. Tests may importreact-router-domdirectly. - New standalone webpack entry points must import
src/shared/js/public-path.jsfirst. - Plugin integration classes in
includes/plugins/use the rootNewspacknamespace despite living in subdirectories.
newspack.php: Main plugin file, defines constants (NEWSPACK_ABSPATH,NEWSPACK_PLUGIN_FILE, etc.), requires Composer autoloader and Action Scheduler.includes/class-newspack.php: Singleton main class. Manuallyinclude_onces files in a specific order viaincludes(), then hooksinit()methods.- Autoloading: Composer
classmapstrategy (not PSR-4). After adding a new PHP file, runcomposer dump-autoloadto update the classmap.
New classes should follow the static init() pattern (dominant, used by 100+ classes):
namespace Newspack;
class My_Feature {
public static function init() {
add_action( 'init', [ __CLASS__, 'register_things' ] );
}
// ... static methods ...
}
My_Feature::init();Two other patterns exist but are legacy or special-purpose:
new ClassName()at file bottom (rare, ~7 classes likeAPI,Profile)Newspack::instance()singleton (only the main class)
Classes that should not be extended are marked final.
| Namespace | Directory |
|---|---|
Newspack |
includes/ (root, most classes) |
Newspack\API |
includes/api/ |
Newspack\CLI |
includes/cli/ |
Newspack\Data_Events |
includes/data-events/ |
Newspack\Data_Events\Connectors |
includes/data-events/connectors/ |
Newspack\Reader_Activation |
includes/reader-activation/ |
Newspack\Reader_Activation\Sync |
includes/reader-activation/sync/ |
Newspack\Reader_Activation\Integrations |
includes/reader-activation/integrations/ |
Newspack\Wizards |
includes/wizards/ |
Newspack\Wizards\Newspack |
includes/wizards/newspack/ |
Newspack\Wizards\Traits |
includes/wizards/traits/ |
Newspack\Optional_Modules |
includes/optional-modules/ |
Newspack\Content_Gate |
includes/content-gate/ |
Newspack\Collections |
includes/collections/ |
Two namespace constants, defined in includes/util.php:
NEWSPACK_API_NAMESPACE=newspack/v1(primary)NEWSPACK_API_NAMESPACE_V2=newspack/v2
Three patterns for registering routes (in order of prevalence):
-
Wizard Section routes (most common): Extend
Wizard_Section, implementregister_rest_routes()(auto-hooked torest_api_init). Route pattern:wizard/{wizard_slug}/{section}. Permission:$this->api_permissions_check(). -
Wizard routes: Extend
Wizard, implementregister_api_endpoints(). Route pattern:wizard/{slug}/.... Permission:$this->api_permissions_check(). -
Standalone controllers: In
includes/api/, extendWP_REST_Controller. Only 2 exist (Plugins_Controller,Wizards_Controller).
Common sanitize callbacks from util.php: Newspack\newspack_clean(), Newspack\newspack_string_to_bool().
Two levels of abstraction:
-
Wizard(abstract,includes/wizards/class-wizard.php): Override$slug,$capability,get_name(). Renders an empty<div>for React hydration. Providesapi_permissions_check(), completion tracking, admin menu registration. -
Wizard_Section(abstract,includes/wizards/class-wizard-section.php): Modular sections within a wizard. Set$wizard_slug, implementregister_rest_routes(). Used by 9+ section classes underincludes/wizards/newspack/(Emails, Pixels, Social, etc.).
For the frontend counterpart, see Frontend > Wizard System below.
Simple integrations live in single files: includes/plugins/class-{plugin}.php. Complex integrations span subdirectories: includes/plugins/woocommerce/, includes/plugins/co-authors-plus/, includes/plugins/woocommerce-subscriptions/, etc.
Some integrations have corresponding Configuration Managers in includes/configuration_managers/.
Feature flags managed by includes/optional-modules/class-optional-modules.php. Check the class for the current module list. Enable/disable via wp newspack optional-modules enable|disable|list.
| Mechanism | Convention | Examples |
|---|---|---|
wp_options |
Prefixed per subsystem | newspack_reader_activation_*, newspack_donation_* |
| Custom Post Types | Short prefix | newspack_rr_email, np_content_gate |
| User meta | np_ prefix |
np_reader, np_reader_email_verified |
| Feature flag constants | In wp-config.php |
NEWSPACK_CONTENT_GATES, NEWSPACK_LOG_LEVEL |
Newspack\Logger provides:
Logger::log( $payload, $header, $type ): gated byNEWSPACK_LOG_LEVELconstant (0 = off, 1 = basic, 2 = verbose).Logger::newspack_log( $code, $message, $data, $type ): firesnewspack_logaction (consumed by Newspack Manager).
Subsystems use a LOGGER_HEADER constant (e.g., Data_Events::LOGGER_HEADER = 'NEWSPACK-DATA-EVENTS').
All under the newspack namespace, defined in includes/cli/. Run wp newspack --help to list available subcommands.
npm run lint:php # PHP linting (PHPCS)
npm run fix:php # Auto-fix PHP issues (PHPCBF)- Tests live in
tests/unit-tests/, extendWP_UnitTestCase. - Bootstrap:
tests/class-newspack-unit-tests-bootstrap.php. Mocks intests/mocks/. - Available
@groupannotations:byline-block,corrections,Access_Rules,WooCommerce_Subscriptions_Integration. - Run tests via
n test-phpfrom the repo directory (see parent AGENTS.md for flags).
For the PHP backend counterpart, see PHP Backend > Wizard System above.
Modern pattern (use for new code):
- Single entry point:
src/wizards/index.tsxusesReact.lazy()+Suspenseto load views by?page=URL param. - Views are function components (
.tsxpreferred). - Data fetching:
useWizardApiFetch(slug)hook fromsrc/wizards/hooks/. - Composition helpers:
WizardsTab,WizardSection,WizardsActionCardfromsrc/wizards/. - Used by: Dashboard, Settings, Audience pages.
Legacy pattern (found in older wizards):
- Standalone webpack entry per wizard:
src/wizards/{name}/index.js. - Uses
withWizard(Component, requiredPlugins)HOC. - Data fetching via
wizardApiFetchprop (from HOC). - Mounts via
createRoot()+render(). - Used by: Setup, Newsletters, Advertising.
@wordpress/data custom store, namespace newspack/wizards (WIZARD_STORE_NAMESPACE):
- Key selectors:
getWizardAPIData(slug),getWizardData(slug),isLoading(). - Key actions:
wizardApiFetch,saveWizardSettings. - Auto-fetches data from
/newspack/v1/wizard/{slug}via resolver. - Helper:
useWizardData(wizardName)frompackages/components/src/wizard/store/utils.js.
Reader activation frontend uses a separate localStorage-based store (src/reader-activation/store.js), not @wordpress/data.
Mixed JS/TS codebase (~30% TypeScript). Newer wizard views are .tsx; older wizards, blocks, and most components remain .js.
- Config extends
newspack-scripts/config/tsconfig.json(strict mode). - Type declarations: ambient
.d.tsfiles with global types (no imports needed). Located atsrc/wizards/types/and feature-leveltypes.d.ts. - Window globals typed via
declare global { interface Window { ... } }.
Order (each group separated by a blank line with a JSDoc comment header):
/** External dependencies */(classnames, lodash, etc.)/** WordPress dependencies */(@wordpress/*)/** Internal dependencies */(relative paths)
Component library: Import from packages/components/src via relative paths (no webpack alias):
import { Button, ActionCard } from '../../packages/components/src';Router: In source code, always import through the proxy, never directly from react-router-dom (tests may import directly):
import Router from '../../packages/components/src/proxied-imports/router';
const { HashRouter, Route, Switch } = Router;Colors in JS: import colors from '../../packages/colors/colors.module.scss';
Base config from newspack-scripts, which extends @wordpress/scripts.
Auto-discovered entries:
- Wizards:
src/wizards/*/index.{js,tsx}(~7 standalone entries) - Other scripts:
src/other-scripts/*/index.js(~11 entries)
Hardcoded entries (~30): reader-activation scripts, content-gate scripts, my-account variants, admin/editor scripts, blocks, collections, newspack-ui, bylines, and more. All declared in webpack.config.js.
Code splitting: Hashed chunk filenames, commons split chunk. Public path set dynamically via src/shared/js/public-path.js from window.newspack_urls.public_path. Any standalone entry point must import this file first.
Ad blocker workaround: advertising/ wizard bundled as billboard.js.
Blocks in src/blocks/ with block.json metadata. Central registration in src/blocks/index.js.
- Each block exports
{ metadata, name, settings }. - Conditional registration based on
window.newspack_blocksfeature flags (has_reader_activation,corrections_enabled,collections_enabled,has_memberships,is_block_theme). - Icons from
packages/icons/with foreground color frompackages/colors/. Seepackages/icons/DEVELOPMENT.mdfor the icon selection hierarchy (prefer@wordpress/iconsfirst, then Newspack icons) and React/PHP usage patterns. - Some blocks have separate webpack entries for frontend
view.jsscripts.
- Design tokens in
packages/colors/colors.module.scss(primary, secondary, tertiary, quaternary, neutral + semantic colors, each with 000-1000 scale). - See
packages/colors/DEVELOPMENT.mdfor the color usage decision tree: backend admin uses WordPress colors (withprimary-600accent override), block icons must useprimary-400, frontend Newspack UI usesnewspack-colors, theme elements use the theme palette. - BEM-ish naming with
newspack-prefix (e.g.,.newspack-wizard__header,.newspack-card). - Tachyons CSS utility library available for utility classes.
- Shared mixins in
src/shared/scss/_mixins.scss.
npm test # IMPORTANT: Always run the full suite. Individual test files cannot run independently.
npm run tsc # TypeScript type checking (watch mode, no emit)- Jest via
newspack-scripts test(wraps@wordpress/scripts). - Test files colocated with source using
.test.jssuffix. - Libraries:
@testing-library/react(render,fireEvent,waitFor,screen). - Mocking:
jest.mock()for@wordpress/data,@wordpress/api-fetch; direct manipulation ofwindow.*globals.
- Create a PHP class in
includes/wizards/newspack/extendingWizard_Section. - Set
$wizard_slugto match the parent wizard, implementregister_rest_routes(). include_oncethe file inincludes/class-newspack.php(order matters).- Run
composer dump-autoload. - Create a React component in
src/wizards/(use modern pattern: function component,.tsx,useWizardApiFetch). - Add a
React.lazy()import insrc/wizards/index.tsxmapped to the?page=param.
- Create a directory in
src/blocks/<block-name>/withblock.json,edit.js,index.js. - Export
{ metadata, name, settings }fromindex.js. - Register the block in
src/blocks/index.js(add feature flag condition if needed viawindow.newspack_blocks). - If the block needs a PHP render callback, register it in
includes/class-blocks.php. - If the block needs a frontend script, add a
view.jsand a hardcoded webpack entry inwebpack.config.js.
- Create
includes/plugins/class-{plugin-name}.phpusing the rootNewspacknamespace. - Follow the static
init()pattern (see Class Initialization Patterns above). include_oncethe file inincludes/class-newspack.php.- Run
composer dump-autoload. - Optionally add a Configuration Manager in
includes/configuration_managers/for setup UI.