Custom functionality for the Jason Chafin WordPress site. It holds the parts of the site that don't depend on the theme, such as content types, fields, analytics and security headers, so that changing the theme doesn't change what the site does.
- Advanced Custom Fields Pro, declared with
Requires Plugins. WordPress won't activate this plugin without it.
Download jc-core-functionality.zip from the latest release and upload it under Plugins → Add New → Upload Plugin.
Use the release zip, not GitHub's Code → Download ZIP. The source archive has no vendor/ directory, so a copy installed from it can't update itself.
Once it's installed, updates come from this repository's GitHub releases through plugin-update-checker. They show up on the Plugins screen like any other plugin update, and through wp plugin update jc-core-functionality. Release candidates (-rc tags) are published as prereleases and are never offered as updates.
Defined as ACF JSON in acf-json/, which the plugin registers as ACF's load and save path:
- People (
person), with a Contact Info field group: name, title, phone, email, website, address, bio and photo. - Quotes (
quote). - Posts get a
subtitlefield. It's also registered as post meta and exposed in the REST API.
[quotes]shows one random quote title from the Quotes post type.
| Source | Arguments | Value |
|---|---|---|
jc/copyright |
none | © <current year> in the site's timezone |
jc/user-data |
key (name, description or avatar), userId |
The user's display name, bio, or avatar URL |
Example:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"jc/copyright"}}}} -->
<p>Copyright Block</p>
<!-- /wp:paragraph -->-
Google Tag Manager: container snippets in
<head>and right after<body>. They're not printed for administrators (manage_options), so your own visits stay out of analytics. The container defaults to the live site's. Change it or turn it off inwp-config.php:define( 'JC_GTM_ID', 'GTM-XXXXXXX' ); // a different container define( 'JC_GTM_ID', '' ); // no GTM, e.g. on staging
The
jc_gtm_container_idfilter does the same. Anything that isn't a validGTM-…ID turns GTM off. -
Security headers on front-end responses:
Referrer-Policy,X-Content-Type-Options,X-Frame-Options,Permissions-Policyand a Content Security Policy (see below). -
XML-RPC disabled: all methods are removed and the RSD link is taken out of
<head>, to block brute-force login attempts through/xmlrpc.php.
Scripts are trusted by a nonce that changes on every request, not by a host list. WordPress adds the nonce to every script printed through its script API (enqueued scripts, inline scripts, the import map and speculation rules). 'strict-dynamic' extends that trust to the scripts they load, such as GTM and GA4. Styles still allow 'unsafe-inline', because block styles are printed inline.
The policy currently ships as Content-Security-Policy-Report-Only. Browsers report what it would block without blocking it. In the meantime a minimal policy is enforced (object-src 'none'; base-uri 'self'; frame-ancestors 'self').
Reports are sent to /wp-json/jc/v1/csp-report and written to the PHP error log, one JSON line each, prefixed [jc-csp]:
grep '\[jc-csp\]' /path/to/php-error.logIf WP_DEBUG_LOG is enabled, WordPress redirects the error log to wp-content/debug.log (or the path the constant names), so look there instead.
Chrome sends reports through the Reporting API (report-to), which only delivers to https:// endpoints and batches reports, so they can arrive a minute or more after the violation. Other browsers use report-uri and send immediately.
When the log shows nothing legitimate being blocked, enforce the policy:
add_filter( 'jc_csp_report_only', '__return_false' );To allow another source, filter the directives:
add_filter( 'jc_csp_directives', function ( $directives ) {
$directives['frame-src'][] = 'https://www.youtube-nocookie.com';
return $directives;
} );Any script printed as a raw <script> tag, rather than through wp_enqueue_script(), wp_add_inline_script() or wp_print_inline_script_tag(), gets no nonce and will be blocked once the policy is enforced.
Deleting the plugin removes only the update checker's stored data. People and Quotes content is never deleted. The post types simply stop being registered until something registers them again.
nvm use # Node 24, from .nvmrc
composer install && npm install # npm install also sets up the pre-commit hook
npm test # node:test (tests/*.test.js), then PHPUnit (tests/php/)
composer lint # PHPCS, WordPress Coding Standards (phpcs.xml.dist)
composer lint-fix # auto-fix what PHPCS can
npm run format # wp-scripts format for JS, JSON, YAML and Markdown
npm run release # bump version, update CHANGELOG.md, tagA pre-commit hook (husky + lint-staged) runs PHPCS on staged PHP and wp-scripts format on staged JS, JSON, YAML and Markdown. CI runs on every pull request: PHP 8.0 syntax and coding standards, the test suites on PHP 8.3 (PHPUnit 12's minimum), and a formatting check. Pushing a vX.Y.Z tag builds the release zip and publishes it as a GitHub release. See ROADMAP.md for planned work.