<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: WPVibes</title>
    <description>The latest articles on DEV Community by WPVibes (@wpvibes).</description>
    <link>https://dev.to/wpvibes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3978746%2F4a5ecd1b-f11b-4194-b1fb-1a490fef4667.jpg</url>
      <title>DEV Community: WPVibes</title>
      <link>https://dev.to/wpvibes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wpvibes"/>
    <language>en</language>
    <item>
      <title>How to Save Contact Form 7 Submissions to Google Sheets</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 20 Aug 2026 12:36:12 +0000</pubDate>
      <link>https://dev.to/wpvibes/how-to-save-contact-form-7-submissions-to-google-sheets-37be</link>
      <guid>https://dev.to/wpvibes/how-to-save-contact-form-7-submissions-to-google-sheets-37be</guid>
      <description>&lt;p&gt;Every Contact Form 7 submission lives in exactly one place: your inbox. If that email lands in spam, or your SMTP setup breaks for a day, that submission is gone. There's no second copy anywhere, because Contact Form 7 was never built to store data at all.&lt;/p&gt;

&lt;p&gt;This guide fixes both problems at once. Form Vibes Pro connects Contact Form 7 to Google Sheets automatically, and saves every submission to your WordPress database at the same time. No automation platform, no per-task limits, and no more relying on a single email that might not arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Contact Form 7 Save Submissions Anywhere on Its Own?
&lt;/h2&gt;

&lt;p&gt;No. Contact Form 7 sends every submission by email and nothing else. There's no entries table in your WordPress dashboard, no export button, and no built-in backup if that email fails to arrive.&lt;/p&gt;

&lt;p&gt;This is intentional. Contact Form 7 was designed to be lightweight, and email-only delivery is part of that design. But it means the only record of a submission is whatever lands in an inbox. A spam filter, a broken SMTP connection, or someone accidentally deleting an email can permanently erase a lead with no way to recover it.&lt;/p&gt;

&lt;p&gt;This is exactly the gap Form Vibes closes. Every submission gets saved to your WordPress database the moment it comes in, and from there it can also sync straight to a Google Sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Google Sheets Sync a Free or Pro Feature?
&lt;/h2&gt;

&lt;p&gt;Google Sheets sync is a Form Vibes Pro feature. Here's what's relevant to this guide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;th&gt;Pro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Save Contact Form 7 entries to WordPress database&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export to CSV&lt;/td&gt;
&lt;td&gt;Yes (up to 1,000 rows)&lt;/td&gt;
&lt;td&gt;Yes (unlimited)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync to Google Sheets&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter which submissions get synced&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to Connect Contact Form 7 to Google Sheets
&lt;/h2&gt;

&lt;p&gt;To keep this concrete, we'll set this up for a simple RSVP form with four fields: your-name, your-email, your-event-date, and your-guest-count. Your own form's field names will be different, but the steps are the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Your Google API Credentials
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.formvibes.com/article/152-how-to-connect-with-google-sheet" rel="noopener noreferrer"&gt;Google requires API credentials&lt;/a&gt;, so Form Vibes can communicate securely with your Google account. These consist of a Client ID and a Client Secret, generated in Google Cloud Console. The full credential creation process is covered in a dedicated guide. Follow that guide to generate your Client ID and Client Secret, then return here to continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect Form Vibes to Google Sheets
&lt;/h3&gt;

&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Form Vibes &amp;gt; Settings &amp;gt; Google Sheet&lt;/strong&gt;&amp;nbsp;in your WordPress admin. Paste your Client ID and Client Secret into their respective fields. Click&amp;nbsp;&lt;strong&gt;Authenticate&lt;/strong&gt;&amp;nbsp;and sign in with your Google account when prompted. After approving the requested permissions, the connection status updates to show the account as connected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgyfcgdhwvyfa8q9uyzqm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgyfcgdhwvyfa8q9uyzqm.webp" alt="Connect Form Vibes to Google Sheets" width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
During authentication, Google shows an "App is not verified" warning. This is expected for a self-configured OAuth connection, and Google's own documentation confirms this is normal for unverified apps. Click Show Advanced, then Go to App, and approve all requested permissions. The connection completes normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up the Google Sheets Connection for Your CF7 Form
&lt;/h3&gt;

&lt;p&gt;With your Google account connected, go to&amp;nbsp;&lt;strong&gt;Form Vibes &amp;gt; Export Profiles&lt;/strong&gt;&amp;nbsp;and click&amp;nbsp;&lt;strong&gt;Add New&lt;/strong&gt;. Under&amp;nbsp;&lt;strong&gt;Type&lt;/strong&gt;, select&amp;nbsp;&lt;strong&gt;Google Sheet&lt;/strong&gt;&amp;nbsp;instead of Local Download. This tells Form Vibes to push submissions to a spreadsheet rather than generate a downloadable file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9cscip5swlt6hfheqih.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9cscip5swlt6hfheqih.webp" alt="Export Profile Type selector showing Local Download and Google Sheet" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose the Form, Spreadsheet, and Worksheet
&lt;/h3&gt;

&lt;p&gt;On the&amp;nbsp;&lt;strong&gt;Data Source&lt;/strong&gt;&amp;nbsp;tab, select your RSVP form from the Form dropdown. Since Contact Form 7 doesn't have a visual field builder, this dropdown lists the form's admin title, such as "Event RSVP Form," not the individual field tags. Forms appear here once they've received at least one submission.&lt;/p&gt;

&lt;p&gt;Choose the Google Spreadsheet where data should be sent. If the spreadsheet you need does not appear, check that the connected Google account has access to it. To create a new spreadsheet directly from Form Vibes, click the plus icon next to the spreadsheet selector.&lt;/p&gt;

&lt;p&gt;Select the specific worksheet, or tab, within the spreadsheet. Create a new one using the plus icon if needed.&lt;/p&gt;

&lt;p&gt;Then, you will be asked to map the columns to your Google Sheet. Click the &lt;strong&gt;"Click here"&lt;/strong&gt; option to start mapping the columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmfkztnrtgxsmlflyzhja.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmfkztnrtgxsmlflyzhja.webp" alt="Configure Data Source Tab Export Profile" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to use custom column headers in the sheet, you can enter the desired names here and map them to the corresponding form fields. For example, you can set the column header &lt;strong&gt;Name&lt;/strong&gt; and map it to the &lt;strong&gt;your-name&lt;/strong&gt; form field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff87g7zxnxzh0p57yurew.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff87g7zxnxzh0p57yurew.webp" alt="Map the Form Fields to Google Sheet" width="800" height="1078"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Data Filter section lets you sync only the submissions that match specific conditions, instead of sending every submission to Google Sheets. This step is optional. Skip it if you want every RSVP synced.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzexsvfudriihl1csc2m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzexsvfudriihl1csc2m.webp" alt="Apply Filter Conditions to Export Profile" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Map Your Fields on the Columns Mapping Tab
&lt;/h3&gt;

&lt;p&gt;Switch to the &lt;strong&gt;Columns Mapping&lt;/strong&gt; tab. If you want to change the default column mapping or assign a specific form field to a particular spreadsheet column, you can do it here.&lt;/p&gt;

&lt;p&gt;After that, map each spreadsheet column to the appropriate Contact Form 7 field. Click&amp;nbsp;&lt;strong&gt;Publish&lt;/strong&gt;&amp;nbsp;to save and activate the connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrgroa1kbnmpen9d0h1h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrgroa1kbnmpen9d0h1h.webp" alt="Columns Mapping tab with RSVP form fields mapped to spreadsheet columns" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens After Someone Submits Your Contact Form 7 Form?
&lt;/h2&gt;

&lt;p&gt;Once the connection is published, no further action is needed. When a guest submits the RSVP form, Form Vibes saves their name, email, event date, and guest count to your WordPress database, and immediately pushes a new row with those same details to the connected Google Spreadsheet.&lt;/p&gt;

&lt;p&gt;The row appears within moments. Anyone with access to the spreadsheet can see the new RSVP without logging into WordPress. Where before this data only existed in an email you might never open, it now lives in two places at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Aren't Contact Form 7 Submissions Reaching Google Sheets?
&lt;/h2&gt;

&lt;p&gt;The most common cause with Contact Form 7 specifically is a field name mismatch, since CF7 tags are typed by hand rather than picked from a visual list. Beyond that, submissions can also fail to sync because a form isn't yet listed in the dropdown, the connected account can't access the spreadsheet, there's a redirect URI mismatch, or the Google Sheets and Drive APIs aren't enabled. Submit a test RSVP from the frontend first, then check the causes below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data is appearing in the wrong columns, or not at all.&lt;/strong&gt;&amp;nbsp;Open the Export Profile and compare each mapped field against the actual tag names in your Contact Form 7 form editor. A field written as&amp;nbsp;&lt;code&gt;your-eventdate&lt;/code&gt;&amp;nbsp;in the form but mapped as&amp;nbsp;&lt;code&gt;your-event-date&lt;/code&gt;&amp;nbsp;in Form Vibes will fail silently. Retype the mapping to match exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Contact Form 7 form is not showing in the Form dropdown.&lt;/strong&gt;&amp;nbsp;Confirm Contact Form 7 is active. Then confirm the form has received at least one submission on the frontend since Form Vibes was activated. Form Vibes only lists forms that have already received at least one submission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spreadsheet is not showing in the dropdown.&lt;/strong&gt;&amp;nbsp;Check that the Google account connected in Form Vibes Settings has access to the spreadsheet. If the spreadsheet is owned by a different Google account, share it with the connected account before trying again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect URI mismatch error.&lt;/strong&gt;&amp;nbsp;The redirect URI entered in your Google Cloud Console app settings must match exactly the one Form Vibes provides. Open your Google Cloud Console project, navigate to OAuth credentials, and verify the redirect URI matches the one shown in Form Vibes Settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Sheets and Drive APIs are not enabled.&lt;/strong&gt;&amp;nbsp;Both APIs must be enabled inside your Google Cloud Console project. If sync silently fails and the field mapping checks out, this is usually the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App verification warning during authentication.&lt;/strong&gt;&amp;nbsp;This is expected for self-configured OAuth apps and does not indicate a problem. Click Show Advanced, then Go to App, and approve all permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Form Vibes for Contact Form 7 and Google Sheets?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It starts you from zero, not from a smoother sync.&lt;/strong&gt;&amp;nbsp;Some plugins improve on a feature Contact Form 7 already has. Contact Form 7 has nothing to improve on here. It has no entries table and no storage of any kind, so Form Vibes is the entire foundation, not an upgrade to one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every submission is also saved to your WordPress database.&lt;/strong&gt;&amp;nbsp;So a temporary Google API hiccup, or a sync that hasn't run yet, never means the data is lost. It's already sitting in WordPress.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One plugin for nine form builders.&lt;/strong&gt;&amp;nbsp;If you use Contact Form 7 alongside Elementor Forms or WPForms, one Form Vibes install handles all of them. No second connector plugin, no second Google configuration. Setting up&amp;nbsp;&lt;a href="https://formvibes.com/tutorial/elementor-form-to-google-sheets/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;Elementor forms with Google Sheets&lt;/a&gt;&amp;nbsp;works exactly the same way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Notes, submission status, analytics, and a role manager&lt;/strong&gt;&amp;nbsp;included, none of which Contact Form 7 has on its own.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Contact Form 7 was built to be lightweight, and that means it stores nothing on its own. One missed email is the only thing standing between a submission and losing it for good. Form Vibes Pro closes that gap completely: every entry is saved to your WordPress database and synced to Google Sheets automatically, so a single inbox is never the only record you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;Does Contact Form 7 have a built-in Google Sheets integration?&lt;/p&gt;

&lt;p&gt;No. Contact Form 7 sends submissions by email only and has no built-in database, export option, or Google Sheets connection. A plugin like Form Vibes Pro is required to sync submissions to a spreadsheet.&lt;/p&gt;

&lt;p&gt;Do I need Form Vibes Pro to connect Contact Form 7 to Google Sheets?&lt;/p&gt;

&lt;p&gt;Yes. The Google Sheets integration is a Form Vibes Pro feature. The free version still saves every Contact Form 7 submission to your WordPress database and lets you export to CSV on demand. The automatic real-time push to Google Sheets is the Pro upgrade.&lt;/p&gt;

&lt;p&gt;What if my Contact Form 7 field names don't match my sheet's column headers?&lt;/p&gt;

&lt;p&gt;Nothing will sync correctly until they match. Since Contact Form 7 fields are typed manually rather than chosen from a visual list, open the Export Profile's Columns Mapping tab and compare each field tag against your actual form editor. Fields like your-event-date and your-eventdate look similar but won't map to each other automatically.&lt;/p&gt;

&lt;p&gt;Can I sync multiple Contact Form 7 forms to different Google Sheets?&lt;/p&gt;

&lt;p&gt;Yes. Set up one Export Profile per Contact Form 7 form, and point each one to its own spreadsheet or its own worksheet inside the same spreadsheet. There's no limit on how many forms you can connect.&lt;/p&gt;

&lt;p&gt;Can I filter which Contact Form 7 submissions get sent to Google Sheets?&lt;/p&gt;

&lt;p&gt;Yes. The Data Filter section supports Equal, Not Equal, Contain, and Not Contain conditions, and lets you choose whether any or all conditions must match. Filtered-out submissions still get saved to your WordPress database; the filter only affects what reaches Sheets.&lt;/p&gt;

&lt;p&gt;Does the same integration work with my other form plugins?&lt;/p&gt;

&lt;p&gt;Yes. The Google Sheets sync in Form Vibes Pro works with Elementor Forms, WPForms, Gravity Forms, Ninja Forms, WS Form, Bricks Builder forms, Beaver Builder forms, and Everest Forms, alongside Contact Form 7. One plugin, one Google connection, all your forms.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>cf7</category>
      <category>contactform7</category>
      <category>googlesheets</category>
    </item>
    <item>
      <title>What's New in WordPress 7.1: Tab Block, Responsive Design, Pseudo States, and much more</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:31:09 +0000</pubDate>
      <link>https://dev.to/wpvibes/whats-new-in-wordpress-71-tab-block-responsive-design-pseudo-states-and-much-more-4p3j</link>
      <guid>https://dev.to/wpvibes/whats-new-in-wordpress-71-tab-block-responsive-design-pseudo-states-and-much-more-4p3j</guid>
      <description>&lt;p&gt;WordPress 7.1 comes out on August 19, 2026. It's the second major WordPress release this year. Instead of one headline feature, this release focuses on improving the entire editing experience: responsive design controls, interactive state styling, better media handling, new blocks, editor usability improvements, and a few developer-focused APIs.&lt;/p&gt;

&lt;p&gt;Whether you're a site owner, content creator, theme developer, or plugin developer, WordPress 7.1 has something that makes building and managing your site faster and more flexible.&lt;/p&gt;

&lt;p&gt;This guide walks through every major feature coming in WordPress 7.1, what it does, and why it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Does WordPress 7.1 Change the Way You Design Pages?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1 lets you style how a block looks on different screen sizes and how it reacts to a mouse or keyboard, directly in the editor, without writing a line of CSS. This is the headline feature of the release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsive Styling Comes to the Block Editor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress 7.1 introduces responsive styling directly inside the editor, allowing you to customize how blocks look across different screen sizes without writing CSS.&lt;/p&gt;

&lt;p&gt;Depending on the block, you'll be able to configure different values for typography, spacing, layout, dimensions, and other design settings across various screen sizes. This gives much greater control over how your content appears on different devices while keeping the editing experience visual and intuitive.&lt;/p&gt;

&lt;p&gt;To use it, select a block, then click the view icon. Choose responsive styling, then pick the device you want to design for: desktop, tablet, or phone. You can make a heading larger on desktop and smaller on mobile, all from the same panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88mfir4tuqqu3kkyejk1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88mfir4tuqqu3kkyejk1.webp" alt="Responsive Styling in WordPress 7.1" width="800" height="764"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tips:&lt;/strong&gt; If you're exploring modern WordPress editing capabilities, check out our guide on&amp;nbsp;&lt;a rel="noopener noreferrer nofollow" href="https://wpvibes.com/blog/php-only-blocks-wordpress/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content"&gt;&lt;strong&gt;PHP-only blocks in WordPress 7.0&lt;/strong&gt;&lt;/a&gt;&amp;nbsp;to see how block development is becoming simpler for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Style Every State Without CSS
&lt;/h3&gt;

&lt;p&gt;Until now, changing how a button or link looks on hover meant writing custom CSS. WordPress 7.1 adds visual controls for these&amp;nbsp;&lt;a href="https://make.wordpress.org/core/2026/08/05/pseudo-and-custom-style-states-in-wordpress-7-1/" rel="noopener noreferrer"&gt;&lt;strong&gt;interactive pseudo states&lt;/strong&gt;&lt;/a&gt;, so you can style them directly in the editor.&lt;/p&gt;

&lt;p&gt;You can switch between states(Hover, Focus, Focus-Visible, and Active) and set a different color, border, background, shadow, or typography for each one. This makes it easier to build engaging interfaces while also improving accessibility.&lt;/p&gt;

&lt;p&gt;A new "State" option lets you set a different style for when someone hovers over a button, tabs into it with a keyboard, or clicks it. The preview updates live as you work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26k94c7fq32atrjpsyen.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26k94c7fq32atrjpsyen.webp" alt="WordPress Pseudo States Styling" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Hover and focus control is only available on the Button block and the Navigation Link block.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New With the Notes Feature in WordPress 7.1?
&lt;/h2&gt;

&lt;p&gt;Notes in WordPress 7.1 lets you leave feedback on a specific piece of text instead of an entire block, so a comment no longer needs a Slack message to explain which sentence it's about. The feature first appeared in WordPress 6.9, and this release makes it genuinely usable for day-to-day editorial work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46ipawzlwm4hgkk5w6kq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46ipawzlwm4hgkk5w6kq.webp" alt="Open Setting to Add a Note in WordPress" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now attach a note to a specific selection of text instead of an entire block, so feedback points at exactly the phrase you mean. Notes support rich formatting: bold (Ctrl/⌘ + B), italics (Ctrl/⌘ + I), links (Ctrl/⌘ + K), and emojis, so a note can actually explain itself instead of reading like a plain-text scrap.&lt;/p&gt;

&lt;p&gt;Type "@" inside a note and a list of your site's users pops up, letting you tag a specific collaborator, who then gets an email notification linking straight back to the post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyn8x69ebpjwsg3qd33w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyn8x69ebpjwsg3qd33w.webp" alt="Add Notes on Particular Line or Whole Para" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also leave more than one note on the same block, which matters if two different people are commenting on two different things in the same paragraph. Long notes collapse by default with a "Show more" toggle, so a detailed note doesn't take over the sidebar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy43fr6t1wy8svfn8cbs4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy43fr6t1wy8svfn8cbs4.webp" alt="Multiple Users Added Notes on Particular Line or Whole Para" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does Client-Side Media Processing Speed Up Your Uploads?
&lt;/h2&gt;

&lt;p&gt;With&amp;nbsp;&lt;a href="https://make.wordpress.org/core/2026/07/22/client-side-media-processing-in-wordpress-7-1/" rel="noopener noreferrer"&gt;client-side media processing&lt;/a&gt;, WordPress 7.1 moves image processing out of your server and into the browser. Previously, when you uploaded an image, your server had to generate every thumbnail size, handle format conversion, and fix rotation using PHP. Now the browser does that work instead, using a fast, established image library called libvips running right inside your browser, and sends the finished files to WordPress.&lt;/p&gt;

&lt;p&gt;The practical upshot: fewer failed uploads on large image files, since you won't hit your host's PHP memory limit mid-upload. Your server also does less work overall, since it's no longer generating every thumbnail size itself. And because the browser's processing produces better-compresse&lt;/p&gt;

&lt;p&gt;Two upload types get a real, visible improvement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Animated GIFs you upload are automatically converted into lightweight video files (using the same kind of technology your browser already uses to play video), so a GIF that used to weigh down your page loads much lighter without you doing anything differently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HEIC photos, the default format iPhones use, are automatically converted to JPG before upload. You no longer need a plugin just to make iPhone photos work on your site.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alongside this, the Media Library grid now scrolls infinitely by default instead of paginating, so you can keep scrolling through your library instead of clicking "next page" repeatedly. If you'd rather have the old paginated view back, there's a toggle for it in your user profile settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faokdsjmuprbtpczi59ov.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faokdsjmuprbtpczi59ov.webp" alt="Enable or Disable Infinite Scroll for Media Library Grid View" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What New Blocks Does WordPress 7.1 Add?
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1 adds two brand new blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tabs Block
&lt;/h3&gt;

&lt;p&gt;The Tabs block organizes content into clickable panels, letting a visitor click between sections instead of scrolling through a long page. It's a natural fit for FAQs, product comparisons, or any content with a handful of parallel options.&lt;/p&gt;

&lt;p&gt;Each tab panel can hold any other block inside it, not just text. You can drop in images, buttons, columns, galleries, or even another Tabs block, so a tab isn't limited to a paragraph.&lt;/p&gt;

&lt;p&gt;The tabs themselves are fully styleable. You can set the color, typography, and spacing for the tab buttons, and control the overall look of the tab panel separately, so the whole block can match your site's design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkktoddvom5mo9ph1spo4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkktoddvom5mo9ph1spo4.webp" alt="New Tabs Blocks In WordPress 7.1" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Playlist Block
&lt;/h3&gt;

&lt;p&gt;The Playlist block lets you embed a simple audio playlist, complete with a waveform visualization that shows the shape of each track. It supports whatever audio file types WordPress already accepts.&lt;/p&gt;

&lt;p&gt;The playlist itself has several toggles you can turn on or off: showing the tracklist, artist names, track numbers, track duration, tracklist images, and whether the track image appears on the play button. You can also set the track order to ascending or descending.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frf2bvnfgksimp1cklsna.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frf2bvnfgksimp1cklsna.webp" alt="New Playlist Blocks In WordPress 7.1" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each track has its own settings: artist, album, title, and a track image, so every song in the playlist can show proper metadata and cover art instead of a generic placeholder.&lt;/p&gt;

&lt;p&gt;On the styling side, the block has its own controls for the block and the waveform background, and their shape, so the playlist can match your site's design rather than looking like a generic embed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzo1ei53x9clii7k02xql.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzo1ei53x9clii7k02xql.webp" alt="Playlist Block Playlist Track Settings In WordPress 7.1" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New for the Icon Block in WordPress 7.1?
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1 opens up the Icon block to icon sets beyond WordPress's own defaults, and gives the Icon block itself a few small but useful upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom SVG Icons API
&lt;/h3&gt;

&lt;p&gt;One of the biggest developer-focused additions in WordPress 7.1 is the new&amp;nbsp;&lt;a href="https://make.wordpress.org/core/2026/07/24/registering-and-rendering-svg-icons-in-wordpress-7-1/" rel="noopener noreferrer"&gt;&lt;strong&gt;Custom SVG Icons API&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Previously, developers who wanted custom icons often had to register them using JavaScript, build custom icon libraries, or depend on third-party solutions. WordPress now provides an official API for registering and managing SVG icon collections.&lt;/p&gt;

&lt;p&gt;Developers can register icons individually or as complete collections and make them available throughout the Block Editor. Once registered, these icons can be used in custom blocks, plugins, themes, and the Icon Block itself.&lt;/p&gt;

&lt;p&gt;If you build WordPress plugins and want the full technical walkthrough, including a working example plugin,&amp;nbsp;&lt;a href="https://wpvibes.com/blog/wordpress-custom-icons/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content"&gt;see our complete guide on registering custom SVG icons in WordPress 7.1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuo8sq4zwvbw8keugbwb3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuo8sq4zwvbw8keugbwb3.webp" alt="Custom SVG Icons Added" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Icon Block Toolbar Improvements
&lt;/h3&gt;

&lt;p&gt;The Icon block itself picked up a few smaller improvements too: newly inserted Icon blocks now start with a default icon instead of an empty placeholder, and the toolbar has new controls to flip an icon horizontally or vertically, or rotate it 90 degrees at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuawnhe2cr8b5fe1ja5fh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuawnhe2cr8b5fe1ja5fh.webp" alt="Icon Block Toolbar Improve With Flip and Rotate Settings" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Else Changed in the WordPress Admin?
&lt;/h2&gt;

&lt;p&gt;A handful of smaller changes add up to a more consistent day-to-day experience.&lt;/p&gt;

&lt;p&gt;The admin toolbar now appears inside the post editor and the Site Editor for the first time. Previously, opening either editor made the toolbar disappear entirely, and the only way back to the dashboard was clicking the "W" logo in the editor's own toolbar, which didn't read as a back button to most people.&lt;/p&gt;

&lt;p&gt;WordPress 7.1 fixes both problems: the editor toolbar now has a proper back button (a left-pointing chevron), and the admin toolbar itself shows your site's icon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frps2lknhqzjou06idn38.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frps2lknhqzjou06idn38.webp" alt="WordPress Admin Toolbar and Back Button Displayed on Post Editor" width="799" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrngqo6c28nh2ks0ly20.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrngqo6c28nh2ks0ly20.webp" alt="WordPress Admin Toolbar Displayed on Site Editor" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Site Editor sidebar&lt;/strong&gt;&amp;nbsp;finally matches whatever admin color scheme you've chosen in your profile, instead of always defaulting to black regardless of your preference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi6t7svw09jqeqd0eewt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi6t7svw09jqeqd0eewt.webp" alt="Site Editor Matched User Profile Theme in WordPress" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also a new Identity section under Design in the Site Editor, grouping your site title, tagline, logo, and icon in one place instead of scattering them across Settings and templates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvl2vqtyat1kuc95wdna.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvl2vqtyat1kuc95wdna.webp" alt="Site Editor Showing New Identity Section" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Other Small Improvements Should You Know About?
&lt;/h2&gt;

&lt;p&gt;A few smaller design tools rounded out this release. None of these are big enough for their own section, but they're worth knowing about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Text-shadow support&lt;/strong&gt;: You can now add a text shadow through Global Styles. It's an early implementation, currently only configurable through theme.json rather than a visual control, so it's more useful to theme developers than to someone editing a single post today.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Background gradients&lt;/strong&gt;: Several blocks (Group, Accordion, Pullquote, Post Content, and Quote) now support background gradients by default, controllable right in the block settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minimum width&lt;/strong&gt;: Blocks can now have a minimum width set, alongside the existing height and width controls.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Developer Experience Improvements Does WordPress 7.1 Bring?
&lt;/h2&gt;

&lt;p&gt;Beyond visual improvements, WordPress 7.1 introduces several enhancements aimed at developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Block Bindings
&lt;/h3&gt;

&lt;p&gt;Block Bindings already let you connect a block's content, like a Heading, Paragraph, or Button, to a dynamic data source such as a custom field, without writing rendering code. WordPress 7.1 extends that same capability to list items, so a bullet or numbered list can pull from a data source too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expanded Abilities API
&lt;/h3&gt;

&lt;p&gt;The Abilities API, which lets WordPress expose specific actions to AI tools and other integrations, picks up better filtering, validation, and a clearer way to mark an ability as publicly available.&lt;/p&gt;

&lt;p&gt;If you're building AI-connected features or automations on top of WordPress, this makes those integrations more reliable and easier to control.&amp;nbsp;&lt;a href="https://wpvibes.com/blog/wordpress-abilities-api-mcp/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content"&gt;See our Abilities API + MCP guide for the full guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1 is a design and collaboration release: responsive styling and hover states without touching CSS, Notes that finally feel built for teams, faster image handling for supported browsers, and two genuinely useful new blocks. None of it demands a relearning curve, but the responsive and interactive styling controls alone are worth exploring the moment you update.&lt;/p&gt;

&lt;p&gt;Keep an eye on your usual update workflow: back up first, test on staging if you run custom blocks or older plugins, then roll it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When will WordPress 7.1 be released?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress 7.1 is scheduled for release on&amp;nbsp;&lt;strong&gt;August 19, 2026&lt;/strong&gt;. If you're running a production website, it's a good idea to test the update on a staging site first, especially if you use custom themes or plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does WordPress 7.1 support real-time, multi-person editing?&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;No. WordPress 7.1 focuses on asynchronous collaboration through the upgraded Notes feature. Two people editing the same post at the same time, Google Docs style, is not part of this release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the Custom SVG Icons API in WordPress 7.1?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Custom SVG Icons API is a new developer feature that provides a standardized way to register and manage custom SVG icon collections. Developers can register individual icons or entire icon libraries and use them throughout the Block Editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does WordPress 7.1 require a new minimum PHP version?&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;WordPress 7.1 doesn't introduce a new PHP requirement beyond what WordPress 7.0 already set. If your site runs comfortably on 7.0, it will run on 7.1.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Custom SVG Icons: An Exciting WordPress 7.1 Feature</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:35:51 +0000</pubDate>
      <link>https://dev.to/wpvibes/custom-svg-icons-an-exciting-wordpress-71-feature-1n8k</link>
      <guid>https://dev.to/wpvibes/custom-svg-icons-an-exciting-wordpress-71-feature-1n8k</guid>
      <description>&lt;p&gt;For years, adding a custom icon to the block editor meant reaching for JavaScript, a build step, or a workaround that only half-worked. WordPress 7.1 is about to change that.&lt;/p&gt;

&lt;p&gt;A new public API for custom SVG icons is landing in the next release. You'll be able to register your own icons, group them into named collections, and have them show up in the&amp;nbsp;&lt;code&gt;core/icon&lt;/code&gt;&amp;nbsp;block picker, render server-side in PHP, and appear over the REST API, all from a plugin, with no JavaScript build step at all.&lt;/p&gt;

&lt;p&gt;This guide walks through building a small plugin called&amp;nbsp;&lt;code&gt;wpvibes-custom-icons&lt;/code&gt;&amp;nbsp;that registers a collection, adds icons two different ways (inline SVG and file paths), and shows how to use those icons in the block editor, in PHP templates, and as custom block icons. The complete plugin is on &lt;a href="https://github.com/webtechhardik/wpviibes-custom-icons" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;WordPress 7.1 releases on August 19, 2026. The code below matches what's expected to ship on release day, and we'll come back and verify every detail against the final build once it's out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;&amp;nbsp;&lt;code&gt;wpvibes-custom-icons&lt;/code&gt;&amp;nbsp;&lt;strong&gt;plugin&lt;/strong&gt;&amp;nbsp;with a registered icon collection called&amp;nbsp;&lt;code&gt;wpvibes&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Three ready-to-use icons&lt;/strong&gt;: a heart (registered inline), a star, and a bookmark (both registered from&amp;nbsp;&lt;code&gt;.svg&lt;/code&gt;&amp;nbsp;files)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A bulk registration pattern&lt;/strong&gt;&amp;nbsp;that reads every SVG file from a folder and registers each one automatically&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A practical shortcode&lt;/strong&gt;&amp;nbsp;for inserting any registered icon into post content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real integration&lt;/strong&gt;&amp;nbsp;with our&amp;nbsp;&lt;a href="https://wpvibes.com/blog/php-only-blocks-wordpress/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content"&gt;PHP-only blocks guide&lt;/a&gt;, using a custom icon as a block icon&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress 7.1 or later&lt;/strong&gt;&amp;nbsp;(7.1 RC works for development)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A few SVG icon files&lt;/strong&gt;&amp;nbsp;(or use the ones provided below)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local development environment&lt;/strong&gt;&amp;nbsp;(LocalWP, WordPress Studio, wp-env)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Basic PHP knowledge&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Understand the API in Three Minutes
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1's custom icons API has three main functions and one organizing concept.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The functions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;wp_register_icon_collection( $name, $args ): register a named group of icons&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;wp_register_icon( $name, $args ): register a single icon&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;wp_get_icon( $name, $args ): render an icon's SVG in PHP (returns the SVG string)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The organizing concept: collections.&amp;nbsp;Every icon belongs to a collection. The collection name becomes a prefix for every icon inside it, which is what makes&amp;nbsp;core/plus&amp;nbsp;different from&amp;nbsp;wpvibes/plus. This keeps custom icons from colliding with core icons or with other plugins.&lt;/p&gt;

&lt;p&gt;Two ways to provide the SVG when registering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;content: the SVG markup as a string, embedded directly in PHP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;file_path: an absolute path to an&amp;nbsp;.svg&amp;nbsp;file on disk&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use one or the other per icon, not both. Inline&amp;nbsp;content&amp;nbsp;is handy for one or two icons;&amp;nbsp;file_path&amp;nbsp;scales better for a folder of many.&lt;/p&gt;

&lt;p&gt;That's the API. Now let's build with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Set Up the Plugin and Register the Collection
&lt;/h2&gt;

&lt;p&gt;While building this, one thing became clear quickly:&amp;nbsp;&lt;strong&gt;the collection has to be registered before you can add icons to it.&lt;/strong&gt;&amp;nbsp;If you try to register an icon whose collection doesn't exist yet, the icon silently fails to appear in the picker.&lt;/p&gt;

&lt;p&gt;Create a folder inside&amp;nbsp;&lt;code&gt;wp-content/plugins/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wpvibes-custom-icons/
├── wpvibes-custom-icons.php
└── icons/
    ├── star.svg
    └── bookmark.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create&amp;nbsp;&lt;code&gt;wpvibes-custom-icons.php&lt;/code&gt;&amp;nbsp;with the plugin header and the collection registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: WPVibes Custom Icons
 * Description: Register custom SVG icons for WordPress 7.1+.
 * Version:     1.0.0
 * Author:      WPVibes
 * Requires at least: 7.1
 * Requires PHP: 7.4
 */&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVIBES_ICONS_PATH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;plugin_dir_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Step 1: Register the icon collection.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_icon_collection'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_icon_collection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_icon_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVibes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-icons'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collection name (&lt;code&gt;wpvibes&lt;/code&gt;) becomes the prefix for every icon that follows:&amp;nbsp;&lt;code&gt;wpvibes/heart&lt;/code&gt;,&amp;nbsp;&lt;code&gt;wpvibes/star&lt;/code&gt;, and so on. The&amp;nbsp;&lt;code&gt;label&lt;/code&gt;&amp;nbsp;is what shows up as a section heading in the icon picker.&lt;/p&gt;

&lt;p&gt;Activate the plugin from&amp;nbsp;&lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;. Nothing to see yet, since collections don't render until they contain icons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Register Icons Two Ways
&lt;/h2&gt;

&lt;p&gt;WordPress supports two ways to register an icon. This step uses both: the inline&amp;nbsp;&lt;code&gt;content&lt;/code&gt;&amp;nbsp;method for the heart icon, and the&amp;nbsp;&lt;code&gt;file_path&lt;/code&gt;&amp;nbsp;method for star and bookmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1: Register an inline icon with&amp;nbsp;content
&lt;/h3&gt;

&lt;p&gt;The heart SVG lives directly in the PHP file. Useful when there's just one or two icons and you don't want extra files.&lt;/p&gt;

&lt;p&gt;Add this to&amp;nbsp;&lt;code&gt;wpvibes-custom-icons.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 2: Register icons using inline SVG content.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_inline_icons'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_inline_icons&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/heart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Heart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-icons'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&amp;gt;&amp;lt;path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/&amp;gt;&amp;lt;/svg&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How the code flows:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The action hooks into&amp;nbsp;&lt;code&gt;init&lt;/code&gt;&amp;nbsp;at priority&amp;nbsp;&lt;code&gt;20&lt;/code&gt;, which is later than the collection registration (default priority&amp;nbsp;&lt;code&gt;10&lt;/code&gt;), so the collection exists before the icon tries to join it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The icon name&amp;nbsp;&lt;code&gt;wpvibes/heart&lt;/code&gt;&amp;nbsp;follows the pattern&amp;nbsp;&lt;code&gt;collection/icon-slug&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The&amp;nbsp;&lt;code&gt;label&lt;/code&gt;&amp;nbsp;shows in the icon picker;&amp;nbsp;&lt;code&gt;content&lt;/code&gt;&amp;nbsp;holds the SVG markup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The SVG uses&amp;nbsp;&lt;code&gt;stroke="currentColor"&lt;/code&gt;&amp;nbsp;so the icon inherits color from surrounding text, with no hardcoded fill.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2: Register icons from&amp;nbsp;&lt;code&gt;.svg&lt;/code&gt;&amp;nbsp;files
&lt;/h3&gt;

&lt;p&gt;For icons stored on disk, use&amp;nbsp;&lt;code&gt;file_path&lt;/code&gt;&amp;nbsp;instead. Save these two files in the&amp;nbsp;&lt;code&gt;icons/&lt;/code&gt;&amp;nbsp;folder:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;icons/star.svg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;polygon&lt;/span&gt; &lt;span class="na"&gt;points=&lt;/span&gt;&lt;span class="s"&gt;"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;icons/bookmark.svg&lt;/code&gt;&lt;strong&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now register both from PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 3: Register icons from .svg files.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_file_icons'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_file_icons&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/star'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Star'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-icons'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'file_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_ICONS_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'icons/star.svg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/bookmark'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Bookmark'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-icons'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'file_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_ICONS_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'icons/bookmark.svg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same registration function, same collection. Only&amp;nbsp;&lt;code&gt;content&lt;/code&gt;&amp;nbsp;becomes&amp;nbsp;&lt;code&gt;file_path&lt;/code&gt;. WordPress reads the file on demand.&lt;/p&gt;

&lt;p&gt;Now open the&amp;nbsp;&lt;code&gt;core/icon&lt;/code&gt;&amp;nbsp;block in the editor. The picker shows a new&amp;nbsp;&lt;strong&gt;WPVibes&lt;/strong&gt;&amp;nbsp;section with Heart, Star, and Bookmark. Insert any of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcje7z8ste2ubesxaes5t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcje7z8ste2ubesxaes5t.webp" alt="Custom Icon Block Picker Added in WordPress" width="799" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; If an icon doesn't appear, the most common cause is registering the icon before the collection exists. Make sure the collection is registered first (default priority), and the icons are registered after (priority&amp;nbsp;&lt;code&gt;20&lt;/code&gt;&amp;nbsp;in the examples above).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Register a Whole Folder of Icons (Bulk Pattern)
&lt;/h2&gt;

&lt;p&gt;For a real icon library, registering one icon at a time doesn't scale. Nobody writes 30&amp;nbsp;wp_register_icon()&amp;nbsp;calls by hand. The practical pattern: drop SVGs into a folder, and let PHP auto-register everything on the next request.&lt;/p&gt;

&lt;p&gt;Replace the individual&amp;nbsp;wpvibes_register_file_icons()&amp;nbsp;function with this bulk version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_all_file_icons'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_all_file_icons&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$icons_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_ICONS_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'icons/'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$svg_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$icons_dir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'*.svg'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$svg_files&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$svg_files&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$file_path&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$slug&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.svg'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ucwords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;wp_register_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wpvibes/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'file_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code flow in plain words:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;glob( $icons_dir . '*.svg' )&lt;/code&gt;&amp;nbsp;returns every&amp;nbsp;&lt;code&gt;.svg&lt;/code&gt;&amp;nbsp;file in the folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;basename( $file_path, '.svg' )&lt;/code&gt;&amp;nbsp;takes just the filename without extension, so&amp;nbsp;&lt;code&gt;advanced-button.svg&lt;/code&gt;&amp;nbsp;becomes&amp;nbsp;&lt;code&gt;advanced-button&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ucwords( str_replace( '-', ' ', $slug ) )&lt;/code&gt;&amp;nbsp;turns the slug into a readable label:&amp;nbsp;&lt;code&gt;advanced-button&lt;/code&gt;&amp;nbsp;becomes "Advanced Button" for the picker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The loop registers each icon under the&amp;nbsp;&lt;code&gt;wpvibes/&lt;/code&gt;&amp;nbsp;collection.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop any new SVG into the&amp;nbsp;&lt;code&gt;icons/&lt;/code&gt;&amp;nbsp;folder, refresh the editor, and it appears in the picker. No code changes needed.&lt;/p&gt;

&lt;p&gt;This is the pattern used to register the WPVibes icon library. The same six lines register the entire icon set: heart (registered inline earlier), star, bookmark, and any additional icons dropped into the folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6khsmuc27nla3kzazqo0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6khsmuc27nla3kzazqo0.webp" alt="Multiple Icons Added in Custom SVG Icon Tab" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Use the Icons in Three Places
&lt;/h2&gt;

&lt;p&gt;Once icons are registered, three places automatically get access to them. Here's each one with a small example.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1: In the core/icon block
&lt;/h3&gt;

&lt;p&gt;The&amp;nbsp;core/icon&amp;nbsp;block picker automatically shows every registered collection. Users insert the block, pick a WPVibes icon, and it renders on the frontend. No extra code needed.&lt;/p&gt;

&lt;p&gt;Icons also appear at the REST endpoint&amp;nbsp;/wp-json/wp/v2/icons&amp;nbsp;(with a companion&amp;nbsp;/wp-json/wp/v2/icon-collections&amp;nbsp;endpoint for collections), for any external tool that reads WordPress data.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2: In PHP via&amp;nbsp;wp_get_icon()
&lt;/h3&gt;

&lt;p&gt;The&amp;nbsp;wp_get_icon()&amp;nbsp;function returns an icon's SVG as a string. The simplest form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;wp_get_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes/heart'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real templates, pass a second argument to control size, class, and accessibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;wp_get_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'wpvibes/star'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'size'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'featured-icon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Featured'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-icons'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;size&lt;/code&gt;&amp;nbsp;sets the SVG's&amp;nbsp;&lt;code&gt;width&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;height&lt;/code&gt;&amp;nbsp;in pixels&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;class&lt;/code&gt;&amp;nbsp;adds a CSS class to the SVG element&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;label&lt;/code&gt;&amp;nbsp;sets&amp;nbsp;&lt;code&gt;aria-label&lt;/code&gt;&amp;nbsp;for screen readers&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A practical shortcode example.&lt;/strong&gt;&amp;nbsp;Here's a small&amp;nbsp;&lt;code&gt;[wpvibes_icon]&lt;/code&gt;&amp;nbsp;shortcode that lets anyone drop an icon into post content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_shortcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_icon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_icon_shortcode'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_icon_shortcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$atts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shortcode_atts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'heart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'text'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'size'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;$atts&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'size'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'size'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sanitize_html_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$icon&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'&amp;lt;span class="wpvibes-icon-inline"&amp;gt;%s %s&amp;lt;/span&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it inside any post, page, or widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;wpvibes_icon&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"heart"&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;wpvibes_icon&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"star"&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Featured post"&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"32"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;wpvibes_icon&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bookmark"&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Save for later"&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"20"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The icon appears next to the text, sized to whatever the&amp;nbsp;&lt;code&gt;size&lt;/code&gt;&amp;nbsp;attribute says. Because the SVG uses&amp;nbsp;&lt;code&gt;currentColor&lt;/code&gt;, the icon picks up the surrounding text color automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3: As a custom block icon in&amp;nbsp;register_block_type()
&lt;/h3&gt;

&lt;p&gt;Custom icons can replace the default Dashicon in any block registration. In our&amp;nbsp;&lt;a href="https://wpvibes.com/blog/php-only-blocks-wordpress/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content"&gt;PHP-only blocks guide&lt;/a&gt;, block icons look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'icon' =&amp;gt; 'warning',   // Uses a Dashicon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With WordPress 7.1 custom icons, that becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'icon' =&amp;gt; 'wpvibes/star',   // Uses a registered custom icon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The block inserter now shows the branded WPVibes icon instead of a generic Dashicon. This works for both JavaScript-registered blocks and PHP-only blocks.&lt;/p&gt;

&lt;p&gt;Same registration function, three consumer contexts. This "register once, use everywhere" pattern is the same architecture used by WordPress's Abilities API, a good sign that WordPress is heading toward a genuinely consistent developer story across its foundational APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Style Custom Icons with CSS
&lt;/h2&gt;

&lt;p&gt;Custom icons look native by default, but real integration means they adapt to their surroundings: inheriting text color, scaling with font size, changing appearance on hover.&lt;/p&gt;

&lt;p&gt;The&amp;nbsp;class&amp;nbsp;argument passed through&amp;nbsp;wp_get_icon()&amp;nbsp;(shown in Step 5.2) already covers the standard case of adding a CSS hook to the SVG. This section covers what to do with that hook, plus patterns that work everywhere the icon appears, including in the block editor where the&amp;nbsp;class&amp;nbsp;argument isn't in play.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1: Use&amp;nbsp;currentColor&amp;nbsp;for color inheritance
&lt;/h3&gt;

&lt;p&gt;The single most important pattern in SVG design: use&amp;nbsp;stroke="currentColor"&amp;nbsp;(or&amp;nbsp;fill="currentColor") instead of hardcoded colors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ Hardcoded color: icon always red --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; 
&lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#dc2626"&lt;/span&gt; 
&lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;    

&lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With&amp;nbsp;currentColor, an icon inside red text renders red. Inside a dark theme, it renders dark. Inside a link, it matches the link color. The SVG behaves like text.&lt;/p&gt;

&lt;p&gt;This is why the icons in our library ship without hardcoded colors. It's the difference between an icon that fits any theme and one that clashes with half of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2: Override color when a specific color is needed
&lt;/h3&gt;

&lt;p&gt;Sometimes an icon&amp;nbsp;must&amp;nbsp;be a specific color regardless of surroundings, like a brand mark or a status indicator. Use CSS to force it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.featured-icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f59e0b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Amber, regardless of surrounding text */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the SVG uses&amp;nbsp;&lt;code&gt;currentColor&lt;/code&gt;, setting&amp;nbsp;&lt;code&gt;color&lt;/code&gt;&amp;nbsp;on the SVG (via the class from Step 5.2) cascades into its stroke or fill. The icon renders amber even inside dark text.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3: Size and hover states
&lt;/h3&gt;

&lt;p&gt;For icons rendered through the shortcode or&amp;nbsp;&lt;code&gt;wp_get_icon()&lt;/code&gt;, the&amp;nbsp;&lt;code&gt;size&lt;/code&gt;&amp;nbsp;argument handles pixel sizing directly. For CSS-controlled sizing or hover states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wpvibes-icon-inline&lt;/span&gt; &lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt; &lt;span class="m"&gt;0.15s&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wpvibes-icon-inline&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.4: When to use CSS classes vs. the&amp;nbsp;&lt;code&gt;size&lt;/code&gt;&amp;nbsp;argument
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use&lt;/strong&gt;&amp;nbsp;&lt;code&gt;size&lt;/code&gt;&amp;nbsp;&lt;strong&gt;in&lt;/strong&gt;&amp;nbsp;&lt;code&gt;wp_get_icon()&lt;/code&gt;&amp;nbsp;for one-off cases where the exact pixel size matters and there's no reason to write CSS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use CSS classes&lt;/strong&gt;&amp;nbsp;(via the&amp;nbsp;&lt;code&gt;class&lt;/code&gt;&amp;nbsp;argument, or on the wrapper element) when the icon appears in many places and should stay consistent, or when styling depends on state (hover, focus, active) or context (inside a link, inside a heading).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the shortcode example in Step 5.2, both approaches work. Small tweaks per shortcode use the&amp;nbsp;&lt;code&gt;size&lt;/code&gt;&amp;nbsp;attribute; site-wide consistency uses CSS on&amp;nbsp;&lt;code&gt;.wpvibes-icon-inline svg&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Remove Icons and Collections
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1 ships two matching functions for unregistering: one for a single icon, one for a whole collection.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1: Remove a single icon
&lt;/h3&gt;

&lt;p&gt;Useful during development, or when a plugin update drops an icon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wp_unregister_icon( 'wpvibes/bookmark' );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The icon disappears from the picker on the next request. Any post that already inserted this icon in a&amp;nbsp;&lt;code&gt;core/icon&lt;/code&gt;&amp;nbsp;block will render empty (WordPress doesn't rewrite post content).&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2: Remove the entire collection on deactivation
&lt;/h3&gt;

&lt;p&gt;When the plugin is deactivated, all its icons should stop appearing in the picker. Unregistering the collection removes every icon inside it in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;register_deactivation_hook( __FILE__, 'wpvibes_unregister_icons' );

function wpvibes_unregister_icons() {

    wp_unregister_icon_collection( 'wpvibes' );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No need to loop through individual icons. The collection cleanup handles it all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the Full Plugin
&lt;/h2&gt;

&lt;p&gt;The complete&amp;nbsp;wpvibes-custom-icons&amp;nbsp;plugin, with sample SVGs, README, and the full bulk registration pattern, is on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Custom Icons Fit the Bigger Picture
&lt;/h2&gt;

&lt;p&gt;WordPress 7.1's custom icons follow the same architectural pattern as the Abilities API and the&amp;nbsp;&lt;a href="https://wpvibes.com/blog/php-only-blocks-wordpress/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content"&gt;&lt;strong&gt;PHP-only blocks&lt;/strong&gt;&lt;/a&gt; guide&amp;nbsp;covered earlier: register once, use everywhere. The&amp;nbsp;core/icon&amp;nbsp;block reads from the registry,&amp;nbsp;wp_get_icon()&amp;nbsp;reads from the same registry in PHP, and the REST API exposes it to external tools including AI agents.&lt;/p&gt;

&lt;p&gt;WordPress is deliberately building foundational APIs, abilities, icons, block bindings, that follow the same register-once, discoverable-everywhere pattern. Plugin authors who invest in these APIs get their code integrated into the editor, the REST layer, and any tool built on the WordPress data model. Icons look like a small feature on the surface. Underneath, they're another piece of a much bigger shift in how WordPress plugins get built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This is a small API on paper, but it quietly closes a gap plugin developers have worked around for years. Register a collection, add icons inline or from files, and use them across the block editor, PHP templates, and REST, all without touching JavaScript. The bulk pattern with&amp;nbsp;glob()&amp;nbsp;scales from three icons to three hundred without changing the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Collections come first: icons attach to collections, not the other way around&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Two registration methods:&amp;nbsp;content&amp;nbsp;for inline,&amp;nbsp;&lt;code&gt;file_path&lt;/code&gt;&amp;nbsp;for icons on disk. Both work in the same collection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bulk registration beats per-icon calls:&amp;nbsp;&lt;code&gt;glob()&lt;/code&gt;&amp;nbsp;over a folder registers unlimited icons in six lines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;wp_get_icon( $name, $args )&lt;/code&gt;&amp;nbsp;accepts size, class, and label: a proper templating function, not just an SVG dump&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;currentColor&lt;/code&gt;&amp;nbsp;in SVGs is the pattern to follow: icons that adapt to their surroundings look native everywhere&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The same registration serves editor + PHP + REST: one function call, three consumer contexts&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Custom icons complete the story for custom blocks. Combined with the&amp;nbsp;PHP-only blocks guide, a plugin can now register blocks, register their icons, and ship a completely branded editor experience without a build step. Once WordPress 7.1 ships on August 19, this is genuinely one of the releases worth updating for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I have to register a collection before registering icons?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Icons must belong to an existing collection, or they silently fail to appear in the picker. Register the collection at default priority on&amp;nbsp;&lt;code&gt;init&lt;/code&gt;, then register icons at a higher priority (like&amp;nbsp;&lt;code&gt;20&lt;/code&gt;) so they run after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I mix inline&amp;nbsp;&lt;/strong&gt;&lt;code&gt;content&lt;/code&gt;&lt;strong&gt;&amp;nbsp;icons and&amp;nbsp;&lt;/strong&gt;&lt;code&gt;file_path&lt;/code&gt;&lt;strong&gt;&amp;nbsp;icons in the same collection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Each&amp;nbsp;&lt;code&gt;wp_register_icon()&lt;/code&gt;&amp;nbsp;call is independent. Use&amp;nbsp;&lt;code&gt;content&lt;/code&gt;&amp;nbsp;for one or two icons,&amp;nbsp;&lt;code&gt;file_path&lt;/code&gt;&amp;nbsp;for the rest. WordPress treats them the same once registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do custom icons work as block icons in&amp;nbsp;&lt;/strong&gt;&lt;code&gt;register_block_type()&lt;/code&gt;&lt;strong&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Pass the registered icon name (like&amp;nbsp;&lt;code&gt;'wpvibes/star'&lt;/code&gt;) to the&amp;nbsp;&lt;code&gt;icon&lt;/code&gt;&amp;nbsp;key in&amp;nbsp;&lt;code&gt;register_block_type()&lt;/code&gt;. The block inserter shows the custom SVG instead of a Dashicon. Works for JavaScript-registered blocks and PHP-only blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does&amp;nbsp;&lt;/strong&gt;&lt;code&gt;wp_get_icon()&lt;/code&gt;&lt;strong&gt;&amp;nbsp;return the icon as HTML, or as a string?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wp_get_icon()&lt;/code&gt;&amp;nbsp;returns the SVG as a string, so you can echo it directly or wrap it in your own HTML. The second&amp;nbsp;&lt;code&gt;$args&lt;/code&gt;&amp;nbsp;array accepts&amp;nbsp;&lt;code&gt;size&lt;/code&gt;,&amp;nbsp;&lt;code&gt;class&lt;/code&gt;,&amp;nbsp;&lt;code&gt;label&lt;/code&gt;&amp;nbsp;(for&amp;nbsp;&lt;code&gt;aria-label&lt;/code&gt;), and&amp;nbsp;&lt;code&gt;title&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What SVG attributes should I remove before registering an icon?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remove hardcoded&amp;nbsp;&lt;code&gt;fill&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;stroke&lt;/code&gt;&amp;nbsp;colors,&amp;nbsp;&lt;code&gt;width&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;height&lt;/code&gt;&amp;nbsp;attributes on the root&amp;nbsp;&lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;&amp;nbsp;(leave the&amp;nbsp;&lt;code&gt;viewBox&lt;/code&gt;), and any&amp;nbsp;&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;&amp;nbsp;blocks or scripts. Keep&amp;nbsp;&lt;code&gt;viewBox&lt;/code&gt;&amp;nbsp;and structural attributes. Use&amp;nbsp;&lt;code&gt;currentColor&lt;/code&gt;&amp;nbsp;for any color reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I register icons from a theme instead of a plugin?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The same&amp;nbsp;&lt;code&gt;wp_register_icon()&lt;/code&gt;&amp;nbsp;call works from a theme's&amp;nbsp;&lt;code&gt;functions.php&lt;/code&gt;&amp;nbsp;using&amp;nbsp;&lt;code&gt;get_stylesheet_directory()&lt;/code&gt;&amp;nbsp;for the file path. The trade-off is that icons registered from a theme disappear when the theme changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if two collections have the same icon name?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Collections namespace icon names, so&amp;nbsp;&lt;code&gt;wpvibes/heart&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;myplugin/heart&lt;/code&gt;&amp;nbsp;coexist without conflict. The full name including the collection prefix is what makes each unique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does WordPress sanitize SVGs on registration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress does not modify the SVG content on registration. The exact string you provide (or file contents you reference) is what gets served. Sanitize SVGs yourself before shipping: remove scripts, external references, and anything unnecessary.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>gutenberg</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PHP-Only Blocks in WordPress 7.0: The Developer's Complete Guide</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 06 Aug 2026 06:05:10 +0000</pubDate>
      <link>https://dev.to/wpvibes/php-only-blocks-in-wordpress-70-the-developers-complete-guide-5e02</link>
      <guid>https://dev.to/wpvibes/php-only-blocks-in-wordpress-70-the-developers-complete-guide-5e02</guid>
      <description>&lt;p&gt;For the first time in eight years, you can register real Gutenberg blocks using only PHP - no React, no npm, no Webpack, no build step. Just a plugin file,&amp;nbsp;&lt;code&gt;register_block_type()&lt;/code&gt;, and a new&amp;nbsp;&lt;code&gt;autoRegister&lt;/code&gt;&amp;nbsp;flag introduced in WordPress 7.0.&lt;/p&gt;

&lt;p&gt;This guide walks you through building a complete plugin with&amp;nbsp;&lt;strong&gt;two production-ready blocks&lt;/strong&gt;&amp;nbsp;- a Notice block and a Category Display block - from setup to shipping. The complete plugin, including all CSS and helper files, is on GitHub. Everything you need to follow along is in this article; everything you need to ship is in the repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A quick clarification before we start:&lt;/strong&gt;&amp;nbsp;Server-rendered Gutenberg blocks have existed for years, but until now they always required a JavaScript layer using&amp;nbsp;&lt;code&gt;ServerSideRender&lt;/code&gt;&amp;nbsp;to scaffold the block in the editor - even though the actual rendering happened in PHP. You still needed&amp;nbsp;&lt;code&gt;block.json&lt;/code&gt;, an&amp;nbsp;&lt;code&gt;edit.js&lt;/code&gt;&amp;nbsp;file, and a build step. WordPress 7.0's&amp;nbsp;&lt;code&gt;autoRegister =&amp;gt; true&lt;/code&gt;&amp;nbsp;flag removes that JavaScript ceremony entirely.&lt;/p&gt;

&lt;p&gt;We'll cover architecture and comparisons at the end. First, let's build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is This Guide For?
&lt;/h2&gt;

&lt;p&gt;WordPress developers comfortable with PHP who want to build blocks without a JavaScript build pipeline. Basic knowledge of&amp;nbsp;&lt;code&gt;register_block_type()&lt;/code&gt;&amp;nbsp;helps but isn't required - this guide explains each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Will You Build?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Notice Block&lt;/strong&gt;: Title, description, and four skin variants (Alert, Warning, Success, Info)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Category Display Block&lt;/strong&gt;: Show post categories in Grid, List, or Compact layout, with optional images (via ACF if installed, plugin placeholder if not), post counts, and recent post lists&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress 7.0 or later&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local development environment&lt;/strong&gt;&amp;nbsp;(LocalWP, WordPress Studio, wp-env, or similar)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code editor&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Basic PHP knowledge&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optional:&lt;/strong&gt;&amp;nbsp;Advanced Custom Fields - the Category Display block supports ACF images if available and falls back to a placeholder otherwise&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tips:&lt;/strong&gt; Test all changes on a local or staging site first. WordPress 7.0 is recent and PHP-only blocks are a new API - verify everything before shipping to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up the Plugin
&lt;/h2&gt;

&lt;p&gt;The plugin contains both blocks, organized cleanly so more blocks can be added later without refactoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 - File structure
&lt;/h3&gt;

&lt;p&gt;Create this folder inside&amp;nbsp;&lt;code&gt;wp-content/plugins/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wpvibes-blocks/
├── wpvibes-blocks.php
├── includes/
│   └── class-blocks-loader.php
├── blocks/
│   ├── class-notice-block.php
│   └── class-category-display-block.php
└── assets/
    ├── notice.css
    ├── category-display.css
    └── images/
        └── category-placeholder.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this structure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The main plugin file loads everything. The loader class registers all blocks in one loop. Each block lives in its own file. Each block's CSS lives in its own file too. Adding a new block later means creating one new class file, adding one line to the loader, and registering one stylesheet. This pattern scales - a plugin with 20 blocks stays maintainable; a single 5,000-line file does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 - The main plugin file
&lt;/h3&gt;

&lt;p&gt;Create&amp;nbsp;&lt;code&gt;wpvibes-blocks.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: WPVibes Blocks
 * Description: PHP-only Gutenberg blocks — Notice and Category Display.
 * Version:     1.0.0
 * Author:      WPVibes
 * Requires at least: 7.0
 * Requires PHP: 7.4
 */&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVIBES_BLOCKS_VERSION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1.0.0'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVIBES_BLOCKS_PATH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;plugin_dir_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVIBES_BLOCKS_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;plugin_dir_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_BLOCKS_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'blocks/class-notice-block.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_BLOCKS_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'blocks/class-category-display-block.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_BLOCKS_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'includes/class-blocks-loader.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;WPVibes_Blocks_Loader&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main file wires things together. Nothing more.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 - The loader
&lt;/h3&gt;

&lt;p&gt;Create&amp;nbsp;&lt;code&gt;includes/class-blocks-loader.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WPVibes_Blocks_Loader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nv"&gt;$block_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'WPVibes_Notice_Block'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'WPVibes_Category_Display_Block'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__CLASS__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'register_styles'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__CLASS__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'register_all_blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register_styles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nf"&gt;wp_register_style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wpvibes-notice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="no"&gt;WPVIBES_BLOCKS_URL&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'assets/notice.css'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="no"&gt;WPVIBES_BLOCKS_VERSION&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;wp_register_style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wpvibes-category-display'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="no"&gt;WPVIBES_BLOCKS_URL&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'assets/category-display.css'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="no"&gt;WPVIBES_BLOCKS_VERSION&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register_all_blocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$block_classes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$class_name&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;class_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$class_name&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nv"&gt;$class_name&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$block&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate the plugin from&amp;nbsp;&lt;strong&gt;Plugins → Installed Plugins&lt;/strong&gt;. Nothing visible yet - the blocks come next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Build the Notice Block
&lt;/h2&gt;

&lt;p&gt;The Notice block shows a message with a title, description, and one of four skin options (Alert, Warning, Success, Info). It's the simpler of the two blocks - a good starting point for learning the API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4beb7zvi5wz5rogbkph2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4beb7zvi5wz5rogbkph2.webp" alt="All four Notice skins created with php stacked on the frontend" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 - Create the block class
&lt;/h3&gt;

&lt;p&gt;Create&amp;nbsp;&lt;code&gt;blocks/class-notice-block.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WPVibes_Notice_Block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nf"&gt;register_block_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wpvibes/notice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'title'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Notice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'description'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'A notice or callout with 4 skins.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'category'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'icon'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'warning'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'style'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-notice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'editor_style'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-notice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'attributes'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'supports'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_supports&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'render_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'render'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Attribute, supports, and render methods below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two keys worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;style&lt;/code&gt;&amp;nbsp;&lt;strong&gt;and&lt;/strong&gt;&amp;nbsp;&lt;code&gt;editor_style&lt;/code&gt;&amp;nbsp;- attach the stylesheet registered in the loader (same handle for both). WordPress loads it only when the block is used.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;render_callback&lt;/code&gt;&amp;nbsp;- the PHP function that returns the block's HTML.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.2 - Define the attributes
&lt;/h3&gt;

&lt;p&gt;Add this method to the class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Important notice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Add your notice message here.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'skin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'enum'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Alert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Warning'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Info'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Skin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;title&lt;/code&gt;&amp;nbsp;- a string, so WordPress auto-generates a text input in the sidebar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;description&lt;/code&gt;&amp;nbsp;- same, another text input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;skin&lt;/code&gt;&amp;nbsp;- string with&amp;nbsp;&lt;code&gt;enum&lt;/code&gt;, so WordPress auto-generates a dropdown with exactly those four options.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The&amp;nbsp;&lt;code&gt;label&lt;/code&gt;&amp;nbsp;key controls what the user sees. Without it, WordPress derives a label from the attribute name.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 - Add block supports
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_supports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'autoRegister'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'color'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'background'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'spacing'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'padding'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'margin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'border'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'color'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'radius'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'style'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'width'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happen here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;autoRegister =&amp;gt; true&lt;/code&gt;&amp;nbsp;- the flag that tells WordPress to register this block from PHP alone, without JavaScript scaffolding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;color&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt;&amp;nbsp;&lt;code&gt;spacing&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt;&amp;nbsp;&lt;code&gt;border&lt;/code&gt;&amp;nbsp;- turn on native design panels in the sidebar. Users get Color, Spacing, and Border controls automatically.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.4 - Write the render callback
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$skin_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-notice--'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'skin'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_block_wrapper_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-notice '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$skin_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'role'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'alert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h4 class="wpvibes-notice__title"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/h4&amp;gt;'&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;p class="wpvibes-notice__description"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/p&amp;gt;'&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'&amp;lt;div %1$s&amp;gt;&amp;lt;div class="wpvibes-notice__icon"&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;div class="wpvibes-notice__content"&amp;gt;%2$s%3$s&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$description&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;get_block_wrapper_attributes()&lt;/code&gt;&amp;nbsp;returns HTML attributes carrying every design choice the user made - color, background, padding, border. Always use it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;esc_html()&lt;/code&gt;&amp;nbsp;on all user-provided content. Standard WordPress escaping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;BEM naming&lt;/strong&gt;&amp;nbsp;-&amp;nbsp;&lt;code&gt;wpvibes-notice&lt;/code&gt;&amp;nbsp;(base) plus&amp;nbsp;&lt;code&gt;wpvibes-notice--alert&lt;/code&gt;&amp;nbsp;(variant). Predictable CSS.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.5 - Add the CSS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wpvibes-notice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex-start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wpvibes-notice--alert&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fee2e2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#991b1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-left-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#dc2626&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.wpvibes-notice--warning&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fef3c7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#92400e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-left-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f59e0b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.wpvibes-notice--success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#d1fae5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#065f46&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-left-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#10b981&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.wpvibes-notice--info&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#dbeafe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e3a8a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-left-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b82f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; The full CSS (icon circles, title and description typography, hover states) is in the plugin repository at&amp;nbsp;&lt;a rel="noopener noreferrer nofollow" href="https://github.com/webtechhardik/wpvibes-blocks/blob/main/assets/notice.css"&gt;assets/notice.css&lt;/a&gt;. Copy the complete file into the plugin's&amp;nbsp;&lt;code&gt;assets/&lt;/code&gt;&amp;nbsp;folder before testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.6 - See it working
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a new page or post&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click&amp;nbsp;&lt;strong&gt;+&lt;/strong&gt;&amp;nbsp;in the block inserter and search for&amp;nbsp;&lt;strong&gt;Notice&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try each skin from the sidebar dropdown&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adjust the native Color and Padding controls&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdbzc0ltldd7rdml567ui.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdbzc0ltldd7rdml567ui.webp" alt="Notice block in the editor with sidebar controls visible" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first PHP-only block is done. No JavaScript. No build step. No&amp;nbsp;&lt;code&gt;block.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; The complete file for reference is at&amp;nbsp;&lt;a rel="noreferrer noopener" href="https://github.com/webtechhardik/wpvibes-blocks/blob/main/blocks/class-notice-block.php"&gt;blocks/class-notice-block.php&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Build the Category Display Block
&lt;/h2&gt;

&lt;p&gt;The Category Display block shows post categories in Grid, List, or Compact layout. For each category: name, post count, an image (from ACF if installed, plugin placeholder otherwise), and a list of recent posts.&lt;/p&gt;

&lt;p&gt;This block covers&amp;nbsp;&lt;strong&gt;all four supported attribute types&lt;/strong&gt;&amp;nbsp;- string, integer, boolean, and enum - in one useful example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3rtzf8zxre0or0r0m1fi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3rtzf8zxre0or0r0m1fi.webp" alt="Category Display block in Grid layout on the frontend" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 - Create the block class
&lt;/h3&gt;

&lt;p&gt;Create&amp;nbsp;&lt;code&gt;blocks/class-category-display-block.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WPVibes_Category_Display_Block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nf"&gt;register_block_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wpvibes/category-display'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'title'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Category Display'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'description'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Display categories with post count, image, and recent posts.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'category'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'widgets'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'icon'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'style'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-category-display'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'editor_style'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-category-display'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'attributes'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'supports'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_supports&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'render_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'render'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Attributes, supports, and render methods below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.2 - Define the attributes (all 4 types)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_attributes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

            &lt;span class="c1"&gt;// ENUM — dropdown&lt;/span&gt;
            &lt;span class="s1"&gt;'layout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'enum'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Grid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'List'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Compact'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Grid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Layout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// INTEGER — number input with min/max&lt;/span&gt;
            &lt;span class="s1"&gt;'columns'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'minimum'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'maximum'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Columns (Grid only)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// INTEGER&lt;/span&gt;
            &lt;span class="s1"&gt;'postCount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'minimum'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'maximum'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Posts per Category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// STRING — text input (ACF field key)&lt;/span&gt;
            &lt;span class="s1"&gt;'acfFieldKey'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'term_featured_image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ACF Image Field Key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// ENUM — dropdown&lt;/span&gt;
            &lt;span class="s1"&gt;'orderBy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'enum'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Post Count'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Slug'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Order By'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// BOOLEAN — toggle&lt;/span&gt;
            &lt;span class="s1"&gt;'showImage'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Show Image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// BOOLEAN&lt;/span&gt;
            &lt;span class="s1"&gt;'showPostCount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Show Post Count'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// BOOLEAN&lt;/span&gt;
            &lt;span class="s1"&gt;'showRecentPosts'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Show Recent Posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// BOOLEAN&lt;/span&gt;
            &lt;span class="s1"&gt;'hideEmpty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'label'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Hide Empty Categories'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What each attribute type teaches:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;layout&lt;/code&gt;&amp;nbsp;&lt;strong&gt;and&lt;/strong&gt;&amp;nbsp;&lt;code&gt;orderBy&lt;/code&gt;&amp;nbsp;- string with&amp;nbsp;&lt;code&gt;enum&lt;/code&gt;. WordPress auto-generates dropdowns with exactly those choices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;columns&lt;/code&gt;&amp;nbsp;&lt;strong&gt;and&lt;/strong&gt;&amp;nbsp;&lt;code&gt;postCount&lt;/code&gt;&amp;nbsp;- integers with&amp;nbsp;&lt;code&gt;minimum&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;maximum&lt;/code&gt;. WordPress auto-generates number inputs that enforce the ranges.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;acfFieldKey&lt;/code&gt;&amp;nbsp;- a plain string. Text input where the user types the ACF field name.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;showImage&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt;&amp;nbsp;&lt;code&gt;showPostCount&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt;&amp;nbsp;&lt;code&gt;showRecentPosts&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt;&amp;nbsp;&lt;code&gt;hideEmpty&lt;/code&gt;&amp;nbsp;- booleans. Each becomes a toggle switch in the sidebar.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's every attribute type WordPress 7.0 auto-generates a control for.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 - Block supports
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_supports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'autoRegister'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'align'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wide'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'full'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'color'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'background'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'spacing'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'padding'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'margin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'border'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'color'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'radius'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'width'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'style'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same as the Notice block, with&amp;nbsp;&lt;code&gt;align&lt;/code&gt;&amp;nbsp;added so users can center or full-width the block.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 - Write the render callback
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$order_by_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'Name'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Post Count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'count'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Slug'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_categories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'hide_empty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hideEmpty'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'orderby'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order_by_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'orderBy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'order'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;p&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'No categories to display.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$layout&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'layout'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$layout_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-category-display--'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$columns_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'grid'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$layout&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;' wpvibes-category-display--cols-'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'columns'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_block_wrapper_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-category-display '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$layout_class&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$columns_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$items_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$items_html&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render_category_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'&amp;lt;div %1$s&amp;gt;%2$s&amp;lt;/div&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$items_html&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the render callback does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Queries categories&lt;/strong&gt;&amp;nbsp;using the&amp;nbsp;&lt;code&gt;hideEmpty&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;orderBy&lt;/code&gt;&amp;nbsp;attributes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handles empty state&lt;/strong&gt;&amp;nbsp;with a friendly message&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Builds layout classes&lt;/strong&gt;&amp;nbsp;-&amp;nbsp;&lt;code&gt;wpvibes-category-display--grid&lt;/code&gt;&amp;nbsp;plus&amp;nbsp;&lt;code&gt;wpvibes-category-display--cols-3&lt;/code&gt;&amp;nbsp;for column-based grids&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Loops through categories&lt;/strong&gt;&amp;nbsp;and renders each via a helper method&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.5 - Render a single category
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render_category_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$image_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'showImage'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$image_url&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_category_image_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'acfFieldKey'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$image_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'&amp;lt;div class="wpvibes-category-display__image"&amp;gt;&amp;lt;img src="%s" alt="%s" loading="lazy"&amp;gt;&amp;lt;/div&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nf"&gt;esc_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$image_url&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nf"&gt;esc_attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$count_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'showPostCount'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$count_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'&amp;lt;span class="wpvibes-category-display__count"&amp;gt;%s&amp;lt;/span&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;_n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'%d post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%d posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-blocks'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$posts_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'showRecentPosts'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="nv"&gt;$recent_posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'category'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'posts_per_page'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'postCount'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'orderby'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'order'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'DESC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$recent_posts&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

                &lt;span class="nv"&gt;$posts_html&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;ul class="wpvibes-category-display__posts"&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$recent_posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;$posts_html&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'&amp;lt;li&amp;gt;&amp;lt;a href="%s"&amp;gt;%s&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nf"&gt;esc_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="nv"&gt;$posts_html&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/ul&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'&amp;lt;div class="wpvibes-category-display__item"&amp;gt;%1$s&amp;lt;div class="wpvibes-category-display__content"&amp;gt;&amp;lt;h3 class="wpvibes-category-display__name"&amp;gt;&amp;lt;a href="%2$s"&amp;gt;%3$s&amp;lt;/a&amp;gt;&amp;lt;/h3&amp;gt;%4$s%5$s&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$image_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;esc_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_category_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nv"&gt;$count_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$posts_html&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each part (image, count, recent posts) is built conditionally based on the toggles. If a toggle is off, that part's HTML variable stays empty and contributes nothing to the final output.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.6 - The ACF image with fallback
&lt;/h3&gt;

&lt;p&gt;The Category Display block uses an ACF image if the site has ACF installed and the field is set. If not, it falls back to a placeholder from the plugin's own assets folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;get_category_image_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$acf_field_key&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Check if ACF is active AND a field key is provided.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'get_field'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_field_key&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="c1"&gt;// ACF term meta uses 'category_' . term_id as the object ID.&lt;/span&gt;
            &lt;span class="nv"&gt;$acf_image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_field_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;term_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

                &lt;span class="c1"&gt;// ACF image field can return an array, an ID, or a URL string&lt;/span&gt;
                &lt;span class="c1"&gt;// depending on the "Return Format" setting.&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;is_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_attachment_image_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$acf_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'medium'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Fallback: plugin's placeholder image.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;WPVIBES_BLOCKS_URL&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'assets/images/category-placeholder.png'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things this pattern teaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;function_exists()&lt;/code&gt;&amp;nbsp;&lt;strong&gt;before using an external plugin's function&lt;/strong&gt;&amp;nbsp;- always. Never assume ACF (or any plugin) is active.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The ACF term meta prefix&lt;/strong&gt;&amp;nbsp;- ACF stores term meta under&amp;nbsp;&lt;code&gt;category_&lt;/code&gt;&amp;nbsp;+&amp;nbsp;&lt;code&gt;term_id&lt;/code&gt;. This trips up many developers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handling ACF's different return formats&lt;/strong&gt;&amp;nbsp;- ACF image fields can return an array, an ID, or a URL, depending on the "Return Format" setting. The code should handle at least the two common ones.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.7 - Add the CSS
&lt;/h3&gt;

&lt;p&gt;Create&amp;nbsp;&lt;code&gt;assets/category-display.css&lt;/code&gt;&amp;nbsp;with the base structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wpvibes-category-display&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wpvibes-category-display--grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wpvibes-category-display--grid.wpvibes-category-display--cols-2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.wpvibes-category-display--grid.wpvibes-category-display--cols-4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wpvibes-category-display--list&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.wpvibes-category-display--compact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; The full CSS (card styling, hover states, image aspect ratios, list-layout flex rules, compact-layout borders, and mobile responsiveness) is in the plugin repository at&amp;nbsp;&lt;a rel="noreferrer noopener" href="https://github.com/webtechhardik/wpvibes-blocks/blob/main/assets/category-display.css"&gt;assets/category-display.css&lt;/a&gt;. Copy the complete file into the plugin's&amp;nbsp;&lt;code&gt;assets/&lt;/code&gt;&amp;nbsp;folder before testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.8 - Add a placeholder image
&lt;/h3&gt;

&lt;p&gt;Place any image at&amp;nbsp;&lt;code&gt;assets/images/category-placeholder.png&lt;/code&gt;&amp;nbsp;- an 800×450 image works well. The plugin repository includes one; a custom branded placeholder also works.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.9 - See it working
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Assign a few posts to different categories&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Insert the&amp;nbsp;&lt;strong&gt;Category Display&lt;/strong&gt;&amp;nbsp;block&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the sidebar, try:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Each layout (Grid, List, Compact)

*   Different column counts (2, 3, 4) while in Grid layout

*   Toggling Show Image, Show Post Count, Show Recent Posts

*   Changing Posts per Category
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzn4dchno662si0pvs9tq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzn4dchno662si0pvs9tq.webp" alt="Category Display block Settings to Changes Layout or Hide/Show Fields" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two production-ready PHP-only blocks - one simple, one covering the full API - done.&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; The complete file for reference is at&amp;nbsp;&lt;a rel="noreferrer noopener" href="https://github.com/webtechhardik/wpvibes-blocks/blob/main/blocks/class-category-display-block.php"&gt;blocks/class-category-display-block.php&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Tips
&lt;/h2&gt;

&lt;p&gt;When something breaks during development, enable WordPress debug logging in&amp;nbsp;&lt;code&gt;wp-config.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_DEBUG_LOG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_DEBUG_DISPLAY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check&amp;nbsp;&lt;code&gt;wp-content/debug.log&lt;/code&gt;&amp;nbsp;after triggering the issue. Nine times out of ten, a PHP error during registration is the culprit — and it will be in the log with a line number.&lt;/p&gt;

&lt;p&gt;For attribute-level debugging (checking what's being passed to the render callback), add temporary&amp;nbsp;&lt;code&gt;error_log()&lt;/code&gt;&amp;nbsp;calls at the top of the render function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Notice block attributes: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove these before shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the Full Plugin
&lt;/h2&gt;

&lt;p&gt;The complete plugin - with all CSS, placeholder image, and README - is on &lt;a href="https://github.com/webtechhardik/wpvibes-blocks" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How PHP-Only Blocks Work Under the Hood
&lt;/h2&gt;

&lt;p&gt;Now that the plugin builds, here's what actually happens when a PHP-only block is registered and rendered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before WordPress 7.0
&lt;/h3&gt;

&lt;p&gt;Building a server-rendered block required these files, at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-plugin/
├── src/block/
│   ├── block.json
│   ├── edit.js
│   └── index.js
├── package.json
└── my-plugin.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The&amp;nbsp;&lt;code&gt;edit.js&lt;/code&gt;&amp;nbsp;typically looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ServerSideRender&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@wordpress/editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ServerSideRender&lt;/span&gt;
            &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A JavaScript component whose only job was to say "please render this on the server." Then&amp;nbsp;&lt;code&gt;npm install&lt;/code&gt;, run a build, and hope everything worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  With WordPress 7.0
&lt;/h3&gt;

&lt;p&gt;WordPress 7.0 adds one flag:&amp;nbsp;&lt;code&gt;autoRegister =&amp;gt; true&lt;/code&gt;. When set, WordPress:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Takes the block registration from PHP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adds it to a JavaScript global in the editor settings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The editor picks it up and uses&amp;nbsp;&lt;code&gt;ServerSideRender&lt;/code&gt;&amp;nbsp;internally for the preview&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a user changes an attribute, the editor calls the REST API, which runs the PHP&amp;nbsp;&lt;code&gt;render_callback&lt;/code&gt;, and swaps in the new HTML&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zero JavaScript written. WordPress handles the ceremony.&lt;/p&gt;

&lt;h3&gt;
  
  
  The runtime flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User changes an attribute in the sidebar
        ↓
Editor sends REST API request
        ↓
WordPress runs render_callback in PHP
        ↓
render_callback returns HTML string
        ↓
Editor swaps HTML into the block preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02zqx3jdfyvq2v332nwb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02zqx3jdfyvq2v332nwb.png" alt="WordPress 7.0 Benefits to Create PHP Block" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is why every attribute change causes a small delay - there's always a server round-trip. That's the deliberate trade-off: no client-side complexity, but no live reactivity either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-generated inspector controls&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress inspects the&amp;nbsp;&lt;code&gt;attributes&lt;/code&gt;&amp;nbsp;array and generates sidebar controls automatically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute definition&lt;/th&gt;
&lt;th&gt;Generated control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type: 'string'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;type: 'string'&lt;/code&gt;&amp;nbsp;+&amp;nbsp;&lt;code&gt;enum: [...]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dropdown (SelectControl)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;type: 'integer'&lt;/code&gt;&amp;nbsp;with&amp;nbsp;&lt;code&gt;minimum&lt;/code&gt;&amp;nbsp;/&amp;nbsp;&lt;code&gt;maximum&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Number input with enforced range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type: 'boolean'&lt;/td&gt;
&lt;td&gt;Toggle switch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Controls are&amp;nbsp;&lt;strong&gt;not&lt;/strong&gt;&amp;nbsp;generated for&amp;nbsp;&lt;code&gt;object&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;array&lt;/code&gt;&amp;nbsp;types, or for attribute names that conflict with reserved block-supports names like&amp;nbsp;&lt;code&gt;style&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Use PHP-Only Blocks?
&lt;/h2&gt;

&lt;p&gt;An honest decision table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use PHP-only blocks when&lt;/th&gt;
&lt;th&gt;Use JavaScript-registered blocks when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Block is display-only (notice, category grid, author box)&lt;/td&gt;
&lt;td&gt;Block needs InnerBlocks (nested blocks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content comes from server data (users, posts, terms, APIs)&lt;/td&gt;
&lt;td&gt;Inline rich-text editing is required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replacing a legacy shortcode&lt;/td&gt;
&lt;td&gt;Editor experience needs live client-side reactivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avoiding a JavaScript build pipeline&lt;/td&gt;
&lt;td&gt;Interactivity API is needed for state management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple enough that a server round-trip on each change is acceptable&lt;/td&gt;
&lt;td&gt;Real-time visual manipulation (drag handles, instant previews) is essential&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both approaches are first-class WordPress citizens. PHP-only blocks aren't a downgrade -they're a shortcut for a specific class of blocks that never needed JavaScript in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are the Limitations of PHP-Only Blocks?
&lt;/h2&gt;

&lt;p&gt;Deliberate scope decisions, not bugs. Knowing them helps pick the right approach.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No InnerBlocks / nested blocks&lt;/td&gt;
&lt;td&gt;Can't build columns, accordions, sections&lt;/td&gt;
&lt;td&gt;No timeline announced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only 4 attribute types (string, integer, boolean, enum)&lt;/td&gt;
&lt;td&gt;No media picker, rich text, date picker&lt;/td&gt;
&lt;td&gt;DataForm being extended to PHP-only blocks in 7.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block Bindings limited in 7.0&lt;/td&gt;
&lt;td&gt;Custom fields, post meta bindings need workarounds&lt;/td&gt;
&lt;td&gt;ACF and Meta Box become native binding sources in 7.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server round-trip on every attribute change&lt;/td&gt;
&lt;td&gt;Preview feels less responsive than JS blocks&lt;/td&gt;
&lt;td&gt;By design - the trade-off for zero JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom&amp;nbsp;&lt;code&gt;style&lt;/code&gt;&amp;nbsp;attribute conflicts with supports&lt;/td&gt;
&lt;td&gt;Must rename to&amp;nbsp;&lt;code&gt;skin&lt;/code&gt;,&amp;nbsp;&lt;code&gt;variant&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;Reserved namespace, not a bug&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can PHP-only blocks contain InnerBlocks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. PHP-only blocks cannot contain nested blocks. For layouts like columns, accordions, or sections that wrap other blocks, use the traditional JavaScript block registration path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do PHP-only blocks work with the Interactivity API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not directly. The Interactivity API is designed for client-side state and reactivity, which PHP-only blocks don't provide. For blocks needing state (toggles, tabs, live filtering), JavaScript block registration is the correct choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which attribute types get auto-generated controls?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four types: string (text input), string with enum (dropdown), integer with minimum/maximum (number input), and boolean (toggle). Types like object and array do not generate controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my custom&amp;nbsp;&lt;/strong&gt;&lt;code&gt;style&lt;/code&gt;&lt;strong&gt;&amp;nbsp;attribute causing errors?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WordPress reserves the&amp;nbsp;&lt;code&gt;style&lt;/code&gt;&amp;nbsp;attribute name when block supports for color, spacing, or typography are enabled. Rename any custom attribute called&amp;nbsp;&lt;code&gt;style&lt;/code&gt;&amp;nbsp;to something like&amp;nbsp;&lt;code&gt;skin&lt;/code&gt;,&amp;nbsp;&lt;code&gt;variant&lt;/code&gt;, or&amp;nbsp;&lt;code&gt;theme&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the editor preview update in real time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Each attribute change triggers a REST API call to the PHP&amp;nbsp;&lt;code&gt;render_callback&lt;/code&gt;, then the editor swaps in the returned HTML. There's a small delay on every change - this is the trade-off for not writing JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use&amp;nbsp;&lt;/strong&gt;&lt;code&gt;wp_enqueue_block_style()&lt;/code&gt;&lt;strong&gt;&amp;nbsp;for PHP-only block CSS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not reliably. &lt;code&gt;wp_enqueue_block_style() &lt;/code&gt;works for JavaScript-registered blocks but doesn't always fire for PHP-only blocks in the editor. Use &lt;code&gt;wp_register_style() &lt;/code&gt;with the &lt;code&gt;style&lt;/code&gt; and &lt;code&gt;editor_style &lt;/code&gt;keys inside &lt;code&gt;register_block_type()&lt;/code&gt; instead - the pattern shown in this guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens in the editor if the render callback throws an error?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The block shows "Error loading block" in the editor preview. Check&amp;nbsp;&lt;code&gt;wp-content/debug.log&lt;/code&gt;&amp;nbsp;for the PHP error - the line number points directly to the issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do PHP-only blocks support WordPress block themes and Full Site Editing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. PHP-only blocks work in block themes, templates, template parts, and the Site Editor. WordPress 7.1 enforces iframe mode for block themes - the examples in this guide use current APIs, so no changes are needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Building the WPVibes Blocks plugin has covered:&lt;/p&gt;

&lt;p&gt;autoRegister =&amp;gt; true&amp;nbsp;is the entire "new API." Everything else is standard&amp;nbsp;register_block_type().&lt;/p&gt;

&lt;p&gt;The&amp;nbsp;style&amp;nbsp;and&amp;nbsp;editor_style&amp;nbsp;keys&amp;nbsp;with&amp;nbsp;wp_register_style()&amp;nbsp;are the right way to attach CSS to a PHP-only block — cleaner and correctly loading in both editor and frontend.&lt;/p&gt;

&lt;p&gt;get_block_wrapper_attributes()&amp;nbsp;carries all block-supports styles into the render output. Always use it.&lt;/p&gt;

&lt;p&gt;Auto-generated sidebar controls&amp;nbsp;work well within their limits - string, integer, boolean, enum.&lt;/p&gt;

&lt;p&gt;Server round-trip on every attribute change&amp;nbsp;is the trade-off - keep render callbacks fast.&lt;/p&gt;

&lt;p&gt;One file per block&amp;nbsp;scales. Adding a new block later is one new file, one line in the loader.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>gutenberg</category>
      <category>development</category>
    </item>
    <item>
      <title>How to Connect Elementor Form Submissions to Google Sheets</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 03 Aug 2026 10:23:40 +0000</pubDate>
      <link>https://dev.to/wpvibes/how-to-connect-elementor-form-submissions-to-google-sheets-4084</link>
      <guid>https://dev.to/wpvibes/how-to-connect-elementor-form-submissions-to-google-sheets-4084</guid>
      <description>&lt;p&gt;If you want to send your Elementor form to Google Sheets automatically, Elementor Pro doesn't offer a built-in way to do it. Most guides send you to Zapier, or a single-purpose connector plugin that only does one thing.&lt;/p&gt;

&lt;p&gt;Form Vibes Pro connects Elementor forms to Google Sheets natively, from inside WordPress. No automation platform. No per-task limits. Every new Elementor submission lands in your spreadsheet automatically.&lt;/p&gt;

&lt;p&gt;This guide walks through the complete setup, from Google API credentials to your first synced entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does This Setup Give You?
&lt;/h2&gt;

&lt;p&gt;Once this is set up, every Elementor submission appears in Google Sheets within seconds, with no manual export needed. Your team or client can see leads live without needing WordPress access. Every entry is also saved to your WordPress database as a backup, so nothing depends on Google alone.&lt;/p&gt;

&lt;p&gt;Elementor Pro doesn't give you a way to send form data anywhere but email. Form Vibes Pro adds the missing Google Sheets connection, plus a full submission dashboard inside WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Google Sheets Sync a Free or Pro Feature?
&lt;/h2&gt;

&lt;p&gt;Google Sheets sync is a Form Vibes Pro feature. Here's what's relevant to this guide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Save Elementor entries to WordPress database&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export to CSV on demand&lt;/td&gt;
&lt;td&gt;Yes (up to 1,000 rows)&lt;/td&gt;
&lt;td&gt;Yes (unlimited)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic sync to Google Sheets&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter which submissions get synced&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://formvibes.com/general/form-vibes-free-vs-pro/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;&lt;strong&gt;See full Pro feature list.&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Elementor Submissions vs Google Sheets Sync: Which Should You Use?
&lt;/h2&gt;

&lt;p&gt;Elementor Pro has a native Submissions screen inside WordPress. Use it for quick in-dashboard checks. Use Google Sheets sync when submissions need to reach people or tools outside WordPress in real time, such as sales teams, clients, external dashboards, or backup workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Connect Elementor Form Submissions to Google Sheets
&lt;/h2&gt;

&lt;p&gt;Setup has three parts: create Google API credentials, connect Form Vibes to your Google account, then set up the Google Sheets connection for your Elementor form.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Your Google API Credentials
&lt;/h3&gt;

&lt;p&gt;Google requires API credentials so Form Vibes can communicate securely with your Google account. These consist of a Client ID and a Client Secret, generated in Google Cloud Console. The full credential creation process is covered in a dedicated guide. Follow that guide to &lt;a href="https://docs.formvibes.com/article/152-how-to-connect-with-google-sheet" rel="noopener noreferrer"&gt;generate your Client ID and Client Secret&lt;/a&gt;, then return here to continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect Form Vibes to Google Sheets
&lt;/h3&gt;

&lt;p&gt;Go to Form Vibes &amp;gt; Settings &amp;gt; Google Sheet in your WordPress admin. Paste your Client ID and Client Secret into their respective fields. Click Authenticate and sign in with your Google account when prompted. After approving the requested permissions, the connection status updates to show the account as connected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjdgqh12lnzcya8c2gxg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjdgqh12lnzcya8c2gxg.webp" alt="Connect Form Vibes to Google Sheets" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tip:&lt;/strong&gt; During authentication, Google shows an "App is not verified" warning. This is expected for a self-configured OAuth connection. Click Show Advanced, then Go to App, and approve all requested permissions. The connection completes normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up the Google Sheets Connection for Your Elementor Form
&lt;/h3&gt;

&lt;p&gt;With your Google account connected, go to Form Vibes &amp;gt; Export Profiles and click Add New. Under Type, select Google Sheet instead of Local Download. This tells Form Vibes to push submissions to a spreadsheet rather than generate a downloadable file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwu4057582kkm14ujnwi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwu4057582kkm14ujnwi.webp" alt="Choose Google Sheet Profile Type in Export Profile" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the Export Profile Settings:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the form dropdown, select the Elementor form whose submissions you want to send to Google Sheets. Forms appear here once they've received at least one submission (see the troubleshooting section below if your form isn't listed yet).&lt;/p&gt;

&lt;p&gt;Choose the Google Spreadsheet where data should be sent. If the spreadsheet you need does not appear, check that the connected Google account has access to it. To create a new spreadsheet directly from Form Vibes, click the plus icon next to the spreadsheet selector.&lt;/p&gt;

&lt;p&gt;Select the specific worksheet, or tab, within the spreadsheet. Create a new one using the plus icon if needed.&lt;/p&gt;

&lt;p&gt;The Data Filter section lets you sync only the submissions that match specific conditions, instead of sending every submission to Google Sheets. To filter, choose whether any condition must be met (OR) or all conditions must be met (AND). Then, for each condition, select the form field to check, choose a match type of Equal, Not Equal, Contain, or Not Contain, and enter the value to match against. Click Add New Condition to stack additional rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiaw99d3wu13j3snqx3o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiaw99d3wu13j3snqx3o.webp" alt="Choose Elementor Form and Filter Data" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Map Your Fields and Publish:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Map each Elementor form field to its corresponding spreadsheet column. If your Elementor form has a Name field and your sheet has a Name column, connect them here. Repeat for every field you want to appear in the spreadsheet. Click Publish to save and activate the connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ya4z6r0wfww5denyra.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ya4z6r0wfww5denyra.webp" alt="Field-to-column mapping for an Elementor form" width="707" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; The Export Profile must be in Published status for submissions to sync automatically. One left in Draft will not push any data to the spreadsheet. Always confirm it is published before sending a test submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens After Someone Submits Your Elementor Form?
&lt;/h2&gt;

&lt;p&gt;Once the connection is published, no further action is needed. When a visitor submits your Elementor form, Form Vibes captures the submission, saves it to your WordPress database, and immediately pushes a new row to the connected Google Spreadsheet. Each Elementor form field appears in the column it was mapped to during setup.&lt;/p&gt;

&lt;p&gt;The submission appears in the sheet within moments. Anyone with access to the spreadsheet can see the new row without logging into WordPress. The spreadsheet keeps updating automatically with every new Elementor submission from that point forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Aren't Elementor Submissions Reaching Google Sheets?
&lt;/h2&gt;

&lt;p&gt;Elementor submissions usually fail to reach Google Sheets because of one of six issues: a form not yet listed in the dropdown, a spreadsheet the connected account can't access, a redirect URI mismatch, a disabled Google API, an unpublished sync setup, or a column mapping that's fallen out of date. Submit a test entry from the frontend of your Elementor form first, then check the causes below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Elementor form is not showing in the Form dropdown.&lt;/strong&gt; Confirm Elementor Pro is active. Then confirm the form has received at least one submission on the frontend since Form Vibes was activated. Form Vibes only lists forms that have already received at least one submission, so a form with zero submissions will not appear in the dropdown yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spreadsheet is not showing in the dropdown.&lt;/strong&gt; Check that the Google account connected in Form Vibes Settings has access to the spreadsheet. If the spreadsheet is owned by a different Google account, share it with the connected account before trying again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect URI mismatch error.&lt;/strong&gt; The redirect URI entered in your Google Cloud Console app settings must match exactly the one Form Vibes provides. Open your Google Cloud Console project, navigate to OAuth credentials, and verify the redirect URI matches the one shown in Form Vibes Settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App verification warning during authentication.&lt;/strong&gt; This is expected for self-configured OAuth apps. Click Show Advanced, then Go to App, and approve all permissions. The warning does not indicate a problem with the connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data is appearing in the wrong columns.&lt;/strong&gt; Open the Export Profile and review the column mapping section. Update the field-to-column pairings if your sheet headers or Elementor form field names have changed since it was last saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Sheets and Drive APIs are not enabled.&lt;/strong&gt; Both APIs must be enabled inside your Google Cloud Console project. If sync silently fails, this is the second most common cause after a redirect URI mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Form Vibes Pro connects Elementor forms to Google Sheets natively, without a third-party automation platform. Set up the Google API credentials once, connect your Google account, configure the sync for your form, and every new submission lands in your spreadsheet automatically.&lt;/p&gt;

&lt;p&gt;Every submission is also saved to your own WordPress database. So if you use other form plugins alongside Elementor, all your submissions live in one dashboard with the same Google Sheets sync, CSV export, filtering, and analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Elementor Pro have a built-in Google Sheets integration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Elementor Pro Forms include native actions for database, email, webhooks, and a handful of marketing tools, but Google Sheets is not one of them. To sync Elementor submissions to Google Sheets you need a plugin like Form Vibes Pro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Form Vibes Pro to connect Elementor forms to Google Sheets?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The Google Sheets integration is a Form Vibes Pro feature. The free version still saves every Elementor submission to your WordPress database and lets you export to CSV on demand. The automatic real-time push to a Google Spreadsheet is the Pro upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I sync multiple Elementor forms to different Google Sheets?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Set up one sync connection per Elementor form, and point each one to its own spreadsheet or its own worksheet inside the same spreadsheet. There is no limit on how many Elementor forms you can connect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the same integration work with my other form plugins?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The Google Sheets sync in Form Vibes Pro works with Contact Form 7, WPForms, Gravity Forms, Ninja Forms, WS Form, Bricks Builder forms, Beaver Builder forms, and Everest Forms. One plugin, one Google connection, all your forms.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>elementorform</category>
      <category>googlesheet</category>
    </item>
    <item>
      <title>WordPress Abilities API + MCP: The Developer's Complete Guide</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Tue, 28 Jul 2026 05:34:11 +0000</pubDate>
      <link>https://dev.to/wpvibes/wordpress-abilities-api-mcp-the-developers-complete-guide-4kpb</link>
      <guid>https://dev.to/wpvibes/wordpress-abilities-api-mcp-the-developers-complete-guide-4kpb</guid>
      <description>&lt;p&gt;AI tools like Claude, Cursor, and ChatGPT can now interact directly with your WordPress site, but only if your site knows how to describe what it can do.&lt;/p&gt;

&lt;p&gt;That's exactly what the WordPress Abilities API solves.&lt;/p&gt;

&lt;p&gt;It lets developers expose WordPress functionality as reusable, self-describing abilities that can be used by PHP, JavaScript, the REST API, and AI tools through the Model Context Protocol (MCP).&lt;/p&gt;

&lt;p&gt;Here's what this guide walks you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Understand what the Abilities API is, in simple words&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create your first custom ability, step by step:&amp;nbsp;&lt;strong&gt;Get Posts by Post Type&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test that ability two different ways&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect it to Claude Desktop using the official MCP Adapter&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build two more real abilities: one that adds missing descriptions to tags and categories, and one that gives you a full site health report.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every piece of code in this guide is real and tested. Let's start from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the WordPress Abilities API?
&lt;/h2&gt;

&lt;p&gt;The WordPress Abilities API is a feature introduced in WordPress 6.9 that gives plugins, themes, and WordPress core a standard way to describe what they can do.&lt;/p&gt;

&lt;p&gt;Before understanding the API, let's first understand what an&amp;nbsp;&lt;strong&gt;ability&lt;/strong&gt;&amp;nbsp;is.&lt;/p&gt;

&lt;p&gt;Think of an ability as a single action that your WordPress site can perform. For example, getting a list of posts, publishing a page, generating a maintenance report, or updating a WooCommerce product. These actions already exist in WordPress, but until now, there wasn't a standard way to describe them so that other plugins, applications, or AI tools could easily discover and use them.&lt;/p&gt;

&lt;p&gt;The Abilities API solves this problem by letting developers register each action as an&amp;nbsp;&lt;strong&gt;ability&lt;/strong&gt;. Along with the code that performs the action, you also describe what the ability does, what information it needs, what it returns, and who is allowed to execute it.&lt;/p&gt;

&lt;p&gt;Every ability includes a few basic pieces of information:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;In our example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;A unique identifier for the ability&lt;/td&gt;
&lt;td&gt;wpvibes/get-posts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description&lt;/td&gt;
&lt;td&gt;Explains what the ability does&lt;/td&gt;
&lt;td&gt;Returns a list of posts filtered by post type, status, and limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;The information the ability needs&lt;/td&gt;
&lt;td&gt;post_type, status, and limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;The information the ability returns&lt;/td&gt;
&lt;td&gt;A list of posts with ID, title, status, date, and link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission&lt;/td&gt;
&lt;td&gt;Who is allowed to run the ability&lt;/td&gt;
&lt;td&gt;Users who can edit posts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, suppose you create an ability called&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;. Instead of another plugin or AI tool needing to understand your plugin's internal code, it simply looks at the ability's definition to learn what inputs it accepts, what output it returns, and whether the current user has permission to run it.&lt;/p&gt;

&lt;p&gt;The actual PHP code that performs the work still lives inside your plugin. The Abilities API simply provides a standard way to describe that functionality so it can be discovered and used consistently across WordPress.&lt;/p&gt;

&lt;p&gt;Now that you understand what an ability is and what information it contains, let's build one from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before writing any code, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress 6.9 or later&lt;/strong&gt;&amp;nbsp;(the Abilities API has been in core since 6.9; 7.0+ is recommended)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A&amp;nbsp;&lt;strong&gt;local development site&lt;/strong&gt;&amp;nbsp;(LocalWP, WordPress Studio, or similar)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP-CLI&lt;/strong&gt;&amp;nbsp;installed and working (needed for testing and for the Claude connection later)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt;&amp;nbsp;installed (the free plan is sufficient for this guide)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A code editor and basic PHP knowledge&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Do everything in this guide on a local or staging site first, never directly on a live production site. The MCP Adapter is still a young project (v0.5.0 at the time of writing) and is evolving quickly. Always check the&amp;nbsp;&lt;a rel="noopener noreferrer nofollow" href="https://github.com/WordPress/mcp-adapter/releases"&gt;official documentation&lt;/a&gt;&amp;nbsp;if something looks different from what's shown here.&lt;/p&gt;

&lt;p&gt;With those ready, there's one more thing to set up before writing any actual ability code: a place for that code to live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Plugin for Your Code
&lt;/h2&gt;

&lt;p&gt;All the code in this guide lives inside one small custom plugin. Here's how to set it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a new folder and file at&amp;nbsp;&lt;code&gt;wp-content/plugins/wpvibes-abilities/wpvibes-abilities.php&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Paste in this basic plugin header:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: WPVibes Abilities Demo
 * Description: Custom abilities for the Abilities API tutorial.
 * Version:     1.0.0
 * Author:      WPVibes
 */&lt;/span&gt;

&lt;span class="c1"&gt;// Exit if accessed directly.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Activate the plugin from&amp;nbsp;&lt;strong&gt;wp-admin → Plugins&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It does nothing yet, we'll add abilities to it one at a time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapxofv68xzaj1itqdpdw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapxofv68xzaj1itqdpdw.webp" alt="Activate the Created Custom Plugin in WordPress" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important: &lt;/strong&gt;Never put ability registration code directly in your theme's &lt;em&gt;functions.php&lt;/em&gt; on a real project. Abilities are functionality, not design, they belong in a plugin, so they keep working even if the theme changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create Your First Ability, "Get Posts by Post Type"
&lt;/h2&gt;

&lt;p&gt;Now that you understand what an ability is, it's time to create one. In this step, we'll build a simple&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;&amp;nbsp;ability that returns a list of posts based on the post type, status, and limit you specify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.1: Register an ability category
&lt;/h3&gt;

&lt;p&gt;Every ability belongs to a category, this helps tools group and display abilities. Add this to&amp;nbsp;&lt;code&gt;wpvibes-abilities.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 1.1: Register a category for our abilities.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_categories_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_ability_category'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_ability_category&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp_register_ability_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPVibes Tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Demo abilities from the WPVibes tutorial.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Categories are registered on their own hook &lt;code&gt;(wp_abilities_api_categories_init)&lt;/code&gt;, while abilities are registered on a different one &lt;code&gt;(wp_abilities_api_init)&lt;/code&gt;. Mixing these two up is one of the most common first-time mistakes. If your ability doesn't appear anywhere, check the hooks first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.2: Register the ability
&lt;/h3&gt;

&lt;p&gt;With the category in place, you can now register your first ability. Register the&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;&amp;nbsp;ability by adding the following code below your category registration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 1.2: Register the "Get Posts" ability.
 */&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_get_posts_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_get_posts_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/get-posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Get Posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns a list of posts filtered by post type, status, and limit.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// What data this ability ACCEPTS.&lt;/span&gt;
            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'post_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The post type to fetch. Example: post, page, product.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'status'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Post status to filter by.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'enum'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'future'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'limit'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'How many posts to return (1-20).'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'minimum'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'maximum'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// What data this ability RETURNS.&lt;/span&gt;
            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'title'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'date'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'link'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="c1"&gt;// The actual logic.&lt;/span&gt;
            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_get_posts_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// Who can run it.&lt;/span&gt;
            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'edit_posts'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="c1"&gt;// Make it visible to AI agents through MCP (used later in this guide).&lt;/span&gt;
            &lt;span class="s1"&gt;'meta'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Understanding the Registration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although the registration looks long, most of it is simply describing the ability.&lt;/p&gt;

&lt;p&gt;The important parts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;'wpvibes/get-posts': the unique name. Always use your own namespace (your plugin or company name) so it never conflicts with other plugins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;input_schema: this is JSON Schema format. It tells any consumer, human, tool, or AI, "you can send me&amp;nbsp;post_type,&amp;nbsp;status, and&amp;nbsp;limit, and here are the rules for each." Notice the&amp;nbsp;enum&amp;nbsp;for status, the AI now knows exactly which values are valid, without guessing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;output_schema: describes what comes back. This lets AI tools understand your response without any extra explanation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;permission_callback: the security gate. Here we require the&amp;nbsp;edit_posts&amp;nbsp;capability, so only users who can edit posts may run this ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;meta.mcp.public =&amp;gt; true: this single line is what will later make the ability visible to Claude through the MCP Adapter.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important: &lt;/strong&gt;Never use&amp;nbsp;&lt;code&gt;'permission_callback' =&amp;gt; '__return_true'&lt;/code&gt;&amp;nbsp;in production code. Every ability must have a real permission check. When AI agents can discover and call your abilities, the permission callback is your main line of defense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.3: Write the execute callback
&lt;/h3&gt;

&lt;p&gt;Registering an ability only describes&amp;nbsp;&lt;strong&gt;what&lt;/strong&gt;&amp;nbsp;it does.&lt;/p&gt;

&lt;p&gt;The actual work happens inside the&amp;nbsp;&lt;strong&gt;execute callback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Whenever someone runs&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;, WordPress calls this function automatically. It receives the validated input from the schema, performs the requested task, and returns the result.&lt;/p&gt;

&lt;p&gt;Add the following function below your ability registration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Step 1.3: The logic that runs when the ability is executed.
 *
 * @param array $input Validated input from the schema.
 * @return array|WP_Error
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_get_posts_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Sanitize inputs (yes, even though the schema already validates them).&lt;/span&gt;
    &lt;span class="nv"&gt;$post_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'post_type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'post_type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$status&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$limit&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Check the post type actually exists on this site.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;post_type_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_type&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'invalid_post_type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'The post type "%s" does not exist on this site.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_type&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'post_type'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'post_status'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'posts_per_page'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Build the response to match our output schema.&lt;/span&gt;
    &lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'date'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Y-m-d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'link'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What This Callback Does&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sanitize inputs anyway.&lt;/strong&gt;&amp;nbsp;The schema validates types, but defense in depth is a WordPress best practice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate important conditions.&lt;/strong&gt;&amp;nbsp;Before querying posts, we check that the requested post type actually exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Perform the requested task.&lt;/strong&gt;&amp;nbsp;We call&amp;nbsp;&lt;code&gt;get_posts()&lt;/code&gt;&amp;nbsp;using the values supplied by the user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Return structured data.&lt;/strong&gt;&amp;nbsp;Instead of returning raw WordPress objects, we build a clean response that matches the&amp;nbsp;&lt;code&gt;output_schema&lt;/code&gt;&amp;nbsp;we defined earlier.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something goes wrong, return an&amp;nbsp;&lt;code&gt;WP_Error&lt;/code&gt;&amp;nbsp;object instead of an empty response. This gives applications and AI tools a clear explanation of what failed.&lt;/p&gt;

&lt;p&gt;Congratulations! You've successfully created and registered your first WordPress ability. Before we connect it to Claude, it's a good idea to verify that everything works correctly. We'll test the ability in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using PHP with WP-CLI&lt;/strong&gt;&amp;nbsp;to confirm the ability executes correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using the official WordPress AI plugin&lt;/strong&gt;&amp;nbsp;to test it through a visual interface.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once both tests are successful, we'll connect the ability to Claude using the MCP Adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Test the Ability with the AI Plugin
&lt;/h2&gt;

&lt;p&gt;WP-CLI is fast and convenient for developers, but sometimes it's easier to test an ability visually.&lt;/p&gt;

&lt;p&gt;The official&amp;nbsp;&lt;a href="https://wordpress.org/plugins/ai/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI plugin&lt;/strong&gt;&lt;/a&gt;&amp;nbsp;in WordPress includes an&amp;nbsp;&lt;strong&gt;Abilities Explorer&lt;/strong&gt;&amp;nbsp;that lets you discover registered abilities, provide input values, and view the response, all from within the WordPress admin area.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Plugins → Add New&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search for&amp;nbsp;&lt;strong&gt;AI&lt;/strong&gt;, then install and activate the plugin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open&amp;nbsp;&lt;strong&gt;Settings → AI&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn on&amp;nbsp;&lt;strong&gt;Enable AI&lt;/strong&gt;&amp;nbsp;at the top of the page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the&amp;nbsp;&lt;strong&gt;Admin Experiments&lt;/strong&gt;&amp;nbsp;tab and enable the&amp;nbsp;&lt;strong&gt;Abilities Explorer&lt;/strong&gt;&amp;nbsp;feature.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfjvury6xvm9tt6kvi3x.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfjvury6xvm9tt6kvi3x.webp" alt="Enable Abilities Explorer Option in AI Plugin WordPress" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Your Ability
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Tools → Abilities Explorer&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find your&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;&amp;nbsp;ability and click&amp;nbsp;&lt;strong&gt;Test&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ypyxfq9zidjl4n59jfz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ypyxfq9zidjl4n59jfz.webp" alt="Check All Abilities and Test Them Using AI Plugin" width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click&amp;nbsp;Invoke Ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The&amp;nbsp;Result&amp;nbsp;section displays the data returned by your ability.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If everything is configured correctly, you should see the same output you received when testing with WP-CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2upu8iij34tr2xvega3l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2upu8iij34tr2xvega3l.webp" alt="Test Ability By Clicking on Invoke Ability" width="710" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Test the Ability with PHP (WP-CLI)
&lt;/h2&gt;

&lt;p&gt;Before connecting any AI, always test the ability directly. If something breaks later during MCP setup, this test tells you whether the problem is in your ability or in the connection.&lt;/p&gt;

&lt;p&gt;The fastest way is WP-CLI. Run this from your site's folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;wp&lt;/span&gt; &lt;span class="k"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'
$ability = wp_get_ability( "wpvibes/get-posts" );
if ( ! $ability ) { echo "Ability not found!"; exit; }
$result = $ability-&amp;gt;execute( array( "post_type" =&amp;gt; "post", "limit" =&amp;gt; 3 ) );
if ( is_wp_error( $result ) ) {
    echo "Error: " . $result-&amp;gt;get_error_message();
} else {
    print_r( $result );
}
'&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, ' --user=[your-user-name].&lt;/p&gt;

&lt;p&gt;You should see an array of your latest 3 posts with their IDs, titles, and links&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frn7s79gn0j2x2jbp3a0t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frn7s79gn0j2x2jbp3a0t.webp" alt="Test Ability Using WP - CLI Command" width="800" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also execute the same ability anywhere in your own plugin or theme code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$ability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes/get-posts'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ability&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$ability&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'post_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;pre&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/pre&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tips:&lt;/strong&gt; Always check the result with&amp;nbsp;&lt;code&gt;is_wp_error( $result )&lt;/code&gt;. A&amp;nbsp;&lt;code&gt;WP_Error&lt;/code&gt;&amp;nbsp;object is truthy in PHP, so a simple&amp;nbsp;&lt;code&gt;if ( $result )&lt;/code&gt;&amp;nbsp;check will pass even when the ability failed. This is one of the most common bugs when working with abilities.&lt;/p&gt;

&lt;p&gt;This "PHP first" test also shows the first real use case: any plugin can now call any other plugin's abilities through one standard interface, with permissions checked automatically. No hard-coded function names, no tight coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect Your Ability to MCP(Claude) with the MCP Adapter
&lt;/h2&gt;

&lt;p&gt;So far, you've created and tested your ability inside WordPress. Now it's time to make it available to an AI assistant.&lt;/p&gt;

&lt;p&gt;To do that, we'll use the&amp;nbsp;&lt;strong&gt;official WordPress MCP Adapter&lt;/strong&gt;. Before setting it up, let's quickly understand how everything fits together.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is the MCP Adapter?
&lt;/h3&gt;

&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;&amp;nbsp;is an open standard that lets AI applications communicate with external systems.&lt;/p&gt;

&lt;p&gt;The&amp;nbsp;&lt;strong&gt;WordPress MCP Adapter&lt;/strong&gt;&amp;nbsp;is an official plugin that acts as a bridge between your WordPress site and AI clients.&lt;/p&gt;

&lt;p&gt;Instead of talking directly to your plugin, Claude communicates with the MCP Adapter. The adapter discovers all abilities you've registered with the Abilities API and exposes them as tools that Claude can use.&lt;/p&gt;

&lt;p&gt;The overall flow looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Desktop
   ↓ Sends the request
MCP Adapter
   ↓ Connects Claude to WordPress
Abilities API
   ↓ Finds and validates the ability
wpvibes/get-posts
   ↓ Runs the callback and returns posts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4.1: Install the MCP Adapter
&lt;/h3&gt;

&lt;p&gt;The adapter isn't in the plugin directory yet. Install it from GitHub:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to the&amp;nbsp;&lt;a href="https://github.com/WordPress/mcp-adapter/releases" rel="noopener noreferrer"&gt;MCP Adapter releases page&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download the latest&amp;nbsp;&lt;a href="http://mcp-adapter.zip" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-adapter.zip&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In wp-admin:&amp;nbsp;&lt;strong&gt;Plugins → Add New → Upload Plugin&lt;/strong&gt;&amp;nbsp;→ upload the zip → Activate&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify it's working with WP-CLI&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wp mcp-adapter list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see at least one server listed, the default one:&amp;nbsp;&lt;code&gt;mcp-adapter-default-server&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; Only abilities with&amp;nbsp;&lt;code&gt;meta.mcp.public =&amp;gt; true&lt;/code&gt;&amp;nbsp;are exposed through the MCP Adapter. This prevents private abilities from becoming available accidentally.&lt;/p&gt;

&lt;p&gt;Step 4.2: Connect Claude Desktop to your site&lt;/p&gt;

&lt;p&gt;Open Claude Desktop's configuration file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt;:&amp;nbsp;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt;:&amp;nbsp;%APPDATA%\Claude\claude_desktop_config.json&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fem646ocn7mlrrsfgz5yf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fem646ocn7mlrrsfgz5yf.webp" alt="Edit Config File in Claude to Connect with Website Using MCP" width="799" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"mcpServers"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"my-wordpress-site"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"args"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"@automattic/mcp-wordpress-remote@latest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="s2"&gt;"env"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"WP_API_URL"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://your-site.com/wp-json/mcp/mcp-adapter-default-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"WP_API_USERNAME"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"your-username"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"WP_API_PASSWORD"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"xxxx xxxx xxxx xxxx"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the following values with your own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP_API_URL&lt;/strong&gt;: Replace&amp;nbsp;your-site.com&amp;nbsp;with your own site's actual address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP_API_USERNAME&lt;/strong&gt;: A WordPress user with permission to execute the ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WP_API_PASSWORD&lt;/strong&gt;: An Application Password for that user.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New to Application Passwords?&amp;nbsp;Jump to the walkthrough at the end of this guide.&lt;/p&gt;

&lt;p&gt;Save the file and fully restart Claude Desktop (quit and reopen, not just close the window).&lt;/p&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Important: &lt;/strong&gt;Claude runs abilities as whichever user's credentials you provided above. Your permission callbacks are checked against that exact user. This is also why permission callbacks matter so much, they're enforced on every single call, even from AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4.3: Run Your Ability From Claude
&lt;/h3&gt;

&lt;p&gt;Open a new chat in Claude Desktop. If everything is configured correctly, your WordPress server appears in the available tools list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tir2w8697i85ghok3si.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tir2w8697i85ghok3si.webp" alt="Website Connected to Claude" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now ask Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Show me my five latest draft posts."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Discover the available abilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select&amp;nbsp;&lt;code&gt;wpvibes/get-posts&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill in the required input values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask for your approval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Execute the ability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Display the returned posts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try a few more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"How many pages does my site have?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"List my latest WooCommerce products" (if you have a&amp;nbsp;&lt;code&gt;product&lt;/code&gt;&amp;nbsp;post type)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Do I have any posts scheduled for the future?"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice something important: you never told Claude how to use your ability. The input schema did all the explaining. This is why we spent time writing good descriptions and schemas in Step 1, they aren't just documentation, they're instructions that AI reads and follows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE (security, read this twice):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Never paste API credentials directly into an AI chat prompt. Credentials belong in the configuration file, where the MCP client handles them, the AI model itself never needs to see them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use HTTPS only for remote connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a dedicated user with only the capabilities your abilities need, don't use your main admin account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start with read-only abilities (like our Get Posts example). Add write or delete abilities only after you're comfortable with how everything works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application Passwords require "pretty" permalinks. If the MCP URL returns a 404, check&amp;nbsp;&lt;strong&gt;Settings → Permalinks&lt;/strong&gt;&amp;nbsp;isn't set to "Plain."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two More Real-World Abilities
&lt;/h2&gt;

&lt;p&gt;Now that you know the full method, here are two more abilities to add to the same plugin. The steps are the same as Example 1, so we'll only explain what's different in each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Add Missing Descriptions to Tags and Categories
&lt;/h3&gt;

&lt;p&gt;Most WordPress sites accumulate tags and categories over time, but descriptions often get skipped when they're created quickly. This matters more than it looks, category and tag archive pages often display that description publicly, and a missing one is a small but real SEO and content-quality gap. This example is also safe to try, since editing a description can always be changed again, nothing gets permanently deleted.&lt;/p&gt;

&lt;p&gt;This example uses two abilities together: one that finds the problem, and one that fixes it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Ability 2a: Find tags/categories with no description
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;What this code does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This ability only looks, it never changes anything. It checks whichever taxonomy you tell it to (tags or categories), goes through every single term in it, and picks out only the ones where description is empty. It hands back a simple list, just the ID, name, and taxonomy for each one. Since nothing gets written or deleted here, Claude will run this one immediately without hesitation, exactly like the read-only &lt;code&gt;get-posts&lt;/code&gt; example from earlier in this guide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_list_terms_without_description_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_list_terms_without_description_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/list-terms-without-description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'List Tags/Categories Without a Description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns a list of tags or categories that currently have no description set.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Which taxonomy to check.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'enum'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_tag'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'default'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'id'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'name'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_list_terms_without_description_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_categories'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_list_terms_without_description_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$taxonomy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'hide_empty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'invalid_taxonomy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'That taxonomy does not exist.'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Only include terms with an empty or missing description.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'id'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'name'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ability 2b: Save a new description for one tag or category&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This ability handles one term at a time, and it needs three pieces of information to run: which term (&lt;code&gt;term_id&lt;/code&gt;), which taxonomy it belongs to, and the new description text to save. It first double-checks that the term actually exists, so it doesn't fail halfway through with a confusing error. Then it hands the new description off to WordPress's own built-in function, &lt;code&gt;wp_update_term()&lt;/code&gt;which saves just that one field. The term's name, its slug, everything else about it stays exactly as it was. Because this only changes a piece of text, and text can always be changed again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_update_term_description_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_update_term_description_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/update-term-description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Update Tag/Category Description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Sets or updates the description for a specific tag or category.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'term_id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The ID of the tag or category to update.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Which taxonomy this term belongs to.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'enum'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_tag'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The new description text to set.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'required'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'term_id'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'new_description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_update_term_description_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_categories'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_update_term_description_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$term_id&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;absint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$taxonomy&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_textarea_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$taxonomy&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'term_not_found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'That tag or category does not exist.'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_update_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'term_id'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'new_description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How you'd actually use it&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Ask Claude: "Which of my categories and tags don't have a description?" Claude runs Ability 2a and shows you the list.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask Claude: "Write good descriptions for those and add them." Claude writes a description for each one itself, then runs Ability 2b once per category/tag to save it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💡&lt;br&gt;
&lt;strong&gt;Tips:&lt;/strong&gt; This is a safe one to try for real. If a description comes out wrong, just ask Claude to write it again with different wording. Nothing here is permanent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Site Maintenance Report (read-only, admin-only)
&lt;/h3&gt;

&lt;p&gt;Imagine asking Claude every Monday morning: "Give me my site's status report," and getting back pending updates, moderation queue, and health flags in one answer, instead of clicking through five admin screens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_register_maintenance_report_ability'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_register_maintenance_report_ability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'wpvibes/maintenance-report'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Site Maintenance Report'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns a status report: versions, pending updates, comment queues, and basic health flags.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-abilities'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes-tools'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// No input needed — an empty object schema.&lt;/span&gt;
            &lt;span class="s1"&gt;'input_schema'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;stdClass&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'output_schema'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'environment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'updates'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'content'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="s1"&gt;'health'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpvibes_maintenance_report_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// Stricter permission: admins only.&lt;/span&gt;
            &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_options'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'mcp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpvibes_maintenance_report_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Update counts need these includes outside of wp-admin context.&lt;/span&gt;
    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/update.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/plugin.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$update_data&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_update_data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$comment_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_count_comments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$theme&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_theme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'environment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'wordpress_version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_bloginfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'php_version'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'active_theme'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$theme&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Name'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$theme&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Version'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'site_url'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;home_url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'updates'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'total_pending'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'plugin_updates'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'plugins'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'theme_updates'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'themes'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'core_update'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$update_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'wordpress'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'comments_awaiting_moderation'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$comment_counts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;moderated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'spam_comments_in_queue'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$comment_counts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;spam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'draft_posts'&lt;/span&gt;                  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;wp_count_posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'scheduled_posts'&lt;/span&gt;              &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;wp_count_posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'health'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'https_active'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;is_ssl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'debug_mode_on'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_DEBUG'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;WP_DEBUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's new here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stricter permission&lt;/strong&gt;:&amp;nbsp;&lt;code&gt;manage_options&lt;/code&gt;&amp;nbsp;(admins only). Version and update information is reconnaissance data; never expose it to low-privilege users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured output&lt;/strong&gt;: we return grouped data (&lt;code&gt;environment&lt;/code&gt;,&amp;nbsp;&lt;code&gt;updates&lt;/code&gt;,&amp;nbsp;&lt;code&gt;content&lt;/code&gt;,&amp;nbsp;&lt;code&gt;health&lt;/code&gt;), not a pre-formatted text report. This gives AI clients the flexibility to present the information however they choose&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep callbacks fast&lt;/strong&gt;: we deliberately skip expensive checks like folder sizes or database size. AI agents may call abilities often; a slow ability makes every conversation slow.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once registered, you can ask Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Give me my site's maintenance report and tell me what needs attention."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude calls the ability, analyzes the returned data, and explains what requires attention, for example, outdated plugins, a growing spam queue, or debug mode being enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've now built your first WordPress ability, tested it, and connected it to Claude using the official MCP Adapter. From here, you can apply the same pattern to expose any WordPress functionality as a secure, reusable ability that works seamlessly with AI tools&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ on WordPress Abilities API
&lt;/h2&gt;

&lt;p&gt;What is the WordPress Abilities API?&lt;/p&gt;

&lt;p&gt;It's a core WordPress feature, added in version 6.9, that lets your site describe its capabilities in a standard format. Plugins, external tools, and AI agents can then discover and safely use those capabilities without custom, one-off integration code.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Do I need to understand MCP to use the Abilities API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. You can register and use abilities entirely in PHP, or expose them over the REST API, without touching MCP at all. MCP is only needed if you specifically want an AI agent like Claude to discover and call your ability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use an ability without connecting it to any AI tool?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Abilities work from PHP, the REST API, and JavaScript, independent of AI. Many abilities are built purely so other plugins or your own code can call them through one consistent, permission-checked interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the MCP Adapter part of WordPress core?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, it's a separate official plugin. WordPress deliberately keeps it outside core so abilities remain protocol-independent. If MCP is ever replaced by something newer, your registered abilities keep working unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between an ability and a REST API endpoint?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A REST endpoint is one specific way to reach functionality over HTTP. An ability is broader: register it once, and it becomes callable from PHP, REST, JavaScript, and MCP, all from a single definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do abilities only work with Claude, or other AI tools too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any MCP-aware AI client works, including Claude Desktop, Claude Code, Cursor, and VS Code. The ability itself doesn't know or care which AI is calling it, that's the whole point of the standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.wordpress.org/apis/abilities-api/" rel="noopener noreferrer"&gt;Abilities API: official Common APIs Handbook entry&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/WordPress/mcp-adapter" rel="noopener noreferrer"&gt;MCP Adapter on GitHub&lt;/a&gt;: docs, releases, and transport guides&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol specification&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Create a WordPress Application Password
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to&amp;nbsp;&lt;strong&gt;Users → Profile&lt;/strong&gt;&amp;nbsp;(or&amp;nbsp;&lt;strong&gt;Users → All Users&lt;/strong&gt;&amp;nbsp;→ click your name).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scroll to&amp;nbsp;&lt;strong&gt;Application Passwords&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Type a name for it, something like "Claude MCP," so you remember what it's for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click&amp;nbsp;&lt;strong&gt;Add New Application Password&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the generated password immediately. WordPress shows it only once. If you lose it, you'll need to generate a new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use this password, not your regular login password, wherever the guide asks for&amp;nbsp;&lt;code&gt;WP_API_PASSWORD&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshggcrp170xubjrzwstv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshggcrp170xubjrzwstv.webp" alt="Create Application Password in WordPress" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>mcp</category>
      <category>abilitiesapi</category>
      <category>claude</category>
    </item>
    <item>
      <title>Complete Guide to WordPress Patterns: Synced, Unsynced, and Partial Sync</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:31:01 +0000</pubDate>
      <link>https://dev.to/wpvibes/complete-guide-to-wordpress-patterns-synced-unsynced-and-partial-sync-1edd</link>
      <guid>https://dev.to/wpvibes/complete-guide-to-wordpress-patterns-synced-unsynced-and-partial-sync-1edd</guid>
      <description>&lt;p&gt;If you've ever wondered what the difference is between synced and unsynced WordPress patterns, the short answer is simple: synced patterns update everywhere, while unsynced patterns only affect the copy you're editing.&lt;/p&gt;

&lt;p&gt;But that's only part of the story. WordPress also supports &lt;strong&gt;Pattern Overrides&lt;/strong&gt;, a feature that enables &lt;strong&gt;partial sync&lt;/strong&gt;. It lets you keep a pattern's design and structure synced across your site while allowing specific content, such as text or images, to be edited independently in each instance.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how synced and unsynced patterns work, how Pattern Overrides enable partial sync, when to use each approach, and how to build reusable patterns step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Patterns in WordPress?
&lt;/h2&gt;

&lt;p&gt;A pattern is a saved group of blocks you can insert anywhere on your site without rebuilding it from scratch. Think of it as a template for a recurring piece of content: a call-to-action, a pricing table, or a notice box.&lt;/p&gt;

&lt;p&gt;Every pattern you create in WordPress falls into one of two categories, and WordPress asks you to choose which one the moment you save it. Getting that choice right up front saves you from a rebuild later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Synced Pattern in WordPress?
&lt;/h2&gt;

&lt;p&gt;A synced pattern stays identical everywhere it's used. Edit it in one place, and that edit applies to every instance across your site automatically. This is the feature formerly known as a reusable block, renamed to synced patterns starting in WordPress 6.3, and it behaves the same way it always did: one source, many locations, always matching.&lt;/p&gt;

&lt;p&gt;Synced patterns make sense for anything that needs to stay consistent no matter where it appears: business hours, a newsletter signup bar, a standard disclaimer, or a footer contact block. Update the phone number once, and it's correct everywhere it's used.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Create a Synced Pattern?
&lt;/h2&gt;

&lt;p&gt;Let's build a real example together: a CTA box, the kind you'd use to promote a plugin or product at the end of a post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start a new pattern
&lt;/h3&gt;

&lt;p&gt;If you're starting fresh instead, go through the Appearance menu:&lt;/p&gt;

&lt;p&gt;If you use a block theme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to Appearance &amp;gt; Editor &amp;gt; Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Add pattern, then Create pattern.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use a classic theme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to Appearance &amp;gt; Design &amp;gt; Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Add pattern, then Create pattern.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Name it and keep Synced on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Type a name, like "Plugin CTA - Standard."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose a category if you want one. This is optional.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep the Synced option turned on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Create.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wqvugj3atkr2ic30dxy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wqvugj3atkr2ic30dxy.webp" alt="Create Synced Pattern in WordPress" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add blocks and design your pattern
&lt;/h3&gt;

&lt;p&gt;WordPress opens a blank canvas. This is where you actually build the pattern, and you can add any block you like here, not just the ones in this example.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a Group block. This holds the CTA together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add your product logo, then insert a Heading block with a clear CTA, such as “Choose the Right Plan for You.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Paragraph block right below it for a short description, like "Manage everything from one dashboard."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a Button block below that, with text like "View Pricing" and "Download Free", and a link to a product page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the Group block, open Styles in the right panel, and add a background color and some padding, so it stands out as a promotional box.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9cuh369945ffl1basu7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9cuh369945ffl1basu7.webp" alt="Design CTA with Synced WordPress Pattern" width="799" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Save the pattern
&lt;/h3&gt;

&lt;p&gt;Click Save in the top right corner. Your CTA pattern now exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Add it to a page so you can see it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open any post or page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the + button (block inserter) in the top left.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the Patterns tab, then My Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click your CTA pattern. It gets added at your cursor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save or Update. Your CTA now shows on the page, with a small purple icon marking it as synced.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9uwe1e5emavlvdd6o3n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9uwe1e5emavlvdd6o3n.webp" alt="Add Synced Pattern on WordPress Page" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: See the sync in action
&lt;/h3&gt;

&lt;p&gt;Open your pattern again from &lt;strong&gt;Appearance &amp;gt; Editor &amp;gt; My Patterns&lt;/strong&gt;, and change the background color or button style.&lt;/p&gt;

&lt;p&gt;Go back to the page you just updated. The change has appeared there too, automatically, with no extra step. That's what "synced" actually means in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Pattern Overrides (Partial Sync)?
&lt;/h2&gt;

&lt;p&gt;Here's the catch with a synced pattern: if you insert the same CTA on ten different posts, every single one promotes the exact same product, since editing it anywhere changes it everywhere. But usually, you want the box design locked while the heading, description, and button change depending on what you're promoting. That's exactly what overrides solve.&lt;/p&gt;

&lt;p&gt;An override marks one specific block inside a synced pattern as editable per instance. Everything else, the background color and padding, stays locked and synced. Only the content in the marked block can change from post to post.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Do You Enable Overrides on a Synced Pattern?
&lt;/h3&gt;

&lt;p&gt;Let's make the button inside our CTA editable on each page, both its text and its link, while keeping the box design locked everywhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open your CTA pattern for editing (&lt;strong&gt;Appearance &amp;gt; Editor &amp;gt; My Patterns&lt;/strong&gt;, then click on it).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the Button block inside it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the block settings panel, open the Advanced section.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn on Enable overrides, and give it a name like "CTA Button."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mn48n3dqwc59s1zzlh4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4mn48n3dqwc59s1zzlh4.webp" alt="Enable Pattern Override in WordPress" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now open any page where you've added this CTA. Click directly into the button and change its text, then update its link to point to a different product. Only that page's button changes. The background color and padding stay exactly the same everywhere else, since only the button was marked as an override, not the styling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an Unsynced Pattern in WordPress?
&lt;/h2&gt;

&lt;p&gt;An unsynced pattern is a starting point, not a shared source. Once you insert it into a page or post, its text, links, and images are ready to edit right away, no extra steps needed. Editing one instance never affects any other place you've used the same pattern.&lt;/p&gt;

&lt;p&gt;Unsynced patterns are the better choice when you want a consistent layout but different content each time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take an info box&lt;/strong&gt;: you might use the same background color and border across dozens of posts, but each one flags something different, a warning on one post, a quick tip on another, a "last updated" note on a third. Editing one shouldn't change the message on any of the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Create an Unsynced Pattern?
&lt;/h2&gt;

&lt;p&gt;The steps are almost identical to creating a synced pattern. This time, let's build an info box, since that's the example where each instance usually needs to say something different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start a new pattern
&lt;/h3&gt;

&lt;p&gt;Same as before, either from a page directly or through the Appearance menu (see Step 1 in the synced section above).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Name it and turn Synced off
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Type a name, like "Info Box - Flexible."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose a category if you want one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn the Synced option off this time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Create.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkyg3mgebtnq26y0d8ti.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkyg3mgebtnq26y0d8ti.webp" alt="Create Unsynced Pattern in WordPress" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add blocks and design your pattern
&lt;/h3&gt;

&lt;p&gt;WordPress opens a blank canvas, same as before.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a Group block. This holds everything together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside the Group block, add a icon and Paragraph block, and type your message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the Group block, open Styles in the right panel, and set a background color, some padding, and a left border. This is what makes it look like a proper info box instead of a plain paragraph.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ym2l427krdpucpg0vla.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ym2l427krdpucpg0vla.webp" alt="Create Info Box with Unsynced Pattern WordPress" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Save the pattern
&lt;/h3&gt;

&lt;p&gt;Click Save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Add it to a page so you can see it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open any post or page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the + button (block inserter).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Patterns, then My Patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click your info box pattern to insert it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save or Update. It shows up on the page. There's no purple icon this time, since it isn't synced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click directly into the message text and change it. This works immediately, no extra step.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06dopkprj06n61q0lfep.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06dopkprj06n61q0lfep.webp" alt="Add Unsynced Pattern on Page" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Add it to a second page with different text
&lt;/h3&gt;

&lt;p&gt;Repeat the same steps on another page, and type a completely different message. Both pages now show the same design, but different text, and neither page affects the other.&lt;/p&gt;

&lt;p&gt;Right now, this info box just shows a message. But say one page needs a "Learn More" link added right below the text, without changing the box everywhere else. That means adding a whole new block, not just changing text, so simple editing won't do it. Here's where Detach comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Detach a Pattern?
&lt;/h2&gt;

&lt;p&gt;Detach unlocks full editing on one specific instance of a pattern, so you can add or remove blocks, something that plain text edits and Overrides can't do.&lt;/p&gt;

&lt;p&gt;Back to our info box example: you want to add a "Learn More" button right below the message, on just one page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Select the inserted info box pattern on that page.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the three-dot menu in the block toolbar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose Detach.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoepsyewgrajjs60r8eb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoepsyewgrajjs60r8eb.webp" alt="Detach Pattern in WordPress" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Add a new Button block right below the message, type "Learn More," and link it wherever it should go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one page now shows the message plus a button. Every other page using this same info box pattern still shows just the message, completely untouched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhapuf0rkag649pkyicxf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhapuf0rkag649pkyicxf.webp" alt="Added Button After Detaching Pattern" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works the same way on a synced pattern, with one difference worth knowing. On an unsynced pattern like this info box, Detach is only needed for structural changes like this, since text was already editable without it.&lt;/p&gt;

&lt;p&gt;On a synced pattern like our CTA, nothing is editable directly at all, so Detach breaks the entire instance free at once, styling included, not just its content.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the Real Difference Between Synced and Unsynced Patterns?
&lt;/h2&gt;

&lt;p&gt;The core difference between synced and unsynced patterns in WordPress comes down to one question: does an edit apply everywhere, or only to the instance you're editing? Synced patterns apply everywhere. Unsynced patterns apply only locally.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Synced Pattern&lt;/th&gt;
&lt;th&gt;Unsynced Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editing behavior&lt;/td&gt;
&lt;td&gt;Updates every instance at once&lt;/td&gt;
&lt;td&gt;Updates only the instance you're editing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Content that must stay identical (footers, CTAs, disclaimers)&lt;/td&gt;
&lt;td&gt;Reusable layouts with changing content (info boxes, testimonials)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text edits&lt;/td&gt;
&lt;td&gt;Need Overrides enabled first&lt;/td&gt;
&lt;td&gt;Work immediately, no extra step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block-level changes (add/remove)&lt;/td&gt;
&lt;td&gt;Need Detach&lt;/td&gt;
&lt;td&gt;Need Detach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library icon&lt;/td&gt;
&lt;td&gt;Purple icon in the pattern list&lt;/td&gt;
&lt;td&gt;No special icon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Shared content&lt;/td&gt;
&lt;td&gt;Reusable layouts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When Should You Use Synced vs Unsynced Patterns?
&lt;/h2&gt;

&lt;p&gt;Use a synced pattern when every page should show the exact same thing, like our CTA box. Use an unsynced pattern when each page needs different content in the same design, like our info box.&lt;/p&gt;

&lt;p&gt;If most of a pattern should stay locked but one small piece needs to change, reach for Overrides instead. If you need to add or remove a block on just one page, use Detach, on either pattern type.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs on Synced and Unsynced WordPress Patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a synced and unsynced pattern in WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A synced pattern updates every instance across your site when you edit it. An unsynced pattern stays independent, so edits only affect the one instance you're working on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I convert an unsynced pattern to a synced pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not directly, but Duplicate gives you a shortcut, it asks the sync setting again on the copy. Or build a new pattern from scratch and choose Synced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do pattern overrides work in WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Overrides let you mark one block inside a synced pattern as editable per instance. The rest of the pattern, including layout and styling, stays synced and locked everywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to detach an unsynced pattern just to edit its text?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Text, links, and images inside an unsynced pattern are editable the moment you insert it. Detach is only needed if you want to add or remove blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a real-world example of when to use a synced pattern versus an unsynced pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A CTA promoting the same offer everywhere is a good synced pattern. An info box with a different note on each post is a better fit for an unsynced pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Synced patterns keep things consistent, unsynced patterns keep things flexible, overrides let you edit content while keeping design locked, and Detach gives you full control over one instance when you truly need it. The CTA box and info box built above are small examples, but the same approach applies to nearly any repeating component on a WordPress site.&lt;/p&gt;

&lt;p&gt;Once reusable components like this are in place, the next challenge is usually keeping the surrounding content accurate over time, since sections can quietly go stale long after a post is published. That's the exact problem &lt;a href="https://wpvibes.com/plugin/content-lifecycle-manager/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=plugin_landing&amp;amp;utm_content=content_lifecycle_manager"&gt;Content Lifecycle Manager&lt;/a&gt; is built to solve, helping you track what needs a second look and when, so your site doesn't just look consistent, it stays accurate too.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>wordpresspatterns</category>
      <category>gutenberg</category>
      <category>reusableblocks</category>
    </item>
    <item>
      <title>Your Form Submissions Don't Have to Stay Locked in a Dashboard</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Wed, 22 Jul 2026 07:41:02 +0000</pubDate>
      <link>https://dev.to/wpvibes/your-form-submissions-dont-have-to-stay-locked-in-a-dashboard-5909</link>
      <guid>https://dev.to/wpvibes/your-form-submissions-dont-have-to-stay-locked-in-a-dashboard-5909</guid>
      <description>&lt;p&gt;Form submissions have a default destiny: land in a database, sit in an admin panel, get checked manually whenever someone remembers to look. That's fine for basic contact form enquiries. It's a missed opportunity for anything where the data itself has value beyond just being stored.&lt;/p&gt;

&lt;p&gt;A few situations where this shows up clearly:&lt;/p&gt;

&lt;p&gt;Someone's running event registrations and wants a live sign-up counter on the landing page - the kind that builds its own social proof as the number climbs. Someone else is collecting business or member listings through a form and needs a searchable directory, without spinning up a whole separate directory plugin just for that. Or a team needs to check incoming submissions without anyone getting a WordPress login.&lt;/p&gt;

&lt;p&gt;All three of these are the same underlying need: taking form data that's normally backend-only and putting it somewhere visible — on an actual page, updating itself, with zero manual maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do you actually put form data on a page?
&lt;/h2&gt;

&lt;p&gt;Normally, this requires custom development — querying a form plugin's database tables, building a template, handling pagination and filtering yourself. Not a huge lift for a developer, but not something most site owners can do themselves either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://formvibes.com/pricing/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=pricing&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt;&lt;/strong&gt; has a feature called Data Profiles that handles this without any of that. Point it at a form, configure what to show and how to filter it, and it generates a shortcode (or an Elementor widget, if that's your builder) that renders live data anywhere on a page.&lt;/p&gt;

&lt;p&gt;Two display types cover most use cases:&lt;br&gt;
&lt;strong&gt;Table&lt;/strong&gt; — rows and columns, configurable per-column visibility and labels, optional pagination, and an optional live search bar so visitors can find a specific entry themselves rather than scrolling through everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counter&lt;/strong&gt; — a single number reflecting how many entries match a given filter, with an optional message wrapped around it using dynamic tags (entry count, form name, plugin name). This is the registration-counter use case in its simplest form.&lt;/p&gt;

&lt;p&gt;Both pull directly from the Form Vibes database at render time — no caching lag, no manual refresh. A new submission shows up on the page the same way it shows up in the admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering matters more than it sounds like it would
&lt;/h2&gt;

&lt;p&gt;The actual configuration worth paying attention to is the filtering layer, since that's what turns "dump all submissions on a page" into something genuinely useful.&lt;/p&gt;

&lt;p&gt;Filters work off form field values with a condition operator (contains, equals, etc.), and multiple conditions can be stacked with AND/OR logic. So a directory page can be scoped to "Category equals Web Design AND Status equals Approved" rather than showing every submission that's ever come in, including test entries and rejected listings.&lt;/p&gt;

&lt;p&gt;For a table specifically, there's also a separate row-expand option — click a row, see the full submission detail without needing every field crammed into the visible columns. Useful when a form has more fields than reasonably fit in a table view but the extra detail is still worth surfacing on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internal-team-access angle
&lt;/h2&gt;

&lt;p&gt;One use case worth calling out specifically: giving a team visibility into submissions without WordPress login access at all.&lt;/p&gt;

&lt;p&gt;Combine a Data Profile table with WordPress's built-in page password feature, and you get a clean, searchable, password-gated view of submission data that anyone with the password can check — no WordPress account, no admin panel exposure, nothing beyond what's on that one page.&lt;/p&gt;

&lt;p&gt;This is a genuinely underused pattern. Client-facing or team-facing dashboards usually get built as custom internal tools; this gets most of the value from two built-in features stacked together. There's a full walkthrough of this specific setup here: &lt;strong&gt;&lt;a href="https://formvibes.com/tutorial/share-form-submissions-with-clients/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;sharing form submissions with clients&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation
&lt;/h2&gt;

&lt;p&gt;Data Profiles is Pro-only - not available on Form Vibes' free tier. Worth knowing before building a workflow around it, though for anything client-facing or public-facing, the alternative is usually custom development anyway, so the comparison point isn't really "&lt;strong&gt;&lt;a href="https://formvibes.com/general/form-vibes-free-vs-pro/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog_content&amp;amp;utm_content=form_vibes"&gt;free vs paid&lt;/a&gt;&lt;/strong&gt;," it's "paid feature vs hiring someone to build the same thing from scratch."&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formsubmissions</category>
      <category>formvibes</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Stop Exporting Everything Just to Filter It Afterward</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:10:16 +0000</pubDate>
      <link>https://dev.to/wpvibes/stop-exporting-everything-just-to-filter-it-afterward-3g5</link>
      <guid>https://dev.to/wpvibes/stop-exporting-everything-just-to-filter-it-afterward-3g5</guid>
      <description>&lt;p&gt;The default workflow for getting specific form submission data out of WordPress looks like this: export everything, open the spreadsheet, delete the rows you don't need, keep what you actually wanted.&lt;/p&gt;

&lt;p&gt;It works. But it means doing manual cleanup every single time. And if the same export is needed again next week — same form, same filters, same fields - the entire process repeats from scratch.&lt;/p&gt;

&lt;p&gt;There's a better approach: filter before exporting, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two export options in Form Vibes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/form-vibes/" rel="noopener noreferrer"&gt;Form Vibes&lt;/a&gt; has two ways to get submission data out of WordPress, and understanding which one fits which situation saves a lot of repetitive work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Export&lt;/strong&gt; is free and available on every plan. Click the button in the submissions toolbar, and all entries for the currently selected form download as a CSV immediately. No configuration, no setup. Up to 1,000 entries per export. Good for one-off backups or full dataset downloads where you genuinely need everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export Profiles&lt;/strong&gt; are a Pro feature and solve a different problem: recurring exports where the same configuration gets used again and again. Instead of downloading everything and filtering afterward, an Export Profile lets you define exactly what you want - which form, which date range, which field values to filter by, which columns to include - save that configuration once, and re-run it in one click any time you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Export Profiles actually work
&lt;/h2&gt;

&lt;p&gt;The setup only needs to happen once per report or workflow. When creating a new Export Profile, four things get configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profile Type
&lt;/h2&gt;

&lt;p&gt;Choose between two output options:&lt;br&gt;
&lt;strong&gt;Local Download&lt;/strong&gt; — exports submissions as a CSV file that downloads directly to your computer on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Sheets&lt;/strong&gt; — connects to a Google Spreadsheet via OAuth and pushes every new submission automatically in real time. Once set up, no manual export step is needed at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Source
&lt;/h2&gt;

&lt;p&gt;This is where you define exactly which submissions to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Form — select which form to pull submissions from&lt;/li&gt;
&lt;li&gt;
Date Range — choose a preset (Last Week, Last Month, Last Quarter) or set a custom window&lt;/li&gt;
&lt;li&gt;
Status — filter submissions by their current status: Read, Unread, or Spam&lt;/li&gt;
&lt;li&gt;
Filter Conditions — the most useful part. Add conditions based on any field value: select a field, choose a condition type (equals, contains, not equal, not contain), enter the value, and stack multiple conditions with AND or OR logic between them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So something like "Status equals Read AND Country equals USA AND Registration Type contains Paid" runs automatically every time the profile is used — no manual spreadsheet filtering afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fields&lt;/strong&gt;&lt;br&gt;
Choose which columns to include in the exported file. Toggle off anything that isn't needed for that specific export. A finance export might include Name, Email, Amount, and Date. A marketing export from the same form might include Name, Email, and Source only. Two profiles, two field selections, both in one click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profile Name&lt;/strong&gt;&lt;br&gt;
Worth being specific - "Monthly Contact Form Leads" or "Weekly Paid Registrations — USA" is far more useful than "Export 1" when the profiles list grows.&lt;/p&gt;

&lt;p&gt;Once published, the profile appears in the Export Profiles list. Click Export Data, confirm, and the file downloads based on saved settings. An export history inside each profile keeps a record of every previously generated file so past exports can be re-downloaded without running the export again.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this actually matters
&lt;/h2&gt;

&lt;p&gt;The filter-before-export approach pays off most noticeably in two situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recurring reports&lt;/strong&gt;. If the same subset of data gets exported on a regular schedule — weekly leads, monthly paid registrations, quarterly support requests — setting it up once as a saved profile and running it in one click each time removes the repetitive configuration work entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different subsets for different people&lt;/strong&gt;. Sales needs qualified leads. Marketing needs newsletter signups. Finance needs paid registrations. Support needs open requests. One form, four different export configurations, all saved and runnable independently without any of them having to touch settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation
&lt;/h2&gt;

&lt;p&gt;Export Profiles are a &lt;a href="https://formvibes.com/pricing/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=pricing&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt; feature. The free Quick Export covers one-off full exports cleanly, but saved configurations with field-level filtering require Pro. Worth knowing upfront before building a workflow around it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why Form Submission Analytics Matters More Than a List of Entries</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 02 Jul 2026 05:46:49 +0000</pubDate>
      <link>https://dev.to/wpvibes/why-form-submission-analytics-matters-more-than-a-list-of-entries-35m7</link>
      <guid>https://dev.to/wpvibes/why-form-submission-analytics-matters-more-than-a-list-of-entries-35m7</guid>
      <description>&lt;p&gt;A contact form goes live. Submissions start coming in. Everything looks fine.&lt;/p&gt;

&lt;p&gt;But ask a more specific question - is this form doing better or worse than it was three months ago? - and most people genuinely can't answer it without manually counting entries across two different date ranges. A flat list of submissions tells you that something happened. It doesn't tell you the shape of what's happening over time.&lt;/p&gt;

&lt;p&gt;This is a gap in almost every form plugin's native entry view: storage without trend data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What trend data actually catches that a list doesn't
&lt;/h2&gt;

&lt;p&gt;There are three patterns worth knowing how to spot, because each one means something different and calls for a different response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sudden drop&lt;/strong&gt;. Steady submissions, then a sharp fall in a single week or month. This usually means something broke - a plugin update touched the form, the page moved to a new URL, or a layout change pushed the form out of place. The fastest way to confirm: submit a test entry yourself. If it goes through cleanly, the drop is likely traffic-related, not a form issue. If it doesn't submit correctly, that's the problem, found in under a minute instead of days of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A slow decline&lt;/strong&gt;. No single bad week, just a gradual downward trend over weeks or months. This usually points to something upstream of the form itself - less traffic reaching the page, content on the page going stale, or something about the form discouraging completion. Looking at the trend week by week, rather than day by day, makes it easier to pin down roughly when the decline started, which helps connect it to whatever changed around that time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An unrepeated spike&lt;/strong&gt;. One period - a week, a month - significantly outperforms the baseline, then things return to normal. This is actually good news if you catch it: something drove a burst of traffic to the form. A campaign, a post that got shared, a backlink. Drilling into daily granularity for that window usually reveals exactly which day or days it happened, which makes it possible to figure out what worked and try to repeat it.&lt;/p&gt;

&lt;p&gt;None of these three patterns are visible from a list of individual entries. They only show up when submissions are aggregated over time and visualized.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do you actually check this?
&lt;/h2&gt;

&lt;p&gt;Most form plugins don't give you a trend view out of the box - just the entry list described earlier. Getting an aggregated view usually means exporting submissions and building the chart manually, or installing something that does the aggregation for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/form-vibes/" rel="noopener noreferrer"&gt;Form Vibes&lt;/a&gt; is a free WordPress plugin that captures submissions into the database and includes a built-in Analytics dashboard for exactly this. It works across every supported form plugin - Contact Form 7, WPForms, Gravity Forms, Elementor, and others with no separate setup per form, and capture starts automatically the moment the plugin is active.&lt;/p&gt;

&lt;p&gt;Analytics lives in two places:&lt;br&gt;
The Analytics page, found at &lt;strong&gt;Form Vibes → Analytics&lt;/strong&gt;, has three controls: a date range picker, a form dropdown (showing total entry count for that form at a glance), and a time filter to group submissions By Day, By Week, or By Month. &lt;/p&gt;

&lt;p&gt;Two chart types are available - bar and line, switchable from the same toolbar, and the last view (date range, form, chart type) is remembered automatically the next time the page is opened.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tqav1nmqao5xeb4xbjl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tqav1nmqao5xeb4xbjl.webp" alt="Form Vibes Analytics" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dashboard widget&lt;/strong&gt;, enabled from &lt;strong&gt;Form Vibes → Settings&lt;/strong&gt;, sits on the main WordPress admin screen - the first page seen on login. It shows a form dropdown, date range, time filter, a line chart, a total submission count, and an expandable breakdown of entries per form, all without navigating away from the dashboard.&lt;/p&gt;

&lt;p&gt;Both are available on the free plan with no Pro upgrade required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftefn6yprx9oixalkdej0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftefn6yprx9oixalkdej0.webp" alt="Form Vibes Dashboard Widget" width="370" height="695"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two chart types, two different questions
&lt;/h2&gt;

&lt;p&gt;Bar charts and line charts of the same data answer slightly different questions, and it's worth being deliberate about which one to reach for.&lt;/p&gt;

&lt;p&gt;A bar chart is for period-to-period comparison - was this month better than last month, did a specific campaign move the number. Each bar is a discrete answer to "how many in this window."&lt;/p&gt;

&lt;p&gt;A line chart is for direction - is the overall trajectory up, down, or flat. A line makes a slow decline visible in a way a row of bars sometimes doesn't, because the eye naturally reads a slope.&lt;/p&gt;

&lt;p&gt;Switching between daily, weekly, and monthly granularity changes what each chart type is good for too. Monthly view is for spotting the big picture across a quarter or more. Daily view is for pinpointing exactly when something changed once you already suspect something did.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation worth knowing upfront
&lt;/h2&gt;

&lt;p&gt;Submission analytics measures submissions, not visits. It will tell you that 40 people filled out the form this month versus 60 last month. It will not tell you how many people loaded the page and left without submitting - that's a different metric (conversion rate) that requires pairing form analytics with actual page-view data from something like GA4.&lt;/p&gt;

&lt;p&gt;This matters because a drop in submissions has two very different possible causes: fewer people are reaching the form, or the same number of people are reaching it but fewer are completing it. Submission-only analytics can't distinguish between those two on its own - it just tells you the volume changed and roughly when, which is still the necessary first signal even without the full picture.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formanalytics</category>
      <category>wordpressforms</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Two Ways to Give a Client Access to Their Form Data Without a WordPress Login</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:30:06 +0000</pubDate>
      <link>https://dev.to/wpvibes/two-ways-to-give-a-client-access-to-their-form-data-without-a-wordpress-login-29e1</link>
      <guid>https://dev.to/wpvibes/two-ways-to-give-a-client-access-to-their-form-data-without-a-wordpress-login-29e1</guid>
      <description>&lt;p&gt;A client asks to see their form submissions. The solution is usually one of two things: create them a WordPress account, or export the data and email it over.&lt;/p&gt;

&lt;p&gt;Both have real downsides. A WordPress login even on a restricted role exposes the dashboard, plugin notices, update alerts, and a dozen things that have nothing to do with their actual request. Non-technical clients end up generating support questions just from being inside an interface they don't need to be in.&lt;/p&gt;

&lt;p&gt;Manual exports solve the access problem but create a different one: every time a new submission comes in, you're back to exporting and sending a file. It's not hard, but it's a task that never really ends, and "is this the latest version?" becomes a recurring question on both sides.&lt;/p&gt;

&lt;p&gt;There's a cleaner way to handle this that avoids both problems entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1 - A dedicated page the client can bookmark
&lt;/h2&gt;

&lt;p&gt;Instead of WordPress access, give the client a single URL that shows only their submissions, live, with no login required.&lt;/p&gt;

&lt;p&gt;The setup, generalized: create a filtered view of the form data scoped to that specific client or form, render it as a table on a normal WordPress page, hide any internal-only fields (IP address, user agent, raw submission IDs - nothing a client needs to see), and publish the page.&lt;/p&gt;

&lt;p&gt;Share the URL directly with the client rather than linking it from your site's navigation. The page stays publicly accessible to anyone with the link, so treat the URL itself as the access control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2 - Sync to a Google Sheet and share it as Viewer
&lt;/h2&gt;

&lt;p&gt;If the client already lives in spreadsheets, skip the custom page entirely and sync submissions directly to a Google Sheet.&lt;/p&gt;

&lt;p&gt;The setup: connect a Google account, configure an export that maps form fields to spreadsheet columns, and point it at a specific tab. Every new submission pushes into the sheet automatically from that point forward.&lt;/p&gt;

&lt;p&gt;Share the sheet with the client's email and set their permission to Viewer, not Editor - this is the part worth getting right. Viewer access means they can read, sort, and filter the data on their end without any risk of someone accidentally deleting or overwriting a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one actually fits your situation
&lt;/h2&gt;

&lt;p&gt;Go with the dedicated page when: you want something that looks like part of your site rather than a raw spreadsheet, the client isn't comfortable in Sheets, you need tight control over exactly which fields are visible, or you want built-in search for a high-volume form.&lt;/p&gt;

&lt;p&gt;Go with Google Sheets when: the client already works in spreadsheets daily, you want to set it up once and never touch it again, or the client needs to do their own filtering/sorting/sharing without depending on you for anything beyond initial setup.&lt;/p&gt;

&lt;p&gt;Nothing stops you from running both at once for the same form - one as the primary client-facing view, the other as an internal backup or reporting source. They operate independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Both of these are built into &lt;a href="https://formvibes.com/pricing/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=pricing&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt; - the page-based view is called a Data Profile, and the spreadsheet sync is a Google Sheets Export Profile. Neither requires touching code or setting up a separate integration tool; both are configured from inside WordPress and connect to forms from any of the major form plugins.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formvibes</category>
      <category>googlesheets</category>
      <category>googlesheetsintegration</category>
    </item>
    <item>
      <title>How to Build a Form Submission Review Workflow for Your WordPress Team</title>
      <dc:creator>WPVibes</dc:creator>
      <pubDate>Thu, 25 Jun 2026 08:33:55 +0000</pubDate>
      <link>https://dev.to/wpvibes/how-to-build-a-proper-form-submission-review-workflow-for-your-wordpress-team-2ibo</link>
      <guid>https://dev.to/wpvibes/how-to-build-a-proper-form-submission-review-workflow-for-your-wordpress-team-2ibo</guid>
      <description>&lt;p&gt;When a form submission arrives, someone on the team needs to review it, act on it, and make sure nothing falls through. If the current process is "it lands in an inbox and whoever sees it first deals with it" - the failure modes are predictable. Leads get missed. The same entry gets followed up twice. A client asks why nobody responded and there is no record of what happened.&lt;/p&gt;

&lt;p&gt;This is not a tooling problem specific to any one form plugin. It exists with Contact Form 7, WPForms, Gravity Forms, or any other builder, because form plugins are built to collect data, not to manage it as a team. The submissions screen in most of them is a flat list with no workflow layer on top.&lt;/p&gt;

&lt;p&gt;Here is what a proper team submission workflow needs, and how to build one inside WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks without a team workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No visibility into what has been handled&lt;/strong&gt;. Every submission looks identical whether it arrived five minutes ago or was actioned three days ago. Team members have no way to tell without opening each entry individually or asking a colleague directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No way to leave context for the next person&lt;/strong&gt;. If you call a lead back and leave a voicemail, that context lives in your memory or a separate note-taking tool - disconnected from the actual submission record. The next person who opens that entry has no idea what already happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No record of who changed what&lt;/strong&gt;. If a field value gets edited, there is no audit trail showing who made the change or when. For any site where more than one person edits data, this is a real accountability gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No controlled access for different people&lt;/strong&gt;. A junior team member reviewing submissions probably should not have delete access. A client wanting to see their own leads should not have full WordPress admin. Without a permission system specific to submission data, it is all-or-nothing access for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the workflow piece by piece
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Status tracking for at-a-glance visibility&lt;/strong&gt;&lt;br&gt;
Every new submission should arrive in a default "needs review" state, visually distinct from entries that have already been actioned. As team members work through submissions, they update the state - read, unread, or flagged as irrelevant.&lt;/p&gt;

&lt;p&gt;The practical value of this is that anyone on the team can filter to "unread" or "pending" and immediately see exactly what needs attention, without having to ask a colleague or scroll through entries that have already been handled. For a two-person team reviewing daily enquiries, this single change removes the recurring "did you see this one?" message entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal notes attached to each entry&lt;/strong&gt;&lt;br&gt;
Status alone tells you whether something has been actioned. It does not tell you what was done. A note field attached directly to each submission solves this - "Called back, left voicemail, following up Friday", "Quote sent, awaiting response", "Duplicate of an earlier entry, do not action again."&lt;/p&gt;

&lt;p&gt;The notes should be completely internal, never visible to the person who submitted the form, and timestamped so the sequence of actions is clear when multiple people are involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role-based access control&lt;/strong&gt;&lt;br&gt;
Not everyone working with submission data needs the same permissions. A useful framework breaks access into four tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
View-only access for clients who need to see their own leads without any ability to change or delete data&lt;/li&gt;
&lt;li&gt;
Review access for junior team members - view entries, add notes, update status, but no export or delete&lt;/li&gt;
&lt;li&gt;
Full working access for account managers - everything above plus export capability&lt;/li&gt;
&lt;li&gt;
Complete access for senior team members or administrators
Define these tiers once per role and they apply automatically to every user assigned that role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;An audit trail for every action&lt;/strong&gt;&lt;br&gt;
Every edit to a submission and every export action should be logged automatically - who made the change, what was altered, and when. This matters in two specific situations: when a submission appears to have been edited and you need to know what the original value was, and when an export was run and you want to know why. Logging an "export reason" alongside the username and timestamp turns a simple action log into a genuinely useful accountability record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Form Vibes implements this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://formvibes.com/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=home&amp;amp;utm_content=form_vibes"&gt;Form Vibes Pro&lt;/a&gt;&lt;/strong&gt; builds exactly this workflow into WordPress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submission Status&lt;/strong&gt; marks every new entry as Unread by default, turning green when marked Read after review, or red when flagged as Spam. Filtering the dashboard to Unread at the start of a session shows exactly what needs attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt; attach permanently to individual entries, visible only to users with Form Vibes access, never to the form submitter. Multiple team members can add separate timestamped notes to the same entry, building a clear history of what happened and when.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role Manager&lt;/strong&gt; gives granular control over four WordPress user roles - Editor, Author, Contributor, and Subscriber - toggling permissions independently across submissions, notes, status, and exports. A Subscriber role with view-only access works well for client logins; a Contributor role with notes and status access but no delete works well for junior team members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Activity Log&lt;/strong&gt; records every field edit and export action automatically, including an Export Reason prompt that captures why an export was run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;None of this requires migrating away from your existing form plugin. Submission Status, Notes, and Role Manager work as a layer on top of Contact Form 7, WPForms, Gravity Forms, Elementor, and other supported builders - no changes to your existing forms.&lt;/p&gt;

&lt;p&gt;For a two-person team currently relying on "whoever sees it first handles it," even just status tracking and notes removes most of the daily back-and-forth. Role-based access becomes worth setting up once clients or junior team members need their own access without full WordPress admin.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>formvibes</category>
      <category>wordpressforms</category>
      <category>formmanagement</category>
    </item>
  </channel>
</rss>
