[{"data":1,"prerenderedAt":127},["ShallowReactive",2],{"article-\u002Fwriting\u002Fdruxt-drupal-13x-resource-list-yours-20260909":3},{"id":4,"title":5,"articleType":6,"categories":7,"date":11,"description":12,"extension":13,"meta":14,"paragraphs":15,"path":122,"readingTime":123,"sitemap":124,"stem":125,"__hash__":126},"articleEntries\u002Farticles-data\u002Fdruxt-drupal-13x-resource-list-yours-20260909.json","Druxt (for Drupal) 1.3.x; the resource list is yours","Blog post",[8,9,10],"Drupal","Druxt","Planet Drupal","2026-09-09T19:30:00+10:00","Druxt's Drupal module answers for twelve JSON:API resources, hardcoded, all of them describing structure. 1.3.0 makes that list yours, and here's what I'd add to mine. Any JSON:API client benefits, not only Druxt.","json",{},[16,28,41,71,79,95,105,113],{"type":17,"layout":18,"regions":19},"section","layout_onecol",{"content":20},[21,24,26],{"type":22,"html":23},"text_formatted","\u003Cp>Druxt's Drupal module carries a list of twelve JSON:API resources it answers for. It's been the same twelve since 2021, hardcoded in a PHP array, and changing it has meant carrying a patch.\u003C\u002Fp>",{"type":22,"html":25},"\u003Cp>1.3.0, tagged today, makes that list yours. The twelve stay exactly as they are, so nothing a site exposes changes when it updates. What is new is being able to choose, and the first thing I would choose is the one my frontend has always had to guess at: the toolbar an administrator configured for a text format.\u003C\u002Fp>\u003Cp>A CORS default ships in the same release. If you keep a proxy rule in front of your frontend so that authenticated calls succeed, this is the release where you delete it.\u003C\u002Fp>",{"type":22,"html":27},"\u003Cp>The module name suggests otherwise, but this is a Drupal-side change. Any client holding the right permission gets the same reads.\u003C\u002Fp>",{"type":17,"title":29,"layout":18,"regions":30},"What it answers today",{"content":31},[32,34,39],{"type":22,"html":33},"\u003Cp>Anyone holding the \u003Ccode>access druxt resources\u003C\u002Fcode> permission can read these, whatever access control would otherwise have said:\u003C\u002Fp>",{"type":35,"title":36,"code":37,"language":38},"code","The list, as it stands","block--block\nconfigurable_language--configurable_language\nentity_form_display--entity_form_display\nentity_form_mode--entity_form_mode\nentity_view_display--entity_view_display\nentity_view_mode--entity_view_mode\nfield_config--field_config\nfield_storage_config--field_storage_config\njsonapi_resource_config--jsonapi_resource_config\nmenu--menu\nmenu_link_content--menu_link_content\nview--view","text",{"type":22,"html":40},"\u003Cp>Every one describes structure, and all of it is configuration, with one exception: \u003Ccode>menu_link_content\u003C\u002Fcode> is content, and we will come back to it. None of them describes how the site prefers things to look or behave, and with the list compiled in, nobody could add one that did.\u003C\u002Fp>",{"type":17,"title":42,"layout":18,"regions":43},"Starting with rich text",{"content":44},[45,47,49,51,58,60,62,67,69],{"type":22,"html":46},"\u003Cp>A frontend offering an editing experience has no way to read the toolbar an administrator configured, so it ships whatever buttons its developer chose and hopes they match.\u003C\u002Fp>",{"type":22,"html":48},"\u003Cp>The reason it cannot read it is the interesting part. The only core permission that grants access to a text format's configuration is \u003Cstrong>administer filters\u003C\u002Fstrong>, which core itself flags \u003Ccode>restrict access\u003C\u002Fcode>. Hand it to an author and they can rewrite the text format, one of the shorter routes to an XSS. So the choice has been between a permission you cannot give them and a frontend guessing at a toolbar.\u003C\u002Fp>",{"type":22,"html":50},"\u003Cp>A checkbox is the third option, on a new settings form at \u003Cem>Configuration &gt; Web services &gt; Druxt\u003C\u002Fem>. It grants read where \u003Cstrong>administer filters\u003C\u002Fstrong> grants write, and it grants it to everyone holding \u003Ccode>access druxt resources\u003C\u002Fcode> instead of to one trusted account. List \u003Ccode>editor--editor\u003C\u002Fcode> and the frontend reads the toolbar and builds from it; change the toolbar in Drupal and the editor changes, with no rebuild: 10 buttons for \u003Ccode>basic_html\u003C\u002Fcode>, 16 for \u003Ccode>full_html\u003C\u002Fcode>.\u003C\u002Fp>",{"type":52,"src":53,"alt":54,"caption":55,"width":56,"height":57},"media","\u002Fimages\u002Fwriting\u002Fdruxt-130-toolbars.png","Two toolbars stacked, each labelled with one word. Above, labelled Drupal: the buttons sit in grey tiles and the separators are tiles of their own. Below, labelled Frontend: the same buttons in the same order in a plain row with thin dividers. In both, reading left to right: bold, italic, strikethrough, superscript, subscript, code, remove format, link, bulleted list, numbered list, block quote, image, table, horizontal line, a heading dropdown, code block and source editing.","\u003Cp>The same toolbar, twice. Drupal draws the separators as draggable tiles because that screen is for editing the toolbar; the frontend draws them as dividers.\u003C\u002Fp>",2560,744,{"type":22,"html":59},"\u003Cp>Which filters run is a second tick, on \u003Ccode>filter_format--filter_format\u003C\u002Fcode>. It tells a frontend whether a caption arrives in a \u003Ccode>data-caption\u003C\u002Fcode> attribute or in the markup. Fetch it once first: JSON:API hands over whole entities, so that is every format on the site and each one's filter settings, an open blob contrib modules fill with their own keys.\u003C\u002Fp>",{"type":22,"html":61},"\u003Cp>What comes back is Drupal's vocabulary, not CKEditor's. \u003Ccode>drupalInsertImage\u003C\u002Fcode> is Drupal's name for \u003Ccode>uploadImage\u003C\u002Fcode>, and an item your build has not loaded is dropped with a console warning rather than an error, so the editor comes up fine, quietly missing buttons the format says it has.\u003C\u002Fp>",{"type":35,"title":63,"code":64,"language":65,"highlighted":66},"Reading the toolbar, server side","\u002F\u002F Nuxt 2. fetch() runs on the server; druxt-site injects $druxt.\nasync fetch() {\n  \u002F\u002F A string: an object flattens to filter=[object Object] and 400s.\n  const { data } = await this.$druxt.getCollection(\n    'editor--editor',\n    'filter[drupal_internal__format]=basic_html',\n  )\n  this.items = ((data[0] || {}).attributes || {}).settings.toolbar.items || []\n}","js","\u003Cspan class=\"token comment\">\u002F\u002F Nuxt 2. fetch() runs on the server; druxt-site injects $druxt.\u003C\u002Fspan>\n\u003Cspan class=\"token keyword\">async\u003C\u002Fspan> \u003Cspan class=\"token function\">fetch\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">(\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">)\u003C\u002Fspan> \u003Cspan class=\"token punctuation\">{\u003C\u002Fspan>\n  \u003Cspan class=\"token comment\">\u002F\u002F A string: an object flattens to filter=[object Object] and 400s.\u003C\u002Fspan>\n  \u003Cspan class=\"token keyword\">const\u003C\u002Fspan> \u003Cspan class=\"token punctuation\">{\u003C\u002Fspan> data \u003Cspan class=\"token punctuation\">}\u003C\u002Fspan> \u003Cspan class=\"token operator\">=\u003C\u002Fspan> \u003Cspan class=\"token keyword\">await\u003C\u002Fspan> \u003Cspan class=\"token keyword\">this\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>$druxt\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>\u003Cspan class=\"token function\">getCollection\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">(\u003C\u002Fspan>\n    \u003Cspan class=\"token string\">'editor--editor'\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">,\u003C\u002Fspan>\n    \u003Cspan class=\"token string\">'filter[drupal_internal__format]=basic_html'\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">,\u003C\u002Fspan>\n  \u003Cspan class=\"token punctuation\">)\u003C\u002Fspan>\n  \u003Cspan class=\"token keyword\">this\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>items \u003Cspan class=\"token operator\">=\u003C\u002Fspan> \u003Cspan class=\"token punctuation\">(\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">(\u003C\u002Fspan>data\u003Cspan class=\"token punctuation\">[\u003C\u002Fspan>\u003Cspan class=\"token number\">0\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">]\u003C\u002Fspan> \u003Cspan class=\"token operator\">||\u003C\u002Fspan> \u003Cspan class=\"token punctuation\">{\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">}\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">)\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>attributes \u003Cspan class=\"token operator\">||\u003C\u002Fspan> \u003Cspan class=\"token punctuation\">{\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">}\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">)\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>settings\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>toolbar\u003Cspan class=\"token punctuation\">.\u003C\u002Fspan>items \u003Cspan class=\"token operator\">||\u003C\u002Fspan> \u003Cspan class=\"token punctuation\">[\u003C\u002Fspan>\u003Cspan class=\"token punctuation\">]\u003C\u002Fspan>\n\u003Cspan class=\"token punctuation\">}\u003C\u002Fspan>",{"type":22,"html":68},"\u003Cp>All of which is me saying: expect a Druxt CKEditor module in the future. Mounting the editor is where the work actually is. CKEditor 5 uses static class blocks, which the bundler Nuxt 2 ships cannot parse, so today you load it from a script tag and match the version Drupal ships rather than the one npm defaults to. That is exactly the sort of thing a module should absorb so nobody else meets it.\u003C\u002Fp>",{"type":22,"html":70},"\u003Cp>Neither is ticked by default. Updating writes the same twelve into configuration and stops there, and nothing on the list is readable by anyone until \u003Ccode>access druxt resources\u003C\u002Fcode> is granted, which installing grants to nobody. Changing the list needs \u003Ccode>administer druxt\u003C\u002Fcode>, which ships restricted.\u003C\u002Fp>",{"type":17,"title":72,"layout":18,"regions":73},"Then everything else",{"content":74},[75,77],{"type":22,"html":76},"\u003Cp>What else is a frontend inventing? All three of these are configuration entities, so they are already in that checkbox list, unticked:\u003C\u002Fp>",{"type":22,"html":78},"\u003Cul>\u003Cli>\u003Cstrong>Image styles.\u003C\u002Fstrong> Effects carry the dimensions a \u003Ccode>srcset\u003C\u002Fcode> needs. The derivative URLs are solved already, by \u003Ca href=\"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fjsonapi_image_styles\">JSON:API Image Styles\u003C\u002Fa> and \u003Ca href=\"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fconsumer_image_styles\">Consumer Image Styles\u003C\u002Fa>, after \u003Ca href=\"https:\u002F\u002Fwww.lullabot.com\u002Farticles\u002Fdecoupled-drupal-hard-problems-image-styles\">Lullabot named it in 2018\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Editorial workflow.\u003C\u002Fstrong> Content moderation's states and transitions would let an editing interface show where a document sits and which moves are legal, instead of a hardcoded draft-and-published pair\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Bulk actions.\u003C\u002Fstrong> \u003Ccode>action--action\u003C\u002Fcode> carries every action a site has configured, so a decoupled content list could offer exactly those\u003C\u002Fli>\n\u003C\u002Ful>",{"type":17,"title":80,"layout":18,"regions":81},"What it will not answer for",{"content":82},[83,85,87,89],{"type":22,"html":84},"\u003Cp>Druxt answers Drupal's entity access check with a yes for any listed route, and lifts JSON:API's limit on filtering by fields the requester could not otherwise see. A listed resource is readable by anyone holding \u003Ccode>access druxt resources\u003C\u002Fcode>, whatever the site would otherwise have said, and on a decoupled site that permission often goes to anonymous.\u003C\u002Fp>",{"type":22,"html":86},"\u003Cp>The list has one axis. It says what, never who: the same yes, site-wide, for everyone holding the permission. That suits configuration, which answers the same whoever is asking. Content varies per entity by published state, owner and field access, so one yes is never right for it. \u003Ccode>user--user\u003C\u002Fcode> would hand over every account, blocked ones included, and enough filtering to enumerate them.\u003C\u002Fp>",{"type":22,"html":88},"\u003Cp>\u003Ccode>menu_link_content\u003C\u002Fcode> is the exception, and it is content, grandfathered because it shipped in the hardcoded list. It is also the resource that gives a frontend the least, so I maintain \u003Ca href=\"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fjsonapi_menu_items\">JSON:API Menu Items\u003C\u002Fa> to cover the gap.\u003C\u002Fp>",{"type":52,"src":90,"alt":91,"caption":92,"width":93,"height":94},"\u002Fimages\u002Fwriting\u002Fdruxt-130-settings-form.png","The Druxt settings form at Configuration, Web services, Druxt. A list headed Exposed resources with a checkbox per configuration entity type. Ticked: block, editor, entity_form_display, entity_form_mode, entity_view_display, entity_view_mode, field_config, field_storage_config, menu, menu_link_content, view. Unticked: action, base_field_override, block_content_type, date_format, filter_format, image_style, media_type, node_type, oauth2_scope, oauth2_token_type, taxonomy_vocabulary, user_role. No content entity types are offered. Help text below reads that a checked resource is readable by anyone with the access druxt resources permission, whatever the site would otherwise allow.","\u003Cp>Configuration entity types only, and no content ones offered at all. \u003Ccode>editor--editor\u003C\u002Fcode> and \u003Ccode>filter_format--filter_format\u003C\u002Fcode> are separate ticks, and the help text under the list says outright that a ticked resource is readable by anyone holding the permission.\u003C\u002Fp>",1280,900,{"type":17,"title":96,"layout":18,"regions":97},"CORS, and the proxy rule you can delete",{"content":98},[99,101,103],{"type":22,"html":100},"\u003Cp>One more thing, and it takes a step out of everyone's setup. On a site that has not configured CORS itself, Druxt turns it on and allows every header, and now sets \u003Ccode>allowedMethods\u003C\u002Fcode> too. That list is what the library underneath advertises methods from, and core ships it empty, so the browser refused the preflight and the request never left.\u003C\u002Fp>",{"type":22,"html":102},"\u003Cp>Anonymous \u003Ccode>GET\u003C\u002Fcode> was never affected, since a simple request is not preflighted. What changes is everything else: writes, and reads that set an \u003Ccode>Authorization\u003C\u002Fcode> header. If you keep a proxy rule or a middleware in front of your frontend so that authenticated calls succeed, this is the release where you delete it. Worth checking rather than assuming, because a refused preflight answers 204 with a clean log, and \u003Ccode>curl\u003C\u002Fcode> never reproduces it: only a browser enforces the response.\u003C\u002Fp>",{"type":22,"html":104},"\u003Cp>A site that configured \u003Ccode>cors.config\u003C\u002Fcode> itself is untouched, and credentials stay off, so cookies and sessions still do not travel cross-origin. An \u003Ccode>Authorization\u003C\u002Fcode> header does. The \u003Ca href=\"https:\u002F\u002Fdruxtjs.org\u002F\">documentation site\u003C\u002Fa> was rebuilt around \u003Ca href=\"https:\u002F\u002Fdruxtjs.org\u002Ftutorials\">tutorials\u003C\u002Fa> last week, and the \u003Ca href=\"https:\u002F\u002Fdruxtjs.org\u002Ftutorials\u002Fauthentication\">login flow\u003C\u002Fa> is the one to start with if authenticated requests are what you have been putting off.\u003C\u002Fp>",{"type":17,"title":106,"layout":18,"regions":107},"What are you working around?",{"content":108},[109,111],{"type":22,"html":110},"\u003Cp>Enough about what I would tick. What I want to know is what you are already working around. Anyone building against Drupal from outside it has written something that guesses at a value Drupal already holds. Mine was a toolbar. It is a checkbox now.\u003C\u002Fp>",{"type":22,"html":112},"\u003Cp>So what is yours, and what would you build if you could simply read it?\u003C\u002Fp>",{"type":17,"layout":18,"regions":114},{"content":115},[116],{"type":117,"description":118,"url":119,"gitpod":120,"drupalUrl":121},"repository","\u003Cp>The Drupal module this ships in. If your projects lean on modules like it, sponsoring is what keeps them maintained.\u003C\u002Fp>","https:\u002F\u002Fgithub.com\u002Fdruxt\u002Fdruxt_drupal",false,"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fdruxt","\u002Fwriting\u002Fdruxt-drupal-13x-resource-list-yours-20260909","6 min",{"loc":122},"articles-data\u002Fdruxt-drupal-13x-resource-list-yours-20260909","0TMmpSgDFN8pTy6pWj4l16jdBzeadBDefLfPbkLsyzM",1788948077955]