├── styled-jsx.png ├── withLayout.png ├── styled-jsx-no-highlighting.png ├── styled-jsx-with-syntax-highlighting.png └── README.md /styled-jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziad-saab/wordpress-api-nextjs-theme/HEAD/styled-jsx.png -------------------------------------------------------------------------------- /withLayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziad-saab/wordpress-api-nextjs-theme/HEAD/withLayout.png -------------------------------------------------------------------------------- /styled-jsx-no-highlighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziad-saab/wordpress-api-nextjs-theme/HEAD/styled-jsx-no-highlighting.png -------------------------------------------------------------------------------- /styled-jsx-with-syntax-highlighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziad-saab/wordpress-api-nextjs-theme/HEAD/styled-jsx-with-syntax-highlighting.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Let's make a WordPress-based blog with React and NodeJS 2 | 3 | In this workshop, we are going to make a blog. To manage the content of our blog, we will be using 4 | WordPress, one of the leading content management systems today. However, contrary to most WordPress 5 | blogs in existence today, we will not be using PHP to create the theme for our blog. Instead, we are 6 | going to manage the front-end with a variety of modern web technologies: JavaScript, React, and some 7 | NodeJS. 8 | 9 | ## Why choose this tech stack? :question: 10 | It's important to note that this way of doing things is not for everyone. As always, our 11 | technology choices will have some upsides and some downsides. In the case of choosing React, here 12 | are, in no particular order, a few pros and cons: 13 | 14 | * :fire: Pros: 15 | * Extremely rapid feedback during the development cycle 16 | * The resulting single-page application can have faster loading times and navigation 17 | * Great ability and ease to add interactive features like infinite scrolling 18 | * Ability to get going without having to learn PHP 19 | * Even greater separation between back-end and front-end 20 | * Ease of managing different development environments without copying WordPress data 21 | * :poop: Cons: 22 | * Some complexity will be introduced by having two different technologies – PHP and JavaScript 23 | * Having a build process for the front-end complicates things 24 | * The need to have two hosting environments, one for WordPress and one for our “theme” 25 | * The reduced number of people who can help us due to our non-conventional setup 26 | 27 | Now that that's out of the way, let's not come back to it and concentrate on the task at hand. 28 | 29 |

30 | 31 |

32 | 33 | --- 34 | 35 | ## Technical requirements :computer: 36 | 37 | 38 | 39 | In order to successfully complete this workshop, you will need: 40 | 41 | * A computer with an internet connection 42 | * The software mentioned in the [next section](#required-software) 43 | * Some familiarity with modern JavaScript, React and NodeJS 44 | * Some familiarity with the command line interface of your machine 45 | * Familiarity with Git is optional, but you should use it to track your progress! 46 | 47 | --- 48 | 49 | ## Thanks :two_hearts: 50 | Before moving forward with the workshop, I'd like to express my thanks to the people who 51 | helped make this possible: 52 | 53 | - [Nathaniel Kitzke](https://github.com/nk1tz): for TAing and fixing a thousand typos 54 | - [Timothy Mulqueen](https://github.com/multimo): for TAing and fixing some glaring mistakes 55 | - [Paule Lepage](https://github.com/nyanofthemoon): for TAing and moral support 56 | 57 |

58 | 59 |

60 | 61 | --- 62 | 63 | ## Getting started! :rocket: 64 | 65 | ### Required software 66 | To follow along successfully, you will need to install some software on your machine: 67 | 68 | * [Install NVM](https://github.com/creationix/nvm) 69 | 70 | NVM is “Node Version Manager”. Through it, we can install any version of NodeJS. For this 71 | workshop, we will be using **NodeJS 8.9.0**. Once NVM is installed, you can run the following 72 | command to install NodeJS: 73 | 74 | ```sh 75 | nvm install 8.9.0 76 | nvm alias default 8.9.0 77 | ``` 78 | 79 | After running these commands, you might need to open a new shell for them to take effect. 80 | 81 | * [Install a recent version of Yarn](https://yarnpkg.com/lang/en/docs/install/) 82 | 83 | Yarn is a package manager for JavaScript code. It will allow us to install external libraries 84 | easily. Yarn will also allow us to execute our code with simple command lines. 85 | 86 | * [Install the WebStorm IDE](https://www.jetbrains.com/webstorm/) 87 | 88 | Many IDEs exist that support JavaScript. WebStorm is a phenomenal IDE that comes batteries 89 | included. Once installed, you will not need any special configurations nor plugins. It will work 90 | out of the box. 91 | 92 | **NOTE**: It is fine to use the IDE of your choice, but WebStorm is *recommended*. It will ensure 93 | that you have the same setup as your workshop-mates and make it easier for the TAs. 94 | 95 | * [Install Google Chrome](https://www.google.com/chrome/index.html) 96 | 97 | You can use the browser of your choice to follow this workshop, but Chrome is recommended. This 98 | will ensure that you have the same setup as your workshop-mates and make it easier for the TAs. 99 | 100 | * Install React Developer Tools 101 | 102 | Available for [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) 103 | and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/), React Developer 104 | Tools is a browser extension that allows you to introspect your running React application, making 105 | it easier to debug. 106 | 107 | * **What about WordPress??** :question: 108 | 109 | You will not need to install a WordPress instance in order to complete the workshop. If you 110 | already have your WordPress blog with WP-API, you will simply be able to use it. If you do not 111 | have a readily-available WordPress blog, you will be able to use [wired.com](https://wired.com)'s 112 | WP-API. 113 | 114 | One of the great things about the setup we will have is that you will easily be able to change 115 | which WordPress blog it is associated to by simply changing one line in your JavaScript code. 116 | 117 |

118 | 119 |

120 | 121 | ### Initial setup 122 | Now that you have installed all the necessary software on your machine, you are ready to start! In 123 | this section, you will be initializing your development environment, creating the base code, and 124 | making your first web page with this setup. 125 | 126 | Start by creating an empty directory on your machine. Then, move to that directory and run the 127 | following commands: 128 | 129 | ```sh 130 | yarn init # You can edit your answers to the prompts later 131 | yarn add react react-dom next 132 | ``` 133 | 134 | Based on the requirements of this workshop, this should all be familiar to you except perhaps for 135 | the `next` framework. [Next.js](https://github.com/zeit/next.js/) is the framework that will allow 136 | us to run the same React application on both the server (Express) and the client (browser). This is 137 | what will allow us to develop our blog with modern front-end technologies without having to forego 138 | the benefits of an initial server rendering – SEO and faster loading. 139 | 140 | Once this is done, open WebStorm and create a new project based on the directory you just setup. 141 | Then, open the `package.json` file and add the following section at the root: 142 | 143 | ``` 144 | "scripts": { 145 | "start": "next" 146 | } 147 | ``` 148 | 149 | Then, create a directory called `pages` at the root of your project. In it, create a file called 150 | `index.js` with the following code: 151 | 152 | ```js 153 | import React from 'react'; 154 | 155 | const HomePage = () => ( 156 |
157 |

Welcome to my blog

158 |
159 | ); 160 | 161 | export default HomePage; 162 | ``` 163 | 164 | Once this is done, you will have created a basic Next.js setup. Go back to your command line and 165 | run: 166 | 167 | ```sh 168 | yarn start 169 | ``` 170 | 171 | After a few seconds, your app should be started. Open your browser and navigate to `localhost:3000` 172 | to see your new creation. If you “view source”, you will notice that the server sent the content of 173 | your React component along with the response. If you open your React Developer Tools, you will see 174 | that the React application is running in the browser. Two environments, one code! :fire: 175 | 176 | This concludes the initial setup of the workshop. Let's seal the deal by 177 | creating a Git repository and committing all this goodness. 178 | 179 | To prevent Git from tracking undesirable artifact files, create a `.gitignore` file at the root of 180 | your project with the following lines: 181 | 182 | ``` 183 | .next 184 | node_modules 185 | ``` 186 | 187 | Then, run `git init`, `git add .` and `git commit` to seal your work inside the repository. 188 | 189 | :warning: **NOTE**: from now on, the workshop instructions will not mention Git anymore. It's up to 190 | you if you want to use it to have a clean commit for each step of the workshop. If you do, it will 191 | allow you to look at the history of your code and afford you all the benefits Git has to offer. 192 | 193 |

194 | 195 |

196 | 197 | --- 198 | 199 | ## Adding another page and linking to it :link: 200 | Next.js makes it easy to add new pages to your application. All you have to do is create a new file 201 | under the `pages` directory. The name of the file will be the same as the URL path to access it. 202 | While this seems limiting – how do we do pretty URLs?? – we will see in a later section that this 203 | can be fixed by using Next.js' powerful request handler. 204 | 205 | Create a new file called `blog.js` in the `pages` directory. In there, follow the same pattern as 206 | `index.js` to create a page that simply says “Recent blog posts”. In the next section, we will use 207 | this page to call the WordPress API and create a blog listing. 208 | 209 | Then, re-open `pages/index.js` and make the following changes: 210 | 211 | ```js 212 | // Add this line at the top of the file 213 | import Link from 'next/link'; 214 | 215 | // Add this line inside the
, below the

216 | Go to the recent posts 217 | ``` 218 | 219 | Then, go back to your browser. Did you notice that the changes appear right away, without having to 220 | reload the page? This is one of the many benefits of using this tech stack, and with Next.js you get 221 | it for free. If you click on the link, you should navigate to the `/blog` page. Using your favorite 222 | technique, convince yourself that this navigation has happened without a full page reload. 223 | 224 | ### Why not simply use an `` for navigation? 225 | If you look at the source code of the `/` page, you will see that the server has rendered an `` 226 | in place of the ``. This is great for SEO purposes. However, using the `` component 227 | makes this link dynamic. In the browser, Next.js prevents the default action and handles navigation 228 | using the History API. This gives you the best of both worlds: SEO and :fire: fast navigation. 229 | 230 | :warning: **NOTE**: It might look weird to have an `` *inside* the `` but this is how Next 231 | wants you to do it. 232 | 233 |

234 | 235 |

236 | 237 | --- 238 | 239 | ## First contact: using the WordPress API! :alien: 240 | In this section, we will use the WordPress API to show a list of recent posts on the `/blog` page. 241 | The WordPress API is based on the [application/hal+json](http://stateless.co/hal_specification.html). 242 | This will make it easy for us to grab not only post data, but all sorts of related data like tags, 243 | categories, and author info. This data is provided through the use of 244 | [hyperlinks](https://developer.wordpress.org/rest-api/using-the-rest-api/linking-and-embedding/), 245 | but the WordPress API allows us to embed those links in order to avoid multiple HTTP requests. 246 | 247 | The **TL;DR** of this is that WordPress API will include HTTP links to related resources, but adding 248 | `?_embed` at the end of a WordPress API URL will automatically embed all the linked resources. Here 249 | is an example of requesting a single blog post without `_embed`: 250 | 251 | ```json 252 | { 253 | "id": 2267743, 254 | "date": "2017-11-11T10:00:23", 255 | "date_gmt": "2017-11-11T15:00:23", 256 | "guid": { 257 | "rendered": "https://www.wired.com/?p=2267743" 258 | }, 259 | "modified": "2017-11-10T18:37:44", 260 | "modified_gmt": "2017-11-10T23:37:44", 261 | "slug": "review-incase-noviconnected-travel-roller", 262 | "type": "post", 263 | "link": "https://www.wired.com/2017/11/review-incase-noviconnected-travel-roller/", 264 | "title": { 265 | "rendered": "Review: Incase NoviConnected Travel Roller" 266 | }, 267 | "content": { 268 | "rendered": "

If you’re the kind of person who will arrive at the airport without charging your phone or laptop, you are also the kind of person who will forget to charge the battery bank in your smart luggage. I discovered this when a three-hour delay kicked off my weekend trip with Incase’s NoviConnected carry-on suitcase. Sorry, but it’s true.

\n

At home, it took six hours of wall charging at home to get the NoviConnected’s 10,050-mAh battery up to 100%. A few days later, I packed my bag, headed to the airport and waited at the gate. By the time I realized I needed to charge my phone, the suitcase’s battery had gone down to 76% after doing nothing, with an estimated one hour and 23 minutes’ worth of juice.

\n

Charging my phone back up to 80% took the suitcase’s battery down to 19%. What about charging a laptop? Forget about it. The bag’s battery bank is strictly for small devices only and it’s best conserved for an emergency situation. Since everything from your boarding pass to your hotel reservation is on your phone, you’ll need that to be juiced up, so it’s not a total loss. Like the rest of the bag’s smart features, it may promise more than it can actually deliver.

\n

For example, take the suitcase’s Smart Luggage tracker. It uses Bluetooth and has a range of up to 33 feet. On a full flight, I ended up checking my carry-on at the airline’s request. When I got to baggage claim, I glanced at the Incase app to determine my luggage’s whereabouts and found out that my bag was out of range. For fifteen minutes, I waited at the baggage carousel like an ordinary plebe, trapped in a seemingly endless purgatory, mired in deep existential uncertainty.

\n

Even walking across my house prompted a notification from my phone, alerting me that the suitcase—safely in my office—was no longer within range. If you want a smart luggage tracker, it should probably use GPS instead of Bluetooth.

\n\n
\n\t\t\t\t\t
\n\t\t

Incase NoviConnected Travel Roller

\n\t\t

5/10

\t\t
\n\t\t
Wired
\n\t\t

Beautiful. Durable. Hubless wheels roll smoothly and easily and are easily replaceable. Battery bank will save you in a pinch. Comes with rain cover.

\n\t\t
Tired
\n\t\t

Bluetooth tracker makes no sense. No side handle. Battery life was disappointing.

\n\t\t
\n\t\t\t\t\t\t\t\tBuy It Now\n\t\t\t  |  Incase\n\t\t\t\t\t\t\t
\n\t
\n\n\t
\n\t\t\n\t\t\t\n\t\t\n\t\t

How We Rate

\n\t\t
\n\t\t\t
    \n\t\t\t\t
  • \n\t\t\t\t\t1/10A complete failure in every way\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t2/10Sad, really\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t3/10Serious flaws; proceed with caution\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t4/10Downsides outweigh upsides\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t5/10Recommended with reservations\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t6/10Solid with some issues\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t7/10Very good, but not quite great\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t8/10Excellent, with room to kvetch\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t9/10Nearly flawless\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t10/10Metaphysical perfection\n\t\t\t\t
  • \n\t\t\t
\n\t\t
\n\t
\n
\n\n

But as a piece of ordinary luggage, the bag performed admirably. It has a simple, durable makrolon polycarbonate shell that showed little wear after a few trips around the baggage carousel. The navy color of my sample case was elegant and surprisingly distinctive among a billion black bags. The interior, with mesh zip pockets and an included laundry bag, easily accommodated several days’ worth of clothes and shoes.

\n

The bag was impossible to tip over, even with a tote bag hooked on top. The hubless wheels rolled smoothly—almost a little too smoothly, as the suitcase escaped me when my attention wandered on a slanting parking lot garage floor—and it’s simple to pop the wheels on and off if you need to fit the suitcase into an overhead compartment. This model also carries a lifetime warranty from Incase, so the wheels are covered if they break (the power bank’s embedded battery has a two-year limited warranty).

\n

The integrated TSA-approved lock works like you’d expect, and it proved useful when I ended up checking my carry-on to save overhead space on two packed flights. However, I would have appreciated a side handle to make it easier to hoist around.

\n

As holiday travel season approaches, you do have to find a way to get all your stuff from Point A to Point B without carrying it all in your arms. Incase is known for their attractive, Apple-compatible accessories and bags, and the NoviConnected is no exception. It’s elegant, rolls with ease, and has the capacity to save your tuchus right when you need it most—when your phone is about to die and you have no way to text your ride or check your car rental confirmation number.

\n

But there are just too many other moderately-priced smart carry-ons out there right now with features that make a bit more sense, like a true GPS locator that can tell you if your baggage has made it to your destination. Or just bring your own spare battery and pop a Tile tracker into your current luggage and call it a day.

\n" 269 | }, 270 | "excerpt": { 271 | "rendered": "

A great piece of luggage is brought down by its poor smart features.

\n" 272 | }, 273 | "author": 609, 274 | "featured_media": 2267773, 275 | "comment_status": "open", 276 | "ping_status": "closed", 277 | "sticky": false, 278 | "format": "standard", 279 | "categories": [ 280 | 36, 281 | 83082, 282 | 4 283 | ], 284 | "tags": [ 285 | 106059, 286 | 106060 287 | ], 288 | "series": [], 289 | "admin-settings": [], 290 | "_links": { 291 | "self": [ 292 | { 293 | "href": "https://www.wired.com/wp-json/wp/v2/posts/2267743" 294 | } 295 | ], 296 | "collection": [ 297 | { 298 | "href": "https://www.wired.com/wp-json/wp/v2/posts" 299 | } 300 | ], 301 | "about": [ 302 | { 303 | "href": "https://www.wired.com/wp-json/wp/v2/types/post" 304 | } 305 | ], 306 | "author": [ 307 | { 308 | "embeddable": true, 309 | "href": "https://www.wired.com/wp-json/wp/v2/users/609" 310 | } 311 | ], 312 | "replies": [ 313 | { 314 | "embeddable": true, 315 | "href": "https://www.wired.com/wp-json/wp/v2/comments?post=2267743" 316 | } 317 | ], 318 | "version-history": [ 319 | { 320 | "href": "https://www.wired.com/wp-json/wp/v2/posts/2267743/revisions" 321 | } 322 | ], 323 | "wp:featuredmedia": [ 324 | { 325 | "embeddable": true, 326 | "href": "https://www.wired.com/wp-json/wp/v2/media/2267773" 327 | } 328 | ], 329 | "wp:attachment": [ 330 | { 331 | "href": "https://www.wired.com/wp-json/wp/v2/media?parent=2267743" 332 | } 333 | ], 334 | "wp:term": [ 335 | { 336 | "taxonomy": "category", 337 | "embeddable": true, 338 | "href": "https://www.wired.com/wp-json/wp/v2/categories?post=2267743" 339 | }, 340 | { 341 | "taxonomy": "post_tag", 342 | "embeddable": true, 343 | "href": "https://www.wired.com/wp-json/wp/v2/tags?post=2267743" 344 | }, 345 | { 346 | "taxonomy": "series", 347 | "embeddable": true, 348 | "href": "https://www.wired.com/wp-json/wp/v2/series?post=2267743" 349 | }, 350 | { 351 | "taxonomy": "admin-settings", 352 | "embeddable": true, 353 | "href": "https://www.wired.com/wp-json/wp/v2/admin-settings?post=2267743" 354 | } 355 | ], 356 | "curies": [ 357 | { 358 | "name": "wp", 359 | "href": "https://api.w.org/{rel}", 360 | "templated": true 361 | } 362 | ] 363 | } 364 | } 365 | ``` 366 | 367 |

368 | 369 |
370 | Get it? JASON 🤣 371 |

372 | 373 | Notice that fields like author, categories, and tags appear as numbers, IDs of their respective 374 | types. The `_links` section contains HTTP links to those resources. Here is the same request with 375 | `_embed` added at the end of the URL: 376 | 377 | ```json 378 | { 379 | "id": 2267743, 380 | "date": "2017-11-11T10:00:23", 381 | "date_gmt": "2017-11-11T15:00:23", 382 | "guid": { 383 | "rendered": "https://www.wired.com/?p=2267743" 384 | }, 385 | "modified": "2017-11-10T18:37:44", 386 | "modified_gmt": "2017-11-10T23:37:44", 387 | "slug": "review-incase-noviconnected-travel-roller", 388 | "type": "post", 389 | "link": "https://www.wired.com/2017/11/review-incase-noviconnected-travel-roller/", 390 | "title": { 391 | "rendered": "Review: Incase NoviConnected Travel Roller" 392 | }, 393 | "content": { 394 | "rendered": "

If you’re the kind of person who will arrive at the airport without charging your phone or laptop, you are also the kind of person who will forget to charge the battery bank in your smart luggage. I discovered this when a three-hour delay kicked off my weekend trip with Incase’s NoviConnected carry-on suitcase. Sorry, but it’s true.

\n

At home, it took six hours of wall charging at home to get the NoviConnected’s 10,050-mAh battery up to 100%. A few days later, I packed my bag, headed to the airport and waited at the gate. By the time I realized I needed to charge my phone, the suitcase’s battery had gone down to 76% after doing nothing, with an estimated one hour and 23 minutes’ worth of juice.

\n

Charging my phone back up to 80% took the suitcase‚Äôs battery down to 19%. What about charging a laptop? Forget about it. The bag‚Äôs battery bank is strictly for small devices only and it’s best conserved for an emergency situation. Since everything from your boarding pass to your hotel reservation is on your phone, you’ll need that to be juiced up, so it’s not a total loss. Like the rest of the bag‚Äôs smart features, it may promise more than it can actually deliver.

\n

For example, take the suitcase’s Smart Luggage tracker. It uses Bluetooth and has a range of up to 33 feet. On a full flight, I ended up checking my carry-on at the airline’s request. When I got to baggage claim, I glanced at the Incase app to determine my luggage’s whereabouts and found out that my bag was out of range. For fifteen minutes, I waited at the baggage carousel like an ordinary plebe, trapped in a seemingly endless purgatory, mired in deep existential uncertainty.

\n

Even walking across my house prompted a notification from my phone, alerting me that the suitcase—safely in my office—was no longer within range. If you want a smart luggage tracker, it should probably use GPS instead of Bluetooth.

\n\n
\n\t\t\t\t\t
\n\t\t

Incase NoviConnected Travel Roller

\n\t\t

5/10

\t\t
\n\t\t\tLearn How We Rate\n\t\t
\n\t\t
Wired
\n\t\t

Beautiful. Durable. Hubless wheels roll smoothly and easily and are easily replaceable. Battery bank will save you in a pinch. Comes with rain cover.

\n\t\t
Tired
\n\t\t

Bluetooth tracker makes no sense. No side handle. Battery life was disappointing.

\n\t\t
\n\t\t\t\t\t\t\t\tBuy It Now\n\t\t\t  |  Incase\n\t\t\t\t\t\t\t
\n\t
\n\n\t
\n\t\t\n\t\t\t\n\t\t\n\t\t

How We Rate

\n\t\t
\n\t\t\t
    \n\t\t\t\t
  • \n\t\t\t\t\t1/10A complete failure in every way\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t2/10Sad, really\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t3/10Serious flaws; proceed with caution\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t4/10Downsides outweigh upsides\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t5/10Recommended with reservations\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t6/10Solid with some issues\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t7/10Very good, but not quite great\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t8/10Excellent, with room to kvetch\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t9/10Nearly flawless\n\t\t\t\t
  • \n\t\t\t\t
  • \n\t\t\t\t\t10/10Metaphysical perfection\n\t\t\t\t
  • \n\t\t\t
\n\t\t
\n\t
\n
\n\n

But as a piece of ordinary luggage, the bag performed admirably. It has a simple, durable makrolon polycarbonate shell that showed little wear after a few trips around the baggage carousel. The navy color of my sample case was elegant and surprisingly distinctive among a billion black bags. The interior, with mesh zip pockets and an included laundry bag, easily accommodated several days’ worth of clothes and shoes.

\n

The bag was impossible to tip over, even with a tote bag hooked on top. The hubless wheels rolled smoothly‚Äîalmost a little too smoothly, as the suitcase escaped me when my attention wandered on a slanting parking lot garage floor‚Äîand it‚Äôs simple to pop the wheels on and off if you need to fit the suitcase into an overhead compartment. This model also carries a lifetime warranty from Incase, so the wheels are covered if they break (the power bank’s embedded battery has a two-year limited warranty).

\n

The integrated TSA-approved lock works like you’d expect, and it proved useful when I ended up checking my carry-on to save overhead space on two packed flights. However, I would have appreciated a side handle to make it easier to hoist around.

\n

As holiday travel season approaches, you do have to find a way to get all your stuff from Point A to Point B without carrying it all in your arms. Incase is known for their attractive, Apple-compatible accessories and bags, and the NoviConnected is no exception. It’s elegant, rolls with ease, and has the capacity to save your tuchus right when you need it most—when your phone is about to die and you have no way to text your ride or check your car rental confirmation number.

\n

But there are just too many other moderately-priced smart carry-ons out there right now with features that make a bit more sense, like a true GPS locator that can tell you if your baggage has made it to your destination. Or just bring your own spare battery and pop a Tile tracker into your current luggage and call it a day.

\n" 395 | }, 396 | "excerpt": { 397 | "rendered": "

A great piece of luggage is brought down by its poor smart features.

\n" 398 | }, 399 | "author": 609, 400 | "featured_media": 2267773, 401 | "comment_status": "open", 402 | "ping_status": "closed", 403 | "sticky": false, 404 | "format": "standard", 405 | "categories": [ 406 | 36, 407 | 83082, 408 | 4 409 | ], 410 | "tags": [ 411 | 106059, 412 | 106060 413 | ], 414 | "series": [], 415 | "admin-settings": [], 416 | "_links": { 417 | "self": [ 418 | { 419 | "href": "https://www.wired.com/wp-json/wp/v2/posts/2267743" 420 | } 421 | ], 422 | "collection": [ 423 | { 424 | "href": "https://www.wired.com/wp-json/wp/v2/posts" 425 | } 426 | ], 427 | "about": [ 428 | { 429 | "href": "https://www.wired.com/wp-json/wp/v2/types/post" 430 | } 431 | ], 432 | "author": [ 433 | { 434 | "embeddable": true, 435 | "href": "https://www.wired.com/wp-json/wp/v2/users/609" 436 | } 437 | ], 438 | "replies": [ 439 | { 440 | "embeddable": true, 441 | "href": "https://www.wired.com/wp-json/wp/v2/comments?post=2267743" 442 | } 443 | ], 444 | "version-history": [ 445 | { 446 | "href": "https://www.wired.com/wp-json/wp/v2/posts/2267743/revisions" 447 | } 448 | ], 449 | "wp:featuredmedia": [ 450 | { 451 | "embeddable": true, 452 | "href": "https://www.wired.com/wp-json/wp/v2/media/2267773" 453 | } 454 | ], 455 | "wp:attachment": [ 456 | { 457 | "href": "https://www.wired.com/wp-json/wp/v2/media?parent=2267743" 458 | } 459 | ], 460 | "wp:term": [ 461 | { 462 | "taxonomy": "category", 463 | "embeddable": true, 464 | "href": "https://www.wired.com/wp-json/wp/v2/categories?post=2267743" 465 | }, 466 | { 467 | "taxonomy": "post_tag", 468 | "embeddable": true, 469 | "href": "https://www.wired.com/wp-json/wp/v2/tags?post=2267743" 470 | }, 471 | { 472 | "taxonomy": "series", 473 | "embeddable": true, 474 | "href": "https://www.wired.com/wp-json/wp/v2/series?post=2267743" 475 | }, 476 | { 477 | "taxonomy": "admin-settings", 478 | "embeddable": true, 479 | "href": "https://www.wired.com/wp-json/wp/v2/admin-settings?post=2267743" 480 | } 481 | ], 482 | "curies": [ 483 | { 484 | "name": "wp", 485 | "href": "https://api.w.org/{rel}", 486 | "templated": true 487 | } 488 | ] 489 | }, 490 | "_embedded": { 491 | "author": [ 492 | { 493 | "id": 609, 494 | "name": "Adrienne So", 495 | "url": "", 496 | "description": "", 497 | "link": "https://www.wired.com/author/adrienne-so/", 498 | "slug": "adrienne-so", 499 | "avatar_urls": { 500 | "24": "https://secure.gravatar.com/avatar/7c93ee8a4e06687ba5bf692dedc794b6?s=24&d=mm&r=g", 501 | "48": "https://secure.gravatar.com/avatar/7c93ee8a4e06687ba5bf692dedc794b6?s=48&d=mm&r=g", 502 | "96": "https://secure.gravatar.com/avatar/7c93ee8a4e06687ba5bf692dedc794b6?s=96&d=mm&r=g" 503 | }, 504 | "_links": { 505 | "self": [ 506 | { 507 | "href": "https://www.wired.com/wp-json/wp/v2/users/609" 508 | } 509 | ], 510 | "collection": [ 511 | { 512 | "href": "https://www.wired.com/wp-json/wp/v2/users" 513 | } 514 | ] 515 | } 516 | } 517 | ], 518 | "wp:featuredmedia": [ 519 | { 520 | "id": 2267773, 521 | "date": "2017-11-10T16:10:13", 522 | "slug": "incase-fa", 523 | "type": "attachment", 524 | "link": "https://www.wired.com/2017/11/review-incase-noviconnected-travel-roller/incase-fa/", 525 | "title": { 526 | "rendered": "incase-FA.jpg" 527 | }, 528 | "author": 10393, 529 | "alt_text": "", 530 | "media_type": "image", 531 | "mime_type": "image/jpeg", 532 | "media_details": { 533 | "width": 2236, 534 | "height": 1119, 535 | "file": "2017/11/incase-FA.jpg", 536 | "sizes": { 537 | "thumbnail": { 538 | "file": "incase-FA-150x150-e1510348383201.jpg", 539 | "width": 150, 540 | "height": 150, 541 | "mime_type": "image/jpeg", 542 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-150x150-e1510348383201.jpg" 543 | }, 544 | "medium": { 545 | "file": "incase-FA-300x150.jpg", 546 | "width": 300, 547 | "height": 150, 548 | "mime_type": "image/jpeg", 549 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-300x150.jpg" 550 | }, 551 | "medium_large": { 552 | "file": "incase-FA-768x384.jpg", 553 | "width": 768, 554 | "height": 384, 555 | "mime_type": "image/jpeg", 556 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-768x384.jpg" 557 | }, 558 | "large": { 559 | "file": "incase-FA-1024x512.jpg", 560 | "width": 1024, 561 | "height": 512, 562 | "mime_type": "image/jpeg", 563 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-1024x512.jpg" 564 | }, 565 | "200-100-thumbnail": { 566 | "file": "incase-FA-200x100-e1510348370909.jpg", 567 | "width": 200, 568 | "height": 100, 569 | "mime_type": "image/jpeg", 570 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-200x100-e1510348370909.jpg" 571 | }, 572 | "200-200-thumbnail": { 573 | "file": "incase-FA-200x200-e1510348352675.jpg", 574 | "width": 200, 575 | "height": 200, 576 | "mime_type": "image/jpeg", 577 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-200x200-e1510348352675.jpg" 578 | }, 579 | "500-500-thumbnail": { 580 | "file": "incase-FA-500x500-e1510348327817.jpg", 581 | "width": 500, 582 | "height": 500, 583 | "mime_type": "image/jpeg", 584 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-500x500-e1510348327817.jpg" 585 | }, 586 | "660-single-full": { 587 | "file": "incase-FA-660x330.jpg", 588 | "width": 660, 589 | "height": 330, 590 | "mime_type": "image/jpeg", 591 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-660x330.jpg" 592 | }, 593 | "315-single-full": { 594 | "file": "incase-FA-315x158.jpg", 595 | "width": 315, 596 | "height": 158, 597 | "mime_type": "image/jpeg", 598 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-315x158.jpg" 599 | }, 600 | "thumbnail-post": { 601 | "file": "incase-FA-300x150-e1510348340197.jpg", 602 | "width": 300, 603 | "height": 150, 604 | "mime_type": "image/jpeg", 605 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-300x150-e1510348340197.jpg" 606 | }, 607 | "600-338-full": { 608 | "file": "incase-FA-600x338-e1510348316142.jpg", 609 | "width": 600, 610 | "height": 338, 611 | "mime_type": "image/jpeg", 612 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-600x338-e1510348316142.jpg" 613 | }, 614 | "600-450-full": { 615 | "file": "incase-FA-600x450-e1510348304347.jpg", 616 | "width": 600, 617 | "height": 450, 618 | "mime_type": "image/jpeg", 619 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-600x450-e1510348304347.jpg" 620 | }, 621 | "929-697-full": { 622 | "file": "incase-FA-929x697-e1510348296219.jpg", 623 | "width": 929, 624 | "height": 697, 625 | "mime_type": "image/jpeg", 626 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-929x697-e1510348296219.jpg" 627 | }, 628 | "125-94-thumbnail": { 629 | "file": "incase-FA-125x94-e1510348402271.jpg", 630 | "width": 125, 631 | "height": 94, 632 | "mime_type": "image/jpeg", 633 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-125x94-e1510348402271.jpg" 634 | }, 635 | "929-523-full": { 636 | "file": "incase-FA-929x523-e1510348287351.jpg", 637 | "width": 929, 638 | "height": 523, 639 | "mime_type": "image/jpeg", 640 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-929x523-e1510348287351.jpg" 641 | }, 642 | "default-top-art": { 643 | "file": "incase-FA-582x291.jpg", 644 | "width": 582, 645 | "height": 291, 646 | "mime_type": "image/jpeg", 647 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-582x291.jpg" 648 | }, 649 | "wide-image": { 650 | "file": "incase-FA-932x466.jpg", 651 | "width": 932, 652 | "height": 466, 653 | "mime_type": "image/jpeg", 654 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-932x466.jpg" 655 | }, 656 | "inset-image": { 657 | "file": "incase-FA-289x145.jpg", 658 | "width": 289, 659 | "height": 145, 660 | "mime_type": "image/jpeg", 661 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-289x145.jpg" 662 | }, 663 | "text-column-width": { 664 | "file": "incase-FA-482x241.jpg", 665 | "width": 482, 666 | "height": 241, 667 | "mime_type": "image/jpeg", 668 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-482x241.jpg" 669 | }, 670 | "facebook-og": { 671 | "file": "incase-FA-1200x630-e1510348277739.jpg", 672 | "width": 1200, 673 | "height": 630, 674 | "mime_type": "image/jpeg", 675 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA-1200x630-e1510348277739.jpg" 676 | }, 677 | "full": { 678 | "file": "incase-FA.jpg", 679 | "width": 2236, 680 | "height": 1119, 681 | "mime_type": "image/jpeg", 682 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA.jpg" 683 | } 684 | }, 685 | "image_meta": { 686 | "aperture": "0", 687 | "credit": "", 688 | "camera": "", 689 | "caption": "", 690 | "created_timestamp": "0", 691 | "copyright": "", 692 | "focal_length": "0", 693 | "iso": "0", 694 | "shutter_speed": "0", 695 | "title": "", 696 | "orientation": "0", 697 | "keywords": [] 698 | } 699 | }, 700 | "source_url": "https://www.wired.com/wp-content/uploads/2017/11/incase-FA.jpg", 701 | "_links": { 702 | "self": [ 703 | { 704 | "href": "https://www.wired.com/wp-json/wp/v2/media/2267773" 705 | } 706 | ], 707 | "collection": [ 708 | { 709 | "href": "https://www.wired.com/wp-json/wp/v2/media" 710 | } 711 | ], 712 | "about": [ 713 | { 714 | "href": "https://www.wired.com/wp-json/wp/v2/types/attachment" 715 | } 716 | ], 717 | "author": [ 718 | { 719 | "embeddable": true, 720 | "href": "https://www.wired.com/wp-json/wp/v2/users/10393" 721 | } 722 | ], 723 | "replies": [ 724 | { 725 | "embeddable": true, 726 | "href": "https://www.wired.com/wp-json/wp/v2/comments?post=2267773" 727 | } 728 | ] 729 | } 730 | } 731 | ], 732 | "wp:term": [ 733 | [ 734 | { 735 | "id": 36, 736 | "link": "https://www.wired.com/category/gear/reviews/accessories/", 737 | "name": "Accessories", 738 | "slug": "accessories", 739 | "taxonomy": "category", 740 | "_links": { 741 | "self": [ 742 | { 743 | "href": "https://www.wired.com/wp-json/wp/v2/categories/36" 744 | } 745 | ], 746 | "collection": [ 747 | { 748 | "href": "https://www.wired.com/wp-json/wp/v2/categories" 749 | } 750 | ], 751 | "about": [ 752 | { 753 | "href": "https://www.wired.com/wp-json/wp/v2/taxonomies/category" 754 | } 755 | ], 756 | "up": [ 757 | { 758 | "embeddable": true, 759 | "href": "https://www.wired.com/wp-json/wp/v2/categories/4" 760 | } 761 | ], 762 | "wp:post_type": [ 763 | { 764 | "href": "https://www.wired.com/wp-json/wp/v2/posts?categories=36" 765 | }, 766 | { 767 | "href": "https://www.wired.com/wp-json/wp/v2/video?categories=36" 768 | } 769 | ], 770 | "curies": [ 771 | { 772 | "name": "wp", 773 | "href": "https://api.w.org/{rel}", 774 | "templated": true 775 | } 776 | ] 777 | } 778 | }, 779 | { 780 | "id": 83082, 781 | "link": "https://www.wired.com/category/gear/", 782 | "name": "Gear", 783 | "slug": "gear", 784 | "taxonomy": "category", 785 | "_links": { 786 | "self": [ 787 | { 788 | "href": "https://www.wired.com/wp-json/wp/v2/categories/83082" 789 | } 790 | ], 791 | "collection": [ 792 | { 793 | "href": "https://www.wired.com/wp-json/wp/v2/categories" 794 | } 795 | ], 796 | "about": [ 797 | { 798 | "href": "https://www.wired.com/wp-json/wp/v2/taxonomies/category" 799 | } 800 | ], 801 | "wp:post_type": [ 802 | { 803 | "href": "https://www.wired.com/wp-json/wp/v2/posts?categories=83082" 804 | }, 805 | { 806 | "href": "https://www.wired.com/wp-json/wp/v2/video?categories=83082" 807 | } 808 | ], 809 | "curies": [ 810 | { 811 | "name": "wp", 812 | "href": "https://api.w.org/{rel}", 813 | "templated": true 814 | } 815 | ] 816 | } 817 | }, 818 | { 819 | "id": 4, 820 | "link": "https://www.wired.com/category/gear/reviews/", 821 | "name": "Reviews", 822 | "slug": "reviews", 823 | "taxonomy": "category", 824 | "_links": { 825 | "self": [ 826 | { 827 | "href": "https://www.wired.com/wp-json/wp/v2/categories/4" 828 | } 829 | ], 830 | "collection": [ 831 | { 832 | "href": "https://www.wired.com/wp-json/wp/v2/categories" 833 | } 834 | ], 835 | "about": [ 836 | { 837 | "href": "https://www.wired.com/wp-json/wp/v2/taxonomies/category" 838 | } 839 | ], 840 | "up": [ 841 | { 842 | "embeddable": true, 843 | "href": "https://www.wired.com/wp-json/wp/v2/categories/83082" 844 | } 845 | ], 846 | "wp:post_type": [ 847 | { 848 | "href": "https://www.wired.com/wp-json/wp/v2/posts?categories=4" 849 | }, 850 | { 851 | "href": "https://www.wired.com/wp-json/wp/v2/video?categories=4" 852 | } 853 | ], 854 | "curies": [ 855 | { 856 | "name": "wp", 857 | "href": "https://api.w.org/{rel}", 858 | "templated": true 859 | } 860 | ] 861 | } 862 | } 863 | ], 864 | [ 865 | { 866 | "id": 106059, 867 | "link": "https://www.wired.com/tag/incase/", 868 | "name": "incase", 869 | "slug": "incase", 870 | "taxonomy": "post_tag", 871 | "_links": { 872 | "self": [ 873 | { 874 | "href": "https://www.wired.com/wp-json/wp/v2/tags/106059" 875 | } 876 | ], 877 | "collection": [ 878 | { 879 | "href": "https://www.wired.com/wp-json/wp/v2/tags" 880 | } 881 | ], 882 | "about": [ 883 | { 884 | "href": "https://www.wired.com/wp-json/wp/v2/taxonomies/post_tag" 885 | } 886 | ], 887 | "wp:post_type": [ 888 | { 889 | "href": "https://www.wired.com/wp-json/wp/v2/posts?tags=106059" 890 | }, 891 | { 892 | "href": "https://www.wired.com/wp-json/wp/v2/podcast?tags=106059" 893 | }, 894 | { 895 | "href": "https://www.wired.com/wp-json/wp/v2/video?tags=106059" 896 | } 897 | ], 898 | "curies": [ 899 | { 900 | "name": "wp", 901 | "href": "https://api.w.org/{rel}", 902 | "templated": true 903 | } 904 | ] 905 | } 906 | }, 907 | { 908 | "id": 106060, 909 | "link": "https://www.wired.com/tag/luggage/", 910 | "name": "luggage", 911 | "slug": "luggage", 912 | "taxonomy": "post_tag", 913 | "_links": { 914 | "self": [ 915 | { 916 | "href": "https://www.wired.com/wp-json/wp/v2/tags/106060" 917 | } 918 | ], 919 | "collection": [ 920 | { 921 | "href": "https://www.wired.com/wp-json/wp/v2/tags" 922 | } 923 | ], 924 | "about": [ 925 | { 926 | "href": "https://www.wired.com/wp-json/wp/v2/taxonomies/post_tag" 927 | } 928 | ], 929 | "wp:post_type": [ 930 | { 931 | "href": "https://www.wired.com/wp-json/wp/v2/posts?tags=106060" 932 | }, 933 | { 934 | "href": "https://www.wired.com/wp-json/wp/v2/podcast?tags=106060" 935 | }, 936 | { 937 | "href": "https://www.wired.com/wp-json/wp/v2/video?tags=106060" 938 | } 939 | ], 940 | "curies": [ 941 | { 942 | "name": "wp", 943 | "href": "https://api.w.org/{rel}", 944 | "templated": true 945 | } 946 | ] 947 | } 948 | } 949 | ], 950 | [], 951 | [] 952 | ] 953 | } 954 | } 955 | ``` 956 | 957 | Notice that there is now an `_embed` section with the actual data for authors, categories, and tags. 958 | Later in the workshop, we will leverage this data to display our pages. 959 | 960 | In order to simplify things further, we will be using an NPM package called 961 | [wpapi](https://github.com/wp-api/node-wpapi). This package will allow us to call WordPress API 962 | endpoints through a set of simple functions. Let's install the package: 963 | 964 | ```sh 965 | yarn add wpapi 966 | ``` 967 | 968 | Then, create a file called `api.js` at the root of your project with the following code: 969 | 970 | ```js 971 | import WPAPI from 'wpapi'; 972 | 973 | let endpoint = 'https://www.wired.com/wp-json'; 974 | if (typeof window !== 'undefined') { 975 | endpoint = `https://cors-anywhere.herokuapp.com/${endpoint}`; 976 | } 977 | 978 | const api = new WPAPI({ endpoint }); 979 | export default api; 980 | ``` 981 | 982 | The `wpapi` package exposes a `WPAPI` constructor taking a base URL as parameter. In this case, we 983 | will be using the wired.com API as explained in the introduction. However, remember that this code 984 | will be running in the server *and* in the browser. If our browser tries to make an HTTP request to 985 | wired.com's WordPress API from a different origin – localhost:3000 – our browser will refuse to let 986 | us see the response due to cross-origin security rules. “Cors Anywhere” is a freely available proxy 987 | server that can take care of this by making the request on our behalf and adding the appropriate 988 | [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers to make our browser comply. 989 | Explaining CORS is beyond the scope of this workshop, but feel free to follow the links to learn 990 | more. The **TL;DR** is that the server will be using the straight URL, and the browser will go 991 | through a proxy server. 992 | 993 | :warning: **NOTE**: Ideally, we will have as little of this kind of “if server / if client” code in 994 | our application. Next.js should be able to deal with it for us in most cases. 995 | 996 | Once this file is created, let's use it in our `pages/blog.js` file. Overwrite the file with the 997 | following content: 998 | 999 | ```js 1000 | import React from 'react'; 1001 | import Link from 'next/link'; 1002 | import api from '../api'; 1003 | 1004 | class Blog extends React.Component { 1005 | static async getInitialProps() { 1006 | const posts = await api.posts().embed(); 1007 | return { posts }; 1008 | } 1009 | 1010 | render() { 1011 | const { posts } = this.props; 1012 | 1013 | return ( 1014 |
1015 |

Recent blog posts

1016 | { 1017 | posts.map(post => ( 1018 |
1019 | 1027 | 1028 |

1029 | 1030 | 1031 |

{post.date}

1032 | { 1033 | !!post._embedded['wp:featuredmedia'] && 1034 | {post._embedded['wp:featuredmedia'][0].alt_text} 1039 | } 1040 |
1041 |
1042 | )) 1043 | } 1044 |

1045 | ); 1046 | } 1047 | } 1048 | 1049 | export default Blog; 1050 | ``` 1051 | 1052 | First, we have transformed the `Blog` page component from a function to a class. This allows us to 1053 | add the static `getInitialProps` function. This function is not part of React but of Next.js. The 1054 | function will be executed prior to rendering the page component. This function can return a Promise, 1055 | which we are doing here through the use of the `async` keyword. Next.js will wait for this Promise 1056 | to resolve before rendering. The return value of the Promise will be passed as props to the React 1057 | component. In `render()`, we are using some simple, pure React code to render the recent posts list. 1058 | We will come back to this code later and refactor it. 1059 | 1060 | Notice the new usage of the `` component. We can build the URL to each post by passing a 1061 | `query` prop in addition to the `href` prop. This will build links like `/blogpost?slug=the-slug`. 1062 | The links are currently not pretty, but will come back and fix it later. 1063 | 1064 | Navigate to `/blog` to see your new blog listing. Now that we are loading some data, it would be a 1065 | good time to check that Next.js is working correctly. If you navigate directly to `/blog` from your 1066 | browser's address bar and view source, you will see that the server sent all the post data. Looking 1067 | at your console, you should not see any HTTP requests made in the browser. However, if you navigate 1068 | to `/` and click on the `/blog` link, you will see the same HTTP request being made in your browser, 1069 | with the addition of the Cors Anywhere proxy. 1070 | 1071 | This concludes the blog listing section. Next, let's make those `/blogpost?slug=...` links work! 1072 | 1073 |

1074 | 1075 |

1076 | 1077 | --- 1078 | 1079 | ## Adding the single blog post page :page_facing_up: 1080 | This will be very similar to the previous section, so we will go a bit faster. Create a file called 1081 | `blogpost.js` in the `pages` directory, and put the following code in it: 1082 | 1083 | ```js 1084 | import React from 'react'; 1085 | import Link from 'next/link'; 1086 | import api from '../api'; 1087 | 1088 | class BlogPost extends React.Component { 1089 | static async getInitialProps({ query: { slug } }) { 1090 | const post = await api.posts().slug(slug).embed(); 1091 | return { post: post[0] }; 1092 | } 1093 | 1094 | render() { 1095 | const { post } = this.props; 1096 | 1097 | return ( 1098 |
1099 |

1100 |

{post.date}

1101 |

By {post._embedded.author[0].name}

1102 | { 1103 | !!post._embedded['wp:featuredmedia'] && 1104 | {post._embedded['wp:featuredmedia'][0].alt_text} 1109 | } 1110 |
1111 |
1112 | ); 1113 | } 1114 | } 1115 | 1116 | export default BlogPost; 1117 | ``` 1118 | 1119 | The main differences are the use of the `query` parameter inside `getInitialProps`. This is one of 1120 | the many parameters that Next.js passes to the function. It allows us to decouple ourselves from the 1121 | URL and the environment. `query` will be passed the same both on the client and the server. We 1122 | extract the slug, and pass it to the `.slug()` method of `wpapi`. In `render()`, we simply use 1123 | `post.content` instead of `post.excerpt` to display the full post. 1124 | 1125 | Now that we have a few pages going, you can see that we don't have a common layout between pages. 1126 | Let's fix this in the next section by wrapping each page with a common header and footer. 1127 | 1128 |

1129 | 1130 |

1131 | 1132 | --- 1133 | 1134 | ## Adding a common layout :house: 1135 | There are many ways to add a common layout to a Next.js application. In our case, we will use a 1136 | simple React higher-order component. That's just a fancy word for saying “component that wraps 1137 | another component”. 1138 | 1139 | Create a directory called `components` at the root of your application. This is by no means a 1140 | special directory to Next.js. It will only allow us to separate our code. In this directory, create 1141 | a new file called `withLayout.js` with the following code: 1142 | 1143 | ```js 1144 | import React from 'react'; 1145 | import Link from 'next/link'; 1146 | 1147 | const withLayout = (WrappedComponent) => { 1148 | class WithLayout extends React.Component { 1149 | static async getInitialProps(...args) { 1150 | if (typeof WrappedComponent.getInitialProps === 'function') { 1151 | return WrappedComponent.getInitialProps(...args); 1152 | } 1153 | 1154 | return {}; 1155 | } 1156 | 1157 | render() { 1158 | const { props } = this; 1159 | 1160 | return ( 1161 |
1162 |
1163 | 1169 |
1170 |
1171 | 1172 |
1173 |
1174 | this is the footer 1175 |
1176 |
1177 | ); 1178 | } 1179 | } 1180 | 1181 | WithLayout.displayName = `withLayout(${WrappedComponent.displayName || WrappedComponent.name || 'UnknownComponent'})`; 1182 | return WithLayout; 1183 | }; 1184 | 1185 | export default withLayout; 1186 | ``` 1187 | 1188 | In this module, we export a function called `withLayout`. This function takes a React component and 1189 | returns a new React component class. The class has a `render()` method, but also a 1190 | `getInitialProps` static function. This is necessary due to how we will be wrapping page components. 1191 | The `getInitialProps` function simply proxies to the wrapped component's `getInitialProps` if it 1192 | exists. The `.displayName` bit at the end is especially helpful with React Developer Tools. It will 1193 | allow us to clearly identify the `withLayout` wrapper as can be seen in the following screenshot: 1194 | 1195 | ![react dev tools view of withLayout](withLayout.png) 1196 | 1197 | Now that the component has been created, let's use it to wrap our page components. 1198 | 1199 | Open `pages/index.js` and make the following changes: 1200 | 1201 | ```js 1202 | // Add this at the top 1203 | import withLayout from '../components/withLayout'; 1204 | 1205 | // Modify the last line to read 1206 | export default withLayout(HomePage); 1207 | ``` 1208 | 1209 | :warning: **NOTE**: You might have to restart your Next.js server after adding the layout. 1210 | 1211 | That's it. The component's code stays the same, but we export a wrapped version of it instead. Now, 1212 | do the same with the other two pages and look at the result in your browser. 1213 | 1214 | --- 1215 | 1216 | ## How about some SEO? :mag: 1217 | Let's look back at what we've done so far. Using a minimal amount of code, we were able to get a 1218 | server-rendered, universal React theme for a WordPress blog. This has given us all the benefits of 1219 | modern front-end technologies as well as the benefits of server-rendered SEO: crawler can see our 1220 | content, and users can see it sooner because it comes with the initial HTTP response. 1221 | 1222 | One major thing that is missing for SEO is a useful title for each page. As you might have expected, 1223 | Next.js makes it easy to add elements to the `` of the page. Let's start by adding a title to 1224 | the home page. Open `pages/index.js` and make the following changes: 1225 | 1226 | ```js 1227 | // Add this line at the top 1228 | import Head from 'next/head'; 1229 | 1230 | // Add this bit after the opening
1231 | 1232 | Welcome to my site 1233 | 1234 | ``` 1235 | 1236 | Then, do the same with the blog listing and single blog post pages. For the single blog post page, 1237 | simply use `{post.title.rendered}`. 1238 | 1239 | What about other SEO elements like `` an so on? The popular 1240 | [Yoast SEO](https://yoast.com/wordpress/plugins/seo/) WordPress plugin allows you to easily add the 1241 | data for these tags from your administrative interface, and they are exposed using 1242 | [WordPress' Post Meta Data](https://codex.wordpress.org/Post_Meta_Data_Section) feature. Sadly, the 1243 | WordPress API does not expose Post Meta Data by default. The 1244 | [`register_rest_field`](https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-response/) 1245 | WordPress function allows an administrator to add certain fields to a standard post or page 1246 | response. 1247 | 1248 | An example of this is the 1249 | [wp-api-yoast-meta](https://plugins.trac.wordpress.org/browser/wp-api-yoast-meta/trunk/plugin.php) 1250 | WordPress plugin. The linked source code shows a clear usage of this function. If you install this 1251 | plugin on the WordPress side, you will be able to access Yoast meta directly from the post response. 1252 | 1253 | Then, you can add whatever fields you need under the `` tag in your React page component. 1254 | 1255 |

1256 | 1257 |

1258 | 1259 | --- 1260 | 1261 | ## My blog looks like :poop: 1262 | So far, we have worked hard to create a meaningful WordPress base theme from the content 1263 | perspective. However, our blog currently looks like :poop: Let's fix this with some CSS. 1264 | 1265 | Next.js uses [styled-jsx](https://github.com/zeit/styled-jsx) to allow scoped styling of components. 1266 | This is one of the many available styling solutions for React components, and it's the recommended 1267 | way to style with Next.js. While you can use other methods, they might require a lot of workarounds 1268 | to work perfectly on both the server and client. 1269 | 1270 | The **TL;DR** of styled-jsx is that you add a ` 1285 | ``` 1286 | 1287 | If this were regular CSS, we would probably not target `header` only. It is a bit too generic. With 1288 | styled-jsx however, things are different. Navigate to your site and use your browser's element 1289 | inspector. You will notice that each element that is part of `WithLayout` has had a seemingly 1290 | random class name added to it, and all the styles you wrote inside ` 1318 | ``` 1319 | 1320 | This will apply the `body` styles without scoping them. 1321 | 1322 | :warning: **NOTE**: Even though these styles are defined as “global”, they will only apply if the 1323 | component that includes them is mounted. Here, `withLayout` should always be mounted since it wraps 1324 | all pages, so it's the perfect place to do this. 1325 | 1326 | Right now, you might be thinking that that's a bit too much mixing in the same file. If that is the 1327 | case, know that styled-jsx allows you to 1328 | [keep your CSS in separate files](https://github.com/zeit/styled-jsx#keeping-css-in-separate-files ). 1329 | Follow the link to learn more about how this is done. 1330 | 1331 | Now that you know how styled-jsx works, it's time to personalize your blog! You could take some 1332 | inspiration from the [twentyseventeen demo](https://2017.wordpress.net/) or any other theme that 1333 | you like, or invent your own. 1334 | 1335 |

1336 | 1337 |

1338 | 1339 | --- 1340 | 1341 | ## Enabling “pretty links” for the single post page :heart: 1342 | The basic page structure of Next.js does not natively support so-called “pretty-links”. If running 1343 | Next.js with the basic command-line like we have been doing, we do not have much control over this. 1344 | Each file in the `pages` directory corresponds to a page URL. However, Next.js provides a NodeJS 1345 | request handler that lets us extend it at will. Let's do it! 1346 | 1347 | To be clear, what we will do in this section is change the single blog post links from: 1348 | 1349 | ``` 1350 | /blogpost?slug=super-cool-post 1351 | ``` 1352 | 1353 | to: 1354 | 1355 | ``` 1356 | /blog/super-cool-post 1357 | ``` 1358 | 1359 | For this section, we will be installing the ExpressJS web server. Then, we will create a server 1360 | module that will import Next.js' request handler. We will then spin up our own ExpressJS web server, 1361 | something that Next normally does for us. In there, we will define a custom route with a path 1362 | parameter, and hand over the control to Next. Then, we will define a catch-all route that will 1363 | simply foward to Next's handler immediately. 1364 | 1365 | First, run `yarn add express`. Then, create a file called `server.js` at the root of your project. 1366 | In there, add the following code: 1367 | 1368 | ```js 1369 | const express = require('express'); 1370 | const next = require('next'); 1371 | 1372 | const port = parseInt(process.env.PORT, 10) || 3000; 1373 | const dev = process.env.NODE_ENV !== 'production'; 1374 | const app = next({ dev }); 1375 | const handle = app.getRequestHandler(); 1376 | const server = express(); 1377 | 1378 | (async () => { 1379 | await app.prepare(); 1380 | 1381 | server.get('/blog/:slug', (req, res) => ( 1382 | app.render(req, res, '/blogpost', req.params) 1383 | )); 1384 | 1385 | server.get('*', (req, res) => ( 1386 | handle(req, res) 1387 | )); 1388 | 1389 | server.listen(port, (err) => { 1390 | if (err) throw err; 1391 | console.log(`Listening on http://0.0.0.0:${port}`); 1392 | }); 1393 | })(); 1394 | ``` 1395 | 1396 | If you've used Express before, some of this code will look familiar. The first `server.get` call 1397 | sets up a route with pattern `/blog/:slug:`. In the handler for that route, we simply pass the 1398 | request parameters – an object containing the `slug` key – to the Next.js `app.render` function. 1399 | Next will in turn pass these parameters to the `getInitialProps` static function of our single blog 1400 | post page component, and the rest will follow. 1401 | 1402 | The second `server.get` is our catch-all route. There, we have nothing special to do. Next.js will 1403 | handle the request from beginning to end through the `handle` function we extracted from it. 1404 | 1405 | To use this new `server.js` file, open `package.json` and change the `start` script to: 1406 | 1407 | ```sh 1408 | NODE_ENV=development node server.js 1409 | ``` 1410 | 1411 | Then, kill the running server and re-run `yarn start`. 1412 | 1413 | Now that this has been setup, you can manually navigate to `/blog/an-existing-slug` and see the 1414 | resulting page. The final step will be to modify the ``s generated by the blog listing page. 1415 | 1416 | Open `pages/blog.js` and change the `` to the following: 1417 | 1418 | ```js 1419 | 1428 | ``` 1429 | 1430 | The `as` prop will make Next.js output the pretty link, but internally Next will use the `href` 1431 | prop to execute the dynamic inter-page navigation. That's all there is to it :) 1432 | 1433 |

1434 | 1435 |

1436 | 1437 | ### 404: Dealing with posts that don't exist 1438 | With this route setup, any URL of the form `/blog/something` will go to our `blogpost.js` page 1439 | component. On the server side, this will always return an HTTP 200 status code, even for posts that 1440 | don't exist. Let's fix this. 1441 | 1442 | Open `pages/blogpost.js` and make the following changes: 1443 | 1444 | ```js 1445 | class BlogPost extends React.Component { 1446 | // Change getInitialProps to the following 1447 | static async getInitialProps({ query: { slug }, res }) { 1448 | const post = (await api.posts().slug(slug).embed())[0]; 1449 | if (post) { 1450 | return { post }; 1451 | } 1452 | 1453 | if (res) { 1454 | res.statusCode = 404; 1455 | } 1456 | return { error: true }; 1457 | } 1458 | 1459 | render() { 1460 | // Add this bit to the top of Render 1461 | if (this.props.error) { 1462 | return
Post not found
; 1463 | } 1464 | 1465 | // ...rest of render 1466 | } 1467 | } 1468 | ``` 1469 | 1470 | Basically, if the WordPress API returns no post for the slug we need, then we will return an error 1471 | page. In addition to this, if we are running the code on the server – the `if (res)` bit – then we 1472 | will set the response's status code to 404 to help with our SEO. 1473 | 1474 |

1475 | 1476 |

1477 | 1478 | --- 1479 | 1480 | ## Interactive feature: infinite scroll! :boom: 1481 | Let's take advantage of the fact that we're using React to easily add an interactive feature: 1482 | infinite scrolling and loading of posts on the `/blog` page. 1483 | 1484 | We will implement this using the [react-waypoint](https://github.com/brigade/react-waypoint) 1485 | component. This component fires a callback function when it becomes visible in the viewport. Let's 1486 | start by installing the package: 1487 | 1488 | ```sh 1489 | yarn add react-waypoint 1490 | ``` 1491 | 1492 | Then, open `pages/blog.js` and make the following changes: 1493 | 1494 | 1. Import the component at the top, and add a `PER_PAGE` constant: 1495 | 1496 | ```js 1497 | import Waypoint from 'react-waypoint'; 1498 | const PER_PAGE = 10; 1499 | ``` 1500 | 1501 | 2. Add an initial state to the `Blog` class: 1502 | 1503 | ```js 1504 | class BlogPost extends React.Component { 1505 | state = { 1506 | page: 1, 1507 | loading: false, 1508 | hasMore: true, 1509 | } 1510 | } 1511 | ``` 1512 | 1513 | 3. Modify the `wpapi` call in `getInitialProps` to load page 1 with 10 items per page: 1514 | 1515 | ```js 1516 | const posts = await api.posts().perPage(PER_PAGE).page(1).embed(); 1517 | ``` 1518 | 1519 | 4. Since this component will become stateful, we will render posts from the state. Add a 1520 | `componentWillMount` method with the following code. Note that we use “will mount” because it runs 1521 | on both the client and the server: 1522 | 1523 | ```js 1524 | this.setState({ 1525 | posts: this.props.posts 1526 | }); 1527 | ``` 1528 | 1529 | 5. Modify the `render()` method: 1530 | 1531 | 1. Change the line that reads: 1532 | 1533 | ```js 1534 | const { posts } = this.props; 1535 | ``` 1536 | 1537 | to: 1538 | 1539 | ```js 1540 | const { posts } = this.state; 1541 | ``` 1542 | 1543 | Otherwise, all the work you did so far will be for nothing, since posts will still be rendered from the initial props! 1544 | 1545 | 2. Add the following before the closing `
`: 1546 | 1547 | ```js 1548 | {this.state.hasMore && } 1549 | {this.state.loading &&

loading...

} 1550 | ``` 1551 | 1552 | 6. Finally, add the `loadMore` method as an instance method of the class: 1553 | 1554 | ```js 1555 | loadMore = async () => { 1556 | if (this.state.loading || !this.state.hasMore) { 1557 | return; 1558 | } 1559 | 1560 | this.setState({ loading: true}); 1561 | const posts = await api.posts().perPage(PER_PAGE).page(this.state.page + 1).embed(); 1562 | if (posts.length > 0) { 1563 | this.setState({ 1564 | posts: this.state.posts.concat(posts), 1565 | page: this.state.page + 1 1566 | }); 1567 | } 1568 | else { 1569 | this.setState({ hasMore: false }); 1570 | } 1571 | this.setState({ 1572 | loading: false 1573 | }); 1574 | } 1575 | ``` 1576 | 1577 | This function will be called by react-waypoint when the bottom of the page is reached. The logic is 1578 | quite straightforward: we load the next page of posts, and concat the result to the end of the posts 1579 | array in our state. If there are no more posts, setting `hasMore` to `false` will completely remove 1580 | the `` element from the `render()` method, thereby preventing calling the callback for 1581 | nothing. 1582 | 1583 | That's it! Load the `/blog` page in your browser and start scrolling. 1584 | 1585 | :warning: **NOTE**: While this is a simple way to implement infinite scrolling, it has one major 1586 | pitfall: if you scroll too much, the DOM will become overloaded with too much content that is not 1587 | being displayed. This can be wasteful. Some infinite loaders try to mitigate this by replacing the 1588 | elements that are out of view with one single empty div with a fixed CSS height. This is more 1589 | complex because it requires knowing the height of each element in advance, but it is certainly 1590 | doable. It is beyond the scope of this workshop. 1591 | 1592 |

1593 | 1594 |

1595 | 1596 | --- 1597 | 1598 | ## Where to go from here? :eyes: 1599 | If you finished this workshop early or want to take it home, here are some things you could work on: 1600 | 1601 | * Add error handling. 1602 | * Add categories and tags to the single post page 1603 | * Add a category listing page 1604 | * Add a single category page 1605 | * Add author pages and link to them 1606 | * Componentize: separate the page components from the rendering logic 1607 | * Add page caching at the ExpressJS level 1608 | * Add a search feature 1609 | 1610 | If you can imagine it, you can do it! 1611 | 1612 |

1613 | 1614 |

1615 | --------------------------------------------------------------------------------