Learn how to create a food ordering app with Gridsome, Snipcart and your favorite headless CMS: Strapi. In this episode: Creating single product views with Gridsome templates.
This article is a guest series by the great Ekene Eze. He’s leading the Developer Experience team at Flutterwave and wrote this blog post through the Write for the Community program.
This series will walk you through the processes of setting up your own food ordering application and by extension any e-commerce app using modern development tools like Strapi, Gridsome and Snipcart.
Table of Contents: 1. Part 1 - Generating a Strapi app and creating products 2. Part 2 - Setting up a Gridsome project 3. Part 3 - Consuming products with Gridsome and GraphQL 4. Part 4 - Creating single product views with Gridsome templates 5. Part 5 - Implementing cart and checkout with Snipcart 6. Part 6 - Deploying the apps
In the last part, we successfully connected our Gridsome application to fetch products from our Strapi backend and display them for users. In this part, we will create single product views with Gridsome templates so that customers can open each product, view more details about it, and also have the opportunity to place orders.
At the moment, we have a product listing page where we can view all products. But clicking on any of the products will lead to a 404 page as we can see below:
Next, let's make it possible for users to view the product pages using Gridsome templates. Gridsome uses templates to create single pages for nodes in a collection. Let's see how that works. In the project's src/templates
folder, create a new file called Product.vue
and set it up like so:
1<!-- src/templates/Product.vue -->
2<template>
3 <Layout>
4 <template>
5 <v-card elevation="0" class="mx-auto center mt-5 mb-3" max-width="900">
6 <v-img
7 class="white--text align-end"
8 height="200px"
9 width="300px"
10 :src="`http://localhost:1337${this.$page.product.image}`"
11 >
12 </v-img>
13 <v-card-title>{{ this.$page.product.title }}</v-card-title>
14 <v-card-text class="text--primary">
15 <div>{{ this.$page.product.description }}</div>
16 </v-card-text>
17 <v-card-subtitle class="pb-0">
18 <h3>Instructions</h3>
19 {{ this.$page.product.instructions }}
20 </v-card-subtitle>
21 <v-card-actions>
22 <v-btn
23 rounded
24 outlined
25 color="orange"
26 text
27 >
28 Add to cart
29 </v-btn>
30 <v-spacer></v-spacer>
31 <v-rating
32 :value="this.$page.product.rating"
33 color="amber"
34 dense
35 half-increments
36 readonly
37 size="14"
38 ></v-rating>
39 <span>
40 <v-card-title>${{ this.$page.product.price }}</v-card-title>
41 </span>
42 </v-card-actions>
43 </v-card>
44 </template>
45 </Layout>
46</template>
47
48<page-query>
49query Products($id: ID!) {
50 product(id: $id) {
51 id
52 title
53 image
54 description
55 price,
56 instructions,
57 rating
58 }
59}
60</page-query>
61
62<script>
63export default {};
64</script>
In the snippet above, we created a new single view product template. In the template, we use Vuetify's UI components (card, buttons etc) to organize the view of the product's details. Finally, to ensure that we route the appropriate product to its equivalent details page, we write a page query
that will return individual product details with respect to the ID we pass into it.
To ensure that this behavior is achieved, we need to update the gridsome.config.js
file in the project root directory:
1 // gridsome.config.js
2 module.exports = {
3 siteName: "Gridsome",
4 plugins: [],
5 templates: {
6 Product: "/products/:id",
7 },
8 };
This is how to set up Gridsome templates and routes. You can learn more about it and other possible configuration option here.
With this, we should be able to route to our individual product pages without hassle. Let's check back on the browser:
Now that we have the template and single page routes setup, let's add the Shop and Support navigations on the header for completeness.
In Gridsome, every new file created in the pages
directory maps to a new route automatically. This means that in order for us to show different pages for the /shop
route and the /support
route, all we need to do is create those pages and populate it with our contents of choice. In my case, I will display the same products we have in the homepage in the /shop
page and display a contact form in the /support
page. Create a new pages/shop.vue
file and update it with the snippet below:
1 <!-- src/pages/shop.vue -->
2 <template>
3 <Layout>
4 <Products />
5 </Layout>
6 </template>
7 <page-query>
8 query{
9 products: allProduct{
10 edges{
11 node{
12 id,
13 title,
14 description,
15 rating,
16 image,
17 price
18 }
19 }
20 }
21 }
22 </page-query>
23 <script>
24 import Products from "../components/Products";
25 export default {
26 components: {
27 Products,
28 },
29 };
30 </script>
Basically, what we are doing is rendering the Products
component again in this page such that when users navigate to it, they still see a list of products to order. You can organize your app differently, but this should be enough for the scope of this series.
We'll do the same for the /support
route, create a new pages/support.vue
file. In this file, we are going to display a form where users can type in their questions and send to the site admins. Because I have this habit of implementing validations on my forms, I will use vee-validate
to handle validations on the form. Install it with the command below:
npm i vee-validate
Next, open the support.vue
file and update it with the snippet below:
1 <!-- src/pages/support.vue -->
2 <template>
3 <Layout>
4 <validation-observer ref="observer">
5 <form class="center mb-4 mt-3">
6 <h3>Contact us</h3>
7 <validation-provider
8 v-slot="{ errors }"
9 name="Name"
10 rules="required|max:10"
11 >
12 <v-text-field
13 v-model="name"
14 :counter="10"
15 :error-messages="errors"
16 label="Name"
17 required
18 ></v-text-field>
19 </validation-provider>
20 <validation-provider
21 v-slot="{ errors }"
22 name="email"
23 rules="required|email"
24 >
25 <v-text-field
26 v-model="email"
27 :error-messages="errors"
28 label="E-mail"
29 required
30 ></v-text-field>
31 </validation-provider>
32 <validation-provider v-slot="{ errors }" name="select" rules="required">
33 <v-select
34 v-model="select"
35 :items="items"
36 :error-messages="errors"
37 label="Query type"
38 data-vv-name="select"
39 required
40 ></v-select>
41 </validation-provider>
42 <v-textarea
43 outlined
44 name="input-7-4"
45 label="What problem do you have"
46 value="Tell us more about the problem"
47 ></v-textarea>
48 <v-textarea
49 outlined
50 name="input-7-4"
51 label="What's your business like"
52 value="Tell us more about your business"
53 ></v-textarea>
54 <v-btn class="mr-4" @click="submit"> Send </v-btn>
55 <v-btn @click="clear"> Clear </v-btn>
56 </form>
57 </validation-observer>
58 <div class="separator"></div>
59 </Layout>
60 </template>
61 <script>
62 import { required, email, max } from "vee-validate/dist/rules";
63 import {
64 extend,
65 ValidationObserver,
66 ValidationProvider,
67 setInteractionMode,
68 } from "vee-validate";
69 setInteractionMode("eager");
70 extend("required", {
71 ...required,
72 message: "{_field_} can not be empty",
73 });
74 extend("max", {
75 ...max,
76 message: "{_field_} may not be greater than {length} characters",
77 });
78 extend("email", {
79 ...email,
80 message: "Email must be valid",
81 });
82 export default {
83 components: {
84 ValidationProvider,
85 ValidationObserver,
86 },
87 data: () => ({
88 name: "",
89 email: "",
90 select: null,
91 errors: null,
92 items: ["Orders", "Delivery", "Tech support", "Refund"],
93 checkbox: null,
94 }),
95 methods: {
96 submit() {
97 this.$refs.observer.validate();
98 },
99 clear() {
100 this.name = "";
101 this.email = "";
102 this.select = null;
103 this.checkbox = null;
104 this.$refs.observer.reset();
105 },
106 },
107 };
108 </script>
109 <style>
110
111 </style>
We have created a form with fields for the customer's name, email, and query type. We also added text area's for the user to provide more detailed information about their issues. Of course, these are all examples and should be replaced with whatever content you deem more appropriate for your use case. If you are new to vee-validate
, it is a Vue.js based framework that makes it easy to validate form inputs and display errors. You can read more about it here.
At this point, if we test out the navigations on the browser, we should get all the new pages we created showing up in their respective routes.
In this part, we've been able to create single product views with Gridsome templates. We've also been able to set up the /shop
and /support
navigations with the corresponding pages showing on the browser. We also used vee-validate
to handle form input validation on the support page. In the next section, we are going to add cart and checkout functionalities to our Gridsome application so that users can indeed order meals from our restaurant. See you there!
https://mealzers.netlify.app
Ekene is a Developer Experience Engineer and Technical Writer. He is currently working with the Developer Experience team at Netlify where they build tools, create content, open-source repos, and demos to teach and also help developers build a better web with Netlify's products and services.