-
-
Notifications
You must be signed in to change notification settings - Fork 133
docs: investigate and document custom fields (meta) support #426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5abb4aa
b2688a9
065e3d7
e9e33b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,196 @@ | ||||||||||||||
| # Custom Fields (Meta) | ||||||||||||||
|
|
||||||||||||||
| Custom fields — also called post meta or metadata — let you attach arbitrary key/value data to WordPress objects (posts, pages, comments, users, and media). WordPress exposes them through the REST API via the `meta` property that is present on every supported object type. | ||||||||||||||
|
|
||||||||||||||
| ## How WordPress exposes custom fields via REST API | ||||||||||||||
|
|
||||||||||||||
| By default the `meta` property in a REST API response is an **empty object** (`{}`). Custom fields are **opt-in**: each meta key must be explicitly registered for REST API exposure, either through core WordPress functions or through plugins. | ||||||||||||||
|
|
||||||||||||||
| ### Approach 1 — `register_post_meta()` (recommended) | ||||||||||||||
|
|
||||||||||||||
| Register a meta key server-side so that it appears in the `meta` object of post/page responses: | ||||||||||||||
|
|
||||||||||||||
| ```php | ||||||||||||||
| add_action('rest_api_init', function () { | ||||||||||||||
| register_post_meta('post', 'my_color', [ | ||||||||||||||
| 'type' => 'string', | ||||||||||||||
| 'description' => 'A color value for the post.', | ||||||||||||||
| 'single' => true, | ||||||||||||||
| 'show_in_rest' => true, | ||||||||||||||
| ]); | ||||||||||||||
| }); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| > **Tip:** Use `register_term_meta()`, `register_comment_meta()`, and `register_user_meta()` for the corresponding object types. | ||||||||||||||
|
|
||||||||||||||
| Once registered, the field appears in responses like this: | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| { | ||||||||||||||
| "id": 42, | ||||||||||||||
| "title": { "rendered": "Hello world" }, | ||||||||||||||
| "meta": { | ||||||||||||||
| "my_color": "blue" | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Approach 2 — `register_rest_field()` | ||||||||||||||
|
|
||||||||||||||
| `register_rest_field()` adds a *top-level* field to REST responses (not nested under `meta`). This is useful for computed values or when you need full control over serialisation: | ||||||||||||||
|
|
||||||||||||||
| ```php | ||||||||||||||
| add_action('rest_api_init', function () { | ||||||||||||||
| register_rest_field('post', 'my_color', [ | ||||||||||||||
| 'get_callback' => function ($post) { | ||||||||||||||
| return get_post_meta($post['id'], 'my_color', true); | ||||||||||||||
| }, | ||||||||||||||
| 'update_callback' => function ($value, $post) { | ||||||||||||||
| update_post_meta($post->ID, 'my_color', sanitize_text_field($value)); | ||||||||||||||
| }, | ||||||||||||||
| 'schema' => [ | ||||||||||||||
| 'type' => 'string', | ||||||||||||||
| 'description' => 'A color value for the post.', | ||||||||||||||
| ], | ||||||||||||||
| ]); | ||||||||||||||
| }); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| Fields registered this way appear at the top level of the JSON object, alongside `id`, `title`, etc. | ||||||||||||||
| Use the `CustomRequest` approach described below to map them. | ||||||||||||||
|
|
||||||||||||||
| ### Approach 3 — Advanced Custom Fields (ACF) plugin | ||||||||||||||
|
|
||||||||||||||
| The popular [ACF plugin](https://www.advancedcustomfields.com/) can expose field groups through the REST API when the **Show in REST API** option is enabled on the field group. ACF exposes the data under an `acf` top-level key. Use the [Custom Request](../customization/customRequest.md) approach to work with ACF data. | ||||||||||||||
|
|
||||||||||||||
| ## Supported object types | ||||||||||||||
|
|
||||||||||||||
| The `Meta` property is available on the following WordPressPCL models: | ||||||||||||||
|
|
||||||||||||||
| | Model | WordPress endpoint | | ||||||||||||||
| |---|---| | ||||||||||||||
| | `Post` | `/wp/v2/posts` | | ||||||||||||||
| | `Page` | `/wp/v2/pages` | | ||||||||||||||
| | `Comment` | `/wp/v2/comments` | | ||||||||||||||
| | `MediaItem` | `/wp/v2/media` | | ||||||||||||||
| | `PostRevision` | `/wp/v2/revisions` | | ||||||||||||||
| | `User` | `/wp/v2/users` | | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+76
to
+78
|
||||||||||||||
| | `PostRevision` | `/wp/v2/revisions` | | |
| | `User` | `/wp/v2/users` | | |
| | `PostRevision` | `/wp/v2/posts/{postId}/revisions` | | |
| | `User` | `/wp/v2/users` | | |
| > **Note:** Revisions are nested under their parent post, so the revisions route includes the post ID. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||
| using System; | ||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||
| using System.Linq; | ||||||||||||||||
| using System.Text.Json; | ||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||||||||||||||||
| using WordPressPCL.Models; | ||||||||||||||||
|
|
@@ -35,7 +36,7 @@ | |||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Assert.AreEqual(post.Content!.Raw, createdPost.Content!.Raw); | ||||||||||||||||
| Assert.Contains(post.Content.Rendered, createdPost.Content!.Rendered); | ||||||||||||||||
|
Check warning on line 39 in tests/WordPressPCL.Tests.Selfhosted/Posts_Tests.cs
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| [TestMethod] | ||||||||||||||||
|
|
@@ -62,7 +63,7 @@ | |||||||||||||||
| Assert.Contains(x => x.Title!.Rendered == title, postsTask); | ||||||||||||||||
|
|
||||||||||||||||
| Assert.AreEqual(post.Content!.Raw, createdPost.Content!.Raw); | ||||||||||||||||
| Assert.Contains(post.Content.Rendered, createdPost.Content!.Rendered); | ||||||||||||||||
|
Check warning on line 66 in tests/WordPressPCL.Tests.Selfhosted/Posts_Tests.cs
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| [TestMethod] | ||||||||||||||||
|
|
@@ -136,7 +137,7 @@ | |||||||||||||||
| post.Content!.Raw = testContent; | ||||||||||||||||
| Post updatedPost = await _clientAuth.Posts.UpdateAsync(post, TestContext.CancellationToken); | ||||||||||||||||
| Assert.AreEqual(updatedPost.Content!.Raw, testContent); | ||||||||||||||||
| Assert.Contains(testContent, updatedPost.Content!.Rendered); | ||||||||||||||||
|
Check warning on line 140 in tests/WordPressPCL.Tests.Selfhosted/Posts_Tests.cs
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| [TestMethod] | ||||||||||||||||
|
|
@@ -227,5 +228,65 @@ | |||||||||||||||
| Assert.AreEqual(count, paged.TotalCount, "TotalCount from GetPagedAsync should match GetCountAsync"); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| [TestMethod] | ||||||||||||||||
| public async Task Posts_Meta_Write_And_Read() | ||||||||||||||||
| { | ||||||||||||||||
| // Arrange — create a post carrying a registered meta value | ||||||||||||||||
| string metaValue = $"pcl-meta-{System.Guid.NewGuid()}"; | ||||||||||||||||
| Post post = new() | ||||||||||||||||
| { | ||||||||||||||||
| Title = new Title("Meta Test Post"), | ||||||||||||||||
| Content = new Content("Meta Test Content"), | ||||||||||||||||
| Meta = JsonSerializer.SerializeToElement(new Dictionary<string, object?> | ||||||||||||||||
| { | ||||||||||||||||
| ["wordpresspcl_test_meta"] = metaValue | ||||||||||||||||
| }), | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| // Act — create then fetch back (edit context to ensure meta is returned) | ||||||||||||||||
| Post createdPost = await _clientAuth.Posts.CreateAsync(post, TestContext.CancellationToken); | ||||||||||||||||
| Post fetchedPost = await _clientAuth.Posts.GetByIdAsync(createdPost.Id, cancellationToken: TestContext.CancellationToken); | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+246
to
+249
|
||||||||||||||||
| // Assert | ||||||||||||||||
| Assert.IsNotNull(fetchedPost.Meta, "Meta should not be null when a registered key was written"); | ||||||||||||||||
| string? readValue = fetchedPost.Meta.Value.GetProperty("wordpresspcl_test_meta").GetString(); | ||||||||||||||||
| Assert.AreEqual(metaValue, readValue, "Meta value read back should equal the value written"); | ||||||||||||||||
|
Comment on lines
+251
to
+253
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| [TestMethod] | ||||||||||||||||
| public async Task Posts_Meta_Update() | ||||||||||||||||
| { | ||||||||||||||||
| // Arrange — create a post, then update only the meta field | ||||||||||||||||
| Post post = new() | ||||||||||||||||
| { | ||||||||||||||||
| Title = new Title("Meta Update Test Post"), | ||||||||||||||||
| Content = new Content("Meta Update Test Content"), | ||||||||||||||||
| Meta = JsonSerializer.SerializeToElement(new Dictionary<string, object?> | ||||||||||||||||
| { | ||||||||||||||||
| ["wordpresspcl_test_meta"] = "initial-value" | ||||||||||||||||
| }), | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| Post createdPost = await _clientAuth.Posts.CreateAsync(post, TestContext.CancellationToken); | ||||||||||||||||
|
|
||||||||||||||||
| // Act — update only the meta field | ||||||||||||||||
| string updatedMetaValue = $"updated-{System.Guid.NewGuid()}"; | ||||||||||||||||
| Post updateRequest = new() | ||||||||||||||||
| { | ||||||||||||||||
| Id = createdPost.Id, | ||||||||||||||||
| Meta = JsonSerializer.SerializeToElement(new Dictionary<string, object?> | ||||||||||||||||
| { | ||||||||||||||||
| ["wordpresspcl_test_meta"] = updatedMetaValue | ||||||||||||||||
| }), | ||||||||||||||||
| }; | ||||||||||||||||
| await _clientAuth.Posts.UpdateAsync(updateRequest, TestContext.CancellationToken); | ||||||||||||||||
|
|
||||||||||||||||
| // Assert — fetch back and verify | ||||||||||||||||
| Post fetchedPost = await _clientAuth.Posts.GetByIdAsync(createdPost.Id, cancellationToken: TestContext.CancellationToken); | ||||||||||||||||
|
||||||||||||||||
| Post fetchedPost = await _clientAuth.Posts.GetByIdAsync(createdPost.Id, cancellationToken: TestContext.CancellationToken); | |
| List<Post> fetchedPosts = (await _clientAuth.Posts.QueryAsync(new PostsQueryBuilder | |
| { | |
| Include = new List<int> { createdPost.Id }, | |
| Context = Context.Edit | |
| }, useAuth: true, cancellationToken: TestContext.CancellationToken)).ToList(); | |
| Post fetchedPost = fetchedPosts.Single(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section claims the
Metaproperty is available only on the listed models, but the codebase also exposesMetaon term models likeCategoryandTag. Either expand the list to include those models/endpoints (term meta) or clarify that the list is limited to the primary entities covered by this doc.