├── .gitignore ├── app-live-demo ├── _archive-body.php ├── footer.php ├── functions.php ├── header.php ├── index.php ├── js │ ├── app.js │ ├── collections │ │ └── archive.js │ ├── models │ │ └── post.js │ ├── routers │ │ └── router.js │ └── views │ │ ├── core.js │ │ └── post.js ├── json-posts.php ├── json.php └── style.css ├── app ├── _archive-body.php ├── footer.php ├── functions.php ├── header.php ├── index.php ├── js │ ├── app.js │ ├── collections │ │ └── archive.js │ ├── models │ │ └── post.js │ ├── routers │ │ └── router.js │ └── views │ │ ├── core.js │ │ └── post.js ├── json-posts.php ├── json.php └── style.css ├── live-demo.md └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /app-live-demo/_archive-body.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |

6 | 7 | {{{ data.title }}} 8 | 9 | 10 | 11 |

12 | 13 |
14 | 15 | {{{ data.content }}} 16 | 17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /app-live-demo/footer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app-live-demo/functions.php: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 12 | > 13 | 14 | 15 | 16 | 17 | <?php wp_title( '|', true, 'right' ); ?> 18 | 19 | 20 | 21 | 22 | 23 | > -------------------------------------------------------------------------------- /app-live-demo/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /app-live-demo/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tollmanz/backbone-wordpress-theme/a2008e54491dbe1c1153a2c1a6a32ccaaf8d23bf/app-live-demo/js/app.js -------------------------------------------------------------------------------- /app-live-demo/js/collections/archive.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tollmanz/backbone-wordpress-theme/a2008e54491dbe1c1153a2c1a6a32ccaaf8d23bf/app-live-demo/js/collections/archive.js -------------------------------------------------------------------------------- /app-live-demo/js/models/post.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tollmanz/backbone-wordpress-theme/a2008e54491dbe1c1153a2c1a6a32ccaaf8d23bf/app-live-demo/js/models/post.js -------------------------------------------------------------------------------- /app-live-demo/js/routers/router.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tollmanz/backbone-wordpress-theme/a2008e54491dbe1c1153a2c1a6a32ccaaf8d23bf/app-live-demo/js/routers/router.js -------------------------------------------------------------------------------- /app-live-demo/js/views/core.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tollmanz/backbone-wordpress-theme/a2008e54491dbe1c1153a2c1a6a32ccaaf8d23bf/app-live-demo/js/views/core.js -------------------------------------------------------------------------------- /app-live-demo/js/views/post.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tollmanz/backbone-wordpress-theme/a2008e54491dbe1c1153a2c1a6a32ccaaf8d23bf/app-live-demo/js/views/post.js -------------------------------------------------------------------------------- /app-live-demo/json-posts.php: -------------------------------------------------------------------------------- 1 | ', ']]>', $content ); 15 | 16 | $data[] = array( 17 | 'id' => get_the_ID(), 18 | 'title' => get_the_title(), 19 | 'content' => $content, 20 | ); 21 | } 22 | } 23 | 24 | echo json_encode( $data ); -------------------------------------------------------------------------------- /app-live-demo/json.php: -------------------------------------------------------------------------------- 1 | query_vars['zt-json'] ) || '1' !== $wp_query->query_vars['zt-json'] ) { 36 | return; 37 | } 38 | 39 | // Set the appropriate header 40 | header( 'Content-Type: application/json; charset=utf-8' ); 41 | 42 | // Help prevent MIME-type confusion attacks in IE8+ 43 | send_nosniff_header(); 44 | 45 | // Render the template and stop execution 46 | get_template_part( 'json', 'posts' ); 47 | exit; 48 | } 49 | endif; 50 | 51 | add_action( 'template_redirect', 'zt_json_template_redirect' ); -------------------------------------------------------------------------------- /app-live-demo/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Backapp Starter 3 | */ -------------------------------------------------------------------------------- /app/_archive-body.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |

6 | 7 | {{{ data.title }}} 8 | 9 | 10 | 11 |

12 | 13 |
14 | 15 | {{{ data.content }}} 16 | 17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /app/footer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/functions.php: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 12 | > 13 | 14 | 15 | 16 | 17 | <?php wp_title( '|', true, 'right' ); ?> 18 | 19 | 20 | 21 | 22 | 23 | > -------------------------------------------------------------------------------- /app/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | /*global jQuery */ 2 | var app = app || {}; 3 | 4 | jQuery(function () { 5 | 'use strict'; 6 | 7 | new app.CoreView(); 8 | }); -------------------------------------------------------------------------------- /app/js/collections/archive.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | var Archive = Backbone.Collection.extend({ 8 | model: app.Post 9 | }); 10 | 11 | app.archive = new Archive(); 12 | })(); -------------------------------------------------------------------------------- /app/js/models/post.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | app.Post = Backbone.Model.extend({ 8 | 9 | defaults: { 10 | id: '', 11 | title: '', 12 | content: '' 13 | } 14 | 15 | }); 16 | })(); -------------------------------------------------------------------------------- /app/js/routers/router.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | var Router = Backbone.Router.extend({ 8 | routes: { 9 | // Catch all routes; allows us to avoid representing all of WordPress' rewrites here 10 | '*notFound' : 'go', 11 | '' : 'go' 12 | }, 13 | 14 | go: function ( pathname ) { 15 | var url = '/'; 16 | 17 | if ( !_.isNull( pathname ) ) { 18 | url += pathname 19 | } 20 | 21 | // Add '/' to make sure URL is relative to the domain 22 | app.archive.url = url + '?zt-json=1'; 23 | app.archive.fetch( { reset : true } ); 24 | } 25 | }); 26 | 27 | app.router = new Router(); 28 | Backbone.history.start( { 29 | pushState: true, 30 | silent: true 31 | } ); 32 | })(); -------------------------------------------------------------------------------- /app/js/views/core.js: -------------------------------------------------------------------------------- 1 | /*global Backbone, jQuery, _ */ 2 | var app = app || {}; 3 | 4 | (function ($) { 5 | 'use strict'; 6 | 7 | app.CoreView = Backbone.View.extend({ 8 | el : function() { 9 | return document.getElementById( 'container' ); 10 | }, 11 | 12 | currentViews: {}, 13 | 14 | events : { 15 | 'click .zt-control' : 'initRouter' 16 | }, 17 | 18 | initialize : function() { 19 | this.$posts = $( '#posts' ); 20 | this.listenTo( app.archive, 'reset', this.addAll ); 21 | }, 22 | 23 | initRouter : function ( evt ) { 24 | evt.preventDefault(); 25 | 26 | // Get the link 27 | var pathname = evt.target.pathname; 28 | 29 | // Trigger the router 30 | app.router.navigate( pathname, { trigger: true } ); 31 | }, 32 | 33 | addAll : function() { 34 | this.$posts.html( '' ); 35 | app.archive.each( this.addOne, this ); 36 | }, 37 | 38 | addOne: function ( post ) { 39 | var view = new app.PostView( { model: post } ); 40 | this.$posts.append( view.render().el ); 41 | } 42 | }); 43 | })(jQuery); -------------------------------------------------------------------------------- /app/js/views/post.js: -------------------------------------------------------------------------------- 1 | /*global Backbone, jQuery, _, wp */ 2 | var app = app || {}; 3 | 4 | (function ($) { 5 | 'use strict'; 6 | 7 | app.PostView = Backbone.View.extend({ 8 | 9 | tagName : 'article', 10 | 11 | className : 'post', 12 | 13 | template : wp.template( 'zt-archive-body' ), 14 | 15 | render : function () { 16 | this.id = 'post-' + this.model.get('id'); 17 | this.$el.html( this.template( this.model.toJSON() ) ); 18 | return this; 19 | } 20 | 21 | }); 22 | })(jQuery); -------------------------------------------------------------------------------- /app/json-posts.php: -------------------------------------------------------------------------------- 1 | ', ']]>', $content ); 15 | 16 | $data[] = array( 17 | 'id' => get_the_ID(), 18 | 'title' => get_the_title(), 19 | 'content' => $content, 20 | ); 21 | } 22 | } 23 | 24 | echo json_encode( $data ); -------------------------------------------------------------------------------- /app/json.php: -------------------------------------------------------------------------------- 1 | query_vars['zt-json'] ) || '1' !== $wp_query->query_vars['zt-json'] ) { 36 | return; 37 | } 38 | 39 | // Set the appropriate header 40 | header( 'Content-Type: application/json; charset=utf-8' ); 41 | 42 | // Help prevent MIME-type confusion attacks in IE8+ 43 | send_nosniff_header(); 44 | 45 | // Render the template and stop execution 46 | get_template_part( 'json', 'posts' ); 47 | exit; 48 | } 49 | endif; 50 | 51 | add_action( 'template_redirect', 'zt_json_template_redirect' ); -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Backapp 3 | */ -------------------------------------------------------------------------------- /live-demo.md: -------------------------------------------------------------------------------- 1 | * WordPress (see starter package) 2 | * style.CSS 3 | * index.php 4 | * functions.php 5 | * js templating 6 | * enqueue scripts 7 | * json.php 8 | * json-posts.php 9 | * _archive-body.php 10 | * Structure (see starter package) 11 | * Use a nice directory structure for the scripts 12 | * app.js 13 | * Main file to "start" the app 14 | * Contains an initial function that sets up our top level events 15 | 16 | ```js 17 | /*global jQuery */ 18 | var app = app || {}; 19 | 20 | jQuery(function () { 21 | 'use strict'; 22 | 23 | new app.CoreView(); 24 | }); 25 | ``` 26 | * /views/core.js 27 | * Contains the top level events that initiate the app 28 | * After the page loads, what can the user do 29 | * In this sample app, the main events are the links in the "sidebar" 30 | 31 | ```js 32 | /*global Backbone, jQuery, _ */ 33 | var app = app || {}; 34 | 35 | (function ($) { 36 | 'use strict'; 37 | 38 | app.CoreView = Backbone.View.extend({ 39 | el : function() { 40 | return document.getElementById( 'container' ); 41 | }, 42 | 43 | events : { 44 | 'click .zt-control' : 'initRouter' 45 | }, 46 | 47 | initRouter : function ( evt ) { 48 | evt.preventDefault(); 49 | } 50 | }); 51 | })(jQuery); 52 | ``` 53 | 54 | * Follow the events 55 | * Backbone.js is event driven 56 | * Set up the initial events and follow the life of that event 57 | 58 | * In **/views/core.js**, in `initRouter`, add: 59 | 60 | ```js 61 | // Get the link 62 | var pathname = evt.target.pathname; 63 | 64 | // Trigger the router 65 | app.router.navigate( pathname, { trigger: true } ); 66 | ``` 67 | 68 | * /routers/router.js 69 | * Allows for connecting client-side events, tied to specific URLs, to actions and events. 70 | 71 | ```js 72 | /*global Backbone */ 73 | var app = app || {}; 74 | 75 | (function () { 76 | 'use strict'; 77 | 78 | var Router = Backbone.Router.extend({ 79 | routes: { 80 | // Catch all routes; allows us to avoid representing all of WordPress' rewrites here 81 | '*notFound' : 'go', 82 | '' : 'go' 83 | }, 84 | 85 | go: function ( pathname ) { 86 | 87 | } 88 | }); 89 | })(); 90 | ``` 91 | 92 | * In **/routers/router.js**, initialize the router and start history 93 | 94 | ``` 95 | app.router = new Router(); 96 | Backbone.history.start( { 97 | pushState: true, 98 | silent: true 99 | } ); 100 | ``` 101 | 102 | * We need to complete the `go` method, but before doing so, we need to add our collection 103 | 104 | * /collections/archive.js 105 | * Set of models 106 | * Allows you to hold a group of models and apply "array-like" functions to them 107 | 108 | ```js 109 | /*global Backbone */ 110 | var app = app || {}; 111 | 112 | (function () { 113 | 'use strict'; 114 | 115 | var Archive = Backbone.Collection.extend({ 116 | model: app.Post 117 | }); 118 | 119 | app.archive = new Archive(); 120 | })(); 121 | ``` 122 | 123 | * Oh boy, now we need to create our model 124 | 125 | * /models/post.js 126 | * Contains data and logic for a specific object in the app 127 | 128 | ```js 129 | /*global Backbone */ 130 | var app = app || {}; 131 | 132 | (function () { 133 | 'use strict'; 134 | 135 | app.Post = Backbone.Model.extend({ 136 | 137 | defaults: { 138 | id: '', 139 | title: '', 140 | content: '' 141 | } 142 | 143 | }); 144 | })(); 145 | ``` 146 | 147 | * Back to the router 148 | * To refresh, we click the link, `initRouter` is executed, which calls `app.router.navigate( pathname );` 149 | * The catchall route is matched and `app.router.go` is executed 150 | * In **/routers/router.js**, `go`, update the collection URL and fetch from server 151 | 152 | ```js 153 | var url = '/'; 154 | 155 | if ( !_.isNull( pathname ) ) { 156 | url += pathname 157 | } 158 | 159 | // Add '/' to make sure URL is relative to the domain 160 | app.archive.url = url + '?zt-json=1'; 161 | app.archive.fetch( { reset : true } ); 162 | ``` 163 | 164 | * Listen for the `archive` reset event in the core view 165 | * Because we want to keep logic related to the core view in the one file, we can set up a listener in the core view to trigger actions when the archive collection is reset 166 | * In **/views/core.js**, add: 167 | 168 | ```js 169 | initialize : function() { 170 | this.$posts = $( '#posts' ); 171 | this.listenTo( app.archive, 'reset', this.addAll ); 172 | }, 173 | ``` 174 | 175 | * Note that `initialize` is called when `app.CoreView` is executed (e.g., `var app = new app.CoreView();`) 176 | * This caches the `#posts` element and sets up the listener 177 | * On `reset`, `addAll` is executed. In **/views/core.js**, add: 178 | 179 | ```js 180 | addAll : function() { 181 | this.$posts.html( '' ); 182 | app.archive.each( this.addOne, this ); 183 | }, 184 | 185 | addOne: function ( post ) { 186 | var view = new app.PostView( { model: post } ); 187 | this.$posts.append( view.render().el ); 188 | } 189 | ``` 190 | 191 | * /views/post.js 192 | 193 | ```js 194 | /*global Backbone, jQuery, _, wp */ 195 | var app = app || {}; 196 | 197 | (function ($) { 198 | 'use strict'; 199 | 200 | app.PostView = Backbone.View.extend({ 201 | 202 | tagName : 'article', 203 | 204 | className : 'post', 205 | 206 | template : wp.template( 'zt-archive-body' ), 207 | 208 | render : function () { 209 | this.id = 'post-' + this.model.get('id'); 210 | this.$el.html( this.template( this.model.toJSON() ) ); 211 | return this; 212 | } 213 | 214 | }); 215 | })(jQuery); 216 | ``` 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Welcome 2 | 3 | This repository holds a WordPress theme that uses Backbone.js to deliver a single page application experience. This remarkably simple theme was created for demonstration purposes during a [WP Sessions](http://wpsessions.com/sessions/wordpress-backbone-js/) talk. The theme is very incomplete and is intended for use as an example of *how to start and structure* a WordPress theme with Backbone.js integration. Additionally, some notes from the session are below. 4 | 5 | As part of this WP Session, I did a live demonstration that builds a [basic theme](https://github.com/tollmanz/backbone-wordpress-theme/tree/master/app-live-demo) into a [functioning Backbone.js theme](https://github.com/tollmanz/backbone-wordpress-theme/tree/master/app). I am including the [steps that I followed](https://github.com/tollmanz/backbone-wordpress-theme/blob/master/live-demo.md) for this code demonstration. If you add [app-live-demo](https://github.com/tollmanz/backbone-wordpress-theme/tree/master/app-live-demo) as a WordPress theme and follow the steps provided in [live-demo.md](https://github.com/tollmanz/backbone-wordpress-theme/blob/master/live-demo.md), you will understand my logic in building this theme. 6 | 7 | ## Backbone.js WordPress Themes (as of 26/Jan/2014) 8 | 9 | * Tareq Hasan's Backbone.js Proof of Concept Theme ([blog](http://tareq.wedevs.com/2013/09/backbone-js-powered-proof-of-concept-wordpress-theme/) | [github](https://github.com/tareq1988/wp-backbone) | [demo](http://demo.wedevs.com/backbone/)) 10 | * Uses the JSON REST API plugin 11 | * Works for most views (e.g., pages, widget links, etc) 12 | * Requires a specific permalink structure 13 | * Uses hashbang based URLs 14 | * Plugin support? Don't know couldn't get it to work 15 | * Emanuel Kluge's Backboned (v2) Theme ([github](https://github.com/herschel666/Backboned-v2) | [demo](http://the-flippers.frogcp.com/)) 16 | * Works with normal WP URLs 17 | * Supports pages, posts, taxonomies 18 | * Uses require.js and supports it's own versions of the deps 19 | * Uses Mustache templates in the PHP; WordPress templating system is largely ignored 20 | * Works as a server side app and a client side app 21 | * Does not use the JSON REST API plugin 22 | * Does not support default permalink structure 23 | * Plugin compatibility issues 24 | * O2 ([site](http://geto2.com/) | [WP API Team Demo](http://wpapiteam.wordpress.com/)) 25 | * Launching in the spring (maybe) 26 | * Not much info available, presently 27 | * Collections ([demo](http://demo.thethemefoundry.com/collections-theme/)) 28 | * Custom JSON API implementation 29 | * Supports all of the default permalink structures 30 | * Attempts to support a lot of plugins 31 | * Works with different MS setups 32 | * Every URL is a potential starting point for the app 33 | * Uses the regular WordPress templating system 34 | 35 | ## Challenges of a Backbone.js WordPress Theme 36 | 37 | Adding a client side application on top of the WordPress server side application changes the assumptions of the software. Developers build WordPress and plugins with the assumption that every page view will be generated by the server. Loading pages via the client changes these assumptions and can present some issues. 38 | 39 | ### Routing 40 | 41 | * WordPress allows for any URL structure to be implemented 42 | * Backbone.js asks you to define this structure and works best with a RESTful API, which WordPress does not support 43 | * Using a "catchall" router helps to avoid this issue: 44 | 45 | ```js 46 | routes: { 47 | '*notFound' : 'go', 48 | '' : 'go' 49 | }, 50 | ``` 51 | 52 | * A [discussion of this issue](https://thethemefoundry.com/blog/backbone-js-routers-collections/) 53 | 54 | ### Plugin support 55 | 56 | * For proper plugin support, enqueues need to work, environments need to be set, and the front end needs to support this 57 | * JSON API needs to mimic a full page load and front needs to alter this environment 58 | 59 | ```php 60 |