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.

avatar

Demola Malomo

Mar 05 2023

9 min read

avatar

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:

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.

Application layout

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

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.

Cloudinary console and upload button for other formats of upload

select web address and enter url

After uploading the image, we will see it displayed on the console.

copy url

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.

Create API key Input details Set permissions

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.

API Key and project ID

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.

Click on the Project Add Integration

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.

Select BigCommerce Add Integration Add Cloudname & API Key

We can get our Cloud Name and API Key from our Cloudinary dashboard.

Cloudinary Details

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.

Create Component

Parameter NameHelp TextTyperequired
nameproduct nametextYES
imgproduct imageCloudinary

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.

component name component properties added properties

Then click on the Save and Close button.

save and close

Following the same approach, we need to create a body_component and add the properties shown below:

Parameter NameHelp TextTyperequired
titletitletextYES
descriptiondescriptiontextYES

component name added properties

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.

Product 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.

Product Page component

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.

create slots

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.

create 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.

create 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.

select flower_component add data add data

We need to repeat the steps above to add the remaining flower_component data. Then click on Save and Publish option.

Hover on the item and click on the plus icon to add component Save and publish changes

Finally, we need to click on the Publish button. This makes our composition available to third-party applications.

publish composition

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>

index file

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.

Modules

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.

API response from Uniform

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 with text and css_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 a flower 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 and error application state by using the use_state hook and specifying None as the initial value. The UseStateHandle struct is used to specify the state type, and the Option 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 the wasm_bindgen_futures and reqwest's Client struct. We also use the match 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 the match control flow to match patterns by doing the following:
    • Maps through the list of flowers and pass the individual flower to the FlowerCard component when the API returns appropriate data
    • Uses the Message and Loader components to match error and loading state, respectively
  • Line 76 - 94: Updates the markup with the Header component and flower_list_logic abstraction

With that done, we can start a development server using the command below:

1trunk serve --open

Working app

Conclusion

This post discussed how to compose a product delivery page experience with Uniform and Rust.

These resources might be helpful:

Related posts