├── .gitignore ├── 404.php ├── README.md ├── author.php ├── category.php ├── footer.php ├── functions.php ├── header.php ├── home.php ├── index.php ├── package-lock.json ├── package.json ├── page.php ├── screenshot.png ├── single.php ├── src ├── App.vue ├── api │ └── index.js ├── app.js ├── components │ ├── 404.vue │ ├── AuthorArchive.vue │ ├── CategoryArchive.vue │ ├── DateArchive.vue │ ├── Home.vue │ ├── Page.vue │ ├── Single.vue │ ├── TagArchive.vue │ ├── TaxonomyArchive.vue │ ├── template-parts │ │ ├── NavMenu.vue │ │ ├── Pagination.vue │ │ └── PostItem.vue │ └── utility │ │ ├── ArchiveLink.vue │ │ ├── PostMeta.vue │ │ ├── PostTaxonomies.vue │ │ ├── ResponsiveImage.vue │ │ └── SiteLoading.vue ├── router │ ├── index.js │ ├── paths.js │ ├── routes.js │ └── utils.js └── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ └── mutations.js ├── style.css ├── tag.php ├── template-parts ├── archive-link.php ├── nav-menu.php ├── pagination.php ├── post-item.php ├── post-meta.php ├── post-taxonomies.php ├── responsive-image.php ├── site-branding.php └── site-loading.php ├── webpack.config.js ├── webpack.dev.config.js └── webpack.prod.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ -------------------------------------------------------------------------------- /404.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |

404 - Page Not Found

6 | 7 |

Apparently nothing exists at this location.

8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Vue.wordpress icon](http://vue-wordpress-demo.bshiluk.com/wp-content/uploads/2019/03/vue-wordpress-logo-e1551495565479.png) 2 | # Vue.wordpress 3 | 4 | > A Wordpress starter theme built using the WP REST API and Vue.js. Optimized for SEO, performance, and ease of development. 5 | 6 | *This theme is intended to be used as a foundation for creating sites that function as a single-page application (SPA) on the front-end, while using Wordpress and the WP REST API to manage content and fetch data.* 7 | 8 | **Check out [a demo of the theme](http://vue-wordpress-demo.bshiluk.com)** 9 | 10 | # Table of Contents 11 | 12 | - [Features](#features) 13 | - [Libraries](#libraries) 14 | - [Usage](#usage) 15 | - [Development](#development) 16 | - [Deployment](#deployment) 17 | - [General Overview](#general-overview) 18 | - [Routing](#routing) 19 | - [Permalinks to Routes](#permalinks-to-routes) 20 | - [Exceptions](#exceptions) 21 | - [Notes on Permalink Structure](#notes-on-permalink-structure) 22 | - [Category and Tag Base](#category-and-tag-base) 23 | - [Homepage and Posts Per Page](#homepage-and-posts-per-page) 24 | - [Internal Link Delegation](#internal-link-delegation) 25 | - [State Management and API Requests](#state-management-and-api-requests) 26 | - [Vuex Initialization and Schema](#vuex-initialization-and-schema) 27 | - [Schema](#schema) 28 | - [Initialization](#initialization) 29 | - [Requesting Data](#requesting-data) 30 | - [Making WP REST API Requests](#making-wp-rest-api-requests) 31 | - [Request Caching](#request-caching) 32 | - [Actions Api Reference](#actions-api-reference) 33 | - [Request an item of type by its slug](#request-an-item-of-type-by-its-slug) 34 | - [Request an item of type by its id](#request-an-item-of-type-by-its-id) 35 | - [Request a list of items](#request-a-list-of-items) 36 | - [Getters Api Reference](#getters-api-reference) 37 | - [Get an item of type by slug](#get-an-item-of-type-by-slug) 38 | - [Get an item of type by id](#get-an-item-of-type-by-id) 39 | - [Get a list of items](#get-a-list-of-items) 40 | - [SEO](#seo-and-server-rendered-content) 41 | - [How It Works](#how-it-works) 42 | - [The Three Basic Approaches](#the-three-basic-approaches) 43 | - [Pseudo Headless](#pseudo-headless) 44 | - [Example index.php](#example-indexphp) 45 | - [Pseudo Headless SEO](#pseudo-headless-seo) 46 | - [Preload Data](#preload-data) 47 | - [Example single.php](#example-singlephp) 48 | - [Example category.php](#example-categoryphp) 49 | - [Preload Data SEO](#preload-data-seo) 50 | - [Progressive Enhancement](#progressive-enhancement) 51 | - [Example home.php](#example-homephp) 52 | - [Progressive Enhancement SEO](#progressive-enhancement-seo) 53 | - [Upcoming Features](#upcoming-features) 54 | - [Final Thoughts](#final-thoughts) 55 | 56 | ## Features 57 | * Front and Back-End run on same host 58 | * Supports various client/server code partitioning strategies for optional SEO optimization 59 | * Hot Module Replacement ( HMR ) for development 60 | * Vue.js Single File Components 61 | * Production / Development Webpack configurations 62 | * Dynamic routing as set up in WP Dashboard ( Settings > Permalinks ) 63 | * Vue Router internal link delegation ( no need to use `` in components ) 64 | * Document title tag update on route change 65 | * Consistent in-component REST API data fetching 66 | * Integrated REST API request caching and batching 67 | * [REST API Data Localizer](https://github.com/bucky355/rest-api-data-localizer) for state initialization and making back-end REST API requests 68 | * Normalized data structure 69 | * Pagination enabled 70 | * Utility components, including ResponsiveImage, SiteLoading, ArchiveLink, e.t.c. 71 | 72 | ## Libraries 73 | To promote flexibility in implementation, styling and dependencies are limited. That said, the theme requires the following libraries: 74 | * [Axios](https://github.com/axios/axios) 75 | * [Vue.js](https://vuejs.org/v2/guide/) 76 | * [Vue Router](https://router.vuejs.org/) 77 | * [Vuex](https://vuex.vuejs.org/) 78 | 79 | ## Usage 80 | 81 | 1. Clone or download this repo in your `/wp-content/themes` directory and activate 82 | 2. Clone or download the [REST API Data Localizer](https://github.com/bucky355/rest-api-data-localizer) companion plugin in your `/wp-content/plugins` directory and activate. 83 | 3. Ensure settings in Settings > Permalinks do not violate rules as described in [Routing](#routing). 84 | 4. Install dependencies `npm install`. 85 | 5. Build the theme `npm run build` 86 | 87 | ### Development 88 | 89 | 1. Start development with HMR `npm run dev` 90 | 2. Edit `functions.php` so Wordpress enqueues the bundle served from webpack dev server 91 | ````php 92 | // Enable For Production - Disable for Development 93 | // wp_enqueue_script('vue_wordpress.js', get_template_directory_uri() . '/dist/vue-wordpress.js', array(), null, true); 94 | 95 | // Enable For Development - Remove for Production 96 | wp_enqueue_script( 'vue_wordpress.js', 'http://localhost:8080/vue-wordpress.js', array(), false, true ); 97 | ```` 98 | ### Deployment 99 | 100 | 1. Build the theme `npm run build` 101 | 2. Only include Wordpress theme files ( i.e. `style.css`, `functions.php`, `index.php`, `screenshot.png` e.t.c.) and the `/dist/` directory 102 | 3. Edit `functions.php` so Wordpress enqueues the bundle served from your `/dist/` directory 103 | ````php 104 | // Enable For Production - Disable for Development 105 | wp_enqueue_script('vue_wordpress.js', get_template_directory_uri() . '/dist/vue-wordpress.js', array(), null, true); 106 | ```` 107 | 108 | ## General Overview 109 | 110 | 1. Wordpress creates initial content and localizes a `__VUE_WORDPRESS__` variable with initial state, routing info, and any client side data not accessible through the WP REST API. 111 | 2. Client renders initial content, and once js is parsed, `__VUE_WORDPRESS__` is used to create the Vuex store, Vue Router routes, and the app is mounted. 112 | 3. Vue takes over all future in-site navigation, using the WP_REST_API to request data, essentially transforming the site into a SPA. 113 | 114 | ## Routing 115 | 116 | ### Permalinks to Routes 117 | 118 | By default, routing works as defined in Settings > Permalinks. You can even define a custom structure. 119 | 120 | ![Settings > Permalinks Screenshot](http://vue-wordpress.com/wp-content/uploads/2019/02/vue-wordpress.com_wp-admin_options-permalink.php_.png) 121 | 122 | ✔️ 'Day and Name' `/%monthnum%/%day%/%postname%/` 123 | 124 | ✔️ 'Month and Name' `/%year%/%monthnum%/%day%/%postname%/` 125 | 126 | ✔️ `/my-blog/%category%/%postname%/` 127 | 128 | ✔️ `/%postname%/%author%/%category%/%year%/%monthnum%/` 129 | 130 | ### Exceptions 131 | 132 | Using 'Plain', 'Numeric', or any custom structure that uses `%post_id%` as the sole identifier. 133 | 134 | ❌ `/%year%/%monthnum%/%post_id%/` 135 | 136 | ❌ `/archives/%post_id%/` 137 | 138 | *However, if you combine the `%post_id%` tag with the `%postname%` tag it will work.* 139 | 140 | ✔️ `/%author%/%post_id%/%category%/%postname%/` 141 | 142 | *If for some reason you're set on using the post id in your permalink structure, editing the `Single.vue` component to use id as a prop and fetching the post by id instead of slug should make it work.* 143 | 144 | Using 'Post name' 145 | 146 | ❌ `/%postname%/` 147 | 148 | *However, you can add a base to make it work* 149 | 150 | ✔️ `/some-base/%postname%/` 151 | 152 | ### Notes on Permalink Structure 153 | 154 | - Using `/%category%/%postname%/` or `/%tag%/%postname%/` will cause problems with nested pages 155 | - Problems with the permalink structure are generally because the router can't differentiate between a post and a page. While this may be solved with Navigation Guards and in component logic to check data and redirect when necessary, the introduced complexity is out of scope for this starter theme. 156 | - Routes for custom post types will be needed to be added as needed. 157 | - Routing is by default dynamic, however when a structure is defined it could be in the best interest of the developer to refactor out some of the dynamic qualities. 158 | 159 | ### Category and Tag Base 160 | 161 | If you want to edit the category and tag permalink bases within the same Settings > Permalinks screen, that is supported as well. 162 | 163 | ### Homepage and Posts Per Page 164 | 165 | Additionally, in Settings > Reading you can adjust what your home page displays and posts shown per page ( labeled 'Blog pages to show at most' ). 166 | 167 | ### Internal Link Delegation 168 | 169 | Within a Vue application, all links not implemented with `` trigger a full page reload. Since content is dynamic and managed using Wordpress, links within content are unavoidable and it would be impractical to try and convert all of them to ``. To handle this, an event listener is bound to the root app component, so that all links referencing internal resources are delegated to Vue Router. 170 | 171 | *Adapted from Dennis Reimann's [Delegating HTML links to vue-router](https://dennisreimann.de/articles/delegating-html-links-to-vue-router.html)* 172 | 173 | This has the added benefit of being able to compose components using the 'link' property returned from WP REST API resources without having to convert to a relative format corresponding to the router base for ``. 174 | 175 | Instead of: 176 | ````html 177 | {{ tag.name }} 178 | ```` 179 | You can do: 180 | ````html 181 | {{ tag.name }} 182 | ```` 183 | 184 | ## State Management and API Requests 185 | 186 | A Vuex store is used to manage the state for this theme, so it is recommended at the very least to know [what it is](https://vuex.vuejs.org/) and become familiar with some of the [core concepts](https://vuex.vuejs.org/guide/state.html). That said, you'd be surprised at the performant and user-friendly themes you can build without even modifying the Vuex files in `/src/store/`. 187 | 188 | ### Vuex Initialization and Schema 189 | 190 | Normally, you would find the schema ( structure ) of a Vuex store in the file that initializes it (or an imported state.js file ). Even though it is initialized in `/src/store/index.js`, the schema is defined in `functions.php`. This allows you to use the use the [REST API Data Localizer](https://github.com/bucky355/rest-api-data-localizer) to prepopulate the store with both WP REST API data in addition to any other data you may need for your state not available through the WP REST API. 191 | 192 | #### Schema 193 | ````php 194 | // functions.php 195 | 196 | new RADL( '__VUE_WORDPRESS__', 'vue_wordpress.js', array( 197 | 'routing' => RADL::callback( 'vue_wordpress_routing' ), 198 | 'state' => array( 199 | 'categories' => RADL::endpoint( 'categories'), 200 | 'media' => RADL::endpoint( 'media' ), 201 | 'menus' => RADL::callback( 'vue_wordpress_menus' ), 202 | 'pages' => RADL::endpoint( 'pages' ), 203 | 'posts' => RADL::endpoint( 'posts' ), 204 | 'tags' => RADL::endpoint( 'tags' ), 205 | 'users' => RADL::endpoint( 'users' ), 206 | 'site' => RADL::callback( 'vue_wordpress_site' ), 207 | ), 208 | ) ); 209 | ```` 210 | 211 | #### Initialization 212 | As you can see below, the `__VUE_WORDPRESS__.state` key is used to initialize the Vuex store. 213 | 214 | ````js 215 | // src/store/index.js 216 | 217 | const { state } = __VUE_WORDPRESS__ 218 | 219 | export default new Vuex.Store({ 220 | state, 221 | getters, 222 | mutations, 223 | actions 224 | }) 225 | ```` 226 | *More advanced implementations may want to decouple this localized store from the Vuex store, but I think it decreases the initial complexity.* 227 | 228 | ### Requesting Data 229 | 230 | #### Making WP REST API Requests 231 | 232 | You will not see any WP REST API requests within the components of the theme. Instead, Vuex actions are dispatched signalling a need for asynchronous data. If the data is not available in the store, only then is a request made. When the request is returned the response data is added to the store. 233 | 234 | #### Request Caching 235 | 236 | Every time there is a request for a list of items, a request object is generated once the response is received. 237 | ````js 238 | // Example request object 239 | { 240 | data: [ 121, 221, 83, 4, 23, 76 ], // ids of items 241 | params: { page: 1, per_page: 6 }, 242 | total: 21, 243 | totalPages: 4 244 | } 245 | ```` 246 | This object is added to the store and next time a list of items of the same type and params are requested, the data key is used to create the response instead of querying the server. 247 | 248 | Note that these request objects are not created for requests for a single resource by its identifier because checking the store for them before making a request is trivial. 249 | 250 | ### Actions Api Reference 251 | 252 | #### Request an item of type by its slug 253 | 254 | ````js 255 | this.$store.dispatch('getSingleBySlug', { type, slug, showLoading }) 256 | ```` 257 | - `type` is the key in the store and the endpoint for the resource 258 | - `string` 259 | - required 260 | - `slug` is the alphanumeric identifier for the resource 261 | - `string` 262 | - required 263 | - `showLoading` indicates whether a loading indicator should be shown 264 | - `boolean` 265 | - default: `false` 266 | 267 | ##### Example 268 | ```` js 269 | // request a page with slug 'about' and show loading indicator 270 | this.$store.dispatch('getSingleBySlug', { type: 'pages', slug: 'about', showLoading: true }) 271 | ```` 272 | 273 | #### Request an item of type by its id 274 | 275 | ````js 276 | this.$store.dispatch('getSingleById', { type, id, batch }) 277 | ```` 278 | - `type` is the key in the store and the endpoint for the resource 279 | - `string` 280 | - required 281 | - `id` is the numeric identifier for the resource 282 | - `number` 283 | - required 284 | - `batch` indicates whether a short delay will be added before making the request. During this delay any requests with duplicate ids are ignored and ids of the same type are consolidated into a single request using the `include` WP REST API argument. 285 | - `boolean` 286 | - default: `false` 287 | 288 | *View the [theme demo](http://vue-wordpress.com), open the Network panel of your browser, and navigate to a posts feed to see the batch feature in action.* 289 | 290 | ##### Examples 291 | ```` js 292 | // request a tag with an id of 13 with batching 293 | this.$store.dispatch('getSingleById', { type: 'tags', id: 13, batch: true }) 294 | // request a tag with an id of 21 with batching 295 | this.$store.dispatch('getSingleById', { type: 'tags', id: 21, batch: true }) 296 | 297 | // Even if these are called from different components or instances of the same component, the batched request would look like /wp/v2/tags/?include[]=13&include[]=21&per_page=100 298 | ```` 299 | 300 | #### Request a list of items 301 | 302 | ````js 303 | this.$store.dispatch('getItems', { type, params, showLoading }) 304 | ```` 305 | - `type` is the key in the store and the endpoint for the resource 306 | - `string` 307 | - required 308 | - `params` are the parameters to use for the request 309 | - `Object` 310 | - required 311 | - `showLoading` indicates whether a loading indicator should be shown 312 | - `boolean` 313 | - default: `false` 314 | 315 | ##### Example 316 | ````js 317 | // request the first page of the most recent posts limited to 10 posts per page 318 | this.$store.dispatch('getItems', { type: 'posts', params: [ page: 1, per_page: 10, orderby: 'date', order: 'desc' ] }) 319 | ```` 320 | 321 | ### Getters Api Reference 322 | 323 | You'll notice the arguments for the getters are a subset of their corresponding action arguments. This is because the actions use the getters themeselves to make sure the data is not already in the store before making a request. 324 | 325 | #### Get an item of type by slug 326 | 327 | ````js 328 | this.$store.getters.singleBySlug({ type, slug }) 329 | ```` 330 | 331 | #### Get an item of type by id 332 | 333 | ````js 334 | this.$store.getters.singleById({ type, id }) 335 | ```` 336 | 337 | #### Get a list of items 338 | 339 | ````js 340 | this.$store.getters.requestedItems({ type, params }) 341 | ```` 342 | 343 | 344 | ## SEO and Server Rendered Content 345 | 346 | ### How It Works 347 | 348 | What enables SEO and server rendered content with this theme is the routing. The Vue app routing mirrors that of how Wordpress resolves query strings to templates using the [Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/). So, on the server side, you know exactly what data the app will need when it renders at a specific location. The problem then becomes how to render the app with that data. Technically, the WP loop and template functions could produce output for crawlers, but it would be unusable for the app. Instead, the REST API Data Localizer is used to simulate the relevant GET requests internally corresponding to the WP template loaded, and localize that response data for the app on render. 349 | 350 | 351 | ### The Three Basic Approaches 352 | 353 | Each approach provides either explicit setup or specific examples and includes the php knowledge required to implement. 354 | 355 | #### Pseudo Headless 356 | 357 | *Minimal to no php knowledge required* 358 | 359 | With this implementation, all you need is an index.php file that renders the element the app will mount on. Add your ``, ``, ``, and `` tags, and call `wp_head()` and `wp_footer()`. These calls ensure Wordpress still manages ``, adds `<meta>`, and enqueues scripts and stylesheets as configured. 360 | 361 | ##### Example index.php 362 | ````html 363 | <!DOCTYPE html> 364 | <html <?php language_attributes(); ?>> 365 | <head> 366 | <meta charset="<?php bloginfo( 'charset' ); ?>"> 367 | <meta name="viewport" content="width=device-width, initial-scale=1"> 368 | <?php wp_head(); ?> 369 | </head> 370 | <body> 371 | <div id="vue-wordpress-app"> 372 | <!-- Enhancing this approach, you could show 373 | a loading indicator here while your app js 374 | is parsed and data is fetched --> 375 | </div> 376 | <?php wp_footer(); ?> 377 | </body> 378 | </html> 379 | ```` 380 | 381 | *Optionally, you can remove `wp_head()`, or `wp_footer()`, or even both and hard code in your `<script>`, `<style>` and `<link>` tags. In this case, the REST API Data Localizer would be incompatible and you would need to refactor the initialization of Vuex and Vue Router so they weren't dependent on its localized data.* 382 | 383 | ##### Pseudo Headless SEO 384 | 385 | Crawlers will no longer be able to properly index your site because even if they could parse js, they would still need asynchronous data on initialization. However, if SEO is not a concern of yours, no harm no foul. On the other hand, assuming you call `wp_head()` and the routing is properly synced, social sharing links should still work properly, as `wp_head()` manages meta in the `<head>` of your site. 386 | 387 | #### Preload Data 388 | 389 | *Minimal php knowledge required* 390 | 391 | This approach allows you to eliminate all initial REST API requests, effectively decreasing initial time to interactive, and allowing crawlers that can render js to index your content. Wordpress resolves the query and uses the appropriate template as per the [Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/). Within these templates, you can just make the appropriate requests based on your apps needs. Since the Vue Router route components correspond to the WP templates this becomes straightforward. 392 | 393 | **The examples below will utilize the [REST API Data Localizer](https://github.com/bucky355/rest-api-data-localizer) and assume a schema as shown [here](#schema). If you are confused on its usage follow the link and refer to the docs.** 394 | 395 | 396 | ##### Example single.php 397 | *Assuming you need the post and its featured media, categories, and tags.* 398 | ````php 399 | <?php 400 | get_header(); 401 | 402 | $p = RADL::get( 'state.posts', get_the_ID() ); 403 | RADL::get( 'state.media', $p['featured_media'] ); 404 | RADL::get( 'state.categories', $p['categories'] ); 405 | RADL::get( 'state.tags', $p['tags'] ); 406 | 407 | get_footer(); 408 | ```` 409 | 410 | ##### Example category.php 411 | *Assuming you need the category, and N posts from X page that have that category 412 | ( N refers to the posts per page set in Settings > Reading ).* 413 | ````php 414 | <?php 415 | get_header(); 416 | 417 | $category_id = get_query_var('cat'); 418 | $per_page = RADL::get( 'state.site' )['posts_per_page']; 419 | $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; 420 | 421 | 422 | RADL::get( 'state.categories', $category_id ); 423 | RADL::get( 'state.posts', array( 424 | 'page' => $paged, 425 | 'per_page' => $per_page, 426 | 'categories' => $category_id 427 | ) ); 428 | 429 | get_footer(); 430 | 431 | ```` 432 | *The preceding examples use `get_header()` and `wp_footer()`, which are Wordpress functions that include the `header.php` and `footer.php` files respectively. If these functions were used in the [Pseudo Headless Example](#example-indexphp) there would be three files that looked like this:* 433 | 434 | **index.php** 435 | ````html 436 | <?php 437 | get_header(); ?> 438 | <!-- Enhancing this approach, you could show 439 | a loading indicator here while your app js 440 | is parsed and data is fetched --> 441 | <?php 442 | get_footer(); 443 | ```` 444 | **header.php** 445 | ````html 446 | <!DOCTYPE html> 447 | <html <?php language_attributes(); ?>> 448 | <head> 449 | <meta charset="<?php bloginfo( 'charset' ); ?>"> 450 | <meta name="viewport" content="width=device-width, initial-scale=1"> 451 | <?php wp_head(); ?> 452 | </head> 453 | <body> 454 | <div id="vue-wordpress-app"> 455 | ```` 456 | **footer.php** 457 | ````html 458 | </div><!-- #vue-wordpress-app --> 459 | <?php wp_footer(); ?> 460 | </body> 461 | </html> 462 | ```` 463 | ##### Preload Data SEO 464 | 465 | When you preload required data, the Vue instance has everything it needs on render. This allows crawlers that can render js to index your site appropriately. However, it should be mentioned that just because these crawlers have the capability of rendering the js before indexing it doesn't mean they will. Here's [an article](https://www.elephate.com/blog/javascript-seo-experiment/) about it for more information. 466 | 467 | Of course the added UX benefit of this approach is that the end users will be able to interact with the content pretty much immediately ( after the js is parse and rendered, and not have to wait for the app to fetch data ). 468 | 469 | #### Progressive Enhancement 470 | 471 | *Intermediate php knowledge required* 472 | 473 | The third and final approach involves recreating the php templates with the REST API Data Localizer corresponding to your different route components. The beauty of the approach is that you don't have to ( and probably shouldn't ) fully recreate how the Vue instance renders the app in your templates. You can opt for a progressive enhancement approach, creating skeleton templates that Vue enhances on render. By default, the theme fully recreates these templates, but it is a starter theme, so this makes sense. You can see this by viewing the [theme demo](http://vue-wordpress-demo.bshiluk.com), disabling javascript in the developer tools, and reloading the site. 474 | 475 | ##### Example home.php 476 | ````html 477 | <?php 478 | get_header(); 479 | $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; 480 | $per_page = RADL::get( 'state.site' )['posts_per_page']; 481 | $posts = RADL::get( 'state.posts', array( 'page' => 1, 'per_page' => $per_page ) ); ?> 482 | 483 | <main> 484 | <section> 485 | 486 | <?php 487 | foreach ( $posts as $p ) { 488 | set_query_var( 'vw_post', $p ); 489 | get_template_part( 'template-parts/post-item' ); 490 | } ?> 491 | 492 | </section> 493 | 494 | <?php 495 | get_template_part( 'template-parts/pagination' ); ?> 496 | 497 | </main> 498 | 499 | <?php 500 | get_footer(); 501 | ```` 502 | - `get_template_part()` is a wp function that includes the php template using the argument passed 503 | - `set_query_var()` is another wp function that sets variables you can access through `get_query_var()` in your template-part ( *you can't pass arguments to `get_template_part()`* ) 504 | - Notice that the query vars set correlate to what's passed to the props of the vue component 505 | - If you want to see the post-item and pagination files refer to the `/template-parts/post-item.php` and `/template-parts/pagination.php` 506 | 507 | ##### Progressive Enhancement SEO 508 | 509 | Crawlers can index your content and you don't have to rely on their ability to render js. Your users get content instantly without waiting for the Vue instance to render. The perceived load time is improved further as users get content instantly and enhanced with functionality and additional content once the Vue instance renders. 510 | 511 | ## Upcoming Features 512 | 513 | I do plan to extend the functionality of the theme depending on whether developers find it useful. Some things I plan to add support for ( in no particular order ) include: 514 | - Search 515 | - Comments 516 | - Page Templates 517 | - Post Formats 518 | - Widgets 519 | - ACF 520 | - Dynamic routing for custom post types 521 | 522 | ## Final Thoughts 523 | 524 | I hope you found this documentation useful. If there are any other topics you would like me to cover, or need clarification on feel free to add an issue. 525 | -------------------------------------------------------------------------------- /author.php: -------------------------------------------------------------------------------- 1 | <?php 2 | get_header(); 3 | $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; 4 | $per_page = RADL::get( 'state.site' )['posts_per_page']; 5 | $author_id = get_query_var( 'author' ); 6 | $author = RADL::get( 'state.users', $author_id ); 7 | $posts = RADL::get( 'state.posts', array( 'page' => $paged, 'per_page' => $per_page, 'author' => $author_id ) ); ?> 8 | 9 | <main> 10 | 11 | <header> 12 | 13 | <h1>Posts by <?php echo $author['name']; ?></h1> 14 | 15 | </header> 16 | 17 | <section> 18 | 19 | <?php 20 | foreach ( $posts as $p ) { 21 | set_query_var( 'vw_post', $p ); 22 | get_template_part( 'template-parts/post-item' ); 23 | } ?> 24 | 25 | </section> 26 | 27 | <?php 28 | get_template_part( 'template-parts/pagination' ); ?> 29 | 30 | </main> 31 | 32 | <?php 33 | get_footer(); 34 | -------------------------------------------------------------------------------- /category.php: -------------------------------------------------------------------------------- 1 | <?php 2 | get_header(); 3 | $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; 4 | $per_page = RADL::get( 'state.site' )['posts_per_page']; 5 | $category_id = get_query_var('cat'); 6 | $category = RADL::get( 'state.categories', $category_id ); 7 | $posts = RADL::get( 'state.posts', array( 'page' => $paged, 'per_page' => $per_page, 'categories' => $category_id ) ); ?> 8 | 9 | <main> 10 | 11 | <header> 12 | 13 | <h1>Archive for <?php echo $category['name']; ?></h1> 14 | 15 | </header> 16 | 17 | <section> 18 | 19 | <?php 20 | foreach ( $posts as $p ) { 21 | set_query_var( 'vw_post', $p ); 22 | get_template_part( 'template-parts/post-item' ); 23 | } ?> 24 | 25 | </section> 26 | 27 | <?php 28 | get_template_part( 'template-parts/pagination' ); ?> 29 | 30 | </main> 31 | 32 | <?php 33 | get_footer(); 34 | -------------------------------------------------------------------------------- /footer.php: -------------------------------------------------------------------------------- 1 | </div><!-- #vue-wordpress-app --> 2 | 3 | <?php wp_footer();?> 4 | 5 | </body> 6 | 7 | </html> -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * Set up theme options on activation 5 | */ 6 | 7 | function vue_wordpress_setup() 8 | { 9 | 10 | add_theme_support( 'title-tag' ); 11 | 12 | add_theme_support( 'post-thumbnails' ); 13 | 14 | add_theme_support( 'custom-logo', array( 15 | 'height' => 160, 16 | 'width' => 160, 17 | ) ); 18 | 19 | register_nav_menus( array( 20 | 'main' => 'Main Menu', 21 | ) ); 22 | 23 | } 24 | 25 | add_action( 'after_setup_theme', 'vue_wordpress_setup' ); 26 | 27 | /** 28 | * Load scripts and styles 29 | */ 30 | 31 | function vue_wordpress_scripts() 32 | { 33 | // Styles 34 | wp_enqueue_style( 'style.css', get_template_directory_uri() . '/style.css' ); 35 | wp_enqueue_style('vue_wordpress.css', get_template_directory_uri() . '/dist/vue-wordpress.css'); 36 | 37 | // Scripts 38 | 39 | // Enable For Production - Disable for Development 40 | wp_enqueue_script('vue_wordpress.js', get_template_directory_uri() . '/dist/vue-wordpress.js', array(), null, true); 41 | 42 | // Enable For Development - Remove for Production 43 | // wp_enqueue_script( 'vue_wordpress.js', 'http://localhost:8080/vue-wordpress.js', array(), false, true ); 44 | } 45 | 46 | add_action( 'wp_enqueue_scripts', 'vue_wordpress_scripts' ); 47 | 48 | /** 49 | * Declare REST API Data Localizer dependency 50 | */ 51 | 52 | if ( !class_exists( 'RADL' ) ) { 53 | add_action( 'admin_notices', function () { 54 | echo '<div class="error"><p>REST API Data Localizer not activated. To use this theme go to <a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">plugins</a> to download and/or activate REST API Data Localizer.</p></div>'; 55 | } ); 56 | return; 57 | } 58 | 59 | /** 60 | * Initialize REST API Data Localizer 61 | */ 62 | 63 | new RADL( '__VUE_WORDPRESS__', 'vue_wordpress.js', array( 64 | 'routing' => RADL::callback( 'vue_wordpress_routing' ), 65 | 'state' => array( 66 | 'categories' => RADL::endpoint( 'categories'), 67 | 'media' => RADL::endpoint( 'media' ), 68 | 'menus' => RADL::callback( 'vue_wordpress_menus' ), 69 | 'pages' => RADL::endpoint( 'pages' ), 70 | 'posts' => RADL::endpoint( 'posts' ), 71 | 'tags' => RADL::endpoint( 'tags' ), 72 | 'users' => RADL::endpoint( 'users' ), 73 | 'site' => RADL::callback( 'vue_wordpress_site' ), 74 | ), 75 | ) ); 76 | 77 | /** 78 | * REST API Data Localizer callbacks 79 | */ 80 | 81 | function vue_wordpress_routing() 82 | { 83 | $routing = array( 84 | 'category_base' => get_option( 'category_base' ), 85 | 'page_on_front' => null, 86 | 'page_for_posts' => null, 87 | 'permalink_structure' => get_option( 'permalink_structure' ), 88 | 'show_on_front' => get_option( 'show_on_front' ), 89 | 'tag_base' => get_option( 'tag_base' ), 90 | 'url' => get_bloginfo( 'url' ) 91 | ); 92 | 93 | if ( $routing['show_on_front'] === 'page' ) { 94 | $front_page_id = get_option( 'page_on_front' ); 95 | $posts_page_id = get_option( 'page_for_posts' ); 96 | 97 | if ( $front_page_id ) { 98 | $front_page = get_post( $front_page_id ); 99 | $routing['page_on_front'] = $front_page->post_name; 100 | } 101 | 102 | if ( $posts_page_id ) { 103 | $posts_page = get_post( $posts_page_id ); 104 | $routing['page_for_posts'] = $posts_page->post_name; 105 | } 106 | 107 | } 108 | 109 | return $routing; 110 | } 111 | 112 | function vue_wordpress_menus() 113 | { 114 | $menus = array(); 115 | // $locations is an array where ([NAME] = MENU_ID); 116 | $locations = get_nav_menu_locations(); 117 | 118 | foreach ( array_keys( $locations ) as $name ) { 119 | $id = $locations[$name]; 120 | $menu = array(); 121 | $menu_items = wp_get_nav_menu_items( $id ); 122 | 123 | foreach ( $menu_items as $i ) { 124 | 125 | array_push( $menu, array( 126 | 'id' => $i->ID, 127 | 'parent' => $i->menu_item_parent, 128 | 'target' => $i->target, 129 | 'content' => $i->title, 130 | 'title' => $i->attr_title, 131 | 'url' => $i->url, 132 | ) ); 133 | 134 | } 135 | 136 | $menus[$name] = $menu; 137 | } 138 | 139 | return $menus; 140 | } 141 | 142 | function vue_wordpress_site() 143 | { 144 | return array( 145 | 'description' => get_bloginfo( 'description' ), 146 | 'docTitle' => '', 147 | 'loading' => false, 148 | 'logo' => get_theme_mod( 'custom_logo' ), 149 | 'name' => get_bloginfo( 'name' ), 150 | 'posts_per_page' => get_option( 'posts_per_page' ), 151 | 'url' => get_bloginfo( 'url' ) 152 | ); 153 | 154 | } 155 | 156 | /** 157 | * In template functions 158 | */ 159 | 160 | function vue_wordpress_min_read( $content ) 161 | { 162 | $length = count( explode( ' ', $content ) ) + 1; 163 | $time = $length / 200; 164 | 165 | if ( is_float( $time ) ) { 166 | $time = ceil( $time ); 167 | } 168 | 169 | return $time . 'min read'; 170 | } 171 | -------------------------------------------------------------------------------- /header.php: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | 3 | <html <?php language_attributes(); ?>> 4 | 5 | <head> 6 | 7 | <meta charset="<?php bloginfo( 'charset' ); ?>"> 8 | 9 | <meta name="viewport" content="width=device-width, initial-scale=1"> 10 | 11 | <?php wp_head(); ?> 12 | 13 | </head> 14 | 15 | <body> 16 | 17 | <div id="vue-wordpress-app" class="container"> 18 | 19 | <?php 20 | get_template_part('template-parts/site-branding'); 21 | set_query_var( 'vw_nav_menu', 'main' ); 22 | get_template_part('template-parts/nav-menu'); 23 | -------------------------------------------------------------------------------- /home.php: -------------------------------------------------------------------------------- 1 | <?php 2 | get_header(); 3 | $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; 4 | $per_page = RADL::get( 'state.site' )['posts_per_page']; 5 | $posts = RADL::get( 'state.posts', array( 'page' => 1, 'per_page' => $per_page ) ); ?> 6 | 7 | <main> 8 | 9 | <section> 10 | 11 | <?php 12 | foreach ( $posts as $p ) { 13 | set_query_var( 'vw_post', $p ); 14 | get_template_part( 'template-parts/post-item' ); 15 | } ?> 16 | 17 | </section> 18 | 19 | <?php 20 | get_template_part( 'template-parts/pagination' ); ?> 21 | 22 | </main> 23 | 24 | <?php 25 | get_footer(); -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | <?php 2 | get_header(); 3 | get_template_part('template-parts/site-loading'); 4 | get_footer(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-wordpress", 3 | "private": true, 4 | "scripts": { 5 | "dev": "webpack-dev-server --env=dev", 6 | "build": "webpack --env=prod" 7 | }, 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "@babel/core": "^7.2.2", 11 | "axios": "^0.18.0", 12 | "babel-core": "^6.26.3", 13 | "babel-loader": "^8.0.5", 14 | "babel-preset-env": "^1.7.0", 15 | "babel-preset-stage-3": "^6.24.1", 16 | "cross-env": "^5.2.0", 17 | "css-loader": "^2.1.0", 18 | "mini-css-extract-plugin": "^0.5.0", 19 | "vue": "^2.6.6", 20 | "vue-loader": "^15.6.2", 21 | "vue-router": "^3.0.2", 22 | "vue-style-loader": "^4.1.2", 23 | "vue-template-compiler": "^2.6.6", 24 | "vuex": "^3.1.0", 25 | "webpack": "^4.29.3", 26 | "webpack-cli": "^3.2.3", 27 | "webpack-dev-server": "^3.1.14" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /page.php: -------------------------------------------------------------------------------- 1 | <?php 2 | get_header(); 3 | $page = RADL::get( 'state.pages', get_the_ID() ); ?> 4 | 5 | <main> 6 | 7 | <h1><?php echo $page['title']['rendered'];?></h1> 8 | 9 | <div><?php echo $page['content']['rendered'];?></div> 10 | 11 | </main> 12 | 13 | <?php get_footer(); ?> -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bshiluk/vue-wordpress/d4143e7f68093e6380c29cc3ab5d5a1d72cab98a/screenshot.png -------------------------------------------------------------------------------- /single.php: -------------------------------------------------------------------------------- 1 | <?php 2 | get_header(); 3 | $single = RADL::get( 'state.posts', get_the_ID() ); ?> 4 | 5 | <main> 6 | 7 | <article> 8 | 9 | <header> 10 | 11 | <?php 12 | if ( $single['featured_media']) { 13 | set_query_var('vw_responsive_image', array( 14 | 'id' => $single['featured_media'], 15 | 'sizes' => '(max-width: 1200px) 100vw, 1200px' 16 | )); 17 | get_template_part('template-parts/responsive-image'); 18 | } ?> 19 | 20 | <h1><?php echo $single['title']['rendered']; ?></h1> 21 | 22 | <?php 23 | set_query_var('vw_post', $single); 24 | get_template_part('template-parts/post-meta'); 25 | get_template_part('template-parts/post-taxonomies'); ?> 26 | 27 | </header> 28 | 29 | <div><?php echo $single['content']['rendered']; ?></div> 30 | 31 | </article> 32 | 33 | </main> 34 | 35 | <?php 36 | get_footer(); -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div 3 | id="vue-wordpress-app" 4 | class="container" 5 | @click="handleClicks" 6 | > 7 | <div 8 | class="site-branding" 9 | @click="$router.push('/')" 10 | > 11 | <img 12 | v-if="logo" 13 | class="logo" 14 | :src="logo.source_url" 15 | :alt="logo.alt_text" 16 | /> 17 | <span>{{ site.name }}</span> 18 | </div> 19 | <nav-menu 20 | class="main-menu" 21 | name="main" 22 | /> 23 | <transition 24 | name="fade" 25 | mode="out-in" 26 | @after-leave="updateScroll" 27 | > 28 | <router-view :key="$route.path" /> 29 | </transition> 30 | <transition name="fade"> 31 | <site-loading v-if="loading" /> 32 | </transition> 33 | </div> 34 | </template> 35 | 36 | <script> 37 | import NavMenu from '@/components/template-parts/NavMenu' 38 | import SiteLoading from '@/components/utility/SiteLoading' 39 | 40 | export default { 41 | components: { 42 | NavMenu, 43 | SiteLoading 44 | }, 45 | data() { 46 | return { 47 | site: this.$store.state.site 48 | } 49 | }, 50 | computed: { 51 | loading() { 52 | return this.$store.state.site.loading 53 | }, 54 | logo() { 55 | if (this.site.logo) { 56 | return this.$store.getters.singleById({ type: 'media', id: this.site.logo }) 57 | } 58 | } 59 | }, 60 | methods: { 61 | getLinkEl(el) { 62 | while (el.parentNode) { 63 | if (el.tagName === 'A') return el 64 | el = el.parentNode 65 | } 66 | }, 67 | handleClicks (e) { 68 | const a = this.getLinkEl(e.target) 69 | if (a && a.href.includes(this.site.url)) { 70 | const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = e 71 | // don't handle if has class 'no-router' 72 | if (a.className.includes('no-router')) return 73 | // don't handle with control keys 74 | if (metaKey || altKey || ctrlKey || shiftKey) return 75 | // don't handle when preventDefault called 76 | if (defaultPrevented) return 77 | // don't handle right clicks 78 | if (button !== undefined && button !== 0) return 79 | // don't handle if `target="_blank"` 80 | if (a.target && a.target.includes('_blank')) return 81 | // don't handle same page links 82 | let currentURL = new URL(a.href, window.location.href) 83 | if (currentURL && currentURL.pathname === window.location.pathname) { 84 | // if same page link has same hash prevent default reload 85 | if (currentURL.hash === window.location.hash) e.preventDefault() 86 | return 87 | } 88 | // Prevent default and push to vue-router 89 | e.preventDefault() 90 | let path = a.href.replace(this.site.url, '') 91 | this.$router.push(path) 92 | } 93 | }, 94 | updateScroll() { 95 | window.scroll(0,0) 96 | } 97 | } 98 | } 99 | </script> 100 | 101 | <style> 102 | 103 | .site-branding { 104 | display: inline-block; 105 | padding: 1rem 0; 106 | cursor: pointer 107 | } 108 | 109 | .logo { 110 | display: inline-block; 111 | vertical-align: middle; 112 | height: 4.8rem; 113 | width: auto; 114 | margin: 0 .4rem 0 0; 115 | } 116 | 117 | .site-branding>span { 118 | vertical-align: middle; 119 | font-size: 2.4rem; 120 | font-weight: bold; 121 | } 122 | 123 | .main-menu { 124 | position: sticky; 125 | top: -1px; 126 | z-index: 2; 127 | display: flex; 128 | flex-flow: row wrap; 129 | justify-content: flex-start; 130 | background: #fff; 131 | padding: 1rem 0; 132 | border-top: 1px solid rgba(0,0,0,.05); 133 | border-bottom: 1px solid rgba(0,0,0,.05); 134 | } 135 | 136 | .main-menu>a { 137 | margin-right: 2%; 138 | } 139 | 140 | /* Vue transition classes 141 | -------------------------------------------- */ 142 | 143 | .fade-enter-active { 144 | transition: opacity .4s cubic-bezier(.4,0,0,1); 145 | } 146 | 147 | .fade-leave-active { 148 | transition: opacity .2s cubic-bezier(.4,0,0,1); 149 | } 150 | 151 | .fade-enter-to, 152 | .fade-leave { 153 | opacity: 1; 154 | } 155 | 156 | .fade-enter, 157 | .fade-leave-to { 158 | opacity: 0; 159 | } 160 | 161 | </style> 162 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | 4 | const { url } = __VUE_WORDPRESS__.routing 5 | 6 | const ajax = axios.create( 7 | { 8 | baseURL: `${url}/wp-json/wp/v2/`, 9 | headers: { 10 | 'Accept': 'application/json', 11 | 'Content-Type': 'application/json' 12 | } 13 | } 14 | ) 15 | 16 | const batchRequest = {} 17 | 18 | function addBatchId(type, id) { 19 | if ( ! batchRequest[type] ) { 20 | batchRequest[type] = {} 21 | batchRequest[type].ids = [id] 22 | batchRequest[type].request = new Promise((resolve, reject) => { 23 | setTimeout(() => { 24 | resolve(batchRequestIds(type)) 25 | batchRequest[type] = null 26 | }, 100) 27 | }) 28 | } else if( ! batchRequest[type].ids.includes(id) ){ 29 | batchRequest[type].ids.push(id) 30 | } 31 | } 32 | 33 | function batchRequestIds(type) { 34 | return ajax.get(`/${type}/`, { params: { include: batchRequest[type].ids, per_page: 100 } }) 35 | } 36 | 37 | export const fetchSingleById = ({ type, id, batch = false }) => { 38 | if (batch) { 39 | addBatchId(type, id) 40 | return batchRequest[type].request 41 | } else { 42 | return ajax.get(`/${type}/${id}`) 43 | } 44 | } 45 | 46 | export const fetchSingle = ({ type, params = {} }) => { 47 | return ajax.get(`/${type}/`, { params }) 48 | } 49 | 50 | export const fetchItems = ({ type, params = {} }) => { 51 | return ajax.get(`/${type}/`, { params }) 52 | } 53 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | 6 | new Vue({ 7 | el: '#vue-wordpress-app', 8 | render: h => h(App), 9 | router, 10 | store 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/404.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <h1>{{ title }}</h1> 4 | <p>{{ message }}</p> 5 | </main> 6 | </template> 7 | 8 | <script> 9 | export default { 10 | data() { 11 | return { 12 | title: '404 - Page Not Found', 13 | message: 'Apparently nothing exists at this location.' 14 | } 15 | }, 16 | created() { 17 | this.$store.dispatch('updateDocTitle', { parts: [ 'Page not found', this.$store.state.site.name ] }) 18 | } 19 | } 20 | </script> 21 | -------------------------------------------------------------------------------- /src/components/AuthorArchive.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <header> 4 | <h1>{{ title }}</h1> 5 | </header> 6 | <section v-if="posts"> 7 | <post-item 8 | v-for="post in posts" 9 | :key="post.id" 10 | :post="post" 11 | /> 12 | <pagination 13 | v-if="totalPages > 1" 14 | :total="totalPages" 15 | :current="page" 16 | /> 17 | </section> 18 | </main> 19 | </template> 20 | 21 | <script> 22 | import PostItem from '@/components/template-parts/PostItem' 23 | import Pagination from '@/components/template-parts/Pagination' 24 | 25 | export default { 26 | name: 'AuthorArchive', 27 | components: { 28 | PostItem, 29 | Pagination 30 | }, 31 | props: { 32 | page: { 33 | type: Number, 34 | required: true 35 | }, 36 | slug: { 37 | type: String, 38 | required: false 39 | } 40 | }, 41 | data () { 42 | return { 43 | authorRequest: { 44 | type: 'users', 45 | slug: this.slug 46 | }, 47 | postsRequest: { 48 | type: 'posts', 49 | params: { 50 | per_page: this.$store.state.site.posts_per_page, 51 | page: this.page, 52 | author: null 53 | }, 54 | showLoading: true 55 | }, 56 | totalPages: 0 57 | } 58 | }, 59 | computed: { 60 | author() { 61 | return this.$store.getters.singleBySlug(this.authorRequest) 62 | }, 63 | posts() { 64 | if (this.author) { 65 | return this.$store.getters.requestedItems(this.postsRequest) 66 | } 67 | }, 68 | title() { 69 | return `Posts by ${this.author ? this.author.name : ''}` 70 | } 71 | }, 72 | methods: { 73 | getAuthor() { 74 | return this.$store.dispatch('getSingleBySlug', this.authorRequest).then(() => { 75 | this.setAuthorParam() 76 | this.$store.dispatch('updateDocTitle', { parts: [ this.author.name, this.$store.state.site.name ] }) 77 | }) 78 | }, 79 | getPosts() { 80 | return this.$store.dispatch('getItems', this.postsRequest) 81 | }, 82 | setAuthorParam() { 83 | this.postsRequest.params.author = this.author.id 84 | }, 85 | setTotalPages() { 86 | this.totalPages = this.$store.getters.totalPages(this.postsRequest) 87 | } 88 | }, 89 | created() { 90 | this.getAuthor().then(() => this.getPosts()).then(() => this.setTotalPages()) 91 | } 92 | } 93 | </script> 94 | -------------------------------------------------------------------------------- /src/components/CategoryArchive.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <header> 4 | <h1>{{ title }}</h1> 5 | </header> 6 | <section v-if="posts"> 7 | <post-item 8 | v-for="post in posts" 9 | :key="post.id" 10 | :post="post" 11 | /> 12 | <pagination 13 | v-if="totalPages > 1" 14 | :total="totalPages" 15 | :current="page" 16 | /> 17 | </section> 18 | </main> 19 | </template> 20 | 21 | <script> 22 | import PostItem from '@/components/template-parts/PostItem' 23 | import Pagination from '@/components/template-parts/Pagination' 24 | 25 | export default { 26 | name: 'CategoryArchive', 27 | components: { 28 | PostItem, 29 | Pagination 30 | }, 31 | props: { 32 | page: { 33 | type: Number, 34 | required: true 35 | }, 36 | slug: { 37 | type: String, 38 | required: true 39 | }, 40 | }, 41 | data () { 42 | return { 43 | postsRequest: { 44 | type: 'posts', 45 | params: { 46 | per_page: this.$store.state.site.posts_per_page, 47 | page: this.page, 48 | categories: null 49 | }, 50 | showLoading: true 51 | }, 52 | categoryRequest: { 53 | type: 'categories', 54 | slug: this.slug 55 | }, 56 | totalPages: 0 57 | } 58 | }, 59 | computed: { 60 | category() { 61 | return this.$store.getters.singleBySlug(this.categoryRequest) 62 | }, 63 | posts() { 64 | if (this.category) { 65 | return this.$store.getters.requestedItems(this.postsRequest) 66 | } 67 | }, 68 | title() { 69 | return `Archive for ${this.category ? this.category.name : ''}` 70 | } 71 | }, 72 | methods: { 73 | getCategory() { 74 | return this.$store.dispatch('getSingleBySlug', this.categoryRequest).then(() => { 75 | this.setPostsRequestParams() 76 | this.$store.dispatch('updateDocTitle', { parts: [ this.category.name, this.$store.state.site.name ] }) 77 | }) 78 | }, 79 | getPosts() { 80 | return this.$store.dispatch('getItems', this.postsRequest) 81 | }, 82 | setPostsRequestParams() { 83 | this.postsRequest.params.categories = this.category.id 84 | }, 85 | setTotalPages() { 86 | this.totalPages = this.$store.getters.totalPages(this.postsRequest) 87 | } 88 | }, 89 | created() { 90 | this.getCategory().then(() => this.getPosts()).then(() => this.setTotalPages()) 91 | } 92 | } 93 | </script> 94 | -------------------------------------------------------------------------------- /src/components/DateArchive.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <header> 4 | <h1>{{ title }}</h1> 5 | </header> 6 | <section v-if="posts"> 7 | <post-item 8 | v-for="post in posts" 9 | :key="post.id" 10 | :post="post" 11 | /> 12 | <pagination 13 | v-if="totalPages > 1" 14 | :total="totalPages" 15 | :current="page" 16 | /> 17 | </section> 18 | </main> 19 | </template> 20 | 21 | <script> 22 | import PostItem from '@/components/template-parts/PostItem' 23 | import Pagination from '@/components/template-parts/Pagination' 24 | 25 | export default { 26 | name: 'DateArchive', 27 | components: { 28 | PostItem, 29 | Pagination 30 | }, 31 | props: { 32 | page: { 33 | type: Number, 34 | required: true 35 | }, 36 | year: { 37 | type: String, 38 | required: true 39 | }, 40 | month: { 41 | type: String, 42 | required: false 43 | }, 44 | day: { 45 | type: String, 46 | required: false 47 | } 48 | }, 49 | data () { 50 | return { 51 | totalPages: 0, 52 | request: { 53 | type: 'posts', 54 | params: { 55 | per_page: this.$store.state.site.posts_per_page, 56 | page: this.page, 57 | after: this.after, 58 | before: this.before 59 | }, 60 | showLoading: true 61 | } 62 | } 63 | }, 64 | computed: { 65 | before() { 66 | let before = new Date(this.after) 67 | if (this.day) { 68 | before.setUTCDate(before.getUTCDate() + 1) 69 | } else if (this.month) { 70 | before.setUTCMonth(before.getUTCMonth() + 1) 71 | } else { 72 | before.setUTCFullYear(before.getUTCFullYear() + 1) 73 | } 74 | return before.toISOString() 75 | }, 76 | after() { 77 | return `${this.year}${this.month ? '-' + this.month : '-01'}${this.day ? '-' + this.day : '-01'}T00:00:00.000Z` 78 | }, 79 | posts() { 80 | return this.$store.getters.requestedItems(this.request) 81 | }, 82 | title() { 83 | let options = { year: 'numeric' } 84 | if (this.month){ 85 | options.month = 'long' 86 | if (this.day) options.day = 'numeric' 87 | } 88 | return `Archive for ${new Date(this.after.replace('T0', 'T1')).toLocaleDateString('en-US', options)}` 89 | } 90 | }, 91 | methods: { 92 | getPosts() { 93 | return this.$store.dispatch('getItems', this.request) 94 | }, 95 | setAfterParam() { 96 | this.request.params.after = `${this.year}${this.month ? '-' + this.month : '-01'}${this.day ? '-' + this.day : '-01'}T00:00:00.000Z` 97 | }, 98 | setBeforeParam() { 99 | let before = new Date(this.request.params.after) 100 | if (this.day) { 101 | before.setUTCDate(before.getUTCDate() + 1) 102 | } else if (this.month) { 103 | before.setUTCMonth(before.getUTCMonth() + 1) 104 | } else { 105 | before.setUTCFullYear(before.getUTCFullYear() + 1) 106 | } 107 | this.request.params.before = before.toISOString() 108 | }, 109 | setTotalPages() { 110 | this.totalPages = this.$store.getters.totalPages(this.request) 111 | } 112 | }, 113 | created() { 114 | this.setAfterParam() 115 | this.setBeforeParam() 116 | this.getPosts().then(() => this.setTotalPages()) 117 | } 118 | } 119 | </script> 120 | -------------------------------------------------------------------------------- /src/components/Home.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <section v-if="posts.length"> 4 | <post-item 5 | v-for="post in posts" 6 | :key="post.id" 7 | :post="post" 8 | /> 9 | <pagination 10 | v-if="totalPages > 1" 11 | :total="totalPages" 12 | :current="page" 13 | /> 14 | </section> 15 | </main> 16 | </template> 17 | 18 | <script> 19 | import PostItem from '@/components/template-parts/PostItem' 20 | import Pagination from '@/components/template-parts/Pagination' 21 | 22 | export default { 23 | name: 'Home', 24 | components: { 25 | PostItem, 26 | Pagination 27 | }, 28 | props: { 29 | page: { 30 | type: Number, 31 | required: true 32 | } 33 | }, 34 | data() { 35 | return { 36 | request: { 37 | type: 'posts', 38 | params: { 39 | per_page: this.$store.state.site.posts_per_page, 40 | page: this.page 41 | }, 42 | showLoading: true 43 | }, 44 | totalPages: 0 45 | } 46 | }, 47 | computed: { 48 | posts() { 49 | return this.$store.getters.requestedItems(this.request) 50 | } 51 | }, 52 | methods: { 53 | getPosts() { 54 | return this.$store.dispatch('getItems', this.request) 55 | }, 56 | setTotalPages() { 57 | this.totalPages = this.$store.getters.totalPages(this.request) 58 | } 59 | }, 60 | created() { 61 | this.getPosts().then(() => this.setTotalPages()) 62 | } 63 | } 64 | </script> 65 | -------------------------------------------------------------------------------- /src/components/Page.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <div v-if="page"> 4 | <h1 v-html="page.title.rendered"></h1> 5 | <div v-html="page.content.rendered"></div> 6 | </div> 7 | </main> 8 | </template> 9 | 10 | <script> 11 | 12 | export default { 13 | name: 'Page', 14 | props: { 15 | slug: { 16 | type: String, 17 | required: true 18 | } 19 | }, 20 | data() { 21 | return { 22 | request: { 23 | type: 'pages', 24 | slug: this.slug, 25 | showLoading: true 26 | } 27 | } 28 | }, 29 | computed: { 30 | page() { 31 | return this.$store.getters.singleBySlug(this.request) 32 | } 33 | }, 34 | methods: { 35 | getPage () { 36 | this.$store.dispatch('getSingleBySlug', this.request).then(() => { 37 | if (this.page) { 38 | this.$store.dispatch('updateDocTitle', { parts: [ this.page.title.rendered, this.$store.state.site.name] }) 39 | } else { 40 | this.$router.replace('/404') 41 | } 42 | }) 43 | } 44 | }, 45 | created () { 46 | this.getPage() 47 | } 48 | } 49 | </script> 50 | -------------------------------------------------------------------------------- /src/components/Single.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <article v-if="post"> 4 | <header> 5 | <responsive-image 6 | v-if="post.featured_media" 7 | :media-id="post.featured_media" 8 | :sizes="'(max-width: 1200px) 100vw, 1200px'" 9 | /> 10 | <h1 v-html="post.title.rendered"></h1> 11 | <post-meta :post="post" /> 12 | <post-taxonomies :post="post" /> 13 | </header> 14 | <div v-html="post.content.rendered"></div> 15 | </article> 16 | </main> 17 | </template> 18 | 19 | <script> 20 | import ResponsiveImage from '@/components/utility/ResponsiveImage' 21 | import PostMeta from '@/components/utility/PostMeta' 22 | import PostTaxonomies from '@/components/utility/PostTaxonomies' 23 | 24 | export default { 25 | name: 'Single', 26 | components: { 27 | ResponsiveImage, 28 | PostMeta, 29 | PostTaxonomies 30 | }, 31 | props: { 32 | slug: { 33 | type: String, 34 | required: false 35 | } 36 | }, 37 | data() { 38 | return { 39 | request: { 40 | type: 'posts', 41 | slug: this.slug, 42 | showLoading: true } 43 | } 44 | }, 45 | computed: { 46 | post() { 47 | return this.$store.getters.singleBySlug(this.request) 48 | } 49 | }, 50 | methods: { 51 | getPost() { 52 | this.$store.dispatch('getSingleBySlug', this.request).then(() => { 53 | this.$store.dispatch('updateDocTitle', { parts: [ this.post.title.rendered, this.$store.state.site.name ] }) 54 | }) 55 | } 56 | }, 57 | created() { 58 | this.getPost() 59 | } 60 | } 61 | </script> 62 | -------------------------------------------------------------------------------- /src/components/TagArchive.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <header> 4 | <h1>{{ title }}</h1> 5 | </header> 6 | <section v-if="posts"> 7 | <post-item 8 | v-for="post in posts" 9 | :key="post.id" 10 | :post="post" 11 | /> 12 | <pagination 13 | v-if="totalPages > 1" 14 | :total="totalPages" 15 | :current="page" 16 | /> 17 | </section> 18 | </main> 19 | </template> 20 | 21 | <script> 22 | import PostItem from '@/components/template-parts/PostItem' 23 | import Pagination from '@/components/template-parts/Pagination' 24 | 25 | export default { 26 | name: 'TagArchive', 27 | components: { 28 | PostItem, 29 | Pagination 30 | }, 31 | props: { 32 | page: { 33 | type: Number, 34 | required: true 35 | }, 36 | slug: { 37 | type: String, 38 | required: true 39 | }, 40 | }, 41 | data () { 42 | return { 43 | postsRequest: { 44 | type: 'posts', 45 | params: { 46 | per_page: this.$store.state.site.posts_per_page, 47 | page: this.page, 48 | tags: null 49 | }, 50 | showLoading: true 51 | }, 52 | tagRequest: { 53 | type: 'tags', 54 | slug: this.slug 55 | }, 56 | totalPages: 0 57 | } 58 | }, 59 | computed: { 60 | tag() { 61 | return this.$store.getters.singleBySlug(this.tagRequest) 62 | }, 63 | posts() { 64 | if (this.tag) { 65 | return this.$store.getters.requestedItems(this.postsRequest) 66 | } 67 | }, 68 | title() { 69 | return `Archive for ${this.tag ? this.tag.name : ''}` 70 | } 71 | }, 72 | methods: { 73 | getTag() { 74 | return this.$store.dispatch('getSingleBySlug', this.tagRequest).then(() => { 75 | this.setPostsRequestParams() 76 | this.$store.dispatch('updateDocTitle', { parts: [ this.tag.name, this.$store.state.site.name ] }) 77 | }) 78 | }, 79 | getPosts() { 80 | return this.$store.dispatch('getItems', this.postsRequest) 81 | }, 82 | setPostsRequestParams() { 83 | this.postsRequest.params.tags = this.tag.id 84 | }, 85 | setTotalPages() { 86 | this.totalPages = this.$store.getters.totalPages(this.postsRequest) 87 | } 88 | }, 89 | created() { 90 | this.getTag().then(() => this.getPosts()).then(() => this.setTotalPages()) 91 | } 92 | } 93 | </script> 94 | -------------------------------------------------------------------------------- /src/components/TaxonomyArchive.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main> 3 | <header> 4 | <h1>{{ title }}</h1> 5 | </header> 6 | <section v-if="posts"> 7 | <post-item 8 | v-for="post in posts" 9 | :key="post.id" 10 | :post="post" 11 | /> 12 | <pagination 13 | v-if="totalPages > 1" 14 | :total="totalPages" 15 | :current="page" 16 | /> 17 | </section> 18 | </main> 19 | </template> 20 | 21 | <script> 22 | import PostItem from '@/components/template-parts/PostItem' 23 | import Pagination from '@/components/template-parts/Pagination' 24 | 25 | export default { 26 | name: 'TaxonomyArchive', 27 | components: { 28 | PostItem, 29 | Pagination 30 | }, 31 | props: { 32 | type: { 33 | type: String, 34 | required: true 35 | }, 36 | page: { 37 | type: Number, 38 | required: true 39 | }, 40 | slug: { 41 | type: String, 42 | required: true 43 | }, 44 | }, 45 | data () { 46 | return { 47 | postsRequest: { 48 | type: 'posts', 49 | params: { 50 | per_page: this.$store.state.site.posts_per_page, 51 | page: this.page, 52 | categories: null, 53 | tags: null 54 | }, 55 | showLoading: true 56 | }, 57 | taxonomyRequest: { 58 | type: this.type, 59 | slug: this.slug 60 | }, 61 | totalPages: 0 62 | } 63 | }, 64 | computed: { 65 | taxonomy() { 66 | return this.$store.getters.singleBySlug(this.taxonomyRequest) 67 | }, 68 | posts() { 69 | if (this.taxonomy) { 70 | return this.$store.getters.requestedItems(this.postsRequest) 71 | } 72 | }, 73 | title() { 74 | return `Archive for ${this.taxonomy ? this.taxonomy.name : ''}` 75 | } 76 | }, 77 | methods: { 78 | getTaxonomy() { 79 | return this.$store.dispatch('getSingleBySlug', this.taxonomyRequest).then(() => { 80 | this.setPostsRequestParams() 81 | this.$store.dispatch('updateDocTitle', { parts: [ this.taxonomy.name, this.$store.state.site.name ] }) 82 | }) 83 | }, 84 | getPosts() { 85 | return this.$store.dispatch('getItems', this.postsRequest) 86 | }, 87 | setPostsRequestParams() { 88 | this.postsRequest.params[this.type] = this.taxonomy.id 89 | }, 90 | setTotalPages() { 91 | this.totalPages = this.$store.getters.totalPages(this.postsRequest) 92 | } 93 | }, 94 | created() { 95 | this.getTaxonomy().then(() => this.getPosts()).then(() => this.setTotalPages()) 96 | } 97 | } 98 | </script> 99 | -------------------------------------------------------------------------------- /src/components/template-parts/NavMenu.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <nav> 3 | <a 4 | v-for="item in menu" 5 | :key="item.id" 6 | :href="item.url" 7 | :target="item.target" 8 | :title="item.title" 9 | v-html="item.content" 10 | ></a> 11 | </nav> 12 | </template> 13 | 14 | <script> 15 | export default { 16 | name: 'NavMenu', 17 | props: { 18 | name: { 19 | type: String, 20 | required: true 21 | } 22 | }, 23 | data() { 24 | return {} 25 | }, 26 | computed: { 27 | menu() { 28 | return this.$store.getters.menu({ name: this.name }) 29 | } 30 | } 31 | } 32 | </script> 33 | -------------------------------------------------------------------------------- /src/components/template-parts/Pagination.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <section class="pagination"> 3 | <a 4 | v-show="current !== 1" 5 | class="pagination__previous" 6 | href="#" 7 | rel="previous" 8 | v-html="'‹ Previous'" 9 | @click.prevent="gotoPage(current - 1)" 10 | ></a> 11 | <a 12 | v-show="current !== total" 13 | class="pagination__next" 14 | href="#" 15 | rel="next" 16 | v-html="'Next ›'" 17 | @click.prevent="gotoPage(current + 1)" 18 | ></a> 19 | </section> 20 | </template> 21 | 22 | <script> 23 | export default { 24 | name: 'Pagination', 25 | props: { 26 | current: { 27 | type: Number, 28 | required: true 29 | }, 30 | total: { 31 | type: Number, 32 | required: true 33 | } 34 | }, 35 | data() { 36 | return {} 37 | }, 38 | methods: { 39 | gotoPage(page) { 40 | if (! page || page > this.total) return 41 | let path = this.$route.path 42 | if (this.current === 1 && page !== 1) { 43 | path += `page/${page}/` 44 | } else if (page === 1) { 45 | path = path.replace(`page/${this.current}/`, '') 46 | } else { 47 | path = path.replace(`page/${this.current}/`, `page/${page}/`) 48 | } 49 | this.$router.push(path) 50 | } 51 | } 52 | } 53 | 54 | </script> 55 | 56 | <style> 57 | 58 | .pagination { 59 | position: relative; 60 | padding-bottom: 4rem; 61 | border-bottom: 1px solid #e8e8e8; 62 | } 63 | 64 | .pagination>a { 65 | font-size: 1.8rem; 66 | display: inline-block; 67 | } 68 | .pagination>a:first-of-type { 69 | float: left; 70 | } 71 | 72 | .pagination>a:last-of-type { 73 | float: right; 74 | } 75 | </style> 76 | 77 | -------------------------------------------------------------------------------- /src/components/template-parts/PostItem.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <article class="post"> 3 | <responsive-image 4 | v-if="post.featured_media" 5 | class="post__featured-media" 6 | :media-id="post.featured_media" 7 | :sizes="'(max-width: 680px) 40vw, 400px'" 8 | /> 9 | <div class="post__content"> 10 | <h2> 11 | <a 12 | :href="post.link" 13 | :title="post.title.rendered" 14 | v-html="post.title.rendered" 15 | ></a> 16 | </h2> 17 | <post-meta :post="post" /> 18 | <div v-html="post.excerpt.rendered"></div> 19 | <post-taxonomies :post="post" /> 20 | </div> 21 | </article> 22 | </template> 23 | 24 | <script> 25 | import ResponsiveImage from '@/components/utility/ResponsiveImage' 26 | import PostMeta from '@/components/utility/PostMeta' 27 | import PostTaxonomies from '@/components/utility/PostTaxonomies' 28 | 29 | export default { 30 | name: 'PostItem', 31 | components: { 32 | ResponsiveImage, 33 | PostMeta, 34 | PostTaxonomies 35 | }, 36 | props: { 37 | post: { 38 | type: Object, 39 | required: true 40 | } 41 | }, 42 | data() { 43 | return {} 44 | } 45 | } 46 | </script> 47 | 48 | <style> 49 | 50 | .post { 51 | display: flex; 52 | flex-flow: row-reverse nowrap; 53 | justify-content: flex-start; 54 | align-items: flex-start; 55 | margin: 4rem 0; 56 | padding-bottom: 4rem; 57 | border-bottom: 1px solid #e8e8e8; 58 | } 59 | 60 | .post__featured-media { 61 | display: block; 62 | flex: 0 0 33%; 63 | max-width: 400px; 64 | height: 240px; 65 | margin-left: 5%; 66 | } 67 | 68 | .post__featured-media > img { 69 | position: absolute; 70 | top: 50%; 71 | left: 50%; 72 | min-height: 100%; 73 | min-width: 100%; 74 | transform: translate3d(-50%,-50%,0); 75 | } 76 | 77 | .post__content { 78 | flex: 1 1 auto; 79 | max-width: 100%; 80 | overflow: hidden; 81 | } 82 | 83 | .post__content > h2 { 84 | margin-top: 0; 85 | } 86 | 87 | @media (max-width: 680px) { 88 | .post { 89 | flex-flow: row wrap; 90 | } 91 | .post__featured-media { 92 | flex: 0 1 100%; 93 | max-width: 100%; 94 | margin: 0 0 2rem 0; 95 | } 96 | } 97 | 98 | </style> 99 | -------------------------------------------------------------------------------- /src/components/utility/ArchiveLink.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <a 3 | :href="link" 4 | :title="title" 5 | v-html="title" 6 | ></a> 7 | </template> 8 | 9 | <script> 10 | export default { 11 | name: 'ArchiveLink', 12 | props: { 13 | archiveType: { 14 | type: String, 15 | required: true 16 | }, 17 | archiveId: { 18 | type: Number, 19 | required: true 20 | } 21 | }, 22 | data() { 23 | return {} 24 | }, 25 | computed: { 26 | request() { 27 | return { type: this.archiveType, id: this.archiveId, batch: true } 28 | }, 29 | item() { 30 | return this.$store.getters.singleById(this.request) 31 | }, 32 | link() { 33 | return this.item ? this.item.link : '' 34 | }, 35 | title() { 36 | return this.item ? this.item.name : '' 37 | } 38 | }, 39 | methods: { 40 | getArchiveItem() { 41 | this.$store.dispatch('getSingleById', this.request) 42 | } 43 | }, 44 | created() { 45 | this.getArchiveItem() 46 | } 47 | } 48 | 49 | </script> 50 | -------------------------------------------------------------------------------- /src/components/utility/PostMeta.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="post-meta"> 3 | <archive-link 4 | :archive-id="author" 5 | archive-type="users" 6 | /> 7 | <time>{{ date }}</time> 8 | <span>{{ readingTime }}</span> 9 | </div> 10 | </template> 11 | 12 | <script> 13 | import ArchiveLink from '@/components/utility/ArchiveLink' 14 | 15 | export default { 16 | name: 'PostMeta', 17 | components: { ArchiveLink }, 18 | props: { 19 | post: { 20 | type: Object, 21 | required: true 22 | } 23 | }, 24 | data() { 25 | return { 26 | author: this.post.author, 27 | date: new Date(this.post.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) 28 | } 29 | }, 30 | computed: { 31 | readingTime() { 32 | // pretty general estimate 33 | let words = this.post.content.rendered.split(' ').length + 1 34 | return `${Math.ceil(words / 200)} min read` 35 | } 36 | } 37 | } 38 | </script> 39 | 40 | <style> 41 | 42 | .post-meta>time::before, 43 | .post-meta>span::before { 44 | content: '\2022'; 45 | margin: 0 .4rem; 46 | } 47 | 48 | </style> 49 | -------------------------------------------------------------------------------- /src/components/utility/PostTaxonomies.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <div 4 | v-if="post.categories.length" 5 | class="categories" 6 | > 7 | <span>Posted in:</span> 8 | <archive-link 9 | v-for="category in post.categories" 10 | :key="category" 11 | archive-type="categories" 12 | :archive-id="category" 13 | /> 14 | </div> 15 | <div 16 | v-if="post.tags.length" 17 | class="tags" 18 | > 19 | <span>Tagged:</span> 20 | <archive-link 21 | v-for="tag in post.tags" 22 | :key="tag" 23 | archive-type="tags" 24 | :archive-id="tag" 25 | /> 26 | </div> 27 | </div> 28 | </template> 29 | 30 | <script> 31 | import ArchiveLink from '@/components/utility/ArchiveLink' 32 | 33 | export default { 34 | name: 'PostTaxonomies', 35 | components: { ArchiveLink }, 36 | props: { 37 | post: { 38 | type: Object, 39 | required: true 40 | } 41 | }, 42 | data() { 43 | return {} 44 | } 45 | } 46 | </script> 47 | 48 | <style> 49 | 50 | .categories>a, 51 | .tags>a { 52 | margin: 0 .4rem; 53 | } 54 | 55 | </style> 56 | -------------------------------------------------------------------------------- /src/components/utility/ResponsiveImage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="responsive-image"> 3 | <transition 4 | name="fade" 5 | :duration="1000" 6 | > 7 | <img 8 | v-if="image" 9 | :src="image.source_url" 10 | :srcset="srcset" 11 | :sizes="sizes" 12 | :alt="image.alt_text" 13 | :title="image.title.rendered" 14 | /> 15 | </transition> 16 | </div> 17 | </template> 18 | 19 | <script> 20 | export default { 21 | name: 'ResponsiveImage', 22 | props: { 23 | mediaId: { 24 | type: Number, 25 | required: true 26 | }, 27 | sizes: { 28 | type: String, 29 | required: false 30 | } 31 | }, 32 | data() { 33 | return { 34 | request: { 35 | type: 'media', 36 | id: this.mediaId, 37 | batch: true 38 | } 39 | } 40 | }, 41 | computed: { 42 | image() { 43 | return this.$store.getters.singleById(this.request) 44 | }, 45 | srcset() { 46 | if (this.image) { 47 | let sizes = this.image.media_details.sizes 48 | return Object.keys(sizes) 49 | .reduce((srcset, size) => { 50 | srcset.push(`${sizes[size].source_url} ${sizes[size].width}w`) 51 | return srcset 52 | }, []) 53 | .join(', ') 54 | } 55 | } 56 | }, 57 | methods: { 58 | getMedia() { 59 | if (this.mediaId) { 60 | this.$store.dispatch('getSingleById', this.request) 61 | } 62 | } 63 | }, 64 | created() { 65 | this.getMedia() 66 | } 67 | } 68 | </script> 69 | 70 | <style> 71 | 72 | .responsive-image { 73 | position: relative; 74 | overflow: hidden; 75 | width: 100%; 76 | background-color: #f8f8f8; 77 | } 78 | 79 | .responsive-image > img { 80 | display: block; 81 | width: auto; 82 | height: auto; 83 | max-width: 100%; 84 | } 85 | 86 | </style> 87 | 88 | -------------------------------------------------------------------------------- /src/components/utility/SiteLoading.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="site-loading-wrap"> 3 | <div class="site-loading"> 4 | <img 5 | v-if="logo" 6 | :src="logo.source_url" 7 | :alt="logo.alt_text" 8 | /> 9 | <span v-else>{{ siteName }}</span> 10 | <div></div> 11 | </div> 12 | </div> 13 | </template> 14 | 15 | <script> 16 | export default { 17 | name: 'SiteLoading', 18 | data() { 19 | return { 20 | logoId: this.$store.state.site.logo, 21 | siteName: this.$store.state.site.name 22 | } 23 | }, 24 | computed: { 25 | logo() { 26 | return this.$store.getters.singleById({ type: 'media', id: this.logoId }) 27 | } 28 | } 29 | } 30 | </script> 31 | 32 | <style> 33 | 34 | .site-loading-wrap { 35 | position: fixed; 36 | top: 0; 37 | bottom: 0; 38 | left: 0; 39 | right: 0; 40 | z-index: 3; 41 | /* background-color: #fff; */ 42 | } 43 | .site-loading { 44 | display: inline-block; 45 | position: absolute; 46 | top: 45vh; 47 | left: 50%; 48 | max-width: 80%; 49 | transform: translate3d(-50%, -50%, 0); 50 | } 51 | 52 | .site-loading>img { 53 | display: block; 54 | position: relative; 55 | height: 8rem; 56 | width: auto; 57 | max-width: 100%; 58 | margin: 0 auto 1rem auto; 59 | } 60 | 61 | .site-loading>span { 62 | font-size: 2rem; 63 | font-weight: bold; 64 | } 65 | 66 | .site-loading>div { 67 | height: .2rem; 68 | background: #f8f8f8; 69 | margin-top: 1rem; 70 | overflow: hidden; 71 | position: relative; 72 | } 73 | 74 | .site-loading>div::before, 75 | .site-loading>div::after { 76 | content: ''; 77 | display: block; 78 | position: absolute; 79 | top: 0; 80 | bottom: 0; 81 | left: -100%; 82 | width: 100%; 83 | background-color: #444; 84 | transform: translateX(-100%); 85 | animation: 2.5s ease-in 0s infinite loadingBar; 86 | } 87 | 88 | .site-loading>div::after { 89 | animation-delay: 1.2s; 90 | animation-timing-function: ease; 91 | } 92 | 93 | @keyframes loadingBar { 94 | 0% { transform: translateX(0) } 95 | 50% { transform: translateX(200%) } 96 | 100% { transform: translateX(200%) } 97 | } 98 | 99 | </style> 100 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import routes from './routes' 4 | 5 | const { url } = __VUE_WORDPRESS__.routing 6 | 7 | // scroll position is handled in @after-leave transition hook 8 | if ('scrollRestoration' in window.history) window.history.scrollRestoration = 'manual' 9 | 10 | Vue.use(Router) 11 | 12 | export default new Router({ 13 | base: url.replace(window.location.origin, ''), 14 | mode: 'history', 15 | routes: routes 16 | }) 17 | -------------------------------------------------------------------------------- /src/router/paths.js: -------------------------------------------------------------------------------- 1 | 2 | const { permalink_structure, category_base, tag_base } = __VUE_WORDPRESS__.routing 3 | 4 | const tagToParam = { 5 | author: ':author', 6 | postname: ':slug', 7 | post_id: ':id(\\d+)', 8 | category: ':cat1/:cat2?/:cat3?', 9 | year: ':year(\\d{4})', 10 | monthnum: ':month(\\d{2})', 11 | day: ':day(\\d{2})', 12 | hour: ':hour(\\d{2})', 13 | minute: ':min(\\d{2})', 14 | second: ':sec(\\d{2})' 15 | } 16 | 17 | // If no category/tag base set WP uses base of singlePost permalink structure excluding tags 18 | const defaultTaxonomyBase = permalink_structure.slice(0, permalink_structure.indexOf('%')) 19 | // Appended to route paths with pagination 20 | const paginateParam = ':page(page\/\\d+)?' 21 | 22 | export default { 23 | authorArchive: `${defaultTaxonomyBase}author/:slug/${paginateParam}`, 24 | categoryArchive: category_base ? `/${category_base}/${tagToParam.category}/${paginateParam}` : `${defaultTaxonomyBase}category/${tagToParam.category}/${paginateParam}`, 25 | dateArchive: `${defaultTaxonomyBase}:year(\\d{4})/:month(\\d{2})?/:day(\\d{2})?/${paginateParam}`, 26 | single: permalink_structure.replace(/\%[a-z_]+\%/g, match => tagToParam[match.slice(1,-1)]).slice(0,-1), 27 | tagArchive: tag_base ? `/${tag_base}/:slug/${paginateParam}` : `${defaultTaxonomyBase}tag/:slug/${paginateParam}`, 28 | postsPage: (slug) => slug ? `/${slug}/${paginateParam}` : `/${paginateParam}` 29 | } -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | // Route components 2 | import Home from '@/components/Home' 3 | import NotFound from '@/components/404' 4 | import AuthorArchive from '@/components/AuthorArchive' 5 | import DateArchive from '@/components/DateArchive' 6 | import CategoryArchive from '@/components/CategoryArchive' 7 | import TagArchive from '@/components/TagArchive' 8 | import Single from '@/components/Single' 9 | import Page from '@/components/Page' 10 | // Route paths as formatted in WP permalink settings 11 | import paths from './paths' 12 | // Route composition utilities 13 | import { 14 | categorySlugFromParams, 15 | pageFromPath 16 | } from './utils' 17 | 18 | 19 | const { show_on_front, page_for_posts, page_on_front } = __VUE_WORDPRESS__.routing 20 | 21 | const postsPageRoute = show_on_front === 'page' && page_for_posts ? { 22 | path: paths.postsPage(page_for_posts), 23 | component: Home, 24 | name: 'Posts', 25 | props: route => ({ page: pageFromPath(route.path) }) 26 | } : null 27 | 28 | const rootRoute = show_on_front === 'page' && page_on_front ? { 29 | path: '/', 30 | component: Page, 31 | name: 'Home', 32 | props: () => ({ slug: page_on_front }), 33 | } : { 34 | path: paths.postsPage(), 35 | component: Home, 36 | name: 'Home', 37 | props: route => ({ page: pageFromPath(route.path) }), 38 | } 39 | 40 | export default [ 41 | rootRoute, 42 | postsPageRoute, 43 | { 44 | path: '/404', 45 | component: NotFound, 46 | name: '404' 47 | }, 48 | { 49 | path: paths.authorArchive, 50 | component: AuthorArchive, 51 | name: 'AuthorArchive', 52 | props: route => (Object.assign(route.params, { page: pageFromPath(route.path) })) 53 | }, 54 | { 55 | path: paths.dateArchive, 56 | component: DateArchive, 57 | name: 'DateArchive', 58 | props: route => (Object.assign(route.params, { page: pageFromPath(route.path) })) 59 | }, 60 | { 61 | path: paths.categoryArchive, 62 | component: CategoryArchive, 63 | name: 'CategoryArchive', 64 | props: route => (Object.assign(route.params, { slug: categorySlugFromParams(route.params), page: pageFromPath(route.path) } )) 65 | }, 66 | { 67 | path: paths.tagArchive, 68 | component: TagArchive, 69 | name: 'TagArchive', 70 | props: route => (Object.assign(route.params, { page: pageFromPath(route.path) })) 71 | }, 72 | { 73 | path: paths.single, 74 | component: Single, 75 | name: 'Single', 76 | props: route => ({ slug: route.params.slug }), 77 | }, 78 | /** 79 | * This also functions as a catch all redirecting 80 | * to 404 if a page isn't found with slug prop 81 | */ 82 | { 83 | path: '/:slugs+', 84 | component: Page, 85 | name: 'Page', 86 | props: route => ({ slug: route.params.slugs.split('/').filter(s => s).pop() }) 87 | } 88 | ].filter(route => route) // Removes empty route objects 89 | -------------------------------------------------------------------------------- /src/router/utils.js: -------------------------------------------------------------------------------- 1 | 2 | function categorySlugFromParams({ cat1, cat2, cat3 }) { 3 | if (cat3 && (cat3 !== 'page' && cat2 !== 'page')) { 4 | return cat3 5 | } else if(cat2 && cat2 !== 'page') { 6 | return cat2 7 | } else { 8 | return cat1 9 | } 10 | } 11 | 12 | function pageFromPath(path) { 13 | let p = path.split('/').filter(i => i) 14 | if (p.length > 1 && p[p.length - 2] === 'page') { 15 | return parseInt(p[p.length - 1]) 16 | } else { 17 | return 1 18 | } 19 | } 20 | 21 | export { 22 | categorySlugFromParams, 23 | pageFromPath 24 | } 25 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | fetchItems, 3 | fetchSingle, 4 | fetchSingleById 5 | } from '@/api' 6 | 7 | export default { 8 | getSingleBySlug({ getters, commit }, { type, slug, showLoading = false }) { 9 | if ( ! getters.singleBySlug({ type, slug }) ) { 10 | if (showLoading) { 11 | commit('SET_LOADING', true) 12 | } 13 | return fetchSingle({ type, params: { slug } }).then(({ data: [ item ] }) => { 14 | commit('ADD_ITEM', { type, item }) 15 | if (showLoading) { 16 | commit('SET_LOADING', false) 17 | } 18 | return item 19 | }) 20 | } 21 | }, 22 | getSingleById({ getters, commit }, { type, id, showLoading = false, batch = false }) { 23 | if ( ! getters.singleById({ type, id }) ) { 24 | if ( showLoading ) { 25 | commit('SET_LOADING', true) 26 | } 27 | return fetchSingleById({ type, id, batch }).then(({ data }) => { 28 | if (batch) { 29 | data.forEach(item => commit('ADD_ITEM', { type, item })) 30 | } else { 31 | commit('ADD_ITEM', { type, item: data }) 32 | } 33 | if (showLoading) { 34 | commit('SET_LOADING', false) 35 | } 36 | }) 37 | } 38 | }, 39 | getItems({ getters, commit }, { type, params, showLoading = false }) { 40 | if ( ! getters.request({ type, params }) ) { 41 | if (showLoading) { 42 | commit('SET_LOADING', true) 43 | } 44 | return fetchItems({ type, params }) 45 | .then(({ data: items, headers: { 'x-wp-total': total, 'x-wp-totalpages': totalPages } }) => { 46 | items.forEach(item => commit('ADD_ITEM', { type, item })) 47 | commit('ADD_REQUEST', { type, request: { params, total: parseInt(total), totalPages: parseInt(totalPages), data: items.map(i => i.id) } }) 48 | if (showLoading) { 49 | commit('SET_LOADING', false) 50 | } 51 | }) 52 | } 53 | }, 54 | updateDocTitle ({ state, commit }, { parts = [], sep = ' – ' }) { 55 | commit('SET_DOC_TITLE', parts.join(sep)) 56 | document.title = state.site.docTitle 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | menu: state => ({ name }) => { 3 | return state.menus[name] 4 | }, 5 | request: state => ({ type, params }) => { 6 | return state[type].requests.find(req => { 7 | if (Object.keys(req.params).length === Object.keys(params).length) { 8 | return Object.keys(req.params).every(key => req.params[key] === params[key]) 9 | } 10 | }) 11 | }, 12 | totalPages: (state, getters) => ({ type, params }) => { 13 | let request = getters.request({ type, params }) 14 | return request ? request.totalPages : 0 15 | }, 16 | requestedItems: (state, getters) => ({ type, params }) => { 17 | let request = getters.request({ type, params }) 18 | return request ? request.data.map(id => state[type][id]) : [] 19 | }, 20 | singleBySlug: state => ({ type, slug }) => { 21 | for (let id in state[type]) { 22 | if (decodeURI(state[type][id].slug) === slug) { 23 | return state[type][id] 24 | } 25 | } 26 | }, 27 | singleById: state => ({ type, id }) => { 28 | return state[type][id] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import mutations from './mutations' 5 | import actions from './actions' 6 | 7 | Vue.use(Vuex) 8 | 9 | const { state } = __VUE_WORDPRESS__ 10 | 11 | export default new Vuex.Store({ 12 | state, 13 | getters, 14 | mutations, 15 | actions 16 | }) 17 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | ADD_ITEM(state, { type, item }) { 5 | if (item && type && ! state[type].hasOwnProperty(item.id)) { 6 | Vue.set(state[type], item.id, item) 7 | } 8 | }, 9 | ADD_REQUEST(state, { type, request }) { 10 | state[type].requests.push(request) 11 | }, 12 | SET_LOADING(state, loading) { 13 | state.site.loading = loading 14 | }, 15 | SET_DOC_TITLE(state, title) { 16 | state.site.docTitle = title 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Vue.wordpress 3 | Author: Brandon Shiluk 4 | Author URI: https://github.com/bucky355 5 | Description: Takes a hybrid approach, using Wordpress to manage and serve initial content. Vue.js takes over all future in-site navigation effectively transforming the site into a SPA. 6 | Version: 1.0.0 7 | Text Domain: vue-wordpress 8 | */ 9 | 10 | 11 | /* Base Styles 12 | ------------------------------------- */ 13 | 14 | html { 15 | font-size: 62.5%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | font-size: 1.5em; 21 | line-height: 1.6; 22 | font-weight: 400; 23 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu,Cantarell, "Helvetica Neue", sans-serif; 24 | color: #444; 25 | background-color: #fff; 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | } 29 | 30 | main { 31 | min-height: 100vh; 32 | overflow: hidden; 33 | } 34 | 35 | .container { 36 | width: 100%; 37 | max-width: 120rem; 38 | margin-left: auto; 39 | margin-right: auto; 40 | padding-left: 1.6rem; 41 | padding-right: 1.6rem; 42 | box-sizing: border-box; 43 | } 44 | 45 | h1 { 46 | font-size: 3.2rem; 47 | } 48 | 49 | h2 { 50 | font-size: 2.4rem; 51 | } 52 | 53 | h3 { 54 | font-size: 2rem; 55 | } 56 | -------------------------------------------------------------------------------- /tag.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | get_header(); 4 | 5 | $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; 6 | $per_page = RADL::get( 'state.site' )['posts_per_page']; 7 | $tag_id = get_query_var('tag_id'); 8 | RADL::get( 'state.tags', $tag_id ); 9 | RADL::get( 'state.posts', array( 'page' => $paged, 'per_page' => $per_page, 'tags' => $tag_id ) ); 10 | 11 | get_footer(); 12 | -------------------------------------------------------------------------------- /template-parts/archive-link.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $archive = get_query_var( 'vw_archive_link' ); ?> 3 | 4 | <a href="<?php echo $archive['link']; ?>" title="<?php echo $archive['name']; ?>"><?php echo $archive['name']; ?></a> -------------------------------------------------------------------------------- /template-parts/nav-menu.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $name = get_query_var( 'vw_nav_menu' ); ?> 3 | 4 | <nav class="main-menu"> 5 | 6 | <?php 7 | foreach ( RADL::get( 'state.menus' )[$name] as $item ): ?> 8 | 9 | <a href="<?php echo $item['url']; ?>" target="<?php echo $item['target']; ?>" 10 | title="<?php echo $item['title']; ?>"><?php echo $item['content']; ?></a> 11 | 12 | <?php 13 | endforeach;?> 14 | 15 | </nav> -------------------------------------------------------------------------------- /template-parts/pagination.php: -------------------------------------------------------------------------------- 1 | <?php 2 | // these are just used to tell if there is a next/previous page 3 | $previous = get_previous_posts_link(); 4 | $next = get_next_posts_link(); 5 | if ( $previous || $next ): ?> 6 | 7 | <section class="pagination"> 8 | 9 | <?php if ( $previous ): ?> 10 | 11 | <a class="pagination__previous" href="<?php echo get_previous_posts_page_link(); ?>" rel="previous">‹ Previous</a> 12 | 13 | <?php endif; ?> 14 | 15 | <?php if ( $next ): ?> 16 | 17 | <a class="pagination__previous" href="<?php echo get_next_posts_page_link(); ?>" rel="next">Next ›</a> 18 | 19 | <?php endif; ?> 20 | 21 | </section> 22 | 23 | <?php 24 | endif; -------------------------------------------------------------------------------- /template-parts/post-item.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $p = get_query_var( 'vw_post' ); ?> 3 | 4 | <article class="post"> 5 | 6 | <?php 7 | if ( $p['featured_media']) { 8 | set_query_var('vw_responsive_image', array( 9 | 'id' => $p['featured_media'], 10 | 'sizes' => '(max-width: 680px) 40vw, 400px', 11 | 'class' => 'post__featured-media' 12 | )); 13 | get_template_part('template-parts/responsive-image'); 14 | } ?> 15 | 16 | <div class="post__content"> 17 | 18 | <h2> 19 | 20 | <a href="<?php echo $p['link']; ?>" title="<?php echo $p['title']['rendered']; ?>"><?php echo $p['title']['rendered']; ?> 21 | 22 | </h2> 23 | 24 | <?php get_template_part('template-parts/post-meta'); ?> 25 | 26 | <div><?php echo $p['excerpt']['rendered']; ?></div> 27 | 28 | <?php get_template_part('template-parts/post-taxonomies'); ?> 29 | 30 | </div> 31 | 32 | </article> 33 | -------------------------------------------------------------------------------- /template-parts/post-meta.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $p = get_query_var( 'vw_post' ); 3 | $author = RADL::get( 'state.users', $p['author'] ); ?> 4 | 5 | <div class="post-meta"> 6 | 7 | <?php 8 | set_query_var( 'vw_archive_link', $author ); 9 | get_template_part( 'template-parts/archive-link' ); ?> 10 | 11 | <time><?php echo date( 'M j', strtotime( $p['date'] ) ); ?></time> 12 | 13 | <span><?php echo vue_wordpress_min_read( $p['content']['rendered'] ) ?></span> 14 | 15 | </div> -------------------------------------------------------------------------------- /template-parts/post-taxonomies.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $p = get_query_var( 'vw_post' ); 3 | $categories = $p['categories']; 4 | $tags = $p['tags']; ?> 5 | 6 | <div> 7 | 8 | <?php if ( count( $categories ) ): ?> 9 | 10 | <div class="categories"> 11 | 12 | <span>Posted in:</span> 13 | 14 | <?php 15 | foreach( $categories as $cat_id ) { 16 | $cat = RADL::get( 'state.categories', $cat_id ); 17 | set_query_var( 'vw_archive_link', $cat ); 18 | get_template_part( 'template-parts/archive-link' ); 19 | } ?> 20 | 21 | </div> 22 | 23 | <?php 24 | endif; 25 | if ( count( $tags ) ): ?> 26 | 27 | <div class="tags"> 28 | 29 | <span>Tagged:</span> 30 | 31 | <?php 32 | foreach( $tags as $tag_id ) { 33 | $tag = RADL::get( 'state.tags', $tag_id ); 34 | set_query_var( 'vw_archive_link', $tag ); 35 | get_template_part( 'template-parts/archive-link' ); 36 | } ?> 37 | 38 | </div> 39 | 40 | <?php endif; ?> 41 | 42 | </div> -------------------------------------------------------------------------------- /template-parts/responsive-image.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $props = get_query_var('vw_responsive_image'); 3 | $image = RADL::get('state.media', $props['id']); 4 | $image_class = empty( $props['class'] ) ? 'responsive-image' : $props['class'] . ' responsive-image'; 5 | $srcset = array_map( function ($size) { 6 | return $size['source_url'] . ' ' . $size['width'] . 'w'; 7 | }, $image['media_details']['sizes']); ?> 8 | 9 | <div class="<?php echo $image_class; ?>"> 10 | 11 | <img src="<?php echo $image['source_url']; ?>" srcset="<?php echo join(', ', $srcset); ?>" 12 | sizes="<?php echo $props['sizes']; ?>" alt="<?php echo $image['alt_text']; ?>" 13 | title="<?php echo $image['title']['rendered']; ?>" /> 14 | 15 | </div> -------------------------------------------------------------------------------- /template-parts/site-branding.php: -------------------------------------------------------------------------------- 1 | <div class="site-branding"> 2 | 3 | <?php 4 | $logo = RADL::get( 'state.media', RADL::get( 'state.site' )['logo'] ); 5 | if ( $logo ): ?> 6 | 7 | <img class="logo" src="<?php echo $logo['source_url']; ?>" alt="<?php echo $logo['alt_text']; ?>" /> 8 | 9 | <?php endif;?> 10 | 11 | <span><?php echo RADL::get( 'state.site' )['name']; ?></span> 12 | 13 | </div> -------------------------------------------------------------------------------- /template-parts/site-loading.php: -------------------------------------------------------------------------------- 1 | <div class="site-loading-wrap"> 2 | 3 | <div class="site-loading"> 4 | 5 | <?php 6 | $logo = RADL::get('state.media', RADL::get('state.site')['logo']); 7 | if ( $logo ): ?> 8 | 9 | <img class="logo" src="<?php echo $logo['source_url']; ?>" alt="<?php echo $logo['alt_text']; ?>" /> 10 | 11 | <?php else: ?> 12 | 13 | <span><?php echo RADL::get('state.site')['name'];?></span> 14 | 15 | <?php endif;?> 16 | 17 | <div></div> 18 | 19 | </div> 20 | 21 | </div> -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = env => require(`./webpack.${env}.config.js`) 3 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | 5 | module.exports = { 6 | entry: path.resolve(__dirname, 'src/app.js'), 7 | mode: 'development', 8 | devServer: { 9 | hot: true, 10 | headers: { 'Access-Control-Allow-Origin': '*' } 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | publicPath: 'http://localhost:8080/', 15 | filename: 'vue-wordpress.js' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.vue$/, 21 | loader: 'vue-loader' 22 | }, 23 | { 24 | test: /\.js$/, 25 | loader: 'babel-loader', 26 | exclude: /node_modules/ 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: [ 31 | 'vue-style-loader', 32 | 'css-loader' 33 | ] 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new VueLoaderPlugin(), 39 | new webpack.HotModuleReplacementPlugin() 40 | ], 41 | resolve: { 42 | alias: { 43 | '@': path.resolve(__dirname, './src') 44 | }, 45 | extensions: ['*', '.js', '.vue', '.json'] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | 6 | module.exports = { 7 | entry: path.resolve(__dirname, 'src/app.js'), 8 | mode: 'production', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'vue-wordpress.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.vue$/, 17 | loader: 'vue-loader' 18 | }, 19 | { 20 | test: /\.js$/, 21 | loader: 'babel-loader', 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | MiniCssExtractPlugin.loader, 28 | 'css-loader' 29 | ] 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new VueLoaderPlugin(), 35 | new MiniCssExtractPlugin({ 36 | filename: 'vue-wordpress.css' 37 | }) 38 | ], 39 | resolve: { 40 | alias: { 41 | '@': path.resolve(__dirname, './src') 42 | }, 43 | extensions: ['*', '.js', '.vue', '.json'] 44 | } 45 | } 46 | --------------------------------------------------------------------------------