Upgrade to Pro — share decks privately, control downloads, hide ads and more …

LoopConf - Burn it down: A case study in CMS re...

Peter Wilson
February 22, 2018

LoopConf - Burn it down: A case study in CMS replatforming

This session will be a case study of a major media company using WordPress as an enterprise grade CMS. We will cover how improved media editing, editorial workflow, and an API first process can take WordPress beyond its blogging stereotype.

Peter Wilson

February 22, 2018
Tweet

More Decks by Peter Wilson

Other Decks in Technology

Transcript

  1. $must_use_plugins = [ 'ffx-options/plugin.php', // Filter get_option calls. /* ...

    Vendor plugins snipped ... */ 'ffx-helpers/plugin.php', // Available during bootstrap. 'ffx-feature-flags/plugin.php', // Available during bootstrap. 'ffx-api-content/plugin.php', 'ffx-api-media/plugin.php', 'ffx-roles-capabilities/plugin.php', 'ffx-article-editor/plugin.php', 'ffx-live-articles/plugin.php', 'ffx-notifications/plugin.php', 'ffx-shortcake/plugin.php', 'ffx-brightcove/plugin.php', 'ffx-profiles/plugin.php', 'ffx-unpublish/plugin.php', 'ffx-wire-feed/plugin.php', 'ffx-image-editor/plugin.php', 'ffx-taxonomies/plugin.php', 'ffx-dashboard/plugin.php', 'ffx-publishing/plugin.php', 'ffx-tooltips/plugin.php', ];
  2. GET 50 tags wp_insert_term( /* Roxie Hart */ ); wp_insert_term(

    /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); Content API
  3. wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' =>

    '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );
  4. # Check term exists (slug) SELECT tt.term_id, tt.term_taxonomy_id FROM wp_terms

    AS t INNER JOIN wp_term_taxonomy as tt ON tt.term_id = t.term_id WHERE t.slug = 'roxie-hart' AND tt.parent = '105' AND tt.taxonomy = 'ffx_tag' ORDER BY t.term_id ASC LIMIT 1
  5. SELECT tt.term_id, tt.term_taxonomy_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy

    as tt ON tt.term_id = t.term_id AND tt.parent = '105' AND tt.taxonomy = 'ffx_tag' ORDER BY t.term_id ASC LIMIT 1
  6. SELECT tt.term_id, tt.term_taxonomy_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy

    as tt ON tt.term_id = t.term_id AND tt.parent = '105' AND tt.taxonomy = 'ffx_tag' ORDER BY t.term_id ASC LIMIT 1 # Check term exists (name) WHERE t.name = 'Roxie Hart'
  7. # Check if (name) exists with same parent. SELECT t.*,

    tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ('ffx_tag') AND t.name IN ('Roxie Hart') AND tt.parent = '105' ORDER BY t.name ASC
  8. # Check slug is unique SELECT term_id FROM wp_terms as

    t WHERE t.slug = 'roxie-hart' ORDER BY t.term_id ASC LIMIT 1
  9. # Check if the relationship data exists. SELECT tt.term_taxonomy_id FROM

    wp_term_taxonomy AS tt INNER JOIN wp_terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = 'ffx_tag' AND t.term_id = 23170
  10. # Actually insert the relationship data. INSERT INTO `wp_term_taxonomy`
 (

    `term_id`, `taxonomy`, `description`, `parent`, `count` ) VALUES (23170, 'ffx_tag', '', 105, 0)
  11. # Logic check for duplicates. SELECT t.term_id, tt.term_taxonomy_id FROM wp_terms

    t INNER JOIN wp_term_taxonomy tt ON ( tt.term_id = t.term_id ) WHERE t.slug = 'roxie-hart' AND tt.parent = 105 AND tt.taxonomy = 'ffx_tag' AND t.term_id < 23170 AND tt.term_taxonomy_id != 23170
  12. # Warm term meta cache SELECT term_id, meta_key, meta_value FROM

    wp_ter # Check for meta key inserting SELECT meta_id FROM wp_termmeta WHERE meta_key = # Insert the meta key INSERT INTO `wp_termmeta` (`term_id`, `meta_key`
  13. # Delete term hierarchy option DELETE FROM `wp_options` WHERE `option_name`

    = 'ffx_tag_children' # Get term hierarchy option (for reasons) SELECT option_value FROM wp_options WHERE option_name = 'ffx_tag_children # Select all terms in the taxonomy to work out hierarchy SELECT t.term_id, tt.parent, tt.count, tt.taxonomy FROM wp_terms AS t I # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN (9 # Update the term hierarchy option INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`)
  14. // Get the total number of pages. $total_pages = get_total_pages();

    for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time() + ( 10 * $page * MINUTE_IN_SECONDS ), 'ffx_import_some_terms_action', $tags ); }
  15. Complex sites need scheduled tasks & asynchronous processing. Meet Cavalcade:

    A #WordPress jobs processing solution 
 humanmade.com/cavalcade Human Made @humanmadeltd 5:31 AM - 30 May 2017
  16. update_option( 'cron', [ /* ... */ ] ); wp_insert_term( 'Roxie

    Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );
  17. update_option( 'cron', [ /* ... */ ] ); wp_insert_term( 'Roxie

    Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );
  18. update_option( 'cron', [ /* ... */ ] ); wp_insert_term( 'Roxie

    Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );
  19. Complex sites need scheduled tasks & asynchronous processing. Meet Cavalcade:

    A #WordPress jobs processing solution 
 humanmade.com/cavalcade Human Made @humanmadeltd 5:31 AM - 30 May 2017
  20. // Get the total number of pages. $total_pages = get_total_pages();

    for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time() + ( 10 * $page * MINUTE_IN_SECONDS ), 'ffx_import_some_terms_action', $tags ); }
  21. // Get the total number of pages. $total_pages = get_total_pages();

    for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_some_terms_action', $tags ); }
  22. // Get the total number of pages. $total_pages = get_total_pages();

    for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_some_terms_action', $tags ); }
  23. // Get the total number of pages. $total_pages = get_total_pages();

    for ( $page = 1; $page <= $total_pages; $page++ ) { // Set the arguments needed to process the current page. $args = [ 'page' => $page, 'qty' => 50 ]; // Schedule a single event to create or update terms. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_tags_cron', $args ); }
  24. // Update the scheduling lock. update_option( TAGS_SCHEDULING_LOCK_OPTION, time() ); //

    Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Set the arguments needed to process the current page. $args = [ 'page' => $page, 'qty' => 50 ]; // Schedule a single event to create or update terms. wp_schedule_single_event(
  25. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_tags_cron', $args );

    } // Delete the scheduling lock. delete_option( TAGS_SCHEDULING_LOCK_OPTION ); update_option( TAGS_SCHEDULED_LOCK_OPTION, time() );
  26. [11] Worker out: [11] Worker err: [11] Worker ret: 0

    [12] Worker status: Array ( [command] => wp cavalcade run 12 [pid] => 57 [running] => [signaled] => [stopped] => [exitcode] => 0 [termsig] => 0 [stopsig] => 0 ) 2018-01-27T01:09:22.458320545Z [12] Worker shutting down... [12] Worker out: [12] Worker err: [12] Worker ret: 0 [14] Running wp cavalcade run 14 (ffx/taxonomies/term_importer/import_tags_cron a:1:{i:0;a:5:{s:8:"tag_ty [14] Started worker [15] Running wp cavalcade run 15 (ffx/taxonomies/term_importer/import_tags_cron a:1:{i:0;a:5:{s:8:"tag_ty [15] Started worker [16] Running wp cavalcade run 16 (ffx/taxonomies/term_importer/import_tags_cron a:1:{i:0;a:5:{s:8:"tag_ty [16] Started worker [ ] Out of workers [ ] Out of workers [ ] Out of workers [ ] Out of workers [14] Worker status: Array
  27. GET 50 tags wp_insert_term( /* Roxie Hart */ ); wp_insert_term(

    /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); Content API
  28. wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn

    */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); GET 50 tags Content API "
  29. wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn

    */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ );
  30. wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn

    */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ );
  31. # Delete term hierarchy option DELETE FROM `wp_options` WHERE `option_name`

    = 'ffx_tag_children' # Get term hierarchy option (for reasons) SELECT option_value FROM wp_options WHERE option_name = 'ffx_tag_children # Select all terms in the taxonomy to work out hierarchy SELECT t.term_id, tt.parent, tt.count, tt.taxonomy FROM wp_terms AS t I # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN (9 # Update the term hierarchy cache (335KB at 23K tags) INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`)
  32. # Delete term hierarchy option DELETE FROM `wp_options` WHERE `option_name`

    = 'ffx_tag_children' # Get term hierarchy option (for reasons) SELECT option_value FROM wp_options WHERE option_name = 'ffx_tag_children # Select all terms in the taxonomy to work out hierarchy SELECT t.term_id, tt.parent, tt.count, tt.taxonomy FROM wp_terms AS t I # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN (9 # Update the term hierarchy cache (335KB at 23K tags) INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`) # Update the term # hierarchy cache # (335KB at 23K tags)
  33. function import_tags( $taxonomy, $tags ) { /* * Disable term

    cache additions to speed up * the import, see comments in CMS-1175. */ wp_suspend_cache_addition( true ); foreach ( $tags[ $taxonomy ] as $tag ) { wp_insert_term( /* $tag */ ); update_term_meta( /* tag meta 1 */ ); update_term_meta( /* tag meta 2 */ ); } }
  34. function import_tags( $taxonomy, $tags ) { /* * Disable term

    cache additions to speed up * the import, see comments in CMS-1175. */ wp_suspend_cache_addition( true ); foreach ( $tags[ $taxonomy ] as $tag ) { wp_insert_term( /* $tag */ ); update_term_meta( /* tag meta 1 */ ); update_term_meta( /* tag meta 2 */ ); } }
  35. Read Cache Read Cache Read Cache Read Cache Write cache

    Write cache Write cache Write cache
  36. add_action( 'admin_init', 'ajax_upload_attachment', 0 ); /** * Ajax handler for

    uploading attachments * * Uploads from the media library are handled
 * by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) {
  37. add_action( 'admin_init', 'ajax_upload_attachment', 0 ); /** * Ajax handler for

    uploading attachments * * Uploads from the media library are handled
 * by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) {
  38. * * Uploads from the media library are handled
 *

    by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) { return; } // Duplicate core functionality. wp_die(); }
  39. * * Uploads from the media library are handled
 *

    by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) { return; } // Duplicate core functionality. }
  40. { 'altText': '', 'description': '', 'caption': '', 'credit': '', 'keywords':

    '', 'sha1': '7e51b009bf0e9097a1fd6ba339a78b6181c 'source': '', 'source_system_name': 'wordpress', 'fileDataURI': 'data:image/jpeg;base64,/9j/4AAQSkZJ }
  41. } // Duplicate core functionality. $schema = Image_Schema\get_image_schema( $image, true

    ); wp_remote_request( 'https://api-media/v0/images', [ 'method' => 'POST', 'headers' => [ 'content-type' => 'application/json', ], 'body' => $schema, 'timeout' => 10, ] );
  42. wp_remote_request( 'https://api-media/v0/images', [ 'method' => 'POST', 'headers' => [ 'content-type'

    => 'application/json', ], 'body' => $schema, 'timeout' => 10, ] ); wp_delete_post( $image_id, true ); wp_die(); }
  43. { "success": true, "data": [ { "id": 4424 /* ...

    */ }, { "id": 4333 /* ... */ }, { "id": 4332 /* ... */ }, { "id": 4330 /* ... */ }, { "id": 4327 /* ... */ }, { "id": 4323 /* ... */ }, { "id": 4321 /* ... */ }, { "id": 4317 /* ... */ }, { "id": 4315 /* ... */ }, { "id": 4312 /* ... */ } ] } /wp-admin/admin-ajax.php?action=query-attachments
  44. { "id": 4424, "filename": "deh-logo.jpg", "url": "https://pwcc.cc/wp-content/uploads/2018/02/deh-logo.jpg "alt": "", "description":

    "", "caption": "", "name": "deh-logo", "dateFormatted": "February 8, 2018", "mime": "image/jpeg", "type": "image", "subtype": "jpeg", "sizes": { "thumbnail": {}, "medium": {}, "large": {},
  45. { "altText": "Plaster casts from Dear Evan Hansen.", "caption": "The

    props department creates a new plaster cast for Dear "dateCreated": "2018-02-07T23:07:57.191Z", "credit": "Internet", "description": "Plaster casts Evan wears in Dear Evan Hansen.", "id": "78b576d29756c0f06ca5a1c450f4cf84bb69e8de", "keywords": "", "source": "Internet" }
  46. /** * Bootstrap the library replacement. */ function bootstrap() {

    add_action( 'wp_ajax_query-attachments', 'ajax_query_attachments', 0 ); add_action( 'wp_ajax_get-attachment', 'ajax_get_attachment', 0 ); }
  47. add_image_size( 'square1x1', 200, 200, true ); add_image_size( 'landscape3x2', 300, 200,

    true ); add_image_size( 'landscape16x9', 357, 201, true ); add_image_size( 'portrait2x3', 200, 300, true );
  48. /** * Retrieve attachment meta field for attachment ID. *

    * This matches the signature of `wp_get_attachment_metadata()` * modified for use with the Media API. * * @param string $attachment_id Attachment ID. Default ''. * @param bool $unfiltered True: filters are not run. * Default false. * @return mixed Attachment meta field. * False on failure. */ function get_attachment_metadata( /* ... */ ) { }
  49. > var_dump( wp_get_attachment_metadata() ); Array( [width] => 2400, [height] =>

    1559, [file] => '2018/02/you-will-be-found.jpg', [sizes] => Array( [square1x1] => Array(width, height, file), [landscape3x2] => Array(width, height, file), [landscape16x9] => Array(width, height, file), [portrait2x3] => Array(width, height, file), [etc] => Array(width, height, file), ) )
  50. > var_dump( API_Media\get_attachment_metadata() ); Array( [width] => 2400, [height] =>

    1559, [file] => 'bd0d64a85c59655b815776ae46c3d14be7a6098e', [url] => 'http://static.ffx.io/bd0d64a85c59655b81…98e', [sizes] => Array( [square1x1] => Array(width, height, file, url), [landscape3x2] => Array(width, height, file, url), [landscape16x9] => Array(width, height, file, url), [portrait2x3] => Array(width, height, file, url), [etc] => Array(width, height, file, url), ) )
  51. { "id": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "filename": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "url": "https://static.ffx.io/bd0d64a85c59655b815776ae46c3d14be "alt": "The Dear

    Evan Hansen cast perform You Will Be Found", "description": "Dear Evan Hansen cast perform You Will Be Found, the em "caption": "Dear Evan Hansen cast perform You Will Be Found, the em "name": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "dateFormatted": "February 8, 2018 09:02am", "mime": "image/jpeg", "type": "image", "subtype": "jpeg", "sizes": { "square1x1": {}, "landscape3x2": {}, "landscape16x9": {},
  52. { "id": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "filename": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "url": "https://static.ffx.io/bd0d64a85c59655b815776ae46c3d14be "alt": "The Dear

    Evan Hansen cast perform You Will Be Found", "description": "Dear Evan Hansen cast perform You Will Be Found, the em "caption": "Dear Evan Hansen cast perform You Will Be Found, the em "name": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "dateFormatted": "February 8, 2018 09:02am", "mime": "image/jpeg", "type": "image", "subtype": "jpeg", "sizes": { “thumbnail": {}, "landscape3x2": {}, "landscape16x9": {}, Not numeric That’s new These names have changed
  53. wp.media.view ◦ Attachment ◦ AttachmentCompat ◦ AttachmentFilters ◦ Attachments ◦

    AttachmentsBrowser ◦ AudioDetails ◦ Button ◦ ButtonGroup ◦ Cropper ◦ DateFilter ◦ EditImage ◦ EditorUploader ◦ Embed ◦ EmbedImage ◦ EmbedLink ◦ EmbedUrl ◦ FocusManager ◦ Frame ◦ Iframe ◦ ImageDetails ◦ Label ◦ MediaDetails ◦ MediaFrame ◦ Menu ◦ MenuItem ◦ Modal ◦ PriorityList ◦ Router ◦ RouterItem ◦ Search ◦ Selection ◦ Settings ◦ Sidebar ◦ SiteIconCropper ◦ SiteIconPreview ◦ Spinner ◦ Toolbar ◦ UploaderInline ◦ UploaderStatus ◦ UploaderStatusError ◦ UploaderWindow ◦ VideoDetails
  54. Inherited from WordPress ◦ 42 views ◦ 6 models ◦

    19 collections # Underscore # Backbone $ jQuery
  55. /* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments =

    require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );
  56. /* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments =

    require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );
  57. /* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments =

    require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );
  58. /* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments =

    require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );
  59. /* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments =

    require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );
  60. /** * wp.media.query * * We're overriding this because we

    need to use our custom * Attachment model and collection. */ module.exports = function ( props = {} ) { return new Attachments( null, { props: _.extend( _.defaults( props, { orderby: 'date', order: 'DESC' } ), { query: true } ) } ); };
  61. /** * wp.media.query * * We're overriding this because we

    need to use our custom * Attachment model and collection. */ module.exports = function ( props = {} ) { return new Attachments( null, { props: _.extend( _.defaults( props, { orderby: 'date', order: 'DESC' } ), { query: true } ) } ); };
  62. /** * wp.media.view.Attachment.Library * * We're overriding this to use

    our custom template for images. */ module.exports = Library.extend( { template: function () { const prefix = ( this.model.get( 'type' ) === 'image' ) ? 'ffx-' : ''; const template = wp.template( `${prefix}attachment` ); return template.apply( this, arguments ); } } );
  63. /** * wp.media.view.Attachment.Library * * We're overriding this to use

    our custom template for images. */ module.exports = Library.extend( { template: function () { const prefix = ( this.model.get( 'type' ) === 'image' ) ? 'ffx-' : ''; const template = wp.template( `${prefix}attachment` ); return template.apply( this, arguments ); } } );
  64. /** * CropPreview Model * * @class * @augments Backbone.Model

    */ module.exports = Backbone.Model.extend( { defaults: { id: '', url: '', attachment_id: '', autoCrop: true, fileName: '', aspect: '', originalWidth: 0, cropWidth: 0, // Attributes below should not saved.
  65. // We don't need this, we have // the Publish

    Box of the Future™! remove_meta_box( 'submitdiv', 'post', 'side' );
  66. // The Publish Box of the Future™! add_meta_box( 'ffx-submitdiv', __(

    'Save & Publish', 'ffx' ), __NAMESPACE__ . '\\the_loading_icon', [ 'post' ], 'side', 'high' );
  67. register_rest_field( 'post', 'ffx_writeoff', [ 'get_callback' => __NAMESPACE__ . '\\get_writeoff', 'update_callback'

    => __NAMESPACE__ . '\\set_writeoff', 'schema' => [ 'description' => __( 'Article writeoff', 'ffx' ), 'type' => 'string', ], ] );
  68. register_rest_field( 'post', 'ffx_writeoff', [ 'get_callback' => __NAMESPACE__ . '\\get_writeoff', 'update_callback'

    => __NAMESPACE__ . '\\set_writeoff', 'schema' => [ 'description' => __( 'Article writeoff', 'ffx' ), 'type' => 'string', ], ] );
  69. register_rest_field( 'post', 'ffx_private', [ 'get_callback' => __NAMESPACE__ . '\\get_privacy', 'update_callback'

    => __NAMESPACE__ . '\\set_privacy', 'schema' => [ 'description' => __( 'Fairfax Visibility', 'ffx' ), 'type' => 'boolean', ], ] );
  70. The Post model ◦ author ◦ categories ◦ comment_status ◦

    content ◦ date ◦ date_gmt ◦ excerpt ◦ featured_media ◦ ffx-legal-status ◦ ffx-post-state ◦ ffx-sources ◦ ffx-tags ◦ ffx_advertisements ◦ ffx_advertiser_logo ◦ ffx_advertiser_name ◦ ffx_article_tool ◦ ffx_authors ◦ ffx_bespoke_url ◦ ffx_brief ◦ ffx_collab_authors ◦ ffx_collab_editor ◦ ffx_collab_watchers ◦ ffx_comments ◦ ffx_comments_open ◦ ffx_commercial_content_ty pe ◦ ffx_correction ◦ ffx_correction_text ◦ ffx_format ◦ ffx_identifier ◦ ffx_in_numbers ◦ ffx_in_numbers_title ◦ ffx_index_headline ◦ ffx_intro ◦ ffx_label ◦ ffx_last_updated ◦ ffx_major_update ◦ ffx_misc_update ◦ ffx_off_time ◦ ffx_primary_tag ◦ ffx_private ◦ ffx_seo_description ◦ ffx_seo_news_keywords ◦ ffx_seo_noindex ◦ ffx_seo_title ◦ ffx_slack_channel ◦ ffx_sponsor ◦ ffx_sports_score ◦ ffx_syndication ◦ ffx_talking_points ◦ ffx_talking_points_live_title ◦ ffx_talking_points_title ◦ ffx_url ◦ ffx_url_content ◦ ffx_url_slug ◦ ffx_why_it_matters ◦ ffx_writeoff ◦ format ◦ id ◦ meta ◦ password ◦ ping_status ◦ slug ◦ status ◦ sticky ◦ template ◦ title
  71. Update via REST API ◦ update post content & create

    new revision ◦ update taxonomies ◦ update meta data
  72. add_filter( 'rest_pre_dispatch', 'move_revision_callback', 10, 3 ); /** Move the 'wp_save_post_revision'

    callback for REST requests. */ function move_revision_callback( $result, $unused, $request ) { /* SNIP: Check for post update request */ // Move default `wp_save_post_revision` callback. remove_action( 'post_updated', 'wp_save_post_revision', 10 ); add_action( 'rest_request_after_callbacks', 'wp_save_post_revision'); return $result; // Support other filters. }
  73. add_filter( 'rest_pre_dispatch', 'move_revision_callback', 10, 3 ); /** Move the 'wp_save_post_revision'

    callback for REST requests. */ function move_revision_callback( $result, $unused, $request ) { /* SNIP: Check for post update request */ // Move default `wp_save_post_revision` callback. remove_action( 'post_updated', 'wp_save_post_revision', 10 ); add_action( 'rest_request_after_callbacks', 'wp_save_post_revision'); return $result; // Support other filters. }
  74. add_filter( 'rest_request_after_callbacks', 'reset_revision_cb' ); /** Reset 'wp_save_post_revision' after REST requests.

    */ function reset_revision_cb( $response ) { // Move default `wp_save_post_revision` callback. add_action( 'post_updated', 'wp_save_post_revision', 10 ); return $response; // Support other filters. }
  75. [img id="9550846e3098113e9fe16878fcbc26e23580e8ea" altTex "body": "<x-placeholder id='2c62…d2'></x-placeholder>", "bodyPlaceholders": { "2c62…d2": {

    "type": "image", "data": { "id": "9550846e3098113e9fe16878fcbc26e23 "caption": "Lin-Manuel Miranda in Hamilton th "credit": "Production, Hamilton the Musical" "aspect": 1.5,
  76. [img id="9550846e3098113e9fe16878fcbc26e23580e8ea" altTex "body": "<x-placeholder id='2c62…d2'></x-placeholder>", "bodyPlaceholders": { "2c62…d2": {

    "type": "image", "data": { "id": "9550846e3098113e9fe16878fcbc26e23 "caption": "Lin-Manuel Miranda in Hamilton th "credit": "Production, Hamilton the Musical" "aspect": 1.5,
  77. [img id="9550846e3098113e9fe16878fcbc26e23580e8ea" altTex "body": "<x-placeholder id='2c62…d2'></x-placeholder>", "bodyPlaceholders": { "2c62…d2": {

    "type": "image", "data": { "id": "9550846e3098113e9fe16878fcbc26e23 "caption": "Lin-Manuel Miranda in Hamilton th "credit": "Production, Hamilton the Musical" "aspect": 1.5,
  78. “ My job is code review and fooling myself that

    today I really will pick up a ticket. Me