Decoupled Drupal Days 2018: Drupal Unhitched

Decoupled Drupal Days 2018: Drupal Unhitched

The CMS in Decoupled Architectures

D74c365206652832a56fd9ba1fb61d99?s=128

dirtystylus

August 17, 2018
Tweet

Transcript

  1. Drupal Unhitched The CMS in Decoupled Architectures Decoupled Drupal Days

    2018
  2. Hi. Mark Llobrera Technology Director, Bluecadet Philadelphia @dirtystylus @bluecadet

  3. None
  4. None
  5. None
  6. Descriptive, Not Prescriptive I’m not telling you how to do

    this stuff. But I hope all of this helps.
  7. 3 Things

  8. 3 Things 1. What is a decoupled CMS?

  9. 3 Things 1. What is a decoupled CMS? 2. Decoupled

    Scenarios
  10. 3 Things 1. What is a decoupled CMS? 2. Decoupled

    Scenarios 3. Decoupling Drupal
  11. 1. What is a Decoupled CMS?

  12. What is a decoupled CMS?

  13. What is a decoupled CMS? •An architecture for websites and

    applications where the CMS is not used to render the user-facing site or application.
  14. Stop me if you’ve heard this one before

  15. Separating CONTENT from PRESENTATION

  16. 2. Decoupled Scenarios

  17. First, a flashback…

  18. None
  19. Touchscreen Interactive + Static XML Penn Museum, Native American Voices

  20. “Authoring XML for 300+ objects by hand is…unwise.”

  21. “Authoring XML for 300+ objects by hand is…unwise.” — Me,

    after lots of pain
  22. CMS (Drupal 8) + Touchscreen Interactive (React) + JS Website

    (React) Smithsonian National Museum of the American Indian nmai.si.edu/americans
  23. None
  24. None
  25. None
  26. None
  27. None
  28. CMS (Drupal 8) + CMS-rendered Website + Digital Signage (React)

    Mann Center for the Performing Arts manncenter.org
  29. None
  30. None
  31. None
  32. None
  33. CMS + CMS-rendered Website + React components BRIC bricartsmedia.org

  34. None
  35. None
  36. CMS + Statically Published React (GatsbyJS) Outrider Foundation outrider.org

  37. None
  38. None
  39. None
  40. None
  41. CMS (Drupal 7) + CMS-rendered Website + Touch Wall (Cinder/C++)

    + 2 Touchscreen Interactives (AngularJS) + iOS / Android Native Applications Smithsonian National Air and Space Museum airandspace.si.edu
  42. None
  43. None
  44. None
  45. None
  46. None
  47. None
  48. PROS and CONS

  49. Pros

  50. Pros •Back and Front End systems are swappable

  51. Pros •Back and Front End systems are swappable •Teams can

    divide and conquer
  52. Cons

  53. Cons •Larger tech stack (Drupal, JS, multiple environments)

  54. Cons •Larger tech stack (Drupal, JS, multiple environments) •Maintenance

  55. Cons •Larger tech stack (Drupal, JS, multiple environments) •Maintenance •For

    websites you lose built-in functionality of CMS: menus/routing/accessibility/SEO
  56. 3. Decoupling Drupal

  57. Start with Content

  58. Start with Content •Getting content ready to travel

  59. Start with Content •Getting content ready to travel •NO Presentation

    specific names
  60. Start with Content •Getting content ready to travel •NO Presentation

    specific names •Decouple your mind
  61. Start with Content •Getting content ready to travel •NO Presentation

    specific names •Decouple your mind •Content in many forms/faces
  62. Example: Two Content Types

  63. Example: Two Content Types •Author

  64. Example: Two Content Types •Author •Book

  65. Author

  66. Book

  67. HOW?? There’s lots of ways:

  68. HOW?? There’s lots of ways: •Core: RESTful Web Services

  69. HOW?? There’s lots of ways: •Core: RESTful Web Services •Custom

    Controller
  70. HOW?? There’s lots of ways: •Core: RESTful Web Services •Custom

    Controller •JSON API
  71. HOW?? There’s lots of ways: •Core: RESTful Web Services •Custom

    Controller •JSON API •GraphQL
  72. RESTful Web Services Two basic approaches:

  73. RESTful Web Services Two basic approaches: •Core RESTful Services

  74. RESTful Web Services Two basic approaches: •Core RESTful Services •REST

    Exports
  75. Core RESTful Services

  76. Core RESTful Services •Core REST Module

  77. Core RESTful Services •Core REST Module •REST UI Module https://drupal.org/project/restui

  78. Request: /node/1?format=json

  79. { "nid": [ { "value": 1 } ], "uuid": [

    { "value": "3036ea2d-7502-4c64-8bc6-2e9ee3a0f897" } ], "vid": [ { "value": 1 } ], "langcode": [ { "value": "en" } ], "type": [ { "target_id": "book", "target_type": "node_type", "target_uuid": "a3c32842-68b2-46fd-b70e-d6ed9e71ee2d" } ], "revision_timestamp": [ { "value": "2018-04-23T18:10:59+00:00", "format": "Y-m-d\\TH:i:sP" } ], "revision_uid": [ { "target_id": 1, "target_type": "user", "target_uuid": "a7484c09-d013-402e-b070-46c0d84a7b28", "url": "/user/1" } ], "revision_log": [], "status": [ { "value": true } ], "title": [ { "value": "The Fifth Season" } ], "uid": [ { "target_id": 1, "target_type": "user", "target_uuid": "a7484c09-d013-402e-b070-46c0d84a7b28", "url": "/user/1" } ], "created": [ { "value": "2018-04-23T18:10:29+00:00", "format": "Y-m-d\\TH:i:sP" } ], "changed": [ { "value": "2018-04-23T18:10:59+00:00", "format": "Y-m-d\\TH:i:sP" } ], "promote": [ { "value": true } ], "sticky": [ { "value": false } ], "default_langcode": [ { "value": true } ], "revision_translation_affected": [ { "value": true } ], "path": [ { "alias": null, "pid": null, "langcode": "en" } ], "body": [ { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n", "format": "basic_html", "processed": "<p>Part one of the Broken Earth trilogy.</p>", "summary": "" } ] }
  80. { "nid": [ { "value": 1 } ], "type": [

    { "target_id": "book", "target_type": "node_type", "target_uuid": "a3c32842-68b2-46fd-b70e-d6ed9e71ee2d" } ], "title": [ { "value": "The Fifth Season" } ], "body": [ { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n", "format": "basic_html", "processed": "<p>Part one of the Broken Earth trilogy.</p>", "summary": "" } ] }
  81. REST Exports

  82. REST Exports •Views! (! old friend)

  83. REST Exports •Views! (! old friend) •“Provide a REST Export”

    checkbox ✅
  84. None
  85. None
  86. None
  87. [ { "title": "Kim Stanley Robinson", "body": "<p>Author of the

    <em>Mars</em> trilogy and <em>New York 2140</em>.</p>\r\n", "field_books": [ "2" ] }, { "title": "N. K. Jemisin", "body": "<p>Hugo Award-winning author of the Broken Earth trilogy.</p>\r\n", "field_books": [ "1" ] } ]
  88. Custom Controller

  89. Custom Controller

  90. Custom Controller •Routing (YML)

  91. Custom Controller •Routing (YML) •Controller Class

  92. Routing mann.season: path: 'api/season' defaults: _controller: \Drupal\mann_api\Controller\Season::api methods: [GET] requirements:

    _access: 'TRUE'
  93. … $a_query = \Drupal::entityQuery('node'); $a_query->condition('status', 1); $a_query->condition('type', 'event'); $entity_ids =

    $a_query->execute(); $events = Node::loadMultiple($entity_ids); foreach ($events as $event) { $item['id'] = (int) $event->nid->value; $date = $event->field_event_date->value; $item['date'] = $date; $item['title'] = $event->title->value; $item['display_title'] = $event->field_display_title->value; $image_data = MediaAssetService::getMediaPathSingle($event, 'field_featured_image'); $item['image'] = urldecode($image_data['image']); $item['image_relative'] = urldecode($image_data['image_relative']); $data[] = $item; } $response = new JsonResponse(['data' => $data]); return $response; …
  94. JSON API drupal.org/project/jsonapi

  95. Out of the box: /jsonapi/node/[Content Type] for example: /jsonapi/node/author

  96. { "data": [ { "type": "node--author", "id": "88223a76-278d-4fc6-9945-0bdb56cc78d3", "attributes": {

    "nid": 2, "body": { "value": "<p>Winner of the Hugo Award.</p>\r\n", "format": "basic_html", "processed": "<p>Winner of the Hugo Award.</p>", "summary": "" }, "field_display_name": { "value": "N. K. Jemisin", "format": "basic_html", "processed": "N. K. Jemisin" } }, "relationships": { "field_books": { "data": [ { "type": "node--book", "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f" } ], }, } ], }
  97. { "attributes": { "nid": 2, "body": { "value": "<p>Winner of

    the Hugo Award.</p>\r\n", "format": "basic_html", "processed": "\<p>Winner of the Hugo Award.</p>", "summary": "" }, "field_display_name": { "value": "N. K. Jemisin", "format": "basic_html", "processed": "N. K. Jemisin" } }, }
  98. { "relationships": { "field_books": { "data": [ { "type": "node--book",

    "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f" } ], }, }
  99. { "relationships": { "field_books": { "data": [ { "type": "node--book",

    "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f" } ], }, }
  100. { "relationships": { "field_books": { "data": [ { "type": "node--book",

    "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f" } ], }, }
  101. Relationships are complicated /jsonapi/node/[Content Type]?include=[Field Name] for example (we want

    field_books): /jsonapi/node/author?include=field_books
  102. { "included": [ { "type": "node--book", "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f", "attributes": {

    "nid": 1, "title": "The Fifth Season", "body": { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n", "format": "basic_html", "processed": "<p>Part one of the Broken Earth trilogy.</p>", "summary": "" } }, } }
  103. { "included": [ { "type": "node--book", "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f", "attributes": {

    "nid": 1, "title": "The Fifth Season", "body": { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n", "format": "basic_html", "processed": "<p>Part one of the Broken Earth trilogy.</p>", "summary": "" } }, } }
  104. { "included": [ { "type": "node--book", "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f", "attributes": {

    "nid": 1, "title": "The Fifth Season", "body": { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n", "format": "basic_html", "processed": "<p>Part one of the Broken Earth trilogy.</p>", "summary": "" } }, } }
  105. { "included": [ { "type": "node--book", "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f", "attributes": {

    "nid": 1, "title": "The Fifth Season", "body": { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n", "format": "basic_html", "processed": "<p>Part one of the Broken Earth trilogy.</p>", "summary": "" } }, } }
  106. "id": "e8c31c68-4fe5-4046-ab3f-1cb42e1f314f"

  107. GraphQL github.com/drupal-graphql

  108. Installation Note

  109. Installation Note •Download the module (or use drush dl graphql)

  110. Installation Note •Download the module (or use drush dl graphql)

    •Run composer require webonyx/graphql-php at root level (error otherwise)
  111. Installation Note •Download the module (or use drush dl graphql)

    •Run composer require webonyx/graphql-php at root level (error otherwise) •Enable the module
  112. Explorer allows you to test out queries

  113. None
  114. Auto-complete!

  115. None
  116. Simple Query query { nodeQuery { entities { entityId, entityLabel

    } } }
  117. { "data": { "nodeQuery": { "entities": [ { "entityId": "1",

    "entityLabel": "The Fifth Season" }, { "entityId": "2", "entityLabel": "New York 2140" }, { "entityId": "3", "entityLabel": "N. K. Jemisin" }, { "entityId": "4", "entityLabel": "Kim Stanley Robinson" } ] } } }
  118. Node Query w/ Types (Author) query { nodeQuery { entities

    { ...on NodeAuthor { entityId title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } } } }
  119. Node Query w/ Types (Author) query { nodeQuery { entities

    { ...on NodeAuthor { entityId title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } } } }
  120. Node Query w/ Types (Author) query { nodeQuery { entities

    { ...on NodeAuthor { entityId title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } } } }
  121. Node Query w/ Types (Author) query { nodeQuery { entities

    { ...on NodeAuthor { entityId title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } } } }
  122. Node Query w/ Types (Author) query { nodeQuery { entities

    { ...on NodeAuthor { entityId title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } } } }
  123. References are Easy(ish) fieldBooks { entity { entityId title }

    }
  124. { "data": { "nodeQuery": { "entities": [ {}, {}, {

    "entityId": "3", "title": "N. K. Jemisin", "body": { "value": "<p>Hugo Award-winning author of the Broken Earth trilogy.</p>\r\n" }, "fieldDisplayName": { "value": "N. K. Jemisin" }, "fieldBooks": [ { "entity": { "entityId": "1", "title": "The Fifth Season" } } ] }, … ] } } }
  125. { "data": { "nodeQuery": { "entities": [ {}, {}, {

    "entityId": "3", "title": "N. K. Jemisin", "body": { "value": "<p>Hugo Award-winning author of the Broken Earth trilogy.</p>\r\n" }, "fieldDisplayName": { "value": "N. K. Jemisin" }, "fieldBooks": [ { "entity": { "entityId": "1", "title": "The Fifth Season" } } ] }, … ] } } }
  126. { "data": { "nodeQuery": { "entities": [ {}, {}, {

    "entityId": "3", "title": "N. K. Jemisin", "body": { "value": "<p>Hugo Award-winning author of the Broken Earth trilogy.</p>\r\n" }, "fieldDisplayName": { "value": "N. K. Jemisin" }, "fieldBooks": [ { "entity": { "entityId": "1", "title": "The Fifth Season" } } ] }, … ] } } }
  127. query { nodeQuery { entities { ...on NodeAuthor { entityId

    title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } ...on NodeBook { entityId title body { value } } } } }
  128. query { nodeQuery { entities { ...on NodeAuthor { entityId

    title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } ...on NodeBook { entityId title body { value } } } } }
  129. query { nodeQuery { entities { ...on NodeAuthor { entityId

    title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } ...on NodeBook { entityId title body { value } } } } }
  130. query { nodeQuery { entities { ...on NodeAuthor { entityId

    title body { value } fieldDisplayName { value } fieldBooks { entity { entityId title } } } ...on NodeBook { entityId title body { value } } } } }
  131. Filter by Node Type query { nodeQuery (filter: {conditions: [{

    field: "type" value: "author" operator: EQUAL }] } ) { … } }
  132. Filter by Node Type query { nodeQuery (filter: {conditions: [{

    field: "type" value: "author" operator: EQUAL }] } ) { … } }
  133. Filter by Node Type query { nodeQuery (filter: {conditions: [{

    field: "type" value: "author" operator: EQUAL }] } ) { … } }
  134. Filter by Node Type query { nodeQuery (filter: {conditions: [{

    field: "type" value: "author" operator: EQUAL }] } ) { … } }
  135. query { nodeQuery (filter: {conditions: [{ field: "type" value: "author"

    operator: EQUAL }] } ) { entities { ...on NodeAuthor { entityId title body { value } fieldBooks { entity { title body { value } } } } } } }
  136. { "data": { "nodeQuery": { "entities": [ { "entityId": "3",

    "title": "N. K. Jemisin", "body": { "value": "<p>Hugo Award-winning author of the Broken Earth trilogy.</p>\r\n" }, "fieldBooks": [ { "entity": { "title": "The Fifth Season", "body": { "value": "<p>Part one of the Broken Earth trilogy.</p>\r\n" } } } ] },
  137. Aliases and Multiple Queries Take a simple Image Media Bundle

    entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  138. entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  139. entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  140. entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  141. entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  142. entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  143. entities { entityLabel ... on NodeHotspot { fieldName, title, fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { url width height alt title } } } }, } }
  144. This gives us the source image. What about derivatives? fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { derivative(style: THUMBNAIL) { url width height } url width height alt title } } }
  145. This gives us the source image. What about derivatives? fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { derivative(style: THUMBNAIL) { url width height } url width height alt title } } }
  146. This gives us the source image. What about derivatives? fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { derivative(style: THUMBNAIL) { url width height } url width height alt title } } }
  147. This gives us the source image. What about derivatives? fieldThumbnailImage

    { entity { ... on MediaImage { fieldMediaImage { derivative(style: THUMBNAIL) { url width height } url width height alt title } } }
  148. What if we need multiple derivatives? Enter aliases.

  149. What if we need multiple derivatives? Enter aliases. An alias

    allows you to run multiple queries for the same field and map them to different names.
  150. fieldThumbnailImage { entity { ... on MediaImage { fieldMediaImage {

    thumbnail:derivative(style: THUMBNAIL) { url width height } large:derivative(style: LARGE) { url width height } url width height alt title } } }
  151. fieldThumbnailImage { entity { ... on MediaImage { fieldMediaImage {

    thumbnail:derivative(style: THUMBNAIL) { url width height } large:derivative(style: LARGE) { url width height } url width height alt title } } }
  152. fieldThumbnailImage { entity { ... on MediaImage { fieldMediaImage {

    thumbnail:derivative(style: THUMBNAIL) { url width height } large:derivative(style: LARGE) { url width height } url width height alt title } } }
  153. fieldThumbnailImage { entity { ... on MediaImage { fieldMediaImage {

    thumbnail:derivative(style: THUMBNAIL) { url width height } large:derivative(style: LARGE) { url width height } url width height alt title } } }
  154. Bonus: Communication

  155. We’ve tried:

  156. We’ve tried: •Polling for changes (are we there yet?)

  157. We’ve tried: •Polling for changes (are we there yet?) •NodeJS

    (get a server in the middle, maybe Socket I/O)
  158. Enter Progressive Web Applications

  159. Enter Progressive Web Applications •CMS to Application communication is a

    use case
  160. Enter Progressive Web Applications •CMS to Application communication is a

    use case •PWAs have Service Workers and Push Notifications
  161. Web Push API

  162. Web Push API •Gives web applications the ability to receive

    messages pushed to them from a server
  163. Web Push API •Gives web applications the ability to receive

    messages pushed to them from a server •Requires a subscription to be activated, but once allowed, the subscription is saved by the browser
  164. Web Push API •Gives web applications the ability to receive

    messages pushed to them from a server •Requires a subscription to be activated, but once allowed, the subscription is saved by the browser •You can pass “messages”, but fundamentally you are passing data for your app or website to interpret as you see fit
  165. Web Push API •Gives web applications the ability to receive

    messages pushed to them from a server •Requires a subscription to be activated, but once allowed, the subscription is saved by the browser •You can pass “messages”, but fundamentally you are passing data for your app or website to interpret as you see fit •Browsers handle pushing the data through Service Workers, so a third party is not required
  166. Fine Print

  167. Fine Print •Limited browser support: ✅ Chrome, ✅ Firefox, "

    Safari (coming soon?)
  168. Fine Print •Limited browser support: ✅ Chrome, ✅ Firefox, "

    Safari (coming soon?) •Spec is considered experimental, so it may change in the future
  169. Fine Print •Limited browser support: ✅ Chrome, ✅ Firefox, "

    Safari (coming soon?) •Spec is considered experimental, so it may change in the future •Great for kiosks, but in the wild, you can’t guarantee that a user will subscribe
  170. Fine Print •Limited browser support: ✅ Chrome, ✅ Firefox, "

    Safari (coming soon?) •Spec is considered experimental, so it may change in the future •Great for kiosks, but in the wild, you can’t guarantee that a user will subscribe •Doesn’t help with non-web-based projects (Cinder/ C++, Unity)
  171. None
  172. Where to Next?

  173. Where to Next? •Two-way communication

  174. Where to Next? •Two-way communication •Electron wrappers for JS interactives

  175. Where to Next? •Two-way communication •Electron wrappers for JS interactives

    •Preview modes for interactives
  176. Takeaways

  177. Takeaways •Start with your Content, and make that portable

  178. Takeaways •Start with your Content, and make that portable •There’s

    many different ways to expose your content
  179. Takeaways •Start with your Content, and make that portable •There’s

    many different ways to expose your content •You can do this!
  180. THANK YOU. Mark Llobrera Technology Director, Bluecadet Philadelphia https://speakerdeck.com/mllobrera @dirtystylus

    @bluecadet