Build a product delivery page with Rust and Uniform
Learn how to build a product delivery page using Cloudinary for media optimization, Uniform for composing experience, and Yew; a Rust-based frontend framework for rendering the product markup.
Demola Malomo
Mar 05 2023
9 min read
A product delivery page shows relevant product details like images, size, colour, price, reviews, etc. It is an essential marketing strategy used to show relevant information that converts leads to sales.
In this post, we will learn how to build a product delivery page using Cloudinary for media optimization, Uniform for composing experience, and Yew; a Rust-based frontend framework for rendering the product markup.
Prerequisites
To fully grasp the concepts presented in this tutorial, the following requirements apply:
- Basic understanding of Rust
- Trunk and Wasm32 installed
- Cloudinary account (create an account here)
- Uniform account (create an account here)
Getting started
Before we dive into building our application, it is paramount we understand the layout of our application. It will serve as a foundation for modelling the products with Cloudinary and Uniform.
In this post, we will focus on modelling the flower components with Cloudinary and Uniform.
Image Sourcing and Upload to Cloudinary
To start building our application, we must upload sample images for our product delivery page.
Sample images
- Rose flower - https://bit.ly/3tdOXHz
- Hibiscus flower - https://bit.ly/3zYQ0yJ
- Orchid flower - https://bit.ly/3UJMmB3
- Tulip flower - https://bit.ly/3fQ3XrW
In our Cloudinary dashboard, we uploaded the images by clicking on the Media Library tab, clicking on Upload, selecting the Web Address option, inputting the URL, and clicking on the Arrow Button to upload.
After uploading the image, we will see it displayed on the console.
Putting it all together on Uniform
With that done, we can start creating component libraries on Uniform. To do this, we must sign up and fill in the required information.
Next, input desired project name and click Continue.
Next, navigate to the Security tab, select API Keys, and click on the rounded Plus Icon to create one. Input marketting_feature
as the API name, click on Add to Project, mark all the permissions, and click on Set Permissions. Then click on the Create API Key to create the API key.
With this done, we should see a screen containing our API Key and Project ID. We need to copy these values as they will come in handy when building our application with Yew.
Understanding Components and Compositions on Uniform
Before we continue modelling our project on Uniform, we must understand the features we will be leveraging to achieve this. Components in Uniform application work similarly to those in a Frontend application; it lets us break our application into smaller reusable building blocks with properties, while a Composition is the combination of one or more components. For our project, we will create a flower_component
.
Add Cloudinary integration support
Uniform improves the product’s digital experience through integration with an existing system. To connect Cloudinary to our project, we need to navigate to the Projects tab, click on the project, and click on any of the highlighted sections to add integrations to our project.
Search or browse through the available integrations, select the Cloudinary integration, click on the Add to project button, input the Cloudname
, API Key
and Save.
We can get our Cloud Name and API Key from our Cloudinary dashboard.
Create components
To get started, navigate to the Projects tab and click on the project. Then navigate to the Canvas tab, select the Component Library, and click on the Add component button.
Parameter Name | Help Text | Type | required |
---|---|---|---|
name | product name | text | YES |
img | product image | Cloudinary |
Input flower_component
as the component name, select shopping cart as the icon, add properties of name
, and img
as shown above, and then click OK.
Then click on the Save and Close button.
Following the same approach, we need to create a body_component
and add the properties shown below:
Parameter Name | Help Text | Type | required |
---|---|---|---|
title | title | text | YES |
description | description | text | YES |
Then click on the Save and Close button.
Now that we have created the flower_component
, it will serve as blueprints/building blocks for creating our product delivery page.
To start, click the Plus Icon, input Product Page
as the component name, and check the Composition Component. Then navigate to the Slots section, and click on the Plus Icon to create a slot.
PS: Slots help us create instances of our component and allow them to accept data dynamically.
Input Flowers
as the Slot Name, select the flower_component
as the allowed components, and click OK.
Then click on the Save and Close button.
With that done, we can start using the Product Page
component to compose our product delivery page. To do this, navigate to the Composition tab, and click on the Plus Icon to create a composition.
Select the Product Page
as the composition type, input Home
as the name, and Create.
Input /flowers
as the slug and click on the Plus Icon to add a component to map out a new component.
PS: The slug inputted will come in handy when searching for our composition.
Select the flower_component and add the corresponding image
and name for the four flowers uploaded to Cloudinary earlier and input matching name.
We need to repeat the steps above to add the remaining flower_component data. Then click on Save and Publish option.
Finally, we need to click on the Publish button. This makes our composition available to third-party applications.
Building the user interface in Yew
With that done, we can start building the user interface and use Uniform to deliver the list of products seamlessly. To get started, we need to navigate to the desired directory and run the command below in our terminal:
cargo new product-page && cd product-page
This command creates a Rust project called product-page
and navigates into the project directory.
Next, we proceed to install the required dependencies by modifying the [dependencies]
section of the Cargo.toml
file as shown below:
1 //other code section goes here 2 3 [dependencies] 4 yew = "0.19" 5 serde = { version = "1.0.145", features = ["derive"] } 6 reqwest = { version = "0.11", features = ["json"] } 7 wasm-bindgen-futures = "0.4"
yew = "0.19"
is a Rust-based frontend framework
serde = { version = "1.0.145", features = ["derive"] }
is a framework for serializing and deserializing Rust data structures. E.g. convert Rust structs to a JSON.
reqwest = { version = "0.11", features = ["json"] }
is a HTTP request crate.
wasm-bindgen-futures = "0.4"
is a Rust-based library for performing asynchronous programming in Yew by bridging the gap between Rust asynchronous programming (futures) and JavaScript Promises
.
We need to run the command below to install the dependencies:
1cargo build
HTML Render
With the project dependencies installed, we need to create an index.html
file with Bootstrap CDN support in the root directory of our project. Yew uses this file as entry point into the DOM, similarly to the way modern Frontend framework works.
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous"> 8 <title>Product Page</title> 9 </head> 10 <body> 11 </body> 12 </html>
Structuring our application
To ensure maintainability and scalability, we need to structure our project properly. To do this, we need to navigate to the src
folder and create a components
and models
folder with the corresponding mod.rs
file to manage visibility.
To use the code in the modules, we need to declare them as a module and import them into the main.rs
file as shown below:
1 use yew::prelude::*; 2 3 //add below 4 mod components; 5 mod models; 6 7 #[function_component(App)] 8 fn app() -> Html { 9 //app code goes here 10 } 11 12 fn main() { 13 yew::start_app::<App>(); 14 }
Creating Application Model
Uniform ships with a language-agnostic Platform API for managing and composing experience. We can test the API by filling in the API Key, Project ID, and Slug.
With that in mind, we can create a product.rs
file inside the models
folder and add the snippet below:
1 use serde::Deserialize; 2 3 #[derive(Clone, Deserialize, PartialEq)] 4 pub struct RootComposition { 5 pub composition: Composition, 6 } 7 8 #[derive(Clone, Deserialize, PartialEq)] 9 pub struct Composition { 10 pub slots: Slots, 11 } 12 13 #[derive(Clone, Deserialize, PartialEq)] 14 pub struct Slots { 15 pub flowers: Vec<Flower>, 16 } 17 18 #[derive(Clone, Deserialize, PartialEq)] 19 pub struct Flower { 20 pub parameters: Parameters, 21 } 22 23 #[derive(Clone, Deserialize, PartialEq)] 24 pub struct Parameters { 25 pub img: Img, 26 pub name: Name, 27 } 28 29 #[derive(Clone, Deserialize, PartialEq)] 30 pub struct Img { 31 pub value: Vec<Value>, 32 } 33 34 #[derive(Clone, Deserialize, PartialEq)] 35 pub struct Value { 36 pub alt: String, 37 pub url: String, 38 } 39 40 #[derive(Clone, Deserialize, PartialEq)] 41 pub struct Name { 42 pub value: String, 43 }
The snippet above does the following:
- Imports the required dependency
- Creates multiple structs that use the
derive
macro to generate implementation support for formatting the output and deserializing the API response object
Next, we must register the product.rs
file as part of the models
module. To do this, open the mod.rs
in the models
folder and add the snippet below:
1pub mod product;
Creating Components
With the model fully set up, we can start creating our application building blocks.
First, we need to navigate to the components
folder and create a header.rs
file and add the snippet below:
1 use yew::prelude::*; 2 3 #[function_component(Header)] 4 pub fn header() -> Html { 5 html! { 6 <nav class="navbar bg-black"> 7 <div class="container-fluid"> 8 <a class="navbar-brand text-white" href="#">{"Product List"}</a> 9 </div> 10 </nav> 11 } 12 }
The snippet above creates a Header
component to represent our application header.
Secondly, we need to create a loader.rs
file in the same components
folder and add the snippet below:
1 use yew::prelude::*; 2 3 #[function_component(Loader)] 4 pub fn loader() -> Html { 5 html! { 6 <div class="spinner-border" role="status"> 7 <span class="visually-hidden">{"Loading..."}</span> 8 </div> 9 } 10 }
The snippet above creates a Loader
component representing a UI when our application is loading.
Thirdly, we need to create a message.rs
file in the same components
folders and add the snippet below:
1 use yew::prelude::*; 2 3 #[derive(Properties, PartialEq)] 4 pub struct MessageProp { 5 pub text: String, 6 pub css_class: String, 7 } 8 9 #[function_component(Message)] 10 pub fn message(MessageProp { text, css_class }: &MessageProp) -> Html { 11 html! { 12 <p class={css_class.clone()}> 13 {text.clone()} 14 </p> 15 } 16 }
The snippet above does the following:
- Imports the required dependency
- Creates a
MessageProp
struct withtext
andcss_class
properties to represent the component property. The#[derive(Properties, PartialEq)]
macros mark the struct as a component prop - Destructures the props and use them as CSS class and display text in the markup
Fourthly, we need to create a flower_card.rs
file in the same components
folders and add the snippet below:
1 use yew::prelude::*; 2 use crate::models::product::Flower; 3 4 #[derive(Properties, PartialEq)] 5 pub struct FlowerCardProp { 6 pub flower: Flower, 7 } 8 9 #[function_component(FlowerCard)] 10 pub fn flower_card(FlowerCardProp { flower }: &FlowerCardProp) -> Html { 11 let name = flower.parameters.name.value.clone(); 12 let image_url = flower.parameters.img.value[0].url.clone(); 13 let image_alt = flower.parameters.img.value[0].alt.clone(); 14 15 html! { 16 <div class="col-md-6 col-lg-4 col-xl-3 mb-5"> 17 <div class="card" style="width: 18rem;"> 18 <img src={image_url} class="card-img-top" alt={image_alt} /> 19 <div class="card-body"> 20 <h5 class="card-title">{name}</h5> 21 </div> 22 </div> 23 </div> 24 } 25 }
The snippet above does the following:
- Imports the required dependencies
- Creates a
FlowerCardProp
component props with aflower
property - Destructures the props by creating a copy of the required parameters and using them in the UI
Finally, we must register the newly created components as part of the components
module. To do this, open the mod.rs
in the components
folder and add the snippet below:
1 pub mod flower_card; 2 pub mod header; 3 pub mod loader; 4 pub mod message;
Putting it all together
With the application components created, we can start using them to build our application by modifying the main.rs
file as shown below:
1 use components::{flower_card::FlowerCard, header::Header, loader::Loader, message::Message}; 2 use models::product::{RootComposition, Slots}; 3 use reqwest::{header, Client, Error}; 4 use yew::prelude::*; 5 6 mod components; 7 mod models; 8 9 #[function_component(App)] 10 fn app() -> Html { 11 let flowers: UseStateHandle<Option<Slots>> = use_state(|| None); 12 let error: UseStateHandle<Option<Error>> = use_state(|| None); 13 14 { 15 //create copies of the states 16 let flowers = flowers.clone(); 17 let error = error.clone(); 18 19 //construct uniform api endpoint 20 let mut headers = header::HeaderMap::new(); 21 headers.insert("Content-Type", "application/json".parse().unwrap()); 22 headers.insert("x-api-key", "<REPLACE WITH API KEY>".parse().unwrap()); 23 let url = format!( 24 "https://uniform.app/api/v1/canvas?projectId={id}&slug={slug}&state=64", 25 id = "<REPLACE WITH PROJECT ID>", 26 slug = "/flowers" 27 ); 28 29 use_effect_with_deps( 30 move |_| { 31 let client = Client::new(); 32 wasm_bindgen_futures::spawn_local(async move { 33 let fetched_flowers = client.get(url).headers(headers).send().await; 34 match fetched_flowers { 35 Ok(response) => { 36 let json = response.json::<RootComposition>().await; 37 match json { 38 Ok(data) => flowers.set(Some(data.composition.slots)), 39 Err(e) => error.set(Some(e)), 40 } 41 } 42 Err(e) => error.set(Some(e)), 43 } 44 }); 45 || () 46 }, 47 (), 48 ); 49 } 50 51 let flower_list_logic = match flowers.as_ref() { 52 Some(flowers) => flowers 53 .flowers 54 .iter() 55 .map(|flower| { 56 html! { 57 <FlowerCard flower={flower.clone()}/> 58 } 59 }) 60 .collect(), 61 None => match error.as_ref() { 62 Some(e) => { 63 println!("{}", e); 64 html! { 65 <Message text={"Error getting list of users"} css_class={"text-danger"}/> 66 } 67 } 68 None => { 69 html! { 70 <Loader /> 71 } 72 } 73 }, 74 }; 75 76 html! { 77 <> 78 <Header /> 79 <section class="section-products mt-5"> 80 <div class="container"> 81 <div class="row justify-content-center text-center"> 82 <div class="col-md-8 col-lg-6"> 83 <div class="header"> 84 <h5 class="">{"Popular Product List"}</h5> 85 </div> 86 </div> 87 </div> 88 <div class="row"> 89 {flower_list_logic} 90 </div> 91 </div> 92 </section> 93 </> 94 } 95 } 96 97 fn main() { 98 yew::start_app::<App>(); 99 }
The snippet above does the following:
- Imports the required dependencies
- Line 11 - 12: Creates a
flowers
anderror
application state by using theuse_state
hook and specifyingNone
as the initial value. TheUseStateHandle
struct is used to specify the state type, and theOption
enum represents an optional value - Line 16 - 17: Creates a copy of the states for safe use within the current scope
- Line 20 - 27: Construct a request
url
by adding the API Key, Project ID, and Slug - Line 29 - 49: Uses the
use_effect_with_deps
hook to perform a side effect of fetching data from the Uniform Platform API asynchronously with thewasm_bindgen_futures
andreqwest
'sClient
struct. We also use thematch
control flow to match JSON response returned and updates the states accordingly - Line 51 - 74: Creates a
flower_list_logic
variable to abstract our application logic by using thematch
control flow to match patterns by doing the following:- Maps through the list of
flowers
and pass the individualflower
to theFlowerCard
component when the API returns appropriate data - Uses the
Message
andLoader
components to match error and loading state, respectively
- Maps through the list of
- Line 76 - 94: Updates the markup with the
Header
component andflower_list_logic
abstraction
With that done, we can start a development server using the command below:
1trunk serve --open
Conclusion
This post discussed how to compose a product delivery page experience with Uniform and Rust.
These resources might be helpful: