{"id":13516262,"url":"https://github.com/Quartz/wp-graphql-content-blocks","last_synced_at":"2025-03-31T06:30:30.880Z","repository":{"id":31901384,"uuid":"130764249","full_name":"Quartz/wp-graphql-content-blocks","owner":"Quartz","description":"Structured content blocks for WPGraphQL","archived":false,"fork":false,"pushed_at":"2021-12-15T11:34:55.000Z","size":97,"stargazers_count":77,"open_issues_count":6,"forks_count":16,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-21T22:11:17.265Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Quartz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-23T22:31:22.000Z","updated_at":"2025-02-08T15:04:50.000Z","dependencies_parsed_at":"2022-07-16T00:30:33.450Z","dependency_job_id":null,"html_url":"https://github.com/Quartz/wp-graphql-content-blocks","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quartz%2Fwp-graphql-content-blocks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quartz%2Fwp-graphql-content-blocks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quartz%2Fwp-graphql-content-blocks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quartz%2Fwp-graphql-content-blocks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Quartz","download_url":"https://codeload.github.com/Quartz/wp-graphql-content-blocks/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246429459,"owners_count":20775805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T05:01:20.835Z","updated_at":"2025-03-31T06:30:30.498Z","avatar_url":"https://github.com/Quartz.png","language":"PHP","funding_links":[],"categories":["Plugins"],"sub_categories":["WordPress"],"readme":"# WPGraphQL Content Blocks (Structured Content)\nThis [WPGraphQL](https://github.com/wp-graphql/wp-graphql) plugin returns a WordPress post’s content as a shallow tree of blocks and allows for some limited validation and cleanup. This is helpful for rendering post content within component-based front-end ecosystems like React.\n\n## What this plugin does\nThis plugin adds a GraphQL field called `blocks` to `Post` in WPGraphQL (and any other post types configured to appear in WPGraphQL).\n\nThe `blocks` field contains a list of the root-level blocks that comprise the post's content. Each block is a distinct HTML element, embeddable URL, shortcode, or Gutenberg block (beta!).\n\nFor example, if a post’s `content` field in GraphQL contained:\n\n```html\n\u003cp\u003eHello world\u003c/p\u003e\n\u003cul\u003e\n\t\u003cli\u003eHere is a list\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nThen the `blocks` field would contain:\n\n```json\n[\n\t{\n\t\t\"type\": \"P\",\n\t\t\"innerHtml\": \"Hello world\"\n\t},\n\t{\n\t\t\"type\": \"UL\",\n\t\t\"innerHTML\": \"\u003cli\u003eHere is a list\u003c/li\u003e\"\n\t}\n]\n```\n\nWhen consuming this field, you can now easily iterate over the blocks and map them to components in your component library. No more `dangerouslySetInnerHTML`ing your entire `post_content`!\n\n## GraphQL fields and types\nAn exhaustive GraphQL of a post’s `blocks` field would look like this:\n\n```graphql\nblocks {\n\ttype\n\ttagName\n\tinnerHtml\n\tattributes {\n\t\tname\n\t\tvalue\n\t}\n\tconnections {\n\t\t... on Post {\n\t\t\t...PostParts\n\t\t}\n\t\t... on MediaItem {\n\t\t\t...MediaItemParts\n\t\t}\n\t}\n}\n```\n\nThis will return a list of `BlockTypes`, defined in `src/types/Blocktype.php`. Let’s break down each of these fields:\n\n### `type`\n\nType: `BlockNameEnumType` (defined in `src/types/enums/BlockNameEnumType.php`)\n\nThe name of the block. For HTML blocks, this is the uppercase version of the HTML tag name, e.g. `P`, `UL`, `BLOCKQUOTE`, `TABLE` etc. Shortcode and embed types are name-spaced with `SHORTCODE_` and `EMBED_` respectively., e.g. `SHORTCODE_CAPTION`, `EMBED_INSTAGRAM`, etc.\n\nHTML block types are hardcoded, as are Gutenberg block types (for now). Embed types are determined by the handlers that have been registered in `global $wp_embed-\u003ehandlers` and shortcodes are determined by the handlers registered with `global $shortcode_tags`. A complete list of permissible block names can seen by browsing the WPGraphQL schema.\n\nYou can filter the type definitions with `graphql_blocks_definitions`.\n\n### tagName\n\nType: `String`\n\nThe suggested HTML tag name for the block. For HTML blocks, this is simply a lowercased version of the type field. For embeds and shortcodes, it will likely be `null`. This field is most useful for Gutenberg blocks, as a hint from the server for which tag to use when wrapping the `innerHtml` (see below).\n\n### `innerHtml`\n\nType: `String`\n\nThe stringified inner content of the block. Can be passed into a React component using `dangerouslySetInnerHTML`, for example.\n\nNote that the value of `innerHtml` is the stringified version of all the block’s descendants *after* they have been parsed. This means that any invalid tags, attributes, etc. will have been stripped out.\n\n### `attributes`\n\nType: List of `BlockAttributeType`s\n\nEach item in the list is a name/value pair describing an attribute of the block. For HTML blocks, these are taken from the HTML attributes, e.g.\n\n```json\n{\n\t\"name\": \"id\",\n\t\"value\":  \"section1\"\n}\n```\n\nNote that this field will only contain valid attributes for the given Block, as defined in `BlockDefinitions`. Invalid attributes are stripped out during the parsing process (see [How Parsing Works](#how-parsing-works)).\n\n### `connections` (beta)\n\nType: List of `MenuItemObjectUnion`s\n\nThe `connections` field returns an array of objects that are connected to the block. For example, if you wanted to upload an image and associate it with a block, that image could be queried as a GraphQL connection here. The `connections` field will **always be empty by default**. Presumably there is some way to derive these connections from the block's attributes, but we have no way of knowing what that correspondence is. If you'd like to use this field, it's up to you to filter `graphql_blocks_output` and populate the `connections` array as you see fit.\n\n## Blocks\n### What is a block?\n\nA block is an atomic piece of content that comprises an article body. We define a block as being an HTML tag (like a paragraph, list, table, etc), a Gutenberg block, a text node (the textual content of a non-empty HTML element or shortcode), a [shortcode](https://codex.wordpress.org/shortcode) (like a caption, gallery, or video), or an [embed](https://codex.wordpress.org/Embeds) (an embeddable URL on its own line in the post content).\n\n### HTML Blocks\n\nAn HTML block is an HTML element (typically a  [block-level](https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements) element) represented by its tag name. If an HTML tag’s name is not included in `BlockNameEnumType`, it will be stripped from the tree. Additionally, at runtime it must meet the requirements specified in the block definitions in order to be considered valid.\n\nAn example HTML block in a GraphQL response looks like this:\n\n```json\n{\n\t\"type\": \"P\",\n\t\"innerHtml\": \"This isn’t the first time Facebook has unveiled new privacy settings in response to user concerns. It debuted a redesign that promised to give users more control over their data \u003ca href=\\\"https://www.theguardian.com/technology/2010/may/26/facebook-new-privacy-controls-data\\\"\u003eback in 2010\u003c/a\u003e. “People think that we don’t care about privacy, but that’s not true,” Zuckerberg said at the time. Yet some observers, including Quartz reporter Mike Murphy, remain skeptical.\",\n\t\"attributes\": []\n}\n```\n\n### Gutenberg blocks (beta)\n\nGutenberg blocks map very well to blocks, but do not have a server-side registration system. Like HTML blocks, we are forced to hardcode a list of core Gutenberg blocks. This list can be extended with `graphql_blocks_definitions` to add your own custom Gutenberg blocks.\n\nAnother issue with Gutenberg is that the markup of a (non-dynamic) block is defined only in JavaScript and then rendered directly into `post_content`. While we are most interested in the `attributes` of a block, the `innerHtml` is also important since the rendered tag name could be important information to, say, a React component tasked with its implementation.\n\nFor this reason, we descend into the `innerHtml` of a Gutenberg block to extract the `tagName` of the surrounding tag, then discard it, leaving just the true \"inner\" HTML of the block.\n\n```json\n{\n\t\"type\": \"CORE_HEADING\",\n\t\"tagName\": \"h2\",\n\t\"attributes\": [],\n\t\"innerHtml\": \"My Heading\"\n}\n```\n\nIn the example above, this allows the `innerHtml` to be `My Heading` instead of `\u003ch2\u003eMy Heading\u003c/h2\u003e`. This is a much better situation for the components that implement this data.\n\nGutenberg blocks present a number of challenges and the spec is still evolving. Take care when using this plugin with Gutenberg blocks since there will likely be breaking changes ahead.\n\n### Shortcode and Embed Blocks\n\nA shortcode block is a WordPress shortcode. *Shortcode/embed blocks are returned untransformed*: the parsing of shortcodes is the responsibility of the front-end consuming the GraphQL endpoint. Only the name of the shortcode, its attributes and any nested content of the shortcode are returned in the GraphQL response.\n\nShortcode block type names are prefixed with the `SHORTCODE_`  namespace by default.\n\n```json\n{\n\t\"type\": \"SHORTCODE_PULLQUOTE\",\n\t\"innerHtml\": \"Here is some \u003cabbr title=\\\"HyperText Markup Language\\\"\u003eHTML\u003c/abbr\u003e within a shortcode\",\n\t\"attributes\": []\n}\n```\n\nAn embed is a distinct block-type that represents WordPress’ [URL-to-markup embedding functionality](https://codex.wordpress.org/Embeds). If WordPress recognizes a URL as an embed, this plugin will output it as an embed block.\n\nEmbed block type names are prefixed with the `EMBED_`  namespace by default.\n\n```json\n{\n\t\"type\": \"EMBED_TWITTER\",\n\t\"innerHtml\": \"\",\n\t\"attributes\": [\n\t\t{\n\t\t\t\"name\": \"url\",\n\t\t\t\"value\": \"https://twitter.com/mcwm/status/978975850455556097\"\n\t\t}\n\t]\n}\n```\n\nBecause neither shortcode or embed blocks are parsed, the markup for embedding the URL is not provided by the plugin.\n\n### Block definitions\n\nWe can specify validation requirements for individual blocks. This allows us to enforce certain rules about blocks that determine where they end up in the tree, what attributes they may have, and whether or not they should end up in the GraphQL response at all.\n\nBlock definitions (and the default definition from which all blocks extend) for blocks can be found in `src/types/shared/BlockDefinitions`. If you are interested in filtering the block definitions (via `graphql_blocks_definitions`) to override behavior or add your own block definitions, you should look over that file.\n\nOur default block definition suits us well for most block HTML elements; we always want them to exist at the root and we will hoist them to the root if we find them nested deeper in the post content HTML. We therefore don’t provide any overrides for most block elements:\n\n```php\n'blockquote' =\u003e [],\n'figure' =\u003e [],\n'h1' =\u003e [],\n'h2' =\u003e [],\n'h3' =\u003e [],\n'h4' =\u003e [],\n'h5' =\u003e [],\n'h6' =\u003e [],\n'hr' =\u003e [],\n// etc\n```\n\nWe permit `\u003cp\u003e` tags to live at the root, but we do not enforce it (i.e. we don’t want to hoist a `\u003cp\u003e` tag out of a parent element) so we use this definition.\n\n```php\n'p' =\u003e [\n\t'root_only' =\u003e false,\n],\n```\n\nWe don’t want inline HTML elements like `\u003ca\u003e` to exist by themselves at the root, and we don’t want to permit the `target` attribute, so we use the following definition:\n\n```php\n'a' =\u003e [\n\t'attributes' =\u003e array(\n\t\t'deny' =\u003e array( 'target' ),\n\t),\n\t'root_only' =\u003e false,\n\t'allow_root' =\u003e false,\n],\n```\n\nNow any `\u003ca\u003e` tag found at the root-level of the post HTML will be wrapped in a `\u003cp\u003e` tag before being added to the tree. Additionally, if there is a `target` attribute, it will not appear in the `attribute` field.\n\n## How parsing works\nHere’s a rough breakdown of the process of parsing post content into blocks. (If the block is a Gutenberg block, we use `gutenberg_parse_blocks` and skip these steps.)\n\n1. The post content string is prepared for parsing (see `Fields::prepare_html` in `src/data/Fields.php`). This includes running the `wpautop`, `wptexturize` and `convert_chars` filters.\n2. The prepared content string is loaded into a [PHP DOMDocument](http://php.net/manual/en/class.domdocument.php)) object. This allows us to recurse the HTML as a tree.\n3. The `DOMDocument` object is passed into an `HTMLBlock` (`src/parser/class-htmlblock.php`) object. This begins the process of recursing the tree. Each child block is assigned a class depending on its type: `HTMLBlock`, `TextBlock`, `EmbedBlock` or `ShortcodeBlock`. Each block is responsible for validating itself against the Block Definitions (`src/types/shared/BlockDefinitions.php`) to determine whether it belongs in the tree or not.\n4. Although the tree is recursed and validated to an infinite depth, the GraphQL type `BlockType` (`src/types/BlockType.php`) will stringify the tree below a depth of 1 for consumption in the GraphQL endpoint.\n\n## Examples\nGiven a query for the content of a post returns the following:\n\nQuery:\n\n```graphql\n{\n\tpost(id: \"cG9zdDoxMjM5NzIx\") {\n\t\tcontent\n\t}\n}\n```\n\nResponse:\n\n```json\n{\n\t\"data\": {\n\t\t\"post\": {\n\t\t\t\"content\": \"\u003cp\u003eNow this is a story all about how\u003cbr /\u003e\\nMy life got flipped turned upside down\u003cbr /\u003e\\nAnd I\u0026#8217;d like to take a minute, just sit right there\u003cbr /\u003e\\nI\u0026#8217;ll tell you how I became the prince of a town called Bel-Air\u003c/p\u003e\\n\u003cp\u003ehttps://www.youtube.com/watch?v=AVbQo3IOC_A\u003c/p\u003e\\n\u003cp\u003eIn West Philadelphia, born and raised\u003cbr /\u003e\\nOn the playground is where I spent most of my days\u003cbr /\u003e\\nChillin\u0026#8217; out, maxin\u0026#8217;, relaxin\u0026#8217; all cool\u003cbr /\u003e\\nAnd all shootin\u0026#8217; some b-ball outside of the school\u003cbr /\u003e\\nWhen a couple of guys who were up to no good\u003cbr /\u003e\\nStarted makin\u0026#8217; trouble in my neighborhood\u003cbr /\u003e\\nI got in one little fight and my mom got scared\u003cbr /\u003e\\nAnd said \u0026#8220;You\u0026#8217;re movin\u0026#8217; with your auntie and uncle in Bel-Air\u003c/p\u003e\\n[pullquote]You\u0026#8217;re movin\u0026#8217; with your auntie and uncle in Bel-Air[/pullquote]\\n\u003cp\u003eI begged and pleaded with her day after day\u003cbr /\u003e\\nBut she packed my suitcase and sent me on my way\u003cbr /\u003e\\nShe gave me a kiss and then she gave me my ticket\u003cbr /\u003e\\nI put my Walkman on and said \u0026#8220;I might as well kick it\u0026#8221;\u003cbr /\u003e\\nFirst class, yo, this is bad\u003cbr /\u003e\\nDrinkin\u0026#8217; orange juice out of a champagne glass\u003cbr /\u003e\\nIs this what the people of Bel-Air livin\u0026#8217; like?\u003cbr /\u003e\\nHmmm, this might be all right\u003cbr /\u003e\\nBut wait, I hear they\u0026#8217;re prissy, bourgeois, and all that\u003cbr /\u003e\\nIs this the type of place that they just sent this cool cat?\u003cbr /\u003e\\nI don\u0026#8217;t think so, I\u0026#8217;ll see when I get there\u003cbr /\u003e\\nI hope they\u0026#8217;re prepared for the Prince of Bel-Air\u003c/p\u003e\\n\u003cp\u003e\u003cimg src=\\\"https://example.com/fresh-prince.jpeg\\\" alt=\\\"The Fresh Prince of Bel Air\\\" /\u003e\u003c/p\u003e\\n\"\n\t\t}\n\t}\n}\n```\n\nThen we would expect a query for the blocks that comprise the post to return the following.\n\nQuery:\n\n```graphql\n{\n\tpost(id: \"cG9zdDoxMjM5NzIx\") {\n\t\tblocks {\n\t\t\ttype\n\t\t\tinnerHtml\n\t\t}\n\t}\n}\n```\n\nResponse:\n\n```json\n{\n\t\"data\": {\n\t\t\"post\": {\n\t\t\t\"blocks\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"P\",\n\t\t\t\t\t\"innerHtml\": \"Now this is a story all about how\u003cbr\u003eMy life got flipped turned upside down\u003cbr\u003eAnd I’d like to take a minute, just sit right there\u003cbr\u003eI’ll tell you how I became the prince of a town called Bel-Air\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"EMBED_YOUTUBE\",\n\t\t\t\t\t\"innerHtml\": \"\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"P\",\n\t\t\t\t\t\"innerHtml\": \"In West Philadelphia, born and raised\u003cbr\u003eOn the playground is where I spent most of my days\u003cbr\u003eChillin’ out, maxin’, relaxin’ all cool\u003cbr\u003eAnd all shootin’ some b-ball outside of the school\u003cbr\u003eWhen a couple of guys who were up to no good\u003cbr\u003eStarted makin’ trouble in my neighborhood\u003cbr\u003eI got in one little fight and my mom got scared\u003cbr\u003eAnd said “You’re movin’ with your auntie and uncle in Bel-Air\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"SHORTCODE_PULLQUOTE\",\n\t\t\t\t\t\"innerHtml\": \"You’re movin’ with your auntie and uncle in Bel-Air\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"P\",\n\t\t\t\t\t\"innerHtml\": \"I begged and pleaded with her day after day\u003cbr\u003eBut she packed my suitcase and sent me on my way\u003cbr\u003eShe gave me a kiss and then she gave me my ticket\u003cbr\u003eI put my Walkman on and said “I might as well kick it”\u003cbr\u003eFirst class, yo, this is bad\u003cbr\u003eDrinkin’ orange juice out of a champagne glass\u003cbr\u003eIs this what the people of Bel-Air livin’ like?\u003cbr\u003eHmmm, this might be all right\u003cbr\u003eBut wait, I hear they’re prissy, bourgeois, and all that\u003cbr\u003eIs this the type of place that they just sent this cool cat?\u003cbr\u003eI don’t think so, I’ll see when I get there\u003cbr\u003eI hope they’re prepared for the Prince of Bel-Air\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"IMG\",\n\t\t\t\t\t\"innerHtml\": \"\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}\n}\n```\n\nWe can also see the attributes for the shortcode and embed blocks by requesting the `attributes` field.\n\nQuery:\n\n```graphql\n{\n\tpost(id: \"cG9zdDoxMjM5NzIx\") {\n\t\tblocks {\n\t\t\ttype\n\t\t\tattributes {\n\t\t\t\tname\n\t\t\t\tvalue\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\nResponse:\n\n```json\n{\n\t\"data\": {\n\t\t\"post\": {\n\t\t\t\"blocks\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"P\",\n\t\t\t\t\t\"attributes\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"EMBED_YOUTUBE\",\n\t\t\t\t\t\"attributes\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"url\",\n\t\t\t\t\t\t\t\"value\": \"https://www.youtube.com/watch?v=AVbQo3IOC_A\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"P\",\n\t\t\t\t\t\"attributes\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"SHORTCODE_PULLQUOTE\",\n\t\t\t\t\t\"attributes\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"P\",\n\t\t\t\t\t\"attributes\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"IMG\",\n\t\t\t\t\t\"attributes\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"src\",\n\t\t\t\t\t\t\t\"value\": \"https://example.com/fresh-prince.jpeg\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"name\": \"alt\",\n\t\t\t\t\t\t\t\"value\": \"The Fresh Prince of Bel Air\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FQuartz%2Fwp-graphql-content-blocks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FQuartz%2Fwp-graphql-content-blocks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FQuartz%2Fwp-graphql-content-blocks/lists"}