8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
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 | *Originally created by [Bucky355](https://github.com/bucky355/vue-wordpress), but since he left the boat, i updated it to the latest versions aka. vue 3 (including vuex & vue-router) and webpack 5.*
7 |
8 | *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.*
9 |
10 | **Check out [a demo of the theme](http://vue-wordpress-demo.bshiluk.com)**
11 |
12 | # Table of Contents
13 |
14 | - [Features](#features)
15 | - [Libraries](#libraries)
16 | - [Usage](#usage)
17 | - [Development](#development)
18 | - [Deployment](#deployment)
19 | - [General Overview](#general-overview)
20 | - [Routing](#routing)
21 | - [Permalinks to Routes](#permalinks-to-routes)
22 | - [Exceptions](#exceptions)
23 | - [Notes on Permalink Structure](#notes-on-permalink-structure)
24 | - [Category and Tag Base](#category-and-tag-base)
25 | - [Homepage and Posts Per Page](#homepage-and-posts-per-page)
26 | - [Internal Link Delegation](#internal-link-delegation)
27 | - [State Management and API Requests](#state-management-and-api-requests)
28 | - [Vuex Initialization and Schema](#vuex-initialization-and-schema)
29 | - [Schema](#schema)
30 | - [Initialization](#initialization)
31 | - [Requesting Data](#requesting-data)
32 | - [Making WP REST API Requests](#making-wp-rest-api-requests)
33 | - [Request Caching](#request-caching)
34 | - [Actions Api Reference](#actions-api-reference)
35 | - [Request an item of type by its slug](#request-an-item-of-type-by-its-slug)
36 | - [Request an item of type by its id](#request-an-item-of-type-by-its-id)
37 | - [Request a list of items](#request-a-list-of-items)
38 | - [Getters Api Reference](#getters-api-reference)
39 | - [Get an item of type by slug](#get-an-item-of-type-by-slug)
40 | - [Get an item of type by id](#get-an-item-of-type-by-id)
41 | - [Get a list of items](#get-a-list-of-items)
42 | - [SEO](#seo-and-server-rendered-content)
43 | - [How It Works](#how-it-works)
44 | - [The Three Basic Approaches](#the-three-basic-approaches)
45 | - [Pseudo Headless](#pseudo-headless)
46 | - [Example index.php](#example-indexphp)
47 | - [Pseudo Headless SEO](#pseudo-headless-seo)
48 | - [Preload Data](#preload-data)
49 | - [Example single.php](#example-singlephp)
50 | - [Example category.php](#example-categoryphp)
51 | - [Preload Data SEO](#preload-data-seo)
52 | - [Progressive Enhancement](#progressive-enhancement)
53 | - [Example home.php](#example-homephp)
54 | - [Progressive Enhancement SEO](#progressive-enhancement-seo)
55 | - [Upcoming Features](#upcoming-features)
56 | - [Final Thoughts](#final-thoughts)
57 |
58 | ## Features
59 | * Front and Back-End run on same host
60 | * Supports various client/server code partitioning strategies for optional SEO optimization
61 | * Hot Module Replacement ( HMR ) for development
62 | * Vue.js Single File Components
63 | * Production / Development Webpack configurations
64 | * Dynamic routing as set up in WP Dashboard ( Settings > Permalinks )
65 | * Vue Router internal link delegation ( no need to use `` in components )
66 | * Document title tag update on route change
67 | * Consistent in-component REST API data fetching
68 | * Integrated REST API request caching and batching
69 | * [REST API Data Localizer](https://github.com/bucky355/rest-api-data-localizer) for state initialization and making back-end REST API requests
70 | * Normalized data structure
71 | * Pagination enabled
72 | * Utility components, including ResponsiveImage, SiteLoading, ArchiveLink, e.t.c.
73 |
74 | ## Libraries
75 | To promote flexibility in implementation, styling and dependencies are limited. That said, the theme requires the following libraries:
76 | * [Axios](https://github.com/axios/axios)
77 | * [Vue.js](https://vuejs.org/v2/guide/)
78 | * [Vue Router](https://router.vuejs.org/)
79 | * [Vuex](https://vuex.vuejs.org/)
80 |
81 | ## Usage
82 |
83 | 1. Clone or download this repo in your `/wp-content/themes` directory and activate
84 | 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.
85 | 3. Ensure settings in Settings > Permalinks do not violate rules as described in [Routing](#routing).
86 | 4. Install dependencies `npm install`.
87 | 5. Build the theme `npm run build`
88 |
89 | ### Development
90 |
91 | 1. Start development with HMR `npm run dev`
92 | 2. Edit `functions.php` so Wordpress enqueues the bundle served from webpack dev server
93 | ````php
94 | // Enable For Production - Disable for Development
95 | // wp_enqueue_script('vue_wordpress.js', get_template_directory_uri() . '/dist/vue-wordpress.js', array(), null, true);
96 |
97 | // Enable For Development - Remove for Production
98 | wp_enqueue_script( 'vue_wordpress.js', 'http://localhost:8080/vue-wordpress.js', array(), false, true );
99 | ````
100 | ### Deployment
101 |
102 | 1. Build the theme `npm run build`
103 | 2. Only include Wordpress theme files ( i.e. `style.css`, `functions.php`, `index.php`, `screenshot.png` e.t.c.) and the `/dist/` directory
104 | 3. Edit `functions.php` so Wordpress enqueues the bundle served from your `/dist/` directory
105 | ````php
106 | // Enable For Production - Disable for Development
107 | wp_enqueue_script('vue_wordpress.js', get_template_directory_uri() . '/dist/vue-wordpress.js', array(), null, true);
108 | ````
109 |
110 | ## General Overview
111 |
112 | 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.
113 | 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.
114 | 3. Vue takes over all future in-site navigation, using the WP_REST_API to request data, essentially transforming the site into a SPA.
115 |
116 | ## Routing
117 |
118 | ### Permalinks to Routes
119 |
120 | By default, routing works as defined in Settings > Permalinks. You can even define a custom structure.
121 |
122 | 
123 |
124 | ✔️ 'Day and Name' `/%monthnum%/%day%/%postname%/`
125 |
126 | ✔️ 'Month and Name' `/%year%/%monthnum%/%day%/%postname%/`
127 |
128 | ✔️ `/my-blog/%category%/%postname%/`
129 |
130 | ✔️ `/%postname%/%author%/%category%/%year%/%monthnum%/`
131 |
132 | ### Exceptions
133 |
134 | Using 'Plain', 'Numeric', or any custom structure that uses `%post_id%` as the sole identifier.
135 |
136 | ❌ `/%year%/%monthnum%/%post_id%/`
137 |
138 | ❌ `/archives/%post_id%/`
139 |
140 | *However, if you combine the `%post_id%` tag with the `%postname%` tag it will work.*
141 |
142 | ✔️ `/%author%/%post_id%/%category%/%postname%/`
143 |
144 | *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.*
145 |
146 | Using 'Post name'
147 |
148 | ❌ `/%postname%/`
149 |
150 | *However, you can add a base to make it work*
151 |
152 | ✔️ `/some-base/%postname%/`
153 |
154 | ### Notes on Permalink Structure
155 |
156 | - Using `/%category%/%postname%/` or `/%tag%/%postname%/` will cause problems with nested pages
157 | - 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.
158 | - Routes for custom post types will be needed to be added as needed.
159 | - 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.
160 |
161 | ### Category and Tag Base
162 |
163 | If you want to edit the category and tag permalink bases within the same Settings > Permalinks screen, that is supported as well.
164 |
165 | ### Homepage and Posts Per Page
166 |
167 | Additionally, in Settings > Reading you can adjust what your home page displays and posts shown per page ( labeled 'Blog pages to show at most' ).
168 |
169 | ### Internal Link Delegation
170 |
171 | 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.
172 |
173 | *Adapted from Dennis Reimann's [Delegating HTML links to vue-router](https://dennisreimann.de/articles/delegating-html-links-to-vue-router.html)*
174 |
175 | 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 ``.
176 |
177 | Instead of:
178 | ````html
179 | {{ tag.name }}
180 | ````
181 | You can do:
182 | ````html
183 | {{ tag.name }}
184 | ````
185 |
186 | ## State Management and API Requests
187 |
188 | 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/`.
189 |
190 | ### Vuex Initialization and Schema
191 |
192 | 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.
193 |
194 | #### Schema
195 | ````php
196 | // functions.php
197 |
198 | new RADL( '__VUE_WORDPRESS__', 'vue_wordpress.js', array(
199 | 'routing' => RADL::callback( 'vue_wordpress_routing' ),
200 | 'state' => array(
201 | 'categories' => RADL::endpoint( 'categories'),
202 | 'media' => RADL::endpoint( 'media' ),
203 | 'menus' => RADL::callback( 'vue_wordpress_menus' ),
204 | 'pages' => RADL::endpoint( 'pages' ),
205 | 'posts' => RADL::endpoint( 'posts' ),
206 | 'tags' => RADL::endpoint( 'tags' ),
207 | 'users' => RADL::endpoint( 'users' ),
208 | 'site' => RADL::callback( 'vue_wordpress_site' ),
209 | ),
210 | ) );
211 | ````
212 |
213 | #### Initialization
214 | As you can see below, the `__VUE_WORDPRESS__.state` key is used to initialize the Vuex store.
215 |
216 | ````js
217 | // src/store/index.js
218 |
219 | const { state } = __VUE_WORDPRESS__
220 |
221 | export default new Vuex.Store({
222 | state,
223 | getters,
224 | mutations,
225 | actions
226 | })
227 | ````
228 | *More advanced implementations may want to decouple this localized store from the Vuex store, but I think it decreases the initial complexity.*
229 |
230 | ### Requesting Data
231 |
232 | #### Making WP REST API Requests
233 |
234 | 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.
235 |
236 | #### Request Caching
237 |
238 | Every time there is a request for a list of items, a request object is generated once the response is received.
239 | ````js
240 | // Example request object
241 | {
242 | data: [ 121, 221, 83, 4, 23, 76 ], // ids of items
243 | params: { page: 1, per_page: 6 },
244 | total: 21,
245 | totalPages: 4
246 | }
247 | ````
248 | 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.
249 |
250 | 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.
251 |
252 | ### Actions Api Reference
253 |
254 | #### Request an item of type by its slug
255 |
256 | ````js
257 | this.$store.dispatch('getSingleBySlug', { type, slug, showLoading })
258 | ````
259 | - `type` is the key in the store and the endpoint for the resource
260 | - `string`
261 | - required
262 | - `slug` is the alphanumeric identifier for the resource
263 | - `string`
264 | - required
265 | - `showLoading` indicates whether a loading indicator should be shown
266 | - `boolean`
267 | - default: `false`
268 |
269 | ##### Example
270 | ```` js
271 | // request a page with slug 'about' and show loading indicator
272 | this.$store.dispatch('getSingleBySlug', { type: 'pages', slug: 'about', showLoading: true })
273 | ````
274 |
275 | #### Request an item of type by its id
276 |
277 | ````js
278 | this.$store.dispatch('getSingleById', { type, id, batch })
279 | ````
280 | - `type` is the key in the store and the endpoint for the resource
281 | - `string`
282 | - required
283 | - `id` is the numeric identifier for the resource
284 | - `number`
285 | - required
286 | - `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.
287 | - `boolean`
288 | - default: `false`
289 |
290 | *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.*
291 |
292 | ##### Examples
293 | ```` js
294 | // request a tag with an id of 13 with batching
295 | this.$store.dispatch('getSingleById', { type: 'tags', id: 13, batch: true })
296 | // request a tag with an id of 21 with batching
297 | this.$store.dispatch('getSingleById', { type: 'tags', id: 21, batch: true })
298 |
299 | // 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
300 | ````
301 |
302 | #### Request a list of items
303 |
304 | ````js
305 | this.$store.dispatch('getItems', { type, params, showLoading })
306 | ````
307 | - `type` is the key in the store and the endpoint for the resource
308 | - `string`
309 | - required
310 | - `params` are the parameters to use for the request
311 | - `Object`
312 | - required
313 | - `showLoading` indicates whether a loading indicator should be shown
314 | - `boolean`
315 | - default: `false`
316 |
317 | ##### Example
318 | ````js
319 | // request the first page of the most recent posts limited to 10 posts per page
320 | this.$store.dispatch('getItems', { type: 'posts', params: [ page: 1, per_page: 10, orderby: 'date', order: 'desc' ] })
321 | ````
322 |
323 | ### Getters Api Reference
324 |
325 | 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.
326 |
327 | #### Get an item of type by slug
328 |
329 | ````js
330 | this.$store.getters.singleBySlug({ type, slug })
331 | ````
332 |
333 | #### Get an item of type by id
334 |
335 | ````js
336 | this.$store.getters.singleById({ type, id })
337 | ````
338 |
339 | #### Get a list of items
340 |
341 | ````js
342 | this.$store.getters.requestedItems({ type, params })
343 | ````
344 |
345 |
346 | ## SEO and Server Rendered Content
347 |
348 | ### How It Works
349 |
350 | 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.
351 |
352 |
353 | ### The Three Basic Approaches
354 |
355 | Each approach provides either explicit setup or specific examples and includes the php knowledge required to implement.
356 |
357 | #### Pseudo Headless
358 |
359 | *Minimal to no php knowledge required*
360 |
361 | 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 ``, and enqueues scripts and stylesheets as configured.
362 |
363 | ##### Example index.php
364 | ````html
365 |
366 | >
367 |
368 |
369 |
370 |
371 |
372 |
373 |