Skip to content

Content Guidelines: Add experimental REST API and custom post type#75164

Merged
ramonjd merged 40 commits intoWordPress:trunkfrom
aagam-shah:ai-content-guidelines-mvp
Feb 27, 2026
Merged

Content Guidelines: Add experimental REST API and custom post type#75164
ramonjd merged 40 commits intoWordPress:trunkfrom
aagam-shah:ai-content-guidelines-mvp

Conversation

@aagam-shah
Copy link
Copy Markdown
Contributor

@aagam-shah aagam-shah commented Feb 3, 2026

What?

Add the backend (REST API + Custom Post Type) for the experimental Content Guidelines feature. This introduces a persistent, machine-readable content guidelines system that allows WordPress site owners to define and manage site-wide content rules — stored as a custom post type with individual post meta fields and accessible via REST API.

This PR is backend-only. The UI/frontend will be handled in a separate PR. The feature is behind the gutenberg-content-guidelines experimental flag and must be enabled via Experiments settings before use.

Why?

WordPress currently has no standardized way for site owners to encode their content rules, brand guidelines, or AI preferences. As AI features become more deeply integrated into WordPress, there needs to be a single source of truth that AI tools can reference to produce content aligned with a site's standards.

Think of it as: theme.json explains how a website should look; content-guidelines explains how a website should behave and how content should be created.

How?

Ships as a Gutenberg experiment gated behind the gutenberg-content-guidelines experiment flag. The REST API uses the standard wp/v2 namespace — the experiment flag controls whether the endpoint is registered.

Architecture

See #75258 for the full technical architecture discussion. The implementation follows the Global Styles pattern — extending WP_REST_Posts_Controller and WP_REST_Revisions_Controller to inherit standard WordPress CRUD behavior, permission checks, and response formatting.

Files — lib/experimental/content-guidelines/

File Purpose
index.php Bootstrap — requires classes, registers CPT and post meta hooks
class-gutenberg-content-guidelines-post-type.php CPT registration, post meta registration, shared constants and helpers
class-gutenberg-content-guidelines-rest-controller.php REST controller extending WP_REST_Posts_Controller — singleton CRUD, query filters, sanitization
class-gutenberg-content-guidelines-revisions-controller.php Revisions controller extending WP_REST_Revisions_Controller — list/get revisions with guideline data, restore endpoint

Tests — phpunit/experimental/content-guidelines/

File Purpose
class-gutenberg-content-guidelines-rest-controller-test.php 30 tests covering CRUD, singleton enforcement, filtering, permissions, revisions, block name validation, and schema

REST API Endpoints

Method Endpoint Description
GET /wp/v2/content-guidelines Get singleton guidelines (supports ?category, ?block, ?status filters)
POST /wp/v2/content-guidelines Create guidelines (singleton enforced)
GET /wp/v2/content-guidelines/{id} Get by ID
PATCH /wp/v2/content-guidelines/{id} Update guidelines
DELETE /wp/v2/content-guidelines/{id} Delete guidelines
GET /wp/v2/content-guidelines/{id}/revisions List revisions
GET /wp/v2/content-guidelines/{id}/revisions/{rev_id} Get single revision
POST /wp/v2/content-guidelines/{id}/revisions/{rev_id}/restore Restore a revision

Guideline Categories

Category Purpose
copy Tone, voice, brand personality, vocabulary preferences
images Preferred styles, colors, moods, subjects
site Site goals, target audience, industry context
additional Catch-all for edge cases
blocks Per-block-type rules (e.g., core/paragraph, core/heading) — auto-discovered from blocks with content role attributes

Key Design Decisions

  • Singleton pattern: One guidelines post per site (like Global Styles)
  • Individual post meta: Each category stored as separate _content_guideline_{category} meta key with revisions_enabled, instead of a JSON blob — enables native WordPress revision support
  • Block meta auto-registration: Block-specific guideline meta keys registered at rest_api_init by scanning the block registry for blocks with content role attributes
  • Permission model: manage_options for write operations, edit_posts for read
  • Sanitization: sanitize_textarea_field() for plain text, mb_strlen/mb_substr for maxLength enforcement (5000 chars for guidelines, 200 for labels)

Testing Instructions

Setup and curl test commands

Setup

# Start wp-env
npx wp-env start

# Enable the Content Guidelines experiment
npx wp-env run cli -- wp option update gutenberg-experiments '{"gutenberg-content-guidelines":1}' --format=json

# Create an application password for API testing (outputs password without spaces)
APP_PASS=$(npx wp-env run cli -- wp user application-password create admin test-cli --porcelain 2>/dev/null | tail -1)

# Set auth and base URL variables
AUTH="admin:${APP_PASS}"
BASE="http://localhost:8888/index.php?rest_route=/wp/v2/content-guidelines"

Test Flows

Create guidelines:

RESULT=$(curl -s -u "$AUTH" -X POST "$BASE" -H "Content-Type: application/json" \
  -d '{"status":"publish","guideline_categories":{"copy":{"guidelines":"Use active voice. Keep sentences short."},"images":{"guidelines":"Prefer high-contrast photography."},"site":{"guidelines":"Target audience: developers."}}}')
echo "$RESULT"

# Extract the post ID for use in subsequent requests
ID=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")

Get singleton:

curl -s -u "$AUTH" "$BASE"

Update:

curl -s -u "$AUTH" -X PATCH "$BASE/$ID" -H "Content-Type: application/json" \
  -d '{"guideline_categories":{"copy":{"guidelines":"Updated copy guidelines."}}}'

Filter by category:

curl -s -u "$AUTH" "$BASE&category=copy"

Filter by block:

curl -s -u "$AUTH" "$BASE&block=core/paragraph"

Add block-specific guideline:

curl -s -u "$AUTH" -X PATCH "$BASE/$ID" -H "Content-Type: application/json" \
  -d '{"guideline_categories":{"blocks":{"core/paragraph":{"guidelines":"Keep paragraphs under 3 sentences."}}}}'

List revisions:

REVISIONS=$(curl -s -u "$AUTH" "$BASE/$ID/revisions")
echo "$REVISIONS"

# Extract the first revision ID for use in restore
REV_ID=$(echo "$REVISIONS" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")

Restore a revision:

curl -s -u "$AUTH" -X POST "$BASE/$ID/revisions/$REV_ID/restore"

Use _fields for partial response:

curl -s -u "$AUTH" "$BASE/$ID&_fields=id,status"

Delete:

curl -s -u "$AUTH" -X DELETE "$BASE/$ID"

Singleton enforcement (should return 400):

# First re-create so we have a published post
curl -s -u "$AUTH" -X POST "$BASE" -H "Content-Type: application/json" \
  -d '{"status":"publish","guideline_categories":{"copy":{"guidelines":"First post."}}}'

# Second create should fail
curl -s -u "$AUTH" -X POST "$BASE" -H "Content-Type: application/json" \
  -d '{"guideline_categories":{"copy":{"guidelines":"second post"}}}'
# Expected: {"code":"rest_guidelines_exists","message":"Content guidelines already exist. Use PATCH to update.","data":{"status":400}}
aagam-shah and others added 17 commits January 27, 2026 10:12
Introduces a new experimental feature that allows site owners to define
editorial guidelines for AI-generated content. The feature includes:

- Custom post type (wp_guidelines) for storing guidelines data
- REST API endpoints under __experimental/content-guidelines
- Admin settings page at Settings > Content Guidelines
- Five guideline categories: Copy, Images, Site, Blocks, Other
- Block-specific guidelines with dropdown selector for all block types
- Draft/Published workflow with revision history
- Redux store for state management

The feature is gated behind the 'gutenberg-content-guidelines' experiment flag.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract get_validated_post() helper in PHP REST controller to reduce duplication
- Add revision state management to store (actions, selectors, reducer)
- Refactor revision-list.tsx to use store instead of direct API calls
- Simplify handleImport function in admin page
- Add import/export controls component
- Fix accessibility issues and lint warnings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Implement responsive two-column layout with guidelines on left and sidebar on right
- Add pagination support for revision history (5 per page) with Previous/Next controls
- Update REST API to support page and per_page parameters with X-WP-Total headers
- Open Site Context panel by default as the first category
- Combine Export/Import controls into a single section with side-by-side buttons
- Remove max-width constraint for better responsive behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Convert revision list from flexbox to table for proper column alignment
- Fix revision numbering to account for pagination
- Keep action column empty for current revision instead of showing dash

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change the Save button to Update when guidelines are published to match
the standard WordPress admin pattern for published content. Also update
the Published status text to use neutral styling instead of link-like
green color.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add robust JSON encoding/decoding helpers following the WordPress Global
Styles pattern. This fixes an issue where content containing quotes and
special characters would corrupt the stored JSON data.

Changes:
- Add JSON encoding flags (JSON_HEX_TAG, JSON_HEX_AMP) for XSS prevention
- Add encode_json_for_storage() helper with wp_slash() for DB compatibility
- Add decode_json_from_storage() helper with json_last_error() checking
- Update all JSON operations to use consistent helper methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract helper functions to eliminate code duplication:
- normalizeGuidelines() and deepClone() in reducer.ts
- extractBlockTitle() and truncateText() in block-guidelines-panel.tsx

Improve code readability:
- Replace nested ternaries with if/else blocks in index.tsx
- Use early returns for null checks in reducer cases
- Extract type casts into local variables

Remove redundant selectors (getRevisionCurrentPage, getRevisionTotalPages)
that duplicated data already available via getRevisionPagination.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce a new selector, getGuidelinesForBlocks, to retrieve guidelines for specified block types from the store state. This enhances the ability to access guidelines based on block names, improving the overall functionality and usability of the content guidelines feature. Additionally, create aliases for naming consistency in existing selectors.
Refactor the REST API endpoints in the content guidelines feature to transition from the '__experimental' namespace to 'wp/v2'. This change ensures compatibility with the current WordPress.com environment and prepares the feature for broader usage. The updates include modifications in the REST controller and various action functions to reflect the new path structure.
- Remove deprecated BlockGuidelinesPanel and CategoryPanel components, replacing them with BlockGuidelinesRow and CategoryRow for improved structure and clarity.
- Update styles in style.scss for better layout and consistency across components.
- Enhance import/export controls for clearer user interaction and improved accessibility.
- Adjust revision list layout to ensure proper alignment and readability.

This refactor aims to streamline the content guidelines management interface, making it more user-friendly and visually coherent.
- Use form-table pattern for category rows (matches Settings pages)
- Use wp-list-table for revision history table
- Use native button styling for pagination
- Use large-text class for textareas
- Use description class for help text
- Use wp-heading-inline for page title
- Update page description text
- Fix table collapse during pagination loading
- Restructure Import/Export section layout
…kens

- Redesign configured blocks with Edit | Remove actions inline with title
- Add striped rows and hover effect for better readability
- Reset block selection after save completes using isSaving prop
- Migrate all custom colors to WordPress design tokens from @wordpress/base-styles/colors
- Use CSS custom properties for theme-aware link colors
Add `can_export => true` to the CPT registration args, allowing
content guidelines to be included in WordPress native export
(Tools → Export) using the WXR format.
- Add "Add Block Guidelines" button that reveals dropdown and textarea
- Use local draft state so block only appears in list after clicking Add
- Move description above the add button
- Remove striped row styling from configured blocks list
- Remove redundant block title label above textarea
# Conflicts:
#	lib/experiments-page.php
#	lib/load.php
- Align REST namespace to wp/v2 in post type registration
- Remove dead wp_localize_script block from admin page
- Fix thunk dispatch cast in restoreRevision action
- Replace native HTML elements with @wordpress/components
- Remove unused publish-controls.tsx component
- Remove unused @wordpress/icons dependency
- Add types field and exports to package.json
- Use structuredClone instead of JSON.parse/stringify
- Remove unused selector aliases
- Add sanitize_callback to REST API args
- Add no_found_rows to WP_Query calls
- Use grid-unit variables and breakpoint mixins in SCSS
- Add generic type params to createReduxStore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 3, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: aagam-shah <aagam94@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: talldan <talldanwp@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Feb 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 3, 2026

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @aagam-shah! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

Rename class files to follow WordPress naming convention
(class-{class-name}.php) and suppress unused parameter warnings
in REST permission callbacks.
…dation

- Introduced constants for maximum lengths of guideline text and category labels.
- Added a new method to sanitize guideline categories, ensuring only valid categories are processed.
- Implemented specific sanitization for standard and blocks categories.
- Updated REST API schema to include validation and sanitization callbacks for guideline categories.
Add client-side maxLength={5000} to all guideline TextareaControl
components to match the server-side schema validation limits.
Updated the sanitize_standard_category method to simplify the sanitization process by removing the unnecessary key reference in the foreach loop. This change enhances code readability and maintains the integrity of the sanitization logic.
@andrewserong
Copy link
Copy Markdown
Contributor

andrewserong commented Feb 16, 2026

As I mentioned on the other issue thanks again for the back and forth on this one and for downscoping to just the backend changes for now 👍

I might not be able to give this a thorough review for a little while as I'm focusing on WP 7.0 tasks early this week, but it might be worth starting to add some PHP tests for the endpoint, as that could help cover the flows you've got in the PR testing instructions.

As a quick example, the updates to the REST attachments controller tests for the client-side media processing experiment could be a good point of reference for adding tests: https://github.com/WordPress/gutenberg/blob/87d31ac853d4ee2f8f42b70703967bedc5a54846/phpunit/experimental/media/class-gutenberg-rest-attachments-controller-test.php

When adding tests, you can add your gutenberg experiment to this array so that the tests environment opts-in to the gutenberg experiment:

$GLOBALS['wp_tests_options'] = array(
'gutenberg-experiments' => array(
'gutenberg-widget-experiments' => '1',
'gutenberg-full-site-editing' => 1,
'gutenberg-form-blocks' => 1,
'gutenberg-block-experiments' => 1,
'gutenberg-media-processing' => 1,
'gutenberg-svg-icon-registry' => 1,
),
);

Hope that helps!

Copy link
Copy Markdown
Member

@ramonjd ramonjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work here @aagam-shah

This is generally testing well according to the description. I just left some comments to get things rolling.

I think in general some unit tests would help, not only to verify the assumptions, but also to help folks reason about the changes.

The experiment flag itself gates whether the endpoint is registered,
so __experimental in the REST path is unnecessary. WordPress defaults
to wp/v2 when show_in_rest is true and no namespace is specified.
- Replace die('Silence is golden.') with exit in all files
- Remove unnecessary phpcs:ignore on permission check callback
- Use core's publish status instead of custom published synonym
- Reuse parent controller's prepare_item_for_response in restore_revision
  via get_rest_controller() for consistent _links and field filtering
- Fix block name regex to match WP_Block_Type_Registry::register()
@aagam-shah aagam-shah force-pushed the ai-content-guidelines-mvp branch from aa5c3a4 to 1238a38 Compare February 16, 2026 08:25
…nes-mvp

# Conflicts:
#	lib/experiments-page.php
#	lib/load.php
#	phpunit/bootstrap.php
@andrewserong andrewserong added the [Feature] Guidelines An experimental feature for adding site-wide editorial rules. label Feb 26, 2026
Copy link
Copy Markdown
Contributor

@andrewserong andrewserong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is testing great for me, thanks again for all the back and forth on the architecture and design for this, @aagam-shah!

I think this is a really good starting point. The PR just needs a rebase, but otherwise I think should be good to go:

✅ This structure stores all content guidelines as plain text, so we don't need to worry about any JSON parsing for this round of work
✅ Adding, getting, updating, deleting, and restoring all appear to work correctly via the API
✅ The API is appropriately guarded behind the Gutenberg experiment

Just left a couple of very minor formatting nits, but nothing major.

In case it helps anyone else test this out, I had Claude put together a JS snippet to run the API from within a browser dev console, which helped me a bit while testing to introspect requests / see what was returned from the API.

Claude-generated JS test script — to use, open up the post editor after enabling the Gutenberg experiment and copy + paste into the dev console
const apiFetch = wp.apiFetch;
  const BASE = '/wp/v2/content-guidelines';

  async function testContentGuidelines() {
        let id;

        // 1. GET when empty
        console.group('1. GET empty');
        const empty = await apiFetch({ path: BASE });
        console.log('id:', empty.id, '(expect 0)');
        console.log('status:', empty.status, '(expect draft)');
        console.groupEnd();

        // 2. CREATE guidelines
        console.group('2. CREATE');
        const created = await apiFetch({
                path: BASE,
                method: 'POST',
                data: {
                        status: 'publish',
                        guideline_categories: {
                                copy: { guidelines: 'Use active voice. Keep sentences short.' },
                                images: { guidelines: 'Prefer high-contrast photography.' },
                                site: { guidelines: 'Target audience: developers.' }
                        }
                }
        });
        id = created.id;
        console.log('id:', id);
        console.log('status:', created.status);
        console.log('categories:', created.guideline_categories);
        console.groupEnd();

        // 3. Singleton enforcement (should throw 400)
        console.group('3. SINGLETON enforcement');
        try {
                await apiFetch({
                        path: BASE,
                        method: 'POST',
                        data: {
                                status: 'draft',
                                guideline_categories: {
                                        copy: { guidelines: 'Second post.' }
                                }
                        }
                });
                console.error('FAIL: should have thrown');
        } catch (e) {
                console.log('code:', e.code, '(expect rest_guidelines_exists)');
        }
        console.groupEnd();

        // 4. GET singleton
        console.group('4. GET singleton');
        const fetched = await apiFetch({ path: BASE });
        console.log('id:', fetched.id, '(expect ' + id + ')');
        console.log('copy:', fetched.guideline_categories.copy.guidelines);
        console.groupEnd();

        // 5. GET by ID
        console.group('5. GET by ID');
        const byId = await apiFetch({ path: BASE + '/' + id });
        console.log('id:', byId.id);
        console.log('status:', byId.status);
        console.groupEnd();

        // 6. Category filter
        console.group('6. Category filter (?category=copy)');
        const filtered = await apiFetch({ path: BASE + '?category=copy' });
        console.log('has copy:', 'copy' in filtered.guideline_categories);
        console.log('has images:', 'images' in filtered.guideline_categories, '(expect false)');
        console.groupEnd();

        // 7. UPDATE
        console.group('7. UPDATE');
        const updated = await apiFetch({
                path: BASE + '/' + id,
                method: 'PATCH',
                data: {
                        guideline_categories: {
                                copy: { guidelines: 'Updated: be concise and direct.' }
                        }
                }
        });
        console.log('copy:', updated.guideline_categories.copy.guidelines);
        console.groupEnd();

        // 8. Add block-specific guideline
        console.group('8. Block guideline');
        const withBlock = await apiFetch({
                path: BASE + '/' + id,
                method: 'PATCH',
                data: {
                        guideline_categories: {
                                blocks: {
                                        'core/paragraph': { guidelines: 'Keep paragraphs under 3 sentences.' },
                                        'core/heading': { guidelines: 'Use sentence case.' }
                                }
                        }
                }
        });
        console.log('blocks:', withBlock.guideline_categories.blocks);
        console.groupEnd();

        // 9. Block filter
        console.group('9. Block filter (?block=core/paragraph)');
        const blockFiltered = await apiFetch({ path: BASE + '?block=core/paragraph' });
        console.log('has core/paragraph:', 'core/paragraph' in (blockFiltered.guideline_categories.blocks || {}));
        console.log('has core/heading:', 'core/heading' in (blockFiltered.guideline_categories.blocks || {}), '(expect false)');
        console.log('has copy:', 'copy' in blockFiltered.guideline_categories, '(expect false)');
        console.groupEnd();

        // 10. _fields partial response
        console.group('10. _fields=id,status');
        const partial = await apiFetch({ path: BASE + '/' + id + '?_fields=id,status' });
        console.log('has id:', 'id' in partial);
        console.log('has status:', 'status' in partial);
        console.log('has guideline_categories:', 'guideline_categories' in partial, '(expect false)');
        console.groupEnd();

        // 11. Revisions
        console.group('11. Revisions');
        const revisions = await apiFetch({ path: BASE + '/' + id + '/revisions' });
        console.log('revision count:', revisions.length);
        if (revisions.length > 0) {
                console.log('latest has guideline_categories:', 'guideline_categories' in revisions[0]);
        }
        console.groupEnd();

        // 12. Restore revision
        if (revisions.length > 1) {
                console.group('12. Restore oldest revision');
                var oldest = revisions[revisions.length - 1];
                const restored = await apiFetch({
                        path: BASE + '/' + id + '/revisions/' + oldest.id + '/restore',
                        method: 'POST'
                });
                console.log('restored id:', restored.id, '(expect ' + id + ')');
                console.log('has guideline_categories:', 'guideline_categories' in restored);
                console.groupEnd();
        }

        // 13. DELETE
        console.group('13. DELETE');
        const deleted = await apiFetch({
                path: BASE + '/' + id + '?force=true',
                method: 'DELETE'
        });
        console.log('deleted:', deleted);
        const afterDelete = await apiFetch({ path: BASE });
        console.log('after delete id:', afterDelete.id, '(expect 0)');
        console.groupEnd();

        console.log('=== Done ===');
  }

  testContentGuidelines().catch(function(e) { console.error('Test failed:', e); });

Once this has been rebased, this LGTM 🚀

I'll be AFK tomorrow so won't be able to help merge until next week, but if this is still open on Monday, happy to help out then.

Nice work!

}

if ( rest_is_field_included( 'date', $fields ) ) {
$data['date'] = mysql_to_rfc3339( $post->post_date );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit for each of these calls to mysql_to_rfc3339: we could call $this->prepare_date_response() to handle conversion and the case for when the date should be treated as null (e.g. drafts).

Not a big deal, of course!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be good to use WP_REST_Posts_Controller::prepare_date_response, in fact any of the inherited methods if they make sense in this context. That way we'll get any upgrades/changes without having to come back and update here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — replaced all four mysql_to_rfc3339() calls with $this->prepare_date_response(). Thanks for the catch!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — switched to $this->prepare_date_response() for all date fields. Agreed on reusing inherited methods where possible.

Comment on lines +300 to +314
/**
* Test that editors can read guidelines (edit_posts capability).
*/
public function test_get_items_editor_can_read() {
wp_set_current_user( self::$admin_id );
$this->create_guidelines( array( 'status' => 'publish' ) );

wp_set_current_user( self::$editor_id );

$request = new WP_REST_Request( 'GET', self::REST_BASE );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add a test that unauthenticated users (or subscriber users) cannot read the guidelines?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_get_items_unauthenticated to cover this. I had skipped it earlier since we already had permission tests for editor-role users across create, update, delete, and restore — but makes sense to explicitly cover the unauthenticated read case too.


// -------------------------------------------------------------------------
// Route registration
// -------------------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit: typically in tests there'll be @covers ::register_routes in the doc block so we likely don't need this kind of visual flag in the test. E.g see the tests for the attachments controller:

Or for creating an item:

/**
* Verifies that skipping sub-size generation works.
*
* @covers ::create_item
* @covers ::create_item_permissions_check
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — removed the visual separators and added @covers annotations to all test methods.

@andrewserong andrewserong added the No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core label Feb 26, 2026
@ramonjd
Copy link
Copy Markdown
Member

ramonjd commented Feb 26, 2026

I'll be AFK tomorrow so won't be able to help merge until next week, but if this is still open on Monday, happy to help out then.

I can help merge tomorrow if it's required. Thanks, folks!!!

Replace direct mysql_to_rfc3339() calls with the inherited
prepare_date_response() method, which handles null dates for drafts
and stays in sync with core improvements.
Verify that unauthenticated users receive a 401 when attempting
to read the content guidelines endpoint.
@aagam-shah
Copy link
Copy Markdown
Contributor Author

Thanks @andrewserong and @ramonjd for the thorough review and all the back and forth on this! As a first-time contributor to Gutenberg, I really appreciate the detailed feedback and guidance — it's been a great learning experience. All the nits and suggestions have been addressed in the latest commits. Let me know if anything else needs attention!

Copy link
Copy Markdown
Member

@ramonjd ramonjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the continued efforts here 🙇🏻

I left some non-blocking comments.

Tested with experiment on and off and works as expected. With the experiment off, the REST route returns:

{"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}

I think it's a good foundation for the further work planned. Cheers!

$blocks = array();
foreach ( $all_meta as $meta_key => $meta_values ) {
if ( self::is_block_meta_key( $meta_key ) ) {
$block_name = self::meta_key_to_block_name( $meta_key );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also idea for follow up.

I was wondering if it'd help to also filter by get_content_blocks here as well. Not sure.

I'm thinking to get blocks that currently exist in the environment, and ignore stale meta entires from blocks no longer available due to a theme switch or plugin deactivation.

}

return false;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea for a follow up: check if you need to test whether block supports contentRole, e..g, ! empty( $block_type->supports['contentRole']

See isContentBlock() and the relevant docs.

It might not matter so much now, but later it it might avoid bugs if the definition of “content blocks” is the same in the backend and in the editor.

@ramonjd ramonjd merged commit 256e03f into WordPress:trunk Feb 27, 2026
44 of 49 checks passed
@github-actions github-actions bot added this to the Gutenberg 22.7 milestone Feb 27, 2026
@aagam-shah aagam-shah deleted the ai-content-guidelines-mvp branch March 9, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Guidelines An experimental feature for adding site-wide editorial rules. First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Type] Experimental Experimental feature or API.

4 participants