<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bolaji Ayodeji's Blog]]></title><description><![CDATA[Software Engineer, Teacher, and Developer Advocate.]]></description><link>https://blog.bolajiayodeji.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1767850452436/4456936f-cba2-49f5-8fa3-f79520372fab.png</url><title>Bolaji Ayodeji&apos;s Blog</title><link>https://blog.bolajiayodeji.com</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 13 May 2026 15:29:08 GMT</lastBuildDate><atom:link href="https://blog.bolajiayodeji.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Using GraphQL for Building Server Driven UIs]]></title><description><![CDATA[I wrote this article in December 2023 for an interview screening task and thought to share it today while clearing my drafts for some new content prep, just in case it's still helpful to someone out t]]></description><link>https://blog.bolajiayodeji.com/using-graphql-for-building-server-driven-uis</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/using-graphql-for-building-server-driven-uis</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[UI]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Fri, 20 Feb 2026 22:04:09 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/a0a62718-ccfd-43fd-a03c-acf801c65981.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>I wrote this article in December 2023 for an interview screening task and thought to share it today while clearing my drafts for some new content prep, just in case it's still helpful to someone out there. The goal was to write a technical article on using GraphQL to build server-driven UIs. I spent quite some time researching the concept, refreshing my very little GraphQL experience and using my general REST API knowledge to pull it off. By the way, when I submitted this task, the reviewer was surprised I claimed to have had a little GraphQL experience and praised the piece for its depth and clarity —the effort put into this was worth it at that point :). All of this was done under 24 hours, by the way, per the instructions.</p>
</blockquote>
<hr />
<p>In conventional client-server architectures, the client application typically calls the shots when it comes to the structure and behaviour of the user interface. However, rising specific business needs have led to a new architecture called Server-Driven UIs (SDUI). In this powerful approach, the server shapes the UI and manages the UI logic and rendering strategy. This enables the client application to become lighter and focus purely on displaying the content dished out by the server. Recently, more mobile-focused companies like Airbnb and Meta have embraced the Server-Driven UI approach, significantly improving their overall engineering architecture and user experience. In this article, you'll learn about the server-driven approach to building user interfaces, why/when you might want to consider it, how some notable companies have implemented it, and how you can start implementing yours using GraphQL.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/aeb18ca5-4e3d-4dc0-810d-ef4635b9013e.jpg" alt="Photo by UX Indonesia on Unsplash." style="display:block;margin:0 auto" />

<p><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">Photo by </mark></em> <a href="https://unsplash.com/@uxindo?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">UX Indonesia</mark></em></a> <em><mark class="bg-yellow-200 dark:bg-yellow-500/30">on </mark></em> <a href="https://unsplash.com/photos/person-writing-on-white-paper-qC2n6RQU4Vw?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">Unsplash</mark></em></a><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">.</mark></em></p>
<h2>What Exactly Are Server Driven UIs?</h2>
<p>A large percentage of software applications (specifically mobile) today require some graphical user interface with which end consumers interact with the application. The experience each user has (usually referred to as UX—User Experience) while interacting with the application in their respective geographical locations determines whether they will be satisfied. This involves all mundane and complex tasks, like how fast specific data is rendered to the user upon a button click or screen swipe, to how quickly they get feedback from the application when they make manual requests by entering some text in an input field. This phenomenon makes it essential for engineering teams to invest in delivering the perfect user experience. If your software product falls under categories like ecommerce or payment services, where every interaction impacts revenue, it gets even more complex. As is commonly said in ecommerce industry, “<em>failure to satisfy users will result in unhappy consumers, influencing conversions, sales, and revenue</em>”—the trio that business owners hate to be affected. This philosophy drives the technological advancements in the ecommerce industry, leading to the rise and wide adoption of concepts like headless, composable commerce, and edge computing. This same philosophy drives the evolution of server-driven UIs, but for a slightly different challenge, which we will discuss soon.</p>
<p>While the resulting experience of rendering data "seems easy" from the user's perspective, it is usually a lot of work for engineering teams at companies that render data to billions of users every second of the day. This involves a coordinated and distributed amount of work on both the client-side (frontend) and server-side (backend) as summarized in the illustration below.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/8e6c6699-d0e5-4ac3-93e6-4a28b46ce64a.jpg" alt="" style="display:block;margin:0 auto" />

<h3>Non-Server Driven UIs</h3>
<p>To explain server-driven UIs, let’s first talk about what the architecture looks like <strong>without</strong> server-driven UIs. A typical application over the HTTP protocol follows a client-server architecture, where data is exchanged between the frontend (client) and the backend (server) via an application programming interface (API). Based on the query requests, the backend retrieves and inserts data into the database and returns the results to the frontend. The frontend then receives the data from the backend in a structured schema and determines where and how to render it to the user interface, based on the UX goals for each UI view (feature). This is how most classic applications function, and it is the common practice today.</p>
<p>Let’s consider a hypothetical MVP ecommerce mobile application that returns the UI components on the home view screen for listing different types of products as described in the list and image below. Each product listing section returns a different type of design and styling for the product(s).</p>
<ol>
<li><p><strong>Carousel block</strong>: for highlighting top products (either based on demand or sales data).</p>
</li>
<li><p><strong>Grid block</strong>: for in-season products (e.g., Christmas, Winter, Summer, etc.).</p>
</li>
<li><p><strong>Checkout block</strong>: quick checkout for a randomly selected discounted product (promotions).</p>
</li>
</ol>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/a027f9b6-f537-4276-bee4-137f050c43e3.jpg" alt="" style="display:block;margin:0 auto" />

<p>The query will look something like this:</p>
<pre><code class="language-graphql">query homePage {
    brand {
        title
		logo
	}
    products (country: US) {
        id
		name
		description
		slug
		price
		currency
		isTop
		isDiscounted
	    seasonTypes {
            name
            description
        }
    }
}
</code></pre>
<p>And the returned JSON data will look like this:</p>
<pre><code class="language-json">{
  "data": {
    "brand": {
			"title": "Welcome to Tailcall Shop!",
			"logo": "&lt;https://avatars.githubusercontent.com/u/112062857&gt;"
		},
    "products": [
      {
        "id": "1",
        "name": "Product A",
        "description": "This is a description for Product A.",
        "slug": "product-a",
        "price": 29.99,
        "currency": "USD",
        "isTop": true,
        "isDiscounted": false,
        "seasonTypes": [
          {
            "name": "Spring",
            "description": "Fresh arrivals for the spring season"
          },
          {
            "name": "Winter",
            "description": "Stay warm with our winter collection"
          },
          {
            "name": "All Seasons",
            "description": "Suitable for all seasons"
          }
        ]
      },
      {
        "id": "2",
        "name": "Product B",
        "description": "Description for Product B.",
        "slug": "product-b",
        "price": 39.99,
        "currency": "USD",
        "isTop": false,
        "isDiscounted": true,
        "seasonTypes": [
          {
            "name": "Christmas",
            "description": "Cool christmas picks"
          },
          {
            "name": "All Seasons",
            "description": "Versatile for all seasons"
          }
        ]
      },
      {
        "id": "3",
        "name": "Product C",
        "description": "Introducing Product C with a unique description.",
        "slug": "product-c",
        "price": 49.99,
        "currency": "USD",
        "isTop": true,
        "isDiscounted": true
      }
    ]
  }
}
</code></pre>
<p>When the frontend receives this JSON data, it’s responsible for consuming the data and rendering it to the user interface. Depending on the sequence of the returned data, the frontend renders the data in the different category blocks from top to bottom using different pre-defined UI components and logic. In addition, based on which <code>seasonTypes</code> the product is associated with, the frontend logic will also sort the product displayed in the Grid Block (knowing that in a real scenario, the number of products could be up to <code>Infinity</code> theoretically).</p>
<p>Now, if you want to reorder the sequence of the category blocks or add new category blocks (which will require adding new UI components), you will need to make some changes to the UI logic (code) on the client-side. This sounds good and should be easily achievable. However, since this is a cross-platform mobile application, code updates will not get to the end user immediately, just like web apps work, where you can deploy a new release and use different rendering strategies to update the entire web application upon a new user request. Each user’s device hosts the software code of the mobile application. Also, the latest version of the app needs to be submitted to the app store (most often both Android and iOS), and after a successful review and approval, the user must update the application manually if they don’t have “auto-update” turned on. There will be many repeated processes when you need to make more changes over time! Plus, users often don’t update their applications immediately after you ship a new version, which makes them lose out on new features, and you lose out on testing feedback. In addition, there might be compatibility issues with breaking changes, where the latest code state no longer matches the previous client. All of these signal the need for a different approach; ergo, server-driven UIs.</p>
<h3>Server Driven UIs</h3>
<p>In this case, and for the same query described above, the returned JSON data will look like this:</p>
<pre><code class="language-json">{
  "data": {
    "brand": {
			"title": "Welcome to Tailcall Shop!",
			"logo": "&lt;https://avatars.githubusercontent.com/u/112062857&gt;"
		},
    "products": [
      {
		"componentType": "gridBlock",
        "id": "1",
        "name": "Product A",
        "description": "This is a description for Product A.",
        "slug": "product-a",
        "price": 29.99,
        "currency": "USD",
        "isDiscounted": false,
        "seasonTypes": [
          {
            "name": "Spring",
            "description": "Fresh arrivals for the spring season"
          },
          {
            "name": "Winter",
            "description": "Stay warm with our winter collection"
          },
          {
            "name": "All Seasons",
            "description": "Suitable for all seasons"
          }
        ]
      },
      {
		"componentType": "carouselBlock",
        "id": "2",
        "name": "Product B",
        "description": "Description for Product B.",
        "slug": "product-b",
        "price": 39.99,
        "currency": "USD",
        "isDiscounted": true,
        "seasonTypes": [
          {
            "name": "Christmas",
            "description": "Cool christmas picks"
          },
          {
            "name": "All Seasons",
            "description": "Versatile for all seasons"
          }
        ]
      },
      {
	    "componentType": "checkoutBlock",
        "id": "3",
        "name": "Product C",
        "description": "Introducing Product C with a unique description.",
        "slug": "product-c",
        "price": 49.99,
        "currency": "USD",
        "isDiscounted": true
      }
    ]
  }
}
</code></pre>
<p>The UI will now read and render the data (the UI Schema) without further processing or delegation. The supplied JSON data already stipulates how and where to present each product data to the UI using the <code>componentType</code> property. The backend now includes UI components for the screen alongside the product information (UI components' layout view, appearance shape, content, etc.), and the client already has the agnostic templating logic to add styling to the displayed visuals. In the above MVP example, product A will be rendered first in the Grid Block, followed by product B in the Carousel Block, as shown in the image below.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/ca3fe969-8018-4fad-9ba9-3255a327f197.jpg" alt="" style="display:block;margin:0 auto" />

<p>The server is now facilitating the UI change. It simultaneously updates the JSON structure and UI with no additional UI logic change and app re-deployment. This approach reduces the client-side logic and leads to consistency and better versioning across all client platforms (including web, iOS, Android, etc.). This architecture becomes super helpful when the UI of your application should change multiple times based on different types of users (e.g., an ecommerce app for multiple merchants where you need to customise the UI differently for each user). You can experiment and iterate faster without doing an entire release cycle.</p>
<h2>Some Case Studies</h2>
<p>Although the MVP example described earlier is hypothetical, many notable companies, including <a href="https://www.reddit.com/r/RedditEng/comments/158f8o3/evolving_reddits_feed_architecture">Reddit</a>, <a href="https://shopify.engineering/server-driven-ui-in-shop-app">Shopify</a>, <a href="https://medium.com/airbnb-engineering/a-deep-dive-into-airbnbs-server-driven-ui-system-842244c5f5">Airbnb</a>, <a href="https://youtu.be/aBu0FIoG36E">Meta</a>, <a href="https://withpersona.com/blog/designing-app-builder-server-driven-ui">Persona</a>, and <a href="https://eng.lyft.com/the-journey-to-server-driven-ui-at-lyft-bikes-and-scooters-c19264a0378e">Lyft</a>, already use this architecture. For example, Reddit revamped its feed architecture across Android and iOS some months ago. According to an iOS engineer at Reddit, the implementation of the previous feed presented several challenges (~<a href="https://www.reddit.com/r/RedditEng/comments/158f8o3/evolving_reddits_feed_architecture">source</a>):</p>
<blockquote>
<p>“Last year our feeds were pretty slow. You’d start up the app, and you’d have to wait too long before getting content to show up on your screen. Equally as bad for us, internally, our feeds code had grown into something of a maintenance nightmare. The current codebase was started around 2017 when the company was considerably smaller than it is today. Many engineers and features have passed through the 6-year-old codebase with minimal architectural oversight. Increasingly, it’s been a challenge for us to iterate quickly as we try new product features in this space.”</p>
</blockquote>
<p>The resulting implementation featured each post unit, represented by a generic Group object with an array of Cell objects, as seen in the image below. With this, they describe anything the Feeds screen shows as a Group (e.g., the Announcement Card or the Trending Carousel).</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/e99b8aee-db36-4d1c-b205-c1eb291b550e.jpg" alt="Example Schema and UI of Reddit Feeds for their Announcement Items (Source: Reddit Engineering Blog)." style="display:block;margin:0 auto" />

<p><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">Example Schema and UI of Reddit Feeds for their Announcement Items (Source: </mark></em> <a href="https://reddit.com/r/RedditEng"><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">Reddit Engineering Blog</mark></em></a><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">).</mark></em></p>
<p>Another Android Engineer at Airbnb mentions similar challenges (~<a href="https://medium.com/airbnb-engineering/a-deep-dive-into-airbnbs-server-driven-ui-system-842244c5f5">source</a>):</p>
<blockquote>
<p>“To show our users a listing, we might request listing data from the backend. Upon receiving this listing data, the client transforms that data into UI. This comes with a few issues. First, there’s listing-specific logic built on each client [referring to web, Android, and iOS] to transform and render the listing data. This logic becomes complicated quickly and is inflexible if we make changes to how listings are displayed down the road. Second, each client has to maintain parity with each other. As mentioned, the logic for this screen gets complicated quickly and each client has their own intricacies and specific implementations for handling state, displaying UI, etc. It’s easy for clients to quickly diverge from one another. Finally, mobile has a versioning problem.”</p>
</blockquote>
<p>The resulting implementation features the backend returning the UI types the data should be rendered with, as seen in the image below. With this, they describe anything the Product Listing screen shows as a Section (e.g., the Title Section or the Home Details Section).</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5cde5eacbe5cf91c26f1f12d/9b31c92d-da70-4e6a-be6a-63758defae4e.png" alt="Example Schema and UI of Airbnb’s Property Overview (Source: The Airbnb Tech Blog)." style="display:block;margin:0 auto" />

<p><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">Example Schema and UI of Airbnb’s Property Overview (Source: </mark></em> <a href="https://medium.com/airbnb-engineering"><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">The Airbnb Tech Blog</mark></em></a><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">).</mark></em></p>
<p>Both implementations demonstrate the concept of delegating UI rendering and logic decisions to the server, with the client just rendering visuals.</p>
<h2>Benefits of Server Driven UIs</h2>
<p>Think about content for a minute; it's easy to ensure mutability for content-intensive applications by leveraging a headless content management system (CMS). You will develop the application to accept dynamic data from a content API and ship it to the user's devices. Multiple content managers can then collaboratively create and modify structured data stored in some repository on the cloud. The content API is updated frequently, and the application returns the new data. <a href="https://blog.bolajiayodeji.com/introduction-to-headless-content-management-systems">I previously wrote about this concept,</a> and you can read more about it. This process is similar to server-driven UIs, but instead of just returning real-time data, the server returns real-time UI component sequences and arrangements. I was talking with a friend who expressed their engineering team's concerns about shipping their enterprise software on a point-of-sale (POS) payments machine, which will power different payment providers (see them like merchants for the end users—the hardware owners). A consumer will purchase each machine with the pre-installed software, which can be updated later, though tedious (especially for less digitally savvy users). Updating the software each time they want to add new UI logic for the newly added providers (merchants) on the hardware is even more complex than typical smartphones, making it a good case for Server-Driven UIs. If implemented successfully, they can ship the hardware with the first version of the software and incrementally update the software’s UI logic from the server as they deem fit.</p>
<p>In summary, some benefits of Server Driven UIs include:</p>
<ul>
<li><p><strong>Better Versioning</strong>: There won't be a need for multiple releases for UI logic changes.</p>
</li>
<li><p><strong>Reduced Bundle Size</strong>: Since no code is added to the frontend logic when the developers need to make UI customisation changes, the app size remains lightweight as shipped. This is essential for software shipped on low-end devices with limited computing power.</p>
</li>
<li><p><strong>Faster Experimentation and Iteration</strong>: Now, engineering teams can experiment with and test new ideas (whether driven by creativity, UX research, or user feedback) faster across all supported platforms without releasing new versions.</p>
</li>
<li><p><strong>Efficient A/B Testing</strong>: Due to the advantages described above, engineering teams can now compare two versions of the UI logic to determine which performs better and faster than they would without SDUI. Now, the dataset would be more extensive since all inactive and active users with the application installed will receive the updates.</p>
</li>
<li><p><strong>Personalised User Experiences</strong>: It becomes easier to adjust the UI rendering based on user demands, user behaviour, preferences, or results from the A/B testing phases.</p>
</li>
<li><p><strong>Easier</strong> <strong>Migration</strong>: It's easier to adopt new frontend frameworks because there's less business logic in them. With the UI shape returned from the server, you can experiment and innovate on the frontend much faster with minimal business impact.</p>
</li>
</ul>
<p>Companies that require both native mobile and web application development may benefit the most from SDUI. This can be software running on a smartphone or an embedded system (Arduino, Raspberry Pi, point-of-sale payment machines, etc.). Generally, server-side UIs can be applied in the following software industries:</p>
<ul>
<li><p><strong>Social Media</strong>: dynamically update feeds and notifications across different devices.</p>
</li>
<li><p><strong>Ecommerce Applications</strong>: dynamically update product listings, promotions, and user interfaces without requiring users to update their applications.</p>
</li>
<li><p><strong>Content Management Systems (CMS)</strong>: dynamically manage and deliver content to different clients, ensuring a consistent user experience across web and mobile interfaces.</p>
</li>
<li><p><strong>Multiplayer Gaming Applications</strong>: dynamically update game interfaces, dashboards, leaderboards, and in-game feeds and notifications.</p>
</li>
<li><p><strong>Enterprise Software</strong>: dynamically manage and update dashboards, reports, and other UI components used by customers and consumers across different devices.</p>
</li>
<li><p><strong>Real-Time Collaboration Tools</strong>: synchronise changes and maintain a consistent user interface for messaging or collaborative document editing software.</p>
</li>
<li><p><strong>Medical Applications</strong>: dynamically update patient records, appointment schedules, transactions, and other relevant information for medical professionals in real-time.</p>
</li>
<li><p><strong>Internet of Things (IoT)</strong>: dynamically control and update the user interfaces of software running on embedded systems.</p>
</li>
<li><p>Et cetera.</p>
</li>
</ul>
<p>Various factors, including the necessity for dynamic changes, platform consistency, and the application's nature, determine the suitability of server-driven UIs. While not suitable for all scenarios, server-driven UIs provide advantages in certain situations.</p>
<h2>Using GraphQL to Build Server Driven UIs</h2>
<p>SDUI is a <a href="https://refactoring.guru/design-patterns">software design pattern</a> you can implement in various ways, including with GraphQL. GraphQL is more suited for something like this compared to its counterpart since it's a query language that offers a better development experience. With a single query to the GraphQL server, you can get multiple UI components (product information, in our case) without multiple API round-trips. Facebook <a href="https://engineering.fb.com/2015/09/14/core-infra/graphql-a-data-query-language">started using GraphQL</a> in 2012 for their native mobile applications, even before announcing it at the React.js Conference in 2015. Although GraphQL seems more known for web applications recently, Facebook's use since its inception shows that the technology is well-suited for mobile application development.</p>
<p>Your schema and design must correlate when building a GraphQL server using the server-driven UI approach. The backend must leverage the existing UI component schemas to build the new view components or screen structure. The frontend UI should update if the server introduces a new design component or structural adjustment. This approach will require focusing on:</p>
<ul>
<li><p>A well-defined UI design system and collection of components (UI elements).</p>
</li>
<li><p>A template processing layer that will receive and process customisation requests.</p>
</li>
<li><p>A well-defined GraphQL Schema.</p>
</li>
<li><p>Other required orchestration layers.</p>
</li>
</ul>
<p>Whatever method you choose to implement SDUI with GraphQL, you should observe the following fundamental principles:</p>
<ol>
<li><p>Start with UI design. Create a design system using patterns like Atomic Design and follow the <a href="https://www.apollographql.com/docs/technotes/TN0027-demand-oriented-schema-design/">demand-oriented schema</a> approach.</p>
</li>
<li><p>Return product information as UI elements (presentation information on UI components' layout view, appearance, content, etc.) and not domain data (core business logic).</p>
</li>
<li><p>Implement the design and API using GraphQL types. You can follow <a href="https://www.apollographql.com/tutorials/schema-design-best-practices">Apollo's recommended approach</a> to schema design, which includes common strategies and best practices.</p>
</li>
<li><p>Avoid including styling values (like font size, colours, etc.) in the server. UI component styling should be pre-built on the client to meet the server's requirements.</p>
</li>
<li><p>Adopt SDUI incrementally if your product is already in production. Start with minor features, reduce business logic in the existing client code, gather feedback, and slowly work your way up.</p>
</li>
</ol>
<p>The schema of our hypothetical MVP described earlier will look like this without a server-driven UI approach in use:</p>
<pre><code class="language-graphql">type Brand {
	title: String;
	logo: Image;
}

type SeasonTypes {
	name: String;
	description: String;
    count: Int;
}

type Product @key(fields: "country") {
    componentType: String;
    id: String;
    name: String
    description: String;
    slug: String;
    price: String;
    currency: String;
	isTop: Boolean;
	isDiscounted: Boolean;
	seasonTypes?: [SeasonTypes];
    count: Int;
}
</code></pre>
<p>If we then switch to server-driven UIs, we will have something like this:</p>
<pre><code class="language-graphql">type Brand {
	title: String;
	logo: Image;
}

type SeasonTypes {
	name: String;
	description: String;
    count: Int;
}

type Product @key(fields: "country") {
  componentType: String;
  id: String;
  name: String
  description: String;
  slug: String;
  price: String;
  currency: String;
  isTop: Boolean;
  isDiscounted: Boolean;
  seasonTypes?: [SeasonTypes];
  count: Int;
}

# ...

type BlockCarousel {
	elements: [...];
	product: [Product];
}

type BlockGrid {
	elements: [...];
	product: [Product];
}

type BlockCheckout {
	elements: [...];
	product: [Product];
}

ProductViews = BlockCarousel | CarouselGrid | CheckoutGrid;

type HomeScreen {
	blocks: ProductViews;
}
</code></pre>
<p>A client-side query for <code>HomeScreen.blocks</code> will return all the product data and different product listing views. The client can then render multiple specific experiences depending on the query result (user’s customisation request). This is a quick pseudocode overview of how implementing Server-Driven UI will look in GraphQL. For further learning, you can check out <a href="https://github.com/csmets/Server-Driven-UI">this project on GitHub</a>, which you can use as a resource or playground to learn SDUI. It covers an end-to-end implementation of how SDUI works and follows the SDUI paradigm by ensuring the clients reference the same data endpoint that provides the UI schematics on how the client should render it. It includes a <code>graphql-server</code>, <code>template-server</code>, <code>style-tokens</code>, <code>web-app</code>, and <code>mobile-app</code> modules alongside extensive documentation that you can read.</p>
<h2>Conclusion</h2>
<p>Server-Driven User Interfaces (SDUIs) provide a dynamic, efficient, and adaptable solution to maintaining and updating the user interfaces of mobile apps. By transferring UI rendering operations from the client to the server, updates and modifications can be implemented fast without requiring users to download a new version of the app. This method results in a more seamless developer experience and a customizable user experience.</p>
<p>You should also note that SDUI is an expensive undertaking that requires significant engineering effort, especially if you have to migrate from an existing architecture. It comes with downsides (like the application becoming more online-first-focused and some offline capabilities needing to be reworked). However, there are benefits if your use case fits it so well that the business value of the upsides outweighs the downsides. As with any technology, evaluating your requirements and understanding the benefits and potential challenges is crucial before implementing server-driven UIs in your application. Hopefully, this article has been insightful, and you can take it further and do advanced research and experimentation.</p>
<p>Thanks for reading this far; cheers! 💙</p>
]]></content:encoded></item><item><title><![CDATA[Career Update: Joining the Digital Public Goods Alliance]]></title><description><![CDATA[Hey! I’m super excited to announce that I have joined the Secretariat Team at the Digital Public Goods Alliance. In this role, I leverage my passion for open source and technical background to assess DPG applications, provide technical support and ad...]]></description><link>https://blog.bolajiayodeji.com/career-update-joining-the-digital-public-goods-alliance</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/career-update-joining-the-digital-public-goods-alliance</guid><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Fri, 18 Oct 2024 13:37:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729182706869/57d4d3af-a1ea-40e9-87ae-169d7a044d3a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey! I’m super excited to announce that I have joined the <a target="_blank" href="https://digitalpublicgoods.net/digital-public-goods-alliance-secretariat?ref=bolajiayodeji">Secretariat Team</a> at the <a target="_blank" href="https://digitalpublicgoods.net?ref=bolajiayodeji">Digital Public Goods Alliance</a>. In this role, I leverage my passion for open source and technical background to assess DPG applications, provide technical support and advocate for open source projects seeking recognition as digital public goods, educate stakeholders about the DPG Standard and DPG application process, contribute to strategic planning efforts to improve the DPG application processes and build/manage relationships with DPG product owners and the entire DPG community.</p>
<hr />
<p>The Digital Public Goods Alliance is a UN-endorsed, multi-stakeholder initiative that accelerates the attainment of the sustainable development goals by facilitating the discovery, development, use of, and investment in <a target="_blank" href="https://digitalpublicgoods.net/digital-public-goods?ref=bolajiayodeji">digital public goods</a>. According to the <a target="_blank" href="https://www.un.org/en/content/digital-cooperation-roadmap/assets/pdf/Roadmap_for_Digital_Cooperation_EN.pdf">UN Secretary General’s Roadmap for Digital Cooperation</a>, digital public goods are open source software, open standards, open data, open AI systems, and open content collections that adhere to privacy and other applicable best practices, do no harm, and are of high relevance for attainment of the United Nations 2030 <a target="_blank" href="https://sdgs.un.org/goals">SDGs</a>. This definition is operationalised through the <a target="_blank" href="https://digitalpublicgoods.net/standard?ref=bolajiayodeji">DPG Standard</a>, and you can find all DPGs being developed and used across the world in the <a target="_blank" href="https://digitalpublicgoods.net/registry?ref=bolajiayodeji">DPG Registry</a>.</p>
<p><img src="https://digitalpublicgoods.s3.ca-central-1.amazonaws.com/wp-content/uploads/2021/11/25160716/Screenshot-2021-11-25-at-11-05-25-DPGA-full-slide-template_June-2021-1.png" alt="Definition of digital public goods." /></p>
<p><img src="https://digitalpublicgoods.s3.ca-central-1.amazonaws.com/wp-content/uploads/2023/06/23195609/Screenshot-2023-06-23-at-15-55-36-DPGA-full-slide-template_June-2021.png" alt="All the DPG categories with supporting icons." /></p>
<blockquote>
<p>Images source: <a target="_blank" href="https://digitalpublicgoods.net/digital-public-goods/">https://digitalpublicgoods.net/digital-public-goods</a>.</p>
</blockquote>
<p>Digital public goods are important to building a more equitable world, solving global challenges, and attaining the sustainable development goals. Open Software like <a target="_blank" href="https://app.digitalpublicgoods.net/r/formsg">FormSG</a> or <a target="_blank" href="https://app.digitalpublicgoods.net/r/opencrvs">OpenCRVS</a> enables government agencies to better serve their citizens, and others like <a target="_blank" href="https://app.digitalpublicgoods.net/r/openfn-integration-toolkit">OpenFn</a> enable other digital solutions to automate processes and exchange data. Open Datasets like <a target="_blank" href="https://app.digitalpublicgoods.net/r/agrovoc-multilingual-thesaurus">AGROVOC Multilingual Thesaurus</a> or <a target="_blank" href="https://app.digitalpublicgoods.net/r/govdirectory">Govdirectory</a> enable different sectors to make better decisions. Likewise, Open Content Collections like <a target="_blank" href="https://app.digitalpublicgoods.net/r/creative-commons-legal-tools">Creative Commons Legal Tools</a> or <a target="_blank" href="https://app.digitalpublicgoods.net/r/data-to-policy-navigator">Data to Policy Navigator</a>.</p>
<p>All these solutions are designed in ways different from all the libraries we <code>npm</code> or <code>pip</code> install. This is part of what makes DPGs different from proprietary or even many typical open source projects. We need solutions many others can adapt to their unique contextual needs, and DPGs are designed for this purpose. They are designed to work in different settings and take into consideration the geographical differences of where they will be used. They are designed to be interoperable, so other solutions can exchange data and continue extending the value provided. They are designed following open standards and best practices. They are designed to anticipate, prevent, and do no harm by design, thereby protecting their users. They are designed to ensure the privacy, security, and integrity of PII data. They can be made part of a country's <a target="_blank" href="https://digitalpublicgoods.net/blog/unpacking-concepts-definitions-digital-public-infrastructure-building-blocks-and-their-relation-to-digital-public-goods?ref=bolajiayodeji">digital public infrastructure</a> (DPI) and even a lot more!</p>
<p>To learn more about applying to become a digital public good, you can read this <a target="_blank" href="https://digitalpublicgoods.net/submission-guide?ref=bolajiayodeji">submission guide</a>, take this <a target="_blank" href="https://digitalpublicgoods.net/eligibility?ref=bolajiayodeji">eligibility test</a>, or read the <a target="_blank" href="https://git.new/dpg-wiki">DPG Wiki</a>.</p>
<hr />
<p>This is a new space for me, and the past two months have been an exciting ride working with amazing colleagues, with so much learning and work already being done. Please feel free to <a target="_blank" href="https://calendar.app.google/Q1sSoYi8GMNniYBt9">book a call</a> or <a target="_blank" href="https://www.linkedin.com/in/bolajiayodeji/">write to me</a> to chat if you’d like to:</p>
<ul>
<li><p>Learn more about the DPGA or Digital Public Goods.</p>
</li>
<li><p>Learn more about how your open source project can become a DPG.</p>
</li>
<li><p>Learn more about how certain DPGs can fit into your country’s DPI or how you can support or implement certain DPGs.</p>
</li>
<li><p>Chat about anything else!</p>
</li>
</ul>
<p>I see <a target="_blank" href="https://digitalpublicgoods.net/digital-public-goods-alliance-strategy-2023-2028?ref=bolajiayodeji">how</a> the Digital Public Good Alliance is facilitating the discovery and deployment of open source technologies, bringing together countries and like-minded organizations to create a thriving global ecosystem for digital public goods. This prompted me to develop an interest in the great work they’ve been doing, and I'm elated to join and help in different capacities to drive the goals of the DPGA towards unlocking the potential of open source technologies for a more equitable world.</p>
<blockquote>
<p>"If we create a product we like that never ends up in people’s hands, who are we working for beyond ourselves? How can we fix that? How do we know we’re making the world a better place and not just creating software for people with free time and a spare laptop?..." —Thibault Martin.</p>
</blockquote>
<p>Cheers to making the world a better place. Onwards! 💙</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/lzQR1K-u0ag?si=RdnhQk9khZnlWpS6">https://youtu.be/lzQR1K-u0ag?si=RdnhQk9khZnlWpS6</a></div>
]]></content:encoded></item><item><title><![CDATA[How to Build Design Editing Apps using Nextjs, Clerk, and IMGLY’s CE.SDK Engine]]></title><description><![CDATA[Creative designs have become more important than ever in the software ecosystem today with many industries and end-consumers having several use cases that require them to offer design editing solutions to either designers or end-consumers. Every busi...]]></description><link>https://blog.bolajiayodeji.com/how-to-build-design-editing-apps-using-nextjs-clerk-and-imglys-cesdk-engine</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-build-design-editing-apps-using-nextjs-clerk-and-imglys-cesdk-engine</guid><category><![CDATA[Next.js]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[sdk]]></category><category><![CDATA[Design]]></category><category><![CDATA[Clerk.dev]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Wed, 11 Sep 2024 14:58:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726066517452/8f221380-a620-43fd-8899-0a4d571259c8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Creative designs have become more important than ever in the software ecosystem today with many industries and end-consumers having several use cases that require them to offer design editing solutions to either designers or end-consumers. Every business creates and uses different digital and print designs for marketing or day-to-day customer service purposes. They do this mostly to allow customers to personalize and customize certain services before they make purchases in the case of e-commerce. With the rise of customer demands, software builders now have to develop editing user interfaces to power different kinds of use cases for their users across the world. Sounds like a lot of work, right? Maybe it is. But could it get any better? Yes!</p>
<p>We’re in the age of composability, a term that describes the ability of engineering teams to compose different optimal tools to build the perfect experience for their users. With that in mind, I spent some time experimenting on how to build media design editing apps and I found a tool called the CreativeEditor SDK Engine that allows you to build fully customizable design editors with fewer lines of code. In this tutorial, I will show you how to use the CE.SDK Engine in a Nextjs web application to build a simple image editor and even a Canva-like editor! As a bonus, I’d also show you how you can build an image background removal app (using the <a target="_blank" href="https://github.com/imgly/background-removal-js">background-removal.js</a> library) and integrate with Clerk Auth.</p>
<blockquote>
<p>If you want to see the final code integration, you can <a target="_blank" href="https://github.com/BolajiAyodeji/attraktives-headshot">head to this repository</a> now, but you might want to read along to learn one or two things, especially if you’re new to this :).</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726048202291/1dd05084-52ff-45fd-8c51-fbb3c861047e.png" alt /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the best out of this tutorial, you need to have the following:</p>
<ul>
<li><p>Nodejs and NPM installed on your computer.</p>
</li>
<li><p>An IDE and terminal installed on your computer.</p>
</li>
<li><p>A web browser installed on your computer.</p>
</li>
<li><p>A stable connection to the internet.</p>
</li>
<li><p>Some prior knowledge of the JavaScript programming language.</p>
</li>
<li><p>Some prior knowledge of the Reactjs JavaScript framework.</p>
</li>
<li><p>A smile on your face :).</p>
</li>
</ul>
<h2 id="heading-the-creativeeditor-engine">The CreativeEditor Engine</h2>
<p>IMG․LY has built a creative engine that allows developers and businesses to add video and photo editing features to their applications. The engine is used to build different SDKs, that can power photo, video, design editing, and creative automation features for unlimited distributed end-users. They currently provide a <a target="_blank" href="https://img.ly/products/creative-sdk?ref=bolajiayodeji">CreativeEditor SDK</a> (CE.SDK), <a target="_blank" href="https://img.ly/products/photo-sdk?ref=bolajiayodeji">PhotoEditor SDK</a>, <a target="_blank" href="https://img.ly/products/video-sdk?ref=bolajiayodeji">VideoEditor SDK</a>, and <a target="_blank" href="https://img.ly/products/camera-sdk?ref=bolajiayodeji">Camera SDK</a> on different platforms including, Web, Server, iOS, and Android. This means you can use these SDKs with a wide range of frameworks like Vanilla JS, Reactjs, Nextjs, Vuejs, Angular, Svelte, Electron, Flutter, ReactNative, Ionic, etc. All these SDKs can be used for a wide range of use cases and industries like:</p>
<ul>
<li><p>Personalized printing.</p>
</li>
<li><p>Ecommerce product customization.</p>
</li>
<li><p>Web2Print.</p>
</li>
<li><p>Automated design templates.</p>
</li>
<li><p>Photo editors.</p>
</li>
<li><p>Video editors.</p>
</li>
<li><p>Design editors (like Canva).</p>
</li>
<li><p>Website editors (like Wix).</p>
</li>
</ul>
<p>The beautiful part is that you can use the SDK to programmatically automate different creative design tasks, workflows, and ideas you have in mind, making your business even more efficient. From code, to design, to export, to print, to ♾️! Fortune 100 businesses and other startups like HP, ZARA, Shopify, Flickr, etc. are already using the SDKs to power hundreds of applications.</p>
<h2 id="heading-building-a-demo-image-background-editing-app">Building a Demo Image Background Editing App</h2>
<p>To show you the basics of CE.SDK Engine, let’s build a simple Nextjs web application that enables users to craft an attractive profile picture for social media platforms. With this application, a user can upload a professional headshot image (or maybe a cute image of their pet 🙃) without a background (usually in PNG format). We will then use the CE.SDK Engine to apply different background options and allow the user to download all the edited variations. In summary, this is what the flow of the application looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726048298772/53ec05cd-29eb-47e3-ae2b-2218d63ce992.png" alt /></p>
<p>Now, let’s get started!</p>
<h3 id="heading-i-install-packages-and-setup-project-structure">[I] Install Packages and Setup Project Structure</h3>
<p>First, use <code>create-next-app</code> to set up a new Nextjs project with TypeScript and App Router like so:</p>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>Next, install all the packages we need:</p>
<pre><code class="lang-bash">npm install @cesdk/engine @cesdk/cesdk-js @imgly/background-removal @clerk/nextjs
</code></pre>
<p>Next, create some extra directories and files, as seen in the file structure below (the ones with an asterisk * beside the name are the new files):</p>
<pre><code class="lang-plaintext">┌── app
    ├── bg-add*
        ├── page.tsx
    ├── bg-remove*
        ├── page.tsx
    ├── components*
        ├── editorCanvas.tsx
        ├── headshotCanvas.tsx
    ├── editor*
        ├── page.tsx
    ├── start*
        ├── page.tsx
    ├── utils*
        ├── grids.ts
    ├── layout.tsx
    ├── page.tsx
┌── public
├── .env.local
...
├── middleware.ts*
├── package.json
└── tsconfig.json
</code></pre>
<h3 id="heading-ii-some-key-terms">[II] Some Key Terms</h3>
<p>Before we proceed further, here are some unique key terms used in the CE.SDK engine which are the fundamental components that make up the engine canvas.</p>
<ul>
<li><p><strong>Canvas</strong>: the HTML element used to draw graphics using JavaScript on the web.</p>
</li>
<li><p><strong>Scene</strong>: the root of the canvas for each instance of the engine accessed with the <a target="_blank" href="https://img.ly/docs/cesdk/engine/api/scene/">Scene API</a>. Generally, a scene will contain multiple pages which will also contain other blocks.</p>
</li>
<li><p><strong>Page</strong>: the parent block of all elements in the scene (usually used to group all blocks/elements together).</p>
</li>
<li><p><strong>Block</strong> (or design block): the main building unit in CE.SDK accessed with the <a target="_blank" href="https://img.ly/docs/cesdk/engine/api/block/">Block API</a> and organized in a hierarchy structure through parent-child relationships inside the scene. Every element is a block in the canvas (e.g., graphics, shapes, images, texts, etc.).</p>
</li>
<li><p><strong>Fill</strong>: an object that defines the contents within a design block (e.g., images, videos, solid colors, gradients, etc.).</p>
</li>
</ul>
<h3 id="heading-iii-setup-the-cesdk-engine">[III] Setup the CE.SDK Engine</h3>
<p>The CreativeEditor SDK Engine (<code>@cesdk/engine</code>) provides different APIs for powering any editing UI. To get started, we need to initialize the engine and pass in the config object. In the <code>/components/headshotCanvas.tsx</code> file you created earlier, add the following code:</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef, useState, ChangeEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> CreativeEngine <span class="hljs-keyword">from</span> <span class="hljs-string">"@cesdk/engine"</span>;

<span class="hljs-keyword">const</span> config = {
  license: process.env.NEXT_PUBLIC_CESDK_LICENSE,
  userId: <span class="hljs-string">"guides-user"</span>,
  baseURL: <span class="hljs-string">"https://cdn.img.ly/packages/imgly/cesdk-engine/1.24.0/assets"</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BgAddPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> cesdk_container = useRef&lt;HTMLDivElement&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> initializeCESDK = <span class="hljs-function">() =&gt;</span> {
    CreativeEngine.init(config).then(<span class="hljs-function">(<span class="hljs-params">engine</span>) =&gt;</span> {
      <span class="hljs-comment">// Append the engine element to the container.</span>
      <span class="hljs-keyword">const</span> container = cesdk_container.current!;
      container.innerHTML = <span class="hljs-string">""</span>;
      container.append(engine.element);

      <span class="hljs-comment">// Create a new scene and page.</span>
      <span class="hljs-keyword">let</span> scene = engine.scene.create();
      <span class="hljs-keyword">const</span> page = engine.block.create(<span class="hljs-string">"page"</span>);
      engine.block.setWidth(page, <span class="hljs-number">500</span>);
      engine.block.setHeight(page, <span class="hljs-number">500</span>);
      engine.block.appendChild(scene, page);
    });
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    initializeCESDK();
  }, []);

  <span class="hljs-keyword">return</span> (
    &lt;div
      ref={cesdk_container}
      style={{ width: <span class="hljs-string">"100vw"</span>, height: <span class="hljs-string">"100vh"</span> }}
      &gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>In the code snippet above, we:</p>
<ul>
<li><p>Setup the <code>config</code> object and add the following parameters:</p>
<ul>
<li><p><code>license</code>: the CE.SDK API key. You can get one and test the platform free for up to 30 days once you sign up using <a target="_blank" href="https://img.ly/forms/free-trial">this form</a>. For testing in development, you can use the <code>mtLT-_GJwMhE7LDnO8KKEma7qSuzWuDxiKuQcxHKmz3fjaXWY2lT3o3Z2VdL5twm</code> license publicly available on the CE.SDK documentation. Only ensure to add that to your <code>.env</code> file using the <code>NEXT_PUBLIC_CESDK_LICENSE</code> name.</p>
</li>
<li><p><code>userID</code>: an optional unique ID tied to your application's user. This is used mainly for tracking user data, specifically to calculate monthly active users.</p>
</li>
<li><p><code>baseURL</code>: link to a public URL where different asset files are stored.</p>
</li>
</ul>
</li>
<li><p>Use the <code>CreativeEngine.init(config)</code> function to start up a new instance of the engine inside an HTML div element with a <code>useRef</code> reference to the element. This element will then contain the engine's <code>&lt;cesdk-canvas/&gt;</code> and will fill out its container.</p>
</li>
</ul>
<p>Next, add the following code in the <code>/bg-add/page.tsx</code> file to render the <code>/components/headshotCanvas.tsx</code> component above in a page (<code>/bg-add</code> in this case):</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> dynamic <span class="hljs-keyword">from</span> <span class="hljs-string">"next/dynamic"</span>;

<span class="hljs-keyword">const</span> CreativeEditorSDKWithNoSSR = dynamic(
  <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">"../components/headshotCanvas"</span>),
  {
    ssr: <span class="hljs-literal">false</span>,
  }
);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> CreativeEditorSDKWithNoSSR;
</code></pre>
<p>We’re doing this to avert errors like <code>ReferenceError: document is not defined</code> which will come up when the page renders with SSR. The CE.SDK Engine is a client-side library that requires various browser features (like <code>document</code>), hence it must be loaded and executed in the browser upon render. With this logic, we will load the library dynamically using <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading#nextdynamic"><code>next/dynamic</code></a> and disable server rendering when the page is loaded in the browser.</p>
<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Tip: <strong>CE.SDK</strong> is an acronym for <strong>CreativeEditor SDK</strong>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The <code>@cesdk/engine</code> package is the<strong> CreativeEditor Engine </strong>or<strong> CreativeEngine </strong>or<strong> CreativeEditor SDK Engine</strong> used in building the CreativeEditor SDK too (you use this to render designs on your own UI). Likewise, the <code>@cesdk/cesdk-js</code> package is the <strong>CreativeEditor SDK</strong> (the customizable Design UI and Studio UI editor you can integrate directly into your application). Quite confusing, right 😃? It took me a while to wrap my head around this and I’m sure you’d get it too :).</div>
</div>

<hr />
<h3 id="heading-iv-add-color-and-image-to-page">[IV] Add Color and Image to Page</h3>
<p>In the same <code>/components/headshotCanvas.tsx</code> file, let’s add more code like so:</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef, useState, ChangeEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> CreativeEngine <span class="hljs-keyword">from</span> <span class="hljs-string">"@cesdk/engine"</span>;

<span class="hljs-keyword">const</span> config = {
  license: process.env.NEXT_PUBLIC_CESDK_LICENSE,
  userId: <span class="hljs-string">"guides-user"</span>,
  baseURL: <span class="hljs-string">"https://cdn.img.ly/packages/imgly/cesdk-engine/1.24.0/assets"</span>,
};

<span class="hljs-comment">// Default image for the page (user can update later).</span>
<span class="hljs-keyword">const</span> defaultImage =
  <span class="hljs-string">"https://github.com/BolajiAyodeji/attraktives-headshot/blob/main/public/headshot.png?raw=true"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BgAddPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> cesdk_container = useRef&lt;HTMLDivElement&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> initializeCESDK = <span class="hljs-function">() =&gt;</span> {
    CreativeEngine.init(config).then(<span class="hljs-function">(<span class="hljs-params">engine</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> container = cesdk_container.current!;
      container.innerHTML = <span class="hljs-string">""</span>;
      container.append(engine.element);

      <span class="hljs-keyword">let</span> scene = engine.scene.create();
      <span class="hljs-keyword">const</span> page = engine.block.create(<span class="hljs-string">"page"</span>);
      engine.block.setWidth(page, <span class="hljs-number">500</span>);
      engine.block.setHeight(page, <span class="hljs-number">500</span>);
      engine.block.appendChild(scene, page);

      <span class="hljs-comment">// Create a graphic block, set the shape/size,</span>
      <span class="hljs-comment">// get the color fill block of the page,</span>
      <span class="hljs-comment">// add a color to the block,</span>
      <span class="hljs-comment">// and add the block to the scene's page.</span>
      <span class="hljs-keyword">const</span> block = engine.block.create(<span class="hljs-string">"graphic"</span>);
      engine.block.setShape(block, engine.block.createShape(<span class="hljs-string">"rect"</span>));
      engine.block.setFill(block, engine.block.createFill(<span class="hljs-string">"color"</span>));
      engine.block.setWidth(block, <span class="hljs-number">500</span>);
      engine.block.setHeight(block, <span class="hljs-number">500</span>);
      <span class="hljs-keyword">const</span> colorFill = engine.block.getFill(page);
        <span class="hljs-comment">// Red RGB color</span>
      engine.block.setColor(colorFill, <span class="hljs-string">"fill/color/value"</span>, {
        r: <span class="hljs-number">1.0</span>,
        g: <span class="hljs-number">0.0</span>,
        b: <span class="hljs-number">0.0</span>,
        a: <span class="hljs-number">1.0</span>
      });
      engine.block.appendChild(page, block);

      <span class="hljs-comment">// Create a block with an image fill</span>
      <span class="hljs-comment">// and add it to the scene's page.</span>
      <span class="hljs-keyword">const</span> imageFill = engine.block.createFill(<span class="hljs-string">"image"</span>);
      engine.block.setString(
        imageFill,
        <span class="hljs-string">"fill/image/imageFileURI"</span>,
        defaultImage
      );
      engine.block.setFill(block, imageFill);
    });
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    initializeCESDK();
  }, []);

  <span class="hljs-keyword">return</span> (
    &lt;div
      ref={cesdk_container}
      style={{ width: <span class="hljs-string">"100vw"</span>, height: <span class="hljs-string">"100vh"</span> }}
      &gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>In the code snippet above, we:</p>
<ul>
<li><p>Create a graphic block, set the shape/size, add a color to the block using the <code>fill/color/value</code> fill, and add the block to the page on the scene. Colors can be specified with either RGB, CMYK, or Spot color formats (I used RGB here).</p>
</li>
<li><p>Create another block using the <code>fill/image/imageFileURI</code> fill and add it to a block.</p>
</li>
</ul>
<p>At this point, you should start seeing the page rendering with a red color on the canvas like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726048693917/9c4b56e1-e530-4b31-b332-e1bbb81daa1c.png" alt /></p>
<h3 id="heading-v-display-multiple-pages-on-the-scene">[V] Display Multiple Pages on the Scene</h3>
<p>Now let’s attempt to display multiple pages in a grid-like format on the same. In the same <code>/components/headshotCanvas.tsx</code> file, now add more code like so:</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> CreativeEngine <span class="hljs-keyword">from</span> <span class="hljs-string">"@cesdk/engine"</span>;
<span class="hljs-keyword">import</span> { grids } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/utils/grids"</span>;

<span class="hljs-keyword">const</span> config = {
  license: process.env.NEXT_PUBLIC_CESDK_LICENSE,
  userId: <span class="hljs-string">"guides-user"</span>,
  baseURL: <span class="hljs-string">"https://cdn.img.ly/packages/imgly/cesdk-engine/1.24.0/assets"</span>,
};

<span class="hljs-keyword">const</span> defaultImage =
  <span class="hljs-string">"https://github.com/BolajiAyodeji/attraktives-headshot/blob/main/public/headshot.png?raw=true"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BgAddPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> cesdk_container = useRef&lt;HTMLDivElement&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> initializeCESDK = <span class="hljs-function">() =&gt;</span> {
    CreativeEngine.init(config).then(<span class="hljs-function">(<span class="hljs-params">engine</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> container = cesdk_container.current!;
      container.innerHTML = <span class="hljs-string">""</span>;
      container.append(engine.element);

      <span class="hljs-keyword">let</span> scene = engine.scene.create();

      <span class="hljs-comment">// Display multiple pages in the scene (as grid)</span>
      <span class="hljs-comment">// using the same properties but different positions and colors.</span>
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt;= <span class="hljs-number">6</span>; i++) {
        <span class="hljs-keyword">const</span> page = engine.block.create(<span class="hljs-string">"page"</span>);
        engine.block.setWidth(page, <span class="hljs-number">500</span>);
        engine.block.setHeight(page, <span class="hljs-number">500</span>);
        engine.block.setPositionX(page, grids[i].x);
        engine.block.setPositionY(page, grids[i].y);
        engine.block.appendChild(scene, page);

        <span class="hljs-keyword">const</span> block = engine.block.create(<span class="hljs-string">"graphic"</span>);
        engine.block.setShape(block, engine.block.createShape(<span class="hljs-string">"rect"</span>));
        engine.block.setFill(block, engine.block.createFill(<span class="hljs-string">"color"</span>));
        engine.block.setWidth(block, <span class="hljs-number">500</span>);
        engine.block.setHeight(block, <span class="hljs-number">500</span>);
        <span class="hljs-keyword">const</span> colorFill = engine.block.getFill(page);
        engine.block.setColor(colorFill, <span class="hljs-string">"fill/color/value"</span>, grids[i].color);
        engine.block.appendChild(page, block);

        <span class="hljs-keyword">const</span> imageFill = engine.block.createFill(<span class="hljs-string">"image"</span>);
        engine.block.setString(
          imageFill,
          <span class="hljs-string">"fill/image/imageFileURI"</span>,
          defaultImage
        );
        engine.block.setFill(block, imageFill);
      }
    });
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    initializeCESDK();
  }, []);

  <span class="hljs-keyword">return</span> (
    &lt;div
      ref={cesdk_container}
      style={{ width: <span class="hljs-string">"100vw"</span>, height: <span class="hljs-string">"100vh"</span> }}
      &gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Also, add the following code in the <code>/utils/grids.ts</code> file:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">interface</span> gridLayout {
  [key: <span class="hljs-built_in">number</span>]: {
    x: <span class="hljs-built_in">number</span>;
    y: <span class="hljs-built_in">number</span>;
    color: { r: <span class="hljs-built_in">number</span>; g: <span class="hljs-built_in">number</span>; b: <span class="hljs-built_in">number</span>; a: <span class="hljs-built_in">number</span> };
  };
}

<span class="hljs-comment">// Red, Blue, Green, Yellow, Pink, and Orange colors (in that order).</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> grids: gridLayout = {
  <span class="hljs-number">1</span>: { x: <span class="hljs-number">-800</span>, y: <span class="hljs-number">-50</span>, color: { r: <span class="hljs-number">1.0</span>, g: <span class="hljs-number">0.0</span>, b: <span class="hljs-number">0.0</span>, a: <span class="hljs-number">1.0</span> } },
  <span class="hljs-number">2</span>: { x: <span class="hljs-number">-250</span>, y: <span class="hljs-number">-50</span>, color: { r: <span class="hljs-number">0.0</span>, g: <span class="hljs-number">0.0</span>, b: <span class="hljs-number">1.0</span>, a: <span class="hljs-number">1.0</span> } },
  <span class="hljs-number">3</span>: { x: <span class="hljs-number">300</span>, y: <span class="hljs-number">-50</span>, color: { r: <span class="hljs-number">0.0</span>, g: <span class="hljs-number">1.0</span>, b: <span class="hljs-number">0.0</span>, a: <span class="hljs-number">1.0</span> } },
  <span class="hljs-number">4</span>: { x: <span class="hljs-number">-800</span>, y: <span class="hljs-number">500</span>, color: { r: <span class="hljs-number">1.0</span>, g: <span class="hljs-number">1.0</span>, b: <span class="hljs-number">0.0</span>, a: <span class="hljs-number">1.0</span> } },
  <span class="hljs-number">5</span>: { x: <span class="hljs-number">-250</span>, y: <span class="hljs-number">500</span>, color: { r: <span class="hljs-number">1.0</span>, g: <span class="hljs-number">0.0</span>, b: <span class="hljs-number">1.0</span>, a: <span class="hljs-number">1.0</span> } },
  <span class="hljs-number">6</span>: { x: <span class="hljs-number">300</span>, y: <span class="hljs-number">500</span>, color: { r: <span class="hljs-number">1.0</span>, g: <span class="hljs-number">0.5</span>, b: <span class="hljs-number">0.0</span>, a: <span class="hljs-number">1.0</span> } },
};
</code></pre>
<p>In the code snippets above, we:</p>
<ul>
<li><p>Use a for loop to create multiple pages using the same properties we used earlier.</p>
</li>
<li><p>Use the <code>setPositionX</code> and <code>setPositionY</code> options to position each page on the scene (coordinate of an element in a document—x and y axis on a graph) so they all appear in a grid sequence. This was a good hack for the grid layout and ideally you might want to display multiple pages horizontally or vertically.</p>
</li>
<li><p>Create a <code>grids</code> object to store the values for the positions and color and dynamically use them inside the loop (<code>grids[i].x</code>, <code>grids[i].y</code>, and <code>grids[i].color</code>) based on the object’s key matching the loop’s index (<code>i</code>).</p>
</li>
</ul>
<p>At this point, your canvas should get polished with a beautiful grid as seen below. NB: the scene will be displayed a little below the default view point on your screen but remember this is a canvas and you can drag the entire scene in any direction you want and reposition.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726048800225/b75957e4-a614-4eda-8f37-267032654189.png" alt /></p>
<h3 id="heading-vi-upload-an-image-locally-to-the-scene">[VI] Upload an Image Locally to the Scene</h3>
<p>Now, let’s add some more logic to allow a user to upload their own image locally. We can easily do this without having to handle any images in the cloud by using the <code>&lt;input type="file&gt;</code> element which will store the accessed files in the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/FileList"><code>FileList</code></a> JavaScript object returned by the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/File"><code>File</code></a> property in the input element. We can then convert the file into a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Blob"><code>Blob</code></a> URL, save it in state, and display it in the canvas using the <code>src</code> attribute of an <code>&lt;img&gt;</code> HTML element.</p>
<p>In the same <code>/components/headshotCanvas.tsx</code> file, now add more code like so:</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef, useState, ChangeEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> CreativeEngine <span class="hljs-keyword">from</span> <span class="hljs-string">"@cesdk/engine"</span>;
<span class="hljs-keyword">import</span> { grids } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/utils/grids"</span>;

<span class="hljs-keyword">const</span> config = {
  license: process.env.NEXT_PUBLIC_CESDK_LICENSE,
  userId: <span class="hljs-string">"guides-user"</span>,
  baseURL: <span class="hljs-string">"https://cdn.img.ly/packages/imgly/cesdk-engine/1.24.0/assets"</span>,
};

<span class="hljs-keyword">const</span> defaultImage =
  <span class="hljs-string">"https://github.com/BolajiAyodeji/attraktives-headshot/blob/main/public/headshot.png?raw=true"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BgAddPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// State to store the Blob URL of the uploaded image.</span>
  <span class="hljs-keyword">const</span> [imagePath, setImagePath] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> cesdk_container = useRef&lt;HTMLDivElement&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> initializeCESDK = <span class="hljs-function">(<span class="hljs-params">imagePath: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    CreativeEngine.init(config).then(<span class="hljs-function">(<span class="hljs-params">engine</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> container = cesdk_container.current!;
      container.innerHTML = <span class="hljs-string">""</span>;
      container.append(engine.element);

      <span class="hljs-keyword">let</span> scene = engine.scene.create();

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt;= <span class="hljs-number">6</span>; i++) {
        <span class="hljs-keyword">const</span> page = engine.block.create(<span class="hljs-string">"page"</span>);
        engine.block.setWidth(page, <span class="hljs-number">500</span>);
        engine.block.setHeight(page, <span class="hljs-number">500</span>);
        engine.block.setPositionX(page, grids[i].x);
        engine.block.setPositionY(page, grids[i].y);
        engine.block.appendChild(scene, page);

        <span class="hljs-keyword">const</span> block = engine.block.create(<span class="hljs-string">"graphic"</span>);
        engine.block.setShape(block, engine.block.createShape(<span class="hljs-string">"rect"</span>));
        engine.block.setFill(block, engine.block.createFill(<span class="hljs-string">"color"</span>));
        engine.block.setWidth(block, <span class="hljs-number">500</span>);
        engine.block.setHeight(block, <span class="hljs-number">500</span>);
        <span class="hljs-keyword">const</span> colorFill = engine.block.getFill(page);
        engine.block.setColor(colorFill, <span class="hljs-string">"fill/color/value"</span>, grids[i].color);
        engine.block.appendChild(page, block);

        <span class="hljs-keyword">const</span> imageFill = engine.block.createFill(<span class="hljs-string">"image"</span>);
        engine.block.setString(
          imageFill,
          <span class="hljs-string">"fill/image/imageFileURI"</span>,
          imagePath || defaultImage
        );
        engine.block.setFill(block, imageFill);
      }
    });
  };

    <span class="hljs-comment">// Function to process the image file upload on input change.</span>
  <span class="hljs-keyword">const</span> uploadImage = <span class="hljs-keyword">async</span> (event: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    <span class="hljs-keyword">if</span> (event.target.files) {
        <span class="hljs-comment">// Get the first file in the list</span>
      <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];
      <span class="hljs-comment">// Create a Blob URL from the file.</span>
      <span class="hljs-keyword">const</span> blobUrl = URL.createObjectURL(file);
      <span class="hljs-comment">// Set the Blob URL to state.</span>
      setImagePath(blobUrl);
    }
  };

    <span class="hljs-comment">// Add imagePath to the useEffect dependency.</span>
  useEffect(<span class="hljs-function">() =&gt;</span> {
    initializeCESDK(imagePath);
  }, [imagePath]);

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex flex-col items-center justify-center"</span>&gt;
      &lt;label htmlFor=<span class="hljs-string">"upload-headshot"</span> className=<span class="hljs-string">"hidden"</span>&gt;
        Upload your image
      &lt;/label&gt;
      &lt;input
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
        accept=<span class="hljs-string">"image/png, image/jpeg, image/jpg"</span>
        id=<span class="hljs-string">"upload-headshot"</span>
        name=<span class="hljs-string">"upload-headshot"</span>
        className=<span class="hljs-string">"mt-6 text-white border-2 border-white rounded-full 
        file:mr-3 file:px-3 file:py-2 file:border-0 
        file:bg-white file:text-black hover:file:bg-blue-200"</span>
        onChange={uploadImage}
        required
      /&gt;
      &lt;div
        ref={cesdk_container}
        style={{ width: <span class="hljs-string">"100vw"</span>, height: <span class="hljs-string">"100vh"</span> }}
        className=<span class="hljs-string">""</span>
      &gt;&lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>In the code snippet above, we:</p>
<ul>
<li><p>Use the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static"><code>URL.createObjectURL()</code></a> method to create a Blob string URL representing the image file. This string URL’s lifetime is tied to the current <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document"><code>document</code></a> in the window.</p>
</li>
<li><p>Use the <code>useState</code> hook to store the Blob URL of the uploaded image.</p>
</li>
<li><p>Add <code>imagePath</code> to the <code>useEffect</code> dependency, so the <code>initializeCESDK()</code> function is called again whenever <code>imagePath</code> changes ensuring that the UI updates accordingly without reloading the page.</p>
</li>
<li><p>Add the new <code>&lt;input type="file"&gt;</code> HTML element with an <code>onChange</code> event.</p>
</li>
</ul>
<p>Now you should be able to upload a new image in the canvas and get the same background colors applied (<strong>NB</strong>: ensure you’re uploading PNG images with a transparent background).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726048882485/6f02fc12-fc20-4957-aa3d-b76c5dabb5c8.png" alt /></p>
<h3 id="heading-vii-download-all-pages-as-png-image">[VII] Download all Pages as PNG Image</h3>
<p>Now let’s add the last feature to allow a user to download all the generated variations of their headshot. In the same <code>/components/headshotCanvas.tsx</code> file, add more code like so (this is the final code of this entire component):</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef, useState, ChangeEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> CreativeEngine, { MimeType, ExportOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"@cesdk/engine"</span>;
<span class="hljs-keyword">import</span> { grids } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/utils/grids"</span>;

<span class="hljs-keyword">const</span> config = {
  license: process.env.NEXT_PUBLIC_CESDK_LICENSE,
  userId: <span class="hljs-string">"guides-user"</span>,
  baseURL: <span class="hljs-string">"https://cdn.img.ly/packages/imgly/cesdk-engine/1.24.0/assets"</span>,
};

<span class="hljs-keyword">const</span> defaultImage =
  <span class="hljs-string">"https://github.com/BolajiAyodeji/attraktives-headshot/blob/main/public/headshot.png?raw=true"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BgAddPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [imagePath, setImagePath] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> cesdk_container = useRef&lt;HTMLDivElement&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> initializeCESDK = <span class="hljs-function">(<span class="hljs-params">imagePath: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    CreativeEngine.init(config).then(<span class="hljs-function">(<span class="hljs-params">engine</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> container = cesdk_container.current!;
      container.innerHTML = <span class="hljs-string">""</span>;
      container.append(engine.element);

      <span class="hljs-keyword">let</span> scene = engine.scene.create();

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt;= <span class="hljs-number">6</span>; i++) {
        <span class="hljs-keyword">const</span> page = engine.block.create(<span class="hljs-string">"page"</span>);
        engine.block.setWidth(page, <span class="hljs-number">500</span>);
        engine.block.setHeight(page, <span class="hljs-number">500</span>);
        engine.block.setPositionX(page, grids[i].x);
        engine.block.setPositionY(page, grids[i].y);
        engine.block.appendChild(scene, page);

        <span class="hljs-keyword">const</span> colorFill = engine.block.getFill(page);

        <span class="hljs-keyword">const</span> block = engine.block.create(<span class="hljs-string">"graphic"</span>);
        engine.block.setShape(block, engine.block.createShape(<span class="hljs-string">"rect"</span>));
        engine.block.setFill(block, engine.block.createFill(<span class="hljs-string">"color"</span>));
        engine.block.setColor(colorFill, <span class="hljs-string">"fill/color/value"</span>, grids[i].color);
        engine.block.setWidth(block, <span class="hljs-number">500</span>);
        engine.block.setHeight(block, <span class="hljs-number">500</span>);
        engine.block.appendChild(page, block);

        <span class="hljs-keyword">const</span> imageFill = engine.block.createFill(<span class="hljs-string">"image"</span>);
        engine.block.setString(
          imageFill,
          <span class="hljs-string">"fill/image/imageFileURI"</span>,
          imagePath || defaultImage
        );

        engine.block.destroy(engine.block.getFill(block));
        engine.block.setFill(block, imageFill);
      }

      <span class="hljs-comment">// Export all pages on the scene.</span>
      <span class="hljs-keyword">const</span> exportButton = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"export_button"</span>)!;
      exportButton.removeAttribute(<span class="hljs-string">"disabled"</span>);
      exportButton.onclick = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-comment">// Specify the image format (PNG).</span>
        <span class="hljs-keyword">const</span> mimeType = <span class="hljs-string">"image/png"</span> <span class="hljs-keyword">as</span> MimeType;
        <span class="hljs-comment">// Specify compression level (original default for PNG is 5).</span>
        <span class="hljs-keyword">const</span> options: ExportOptions = {
          pngCompressionLevel: <span class="hljs-number">9</span>,
        };

        <span class="hljs-keyword">const</span> pages = engine.scene.getPages();
        <span class="hljs-comment">// Loop through all the pages on the scene.</span>
        pages.map(<span class="hljs-keyword">async</span> (page) =&gt; {
          <span class="hljs-comment">// Download multiple Blob files as PNG for each page.</span>
          <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">await</span> engine.block.export(page, mimeType, options);
          <span class="hljs-keyword">const</span> anchor = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"a"</span>);
          anchor.href = URL.createObjectURL(blob);
          anchor.download = <span class="hljs-string">`attraktives-hs-<span class="hljs-subst">${page}</span>.png`</span>;
          anchor.click();
        });
      };
    });
  };

  <span class="hljs-keyword">const</span> uploadImage = <span class="hljs-keyword">async</span> (event: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    <span class="hljs-keyword">if</span> (event.target.files) {
      <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];
      <span class="hljs-keyword">const</span> blobUrl = URL.createObjectURL(file);
      setImagePath(blobUrl);
    }
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    initializeCESDK(imagePath);
  }, [imagePath]);

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex flex-col items-center justify-center"</span>&gt;
      &lt;label htmlFor=<span class="hljs-string">"upload-headshot"</span> className=<span class="hljs-string">"hidden"</span>&gt;
        Upload your image
      &lt;/label&gt;
      &lt;input
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
        accept=<span class="hljs-string">"image/png, image/jpeg, image/jpg"</span>
        id=<span class="hljs-string">"upload-headshot"</span>
        name=<span class="hljs-string">"upload-headshot"</span>
        className=<span class="hljs-string">"pt-6 text-white
        file:mr-4 file:py-2 file:px-4
        file:rounded-full file:border-0
          file:bg-white file:text-black
          hover:file:bg-blue-200"</span>
        onChange={uploadImage}
        required
      /&gt;
      &lt;button
        id=<span class="hljs-string">"export_button"</span>
        className=<span class="hljs-string">"w-80 lg:w-52 px-6 py-4 mt-6 text-center bg-white text-black hover:bg-blue-200"</span>
      &gt;
        Download Pages &amp;nbsp; ↯
      &lt;/button&gt;
      &lt;div
        ref={cesdk_container}
        style={{ width: <span class="hljs-string">"100vw"</span>, height: <span class="hljs-string">"100vh"</span> }}
      &gt;&lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>In the code snippet above, we:</p>
<ul>
<li><p>Specify the image format and compression level of the export.</p>
</li>
<li><p>Loop through all the pages on the scene and download multiple Blob files as PNG for each page.</p>
</li>
<li><p>Add a <code>&lt;button&gt;</code> element to the page and use the <code>id</code> of the element to add a <code>&lt;a href="" download=""&gt;</code> element so clicking the button will automatically trigger the downloads. Since there are just six images, this probably is sufficient and there’s no need to add a zip file logic (if the pages on the scene increase, a zip file will be best!).</p>
</li>
</ul>
<p>Here’s what the final page should look like now. You can now use other different images from your computer and download them swiftly :).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726048945799/c30f7297-bfe5-494f-98d0-6db5de4cf4e7.png" alt /></p>
<h2 id="heading-bonus-building-an-image-background-removal-app">Bonus: Building an Image Background Removal App</h2>
<p>For users to effectively use the app we just built, they need to always upload a transparent image with its background removed. So why not build another app that allows a user to remove the background of an image first before proceeding to add background colors? Well, IMG․LY also built a <a target="_blank" href="https://github.com/imgly/background-removal-js">background-removal.js</a> library that allows you to remove backgrounds from images right in the browser environment with no additional costs or privacy concerns! The library used the Neural Network (<a target="_blank" href="https://onnx.ai/">ONNX model</a>) and WASM files which are hosted by default.</p>
<p>You can pass either an <code>ImageData</code>, <code>ArrayBuffer</code>, <code>Uint8Array</code>, <code>Blob</code>, <code>URL</code>, or <code>string</code> to the <code>imglyRemoveBackground()</code> function from the package and the result will be another Blob URL (PNG image format). You can learn more by reading the <a target="_blank" href="https://github.com/imgly/background-removal-js/tree/main/packages/web">documentation</a>. That said, I gave the library a quick spin and this is what the code looks like (in the <code>/bg-remove/page.tsx</code> file):</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useState, ChangeEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> imglyRemoveBackground <span class="hljs-keyword">from</span> <span class="hljs-string">"@imgly/background-removal"</span>;

<span class="hljs-keyword">const</span> loadingGif = <span class="hljs-string">"/loading.gif"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BgRemovePage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// State for the initial Blob URL of the uploaded image.</span>
  <span class="hljs-keyword">const</span> [initialImagePath, setInitialImagePath] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
  <span class="hljs-comment">// State for the final Blob URL of the processed image.</span>
  <span class="hljs-keyword">const</span> [finalImagePath, setFinalImagePath] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
  <span class="hljs-comment">// State to track when the image is processing.</span>
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> uploadImage = <span class="hljs-keyword">async</span> (event: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setLoading(<span class="hljs-literal">true</span>);
    setFinalImagePath(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">if</span> (event.target.files) {
      <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];
      <span class="hljs-keyword">const</span> initialBlobUrl = URL.createObjectURL(file);
      setInitialImagePath(initialBlobUrl);
      imglyRemoveBackground(initialBlobUrl)
        .then(<span class="hljs-function">(<span class="hljs-params">blobUrl</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> finalBlobUrl = URL.createObjectURL(blobUrl);
          setFinalImagePath(finalBlobUrl);
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Something went wrong."</span>, error);
        })
        .finally(<span class="hljs-function">() =&gt;</span> {
          setLoading(<span class="hljs-literal">false</span>);
        });
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex flex-col h-screen items-center justify-center"</span>&gt;
      &lt;label htmlFor=<span class="hljs-string">"upload-image"</span> className=<span class="hljs-string">"hidden"</span>&gt;
        Upload your image
      &lt;/label&gt;
      &lt;input
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
        accept=<span class="hljs-string">"image/png, image/jpeg, image/jpg, image/webp"</span>
        id=<span class="hljs-string">"upload-image"</span>
        name=<span class="hljs-string">"upload-image"</span>
        className=<span class="hljs-string">"text-white border-2 border-white rounded-full 
        file:mr-3 file:px-3 file:py-2 file:border-0 
      file:bg-white file:text-black hover:file:bg-blue-200"</span>
        onChange={uploadImage}
        disabled={loading}
      /&gt;

      {initialImagePath &amp;&amp; (
        &lt;div className=<span class="hljs-string">"grid grid-cols-2 gap-8 m-12"</span>&gt;
          &lt;Image
            src={initialImagePath}
            alt=<span class="hljs-string">"Preview Initial Image"</span>
            width={<span class="hljs-number">400</span>}
            height={<span class="hljs-number">400</span>}
            className=<span class="hljs-string">"border-2 border-white"</span>
          /&gt;
          &lt;Image
            src={!finalImagePath ? loadingGif : finalImagePath}
            alt=<span class="hljs-string">"Preview Final Image"</span>
            width={<span class="hljs-number">400</span>}
            height={<span class="hljs-number">400</span>}
            className={<span class="hljs-string">`<span class="hljs-subst">${finalImagePath ? <span class="hljs-string">"border-2 border-white"</span> : <span class="hljs-string">""</span>}</span>`</span>}
          /&gt;
        &lt;/div&gt;
      )}

      {initialImagePath &amp;&amp; !finalImagePath &amp;&amp; (
        &lt;p&gt;
          Hang on :). Removing image background
          &lt;span className=<span class="hljs-string">"inline-block ml-2 animate-ping"</span>&gt;...&lt;/span&gt;
        &lt;/p&gt;
      )}

      {finalImagePath &amp;&amp; (
        &lt;a
          className=<span class="hljs-string">"w-80 lg:w-52 px-6 py-4 text-center bg-white text-black hover:bg-blue-200"</span>
          href={finalImagePath}
          download=<span class="hljs-string">"attraktives-hs"</span>
        &gt;
          Download Image
        &lt;/a&gt;
      )}
    &lt;/div&gt;
  );
}
</code></pre>
<p>As seen in the documentation, the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer"><code>SharedArrayBuffer</code></a> object has some security requirements and hence two headers need to be set to cross-origin isolate the page where the library is being used. To add this, update your <code>next.config.mjs</code> config file like so:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('next').NextConfig}</span> </span>*/</span>
<span class="hljs-keyword">const</span> nextConfig = {
  <span class="hljs-keyword">async</span> headers() {
    <span class="hljs-keyword">return</span> [
      {
        <span class="hljs-attr">source</span>: <span class="hljs-string">"/bg-remove"</span>,
        <span class="hljs-attr">headers</span>: [
          {
            <span class="hljs-attr">key</span>: <span class="hljs-string">"Cross-Origin-Opener-Policy"</span>,
            <span class="hljs-attr">value</span>: <span class="hljs-string">"same-origin"</span>,
          },
          {
            <span class="hljs-attr">key</span>: <span class="hljs-string">"Cross-Origin-Embedder-Policy"</span>,
            <span class="hljs-attr">value</span>: <span class="hljs-string">"require-corp"</span>,
          },
        ],
      },
    ];
  },
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> nextConfig;
</code></pre>
<p>This is what the result looks like. Works like magic!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726049030490/2a2a00bb-f6ea-4f31-8b82-205c173e9246.png" alt /></p>
<p>This app doesn’t require a license so you can use it as much as you want <a target="_blank" href="https://attraktives-hs.vercel.app/bg-remove">here</a> (I deployed the finished code and will probably make more changes later to make the experience better). For first-time use, the <code>WASM</code> and <code>ONNX</code> model files are fetched in the browser and this might take some time depending on your bandwidth. But it would be faster for subsequent requests since the files are cached. Give it a spin and feel free to share it with your friends and enemies too :).</p>
<h2 id="heading-bonus-building-a-canva-like-design-editor">Bonus: Building a Canva-Like Design Editor</h2>
<p>So far, we’ve explored how to use the CE.SDK Engine (<code>@cesdk/engine</code>). But there’s also the CE.SDK (<code>@cesdk/cesdk-js</code>) that offers more customizable editing UI components (more like a full-suite Canva-like design editor experience). You get the chance to customize the editor as much as your need requires. I won’t go into much detail here since we’ve covered quite a lot already but you can read the <a target="_blank" href="https://img.ly/docs/cesdk/">documentation</a> to learn more. To test this quickly, you can add the code below in the <code>/coomponents/editorCanvas.tsx</code>:</p>
<pre><code class="lang-ts"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { UserButton } <span class="hljs-keyword">from</span> <span class="hljs-string">"@clerk/nextjs"</span>;
<span class="hljs-keyword">import</span> CreativeEditorSDK, { Configuration } <span class="hljs-keyword">from</span> <span class="hljs-string">"@cesdk/cesdk-js"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">EditorPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> cesdk_container = useRef(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> router = useRouter();

  <span class="hljs-keyword">const</span> config: Configuration = {
    license: process.env.NEXT_PUBLIC_CESDK_LICENSE,
    userId: <span class="hljs-string">"guides-user"</span>,
    baseURL: <span class="hljs-string">"https://cdn.img.ly/packages/imgly/cesdk-js/1.24.0/assets"</span>,
    ui: {
      elements: {
        view: <span class="hljs-string">"default"</span>,
        navigation: {
          show: <span class="hljs-literal">true</span>,
          action: {
            close: <span class="hljs-literal">true</span>,
            back: <span class="hljs-literal">true</span>,
            load: <span class="hljs-literal">true</span>,
            save: <span class="hljs-literal">true</span>,
            <span class="hljs-keyword">export</span>: {
              show: <span class="hljs-literal">true</span>,
              format: [<span class="hljs-string">"application/pdf"</span>, <span class="hljs-string">"image/png"</span>],
            },
            download: <span class="hljs-literal">true</span>,
          },
        },
        panels: {
          settings: {
            show: <span class="hljs-literal">true</span>,
          },
        },
      },
    },
    callbacks: {
      onUpload: <span class="hljs-string">"local"</span>,
      onBack: <span class="hljs-function">() =&gt;</span> {
        router.push(<span class="hljs-string">"/start"</span>);
      },
      onClose: <span class="hljs-function">() =&gt;</span> {
        router.push(<span class="hljs-string">"/start"</span>);
      },
    },
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> container = cesdk_container.current!;
    <span class="hljs-keyword">if</span> (container) {
      CreativeEditorSDK.create(container, config).then(<span class="hljs-keyword">async</span> (instance) =&gt; {
        instance.addDefaultAssetSources();
        instance.addDemoAssetSources({ sceneMode: <span class="hljs-string">"Design"</span> });
        <span class="hljs-keyword">await</span> instance.createDesignScene();
      });
    }
  });

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;div className=<span class="hljs-string">"flex flex-col items-center p-2"</span>&gt;
        &lt;UserButton afterSignOutUrl={<span class="hljs-string">"/start"</span>} /&gt;
      &lt;/div&gt;
      &lt;div
        ref={cesdk_container}
        style={{ width: <span class="hljs-string">"100vw"</span>, height: <span class="hljs-string">"100vh"</span> }}
      &gt;&lt;/div&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>The setup is similar to the previous we covered. All we need to do now is add the following code in the <code>/editor/page.tsx</code> file to render the component above in a page (<code>/editor</code> in this case):</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> dynamic <span class="hljs-keyword">from</span> <span class="hljs-string">"next/dynamic"</span>;

<span class="hljs-keyword">const</span> CreativeEditorSDKWithNoSSR = dynamic(
  <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">"../components/editorCanvas"</span>),
  {
    ssr: <span class="hljs-literal">false</span>,
  }
);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> CreativeEditorSDKWithNoSSR;
</code></pre>
<p>With this, your canvas should looks like the amazing screenshot below!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726049084041/421ba50a-84b8-4647-b5b7-23fa93c00be8.png" alt /></p>
<h2 id="heading-authentication-and-protected-pages-with-clerk">Authentication and Protected Pages with Clerk</h2>
<p>If you observed, I imported a <code>@clerk/nextjs</code> package in the snippet above. That’s Clerk, a complete suite of embeddable UIs, flexible APIs, and admin dashboards to authenticate and manage users. By installing the SDK package and importing a few components, you can setup auth routes, authenticate users, and protect certain pages from unauthenticated access. The major configuration is done in the <code>/middleware.ts</code> file as seen in the code snippet below where I specify the <code>/editor</code> route to protect (will not be accessible unless the user signs in). You can learn more by reading <a target="_blank" href="https://clerk.com/docs/references/nextjs/overview">Clerk’s Nextjs documentation</a> and exploring my setup in <a target="_blank" href="https://github.com/BolajiAyodeji/attraktives-headshot/tree/main/app/auth">the repository</a>.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { clerkMiddleware, createRouteMatcher } <span class="hljs-keyword">from</span> <span class="hljs-string">"@clerk/nextjs/server"</span>;

<span class="hljs-keyword">const</span> isProtectedRoute = createRouteMatcher([<span class="hljs-string">"/editor"</span>]);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> clerkMiddleware(<span class="hljs-function">(<span class="hljs-params">auth, req</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (isProtectedRoute(req)) auth().protect();
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  matcher: [<span class="hljs-string">"/((?!.+\\.[\\w]+$|_next).*)"</span>, <span class="hljs-string">"/"</span>, <span class="hljs-string">"/(api|trpc)(.*)"</span>],
};
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726049140632/f8a7ddee-2fc4-4249-a9b2-58a87156f7b7.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726049146886/f0f3bd6b-7c28-41e9-ad5a-beeb6f5151e9.png" alt /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Phew (happy sigh 😅)! That was quite some learning and building! Trust me, there’s so much more you can do with the CE.SDK Engine and CE.SDK; we only attempted to scratch the surface in this tutorial. But I believe you’ve learned some new things here and this gives you a good foundation to get started with and then you can further your learning (I’ve attached some helpful resources in the next section that you might want to check out). Do let me know what you think and how helpful all of the information is to you! I’m keen to hear what you plan to build next with CE.SDK or any use case you have in mind (feel free to leave a comment below). You can explore the code on <a target="_blank" href="https://github.com/BolajiAyodeji/attraktives-headshot">GitHub</a> too. Thanks for reading this far!</p>
<h2 id="heading-further-resources">Further Resources</h2>
<ul>
<li><p><a target="_blank" href="https://img.ly/docs/cesdk">CreativeEditor SDK documentation</a>.</p>
</li>
<li><p><a target="_blank" href="https://img.ly/docs/cesdk">CreativeEditor SDK collection of examples</a> (for different platforms).</p>
</li>
<li><p><a target="_blank" href="https://img.ly/showcases/cesdk">Collection of preconfigured starter-kits</a>.</p>
</li>
<li><p><a target="_blank" href="https://clerk.dev/docs">Clerk guides and resources</a>.</p>
</li>
</ul>
<hr />
<blockquote>
<p><strong>Friendly disclaimer</strong>: This is not a paid promotional for IMGLY or so :). I built this app and wrote this tutorial months ago as part of an interview process and I’m happy to share my learnings with everyone as always. I hope you still find the content useful; there’s quite a lot you can learn here. Cheers!</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[How to Build an Audio Chatbot with Nextjs, OpenAI, and ElevenLabs]]></title><description><![CDATA[With the rise of artificial intelligence (AI) and large language models (LLMs), it has become easier to solve different human problems than ever before. Even consumers with little to no technical expertise can benefit from AI. Humans can now automate...]]></description><link>https://blog.bolajiayodeji.com/how-to-build-an-audio-chatbot-with-nextjs-openai-and-elevenlabs</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-build-an-audio-chatbot-with-nextjs-openai-and-elevenlabs</guid><category><![CDATA[software development]]></category><category><![CDATA[AI]]></category><category><![CDATA[openai]]></category><category><![CDATA[chatbot]]></category><category><![CDATA[llm]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[elevenlabs]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Mon, 18 Mar 2024 16:28:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710544209800/51e1e430-eea7-4089-8ce2-7a9db6a797d0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With the rise of artificial intelligence (AI) and large language models (LLMs), it has become easier to solve different human problems than ever before. Even consumers with little to no technical expertise can benefit from AI. Humans can now automate complex tasks, gain insights from data, enjoy the benefits of assistive services, and many more. The possibilities of LLMs are endless, and we're just beginning to scratch the surface of what's possible with the numerous product features rising in the AI industry. Over the past weeks, I experimented with AI, LLMs, and the latest Nextjs App Router to see how AI tools and audio can be integrated on the web. This was really fun, and I learned some new things I’d like to share with you today.</p>
<p>After building some demos, I came up with a simple chatbot implementation. In this tutorial, we will build a conversational text and voice chatbot with full-stack Nextjs, OpenAI, and ElevenLabs. A user would be able to ask a question by text and get an audio and text response. Sounds interesting to you? Then Let’s get into it! If you want to see the code, you can <a target="_blank" href="https://github.com/BolajiAyodeji/chat-with-siri">head to this repository</a> now, but you might want to read along to learn one or two things, especially if you’re new to this :).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709209560686/swmS9psd3.png" alt="The resulting UI of the project" /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the best out of this tutorial, you need to have/do the following:</p>
<ul>
<li><p>Nodejs and NPM installed on your computer.</p>
</li>
<li><p>An IDE and terminal installed on your computer (I use Visual Studio Code!).</p>
</li>
<li><p>A web browser installed on your computer (I use Arc!).</p>
</li>
<li><p>Some prior knowledge of the JavaScript and TypeScript programming language.</p>
</li>
<li><p>Some prior knowledge of the Reactjs JavaScript library.</p>
</li>
<li><p>Some prior knowledge of the Nextjs JavaScript framework.</p>
</li>
<li><p>A smile on your face :).</p>
</li>
</ul>
<h2 id="heading-chatbot-implementation-flow">Chatbot Implementation Flow</h2>
<p>Since our goal is to allow a user to ask a question by text and get an audio response, we would need to:</p>
<ol>
<li><p>Build a user interface with an input element for receiving the text input.</p>
</li>
<li><p>Send the text input to OpenAI using their API and receive a text response.</p>
</li>
<li><p>Send the received text response to ElevenLabs using their API and receive an audio response.</p>
</li>
<li><p>Return the final results (text response from OpenAI and audio response from ElevenLabs) to the user in the same user interface.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709293509026/getAdI-IJ.png" alt="Implementation flow of the audio chatbot app" /></p>
<h2 id="heading-key-concepts">Key Concepts</h2>
<p>Now that we have a draft implementation flow designed, let’s briefly talk about some key concepts and the core tools we will be using for this application (specifically, OpenAI’s GPT and ElevenLabs’ Speech Synthesis Models) in the sections below.</p>
<h3 id="heading-al-and-llm-terms">AL and LLM Terms</h3>
<p>The terms below should help you get acquainted with some of the concepts introduced in this tutorial, especially if you’re new to AI and LLMs.</p>
<details><summary>Artificial Intelligence (AI)</summary><div data-type="detailsContent">According to <a target="_blank" href="https://en.wikipedia.org/wiki/Artificial_intelligence">Wikipedia</a>, “Artificial intelligence, is a field of study in computer science and is the intelligence of machines or software, as opposed to the intelligence of other living beings, primarily of humans.”</div></details><details><summary>Machine Learning (ML)</summary><div data-type="detailsContent">A branch of artificial intelligence and collection of algorithms/statistical techniques used to create computational systems (usually called models) that learn from data to make predictions and intelligent decisions. Basically, you provide a model with a data set and train it with some algorithms, and when you introduce new data, the model will use the learned knowledge to analyze the new data set. ML is widely used for use cases like sentiment analysis, recommendation systems, language translation, chatbots, etc. To learn more, you can read <a target="_blank" href="https://github.com/BolajiAyodeji/deploy-ml-web-workshop/blob/main/lessons/01.md">this lesson</a> in my free “Deploying Machine Learning Models to the Web” workshop.</div></details><details><summary>Generative AI (GenAI)</summary><div data-type="detailsContent">An intelligent machine (generative model) that is capable of generating text, audio, images, and other data types in response to user prompts.</div></details><details><summary>Large Language Models (LLMs)</summary><div data-type="detailsContent">A form of generative AI. This is what the popular ChatGPT you know and other similar chat agents are built on.</div></details><details><summary>Prompt Engineering</summary><div data-type="detailsContent">The art of creating prompts (user inputs/questions) used to interact with large language models in order to maximize the responses. The better and more customized the prompt, the more optimal results you will get from the LLM.</div></details><details><summary>Speech Synthesis</summary><div data-type="detailsContent">The production of artificial human speech using artificial intelligence. This could be used for different use cases, including text-to-speech, speech-to-speech, audio/video dubbing, etc.</div></details>

<h3 id="heading-openais-gpt-generative-pre-trained-transformers">OpenAI’s GPT (Generative Pre-trained Transformers)</h3>
<p>OpenAI is an AI research company building artificial general intelligence (AGI). They’ve built several models (you can <a target="_blank" href="https://platform.openai.com/docs/models/overview">explore the full list here</a>), but for this tutorial, we will use the GPT text generation model through the <a target="_blank" href="https://platform.openai.com/docs/guides/text-generation/chat-completions-api">Chat Completions API</a>. OpenAI uses its large language model to build ChatGPT and also provides the LLM as an API for developers to build their own applications. This allows developers to build creative stuff powered by AI for different use cases. I’m sure in the past year, you have seen almost every startup include some AI feature in their product, like GitHub Copilot, Notion AI, Grammarly AI, Vercel’s v0, tldraw make a real, etc. These startups either build their own in-house model or use OpenAI’s GPT or DALL-E or Whisper models (or other similar models like <a target="_blank" href="https://llama.meta.com">Meta's LLaMA</a>, <a target="_blank" href="https://gemini.google.com">Google’s Gemini</a>, <a target="_blank" href="https://claude.ai">Anthropic’s Claude</a>, <a target="_blank" href="https://midjourney.com">Midjourney</a>, <a target="_blank" href="https://mistral.ai">Mistral AI</a>, <a target="_blank" href="https://stability.ai/">Stability AI</a>, etc.). I’m sure many like myself started hearing more about LLMs from the launch of ChatGPT for consumers, but many companies have been building AI tools for years now, like <a target="_blank" href="https://twitter.com/iambolajiayo/status/1211704914482728962">Microsoft’s Sketch2Code</a>, amongst many others.</p>
<h3 id="heading-elevenlabs-speech-synthesis">ElevenLabs Speech Synthesis</h3>
<p>ElevenLabs has created realistic <a target="_blank" href="http://elevenlabs.io/?from=bolajiayodeji2995">AI audio models</a>, providing the ability to generate speech in hundreds of voices in 25+ languages. You can design brand new synthetic voices from scratch, clone your own voice, or use one from their voice library for speech synthesis (text-to-speech or speech-to-speech), creating audiobooks, or even video translations. Using their API to generate audio from text is quite straightforward, as seen in the code snippet below. You can also look at <a target="_blank" href="https://github.com/BolajiAyodeji/elevenlabs-api-sandbox">this sandbox repository</a> I created with quick examples of making requests with ElevenLabs’ API and client libraries (JavaScript and Python).</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateAudio</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> voiceId = <span class="hljs-string">"IKne3meq5aSn9XLyUdCD"</span>;

  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(
    <span class="hljs-string">`https://api.elevenlabs.io/v1/text-to-speech/<span class="hljs-subst">${voiceId}</span>`</span>,
    {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
      <span class="hljs-attr">headers</span>: {
        <span class="hljs-attr">Accept</span>: <span class="hljs-string">"audio/mpeg"</span>,
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        <span class="hljs-string">"xi-api-key"</span>: process.env.ELEVENLABS_API_KEY,
      },
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-attr">model_id</span>: <span class="hljs-string">"eleven_multilingual_v2"</span>,
        <span class="hljs-attr">text</span>: <span class="hljs-string">"Hi, I'm Bolaji Ayodeji. How are you doing today?"</span>,
        <span class="hljs-attr">voice_settings</span>: { <span class="hljs-attr">similarity_boost</span>: <span class="hljs-number">0.5</span>, <span class="hljs-attr">stability</span>: <span class="hljs-number">0.5</span> },
      }),
    }
  );

  <span class="hljs-keyword">await</span> fs.writeFile(<span class="hljs-string">"audio-js.mp3"</span>, <span class="hljs-keyword">await</span> response.buffer(), <span class="hljs-string">"binary"</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Audio file saved successfully!"</span>);
}
</code></pre>
<p>Here’s a sample output of the request above using a random voice selection:</p>
<iframe src="https://widgets.commoninja.com/iframe/632af982-abdc-42b4-9c7e-0cc655e51cfa" width="100%" height="100%"></iframe>

<p>I also used their dubbing feature to translate the audio in <a target="_blank" href="https://youtu.be/70JYP0wwIEY">this summary video</a> below of myself and other speakers at InfoBip’s Shift conference to French. Now, I can argue with you that I was born in Paris 😅.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/ke623yjcWBg">https://youtu.be/ke623yjcWBg</a></div>
<p> </p>
<p>Just kidding. Anyway, let’s get back to the tutorial :).</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>To get started, use <code>create-next-app</code> to set up a new Nextjs project with TypeScript and App Router like so:</p>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>Next, create some extra directories and files, as seen in the file structure below (the directories with an asterisk * beside the name are the extras):</p>
<pre><code class="lang-plaintext">┌── app
    ├── api*
       ├── chat
         ├── route.ts
       ├── speech
         ├── route.ts
    ├── chat*
      ├── page.tsx
    ├── components*
      ├── chatControls.tsx
      ├── chatInput.tsx
      ├── chatMessages.tsx
      ├── chatVoice.tsx
    ├── hooks*
      ├── useLocalStorage.ts
    ├── types*
      ├── chat.ts
    ├── utils*
      ├── getVoices.ts
      ├── notifyUser.ts
    ├── layout.tsx
    ├── page.tsx
┌── public
├── .env.local
...
├── package.json
└── tsconfig.json
</code></pre>
<p>Next, install ElevenLabs’ TypeScript SDK and the React Toastify library using the command below:</p>
<pre><code class="lang-bash">npm install elevenlabs react-toastify
</code></pre>
<p>Now, let’s generate API keys on both platforms we will be using.</p>
<h3 id="heading-setting-up-openai">Setting up OpenAI</h3>
<p>Kindly follow the steps below to create an OpenAI developer account and generate an API key:</p>
<p><strong>1.</strong> Visit <a target="_blank" href="https://platform.openai.com/login?launch">this website</a> and create a new account.</p>
<p><strong>2.</strong> Upon successful signup, you will be redirected to a new page. Select the “API” card displayed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709213474705/ibh8GdRYE.png" alt="Screenshot 2024-02-14 at 11.14.53.png" /></p>
<p><strong>3.</strong> On the left side of the page, click “API Keys”, and this will take you to the desired page.</p>
<p><strong>4.</strong> Now click the “Create new secret key” button and create a new API key. Once you do that, copy the key and add it to the <code>.env.local</code> file you created earlier (i.e., <code>OPENAI_API_KEY=&lt;the key here&gt;</code>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709213521368/wmuKrJJ99.png" alt="Screenshot 2024-02-14 at 11.18.59.png" /></p>
<p><strong>5.</strong> When you create a new account, you might get some free credits for your first few requests. If you exhaust that, you will need to buy some credits. All you have to do is click on "Settings" on the left side of the page and select "Billing" and this will take you to the desired page. Now click the "Add to credit balance" button to top up any amount you want (at least $5).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709664385483/-DcC4GHbA.png" alt /></p>
<h3 id="heading-setting-up-elevenlabs">Setting up ElevenLabs</h3>
<p>Kindly follow the steps below to create an ElevenLabs account and generate an API key:</p>
<p><strong>1.</strong> Visit <a target="_blank" href="https://elevenlabs.io/sign-up">this website</a> and create a new account.</p>
<p><strong>2.</strong> Upon successful signup, you will be redirected to a new page. At the bottom left section of the page, select the icon displayed, click on your name, and click on the “Profile + API” option.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709665783587/579HeCV2w.png" alt="Screenshot 2024-02-14 at 11.27.38.png" /></p>
<p><strong>3.</strong> You should see a pop-up with an API key displayed. Copy the key and add it to the <code>.env.local</code> file you created earlier (i.e., <code>ELEVENLABS_API_KEY=&lt;the key here&gt;</code>). You can also regenerate a new API key here anytime you want by clicking the 🔄 button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709665837373/jacLn3TCb.png" alt="Screenshot 2024-02-14 at 11.29.51.png" /></p>
<p><strong>4.</strong> When you create a new account, you might get free 10,000 characters per month which should be sufficient. If you exhaust that and you want more, you can upgrade your plan in the "<a target="_blank" href="https://elevenlabs.io/app/subscription">Settings &gt; Subscription</a>" page.</p>
<h2 id="heading-chatbot-implementation">Chatbot Implementation</h2>
<p>Now that we have our API keys ready, we can proceed to implement the project. With Nextjs, you can create APIs (API Routes) and there are so many ways to fetch data on the client or server-side aside. But since we’re using the latest version of Nextjs, we can experiment with the new Route Handlers and Server Actions.</p>
<ul>
<li><p><a target="_blank" href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers">Route Handlers</a> are a replacement for the previous <a target="_blank" href="https://nextjs.org/docs/pages/building-your-application/routing/api-routes">API Routes</a> and can be defined in directories anywhere in the <code>app</code> directory, inside a <code>route.js</code> or <code>route.ts</code> file. A good convention is to define them in a <code>/app/api</code> subdirectory.</p>
</li>
<li><p><a target="_blank" href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">Server Actions</a> are asynchronous functions that are executed on the server and can be used in both <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">Server</a> and <a target="_blank" href="https://nextjs.org/docs/app/building-your-application/rendering/client-components">Client</a> components to handle form submissions and data mutations. In a Server Component, you must use the <code>"use server"</code> directive at an inline function or module level and in a Client Component, you must import another file that uses the <code>"use server"</code> directive at the module level. You can watch <a target="_blank" href="https://youtu.be/dDpZfOQBMaU">this video</a> to learn more about this concept and it's benefits.</p>
</li>
</ul>
<p>Now that we're up to speed, let's proceed with building our API. We will create a public API to fetch data from OpenAI and ElevenLabs and then use the APIs somewhere in the UI upon user request to generate responses.</p>
<h3 id="heading-type-interfaces">Type Interfaces</h3>
<p>First, let's define all the type interfaces we will import across the project in the <code>/app/types/chat.ts</code> file, as seen in the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Voice <span class="hljs-keyword">as</span> VoiceResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"elevenlabs/api"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> userRole = <span class="hljs-string">"user"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> botRole = <span class="hljs-string">"assistant"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Message {
  role: <span class="hljs-keyword">typeof</span> userRole | <span class="hljs-keyword">typeof</span> botRole;
  content: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ChatVoiceProps {
  voices: VoiceResponse[];
  selectedVoice: <span class="hljs-built_in">string</span>;
  setSelectedVoice: <span class="hljs-function">(<span class="hljs-params">voice: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ChatMessagesProps {
  messages: Message[];
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ChatInputProps {
  input: <span class="hljs-built_in">string</span>;
  setInput: <span class="hljs-function">(<span class="hljs-params">input: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>;
  loading: <span class="hljs-built_in">boolean</span>;
  sendMessage: <span class="hljs-function">(<span class="hljs-params">e: React.FormEvent&lt;HTMLFormElement&gt;</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ChatControlsProps {
  audioRef: React.RefObject&lt;HTMLAudioElement&gt;;
  savedAudio: <span class="hljs-built_in">boolean</span>;
  messages: Message[];
  clearMessages: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
}
</code></pre>
<h3 id="heading-api-for-openai-requests">API for OpenAI Requests</h3>
<p>Now, let's create a new API. In the <code>/app/chat/route.ts</code> file, add the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Message } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/types/chat"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> runtime = <span class="hljs-string">"edge"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: Request</span>) </span>{
  <span class="hljs-keyword">const</span> { messages } = <span class="hljs-keyword">await</span> req.json();

  messages.map(<span class="hljs-function">(<span class="hljs-params">message: Message</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (message.role === <span class="hljs-string">"user"</span>) {
      message.content = <span class="hljs-string">`Imagine you are Siri, Apple's digital assistant and a user asks you the question: "<span class="hljs-subst">${message.content}</span>". Kindly generate a suitable response.`</span>;
    }

  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://api.openai.com/v1/chat/completions"</span>, {
    method: <span class="hljs-string">"POST"</span>,
    headers: {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${process.env.OPENAI_API_KEY}</span>`</span>
    },
    body: <span class="hljs-built_in">JSON</span>.stringify({
      model: <span class="hljs-string">"gpt-3.5-turbo"</span>,
      max_tokens: <span class="hljs-number">100</span>,
      temperature: <span class="hljs-number">0.7</span>,
      n: <span class="hljs-number">1</span>,
      messages
    })
  });
  <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();

  <span class="hljs-keyword">if</span> (data.error &amp;&amp; data.error.code === <span class="hljs-string">"invalid_api_key"</span>) {
    <span class="hljs-keyword">return</span> Response.json(<span class="hljs-string">"Something went wrong. Kindly check for error alerts."</span>, {
      status: <span class="hljs-number">401</span>
    });
  }

  <span class="hljs-keyword">const</span> output = data.choices[<span class="hljs-number">0</span>]?.message?.content?.trim();

  <span class="hljs-keyword">return</span> Response.json(output);
}
</code></pre>
<p>Here, we make a POST request to OpenAI’s <a target="_blank" href="https://platform.openai.com/docs/guides/text-generation/chat-completions-api">Chat Completion API</a>, passing in the <code>messages</code> value from the UI. The request’s body expects the following parameters:</p>
<ul>
<li><p><code>model</code>: the ID of the model to use. You can explore <a target="_blank" href="https://platform.openai.com/docs/models/model-endpoint-compatibility">the list of all the available models</a>, including <a target="_blank" href="https://openai.com/pricing">how they affect your API credits</a>. For this tutorial, we will use the <code>gpt-3.5-turbo</code> model since it's more cost-effective.</p>
</li>
<li><p><code>max_tokens</code>: The maximum number of <a target="_blank" href="https://platform.openai.com/tokenizer">tokens</a> that can be generated in the chat completion. Generally, one token is equivalent to ~4 characters of English text.</p>
</li>
<li><p><code>temperature</code>: Values between 0 and 2 that determine how GPT should generate a response. Higher values will lead to more random or creative responses, while lower values will lead to more focused and deterministic outputs.</p>
</li>
<li><p><code>n</code>: The number of chat completion choices GPT will generate for each input message. If you want to offer your users multiple options, then you should change this parameter, but for our use case, one is fine.</p>
</li>
<li><p><code>messages</code>: A list of messages of the entire conversation. This will be an array of objects, with each object containing messages from either the user, GPT, or both. Each object must contain:</p>
<ul>
<li><p><code>role</code>: who’s sending the message (either <code>user</code>, <code>system</code>, <code>assistant</code> or <code>tool</code>)</p>
</li>
<li><p><code>content</code>: the content of the message (e.g., <code>How can I fry plantain?</code>)</p>
</li>
</ul>
</li>
</ul>
<pre><code class="lang-javascript">[
   {
     <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
     <span class="hljs-string">"content"</span>: <span class="hljs-string">"Hello world!"</span>
   },
   {
     <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
     <span class="hljs-string">"content"</span>: <span class="hljs-string">"Hello! How can I assist you today?"</span>
   },
   {
     <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
     <span class="hljs-string">"content"</span>: <span class="hljs-string">"How can I fry plantain?"</span>
   },
   {
     <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
     <span class="hljs-string">"content"</span>: <span class="hljs-string">"Frying plantains is a straightforward process..."</span>
   }
   ...
];
</code></pre>
<p>To learn more about each of these parameters and the other available options, read OpenAI’s <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat">API Reference documentation</a> or read the <a target="_blank" href="https://www.promptingguide.ai/introduction/settings">LLM Settings section</a> of this Prompt Engineering Guide (highly recommend!). When a request is made to this API route, it returns an object like the code snippet below, and we will then render the data to the UI.</p>
<pre><code class="lang-javascript">{
  <span class="hljs-attr">id</span>: <span class="hljs-string">'chatcmpl-5sDSChIREMEImOpENToworK'</span>,
  <span class="hljs-attr">object</span>: <span class="hljs-string">'chat.completion'</span>,
  <span class="hljs-attr">created</span>: <span class="hljs-number">1377924170</span>,
  <span class="hljs-attr">model</span>: <span class="hljs-string">'gpt-3.5-turbo-0613'</span>,
  <span class="hljs-attr">choices</span>: [
      {
          <span class="hljs-attr">index</span>: <span class="hljs-number">0</span>,
          <span class="hljs-attr">message</span>: { <span class="hljs-attr">role</span>: <span class="hljs-string">'assistant'</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">'Hello! How can I assist you today?'</span> },
          <span class="hljs-attr">logprobs</span>: <span class="hljs-literal">null</span>,
          <span class="hljs-attr">finish_reason</span>: <span class="hljs-string">'stop'</span>
        }
    ],
  <span class="hljs-attr">usage</span>: { <span class="hljs-attr">prompt_tokens</span>: <span class="hljs-number">219</span>, <span class="hljs-attr">completion_tokens</span>: <span class="hljs-number">9</span>, <span class="hljs-attr">total_tokens</span>: <span class="hljs-number">228</span> },
  <span class="hljs-attr">system_fingerprint</span>: <span class="hljs-literal">null</span>
}
</code></pre>
<h3 id="heading-api-for-elevenlabs-requests">API for ElevenLabs Requests</h3>
<p>Next, let's create a new API. In the <code>/app/speech/route.ts</code> file, add the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ElevenLabsClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"elevenlabs"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: Request</span>) </span>{
  <span class="hljs-keyword">const</span> { message, voice } = <span class="hljs-keyword">await</span> req.json();

  <span class="hljs-keyword">const</span> elevenlabs = <span class="hljs-keyword">new</span> ElevenLabsClient({
    apiKey: process.env.ELEVENLABS_API_KEY
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> audio = <span class="hljs-keyword">await</span> elevenlabs.generate({
      voice,
      model_id: <span class="hljs-string">"eleven_turbo_v2"</span>,
      voice_settings: { similarity_boost: <span class="hljs-number">0.5</span>, stability: <span class="hljs-number">0.5</span> },
      text: message
    });

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(audio <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>, {
      headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"audio/mpeg"</span> }
    });
  } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
    <span class="hljs-built_in">console</span>.error(error);
    <span class="hljs-keyword">return</span> Response.json(error, { status: error.statusCode });
  }
}
</code></pre>
<p>Here, we make a POST request to ElevenLabs’ Text-to-Speech API, passing in the <code>voice</code> and <code>text</code> values from the UI. The request’s body expects the following parameters:</p>
<ul>
<li><p><code>voice</code>: the name of the voice to be used in generating the audio. ElevenLabs offers pre-made and community-created voices in 29 languages, but only about <a target="_blank" href="https://elevenlabs.io/docs/voicelab/pre-made-voices">35+ are added by default</a> to your account. To add more, explore the <a target="_blank" href="https://elevenlabs.io/app/voice-library">voice library</a> or make a <a target="_blank" href="https://elevenlabs.io/app/voice-lab">new voice</a>.</p>
</li>
<li><p><code>model_id</code>: the ID of the model to use. These are the available options as extracted from the <a target="_blank" href="https://elevenlabs.io/docs/api-reference/get-models"><code>/models</code> API</a>:</p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model ID</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><code>eleven_multilingual_v2</code></td><td>Our state of the art multilingual speech synthesis model, able to generate life-like speech in 29 languages.</td></tr>
<tr>
<td><code>eleven_monolingual_v1</code></td><td>Use our standard English language model to generate speech in a variety of voices, styles and moods.</td></tr>
<tr>
<td><code>eleven_english_sts_v2</code></td><td>Our state-of-the-art speech to speech model suitable for scenarios where you need maximum control over the content and prosody of your generations.</td></tr>
<tr>
<td><code>eleven_multilingual_sts_v2</code></td><td>Our cutting-edge, multilingual speech-to-speech model is designed for situations that demand unparalleled control over both the content and the prosody of the generated speech across various languages.</td></tr>
<tr>
<td><code>eleven_turbo_v2</code></td><td>Our cutting-edge turbo model is ideally suited for tasks demanding extremely low latency.</td></tr>
</tbody>
</table>
</div><ul>
<li><p><code>voice_settings</code>: the settings to override the default stored voice settings of the voice in use. You can learn more about the options for this in <a target="_blank" href="https://elevenlabs.io/docs/speech-synthesis/voice-settings">this documentation</a>.</p>
</li>
<li><p><code>text</code>: the text message you want to convert to speech.</p>
</li>
</ul>
<p>To learn more about each of these parameters and the other available options, read ElevenLabs’ <a target="_blank" href="https://elevenlabs.io/docs/api-reference/text-to-speech">API Reference documentation</a>. When a request is made to this API route, it returns a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a> object <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Response/body">ReadableStream</a> raw data. Since we’re working on the web, we can’t use the <code>play()</code> function that comes with the SDK to play the audio directly. We will then have to process this Blob object to work inside a <code>&lt;audio&gt;</code> element.</p>
<h3 id="heading-composing-the-nextjs-ui-utility-files">Composing the Nextjs UI (Utility Files)</h3>
<p>First, add the code below in the <code>/app/utils/getVoices.ts</code> file to fetch voices from ElevenLabs (all voices in your account). If you created a cloned voice or generated a new one, they all will be returned from the API.</p>
<pre><code class="lang-javascript"><span class="hljs-string">"use server"</span>;

<span class="hljs-keyword">import</span> { ElevenLabsClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"elevenlabs"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVoices</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> elevenlabs = <span class="hljs-keyword">new</span> ElevenLabsClient({
    <span class="hljs-attr">apiKey</span>: process.env.ELEVENLABS_API_KEY,
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> allVoices = <span class="hljs-keyword">await</span> elevenlabs.voices.getAll();
    <span class="hljs-keyword">return</span> allVoices.voices;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(error);
  }
}
</code></pre>
<p>Next, add the code below in the <code>/app/utils/notifyUser.ts</code> file. We will use this to display alerts using the React Toastify library.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { toast } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-toastify"</span>;
<span class="hljs-keyword">import</span> type { ToastContent, ToastOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-toastify"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">notifyUser</span>(<span class="hljs-params">
  message: ToastContent,
  options: ToastOptions
</span>) </span>{
  toast(message, {
    <span class="hljs-attr">position</span>: <span class="hljs-string">"top-right"</span>,
    <span class="hljs-attr">theme</span>: <span class="hljs-string">"light"</span>,
    <span class="hljs-attr">hideProgressBar</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">closeOnClick</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">pauseOnHover</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">draggable</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">pauseOnFocusLoss</span>: <span class="hljs-literal">false</span>,
    ...options,
  });
}
</code></pre>
<p>You will also need to addd<code>&lt;ToastContainer&gt;</code> and the <code>ReactToastify.min.css</code> CSS file in <code>app/layout.tsx</code> to make the library work like so:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ToastContainer } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-toastify"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"react-toastify/dist/ReactToastify.min.css"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;html lang=<span class="hljs-string">"en"</span>&gt;
      &lt;body&gt;
        {children}
        &lt;ToastContainer stacked /&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<h3 id="heading-composing-the-nextjs-ui-hooks">Composing the Nextjs UI (Hooks)</h3>
<p>Add the code below in the <code>/app/hooks/useLocalStorage.ts</code> file to save and get data from <code>localStorage</code> and <code>useState</code>. We will reuse this multiple times later to store data like the messages and selected voice.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useLocalStorage</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, initialValue: T</span>): [<span class="hljs-title">T</span>, (<span class="hljs-params">value: T</span>) =&gt; <span class="hljs-title">void</span>] </span>{
  <span class="hljs-keyword">const</span> [savedValue, setSavedValue] = useState(initialValue);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> item = <span class="hljs-built_in">localStorage</span>.getItem(key);
    <span class="hljs-keyword">if</span> (item) setSavedValue(<span class="hljs-built_in">JSON</span>.parse(item));
  }, [key]);

  <span class="hljs-keyword">const</span> updateValue = <span class="hljs-function">(<span class="hljs-params">value: T</span>) =&gt;</span> {
    setSavedValue(value);
    <span class="hljs-built_in">localStorage</span>.setItem(key, <span class="hljs-built_in">JSON</span>.stringify(value));
  };
  <span class="hljs-keyword">return</span> [savedValue, updateValue];
}
</code></pre>
<h3 id="heading-composing-the-nextjs-ui-chat-page">Composing the Nextjs UI (Chat Page)</h3>
<p>Now that we have created our API routes, we will create and import a couple of components inside the <code>/app/chat/page.tsx</code> file (this is the file that renders the UI on the <code>/chat</code> slug) as seen below. If you’re familiar with Reactjs, you should be able to understand what’s going on here. However, I have added comments to summarize each part of the logic. Kindly read through them slowly for some more context.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-comment">// Import all we need.</span>
<span class="hljs-keyword">import</span> { useState, useEffect, useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> ChatVoice <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/components/chatVoice"</span>;
<span class="hljs-keyword">import</span> ChatMessages <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/components/chatMessages"</span>;
<span class="hljs-keyword">import</span> ChatControls <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/components/chatControls"</span>;
<span class="hljs-keyword">import</span> ChatInput <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/components/chatInput"</span>;
<span class="hljs-keyword">import</span> useLocalStorage <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/hooks/useLocalStorage"</span>;
<span class="hljs-keyword">import</span> getVoices <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/utils/getVoices"</span>;
<span class="hljs-keyword">import</span> notifyUser <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/utils/notifyUser"</span>;
<span class="hljs-keyword">import</span> { userRole, botRole, Message } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/types/chat"</span>;
<span class="hljs-keyword">import</span> { Voice <span class="hljs-keyword">as</span> VoiceResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"elevenlabs/api"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Ref hook to update the audio element.</span>
  <span class="hljs-keyword">const</span> audioRef = useRef&lt;HTMLAudioElement&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-comment">// Store the list of voices.</span>
  <span class="hljs-keyword">const</span> [voices, setVoices] = useState&lt;VoiceResponse[]&gt;([]);

  <span class="hljs-comment">// Store the name of the selected voice.</span>
  <span class="hljs-keyword">const</span> [selectedVoice, setSelectedVoice] = useLocalStorage&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">"selectedVoice"</span>, <span class="hljs-string">"Myra"</span>);

  <span class="hljs-comment">// Store the message the user enters in the input field.</span>
  <span class="hljs-keyword">const</span> [input, setInput] = useState(<span class="hljs-string">""</span>);

  <span class="hljs-comment">// Store the list of all messages (user and bot).</span>
  <span class="hljs-keyword">const</span> [messages, setMessages] = useLocalStorage&lt;Message[]&gt;(<span class="hljs-string">"chatMessages"</span>, []);

  <span class="hljs-comment">// Store the state of generating a response.</span>
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);

  <span class="hljs-comment">// Store the state of generating the audio response.</span>
  <span class="hljs-keyword">const</span> [savedAudio, setSavedAudio] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);

  <span class="hljs-comment">// Function to make a request to the /chat API route (JSON response).</span>
  <span class="hljs-keyword">const</span> getOpenAIResponse = <span class="hljs-keyword">async</span> (chatMessages: Message[]) =&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/chat"</span>, {
      method: <span class="hljs-string">"POST"</span>,
      headers: {
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
      },
      body: <span class="hljs-built_in">JSON</span>.stringify({ messages: chatMessages })
    });

    <span class="hljs-comment">// Notify the user if the API Key is invalid.</span>
    <span class="hljs-keyword">if</span> (response.status === <span class="hljs-number">401</span>) {
      notifyUser(<span class="hljs-string">"Your OpenAI API Key is invalid. Kindly check and try again."</span>, {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"error"</span>,
        autoClose: <span class="hljs-number">5000</span>
      });
    }

    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
    <span class="hljs-keyword">return</span> data;
  };

  <span class="hljs-comment">// Function to make a request to the /speech API route (BLOB response).</span>
  <span class="hljs-keyword">const</span> getElevenLabsResponse = <span class="hljs-keyword">async</span> (text: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/speech"</span>, {
      method: <span class="hljs-string">"POST"</span>,
      headers: {
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
      },
      body: <span class="hljs-built_in">JSON</span>.stringify({
        message: text,
        voice: selectedVoice
      })
    });

    <span class="hljs-comment">// Notify the user if the API Key is invalid.</span>
    <span class="hljs-keyword">if</span> (response.status === <span class="hljs-number">401</span>) {
      notifyUser(<span class="hljs-string">"Your ElevenLabs API Key is invalid. Kindly check and try again."</span>, {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"error"</span>,
        autoClose: <span class="hljs-number">5000</span>
      });
    }

    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.blob();
    <span class="hljs-keyword">return</span> data;
  };

  <span class="hljs-comment">// When the user clicks the submit button, this function runs.</span>
  <span class="hljs-keyword">const</span> sendMessage = <span class="hljs-keyword">async</span> (event: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    event.preventDefault();

    <span class="hljs-comment">// Set the loading state to true.</span>
    setLoading(<span class="hljs-literal">true</span>);
    <span class="hljs-comment">// Clear the input field.</span>
    setInput(<span class="hljs-string">""</span>);

    <span class="hljs-comment">// Store all messages from the user in an array of objects.</span>
    <span class="hljs-keyword">const</span> chatMessages: Message[] = [...messages, { role: userRole, content: input }];
    setMessages(chatMessages);

    <span class="hljs-comment">// Make a request to OpenAI with the user's message.</span>
    <span class="hljs-comment">// For subsequent requests, this will include previous messages.</span>
    <span class="hljs-keyword">const</span> botChatResponse = <span class="hljs-keyword">await</span> getOpenAIResponse(chatMessages);

    <span class="hljs-comment">// Make a request to ElevenLabs with the current response from OpenAI.</span>
    <span class="hljs-keyword">const</span> botVoiceResponse = <span class="hljs-keyword">await</span> getElevenLabsResponse(botChatResponse);

    <span class="hljs-comment">// Read the contents of the Blob file using JavaScript's FileReader</span>
    <span class="hljs-comment">// https://developer.mozilla.org/en-US/docs/Web/API/FileReader.</span>
    <span class="hljs-keyword">const</span> reader = <span class="hljs-keyword">new</span> FileReader();
    <span class="hljs-comment">// Start reading the contents of the Blob file</span>
    <span class="hljs-comment">// Once finished, the `result` attribute will contain the</span>
    <span class="hljs-comment">// URL representing the file's data (base64 encoded string).</span>
    reader.readAsDataURL(botVoiceResponse);
    reader.onload = <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (audioRef.current) {
        <span class="hljs-comment">// Pass the file to the &lt;audio&gt; element's src attribute.</span>
        <span class="hljs-comment">// Will look like: data:audio/mpeg;base64,//uQxAAAC213G6GI0ZA8...</span>
        audioRef.current.src = reader.result <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;
        <span class="hljs-comment">// Immediately play the audio file.</span>
        audioRef.current.play();
      }
    };

    <span class="hljs-comment">// Add the bot's response to all the stored messages.</span>
    setMessages([...chatMessages, { role: botRole, content: botChatResponse }]);
    <span class="hljs-comment">// Set the loading state to false.</span>
    setLoading(<span class="hljs-literal">false</span>);
    <span class="hljs-comment">// Set the state of the audio to saved.</span>
    setSavedAudio(<span class="hljs-literal">true</span>);
  };

  <span class="hljs-comment">// Function to clear all messages from local storage.</span>
  <span class="hljs-keyword">const</span> clearMessages = <span class="hljs-keyword">async</span> () =&gt; {
    setMessages([]);
    <span class="hljs-built_in">localStorage</span>.removeItem(<span class="hljs-string">"chatMessages"</span>);
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// Fetch the voices from the getVoices() utility and store them.</span>
    getVoices()
      .then(<span class="hljs-function">(<span class="hljs-params">voices</span>) =&gt;</span> {
        setVoices(voices ?? []);
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching voices:"</span>, error);
      });
  }, []);

  <span class="hljs-keyword">return</span> (
    &lt;main className=<span class="hljs-string">"flex flex-col min-h-screen items-center justify-between py-4 px-4 lg:px-0"</span>&gt;
      {voices.length === <span class="hljs-number">0</span> ? (
        &lt;p className=<span class="hljs-string">"text-white text-9xl animate-ping"</span>&gt;...&lt;/p&gt;
      ) : (
        &lt;&gt;
          &lt;ChatVoice {...{ voices, selectedVoice, setSelectedVoice }} /&gt;
          &lt;ChatMessages {...{ messages }} /&gt;
          &lt;div className=<span class="hljs-string">"flex flex-col items-center w-full fixed bottom-0 pb-3 bg-gray-900"</span>&gt;
            &lt;ChatControls
              {...{
                audioRef,
                savedAudio,
                messages,
                clearMessages
              }}
            /&gt;
            &lt;ChatInput
              {...{
                input,
                setInput,
                loading,
                sendMessage
              }}
            /&gt;
          &lt;/div&gt;
        &lt;/&gt;
      )}
    &lt;/main&gt;
  );
}
</code></pre>
<p>In summary, we use the <code>useState</code>, <code>useEffect</code>, and <code>useRef</code> react hooks to set up some states, fetch data from our API, fetch the voices from the utility function, run some logic to process the data, and render the UI passing in the destructed prop values to the <code>ChatVoice</code>, <code>ChatMessages</code>, <code>ChatControls</code>, and <code>ChatInput</code> components.</p>
<h3 id="heading-composing-the-nextjs-ui-components">Composing the Nextjs UI (Components)</h3>
<p>Now let's add the code for all the components we used earlier in the <code>/chat</code> page.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">All the SVG icons used can be found in the public <a target="_blank" href="https://github.com/BolajiAyodeji/chat-with-siri/tree/main/public">GitHub repository</a>.</div>
</div>

<hr />
<p>1️⃣: In the <code>/app/components/chatVoice.tsx</code> file, add the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ChatVoiceProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/types/chat"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatVoice</span>(<span class="hljs-params">{ voices, selectedVoice, setSelectedVoice }: ChatVoiceProps</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex flex-col w-full z-10 fixed top-0 text-center items-center bg-gray-900"</span>&gt;
      &lt;div className=<span class="hljs-string">"p-4 lg:p-8 lg:w-3/4 xl:w-2/4 border-0 lg:border-x-2 lg:border-white"</span>&gt;
        &lt;label className=<span class="hljs-string">"mb-2 block text-sm lg:text-base"</span> htmlFor=<span class="hljs-string">"voices"</span>&gt;
          Change Siri&amp;apos;s Voice:
        &lt;/label&gt;
        &lt;select
          id=<span class="hljs-string">"voices"</span>
          name=<span class="hljs-string">"voices"</span>
          className=<span class="hljs-string">"p-2 w-4/4 text-sm lg:text-base appearance-none bg-transparent border border-white text-blue-500"</span>
          value={selectedVoice}
          onChange={<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> setSelectedVoice(event.target.value)}
        &gt;
          {voices &amp;&amp; voices
            .sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> (a.name!.localeCompare(b.name!))
            .map(<span class="hljs-function">(<span class="hljs-params">voice</span>) =&gt;</span> (
              &lt;option key={voice.voice_id} value={voice.name}&gt;
                {voice.name} ({voice.labels?.age} {voice.labels?.accent} {voice.labels?.gender})
              &lt;/option&gt;
            ))}
        &lt;/select&gt;
      &lt;/div&gt;
      &lt;hr className=<span class="hljs-string">"w-full lg:w-3/4 xl:w-2/4"</span> /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>2️⃣: In the <code>/app/components/chatMessages.tsx</code> file, add the code below.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ChatMessagesProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/types/chat"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatMessages</span>(<span class="hljs-params">{ messages }: ChatMessagesProps</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"absolute mt-20 lg:mt-40 pt-16 pb-48 px-6 w-full lg:w-3/4 xl:w-2/4 border-0 lg:border-x-2 lg:border-white"</span>&gt;
      {messages &amp;&amp; messages.length === <span class="hljs-number">0</span> ? (
        &lt;div className=<span class="hljs-string">"flex flex-col items-center"</span>&gt;
          &lt;Image
            src=<span class="hljs-string">"/logo.svg"</span>
            className=<span class="hljs-string">"mt-20"</span>
            alt=<span class="hljs-string">"Chat With Siri Logo"</span>
            width={<span class="hljs-number">650</span>}
            height={<span class="hljs-number">10</span>}
            priority
          /&gt;
          &lt;h2 className=<span class="hljs-string">"text-xl text-center mt-12 animate-none lg:animate-bounce"</span>&gt;
            Hi there! How can I help you today?
          &lt;/h2&gt;
        &lt;/div&gt;
      ) : (
        messages.map(<span class="hljs-function">(<span class="hljs-params">message, index</span>) =&gt;</span> (
          &lt;div key={index} className=<span class="hljs-string">"my-6 lg:my-10"</span>&gt;
            {message.role === <span class="hljs-string">"assistant"</span> ? (
              &lt;div className=<span class="hljs-string">"flex mb-2 lg:mb-4"</span>&gt;
                &lt;Image src=<span class="hljs-string">"/bot.svg"</span> alt=<span class="hljs-string">"Robot Icon"</span> width={<span class="hljs-number">20</span>} height={<span class="hljs-number">20</span>} priority /&gt;
                &lt;span className=<span class="hljs-string">"ml-2 text-md lg:text-lg font-semibold text-blue-500"</span>&gt;Siri:&lt;/span&gt;
              &lt;/div&gt;
            ) : (
              &lt;div className=<span class="hljs-string">"flex mb-2 lg:mb-4"</span>&gt;
                &lt;Image src=<span class="hljs-string">"/face.svg"</span> alt=<span class="hljs-string">"Face Icon"</span> width={<span class="hljs-number">18</span>} height={<span class="hljs-number">18</span>} priority /&gt;
                &lt;span className=<span class="hljs-string">"ml-2 text-md lg:text-lg font-semibold text-teal-500"</span>&gt;You:&lt;/span&gt;
              &lt;/div&gt;
            )}
            &lt;span className=<span class="hljs-string">"text-md lg:text-lg"</span>&gt;{message.content}&lt;/span&gt;
          &lt;/div&gt;
        ))
      )}
    &lt;/div&gt;
  );
}
</code></pre>
<p>3️⃣: In the <code>/app/components/chatControls.tsx</code> file, add the code below.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ChatControlsProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/types/chat"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatControls</span>(<span class="hljs-params">{
  audioRef,
  savedAudio,
  messages,
  clearMessages
}: ChatControlsProps</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div className={<span class="hljs-string">`flex <span class="hljs-subst">${messages.length &gt; <span class="hljs-number">0</span> ? <span class="hljs-string">`block`</span> : <span class="hljs-string">`hidden`</span>}</span>`</span>}&gt;
      &lt;button
        title=<span class="hljs-string">"Replay audio response"</span>
        disabled={!savedAudio}
        onClick={<span class="hljs-function">() =&gt;</span> audioRef.current &amp;&amp; audioRef.current.play()}
      &gt;
        &lt;Image src=<span class="hljs-string">"/play.svg"</span> alt=<span class="hljs-string">"Plus Icon"</span> width={<span class="hljs-number">40</span>} height={<span class="hljs-number">24</span>} priority /&gt;
      &lt;/button&gt;
      &lt;button
        title=<span class="hljs-string">"Start new chat"</span>
        onClick={clearMessages}
        className=<span class="hljs-string">"p-2 border-white text-black"</span>
      &gt;
        &lt;Image src=<span class="hljs-string">"/plus.svg"</span> alt=<span class="hljs-string">"Plus Icon"</span> width={<span class="hljs-number">40</span>} height={<span class="hljs-number">24</span>} priority /&gt;
      &lt;/button&gt;
      &lt;audio ref={audioRef} controls className=<span class="hljs-string">"mb-2 hidden"</span> /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>4️⃣: In the <code>/app/components/chatInput.tsx</code> file, add the code below.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ChatInputProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/app/types/chat"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ChatInput</span>(<span class="hljs-params">{ input, setInput, loading, sendMessage }: ChatInputProps</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;form onSubmit={sendMessage}&gt;
      &lt;label className=<span class="hljs-string">"hidden"</span> htmlFor=<span class="hljs-string">"message"</span>&gt;
        Enter your message here:
      &lt;/label&gt;
      &lt;input
        id=<span class="hljs-string">"message"</span>
        name=<span class="hljs-string">"message"</span>
        value={input}
        placeholder=<span class="hljs-string">"What's on your mind?..."</span>
        onChange={<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> setInput(event.target.value)}
        className=<span class="hljs-string">"w-56 md:w-72 lg:w-96 p-4 border-2 text-white bg-transparent focus:outline-none"</span>
      /&gt;
      &lt;button
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span>
        title=<span class="hljs-string">"Send message"</span>
        disabled={input.trim() === <span class="hljs-string">""</span> || input.trim().length &lt; <span class="hljs-number">5</span>}
        className=<span class="hljs-string">"w-24 md:w-24 lg:w-auto px-4 lg:px-8 py-4 border-2 border-white bg-white text-black"</span>
      &gt;
        {loading ? &lt;p className=<span class="hljs-string">"animate-spin"</span>&gt;⏳&lt;/p&gt; : <span class="hljs-string">"Ask Siri"</span>}
      &lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>Now visit <code>http://localhost:3000/chat</code> to view the finished and functional UI. The screenshot below shows a breakdown of the four (4) components in different parts of the chat page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709919682213/zEzAazgzL.png" alt="The resulting UI of the project with a breakdown of each component." /></p>
<p>In summary, a user can:</p>
<ul>
<li><p>Ask a question by text and get a text and audio response.</p>
</li>
<li><p>Change the voice of the audio response (40+ options).</p>
</li>
<li><p>Start a new chat session (the plus button).</p>
</li>
<li><p>Replay the last audio response (the play button).</p>
</li>
</ul>
<p>Here’s a quick video where I demoed the finished application and quickly walked through all the moving parts of the code:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=AeOr3zmkxBY">https://www.youtube.com/watch?v=AeOr3zmkxBY</a></div>
<p> </p>
<h2 id="heading-notes-and-recommendations">Notes and Recommendations</h2>
<p>There are a couple of things we can improve in this demo. If your users will be generating responses with a long amount of text, there would be increased latency and some delay before the final audio response is delivered, especially since we have to wait for OpenAI to respond with the full text and then ElevenLabs with the complete audio. A common solution to building conversational UIs like this is streaming the responses. <a target="_blank" href="https://sdk.vercel.ai/docs/concepts/streaming">Streaming</a> allows you to receive the responses in chunks and send them to the UI bit by bit, offering a better loading user experience. Both <a target="_blank" href="https://platform.openai.com/docs/api-reference/streaming">OpenAI</a> and <a target="_blank" href="https://elevenlabs.io/docs/api-reference/streaming">ElevenLabs</a> support streaming. We can then use <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Server Side Events (SSE)</a> or Websockets to process the chunks of data returned during streaming.</p>
<p>In a future tutorial, I will explain how to extend this demo to support streaming. In that version, we should be able to stream the response from OpenAI and pass the chunks of the stream to ElevenLabs, leading to reduced latency. For now, take some time to study how <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Server Side Events</a> and <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a> Web API work, just like I would be doing. Vercel has also built an <a target="_blank" href="https://sdk.vercel.ai/docs">AI SDK</a> you can use to build conversational streaming user interfaces in both JavaScript and TypeScript. The SDK supports multiple frontend frameworks, and they have <a target="_blank" href="https://github.com/vercel/ai/tree/main/examples">example templates</a> for different LLM providers (they call them adapters). With their SDK, you can use some wrappers to build the streaming logic without bothering about the underlying moving parts.</p>
<p>For now, I used the ElevenLabs <code>eleven_turbo_v2</code> model since I’m working with just the English language, and it’s more suitable for tasks demanding low latency like ours (you can <a target="_blank" href="https://elevenlabs.io/docs/api-reference/reducing-latency">read this guide</a> to learn more) and intentionally set the <code>max_tokens</code> OpenAI parameter to <code>100</code>. I also added a hack in <a target="_blank" href="https://github.com/BolajiAyodeji/chat-with-siri/blob/main/app/api/chat/route.ts#L8,L15">the OpenAI prompt</a> to reduce the number of characters returned like so:</p>
<pre><code class="lang-javascript">  messages.map(<span class="hljs-function">(<span class="hljs-params">message: Message</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (message.role === <span class="hljs-string">"user"</span>) {
      message.content = <span class="hljs-string">`Imagine you are Siri, Apple's digital assistant 
and a user asks you the question: "<span class="hljs-subst">${message.content}</span>". 
Kindly generate a suitable response with less than 100 characters.`</span>;
    }
  });
</code></pre>
<p>This way, we reduce the response time, and I also get to maximize API credits 😉. For this demo, this works fine, and GPT will respond to every question with the best response that fits into a hundred characters. Thank you for reading this far! Don’t forget to share this article and give the finished project a star.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/BolajiAyodeji/chat-with-siri">https://github.com/BolajiAyodeji/chat-with-siri</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>So far, we’ve built a simple chatbot using Nextjs App Router, OpenAI, and ElevenLabs. If this is your first time doing this, I’m sure you have learned quite some new things. There’s so much more you can experiment with this, and I’m looking forward to seeing them (please leave a comment if you want). You can now go ahead and fork <a target="_blank" href="https://github.com/BolajiAyodeji/chat-with-siri">the project</a>, extend the features for your own ideas, or even contribute to the code.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Always remember that AI is good, and there are so many helpful ways it can be used to solve problems that improve the lives of humans and developers like us. Please don’t misuse these tools and use them for good :). Cheers! 💙</div>
</div>

<h2 id="heading-further-learning">Further Learning</h2>
<p>To learn more about each of the tools we used in this tutorial, kindly take a look at the following helpful resources. I’m sure you’d love them.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Nextjs</td><td>OpenAI</td><td>ElevenLabs</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://nextjs.org/docs">nextjs.org/docs</a> - learn about Next.js features and API.</td><td><a target="_blank" href="https://beta.openai.com/docs">beta.openai.com/docs</a> - learn about OpenAI's API features.</td><td><a target="_blank" href="https://docs.elevenlabs.com">docs.elevenlabs.com</a> - learn about ElevenLabs' Text-to-Speech API features and API.</td></tr>
<tr>
<td><a target="_blank" href="https://nextjs.org/learn">nextjs.org/learn</a> - an interactive Next.js tutorial.</td><td><a target="_blank" href="https://github.com/openai/openai-node">openai-node</a> - the official Node.js/Typescript library for the OpenAI API.</td><td><a target="_blank" href="https://github.com/elevenlabs/elevenlabs-js">elevenlabs-js</a> - the official JavaScript library for ElevenLabs Text to Speech API.</td></tr>
<tr>
<td><a target="_blank" href="https://react.dev/learn">react.dev/learn</a> - the official React.js documentation.</td><td><a target="_blank" href="https://sdk.vercel.ai">sdk.vercel.ai</a> - an open-source library designed to help developers build conversational streaming user interfaces in JavaScript and TypeScript.</td><td><a target="_blank" href="https://github.com/elevenlabs/elevenlabs-python">elevenlabs-python</a> - the official Python library for ElevenLabs Text to Speech API.</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[How to Create an Automated Profile README using Nodejs and GitHub Actions]]></title><description><![CDATA[Some years ago, GitHub introduced the new Profile README feature that allowed GitHub users to pin a markdown file on their profile using a special repository named after their GitHub username. Since then, developers have used this file as a quick por...]]></description><link>https://blog.bolajiayodeji.com/how-to-create-an-automated-profile-readme-using-nodejs-and-github-actions</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-create-an-automated-profile-readme-using-nodejs-and-github-actions</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[GitHub Actions]]></category><category><![CDATA[GitHub API]]></category><category><![CDATA[rss]]></category><category><![CDATA[markdown]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Mon, 04 Dec 2023 21:12:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701709677548/82caadb5-62fa-45f1-998e-8e34a854c636.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some years ago, GitHub introduced the new Profile README feature that allowed GitHub users to pin a markdown file on their profile using a special repository named after their GitHub username. Since then, developers have used this file as a quick portfolio and for highlighting their achievements. I wrote <a target="_blank" href="https://blog.bolajiayodeji.com/how-to-add-a-readme-file-to-your-github-profile">an article</a> shortly after the release to share how to set it up and highlight some cool profiles I had seen. This tutorial is a sequel to that article, and I will share how I took my GitHub profile to the next level with the power of scripting and CI/CD automation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701643614721/eb0Mwocec.png" alt="Screenshot of my GitHub README Profile page" /></p>
<p>In this article, you'll learn how to automate your GitHub README Profile so that it can update itself anytime you want. We will make this possible using a custom script built with Nodejs/TypeScript, markdown-it library, GitHub API, RSS feeds, and GitHub Actions. If you want to see the code, you can <a target="_blank" href="https://github.com/BolajiAyodeji/BolajiAyodeji">head to this repository</a>, but you should read along to learn one or two things you can apply in other use cases (especially if you're new to writing automated scripts).</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the best out of this tutorial, you need to know/do the following:</p>
<ul>
<li><p>Have an existing GitHub account.</p>
</li>
<li><p>Read the <a target="_blank" href="https://blog.bolajiayodeji.com/how-to-add-a-readme-file-to-your-github-profile">first article</a> and create your README profile.</p>
</li>
<li><p>Have Nodejs and NPM installed on your computer.</p>
</li>
<li><p>Some prior knowledge of the JavaScript programming language.</p>
</li>
</ul>
<h2 id="heading-the-problem">The Problem</h2>
<p>GitHub allows you to pin only six (6) repositories on your profile. I wanted to highlight more repositories while retaining the existing ones in the most minimal way possible (without having to use <a target="_blank" href="https://github.com/anuraghazra/github-readme-stats#github-extra-pins">these kinds of badges</a>). I also wanted a way to highlight some content from <a target="_blank" href="https://bawd.bolajiayodeji.com">my newsletter</a> so I can get some traffic from there and potentially new subscribers that fit my target audience. I remember seeing some profiles in the past that listed their latest blog posts or music they were listening to. So I researched, found some of those profiles, and discovered they ran a script that automatically generated their markdown file. I then decided to make mine.</p>
<h2 id="heading-the-proposed-solution">The Proposed Solution</h2>
<p>Generally, the README file is written in <a target="_blank" href="https://markdownguide.org/getting-started?utm_source=https://blog.bolajiayodeji.com">markdown</a>, a markup language for creating formatted text. The goal is to simultaneously generate the entire markdown file, including the static and dynamic sections. This includes the badges, open-source statistics, and text descriptions. The new dynamic sections are the following:</p>
<ul>
<li><p>List of selected GitHub repositories.</p>
</li>
<li><p>List of my recent blog posts.</p>
</li>
<li><p>List of my recent newsletters.</p>
</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<p>We can easily use <a target="_blank" href="https://github.com/markdown-it/markdown-it">markdown-it</a>, a markdown parser for rendering a mix of JavaScript and plain text into a markdown file. To get started, kindly create a new directory with the following file structure:</p>
<pre><code class="lang-text">┌── .github
    ┌──  workflows
        ├── build.yml
┌── src
    ├── app.ts
    ├── fetchGitHubData.ts
    ├── fetchRssData.ts
├── package.json
└── tsconfig.json
</code></pre>
<p>Next, install the required <a target="_blank" href="https://github.com/BolajiAyodeji/BolajiAyodeji/blob/main/package.json">packages</a> using the command below:</p>
<pre><code class="lang-bash">npm install fs markdown-it rss-parser tsc-watch
</code></pre>
<pre><code class="lang-bash">npm install --save-dev @types/node
</code></pre>
<p>I'm using TypeScript and will use <code>ts</code> to compile to JavaScript during the build. My <code>tsconfig.json</code> and <code>package.json</code> scripts file looks looks like this:</p>
<pre><code class="lang-json"><span class="hljs-comment">// tsconfig.json</span>
{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES2015"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"forceConsistentCasingInFileNames"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"build"</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/*"</span>]
}
</code></pre>
<pre><code class="lang-json"><span class="hljs-comment">// package.json</span>
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"bolajiayodeji"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Bolaji Ayodeji's GitHub Profile"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"src/app.ts"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"npx tsc"</span>,
    <span class="hljs-attr">"build:watch"</span>: <span class="hljs-string">"npx tsc-watch"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run build &amp;&amp; node build/app.js"</span>,
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
...
}
</code></pre>
<h2 id="heading-fetching-the-data-from-github-api">Fetching the Data from GitHub API</h2>
<p>You can fetch different kinds of data with <a target="_blank" href="https://docs.github.com/en/rest">GitHub's API</a>. There are two options: use <a target="_blank" href="https://api.github.com">the API</a> directly or the <a target="_blank" href="https://github.com/octokit/octokit.js">Octokit.js library</a> (this is the route GitHub recommends). I decided to use the API directly since the Octokit.js library included extra stuff I wouldn't need, which is an overkill for this use case.</p>
<p>To fetch a repository, you will use the <code>https://api.github.com/repos/{owner}/{name}</code> endpoint with some parameters. <code>owner</code> represents my GitHub username and <code>name</code> is the repository's name. A GET request should return a JSON data like so:</p>
<pre><code class="lang-json">{
<span class="hljs-attr">"id"</span>: <span class="hljs-number">278287314</span>,
<span class="hljs-attr">"node_id"</span>: <span class="hljs-string">"MDEwOlJlcG9zaXRvcnkyNzgyODczMTQ="</span>,
<span class="hljs-attr">"name"</span>: <span class="hljs-string">"BolajiAyodeji"</span>,
<span class="hljs-attr">"full_name"</span>: <span class="hljs-string">"BolajiAyodeji/BolajiAyodeji"</span>,
<span class="hljs-attr">"private"</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">"owner"</span>: {
<span class="hljs-attr">"login"</span>: <span class="hljs-string">"BolajiAyodeji"</span>,
<span class="hljs-attr">"id"</span>: <span class="hljs-number">30334776</span>,
<span class="hljs-attr">"node_id"</span>: <span class="hljs-string">"MDQ6VXNlcjMwMzM0Nzc2"</span>,
<span class="hljs-attr">"avatar_url"</span>: <span class="hljs-string">"https://avatars.githubusercontent.com/u/30334776?v=4"</span>,
<span class="hljs-attr">"gravatar_id"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://api.github.com/users/BolajiAyodeji"</span>,
<span class="hljs-attr">"html_url"</span>: <span class="hljs-string">"https://github.com/BolajiAyodeji"</span>,
...
},
<span class="hljs-attr">"html_url"</span>: <span class="hljs-string">"https://github.com/BolajiAyodeji/BolajiAyodeji"</span>,
<span class="hljs-attr">"description"</span>: <span class="hljs-string">"My automated GitHub Profile README built using TypeScript and GitHub Actions."</span>,
...
<span class="hljs-attr">"created_at"</span>: <span class="hljs-string">"2020-07-09T06:55:36Z"</span>,
<span class="hljs-attr">"updated_at"</span>: <span class="hljs-string">"2023-11-11T09:25:16Z"</span>,
...
<span class="hljs-attr">"stargazers_count"</span>: <span class="hljs-number">4</span>,
<span class="hljs-attr">"watchers_count"</span>: <span class="hljs-number">4</span>,
<span class="hljs-attr">"language"</span>: <span class="hljs-string">"TypeScript"</span>,
...
<span class="hljs-attr">"open_issues_count"</span>: <span class="hljs-number">0</span>,
<span class="hljs-attr">"license"</span>: {
<span class="hljs-attr">"key"</span>: <span class="hljs-string">"mit"</span>,
<span class="hljs-attr">"name"</span>: <span class="hljs-string">"MIT License"</span>,
...
},
...
<span class="hljs-attr">"topics"</span>: [
<span class="hljs-string">"github-actions"</span>,
<span class="hljs-string">"github-api"</span>,
<span class="hljs-string">"hashnode"</span>,
<span class="hljs-string">"javascript"</span>,
...
],
<span class="hljs-attr">"visibility"</span>: <span class="hljs-string">"public"</span>,
<span class="hljs-attr">"forks"</span>: <span class="hljs-number">19</span>,
...
}
</code></pre>
<p>You can test this live yourself: <a target="_blank" href="https://api.github.com/repos/BolajiAyodeji/BolajiAyodeji"><code>api.github.com/repos/BolajiAyodeji/BolajiAyodeji</code></a>.</p>
<blockquote>
<p>If you make a request to GitHub's API using an access token through the Octokit.js library, you can make API requests without passing the <code>owner</code> parameter. GitHub will infer that from your token.</p>
</blockquote>
<p>Now, we will create a <code>fetchGitHubData()</code> function in <code>src/fetchGitHubData.ts</code> that will accept an array of repository names as the parameter. This array will look like this:</p>
<pre><code class="lang-javascript">[
  <span class="hljs-string">"BolajiAyodeji"</span>,
  <span class="hljs-string">"fed-unis-perf-eval"</span>,
  <span class="hljs-string">"movie_reviews_sentiment_analysis"</span>,
  <span class="hljs-string">"dotfiles"</span>
];
</code></pre>
<p>Here's the function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchGitHubData</span>(<span class="hljs-params">repos: <span class="hljs-built_in">Array</span>&lt;<span class="hljs-built_in">string</span>&gt;</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">string</span>&gt; </span>{
  <span class="hljs-keyword">const</span> owner = <span class="hljs-string">"BolajiAyodeji"</span>;

  <span class="hljs-keyword">const</span> list = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
    repos.map(<span class="hljs-keyword">async</span> (repo) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`https://api.github.com/repos/<span class="hljs-subst">${owner}</span>/<span class="hljs-subst">${repo}</span>`</span>);
      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`"<span class="hljs-subst">${owner}</span>/<span class="hljs-subst">${repo}</span>" not found. Kindy review your list of repositories.`</span>);
      }
      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();

      <span class="hljs-keyword">const</span> {
        html_url: url,
        full_name: name,
        stargazers_count: stars,
        forks_count: forks,
        description: desc
      } = data;

      <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;li&gt;&lt;a href=<span class="hljs-subst">${url}</span> target="_blank" rel="noopener noreferrer"&gt;<span class="hljs-subst">${name}</span>&lt;/a&gt; (&lt;b&gt;<span class="hljs-subst">${stars}</span>&lt;/b&gt; ✨ and &lt;b&gt;<span class="hljs-subst">${forks}</span>&lt;/b&gt; 🍴): <span class="hljs-subst">${desc}</span>&lt;/li&gt;`</span>;
    })
  );

  <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;ul&gt;<span class="hljs-subst">${list.join(<span class="hljs-string">""</span>)}</span>\n&lt;li&gt;More coming soon :).&lt;/li&gt;\n&lt;/ul&gt;`</span>;
}
</code></pre>
<p>In the function above, we loop through the array, fetch data for all the repositories, and extract the following fields:</p>
<ul>
<li><p><code>html_url</code>: link to the repository.</p>
</li>
<li><p><code>full_name</code>: name of the repository (including my username attached)</p>
</li>
<li><p><code>stargazers_count</code>: number of stars.</p>
</li>
<li><p><code>forks_count</code>: number of forks.</p>
</li>
<li><p><code>description</code>: description of the repository.</p>
</li>
</ul>
<p>We then use the data to render an HTML list element (<code>&lt;li&gt;</code>) for each repository in the array, join all the lists together, add an additional static list element with custom text, and return an unordered HTML list element (<code>&lt;ul&gt;</code>). The final result should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701722479958/LwGreLLVS.png" alt="Screenshot of my GitHub README Profile page" /></p>
<p>This function is useless for now, but we will use it later in <code>src/app.ts</code> to render the markdown. Let's proceed!</p>
<h2 id="heading-fetching-the-data-from-rss-feeds">Fetching the Data from RSS Feeds</h2>
<p>We can use an RSS feed for the blog post and newsletter sections. According to <a target="_blank" href="https://en.wikipedia.org/wiki/RSS?utm_source=https://blog.bolajiayodeji.com">Wikipedia</a>, RSS (Really Simple Syndication) is a web feed written in XML (eXtensible Markup Language) that allows users and applications to access website updates in a computer-readable format. People use RSS to access the latest published content from a website page. Mostly, a user will use an RSS reader to access content from multiple XML files in an organized manner and read them.</p>
<p>My blog runs on Hashnode, which provides an <code>rss.xml</code> file by default on <code>https://blog.bolajiayodeji.com/rss.xml</code>. My newsletter also runs on Substack, which also provides an <code>rss.xml</code> file on <code>https://bawd.bolajiayodeji.com/feed</code>. So, each time a new post is added to my blog or newsletter, the RSS feed automatically includes the page's current data. This makes it a good data source for my GitHub Profile.</p>
<p>Since both RSS feeds are similar in structure, I can create a <code>fetchRssData()</code> function in <code>src/fetchRssData.ts</code> for both. We will first use the <a target="_blank" href="https://github.com/rbren/rss-parser">rss-parser</a> library to turn the RSS XML feeds into JavaScript objects and then iterate to find the necessary fields.</p>
<p>The XML looks like this (<a target="_blank" href="https://blog.bolajiayodeji.com/rss.xml">view the image below live for a better experience</a>):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701638268459/nWfEscD3g.png" alt="Screenshot of the RSS feed of my blog" /></p>
<p>The parsed version looks like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"items"</span>: [
    {
      <span class="hljs-attr">"creator"</span>: <span class="hljs-string">"Bolaji Ayodeji"</span>,
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"My Developer Advocate Portfolio"</span>,
      <span class="hljs-attr">"link"</span>: <span class="hljs-string">"https://blog.bolajiayodeji.com/my-developer-advocate-portfolio"</span>,
      <span class="hljs-attr">"pubDate"</span>: <span class="hljs-string">"Mon, 28 Aug 2023 13:03:08 GMT"</span>,
      <span class="hljs-attr">"dc:creator"</span>: <span class="hljs-string">"Bolaji Ayodeji"</span>,
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"..."</span>,
      <span class="hljs-attr">"contentSnippet"</span>: <span class="hljs-string">"..."</span>,
      <span class="hljs-attr">"guid"</span>: <span class="hljs-string">"https://blog.bolajiayodeji.com/my-developer-advocate-portfolio"</span>,
      <span class="hljs-attr">"isoDate"</span>: <span class="hljs-string">"2023-08-28T13:03:08.000Z"</span>
    },
    { another item... }
    { more items... }
    {
      <span class="hljs-attr">"creator"</span>: <span class="hljs-string">"Bolaji Ayodeji"</span>,
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"How to Deploy a Machine Learning Model to the Web"</span>,
      <span class="hljs-attr">"link"</span>: <span class="hljs-string">"https://blog.bolajiayodeji.com/how-to-deploy-a-machine-learning-model-to-the-web"</span>,
      <span class="hljs-attr">"pubDate"</span>: <span class="hljs-string">"Sat, 03 Sep 2022 08:49:08 GMT"</span>,
      <span class="hljs-attr">"dc:creator"</span>: <span class="hljs-string">"Bolaji Ayodeji"</span>,
    ...
    }
...
}
</code></pre>
<p>This is what the final function will look like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> Parser <span class="hljs-keyword">from</span> <span class="hljs-string">"rss-parser"</span>;
<span class="hljs-keyword">const</span> parser = <span class="hljs-keyword">new</span> Parser();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchRssData</span>(<span class="hljs-params">url: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">string</span>&gt; </span>{
  <span class="hljs-keyword">const</span> feed = <span class="hljs-keyword">await</span> parser.parseURL(url);

  <span class="hljs-keyword">const</span> list = feed.items.slice(<span class="hljs-number">0</span>, <span class="hljs-number">5</span>).map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(item.pubDate <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);
    <span class="hljs-keyword">const</span> publishedDate = <span class="hljs-string">`<span class="hljs-subst">${date.getDate()}</span>/<span class="hljs-subst">${date.getMonth() + <span class="hljs-number">1</span>}</span>/<span class="hljs-subst">${date.getFullYear()}</span>`</span>;

    <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;li&gt;&lt;a href=<span class="hljs-subst">${item.link}</span> target="_blank" rel="noopener noreferrer"&gt;<span class="hljs-subst">${item.title}</span>&lt;/a&gt; (<span class="hljs-subst">${publishedDate}</span>).&lt;/li&gt;`</span>;
  });

  <span class="hljs-keyword">return</span> <span class="hljs-string">`
  &lt;ul&gt;
    <span class="hljs-subst">${list.join(<span class="hljs-string">""</span>)}</span>
  &lt;/ul&gt;\n
  <span class="hljs-subst">${
    url.endsWith(<span class="hljs-string">"rss.xml"</span>)
      ? <span class="hljs-string">`Read more blog posts: <span class="hljs-subst">${url.replace(<span class="hljs-regexp">/\/rss.xml$/</span>, <span class="hljs-string">""</span>)}</span>`</span>
      : <span class="hljs-string">`Read more newsletter issues: <span class="hljs-subst">${url.replace(<span class="hljs-regexp">/\/feed$/</span>, <span class="hljs-string">""</span>)}</span>`</span>
  }</span>.
  `</span>;
}
</code></pre>
<p>In the function above, we use the RSS link to fetch the data, extract just the first five items (posts), and extract the following fields:</p>
<ul>
<li><p><code>pubDate</code>: timestamp for when the content was published.</p>
</li>
<li><p><code>link</code>: link to the post.</p>
</li>
<li><p><code>title</code>: title of the post.</p>
</li>
</ul>
<p>We then use the data to render an HTML list element (<code>&lt;li&gt;</code>) for each published post, do some date formatting, join all the lists together, add an additional static list element with custom text, and return an unordered HTML list element (<code>&lt;ul&gt;</code>). The final result should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701723020483/KzTrsaasQ.png" alt="Screenshot of my GitHub README Profile page" /></p>
<h2 id="heading-generating-the-final-markdown-file">Generating the Final Markdown File</h2>
<p>Now that we have the <code>fetchGitHubData()</code> and <code>fetchRssData()</code> functions, let's combine everything and generate the <code>README.md</code> file we need. In <code>src/app.ts</code> add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">const</span> md = <span class="hljs-built_in">require</span>(<span class="hljs-string">"markdown-it"</span>)({
  html: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Enable HTML tags in source</span>
  breaks: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Convert '\n' in paragraphs into &lt;br&gt;</span>
  linkify: <span class="hljs-literal">true</span> <span class="hljs-comment">// Autoconvert URL-like text to links</span>
});
<span class="hljs-keyword">import</span> { fetchRssData } <span class="hljs-keyword">from</span> <span class="hljs-string">"./fetchRssData"</span>;
<span class="hljs-keyword">import</span> { fetchGitHubData } <span class="hljs-keyword">from</span> <span class="hljs-string">"./fetchGitHubData"</span>;

<span class="hljs-keyword">const</span> blogFeedUrl = <span class="hljs-string">"https://blog.bolajiayodeji.com/rss.xml"</span>;
<span class="hljs-keyword">const</span> newsletterFeedUrl = <span class="hljs-string">"https://bawd.bolajiayodeji.com/feed"</span>;

<span class="hljs-keyword">const</span> ossProjectRepos = [
  <span class="hljs-string">"BolajiAyodeji"</span>,
  <span class="hljs-string">"fed-unis-perf-eval"</span>,
  <span class="hljs-string">"movie_reviews_sentiment_analysis"</span>,
  <span class="hljs-string">"dotfiles"</span>
];
<span class="hljs-keyword">const</span> ossLearningMaterialRepos = [<span class="hljs-string">"deploy-ml-web-workshop"</span>, <span class="hljs-string">"cl-composable-commerce-workshop"</span>];

<span class="hljs-keyword">const</span> githubUsername = <span class="hljs-string">"BolajiAyodeji"</span>;
<span class="hljs-keyword">const</span> websiteUrl = <span class="hljs-string">"https://bolajiayodeji.com"</span>;
<span class="hljs-keyword">const</span> blogUrl = <span class="hljs-string">"https://blog.bolajiayodeji.com"</span>;
<span class="hljs-keyword">const</span> newsletterUrl = <span class="hljs-string">"https://bawd.bolajiayodeji.com"</span>;
<span class="hljs-keyword">const</span> youtubeUrl = <span class="hljs-string">"https://youtube.com/c/bolajiayodeji"</span>;
<span class="hljs-keyword">const</span> slidesUrl = <span class="hljs-string">"https://slides.com/bolajiayodeji"</span>;
<span class="hljs-keyword">const</span> twitterUrl = <span class="hljs-string">"https://twitter.com/iambolajiayo"</span>;
<span class="hljs-keyword">const</span> linkedinUrl = <span class="hljs-string">"https://linkedin.com/in/iambolajiayo"</span>;
<span class="hljs-keyword">const</span> githubSponsorsUrl = <span class="hljs-string">"https://github.com/sponsors/BolajiAyodeji"</span>;
<span class="hljs-keyword">const</span> patreonUrl = <span class="hljs-string">"https://patreon.com/bolajiayodeji"</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateMarkdown</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> websiteBadge = <span class="hljs-string">`[![Website Badge](https://img.shields.io/badge/-Website-3B7EBF?style=for-the-badge&amp;logo=amp&amp;logoColor=white)](<span class="hljs-subst">${websiteUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> hashnodeBadge = <span class="hljs-string">`[![Blog Badge](https://img.shields.io/badge/-Blog-3B7EBF?style=for-the-badge&amp;logo=Hashnode&amp;logoColor=white)](<span class="hljs-subst">${blogUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> substackBadge = <span class="hljs-string">`[![Newsletter Badge](https://img.shields.io/badge/-Newsletter-3B7EBF?style=for-the-badge&amp;logo=Substack&amp;logoColor=white)](<span class="hljs-subst">${newsletterUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> youtubeBadge = <span class="hljs-string">`[![YouTube Badge](https://img.shields.io/badge/-Youtube-3B7EBF?style=for-the-badge&amp;logo=Youtube&amp;logoColor=white)](<span class="hljs-subst">${youtubeUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> slidesBadge = <span class="hljs-string">`[![Slides Badge](https://img.shields.io/badge/-Slides-3B7EBF?style=for-the-badge&amp;logo=slides&amp;logoColor=white)](<span class="hljs-subst">${slidesUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> linkedinBadge = <span class="hljs-string">`[![Linkedin Badge](https://img.shields.io/badge/-LinkedIn-3B7EBF?style=for-the-badge&amp;logo=Linkedin&amp;logoColor=white)](<span class="hljs-subst">${linkedinUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> twitterBadge = <span class="hljs-string">`[![Twitter Badge](https://img.shields.io/badge/-@iambolajiayo-3B7EBF?style=for-the-badge&amp;logo=x&amp;logoColor=white)](<span class="hljs-subst">${twitterUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> githubSponsorsBadge = <span class="hljs-string">`[![GitHub Sponsors Badge](https://img.shields.io/badge/-github%20sponsors-3B7EBF?style=for-the-badge&amp;logo=github&amp;logoColor=white)](<span class="hljs-subst">${githubSponsorsUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> patreonBadge = <span class="hljs-string">`[![Patreon Badge](https://img.shields.io/badge/-Patreon-3B7EBF?style=for-the-badge&amp;logo=Patreon&amp;logoColor=white)](<span class="hljs-subst">${patreonUrl}</span>)`</span>;
  <span class="hljs-keyword">const</span> profileCountBadge = <span class="hljs-string">`![Profile Views Count Badge](https://komarev.com/ghpvc/?username=<span class="hljs-subst">${githubUsername}</span>&amp;style=for-the-badge)`</span>;

  <span class="hljs-keyword">const</span> githubStatsCardDark = <span class="hljs-string">`[![GitHub-Stats-Card-Dark](https://github-readme-stats.vercel.app/api?username=<span class="hljs-subst">${githubUsername}</span>&amp;show_icons=true&amp;hide_border=true&amp;include_all_commits=true&amp;card_width=600&amp;custom_title=GitHub%20Open%20Source%20Stats&amp;title_color=3B7EBF&amp;text_color=FFF&amp;icon_color=3B7EBF&amp;hide=contribs&amp;show=reviews,prs_merged,prs_merged_percentage&amp;theme=transparent#gh-dark-mode-only)](https://github.com/<span class="hljs-subst">${githubUsername}</span>/<span class="hljs-subst">${githubUsername}</span>#gh-dark-mode-only)`</span>;
  <span class="hljs-keyword">const</span> githubStatsCardLight = <span class="hljs-string">`[![GitHub-Stats-Card-Light](https://github-readme-stats.vercel.app/api?username=<span class="hljs-subst">${githubUsername}</span>&amp;show_icons=true&amp;hide_border=true&amp;include_all_commits=true&amp;card_width=600&amp;custom_title=GitHub%20Open%20Source%20Stats&amp;title_color=3B7EBF&amp;text_color=474A4E&amp;icon_color=3B7EBF&amp;hide=contribs&amp;show=reviews,prs_merged,prs_merged_percentage&amp;theme=transparent#gh-light-mode-only)](https://github.com/<span class="hljs-subst">${githubUsername}</span>/<span class="hljs-subst">${githubUsername}</span>#gh-light-mode-only)`</span>;

  <span class="hljs-keyword">const</span> markdownText = <span class="hljs-string">`&lt;div align="center"&gt;\n

  <span class="hljs-subst">${websiteBadge}</span> <span class="hljs-subst">${hashnodeBadge}</span> <span class="hljs-subst">${substackBadge}</span> <span class="hljs-subst">${youtubeBadge}</span> <span class="hljs-subst">${slidesBadge}</span> <span class="hljs-subst">${linkedinBadge}</span> <span class="hljs-subst">${twitterBadge}</span> <span class="hljs-subst">${githubSponsorsBadge}</span> <span class="hljs-subst">${patreonBadge}</span> <span class="hljs-subst">${profileCountBadge}</span>\n

  ---\n

  Hi there 👋🏾! I'm an innovative technology professional with progressive IT, web engineering, data, embedded systems, developer relations, documentation, technical writing, open-source, community building, and entrepreneurship experience in for-profit startups and non-profit technology and education organizations. I create technical content, build open-source projects and learning materials, speak/teach at developer meetups/conferences, and build several technical communities.\n

  ---\n

  <span class="hljs-subst">${githubStatsCardDark}</span>\n
  <span class="hljs-subst">${githubStatsCardLight}</span>\n

  &lt;/div&gt;\n

  ---\n

  ## Highlights

  &lt;details&gt;\n
  &lt;summary&gt;OSS Projects&lt;/summary&gt;\n
  &lt;br /&gt;
  Here are some of my other projects you might want to check out that are not pinned:\n
  &lt;br /&gt;\n&lt;br /&gt;
  <span class="hljs-subst">${<span class="hljs-keyword">await</span> fetchGitHubData(ossProjectRepos)}</span>\n
  &lt;/details&gt;\n

  &lt;details&gt;\n
  &lt;summary&gt;OSS Learning Materials&lt;/summary&gt;\n
  &lt;br /&gt;
  Here are some of my unique-styled workshop materials you can use to learn key concepts at your own pace:\n
  &lt;br /&gt;\n&lt;br /&gt;
  <span class="hljs-subst">${<span class="hljs-keyword">await</span> fetchGitHubData(ossLearningMaterialRepos)}</span>\n
  &lt;/details&gt;\n

  &lt;details&gt;\n
  &lt;summary&gt;Recent Blogposts&lt;/summary&gt;\n
  &lt;br /&gt;
  <span class="hljs-subst">${<span class="hljs-keyword">await</span> fetchRssData(blogFeedUrl)}</span>\n
  &lt;/details&gt;\n

  &lt;details&gt;\n
  &lt;summary&gt;Recent Newsletters&lt;/summary&gt;\n
  &lt;br /&gt;
  <span class="hljs-subst">${<span class="hljs-keyword">await</span> fetchRssData(newsletterFeedUrl)}</span>\n
  &lt;/details&gt;\n

  &lt;details&gt;\n
  &lt;summary&gt;Quick Tips&lt;/summary&gt;\n\n
  - 💬 How to reach me: DM [@iambolajiayo](https://twitter.com/iambolajiayo) on X (Twitter).\n
  - 📬 Where to find me: Subscribe to my [newsletter](https://bawd.bolajiayodeji.com/subscribe) to hear from me bi-weekly or send a game request on [chess.com](https://chess.com/member/bolajiayodeji).\n
  - 📖 Book recommendations: [Knowing God by J. I. Packer](https://bit.ly/3EdCFUW) and [Atomic Habits by James Clear](https://bit.ly/45r1kBH).\n
  - 💙 Fun fact: I'm in a blissful relationship [with Jesus Christ](https://biblegateway.com/passage/?search=1+Corinthians+15%3A1-11&amp;version=NKJV). Check [this](https://bit.ly/3KYYHij) out :).\n
  &lt;/details&gt;\n

  ---\n

  &lt;a href="#"&gt;Learn how this works.&lt;/a&gt; &lt;a href="https://github.com/BolajiAyodeji/BolajiAyodeji/actions/workflows/build.yml"&gt;&lt;img src="https://github.com/BolajiAyodeji/BolajiAyodeji/actions/workflows/build.yml/badge.svg" align="right" alt="Rebuild README.md file"&gt;&lt;/a&gt;\n

  &lt;div align="center"&gt;\n
   &lt;a href="https://bolajiayodeji.com" target="_blank" rel="noopener noreferrer"&gt;&lt;img src="https://bolajiayodeji.com/favicon.png" width="30" /&gt;&lt;/a&gt;\n
  &lt;/div&gt;`</span>;

  <span class="hljs-keyword">const</span> result = md.render(markdownText);

  fs.writeFile(<span class="hljs-string">"README.md"</span>, result, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Something went wrong: <span class="hljs-subst">${error}</span>.`</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`✅ README.md file was succesfully generated.`</span>);
  });
}

generateMarkdown();
</code></pre>
<p>Here, I manually add all the static markdown content that existed before (text, badges, etc.) in the <code>markdownText</code> variable, add the new dynamic sections with the functions, render the markdown file in the <code>result</code> variable, and create the markdown file using Node.js's <code>fs.writeFile()</code> method.</p>
<h2 id="heading-setting-up-the-cicd-pipeline-with-github-actions">Setting up the CI/CD Pipeline with GitHub Actions</h2>
<p>Now, to the cool part! So far, the major work has been done, and if I run <code>npm run build</code> and <code>npm run start</code> locally, a new <code>README.md</code> file will be generated, including any changes that come from the API and RSS feeds. But I shouldn't have to do this manually every time I want to change something, right? So what can we do?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701642047536/ARFND1eby.jpg" alt="A Meme from Money Heist showing the casts in a classroom" /></p>
<p>That's where CI/CD and cron jobs come in.</p>
<hr />
<h3 id="heading-cicd-pipeline">CI/CD Pipeline</h3>
<p>If you're new to this, CI/CD (which means Continuous Integration / Continuous Delivery or Deployment) is a process used in software engineering to merge code and automate specific repeated sequence of tasks (e.g., tests) that are required before and/or after deploying a software (e.g., releases) for users to access. In our case, we want to run the <code>npm run start</code> command on the cloud repeatedly on a fixed day/time and push the updated README file to the special GitHub repository using Git. This way, anytime a user views the GitHub profile, they see the latest content (the latest deployed version of the README based on the schedule). If there's an error, the process is expected to fail and return the errors in the log file.</p>
<p>This is a basic use case of CI/CD. For more extensive software, you would have a more complex pipeline (managed mainly by DevOps or SRE engineers) handling things like running unit/integration tests, updating data, release notes, etc.</p>
<h3 id="heading-cron-jobs-scheduling">Cron Jobs Scheduling</h3>
<p>The scheduling would be handled with something called a Cron Job. This is the standard method of scheduling specific tasks on a server at fixed times. At the right time or interval, certain tasks (jobs) will be executed. If you're new to this concept, you might be able to visualize how this will work based on our Profile README use case. It doesn't make sense to update the README file every second since I'm not publishing new content every second. To avoid wasting resources, setting up a Cron Job to execute the logic daily or even weekly is ideal. With this, a user will still get the most recent content. You can adjust the schedule to fit your needs if you're doing this for another use case.</p>
<p>Cron jobs are composed of expressions. An expression includes a string of five fields (<code>* * * * *</code>) which will contain numbers and some special characters. The character asterisk (<strong>*</strong>) represents <strong>any value</strong>, comma (<strong>,</strong>) represents <strong>a list of values</strong>, hyphen (<strong>-</strong>) represents <strong>a range of values</strong>, and slash (<strong>\</strong>) represents <strong>a step of values</strong>. Each field represents the following:</p>
<ol>
<li><p>Minute (0 - 59).</p>
</li>
<li><p>Hour (0 - 23).</p>
</li>
<li><p>Day of the month (1 - 31).</p>
</li>
<li><p>Month (1 - 12).</p>
</li>
<li><p>Day of the week (0 - 6 with 0 being Sunday).</p>
</li>
</ol>
<p>A cron expression <code>* * * * *</code> will mean run every minute, <code>*/30 * * * *</code> will mean run every 30 minutes, <code>*/30 7 * * *</code> will mean run every 30 minutes between 07:00 and 07:59, <code>*/30 7-10 * * *</code> will mean run every 30 minutes between 07:00 and 10:59, <code>* 12-18 * * *</code> will mean run every minute between 12:00 and 18:00, <code>0 12-18 * * *</code> will mean run hourly between 12:00 and 18:00, <code>0 12-18 5 5 *</code> will mean run hourly between 12:00 and 18:00 on the 5th day of March only, <code>0 12 * * *</code> will mean run daily at 12:00, etc. There are so many permutations, and if you're interested, you can create, test, and practice different cron expressions using services like <a target="_blank" href="https://cronexpressiontogo.com?utm_source=https://blog.bolajiayodeji.com">Cron Expression To Go</a> or <a target="_blank" href="https://crontab.guru?utm_source=https://blog.bolajiayodeji.com">Crontab Guru</a>.</p>
<h3 id="heading-github-actions">GitHub Actions</h3>
<p>You can set up a CI/CD pipeline using several tools like CircleCI, Travis CI, GitHub Actions, etc. We will use GitHub Actions for this use case since it's provided by the platform we're already using (but you can achieve the same with the others). GitHub Actions allows you to automate your build, test, and deployment pipeline with workflows inside a GitHub repository. These workflows can run on every pull request to your repository, merged pull requests, cron jobs, etc. You can learn more about GitHub Actions by reading <a target="_blank" href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions?utm_source=https://blog.bolajiayodeji.com">this guide</a> on GitHub Docs.</p>
<p>In the <code>.github/workflows/build.yml</code>, add the following code:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Profile</span> <span class="hljs-string">README</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
  <span class="hljs-attr">workflow_dispatch:</span>
  <span class="hljs-attr">schedule:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">"0 6 * * *"</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">Repo</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Use</span> <span class="hljs-string">Node.js</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">node-version:</span> <span class="hljs-string">"18"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Rebuild</span> <span class="hljs-string">README.md</span> <span class="hljs-string">File</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run</span> <span class="hljs-string">start</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Commit</span> <span class="hljs-string">and</span> <span class="hljs-string">Push</span> <span class="hljs-string">if</span> <span class="hljs-string">Changed</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|-
          git add .
          git diff
          git config --global user.email "mailtobolaji+actionsbot@gmail.com"
          git config --global user.name "BA Actions Bot"
          git commit -m "docs: auto-update README.md" -a || exit 0
          git push</span>
</code></pre>
<p>Here, I configure GitHub Actions to set the name of the build, set the determinant of the pipeline (a cron schedule), set the cron schedule to run 06:00 AM daily (GitHub’s cron schedule runs with the UTC timezone), add some steps, install all the required dependencies, run the <code>npm start</code> command, generate the new <code>README.md</code> file, and push the changes using Git (if there are any changes in the file). If no changes exist, the process terminates, and no commit is pushed.</p>
<p>If you push the entire code to GitHub, the build will run automatically in GitHub Actions upon each push. But also, it will run on its own every day as scheduled. You can see the log of all builds in the "Actions" tab of the repository.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701706236607/3gwLaWAZa.png" alt="A screenshot of a GitHub Actions log" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701706325157/RsUVQ3VaB.png" alt="A screenshot of a GitHub Actions log" /></p>
<p>Also, kindly note that you might experience some delays with GitHub Actions for the first time. Initially, I chose 00:00 as my cron schedule, but it wasn't working. <a target="_blank" href="https://github.com/orgs/community/discussions/52477">Research</a> showed that sometimes it fails during peak hours, like the first hour of the day when usage is high. I switched to 06:00, which still failed, but started working fine the next day or so. In summary, GitHub Actions can be unpredictable for cron jobs, and you can use a dedicated infrastructure if you need a reliable execution at an exact time. But for this use case, it works just fine once it gets stable (nothing critical is going on here).</p>
<h3 id="heading-custom-github-bot-account">Custom GitHub Bot Account</h3>
<p>If you observed, I used a custom <code>user.email</code> and <code>user.name</code> when setting up the GitHub actions push step. If you use a random email, it will push to GitHub with a non-existent account (no profile picture will be shown). My first thought was to use my actual GitHub account email that will push the changes as myself. But to make things much more neat, I created a <a target="_blank" href="https://github.com/ba-actions-bot">new GitHub account</a> that acts as a bot. This way, I can tell all the commits that were auto-generated from the script from mine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701710367914/xv95vopOV.png" alt="Screenshot of a GitHub commits page" /></p>
<p>To create the account, you can use your existing Gmail (if you don't have Gmail, use any email). Google allows you to create email aliases and receive emails from all in your inbox. All you must do is add a plus (+) sign after your email address and some custom text. Generally, I use this hack to create multiple email aliases for my Gmail so I can easily track specific emails based on context (e.g. if the email is bolaji@gmail.com, an alias can be bolaji+actionsbot@gmail.com). You're good to go once you create the account and set a nice name/profile picture. If you use this email to configure Git in the GitHub Actions, it will reflect accordingly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701710606078/SqDsscFWC.png" alt="Screenshot of a GitHub commits page" /></p>
<p>PS: if you choose to do this with a Gmail alias, ensure to save the password somewhere because if you forget the password and need to log in (maybe to change the profile picture), you won't be able to use the "Forget password" feature with a Gmail alias email address. GitHub's form validation filters it out.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701710836502/7SARlHKJI.png" alt="GitHub's forgot password page" /></p>
<p>However, to solve this, manually change the "Backup email address" option to "Only allow primary email" in GitHub's dashboard (Settings &gt; Emails).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701712234763/7HILljuwI.png" alt="GitHub's email seetings page" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's about it! Now, you've learned how to automate scripts with CI/CD. If you're new to many of the concepts discussed in this tutorial, you should have learned some new things you can expand on. You can also extend the ideas here and customize the script to do anything you want it to. Just find a data source and do your thing.</p>
<p>I'm keen to see what you build with this, so please leave a comment with a link to your GitHub README Profile if you do something like this. Remember that this goes beyond markdown files; you can use the automated script idea to do anything else!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701721829132/O76tPm8r9.png" alt="Screenshot of my GitHub profile page" /></p>
<p>I'm happy with the results and I look forward to making more changes in the future as the need arises :). Thanks for reading this far; cheers!</p>
]]></content:encoded></item><item><title><![CDATA[My Developer Advocate Portfolio]]></title><description><![CDATA[I'm back in the job market and hunting for my next gig! I'm keeping an open mind about the type of company and technology industry I'd like to work with next; however, I'm looking for a full-time Developer Advocate, Developer Relations Engineer, Deve...]]></description><link>https://blog.bolajiayodeji.com/my-developer-advocate-portfolio</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/my-developer-advocate-portfolio</guid><category><![CDATA[developer relations]]></category><category><![CDATA[portfolio]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Career]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Mon, 28 Aug 2023 13:03:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1692733408514/2670436e-61a2-4935-99fb-16d7c9c552c7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm back in the job market and hunting for my next gig! I'm keeping an open mind about the type of company and technology industry I'd like to work with next; however, I'm looking for a full-time <strong>Developer Advocate</strong>, <strong>Developer Relations Engineer</strong>, <strong>Developer Experience Engineer</strong>, <strong>Developer Relations Program Manager</strong>, <strong>Solutions Engineer</strong>, <strong>Technical Writer</strong>, <strong>Documentation Engineer</strong>, <strong>Community Manager</strong>, or other similar developer/customer success roles with focus on developer relations/education, technical integrations, documentation, content production/marketing, and developer programs/community management.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692802932861/e8402000-2f25-4217-be36-357a9993657d.jpeg" alt="A collage with some images of Bolaji and some friends/colleagues at different developer conferences." class="image--center mx-auto" /></p>
<h2 id="heading-a-little-about-me">A Little About Me</h2>
<p>I’m a Software Engineer (focusing on JavaScript/Python web engineering, data analysis, and embedded systems—Raspberry Pi mainly), Content Creator, Teacher, and Developer Advocate with five (5) years of professional working experience. I’m very passionate about sharing knowledge, developer education, helping them become better at what they do, working with developers/customers, and helping dev-tool companies connect and sustain profitable relationships with their IDPs/ICPs. I’m mostly writing technical articles on my <a target="_blank" href="https://blog.bolajiayodeji.com/">blog</a>, running a bi-weekly <a target="_blank" href="https://bawd.bolajiayodeji.com/">newsletter</a>, building <a target="_blank" href="https://github.com/bolajiayodeji">open-source projects</a>, organizing/speaking at meetups/conferences, and building technical communities. Being able to solve problems systematically and efficiently is my greatest strength. Using a hands-on technical approach paired with strong communication and management skills, I collaborate with people, establish and lead teams, professionally develop support, enhance operations, and manage information systems, communities, and solutions.</p>
<p>As a developer advocate, I've spent the last years working with outstanding teams, building great developer and API products. This developer relations career portfolio will show you why I would be a valuable addition to your company.</p>
<h2 id="heading-what-i-can-offer">What I Can Offer</h2>
<p>Here are a few reasons that make me a great addition to your team:</p>
<ul>
<li><p>I'm keen on doing my best in any responsibility assigned to me. It's a personal principle; I always strive to do my best on any given task using all that is available to me and iterate deliverables based on feedback.</p>
</li>
<li><p>I enjoy teaching, learning new stuff, and sharing knowledge. This is the foundation of my developer relations/education strengths and my entire career.</p>
</li>
<li><p>I can adapt to new conditions, company goals/visions, go-to-market strategy requirements, and the overall business needs of your developer product.</p>
</li>
<li><p>I'm an honest person and don't tell lies. I won't tell developers that your product is the best in the world (unless proven), but I will take my time to understand the key strengths/benefits/limitations of your product and scream the benefits to developers at the top of my voice, show them how those benefits offer them value, help them get comfortable with building with your product, and build a trust relationship with them.</p>
</li>
<li><p>I have solid experience working with and leading developer communities since my teenage years, with some notable ones being <a target="_blank" href="https://github.com/fbdevelopercircles">Meta Developer Circles</a> (previously Facebook Developers Circles), <a target="_blank" href="https://education.github.com/experts">GitHub Education Campus Expert</a>, and <a target="_blank" href="https://oscafrica.org">Open Source Community Africa</a>.</p>
</li>
<li><p>I have a Bachelor's Degree in Computer Science. While this is usually not an essential determinant for most companies, it is valuable to me. I spent quite some years acquiring theoretical, practical, and research knowledge that impacts some of my current strengths and interests (especially around Human-Computer Interaction (HCI), Data Analysis, Machine Learning, Electronics, and Embedded Systems).</p>
</li>
<li><p>Aside from my years of solid technical writing experience:</p>
<ul>
<li><p>I have some experience with academic research writing and published my first paper on <a target="_blank" href="https://ieeexplore.ieee.org/document/10051461">Performance and Accessibility Evaluation of University Websites in Nigeria</a> in the 2022 5th Information Technology for Education and Development (ITED). I'm currently working on publishing my second research paper for my undergraduate dissertation.</p>
</li>
<li><p>I also actively contribute technical writing efforts to <a target="_blank" href="https://thegooddocsproject.dev">The Good Docs Project</a>, where best practice templates and writing instructions for documenting OSS projects are created. I worked last on the <a target="_blank" href="https://gitlab.com/tgdp/templates/-/tree/main/installation-guide">Installation Guide Template</a> alongside a partner in the <a target="_blank" href="https://gitlab.com/tgdp/templates/-/releases/v1.1.0">Capilano</a> and <a target="_blank" href="https://gitlab.com/tgdp/templates/-/releases/v1.1.0">Dragon</a> release, and I'm currently working on a new API Use Cases Template.</p>
</li>
</ul>
</li>
<li><p>I'm mostly humorous, and people enjoy working with me :).</p>
</li>
</ul>
<hr />
<p>My key strengths are:</p>
<ul>
<li><p>Writing code for things that work on the web using JavaScript, TypeScript, Reactjs, Nextjs, Python, Flask, and other frameworks.</p>
</li>
<li><p>Writing code for things that work on embedded systems using Python, OpenCV, and other related libraries.</p>
</li>
<li><p>API documentation.</p>
</li>
<li><p>Technical content creation.</p>
</li>
<li><p>Content production/marketing.</p>
</li>
<li><p>Community/programs management.</p>
</li>
<li><p>Data analysis and reporting.</p>
</li>
<li><p>Developer events organization.</p>
</li>
<li><p>Educational programs management.</p>
</li>
<li><p>Educational curriculum development.</p>
</li>
<li><p>Memes collection and efficient distribution :).</p>
</li>
</ul>
<hr />
<p>As the next member of your team, I will bring my software engineering and developer relations experience to:</p>
<ul>
<li><p>Build trust relationships with developers and customers using your product.</p>
</li>
<li><p>Stand as a mediator between your product and developers using the products—helping them be successful with your product offerings.</p>
</li>
<li><p>Talk to your prospective technical or non-technical customers in different engineering teams and help them understand the value of your product.</p>
</li>
<li><p>Build tools, code demos, integrations, content, educational materials, and documentation to ensure developers can fully utilize your products.</p>
</li>
<li><p>Work with strategic partners to show the power of your product when composed with other tools and services developers love (including both open-sourced and commercial) by building brilliant/creative integration ideas and templates.</p>
</li>
<li><p>Educate developers on your product via open-source, public speaking/technical workshops at conferences or local developer meetups, documentation, and published tutorials/videos.</p>
</li>
<li><p>Lead conversations around the latest technology advancements and best practices around and outside your product in the developer community at in-person events, online events, or social forums.</p>
</li>
<li><p>Build, grow, and nurture a community of developers who use your product.</p>
</li>
<li><p>Work closely and collaborate with your engineering, product, marketing, sales, customer success, and other existing internal teams to improve your product's developer experience.</p>
</li>
<li><p>Test your new product features and give early feedback that will aid improvements.</p>
</li>
<li><p>Gather feedback from the developer community on old/new product features for further planning, prioritization, and shipping.</p>
</li>
<li><p>Track and measure the success and failure of the activities and responsibilities you assign to me, showing the business value of developer relations at your company.</p>
</li>
</ul>
<hr />
<h2 id="heading-devrel-career-achievements">DevRel Career Achievements</h2>
<p>Here are some notable and publicly available samples of my DevRel, content creation, open-source work at my previous companies, non-profit educational commitments, and community engagements.</p>
<ul>
<li><p>All technical writing content: https://blog.bolajiayodeji.com.</p>
</li>
<li><p>Archive of my weekly newsletter: https://bawd.bolajiayodeji.com.</p>
</li>
<li><p>All public speaking slides: https://slides.com/bolajiayodeji.</p>
</li>
<li><p>All writings on freeCodeCamp: https://freecodecamp.org/news/author/bolajiayodeji.</p>
</li>
<li><p>All writings on opensource․com: https://opensource.com/users/bolajiayodeji.</p>
</li>
</ul>
<hr />
<h3 id="heading-code-and-integration-samples">Code and Integration Samples</h3>
<ul>
<li><p>You can find all my code, documentation, and other contributions to personal and external OSS projects on <a target="_blank" href="https://github.com/BolajiAyodeji">GitHub</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/BolajiAyodeji/chat-with-siri">chat-with-siri</a> — A text-to-speech audio chatbot built using Nextjs, OpenAI, and ElevenLabs. I worked on the code, documentation, and supporting tutorial to show how developers can leverage generative AI models to provide LLM audio chat solutions to consumers.</p>
</li>
<li><p><a target="_blank" href="https://github.com/commercelayer/commercelayer-sanity-template">commercelayer-sanity-template</a> — A multi-country ecommerce template built using Commerce Layer API, JavaScript, TypeScript, Nextjs, and Sanity Studio. In my previous role, I worked on the code and documentation to show the strengths of Commerce Layer's API when composed with a headless CMS for managing both commerce and content data in an ecommerce store. I worked on the first version of the project alongside a colleague (Ale!) and began maintaining the project afterwards to ensure it was up-to-date. My recent contributions were:</p>
<ul>
<li><p>Refactoring the components and UI logic, implementing the use of a single locale for product slugs, code cleaning, and migrating to the <code>pnpm</code> package manager in <a target="_blank" href="https://github.com/commercelayer/commercelayer-sanity-template/pull/74">PR #74</a>.</p>
</li>
<li><p>Upgrading to the latest Sanity version (v3) and Nextjs 13, fixing all breaking changes, updating the groq queries to use <code>createClient</code> in next-sanity, adding French translations for i18n, and introducing schemas type-checking with TypeScript in <a target="_blank" href="https://github.com/commercelayer/commercelayer-sanity-template/pull/67">PR #67</a>.</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://github.com/commercelayer/examples/tree/main/cms/nextjs-contentful-store">nextjs-contentful-store</a> — A multi-country ecommerce store built using Commerce Layer API, JavaScript, TypeScript, Nextjs, and Contentful. I worked on the first version of the project and documentation. I improved the initial sanity-template code here, introducing a new cart implementation using Commerce Layer's hosted-cart and other additions in <a target="_blank" href="https://github.com/commercelayer/examples/pull/5">PR #5</a>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/commercelayer/examples/blob/main/solutions/pay-with-tweet">pay-with-tweet</a> — A demo external payment gateway that enables customers to pay for an order by just making a tweet without any cash transaction built using Commerce Layer API, Twitter API, JavaScript, TypeScript, and Nodejs. I worked on the code, documentation, and supporting tutorial to show the strengths and flexibility of Commerce Layer's external payment gateway.</p>
</li>
<li><p><a target="_blank" href="https://github.com/commercelayer/examples/blob/main/solutions/commercelayer-slackbot">commercelayer-slackbot</a> — A Slackbot that responds with orders/returns summaries upon request and allows users to checkout pending orders directly from a Slack channel built using Commerce Layer API, JavaScript, TypeScript, Slack Boltjs library, and Supabase. I worked on the code, documentation, and supporting tutorial.</p>
</li>
<li><p><a target="_blank" href="https://github.com/commercelayer/examples/blob/main/webhooks/sendgrid-templated-emails">sendgrid-templated-emails</a> — A Nodejs script for sending templated order confirmation (or any type) emails to customers using Commerce Layer Webhooks, SendGrid API, and Zapier Code Runner. I worked on the code, documentation, and supporting tutorial.</p>
</li>
<li><p><a target="_blank" href="https://github.com/commercelayer/examples/blob/main/webhooks/twilio-sms-notification">twilio-sms-notification</a> — A Nodejs script for sending an SMS notification to customers when an SKU that had finished is back in stock using Commerce Layer Webhooks and Twilio SMS API. I worked on the code, documentation, and supporting tutorial.</p>
</li>
</ul>
<hr />
<h3 id="heading-content-and-documentation-samples">Content and Documentation Samples</h3>
<ul>
<li><p><a target="_blank" href="https://docs.commercelayer.io/core/readme/manual-configuration">A step-by-step manual configuration CLI reference guide</a>.</p>
</li>
<li><p><a target="_blank" href="https://commercelayer.io/docs/core-concepts">Commerce Layer Core Concepts</a>.</p>
</li>
<li><p><a target="_blank" href="https://commercelayer.io/docs/data-model">Commerce Layer Data Model</a>.</p>
</li>
<li><p><a target="_blank" href="https://commercelayer.io/blog/how-we-built-the-commerce-layer-slackbot-with-node-js-and-slack-api">How to build a Commerce Layer Slackbot with Node.js and Slack API</a>.</p>
</li>
<li><p><a target="_blank" href="https://commercelayer.io/blog/building-an-external-payment-gateway-with-twitter-api">Building an external payment gateway with the Twitter API</a>.</p>
</li>
<li><p><a target="_blank" href="https://commercelayer.io/blog/a-comprehensive-guide-to-commerce-layer-webhooks">A comprehensive guide to Commerce Layer webhooks</a>.</p>
</li>
<li><p><a target="_blank" href="https://commercelayer.io/blog/how-to-send-templated-emails-with-commerce-layer-and-sendgrid">How to send templated emails with Commerce Layer and SendGrid</a>.</p>
</li>
<li><p><a target="_blank" href="https://hashnode.com/series/she-inspires-cjt0d02lq001e7ps2wo420g15">She Inspires Series on Hashnode</a>.</p>
</li>
<li><p>Product overview video recording: <a target="_blank" href="https://youtu.be/LdFHjM-m8fo">Introducing Commerce Layer Postman Collection</a></p>
</li>
</ul>
<hr />
<h3 id="heading-teachingworkshopspodcastpublic-speaking">Teaching/Workshops/Podcast/Public Speaking</h3>
<ul>
<li><p>👨🏾‍🏫 Deploying Machine Learning Models to the Web (<a target="_blank" href="https://github.com/BolajiAyodeji/deploy-ml-web-workshop">workshop material</a>).</p>
</li>
<li><p>👨🏾‍🏫 Building an Ecommerce Solution with Commerce Layer Demo Stores (<a target="_blank" href="https://github.com/BolajiAyodeji/cl-composable-commerce-workshop">workshop material</a>).</p>
</li>
<li><p>👨🏾‍🏫 Anatomy of the Web and Version Control (<a target="_blank" href="https://slides.com/bolajiayodeji/anatomy-of-the-web-and-version-control">workshop slides</a>).</p>
</li>
<li><p>👨🏾‍🏫 Version Control with Git and GitHub (<a target="_blank" href="https://slides.com/bolajiayodeji/version-control-with-git-and-github">workshop slides</a> | <a target="_blank" href="https://www.youtube.com/watch?v=bWZwY6--h_I">video</a>).</p>
</li>
<li><p>🎤 Effective Documentation: The Key to Open Source Growth (<a target="_blank" href="https://slides.com/bolajiayodeji/effective-oss-docs">slides</a> | <a target="_blank" href="https://www.youtube.com/live/eHJZFB7IYWU?feature=share&amp;t=1262">video</a>).</p>
</li>
<li><p>🎤 What is Headless eCommerce? feat. Bolaji Ayodeji - Effective Product Development #0005 (<a target="_blank" href="https://youtu.be/bRlysrKAtek&amp;t=25">video</a>).</p>
</li>
<li><p>🎤 This Dot Media State of Web Performance 2/22/22 (<a target="_blank" href="https://www.youtube.com/watch?v=yMbUJtA5BVU">video</a>).</p>
</li>
<li><p>🎤 Building Composable Commerce on the Cloud for Emerging Markets (<a target="_blank" href="https://slides.com/bolajiayodeji/building-composable-commerce-on-the-cloud-for-emerging-markets">slides</a>).</p>
</li>
<li><p>🎤 Headless Commerce done right, with Commerce Layer (<a target="_blank" href="https://slides.com/bolajiayodeji/headless-commerce-done-right-with-commerce-layer">slides</a> | <a target="_blank" href="https://youtu.be/lOsprXNBrRc">video</a>).</p>
</li>
<li><p>🎙️ Sustain OSS Episode 131: Bolaji Ayodeji on Open Source Community Africa (<a target="_blank" href="https://podcast.sustainoss.org/131">podcast</a>).</p>
</li>
<li><p>🎙️ Web Standards and Future Trends: Staying Ahead in the Evolving Web Landscape (<a target="_blank" href="https://podcasters.spotify.com/pod/show/favfront-end-dev/episodes/Web-Standards-and-Future-Trends-Staying-Ahead-in-the-Evolving-Web-Landscape-with-Bolaji-Ayodeji-e276atn/a-a5g0o6">podcast</a>).</p>
</li>
</ul>
<hr />
<h3 id="heading-courses-and-bootcamps">Courses and BootCamps</h3>
<p>I organized a <a target="_blank" href="https://tau.edu.ng/news-page.php?i=75&amp;a=tau-taico-to-host-maiden-software-engineering-bootcamp">Software Engineering BootCamp</a> at <a target="_blank" href="https://tau.edu.ng">Thomas Adewumi University</a> in 2022 for their university and high school students that introduced them to key areas of Software Engineering, such as Data Science, Machine Learning, and Full-stack Web Development. Aside from organizing and managing the entire BootCamp/Curriculum, I taught the Anatomy of the Web and Version Control class.</p>
<p>Although not publicly visible (only visible to registered students), I partnered with <a target="_blank" href="https://altschoolafrica.com">AltSchool Africa</a> in the past to teach an <strong>Introduction to Open Source and Version Control with Git and GitHub</strong> course for their <a target="_blank" href="https://altschoolafrica.com/schools/engineering">School of Engineering</a> Class of 22” and 23". I have since received good feedback from students who took the course (most of which were verbal, but you can find some written ones below).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692719734881/7d24e5a5-ff55-4d1e-9531-d58fbeb91243.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692719797813/f3d63025-b82a-4bde-99b5-f4a78b59b9ac.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-extra-community-and-open-source-contributions">Extra Community and Open Source Contributions</h3>
<p>I'm currently a core team member and the community manager for <a target="_blank" href="https://oscafrica.org">Open Source Community Africa</a>, a community for open source lovers, enthusiasts, advocates and experts within and across Africa with the sole aim of increasing the rate of credible contributions by African software developers, designers, writers and everyone involved in the sphere of technology to open source projects both locally and globally. I manage the digital community on Discord of 4000+ members, co-organizing and facilitating virtual community events, managing communications via newsletters and blog posts, preparing fiscal year budgets, preparing community/event data visualization/reports, facilitating community partnerships with companies like GitHub, Google OSPO, SustainOSS, Propel, TalentQL, etc., supporting community members to participate in OSS programs like Hacktoberfest, GSOC, GSOD, Outreachy, etc., and running the <a target="_blank" href="https://oscafrica.org/community">Chapters program</a> that spans 30+ chapters in three continents of Africa. Over the years, we have had commendable community growth and results of members contributing to OSS, creating OSS projects, and advancing their careers.</p>
<p>I have also been co-organizing the yearly <a target="_blank" href="https://festival.oscafrica.org">Open Source Festival</a> and <a target="_blank" href="https://festival.oscafrica.org/sustain-africa">Sustain Africa</a> with other core team and committee members for three years now. The festival is a conference that attracts student delegates, developers, designers and corporate organizations on a large scale with a series of talks, workshops, and awareness of open-sourced developer tools (the biggest of its kind in Africa and Nigeria). Sustain, on the other hand, is a one-day conversation for open-source maintainers and contributors where we talk about Sustainability—the sustainability of resources and the sustainability of its people, the comprehensive overview of how FOSS is developed, maintained, utilized with the aim of providing a roadmap for solving the cultural, financial, and institutional issues among open-source project maintainers. You can <a target="_blank" href="https://blog.oscafrica.org/sustain-africa-2023-community-report">read the 2023 community report</a> to learn more.</p>
<hr />
<h2 id="heading-testimonials">Testimonials</h2>
<p>Here's what a couple of my previous colleagues and industry leaders have to say about my work:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It has been my pleasure to know and collaborate with Bolaji for several years now, and to say he is a gem in the tech community would be an understatement. Bolaji stands out as someone who is not only a competent Developer Advocate but one also genuinely cares about uplifting and educating others. One of Bolaji's most striking qualities is his passion for sharing and teaching. As he learns, he shares that knowledge, ensuring that others too can benefit. His content, which spans a wide array of tech topics, is a testament to his versatility as a Developer Advocate. Apart from his technical acumen, what makes Bolaji an exceptional Developer Advocate is his kind and humble nature. He interacts with everyone with so much respect and humility, creating an environment of trust and collaboration. His track record, from his involvement with leading developer communities to his insightful technical writings, speaks for itself. For companies seeking a Developer Advocate or Developer Experience Engineer who will go the extra mile, invest wholeheartedly in community growth, and bridge the gap between product teams and developers with grace and expertise, Bolaji Ayodeji is the one. I am confident that he will continue to make significant positive impacts wherever his journey takes him, and you'd be fortunate to be a part of this story. — <a target="_blank" href="https://linkedin.com/in/angiejones">Angie Jones</a>, Global VP of Developer Relations at <a target="_blank" href="https://tbd.website">TBD at Block, Inc</a>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Bolaji has a natural passion for knowledge and a firm grasp of modern FE engineering tools and techniques. He is a self-starter and unique in his ability to confidently transfer knowledge and pen his thoughts in a coherent and succinct manner. I have always known him to be a hardworking and pleasant person to work with. Bolaji’s future is bright and I cannot wait to see all that he will accomplish. —<a target="_blank" href="https://linkedin.com/in/josephakintolayo/">Joseph Akintola</a>, Founder and CEO of <a target="_blank" href="https://deposits.inc">Deposits</a>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I got the opportunity to work alongside Bolaji at Hashnode and it has been an immense learning experience. Bolaji is passionate about what he does and deeply cares the values. He has always been supportive to the team members. He takes his responsibilities seriously and deliver the best result possible. He played a key role in the growth of Hashnode in past here. Personally, I have learned a lot about technical writing and content creation from him and I would always be grateful for that. —<a target="_blank" href="https://www.linkedin.com/in/iamshadmirza/">Shad Mirza</a>, Senior Software Engineer at <a target="_blank" href="https://hashnode.com">Hashnode</a>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I have had the pleasure of working with Bolaji for over 4 years on Open Source Community Africa and I can confidently say that he is one of the most hardworking individuals I have ever worked with. His work ethic is unparalleled, and he has an innate ability to take charge and lead tasks with ease. I have experienced Bolaji take on numerous challenges and handle them all with grace and efficiency. He is always quick to take the lead on new tasks and projects, and his ability to juggle multiple responsibilities is truly remarkable. Additionally, he has an incredible attention to detail and always ensures that everything he works on is of the highest quality. He is a true asset to any organization or company and I feel lucky to have had the opportunity to work with him for so long. — <a target="_blank" href="https://linkedin.com/in/peace-ojemeh-0b5bb2151">Peace Ojemeh</a>, Ecosystem Development at <a target="_blank" href="https://ethereum.org">Ethereum Foundation</a>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Bolaji is focused, driven and excited about his work. He is a life long learner and a joy to work with. If you need things done with diligence and care, Bolaji is the person to reach out to. —<a target="_blank" href="https://linkedin.com/in/hauwa-aguillard-24a386150">Hauwa Aguillard</a>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I rarely come across energetic talents who stand out like Bolaji. I had the pleasure of supervising Bolaji during his internship at Energy Detectors. His ability get things done fast while maintaining industry standard is unlike any I've seen before. I totally recommend working with him. —<a target="_blank" href="https://linkedin.com/in/salaudeenabdulrahman">Abdul Rahman</a>.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692715622837/714ebaec-881b-4d0e-a1ec-25c8d77f3767.jpeg" alt="An image of Bolaji giving a talk at a conference." class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-contact-me">Contact Me</h2>
<p>Please feel free to share any opportunity you find or referrals; I'd be happy to share my resume on demand. While I look forward to settling somewhere full-time, I'm open to doing short-term contract or consultation work for you or your company/project if your needs fit into any of my strengths. So, if you have anything for me, you can email me at mailtobolaji@gmail.com or DM me on <a target="_blank" href="https://linkedin.com/in/iambolajiayo">LinkedIn</a> or <a target="_blank" href="https://twitter.com/iambolajiayo">X (Twitter)</a>. Thank you 💙!</p>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693225003717/f63e77c6-c9a2-43f6-af3c-b936d945af49.jpeg" alt class="image--center mx-auto" /></p>
<p>:).</p>
]]></content:encoded></item><item><title><![CDATA[Building an Ecommerce Store with Nextjs and Commerce Layer Demo Store]]></title><description><![CDATA[Have you ever wanted to learn how to build an ecommerce website as a developer? Look no further! In this article, I’ll walk you through the process of building a sleek, modern, and user-friendly conference swag digital store using composable commerce...]]></description><link>https://blog.bolajiayodeji.com/building-an-ecommerce-store-with-nextjs-and-commerce-layer-demo-store</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/building-an-ecommerce-store-with-nextjs-and-commerce-layer-demo-store</guid><category><![CDATA[ecommerce]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Sat, 21 Jan 2023 09:04:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674291123702/f203f18b-e689-4009-a493-289f9e50a0bf.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever wanted to learn how to build an ecommerce website as a developer? Look no further! In this article, I’ll walk you through the process of building a sleek, modern, and user-friendly conference swag digital store using composable commerce APIs, micro-frontends, and the Commerce Layer Nextjs Demo Store project. With the help of this powerful tool, you'll be able to create a visually appealing and functional international ecommerce website that is optimized for success. I'll show you step-by-step how to set up your store, add commerce data, and deploy your website in no time. Let’s get started!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674278727743/bd1b0090-3a6f-4bc7-acfa-0dddedc2bd0d.png" alt="A screenshot of the final demo store" class="image--center mx-auto" /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ol>
<li>Some JavaScript, command-line, and API knowledge are required.</li>
<li>A working programming IDE (I use vscode; you can also try it).</li>
<li>A stable internet connection.</li>
<li>Optionally, you can study these <a target="_blank" href="https://commercelayer.io/docs/data-model">data models</a> to understand the key Commerce Layer API entities, their mutual relationships, and common usage.</li>
</ol>
<h2 id="heading-introduction-to-composable-commerce">Introduction to Composable Commerce</h2>
<p>Before we proceed, let’s understand what the concept “composable” means. Before now, ecommerce websites were built with a monolithic architecture where all components (frontend, backend, content, and commerce data) were coupled into one system. Over time, this approach posed different weaknesses related to speed, flexibility, scalability, security, modularity, etc. Most importantly, it became difficult for different sub-teams in an engineering team to effectively build in development and collaborate on the product—leading to a poor developer experience. All these weaknesses directly affect the sales and customer retention part of an ecommerce business, which shouldn’t be the case. Hence, there was the rise of a better approach that allows for an independent and agnostic setup for all teams (frontend, backend, content, etc.) and effective product development flow. This approach, called “composable commerce,” allowed for a better developer experience, more room for creativity and innovation from each team, more room for adapting to new market opportunities, and eventually, customer satisfaction, increased revenue, and customer retention.</p>
<p><a target="_blank" href="https://commercelayer.io/docs/core-concepts/composable-commerce">Composable commerce</a> allows developers to review, select, and utilize various available commerce solutions as required to satisfy specific business needs in the different sub-teams of the engineering team. Developers can compose their development stack by selecting best-of-breed tools from <a target="_blank" href="https://www.postman.com/explore/apis">API-first solutions</a>, headless solutions, composable microservices, or even microfrontends. If any of these terms are unfamiliar, you can do a quick Google search to learn more about them. Generally, a composable commerce solution includes:</p>
<ul>
<li>The front-end presentation layer decoupled from the backend logic.</li>
<li>Individual components (autonomously developed, deployed, and managed) targeted toward specific business requirements.</li>
<li>APIs connecting each of these individual components.</li>
<li>Integration with reliable API-first external services.</li>
<li>SAAS and cloud computing services.</li>
<li>Real-time data shared between applications with webhooks.</li>
<li>Possibilities to build an independent and agnostic internal application on top of the existing data and composable architecture.</li>
</ul>
<p>The beauty of the composable commerce architecture is that you can distribute your ecommerce service anywhere your customer wants them (either on mobile, web, smart devices, CLIs, etc.) so long the envisaged sales channel allows API mechanisms. For brands and developers, the options are numerous; for example, you can go as easy as making a static HTML website shoppable using a <a target="_blank" href="https://github.com/commercelayer/drop-in.js">dropin library</a> to power certain transactional functionalities or building a fully functional ecommerce website with commerce platforms like Commerce Layer and dev tools like the <a target="_blank" href="https://github.com/commercelayer/demo-store">Demo Store</a> project (which we will do in this article!).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674278790317/445ac719-7d50-4650-b79b-fe564e6193c0.png" alt="Illustration of an ecommerce website showing components powered by commerce APIs" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674289099875/2e9e1ebb-4676-4e9d-b10c-c01aae221090.jpeg" alt="Illustration showing components of a modern ecommerce store" class="image--center mx-auto" /></p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>The Demo Store project is a static ecommerce solution with no third-party services required, so you can easily tailor your own with different levels of customization. As such, even content is stored as JSON files. The Demo Store comes with:</p>
<ul>
<li>A full product catalog management with taxonomies and taxons.</li>
<li>Single product variants management.</li>
<li>Multi-language capabilities to make selling internationally easier.</li>
<li>A built-in search engine with a faceted search (filtered search).</li>
<li>An extensive set of features provided out of the box by Commerce Layer APIs (multiple currency price lists, inventory models that support multiple stock locations and fulfillment strategies, market-specific payment gateways, delivery options, carrier accounts, etc.).</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674278831081/5c52de74-9cba-498e-ae91-8cc6e8679378.png" alt="A screenshot of the demo store's product page" class="image--center mx-auto" /></p>
<p>In the sections below, I’d show you how to set up and configure your own ecommerce store using the demo store project. Ready? Let’s go!</p>
<h3 id="heading-get-your-api-credentials">Get your API Credentials</h3>
<p>The demo store project is powered by Commerce Layer— a composable commerce API for developers and brands that you can use to build an international ecommerce website to process millions of orders. All the amazing API resources, Nextjs, TypeScript, and other tools were used in building the Demo Store ecommerce project. You can explore these <a target="_blank" href="https://www.postman.com/commercelayer">Postman collections</a> to test specific resources and see how they all work. To get started, all requests to Commerce Layer API must be authenticated with an <a target="_blank" href="https://oauth.net/2/">OAuth2</a> bearer token. You need to execute an authorization flow using a valid application type as the client to get an access token. Each application type has its usage, authentication flow, and permissions. The application type can be authenticated with different authorization grant flows using the available authorization grant types as described in the table below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674278967406/ce08909f-2ddd-4b94-a158-efb235fcdcad.jpeg" alt="A screenshot of the application types table comparison" class="image--center mx-auto" /></p>
<p>Kindly create a Commerce Layer account <a target="_blank" href="https://dashboard.commercelayer.io/sign_up">here</a>, follow the onboarding tutorial to create an organization, and read <a target="_blank" href="https://docs.commercelayer.io/core/applications#creating-an-application">this guide</a> to learn how to create a <code>sales_channel</code> and <code>integration</code> application type. When you are done creating them, save your organization slug, base endpoint, and the generated <code>CLIENT_ID</code> and <code>clientSecret</code> credential—we will use this to configure the demo store and seed some demo data shortly.</p>
<h3 id="heading-install-and-configure-the-demo-store">Install and Configure the Demo Store</h3>
<p>The Demo Store project is a GitHub template that uses the <a target="_blank" href="https://github.com/commercelayer/demo-store-core">demo-store-core</a> repository as a <a target="_blank" href="https://git-scm.com/book/en/v2/Git-Tools-Submodules">git submodule</a>. This will retain the features, look, and feel of the default Demo Store. With this, you won't have to care about the whole source code but instead focus on your data/content and easily get free updates with almost no risk just by running the <code>git submodule update --remote &amp;&amp; npm install</code> command. Kindly follow the steps below to get started:</p>
<p>→ Click on the "Use this template" button from the <a target="_blank" href="https://github.com/commercelayer/demo-store">demo-store</a>'s homepage on GitHub, as seen in the screenshot below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674278992218/e5e84288-2fcd-4944-ac7f-698dc63cbccc.png" alt="A screenshot of the &quot;use template&quot; button on GitHub" class="image--center mx-auto" /></p>
<p>→ Run the following commands to clone the forked repository to your local computer and install the required dependencies:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;your-repository-url&gt; my-new-demo-store
<span class="hljs-built_in">cd</span> my-new-demo-store
git submodule update --init
npm install
</code></pre>
<p>→ Run the following commands to copy all the required environment variables and data files into your demo store:</p>
<pre><code class="lang-bash">cp -r ./demo-store-core/packages/website/data/json ./data/json

cp -r ./demo-store-core/packages/website/locales ./data/locales

cp -r ./demo-store-core/packages/website/config ./config
</code></pre>
<p>→ Create a <code>.env.local</code> file and add the following variables, including your previously created <code>sales_channel</code> application credentials (Client ID and Base endpoint):</p>
<pre><code class="lang-plaintext">SITE_URL=http://localhost:3000
NEXT_PUBLIC_CL_CLIENT_ID=coF7ofOKxTlbYfN...
NEXT_PUBLIC_CL_ENDPOINT=https://your-demo-store.commercelayer.io

NEXT_PUBLIC_JSON_DATA_FOLDER=../../../data/json/
NEXT_PUBLIC_LOCALES_DATA_FOLDER=../../../data/locales/
NEXT_PUBLIC_CONFIG_FOLDER=../../../config/
</code></pre>
<h3 id="heading-seed-organization-with-demo-data">Seed Organization with Demo Data</h3>
<p>To create an ecommerce store, you need to add some commerce resources to your organization. Suppose this is your first time doing something like this. In that case, you can easily seed your organization automatically with demo data and use that to learn about the key commerce resources required. When you’re ready, you can add the actual data for your ecommerce store. Kindly follow the steps below to seed your newly created Commerce Layer organization:</p>
<p>→ Install the <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli">Commerce Layer CLI</a> using the command (this will give you access to the <code>cl</code> command):</p>
<pre><code class="lang-bash">npm install -g @commercelayer/cli

or

yarn global add @commercelayer/cli
</code></pre>
<p>→ Install the <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli-plugin-seeder">seeder plugin</a> and the <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli-plugin-imports">imports plugin</a> using the command:</p>
<pre><code class="lang-bash">cl plugins:install seeder
cl plugins:install imports
</code></pre>
<p>→ Log into the CLI with your previously created <code>integration</code> application credentials like so:</p>
<pre><code class="lang-bash">cl applications:login \
  --clientId Oy5F2TbPYhOZsxy1tQd9ZVZ... \
  --clientSecret 1ZHNJUgn_1lh1mel06gGDqa... \
  --organization your-demo-store \
  --<span class="hljs-built_in">alias</span> cli-admin
</code></pre>
<p>→ Run the command below to populate your organization with all the resources you need to build a multi-market ecommerce with Commerce Layer (this includes markets, SKUs, prices, inventory, stock locations, etc.):</p>
<pre><code class="lang-bash">npm run seeder:seed -ws --if-present
</code></pre>
<p>→ If the command above fails with errors (usually because of unstable internet), you can seed your organization with sample data using the command (but this process will take a bit more time):</p>
<pre><code class="lang-bash">cl seeder:seed -b custom -n demo_store_full -u ./demo-store-core/packages/setup/
</code></pre>
<p>→ The <code>data/json/countries.json</code> file contains a list of available countries for your ecommerce store. Choose the countries you want to start with and replace all findings of <code>"market": xxxxx</code> with the valid market scopes from your organization (you can find these in the <code>sales_channel</code> application you created earlier).</p>
<p>→ Now, you can start the development server and visit <code>http://localhost:3000</code> to preview your ecommerce store like so:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674279015561/6807997e-256e-4798-a2c9-a41d037cc1a7.png" alt="A screenshot of the demo store's search page" class="image--center mx-auto" /></p>
<p>You must first select a country when you visit the homepage. When you select the United States, for example, you will be redirected to the <a target="_blank" href="http://localhost:3000/it-US/"><code>http://localhost:3000/en-US/</code></a> path, which will return a shopping experience from the United States market in Commerce Layer with English content translations. Changing the language to French, for example, will redirect to a <code>http://localhost:3000/fr-US</code> page. Changing the country to Italy will return a shopping experience from the Europe market in Commerce Layer and redirect to a <code>http://localhost:3000/fr-IT</code> page. You can play around with it for other countries and languages.</p>
<p>This is a fully-functional international ecommerce website with a multi-market setup, facet search, cart, and checkout in just a few minutes and fewer development efforts. Also, if you already have an existing CMS with content, you can still use that to generate the required content JSON file (we will show you how in a follow-up tutorial 😉). Now, you can go ahead and make transactional changes for each market in Commerce Layer, and your demo store will return the right experience to your users. Isn’t this cool?</p>
<p>In the next section, you will learn how to do more by customizing your store and adding more translations, pages, countries, and other commerce resources.</p>
<h3 id="heading-customizing-your-demo-store">Customizing your Demo Store</h3>
<p>The Demo Store project is built around three main data elements stored as JSON files to decouple them from any third-party services you might want to add yourself. To customize your store, you'll have to create and manage these files.</p>
<ol>
<li><p>The <strong>content</strong> JSON files are located in the <code>data/json/</code> directory with different structured text and image content (here is <a target="_blank" href="https://github.com/BolajiAyodeji/conf-swag-demo-store/tree/master/data/json">a sample</a> from the finished code).</p>
</li>
<li><p>The <strong>locale</strong> JSON files are located in the <code>data/locales/</code> directory with a collection of language translation files (here is <a target="_blank" href="https://github.com/BolajiAyodeji/conf-swag-demo-store/tree/master/data/locales">a sample</a> from the finished code with french and Italian translations).</p>
</li>
<li><p>The <strong>configuration</strong> files are located in the <code>config/</code> directory with some JS files that change the behavior of search facets and variants swatching for products with multiple variants (which in turn means the existence of multiple SKUs).</p>
</li>
</ol>
<p>When you are done with any change, always ensure to check that everything is correct by running the command:</p>
<pre><code class="lang-bash">npm run <span class="hljs-built_in">test</span>:data
</code></pre>
<p>You can look at <a target="_blank" href="https://github.com/BolajiAyodeji/conf-swag-demo-store">the finished code</a> to see all the extra pages and translation changes added to the demo conference swag ecommerce website. Alternatively, if you need to fully customize your store (behavior, UI, UX, etc.), then all you have to do is fork the <a target="_blank" href="https://github.com/commercelayer/demo-store-core">demo-store-core</a> repository, which contains the source code, follow the instructions to set things up locally and configure it as earlier discussed.</p>
<h2 id="heading-deploying-the-demo-store">Deploying the Demo Store</h2>
<p>There are two things you need to do before you deploy your store using your favorite hosting platforms (e.g., Netlify, Vercel, GitHub Pages, Cloudflare Pages, etc.):</p>
<ol>
<li><p>Add all the environment variables you currently have in <code>.env.local</code> to the deployment service you'll use to run the build.</p>
</li>
<li><p>Decide if you want to use static site generation (SSG) or server-side rendering (SSR) in production.</p>
</li>
</ol>
<p>The next sections will outline the steps required for each option.</p>
<h3 id="heading-static-site-generation-ssg">Static site generation (SSG)</h3>
<p>→ Add this in addition to other variables in your env file:</p>
<pre><code class="lang-plaintext">NEXT_PUBLIC_DATA_FETCHING=ssg
</code></pre>
<p>→ Run <code>npm run export</code> to create a static optimized production build of your application.</p>
<p>→ Copy the folder in <code>demo-store-core/packages/website/out</code> to your preferred static hosting.</p>
<h3 id="heading-server-side-rendering-ssr">Server-side rendering (SSR)</h3>
<p>→ Add this in addition to other variables in your env file:</p>
<pre><code class="lang-plaintext">NEXT_PUBLIC_DATA_FETCHING=ssr
</code></pre>
<p>→ Set the output directory in your deployment service to <code>demo-store-core/packages/website/.next</code>.</p>
<p>→ Set <code>npm run build</code> as your build command to create an optimized production build of your application.</p>
<h3 id="heading-cicd-deployment">CI/CD Deployment</h3>
<p>You can use the same steps and options if you set up CI/CD deployment directly from a GitHub or GitLab repository. For example, you can create a <code>netlify.toml</code> file for Netlify with SSG like so:</p>
<pre><code class="lang-toml"><span class="hljs-section">[build]</span>
  <span class="hljs-attr">command</span>=<span class="hljs-string">"npm run export"</span>
  <span class="hljs-attr">publish</span>=<span class="hljs-string">"demo-store-core/packages/website/out"</span>

<span class="hljs-section">[build.processing]</span>
  <span class="hljs-attr">skip_processing</span> = <span class="hljs-literal">true</span>

<span class="hljs-section">[build.environment]</span>
  <span class="hljs-attr">NEXT_PUBLIC_JSON_DATA_FOLDER</span>=<span class="hljs-string">"../../../data/json/"</span>
  <span class="hljs-attr">NEXT_PUBLIC_LOCALES_DATA_FOLDER</span>=<span class="hljs-string">"../../../data/locales/"</span>
  <span class="hljs-attr">NEXT_PUBLIC_CONFIG_FOLDER</span>=<span class="hljs-string">"../../../config/"</span>

<span class="hljs-section">[template.environment]</span>
  <span class="hljs-attr">SITE_URL</span>=<span class="hljs-string">"https://your-demo-store.netlify.app"</span>
  <span class="hljs-attr">NEXT_PUBLIC_CL_CLIENT_ID</span>=<span class="hljs-string">"coF7ofOKxTlbYfNNe..."</span>
  <span class="hljs-attr">NEXT_PUBLIC_CL_ENDPOINT</span>=<span class="hljs-string">"https://your-demo-store.commercelayer.io"</span>
</code></pre>
<p>Or a <code>vercel.json</code> file for Vercel with SSR like so:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"version"</span>: <span class="hljs-number">2</span>,
  <span class="hljs-attr">"buildCommand"</span>: <span class="hljs-string">"npm run build"</span>,
  <span class="hljs-attr">"outputDirectory"</span>: <span class="hljs-string">"demo-store-core/packages/website/.next"</span>,
  <span class="hljs-attr">"build"</span>: {
    <span class="hljs-attr">"env"</span>: {
      <span class="hljs-attr">"ENABLE_EXPERIMENTAL_COREPACK"</span>: <span class="hljs-string">"1"</span>,
      <span class="hljs-attr">"NEXT_PUBLIC_DATA_FETCHING"</span>: <span class="hljs-string">"ssr"</span>,
      <span class="hljs-attr">"NEXT_PUBLIC_JSON_DATA_FOLDER"</span>: <span class="hljs-string">"../../../data/json/"</span>,
      <span class="hljs-attr">"NEXT_PUBLIC_LOCALES_DATA_FOLDER"</span>: <span class="hljs-string">"../../../data/locales/"</span>,
      <span class="hljs-attr">"NEXT_PUBLIC_CONFIG_FOLDER"</span>: <span class="hljs-string">"../../../config/"</span>,
      <span class="hljs-attr">"SITE_URL"</span>: <span class="hljs-string">"https://conf-swag-demo-store.vercel.app"</span>,
      <span class="hljs-attr">"NEXT_PUBLIC_CL_CLIENT_ID"</span>: <span class="hljs-string">"coF7ofOKxTlbYfNNe..."</span>,
      <span class="hljs-attr">"NEXT_PUBLIC_CL_ENDPOINT"</span>: <span class="hljs-string">"https://your-demo-store.commercelayer.io"</span>
    }
  }
}
</code></pre>
<p>PS: If you use this option, please make sure that your Commerce Layer API credentials are private. The API credentials in the <code>netlify.toml</code> or <code>vercel.json</code> files should only be added to a private repository. You should always add API credentials directly to your deployment service. I added mine to the repository just for educational purposes.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>And that’s a wrap!</p>
<p>I hope you’ve found this tutorial useful and have learned how composable commerce and international websites work. Feel free to expand your knowledge by researching more, customizing your ecommerce store, integrating with other third-party services, adding more commerce data to your Commerce Layer organization, and exploring Commerce Layer’s other suite of <a target="_blank" href="https://commercelayer.io/developers">developer tools</a>. You can go ahead and use this project to start selling conference swags or any products and receive orders for free (courtesy of the Commerce Layer free <a target="_blank" href="https://commercelayer.io/pricing">Developer plan</a>) 😃.</p>
<p>I’d also love to see what you build with this, so please feel free to share what you’ve built and let me know what you think about the tutorial in the comment section or in <a target="_blank" href="https://commercelayer.io/developers">Commerce Layer's Slack community</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Introducing GitHub Community Health Files]]></title><description><![CDATA[Hey! This article is going to be very short and unstyled. My goal here is to introduce this feature I find very helpful to those who haven't heard of it and probably fill up the SEO space for those searching for it.
GitHub recently introduced a new f...]]></description><link>https://blog.bolajiayodeji.com/introducing-github-community-health-files</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/introducing-github-community-health-files</guid><category><![CDATA[GitHub]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Tue, 27 Sep 2022 02:56:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1664247184211/m5CrPGXXo.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey! This article is going to be very short and unstyled. My goal here is to introduce this feature I find very helpful to those who haven't heard of it and probably fill up the SEO space for those searching for it.</p>
<p>GitHub recently introduced a new feature that allows you to create default files like <code>CONTRIBUTING.md</code>, <code>CODE_OF_CONDUCT.md</code>, or even ISSUES templates in a public organization. These default files will be used for any repository owned by the organization that does not contain its file of that type. So if you have open-sourced projects in your company, project, or personal GitHub organization, then this is for you!</p>
<p>All you have to do is create a <code>.github</code> repository and put all the files and templates in it (here's a <a target="_blank" href="https://github.com/commercelayer/.github">perfect example</a>). Then, you can find all the supported files <a target="_blank" href="https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file#supported-file-types">here</a> in the GitHub docs. I've also attached a screenshot below to save you some time (but feel free to click the link above to learn more about how to set up health files properly on GitHub). Cheers! 💙</p>
<p><img src="https://i.imgur.com/fcNHOQp.png" alt="A screenshot of a section from GitHub docs" /></p>
]]></content:encoded></item><item><title><![CDATA[Contributing to Open Source Pocket Guide]]></title><description><![CDATA[Open Source is a flourishing and beneficial ecosystem that publicly solves problems in communities and industries using software through a decentralized model and community contributions. On the other hand, version control is an essential part of eve...]]></description><link>https://blog.bolajiayodeji.com/contributing-to-open-source-pocket-guide</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/contributing-to-open-source-pocket-guide</guid><category><![CDATA[Open Source]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[General Programming]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Sun, 11 Sep 2022 11:35:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662379950778/XGrt8pYE9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Open Source is a flourishing and beneficial ecosystem that publicly solves problems in communities and industries using software through a decentralized model and community contributions. On the other hand, version control is an essential part of everyday modern-day software engineering practices and is the core backbone of open-source. In this article, you will learn more about Open Source, Version Control with Git and GitHub, and how to contribute to projects effectively. This will be a quick overview pocket guide, and I'll try to mention the fundamental details you need while linking you to other external resources for further learning.</p>
<p>Let's roll!</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Basic programming knowledge</p>
</li>
<li><p>A working laptop running on any operating system</p>
</li>
<li><p>An IDE and required tools for programming</p>
</li>
<li><p>A smile on your face :)</p>
</li>
</ul>
<h2 id="heading-what-exactly-is-open-source">What Exactly is Open Source?</h2>
<p>According to <a target="_blank" href="https://en.wikipedia.org/wiki/Open-source_software">Wikipedia</a>, Open Source (also known as OSS) is: “The source code of a software application that is released under a license in which the copyright holder grants users the rights to use, study, change, and distribute the software and its source code to anyone and for any purpose.”</p>
<p>Basically, this is a public code base on the internet that is developed either collaboratively by multiple people or by one person. OSS allows users to adapt software to their personal needs and publish any modification for users with similar needs either on the original project or as another project.</p>
<h2 id="heading-why-open-source">Why Open Source?</h2>
<p>OSS is about collaborating with people from different regions, cultures, and technical backgrounds working remotely. This is why it is an essential practice for anyone working in tech to enable them to learn and build the collaboration skills needed to work effectively with software teams on software products.</p>
<p>Some benefits of contributing to open-source projects include:</p>
<ul>
<li><p>Opportunity to build technical work experience.</p>
</li>
<li><p>Opportunity to gain experience with collaborating with people.</p>
</li>
<li><p>Opportunity to build communication and soft skills.</p>
</li>
<li><p>Opportunity to gain domain-specific industry knowledge.</p>
</li>
<li><p>Possibility to earn money or certain financial rewards.</p>
</li>
<li><p>Access to mentorship and network opportunities.</p>
</li>
<li><p>Ultimately, career growth.</p>
</li>
</ul>
<h2 id="heading-fields-in-open-source">Fields in Open Source</h2>
<p>Here are several ways you can contribute to open-source with your existing skills:</p>
<h4 id="heading-1-software-engineering">1. Software Engineering</h4>
<ul>
<li><p>Writing code</p>
</li>
<li><p>Writing tests for code</p>
</li>
<li><p>Reviewing code</p>
</li>
<li><p>Fixing bugs</p>
</li>
<li><p>Beta testing</p>
</li>
<li><p>QA testing</p>
</li>
</ul>
<h4 id="heading-2-data-engineering">2. Data Engineering</h4>
<ul>
<li><p>Writing code</p>
</li>
<li><p>Data collection and cleaning</p>
</li>
<li><p>Data analysis</p>
</li>
<li><p>Data visualization</p>
</li>
<li><p>Research</p>
</li>
</ul>
<h4 id="heading-3-content-writing">3. Content Writing</h4>
<ul>
<li><p>Writing documentation</p>
</li>
<li><p>Writing tutorials</p>
</li>
<li><p>Writing translations</p>
</li>
<li><p>Writing newsletters</p>
</li>
<li><p>Creating or editing content</p>
</li>
</ul>
<h4 id="heading-4-design">4. Design</h4>
<ul>
<li><p>Designing designs 🥹</p>
</li>
<li><p>UX surveys and research</p>
</li>
<li><p>Creating styles guides</p>
</li>
<li><p>Producing brand materials</p>
</li>
</ul>
<h4 id="heading-5-marketing">5. Marketing</h4>
<ul>
<li><p>Promoting projects</p>
</li>
<li><p>Engaging users on the internet</p>
</li>
<li><p>Converting new users through certain means</p>
</li>
<li><p>Developing marketing strategies</p>
</li>
<li><p>Running Producthunt launches</p>
</li>
</ul>
<h4 id="heading-6-project-management">6. Project Management</h4>
<ul>
<li><p>Managing the development phases of the project</p>
</li>
<li><p>Managing the allocation of resources and funds</p>
</li>
<li><p>Tasks and issues organization</p>
</li>
<li><p>Tooling and automation</p>
</li>
</ul>
<h4 id="heading-7-community-management">7. Community Management</h4>
<ul>
<li><p>Managing people and contributors</p>
</li>
<li><p>Organizing events</p>
</li>
<li><p>Organizing programs</p>
</li>
<li><p>Enforcing the code of conduct</p>
</li>
</ul>
<h4 id="heading-8-reporting-bugs">8. Reporting bugs</h4>
<h4 id="heading-9-research">9. Research</h4>
<h4 id="heading-10-idea-suggestions">10. Idea suggestions</h4>
<h4 id="heading-11-funding">11. Funding</h4>
<h4 id="heading-12-et-cetera">12. Et. Cetera</h4>
<h2 id="heading-getting-started-with-open-source">Getting Started with Open Source</h2>
<p>The first thing you need to do is to understand what your skills and strength are. The fields in the OSS section should have given you ideas of certain things you can do. This is very important because it gives you the right perception and confidence about who you are and what you can do. Then you can use this knowledge to search for projects matching your skills. For example, Ruth has some intermediate experience with the JavaScript programing language, advanced understanding of the Python programing language, some technical writing experience, and can write some unit tests. With this, Ruth can contribute code to some JavaScript and Python projects based on the complexities of the tasks (offering her an opportunity to gain more experience) and can contribute to writing documentation for any project.</p>
<p>Next, you can decide to find some projects that you're interested in (this is optional but something you can consider and use as a guide to finding projects). For example, you might be interested in general developer toolings for a specific language, projects related to e-commerce, climate change, education, UI components, web performance, etc.</p>
<p>Once you have completed the first and second phases, it's time to find projects. Here are some places:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/showcases/great-for-new-contributors">GitHub Showcase</a>: A collection of projects with a history and reputation for being welcoming to new open source contributors.</p>
</li>
<li><p><a target="_blank" href="https://github.com/explore">GitHub Explore</a>: a recommendation of projects on GitHub based on your interests.</p>
</li>
<li><p><a target="_blank" href="https://github.com/topics">GitHub Topics</a>: a collection of projects on GitHub categorized based on popular topics.</p>
</li>
<li><p><a target="_blank" href="https://goodfirstissues.com">Good First Issues</a>: a collection of open issues on GitHub with the ability to filter by programming language, issue label, or repository.</p>
</li>
<li><p><a target="_blank" href="www.pullrequestroulette.com">Pull Request Roulette</a>: a list of pull requests submitted for review belonging to open-source projects hosted on Github.</p>
</li>
<li><p><a target="_blank" href="https://ovio.org/projects">Ovio Projects</a>: a curated list of projects and issues with the ability to filter by programming language, topic, and activity.</p>
</li>
<li><p><a target="_blank" href="https://codetriage.com">Code Triage</a>: a collection of different GitHub repositories to pick from, and you receive a different open issue in your inbox every day.</p>
</li>
<li><p><a target="_blank" href="https://up-for-grabs.net">Up For Grabs</a>: a curated list of projects which have curated tasks specifically for new contributors.</p>
</li>
<li><p><a target="_blank" href="https://24pullrequests.com">24 Pull Requests</a>: a program that allows you to find open-source projects to contribute to and gift them your contribution as a Christmas gift.</p>
</li>
</ul>
<p>...and lots more. With these, I believe you can begin finding projects to contribute to.</p>
<p>I'd recommend you check out the <a target="_blank" href="https://opensauced.pizza">Open Sauced</a> tool created by @bdougie. With this, you can find projects and manage your OSS contributors on a dashboard with project intelligence, community insights, and contributor analytics (this is very useful for maintainers too). You should also join the <a target="_blank" href="https://discord.gg/pRJgjH9SwR">Open Source Community Africa</a> to network and collaborate with over 3,200+ contributors and creators across Africa.</p>
<h2 id="heading-git-and-github">Git and GitHub</h2>
<p>Before we proceed to the main stuff, you will need some knowledge of version control to contribute to open-source. For some kinds of contributions, you might not need this, but it won't hurt to learn it once and for all. Version control is the process of tracking and managing changes to software code or a set of files over time. <a target="_blank" href="https://git-scm.com">Git</a> is the most widely used modern version control system in the world today. It's A distributed and actively maintained open source project initially developed in 2005 by Linus Torvalds (the famous creator of the Linux kernel). <a target="_blank" href="https://github.com">GitHub</a>, on the other hand, is a web-based hosting service for version control using Git that is mostly used for software code (there's also <a target="_blank" href="https://about.gitlab.com">GitLab</a> or <a target="_blank" href="https://bitbucket.org/product">Bitbucket</a> if you prefer those, but most OSS projects are hosted on GitHub). If you want to learn everything about Git from start to finish, then take <a target="_blank" href="https://www.youtube.com/watch?v=RGOj5yH7evk">this course</a>, read <a target="_blank" href="https://git-scm.com/docs">the docs</a>, or read <a target="_blank" href="https://git-scm.com/book/en/v2">the Pro Git book</a>. If you're a student at <a target="_blank" href="https://altschoolafrica.com/schools/engineering">AltSchool Africa</a>'s School of Engineering, you should have access to the Introduction to Open Source course I taught, which covers most of the foundational concepts of Git and all.</p>
<p>Anyways, download Git for your most preferred operating system <a target="_blank" href="https://git-scm.com/download">here</a>, configure it like so:</p>
<pre><code class="lang-bash">git config --global user.name <span class="hljs-string">"Bolaji Ayodeji"</span>
git config --global user.email &lt;your_email&gt;@gmail.com
git config --global init.defaultBranch main
</code></pre>
<p>Now you can use Git to manage your code locally for your projects as a practice (if you don't know how to use it already). It's nothing too serious; these are the basic commands you will often use while contributing to OSS projects:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;url&gt;
git add &lt;filename&gt; or git add * or git add . or git add --all
git status
git commit -m <span class="hljs-string">"commit description"</span>
git commit --amend
git push origin &lt;branch name&gt;
git push &lt;remote&gt; &lt;localBranch:&lt;remoteBranch&gt;
git push origin main or git push origin --set-upstream &lt;branch name&gt;
git <span class="hljs-built_in">log</span>
git pull
git branch -a
git branch staging
git checkout -b &lt;branch name&gt;
git checkout main
</code></pre>
<p>You can always run the <code>git help</code> command to learn more or <code>git help &lt;command name&gt;</code> to learn about a particular command, e.g., <code>git help branch</code>. As a bonus; if you’re a student (at any level), you can apply to get the <a target="_blank" href="https://education.github.com/discount_requests/student_application?utm_source=bolajiayodeji-content">GitHub Student Developer Pack</a> and access the best developer tools and courses for FREE.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662383280488/oK3-AHwr0.png" alt="Git help command response" /></p>
<h2 id="heading-the-anatomy-of-an-open-source-project">The Anatomy of an Open Source Project</h2>
<p>Generally, open source projects are built on four main components, the <strong>Source</strong> code, a <strong>Licence</strong>, <strong>Documentation</strong>, and <strong>People</strong>. These sets of people can further be broken into more parts:</p>
<ul>
<li><p><strong>Creator(s)</strong>: Those who created the project.</p>
</li>
<li><p><strong>Maintainer(s)</strong>: Those who actively manage the entire project.</p>
</li>
<li><p><strong>Contributor(s)</strong>: Those who contribute to the project (someone like you).</p>
</li>
<li><p><strong>Users</strong>: Those who use the project (developers or customers).</p>
</li>
<li><p><strong>Working group(s)</strong>: A collection of contributors split into domain-specific groups to focus on a discussion or activity around a specific subject area (e.g., testing group, DevOps group, documentation group, code reviews group, performance group, etc.).</p>
</li>
<li><p><strong>Sponsor(s)</strong>: Those who contribute to the project with financial support.</p>
</li>
</ul>
<hr />
<p>OSS projects usually include the following files (usually in text or markdown format) and documents:</p>
<ul>
<li><p><strong>License</strong>: the legal document that explains how and to what extent the project can be freely used, modified, and shared (<a target="_blank" href="https://github.com/commercelayer/commercelayer-microstore/blob/master/LICENSE">example</a>).</p>
</li>
<li><p><strong>Code of conduct</strong>: the document that outlines the rules, norms, acceptable practices, and responsibilities of anyone who decides to participate in the project in any way—including what happens when someone violates any of the rules (<a target="_blank" href="https://github.com/commercelayer/.github/blob/master/CODE_OF_CONDUCT.md">example</a>).</p>
</li>
<li><p><strong>README</strong>: the markdown file that displays under any repository on GitHub. This is usually the entry point to any project, so you might find the documentation here and links to other necessary documents. (<a target="_blank" href="https://github.com/oven-sh/bun/blob/main/README.md">example</a>).</p>
</li>
<li><p><strong>Documentation</strong>: the file that contains all documentation resources for the project, including guides, API references, tutorials, data models, etc. (<a target="_blank" href="https://reactjs.org/docs/getting-started.html">example</a>).</p>
</li>
<li><p><strong>Contributing docs</strong>: the document that explains how to get started with contributing to the project, including installation guides, configuration, etc. (<a target="_blank" href="https://github.com/googleapis/python-firestore/blob/main/CONTRIBUTING.rst">example</a>).</p>
</li>
<li><p><strong>Security</strong>: the file that explains how to submit vulnerability reports or any security issue (<a target="_blank" href="https://github.com/appsmithorg/appsmith/blob/release/SECURITY.md">example</a>).</p>
</li>
<li><p><strong>Issues</strong>: the board with a collection of tasks and bug reports awaiting someone like you to fix them (<a target="_blank" href="https://github.com/microsoft/TypeScript/issues">example</a>).</p>
</li>
<li><p><strong>Pull requests</strong>: the board with a collection of solutions submitted to fix certain issue(s) (<a target="_blank" href="https://github.com/brix/crypto-js/pulls">example</a>).</p>
</li>
<li><p><strong>Discussions</strong>: the place where maintainers, contributors, and users discuss on GitHub. Usually, you'd report bugs here or ask the community for help when you encounter an issue with the project (<a target="_blank" href="https://github.com/vercel/next.js/discussions">example</a>).</p>
</li>
</ul>
<p>And that's pretty much it. Most projects will also have a communication channel on maybe Discord or Slack for conversations and interactions between community members.</p>
<h2 id="heading-the-phases-of-contributing-to-open-source">The Phases of Contributing to Open Source</h2>
<p>Now you have some background knowledge, know how to use Git, have a GitHub account, and you have found some projects you would like to delve into. Here's what to do next in the exact order:</p>
<ol>
<li><p>Indicate interest under the issue you want to work on or send a message in the project chat communication channel to declare you want to work on XYZ. It's wise to do this to ensure someone else isn't working on the issue. If you don't get feedback quickly (different projects have different response rates depending on the number of requests), you can begin working on your solution <strong>slowly</strong> but try not to do anything serious till you get approval, or the issue is assigned to you.</p>
</li>
<li><p>Request for assignment to an issue.</p>
</li>
<li><p>Open an issue and clearly explain what you want to contribute, your thought process, and how you intend to build your solution. Try to wait for some feedback here in order to validate your idea against the requirements of preferences of the project owner (remember it isn't your project, the same way the company you work at isn't yours, so you can't make business decisions for them but can only suggest project features and all).</p>
</li>
<li><p>Read the documentation and contributing guidelines.</p>
</li>
<li><p>Fork the repository to your GitHub account.</p>
</li>
<li><p>Clone the forked repository to your local computer.</p>
</li>
<li><p>Set up locally following the contributing guidelines.</p>
</li>
<li><p>Create a new branch locally.</p>
</li>
<li><p>Work on your changes locally, versioning along the way.</p>
</li>
<li><p>Test your changes locally.</p>
</li>
<li><p>Push your branch and changes to your fork on your GitHub account.</p>
</li>
<li><p>Submit a pull request to the original repository and await feedback.</p>
</li>
<li><p>Review feedback, process the feedback, respond kindly, and make necessary changes.</p>
</li>
<li><p>Done! ✅</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We've come to the end of this guide, and I hope you find it helpful. This is a pocket guide you can keep close whenever you intend to contribute to a project, and you've forgotten something. If you're new to open-source, this can also be a good starting guide. Likewise, frequent contributors can use this guide to refresh their knowledge or fill up missing parts. In summary, this guide is for everyone, and I hope it helps you become a better open-source contributor. Cheers! 💙</p>
<h2 id="heading-useful-resources">Useful Resources</h2>
<ul>
<li><p><a target="_blank" href="https://training.github.com">Git Cheat Sheets</a> (in different languages)</p>
</li>
<li><p><a target="_blank" href="https://opensource.guide">Open Source Guides</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/oscafrica/awesome-open-source">A collection of recommended OSS resources, tools, and communities</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/collections/tools-for-open-source">Software to make running OSS projects a little bit easier</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Deploy a Machine Learning Model to the Web]]></title><description><![CDATA[One essential and last phase of the CRISP-DM data framework is deployment. The key focus in this phase is the usability of the developed model by intended users or customers. Depending on the type of solution and use case, this can involve deploying ...]]></description><link>https://blog.bolajiayodeji.com/how-to-deploy-a-machine-learning-model-to-the-web</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-deploy-a-machine-learning-model-to-the-web</guid><category><![CDATA[Python]]></category><category><![CDATA[Flask Framework]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Cloud Computing]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Sat, 03 Sep 2022 08:49:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662149984922/RmOWyTeB_.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One essential and last phase of the <a target="_blank" href="https://en.wikipedia.org/wiki/Cross-industry_standard_process_for_data_mining">CRISP-DM</a> data framework is <strong>deployment</strong>. The key focus in this phase is the usability of the developed model by intended users or customers. Depending on the type of solution and use case, this can involve deploying and integrating the model on any medium like the web, a mobile application, a hardware embedded system, etc. While this "sounds easy", many junior ML engineers find it daunting to deploy their projects on the web for their intended users to test and for their solutions to solve their users' problems.</p>
<p>In this tutorial, I will introduce model deployment by showing you the basic steps and processes of deploying a model on a web application built with Python and Flask. Then, I'll share an already-built model and show you how you can export yours (if you have any). We will build the app, integrate the model, and deploy it to Heroku with Git and GitHub. I'd also share some alternative tools and frameworks to consider based on your use case.</p>
<p>Sounds like something you want to learn? Then read on :)</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>A working computer running on any operating system.</li>
<li>Some HTML, CSS, JavaScript, and Python knowledge.</li>
<li>Basic knowledge of navigating around the command-line.</li>
<li>A smile on your face :)</li>
</ul>
<h2 id="heading-whats-a-machine-learning-model">What's a Machine Learning Model?</h2>
<p>A machine learning model is a file that has been trained using specific data to recognize certain types of similar patterns and make informed predictions. You provide a model with a set of data and train it with some algorithms. When you introduce a new set of data, the model will use learned knowledge to recognize the new data set. This is typically what happens with Face recognition security systems, where the device recognizes your face as a human face, not an object. Another example is a recommendation system, like in the demo I built for this tutorial. You will provide the model with multiple words associated with an output class (either positive or negative). The model will then learn from them so that when you provide a new sentence, the model can determine if it is positive or negative.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662060176395/usnmOCI3S.png" alt="Screenshot 2022-09-01 at 8.22.44 PM.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662060301439/erTr0KtJI.png" alt="Screenshot 2022-09-01 at 8.24.50 PM.png" /></p>
<p>There are three paradigms used to develop models, namely:</p>
<ul>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/Supervised_learning">Supervised Learning</a> (this is a type of ML where an algorithm is given a large input dataset with corresponding output or event/class, usually prepared in consultation with the subject matter domain expert).</li>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/Unsupervised_learning">Unsupervised Learning</a> (this is a type of ML where an algorithm is given some input dataset without the desired output or event/class and subject matter domain expert consultation).</li>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/Reinforcement_learning">Reinforcement Learning</a> (this is a type of ML algorithm that maps situations to actions that result in the highest possible reward).</li>
</ul>
<p>You can learn more by clicking on the links of each category.</p>
<h2 id="heading-building-a-machine-learning-model-with-python">Building a Machine Learning Model with Python</h2>
<p>You can build machine learning models with Python and other frameworks. To get started, I recommend you take <a target="_blank" href="https://developers.google.com/machine-learning/crash-course">this Machine Learning crash course</a> from Google that will teach you the fundamental machine learning concepts with a series of video lectures, real-world case studies, and hands-on practice exercises.</p>
<p><img src="https://developers.google.com/machine-learning/data-prep/images/5phases.svg" alt="Google ML Crash Course Phases" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1662042942757/Qw7NjiXE3.png" alt="Google ML Crash Course Overview" /></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://developers.google.com/machine-learning/crash-course">https://developers.google.com/machine-learning/crash-course</a></div>
<h2 id="heading-serializing-a-machine-learning-model">Serializing a Machine Learning Model</h2>
<blockquote>
<p>Serialization is the process of converting an object to a stream of bytes which is then stored or transmitted to memory or a file. You can learn more in <a target="_blank" href="https://en.wikipedia.org/wiki/Serialization">this Wikipedia article</a>.</p>
</blockquote>
<p>Now I'll assume you have learned how to build stuff with Machine Learning, or you have already created some model(s) that you would like to integrate with a user interface and deploy to the web. The first step you need to do is to export and save your model in a file format so you can use it while integrating with other technologies. Doing this will serialize your machine learning algorithms and save the serialized format to a file you can de-serialize later. There are several ways to do this based on the algorithms/frameworks used in developing your model or the requirements of your model. Some tools or file formats you can use include <a target="_blank" href="https://scikit-learn.org/stable/model_persistence.html">Pickle (Pkl format)</a>, <a target="_blank" href="https://joblib.readthedocs.io/en/latest/persistence.html">Joblib (Pkl or Joblib format)</a>, <a target="_blank" href="https://www.tensorflow.org/guide/keras/save_and_serialize">Tensorflow Keras (H5 or JSON format</a>), etc.</p>
<h2 id="heading-building-the-web-application-with-flask">Building the Web Application with Flask</h2>
<p><img src="https://github.com/BolajiAyodeji/movie_reviews_sentiment_analysis/blob/main/data/app_demo.gif?raw=true" alt="Web App Demo" /></p>
<p>I built a basic machine learning model to predict whether a movie review is positive or negative. I used four classification models (Support Vector Machine, Decision Tree, Naive Bayes, and Logistic Regression) and then evaluated each of them to find the one with the best accuracy. I then optimized the model with GridSearchCV and serialized it to a <code>.pkl</code> file using Pickle like so:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pickle

filename = <span class="hljs-string">'movie_reviews_sentiment_analysis.pkl'</span>

<span class="hljs-comment"># Save model (serialize)</span>
pickle.dump(svc_grid, open(filename, <span class="hljs-string">'wb'</span>))

<span class="hljs-comment"># Load model (de-serialize)</span>
pickle.load(open(filename, <span class="hljs-string">'rb'</span>))
</code></pre>
<p>You can view the notebook for the machine model here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/BolajiAyodeji/movie_reviews_sentiment_analysis/blob/main/model-notebook.ipynb">https://github.com/BolajiAyodeji/movie_reviews_sentiment_analysis/blob/main/model-notebook.ipynb</a></div>
<p>Now that we have a working model, let's build a basic web application using Flask, HTML, CSS, and some JavaScript in the following sections.</p>
<h3 id="heading-project-folder-directory-setup">Project Folder Directory Setup</h3>
<p>Create a new directory on your computer and create the following subfolders, files, and subfiles. If you want to use the same model and files I used, you can fetch everything from <a target="_blank" href="https://github.com/BolajiAyodeji/movie_reviews_sentiment_analysis">this repository</a> on GitHub.</p>
<pre><code class="lang-txt">├── data
  ├── dataset.csv (the dataset for your model)
  ├── &lt;any other required data files&gt;
├── webapp
  ├── model
     ├── movie_reviews_sentiment_analysis.pkl
     ├── vectorizer.pkl
  ├── static
     ├── main.css
     ├── main.js
  ├── templates
     ├── main.html
  ├── app.py
├── &lt;your model notebook&gt;.ipynb
</code></pre>
<h3 id="heading-flask-installation-and-setup">Flask Installation and Setup</h3>
<p><a target="_blank" href="https://github.com/pallets/flask">Flask</a> is a lightweight Python micro framework for building web applications (if you want to learn more about Flask, you can <a target="_blank" href="https://www.fullstackpython.com/flask.html">check this out</a>). To get started, we will install Python, Flask, and Pickle using PIP if you haven't already, like so:</p>
<pre><code class="lang-bash">pip install python3

pip install flask

pip install pickle
</code></pre>
<h3 id="heading-create-the-required-flask-templates">Create the Required Flask Templates</h3>
<p>Add the following markup code with some Tailwind styling to the <code>main.html</code> file in <code>webapp/templates</code> to create a form with a <code>&lt;select&gt;</code> field, a <code>&lt;textarea&gt;</code> field, and a submit button:</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"X-UA-Compatible"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"IE=edge"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Movie Reviews Sentiment Analysis<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span>
      <span class="hljs-attr">href</span>=<span class="hljs-string">"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"</span>
      <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
    /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ url_for('static',filename='main.css') }}"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-mono"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-center h-screen"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-12 max-w-lg bg-white rounded-lg overflow-hidden shadow-lg"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-bold text-xl mb-2"</span>&gt;</span>
          Movie Reviews Sentiment Analysis
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"review-form"</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"{{ url_for('main') }}"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"POST"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"block text-gray-700 text-sm font-bold mb-2"</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"movie"</span>&gt;</span>
            Select a movie:
          <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">select</span>
            <span class="hljs-attr">id</span>=<span class="hljs-string">"movie"</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"movie"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"block appearance-none w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"</span>
            <span class="hljs-attr">required</span>
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"The Good Doctor"</span>&gt;</span>The Good Doctor<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Money Heist"</span>&gt;</span>Money Heist<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Blacklist"</span>&gt;</span>Blacklist<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"The Return of Kibi"</span>&gt;</span>The Return of Kibi<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"The Imitation Game"</span>&gt;</span>The Imitation Game<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"The Dark Knight"</span>&gt;</span>The Dark Knight<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Blindspot"</span>&gt;</span>Blindspot<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Ozark"</span>&gt;</span>Ozark<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Vincenzo"</span>&gt;</span>Vincenzo<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">label</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"block text-gray-700 text-sm font-bold mb-2"</span>
            <span class="hljs-attr">for</span>=<span class="hljs-string">"review"</span>
          &gt;</span>
            Enter your review:
          <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"review"</span>
            <span class="hljs-attr">id</span>=<span class="hljs-string">"review"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"shadow appearance-none border rounded w-full py-2  px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"</span>
            <span class="hljs-attr">rows</span>=<span class="hljs-string">"5"</span>
            <span class="hljs-attr">required</span>
          &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
            <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>
            <span class="hljs-attr">value</span>=<span class="hljs-string">"Submit"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full bg-blue-500 text-white font-bold mt-8 p-4 rounded focus:outline-none focus:shadow-outline"</span>
          /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"result mt-12"</span> <span class="hljs-attr">align</span>=<span class="hljs-string">"center"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"predict_text"</span>&gt;</span>{{ predict_text }}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"selected_movie"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-gray-700"</span>&gt;</span>{{ movie }}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-blue-700 text-lg font-bold border rounded mt-4"</span>&gt;</span>
            {{ result }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>
      <span class="hljs-attr">src</span>=<span class="hljs-string">"{{ url_for('static',filename='main.js') }}"</span>
      <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span>
    &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Each input element has an ID with the name of the feature required by the model. The
form has a POST method set, and upon successful form submission, the data is sent to
<code>main.py</code> file for processing. The last section displays the results from the processing done in
<code>main.py</code>. The variables in <code>{{ }}</code> represents the dynamic data to be expected upon
successful form submission.</p>
<p>For some extra fancy background, add the following code to the <code>main.css</code> file in <code>webapp/static</code>:</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#4267b2</span>;
  <span class="hljs-attribute">background-image</span>: <span class="hljs-built_in">url</span>(<span class="hljs-string">"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' viewBox='0 0 192 192'%3E%3Cpath fill='%239C92AC' fill-opacity='0.4' d='M192 15v2a11 11 0 0 0-11 11c0 1.94 1.16 4.75 2.53 6.11l2.36 2.36a6.93 6.93 0 0 1 1.22 7.56l-.43.84a8.08 8.08 0 0 1-6.66 4.13H145v35.02a6.1 6.1 0 0 0 3.03 4.87l.84.43c1.58.79 4 .4 5.24-.85l2.36-2.36a12.04 12.04 0 0 1 7.51-3.11 13 13 0 1 1 .02 26 12 12 0 0 1-7.53-3.11l-2.36-2.36a4.93 4.93 0 0 0-5.24-.85l-.84.43a6.1 6.1 0 0 0-3.03 4.87V143h35.02a8.08 8.08 0 0 1 6.66 4.13l.43.84a6.91 6.91 0 0 1-1.22 7.56l-2.36 2.36A10.06 10.06 0 0 0 181 164a11 11 0 0 0 11 11v2a13 13 0 0 1-13-13 12 12 0 0 1 3.11-7.53l2.36-2.36a4.93 4.93 0 0 0 .85-5.24l-.43-.84a6.1 6.1 0 0 0-4.87-3.03H145v35.02a8.08 8.08 0 0 1-4.13 6.66l-.84.43a6.91 6.91 0 0 1-7.56-1.22l-2.36-2.36A10.06 10.06 0 0 0 124 181a11 11 0 0 0-11 11h-2a13 13 0 0 1 13-13c2.47 0 5.79 1.37 7.53 3.11l2.36 2.36a4.94 4.94 0 0 0 5.24.85l.84-.43a6.1 6.1 0 0 0 3.03-4.87V145h-35.02a8.08 8.08 0 0 1-6.66-4.13l-.43-.84a6.91 6.91 0 0 1 1.22-7.56l2.36-2.36A10.06 10.06 0 0 0 107 124a11 11 0 0 0-22 0c0 1.94 1.16 4.75 2.53 6.11l2.36 2.36a6.93 6.93 0 0 1 1.22 7.56l-.43.84a8.08 8.08 0 0 1-6.66 4.13H49v35.02a6.1 6.1 0 0 0 3.03 4.87l.84.43c1.58.79 4 .4 5.24-.85l2.36-2.36a12.04 12.04 0 0 1 7.51-3.11A13 13 0 0 1 81 192h-2a11 11 0 0 0-11-11c-1.94 0-4.75 1.16-6.11 2.53l-2.36 2.36a6.93 6.93 0 0 1-7.56 1.22l-.84-.43a8.08 8.08 0 0 1-4.13-6.66V145H11.98a6.1 6.1 0 0 0-4.87 3.03l-.43.84c-.79 1.58-.4 4 .85 5.24l2.36 2.36a12.04 12.04 0 0 1 3.11 7.51A13 13 0 0 1 0 177v-2a11 11 0 0 0 11-11c0-1.94-1.16-4.75-2.53-6.11l-2.36-2.36a6.93 6.93 0 0 1-1.22-7.56l.43-.84a8.08 8.08 0 0 1 6.66-4.13H47v-35.02a6.1 6.1 0 0 0-3.03-4.87l-.84-.43c-1.59-.8-4-.4-5.24.85l-2.36 2.36A12 12 0 0 1 28 109a13 13 0 1 1 0-26c2.47 0 5.79 1.37 7.53 3.11l2.36 2.36a4.94 4.94 0 0 0 5.24.85l.84-.43A6.1 6.1 0 0 0 47 84.02V49H11.98a8.08 8.08 0 0 1-6.66-4.13l-.43-.84a6.91 6.91 0 0 1 1.22-7.56l2.36-2.36A10.06 10.06 0 0 0 11 28 11 11 0 0 0 0 17v-2a13 13 0 0 1 13 13c0 2.47-1.37 5.79-3.11 7.53l-2.36 2.36a4.94 4.94 0 0 0-.85 5.24l.43.84A6.1 6.1 0 0 0 11.98 47H47V11.98a8.08 8.08 0 0 1 4.13-6.66l.84-.43a6.91 6.91 0 0 1 7.56 1.22l2.36 2.36A10.06 10.06 0 0 0 68 11 11 11 0 0 0 79 0h2a13 13 0 0 1-13 13 12 12 0 0 1-7.53-3.11l-2.36-2.36a4.93 4.93 0 0 0-5.24-.85l-.84.43A6.1 6.1 0 0 0 49 11.98V47h35.02a8.08 8.08 0 0 1 6.66 4.13l.43.84a6.91 6.91 0 0 1-1.22 7.56l-2.36 2.36A10.06 10.06 0 0 0 85 68a11 11 0 0 0 22 0c0-1.94-1.16-4.75-2.53-6.11l-2.36-2.36a6.93 6.93 0 0 1-1.22-7.56l.43-.84a8.08 8.08 0 0 1 6.66-4.13H143V11.98a6.1 6.1 0 0 0-3.03-4.87l-.84-.43c-1.59-.8-4-.4-5.24.85l-2.36 2.36A12 12 0 0 1 124 13a13 13 0 0 1-13-13h2a11 11 0 0 0 11 11c1.94 0 4.75-1.16 6.11-2.53l2.36-2.36a6.93 6.93 0 0 1 7.56-1.22l.84.43a8.08 8.08 0 0 1 4.13 6.66V47h35.02a6.1 6.1 0 0 0 4.87-3.03l.43-.84c.8-1.59.4-4-.85-5.24l-2.36-2.36A12 12 0 0 1 179 28a13 13 0 0 1 13-13zM84.02 143a6.1 6.1 0 0 0 4.87-3.03l.43-.84c.8-1.59.4-4-.85-5.24l-2.36-2.36A12 12 0 0 1 83 124a13 13 0 1 1 26 0c0 2.47-1.37 5.79-3.11 7.53l-2.36 2.36a4.94 4.94 0 0 0-.85 5.24l.43.84a6.1 6.1 0 0 0 4.87 3.03H143v-35.02a8.08 8.08 0 0 1 4.13-6.66l.84-.43a6.91 6.91 0 0 1 7.56 1.22l2.36 2.36A10.06 10.06 0 0 0 164 107a11 11 0 0 0 0-22c-1.94 0-4.75 1.16-6.11 2.53l-2.36 2.36a6.93 6.93 0 0 1-7.56 1.22l-.84-.43a8.08 8.08 0 0 1-4.13-6.66V49h-35.02a6.1 6.1 0 0 0-4.87 3.03l-.43.84c-.79 1.58-.4 4 .85 5.24l2.36 2.36a12.04 12.04 0 0 1 3.11 7.51A13 13 0 1 1 83 68a12 12 0 0 1 3.11-7.53l2.36-2.36a4.93 4.93 0 0 0 .85-5.24l-.43-.84A6.1 6.1 0 0 0 84.02 49H49v35.02a8.08 8.08 0 0 1-4.13 6.66l-.84.43a6.91 6.91 0 0 1-7.56-1.22l-2.36-2.36A10.06 10.06 0 0 0 28 85a11 11 0 0 0 0 22c1.94 0 4.75-1.16 6.11-2.53l2.36-2.36a6.93 6.93 0 0 1 7.56-1.22l.84.43a8.08 8.08 0 0 1 4.13 6.66V143h35.02z'%3E%3C/path%3E%3C/svg%3E"</span>);
}
</code></pre>
<p>Now, add the code below to the <code>main.js</code> file in <code>webapp/static</code>. This will prevent the form
from storing state from the previous form sessions:</p>
<pre><code class="lang-js"><span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.history.replaceState) {
  <span class="hljs-built_in">window</span>.history.replaceState(<span class="hljs-literal">null</span>, <span class="hljs-literal">null</span>, <span class="hljs-built_in">window</span>.location.href);
}
</code></pre>
<p>Now you can use the command <code>flask run</code> to start the Flask server on <code>http://127.0.0.1:5000</code> to preview the UI you have developed so far (PS: ensure you run the command inside the <code>webapp</code> directory).</p>
<h3 id="heading-model-integration-with-python-and-flask">Model Integration with Python and Flask</h3>
<p>In the <code>main.py</code> file located in <code>webapp</code>, add the following Python code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> flask
<span class="hljs-keyword">import</span> pickle
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd

<span class="hljs-comment"># Use pickle to load in the pre-trained model.</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">f'model/movie_reviews_sentiment_analysis.pkl'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> f:
    model = pickle.load(f)

<span class="hljs-comment"># Use pickle to load in vectorizer.</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">f'model/vectorizer.pkl'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> f:
    vectorizer = pickle.load(f)

app = flask.Flask(__name__, template_folder=<span class="hljs-string">'templates'</span>)

<span class="hljs-meta">@app.route('/', methods=['GET', 'POST'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-keyword">if</span> flask.request.method == <span class="hljs-string">'GET'</span>:
        <span class="hljs-keyword">return</span>(flask.render_template(<span class="hljs-string">'main.html'</span>))

    <span class="hljs-keyword">if</span> flask.request.method == <span class="hljs-string">'POST'</span>:
        review = flask.request.form[<span class="hljs-string">'review'</span>]
        predict_text = <span class="hljs-string">"Prediction sentiment for movie: "</span>
        movie = flask.request.form.get(<span class="hljs-string">"movie"</span>)
        prediction = model.predict(vectorizer.transform([review]))
        <span class="hljs-keyword">return</span>(flask.render_template(<span class="hljs-string">'main.html'</span>, predict_text=predict_text, movie=movie, result=prediction))

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    app.run()
</code></pre>
<p>This code will import the pre-trained model and create a new route with the desired request methods (GET and POST). Then, inside the <code>main()</code> function, we will write the functionality for both requests, make predictions based on the input variables and return the result values to the <code>/</code> route (homepage). With this, our previously developed model can now take in new data from the input fields and return output data.</p>
<h2 id="heading-deploying-to-the-cloud">Deploying to the Cloud</h2>
<p>Now, the most crucial part of this tutorial. How do we make the web application we have built accessible to anyone on the web? Well, we deploy it to the web using cloud services. For example, you can deploy to Heroku by following the steps below:</p>
<h4 id="heading-1-set-up-git-in-your-working-directory-and-push-your-code-to-a-hosting-service-like-githubhttpsgithubcom">1. Set up Git in your working directory and push your code to a hosting service like <a target="_blank" href="https://github.com">GitHub</a>.</h4>
<h4 id="heading-2-download-and-install-the-heroku-cli-using-the-command">2. Download and install the Heroku CLI using the command:</h4>
<pre><code class="lang-bash">brew tap heroku/brew &amp;&amp; brew install heroku

//or

curl https://cli-assets.heroku.com/install.sh | sh
</code></pre>
<p>You can download for Windows or use other methods by reading <a target="_blank" href="https://devcenter.heroku.com/articles/heroku-cli">the official docs</a>.</p>
<h4 id="heading-3-if-you-havent-already-log-in-to-your-heroku-account-and-follow-the-prompts-to-create-a-new-ssh-public-key-using-the-command">3. If you haven't already, log in to your Heroku account and follow the prompts to create a new SSH public key using the command:</h4>
<pre><code class="lang-bash">heroku login
</code></pre>
<h4 id="heading-4-create-a-procfile-and-add-the-following">4. Create a <code>Procfile</code> and add the following:</h4>
<pre><code>web: gunicorn app:app
</code></pre><h4 id="heading-5-create-a-requirementstxt-file-in-the-webapp-directory-and-add-the-following">5. Create a <code>requirements.txt</code> file in the <code>webapp</code> directory and add the following:</h4>
<pre><code class="lang-txt">flask==2.0.1
pandas==1.2.4
sklearn==0.0
gunicorn==20.1.0
</code></pre>
<h4 id="heading-6-deploy-your-application-to-heroku-with-git-using-the-commands-below">6. Deploy your application to Heroku with Git using the commands below:</h4>
<pre><code class="lang-bash">git add .
git commit -m <span class="hljs-string">"deploy to heroku"</span>
git push heroku main
</code></pre>
<p>And that's pretty much it! You can explore the demo using <a target="_blank" href="https://movie-reviews-sent-analysis.herokuapp.com">this link</a>. As you may already know, Heroku <a target="_blank" href="https://blog.heroku.com/next-chapter">is shutting down free dynos</a> soon, which means you'd have to pay some $$ to deploy to Heroku. If you want, you can consider alternatives, like <a target="_blank" href="https://clouddley.com">Clouddley</a>, <a target="_blank" href="https://railway.app">Railway</a>, <a target="_blank" href="https://fly.io">Fly</a>, <a target="_blank" href="https://www.deta.sh">Deta Cloud</a>, <a target="_blank" href="https://firebase.google.com">Firebase</a>, etc. All the alternatives have similar deployment steps using their CLIs; for example, you can <a target="_blank" href="https://fly.io/docs/getting-started/python">get up and running with Fly</a> with a few commands.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You can build a web application for your machine learning model using any web framework of your choosing, depending on the technology your model is built with. For Python, you can also consider other frameworks like Django, or if you're building with <a target="_blank" href="https://tensorflow.org/js">Tensorflow</a>—a library that lets you create, train, and use trained machine learning models in Javascript, you can use frameworks like Reactjs (web) or React Native (mobile) to deploy your stuff to your end users.</p>
<p>If you want to stick with Python completely, you can also build a REST API using <a target="_blank" href="https://fastapi.tiangolo.com/tutorial">Fast API</a> and consume that in your JavaScript, Reactjs, Vuejs, etc. application (like we just did). @Youngestdev wrote the perfect book for you on <a target="_blank" href="https://www.amazon.com/Building-Python-APIs-FastAPI-high-performance/dp/1801076634">how to build Python Web APIs with FastAPI</a>; you should get a copy and read it :winks:. Another exciting alternative is <a target="_blank" href="https://gradio.app">Gradio</a>—the fastest way to demo your machine learning model with a friendly web interface so anyone can use it anywhere. @abdulsamodazeez wrote an insightful piece around <a target="_blank" href="https://abdulsamodazeez.com/how-to-build-a-machine-learning-web-app-in-python-using-gradio">how to deploy to Gradio</a>; you should read it.</p>
<p>And that should be all! I hope you've learned something new or found a link to another resource that can help you become a better Machine Learning Engineer. You should use this tutorial as a guide to building a web app for your use case, so be sure to <a target="_blank" href="https://github.com/BolajiAyodeji/movie_reviews_sentiment_analysis">explore the code</a> anytime and rewrite it to suit your needs using your HTML, CSS, JavaScript, and Python knowledge. If you have any further questions, feel free to leave them in the comments, and I'll respond as soon as possible. Cheers! 💙</p>
]]></content:encoded></item><item><title><![CDATA[How to Setup Google Analytics 4 in a Next.js Project]]></title><description><![CDATA[Google Analytics is a web analytics service that tracks and reports several types of website traffic. GA4 (Google Analytics 4) was recently released, which Google claims is a new property designed for the future of website traffic measurement. Techni...]]></description><link>https://blog.bolajiayodeji.com/how-to-setup-google-analytics-4-in-a-nextjs-project</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-setup-google-analytics-4-in-a-nextjs-project</guid><category><![CDATA[Next.js]]></category><category><![CDATA[Google]]></category><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Thu, 23 Jun 2022 08:43:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655907318075/AU1-JAk9Z.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://analytics.google.com">Google Analytics</a> is a web analytics service that tracks and reports several types of website traffic. GA4 (Google Analytics 4) was recently released, which Google claims is a new property designed for the future of website traffic measurement. Technically the upgrade comes with better features and offerings that you can learn more about <a target="_blank" href="https://support.google.com/analytics/answer/10089681?hl=en">here</a>, and on 1st July 2023, the standard Universal Analytics (UA) properties <a target="_blank" href="https://support.google.com/analytics/answer/11583528">will be deprecated</a>. I recently had to set up GA4 on a Nextjs project and thought to document the steps here for myself and YOU. This will be a concise tutorial and I'd highlight the steps you need to set up the <code>gtag.js</code> script.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>You already know how to use Google Analytics.</li>
<li>You've built or you're building a project with Nextjs.</li>
<li>You already know some JavaScript and Reactjs.</li>
</ul>
<h2 id="heading-step-one">Step One</h2>
<p>Read <a target="_blank" href="https://support.google.com/analytics/answer/9744165?hl=en">this guide</a> from Google's documentation to create a new GA4 property and data stream. Once successfully, you should have a generated MEASUREMENT ID that looks like: <code>G-YQR79670LO</code>. In the next steps, you will use this to configure your Nextjs web progress to track user interactions on your website and send streams of data back to Google Analytics.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655905753296/mUHa1Qtxh.png" alt="A screenshot of my GA4 dashboard" /></p>
<h2 id="heading-step-two">Step Two</h2>
<p>Create a <code>.env</code> file and add the MEASUREMENT ID as an environment variable like so:</p>
<pre><code>NEXT_PUBLIC_MEASUREMENT_ID=<span class="hljs-string">"&lt;add_your_measurement_id_here&gt;"</span>
</code></pre><h2 id="heading-step-three">Step Three</h2>
<p>Open your <code>_app.js</code> or <code>_app.ts</code> or <code>_app.tsx</code> file in <code>/pages</code>, import the variable created earlier, and <a target="_blank" href="https://nextjs.org/docs/basic-features/script">Next Script</a> component (Nextjs's extension of the HTML <code>&lt;script&gt;</code> element) like so:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> Script <span class="hljs-keyword">from</span> <span class="hljs-string">"next/script"</span>;
<span class="hljs-keyword">const</span> GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_MEASUREMENT_ID;
</code></pre>
<h2 id="heading-step-four">Step Four</h2>
<p>Now add the code below after your <code>&lt;Head&gt;</code> component.</p>
<pre><code class="lang-js">&lt;Script
  src={<span class="hljs-string">`https://www.googletagmanager.com/gtag/js?id=<span class="hljs-subst">${GA_MEASUREMENT_ID}</span>`</span>}
  strategy=<span class="hljs-string">"afterInteractive"</span>
/&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Script</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"google-analytics"</span> <span class="hljs-attr">strategy</span>=<span class="hljs-string">"afterInteractive"</span>&gt;</span><span class="javascript">
  {<span class="hljs-string">`
    window.dataLayer = window.dataLayer || [];
    function gtag(){window.dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', '<span class="hljs-subst">${GA_MEASUREMENT_ID}</span>');
  `</span>}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">Script</span>&gt;</span></span>
</code></pre>
<p>The <code>afterInteractive</code> strategy on the Script component loads the script immediately after the page becomes interactive. Now you can deploy your changes to your desired host (maybe Netlify, Vercel, or Cloudfare Pages). Although, before deploying, ensure to add the environment variable (<code>NEXT_PUBLIC_MEASUREMENT_ID</code>) you created earlier in your deployment service. Most hosting services provide this option, so you can easily add variables via their dashboard. Once done, you should be good to go. It might take hours to reflect on your GA dashboard completely, but you should be getting hits already.</p>
<p>To confirm if you did everything right when you deployed your website, inspect the website's elements and you should see the script tags below at the bottom of the served HTML page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655965680635/GPCWP10rx.png" alt="Screenshot of some HTML tags on a website" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope you learned something and you found this article useful. Tracking user activities on your website is a great way to learn more about your users and how to serve them better or serve those you're not serving yet. In all, it's essential for all kinds of projects on the web. Cheers! 💙</p>
]]></content:encoded></item><item><title><![CDATA[How to Build an International Ecommerce Website with Sanity and Commerce Layer]]></title><description><![CDATA[One of the greatest benefits of composable, headless commerce is the flexibility it introduces to the developer experience of building shopping experiences. Decoupling website content and commerce data makes it easier for content and commerce teams o...]]></description><link>https://blog.bolajiayodeji.com/how-to-build-an-international-ecommerce-website-with-sanity-and-commerce-layer</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-build-an-international-ecommerce-website-with-sanity-and-commerce-layer</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[software development]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Mon, 11 Apr 2022 13:19:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649496604614/-cch6LsxT.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the greatest benefits of composable, headless commerce is the flexibility it introduces to the developer experience of building shopping experiences. Decoupling website content and commerce data makes it easier for content and commerce teams of experts to work independently and more efficiently. With Commerce Layer, content managers can work with a best-of-breed headless CMS like Sanity, merchants can build their inventory in Commerce Layer, and developers can build with any stack in their most preferred programming language while utilizing Commerce Layer’s APIs.</p>
<p>In this tutorial, you will learn how we built the <a target="_blank" href="https://github.com/commercelayer/sanity-template-commercelayer">Commerce Layer Starter</a> with Nextjs, Sanity studio, and deployed it to Netlify. At the end of the tutorial, you should be able to set up and build your own 1-click sanity starter with ease or integrate with Commerce Layer.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>Git installed (Learn how to install Git <a target="_blank" href="https://github.com/git-guides/install-git">here</a> if you haven't already).</li>
<li>Node and NPM installed (Learn how to install Nodejs <a target="_blank" href="https://nodejs.org/en/download/package-manager/">here</a> if you haven't already).</li>
<li>Basic knowledge of how to use the terminal.</li>
<li>Basic knowledge of NPM.</li>
<li>A grin on your face 😉.</li>
</ul>
<h2 id="heading-introduction-to-commerce-layer">Introduction to Commerce Layer</h2>
<p><a target="_blank" href="https://commercelayer.io">Commerce Layer</a> is a transactional commerce API and order management for international brands. It lets you make any digital experience shoppable, anywhere. You can build a multi-language website with Shopify, Contentful, WordPress, or any other CMS you already love. Then, add Commerce Layer for multi-currency prices, distributed inventory, localized payment gateways, promotions, orders, subscriptions, and more.</p>
<p>Unlike traditional solutions, <a target="_blank" href="https://commercelayer.io/why">Commerce Layer was built for the new era</a>. It natively supports the most modern development workflows, such as the Jamstack. Ecommerce businesses can integrate Commerce Layer with a single backend and serve on multiple presentation layers enabling them to build outstanding shopping experiences, go headless, and scale their business globally. You can check out our <a target="_blank" href="https://commercelayer.io/developers/">developer resources</a> to learn more and get started.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649777866598/IrP5NR4c0.png" alt="A screenshot of Commerce Layer's website homepage" /></p>
<h2 id="heading-introducing-sanity-studio">Introducing Sanity Studio</h2>
<p>The Sanity studio is an open-source content management system built with React.js. It offers rapid configuration, free form customization, reusable structured content, a comfortable editor, real-time collaboration, toolkits, plugins, and more features to enable you to create the best content workflow.</p>
<p>Sanity provides the possibility to create starter templates that can be re-used by developers easily. The starter is primarily a repository hosted on GitHub that contains some meta-information, demo content, schema, and frontend(s) that will end up in a new repository when a developer installs the starter through <a target="_blank" href="https://www.sanity.io/starters">sanity.io/starters</a>. When a developer installs the starter, Sanity creates a new project on Sanity and a new repository on GitHub with the starter code, attaches a new Sanity <code>datasetId</code> to the starter, and deploys the project to Netlify simultaneously.</p>
<p>Generally, a sanity starter can include a Sanity studio, a frontend application, both or multiple frontends and studios. For the purpose of this tutorial, we will create a starter that will include a studio and frontend. Our starter will include:</p>
<ul>
<li>An ecommerce storefront built with Nextjs and <a target="_blank" href="https://github.com/commercelayer/commercelayer-react-components">Commerce Layer react components library</a>.</li>
<li>International shopping capabilities powered by <a target="_blank" href="https://docs.commercelayer.io/developers/v/api-reference/">Commerce Layer APIs</a>.</li>
<li>Some ecommerce data imported using the <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli">Commerce Layer CLI</a>.</li>
<li>Structured content on Sanity studio.</li>
<li>Localization support.</li>
<li>Deployment configuration settings to Netlify.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649682532486/yZQ5D5vgy.png" alt="A GitHub repo cover image" /></p>
<h2 id="heading-sanity-starter-project-structure">Sanity Starter Project Structure</h2>
<p>Sanity has a defined specification for starters which includes some required files and directories. These specifications provide information about the starter to developers using the starter and make the project function as a reusable starter. Below is the folder structure of a Sanity project with required files and directories (without any frontend added):</p>
<pre><code class="lang-bash">├── .sanity-template
├── .sanity-template/assets
├── .sanity-template/data
├── .sanity-template/manifest.json
├── README.md
</code></pre>
<ul>
<li><strong><code>.sanity-template</code></strong> is the root directory where all meta-information for using this repository as a template on <a target="_blank" href="http://sanity.io/starters">sanity.io/starters</a> is stored.</li>
<li><strong><code>.sanity-template/assets</code></strong> is the directory for storing assets related to displaying information about the starter. In this case, preview images for the overall project and for each site the starter contains.</li>
<li><strong><code>.sanity-template/data</code></strong> is the directory to store a Sanity dataset export if you want the starter to launch with some demo content.</li>
<li><strong><code>.sanity-template/manifest.json</code></strong> is the JSON file containing details about the Starter as well as deployment information.</li>
<li><strong><code>README.md</code></strong> is the markdown file for this project that will be displayed on the Create page.</li>
</ul>
<p>For a finished starter project, the root of the project should contain all deployable code, including the frontend and studio. Generally, a project spun from a starter is split into three parts:</p>
<ol>
<li>The root for all frontend code</li>
<li>The <code>/studio</code> directory for all studio code.</li>
<li>The <code>.sanity-template</code> for all starter meta-information.</li>
</ol>
<p>Here is a sample from the <a target="_blank" href="https://github.com/commercelayer/sanity-template-commercelayer">Commerce Layer sanity starter</a> as seen in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649682579244/W92y68XXE.jpg" alt="sanity-starter-github-repo-screenshot" /></p>
<h2 id="heading-how-we-built-the-commerce-layer-sanity-starter">How We Built the Commerce Layer Sanity Starter</h2>
<p>In this section, you will learn how we built a starter with an ecommerce application with transactional functionalities powered by <a target="_blank" href="https://commercelayer.io/">Commerce Layer</a> APIs, structured content on Sanity, imported seed data, and deployment configuration to Netlify. If you want to follow along with the guide, you can take a look at the finished project on GitHub <a target="_blank" href="https://github.com/commercelayer/sanity-template-commercelayer">here</a> or even install the starter <a target="_blank" href="https://www.sanity.io/create?template=commercelayer/sanity-template-commercelayer">here</a>.</p>
<p>Here is a sequential breakdown of all the steps taken to develop a starter:</p>
<h3 id="heading-1-setup-a-new-sanity-project-using-the-sanity-cli">1️⃣  Setup a new Sanity project using the Sanity CLI</h3>
<p>Sanity has a command-line interface that we can use to interact with Sanity, create new projects, manage datasets, import data, and much more from the CLI. We'll use this CLI to set up a new sanity project following the steps below:</p>
<p><strong>1: Install the CLI</strong></p>
<p>Run the command below to install the Sanity CLI.</p>
<pre><code class="lang-bash">npm install -g @sanity/cli
</code></pre>
<p><strong>2: Create a new project</strong></p>
<p>Run the command below to bootstrap a new project which will log you into Sanity, create a new project, set up a dataset, and generate the files needed to run the studio environment locally.</p>
<pre><code class="lang-bash">sanity init
</code></pre>
<p><strong>3: Run the studio</strong></p>
<p>Run the command below to build the initial JavaScript code required to run the studio, and start a local web server.</p>
<pre><code class="lang-bash">sanity start
</code></pre>
<p>The studio should now run on <code>[localhost:3333](http://localhost:3333)</code>. You can always run <code>sanity help</code> to get an overview of other available and useful commands in the Sanity CLI.</p>
<h3 id="heading-2-content-modeling-for-the-created-sanity-studio">2️⃣  Content modeling for the created Sanity studio</h3>
<p>Now that we understand how Sanity works and have set up a new sanity project, let's structure our sanity studio schema. Sanity schema defines how your content should be modeled, and this structure reflects in the studio UI. The schema describes the different field types a document consists of. Sanity uses the <code>schema.js</code> file in the <code>/schemas</code> directory to determine the content model of the project.</p>
<p>With Sanity, you define a block of content as a document or split your documents into modules and import them into the parent <code>schema.js</code> file. Generally, there are three categories of Sanity schema types:</p>
<ul>
<li>Document types (<a target="_blank" href="https://www.sanity.io/docs/document-type">document</a> and other published custom schemas)</li>
<li>Primitive types (e.g., <a target="_blank" href="https://www.sanity.io/docs/boolean-type">boolean</a>, <a target="_blank" href="https://www.sanity.io/docs/string-type">string</a>, <a target="_blank" href="https://www.sanity.io/docs/text-type">text</a>, <a target="_blank" href="https://www.sanity.io/docs/number-type">number</a>, <a target="_blank" href="https://www.sanity.io/docs/array-type">array</a>, <a target="_blank" href="https://www.sanity.io/docs/datetime-type">datetime</a>, and <a target="_blank" href="https://www.sanity.io/docs/url-type">URL</a>)</li>
<li>Object types (e.g., <a target="_blank" href="https://www.sanity.io/docs/object-type">object</a>, <a target="_blank" href="https://www.sanity.io/docs/block-type">block</a>, <a target="_blank" href="https://www.sanity.io/docs/span-type">span</a>, <a target="_blank" href="https://www.sanity.io/docs/reference-type">reference</a>, <a target="_blank" href="https://www.sanity.io/docs/slug-type">slug</a>, <a target="_blank" href="https://www.sanity.io/docs/image-type">image</a>, and <a target="_blank" href="https://www.sanity.io/docs/file-type">file</a>)</li>
</ul>
<p>You can find all of the Sanity’s types in this <a target="_blank" href="https://www.sanity.io/docs/schema-types">reference documentation</a> or learn how to structure your content model based on your needs by reading <a target="_blank" href="https://www.sanity.io/guides/introduction-to-content-modeling">this comprehensive guide</a>.</p>
<p>For the Commerce Layer starter, our <code>schema.js</code> looks like so in the snippet below with imports of several other module documents. You can view the schema code for each module <a target="_blank" href="https://github.com/commercelayer/sanity-template-commercelayer/tree/main/studio/schemas">here</a> in the GitHub repository.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> createSchema <span class="hljs-keyword">from</span> <span class="hljs-string">'part:@sanity/base/schema-creator'</span>
<span class="hljs-keyword">import</span> schemaTypes <span class="hljs-keyword">from</span> <span class="hljs-string">'all:part:@sanity/base/schema-type'</span>

<span class="hljs-comment">// We import object and document schemas</span>
<span class="hljs-keyword">import</span> product <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>
<span class="hljs-keyword">import</span> country <span class="hljs-keyword">from</span> <span class="hljs-string">'./country'</span>
<span class="hljs-keyword">import</span> variant <span class="hljs-keyword">from</span> <span class="hljs-string">'./variant'</span>
<span class="hljs-keyword">import</span> size <span class="hljs-keyword">from</span> <span class="hljs-string">'./size'</span>
<span class="hljs-keyword">import</span> taxon <span class="hljs-keyword">from</span> <span class="hljs-string">'./taxon'</span>
<span class="hljs-keyword">import</span> taxonomy <span class="hljs-keyword">from</span> <span class="hljs-string">'./taxonomy'</span>
<span class="hljs-keyword">import</span> catalog <span class="hljs-keyword">from</span> <span class="hljs-string">'./catalog'</span>
<span class="hljs-keyword">import</span> blockContent <span class="hljs-keyword">from</span> <span class="hljs-string">'./blockContent'</span>

<span class="hljs-keyword">import</span> productImage <span class="hljs-keyword">from</span> <span class="hljs-string">'./productImage'</span>
<span class="hljs-keyword">import</span> localeString <span class="hljs-keyword">from</span> <span class="hljs-string">'./locale/String'</span>
<span class="hljs-keyword">import</span> localeText <span class="hljs-keyword">from</span> <span class="hljs-string">'./locale/Text'</span>
<span class="hljs-keyword">import</span> localeSlug <span class="hljs-keyword">from</span> <span class="hljs-string">'./locale/Slug'</span>
<span class="hljs-keyword">import</span> localeBlockContent <span class="hljs-keyword">from</span> <span class="hljs-string">'./locale/BlockContent'</span>

<span class="hljs-comment">// Then we give our schema to the builder and provide the result to Sanity</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> createSchema({
  <span class="hljs-comment">// We name our schema</span>
  <span class="hljs-attr">name</span>: <span class="hljs-string">'default'</span>,
  <span class="hljs-comment">// Then proceed to concatenate our document type</span>
  <span class="hljs-comment">// to the ones provided by any plugins that are installed</span>
  <span class="hljs-attr">types</span>: schemaTypes.concat([
    <span class="hljs-comment">// The following are document types which will appear</span>
    <span class="hljs-comment">// in the studio.</span>
    product,
    country,
    variant,
    size,
    taxon,
    taxonomy,
    catalog,
    <span class="hljs-comment">// When added to this list, object types can be used as</span>
    <span class="hljs-comment">// { type: "typename" } in other document schemas</span>
    productImage,
    blockContent,
    localeString,
    localeText,
    localeSlug,
    localeBlockContent,
  ]),
})
</code></pre>
<h3 id="heading-3-add-content-to-sanity-studio">3️⃣  Add content to Sanity studio</h3>
<p>If you are working on a new project like we did when we began developing the starter, you will have to manually add content to your project using the Sanity studio running on <code>[localhost:3333](http://localhost:3333)</code>. The studio should now have the content fields populated with the configured content schemas in the “Desk” view. You can use that to add content to your project, as seen in the screenshot below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649682643887/tL_1yhMVy.jpg" alt="sanity-studio-desk-view-screenshot" /></p>
<p>If you are starting a new project using a starter or a previously saved project, then you can easily import a dataset with saved data following the steps below:</p>
<ul>
<li>Extract the <code>production.tar.gz</code> file in <code>/.sanity-template/data</code> directory using the command below:</li>
</ul>
<pre><code class="lang-bash">tar -xf production.tar.gz
</code></pre>
<p>The extracted folder name should look like <code>production-export-2021-02-26t14-15-56-557z</code>.</p>
<ul>
<li>Run the command below in <code>/studio</code> to import the <code>data.ndjson</code> file in the extracted folder.</li>
</ul>
<pre><code class="lang-bash">sanity dataset import ../.sanity-template/data/&lt;name of extracted folder&gt;/data.ndjson &lt;your_dataset&gt;
</code></pre>
<p>You should check the running Sanity studio now to preview the imported content.</p>
<h3 id="heading-4-add-frontend-and-integrate-with-sanity">4️⃣  Add frontend and integrate with Sanity</h3>
<p>Before you add all frontend code to the root directory, you should move the Sanity studio code into a directory named <code>/studio</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649682680953/Gf7puWQ7B.jpg" alt="sanity-starter-github-repo-studio-folder-screenshot" /></p>
<p>At this stage, you will add the frontend code of your project, which can either be a blog, marketing website, CRM, or storefront. The major thing to do here is to use any of the <a target="_blank" href="https://www.sanity.io/docs/client-libraries">Sanity client</a> libraries to integrate Sanity into your frontend and fetch data. In our case, we used the official <a target="_blank" href="https://www.sanity.io/docs/js-client">Javascript client</a> that works in Node.js and modern browsers.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> sanityClient <span class="hljs-keyword">from</span> <span class="hljs-string">'@sanity/client'</span>

<span class="hljs-keyword">const</span> client = sanityClient({
  <span class="hljs-attr">projectId</span>: process.env.SANITY_PROJECT_ID <span class="hljs-keyword">as</span> string,
  <span class="hljs-attr">dataset</span>: process.env.SANITY_DATASET <span class="hljs-keyword">as</span> string,
  <span class="hljs-attr">useCdn</span>: process.env.NODE_ENV === <span class="hljs-string">'production'</span>, <span class="hljs-comment">// `false` if you want to ensure fresh data</span>
})
</code></pre>
<p>Here’s an example of how we query Sanity to fetch the country and product data:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> _ <span class="hljs-keyword">from</span> <span class="hljs-string">'lodash'</span>
<span class="hljs-keyword">import</span> {
  SanityCountry,
  SanityProduct
} <span class="hljs-keyword">from</span> <span class="hljs-string">'./typings'</span>

<span class="hljs-comment">//Countries</span>
<span class="hljs-keyword">const</span> sanityAllCountries = <span class="hljs-keyword">async</span> (locale = <span class="hljs-string">'en-US'</span>) =&gt; {
  <span class="hljs-keyword">const</span> lang = parseLocale(locale, <span class="hljs-string">'_'</span>, <span class="hljs-string">'-'</span>, <span class="hljs-string">'lowercase'</span>)
  <span class="hljs-keyword">const</span> query = <span class="hljs-string">`*[_type == "country"]{
    name,
    code,
    marketId,
    defaultLocale,
    "image": {
      "url": image.asset-&gt;url
    },
    'catalog': {
      'id': catalog-&gt;_id
    }
  } | order(name["<span class="hljs-subst">${lang}</span>"] asc)`</span>
  <span class="hljs-keyword">const</span> countries = <span class="hljs-keyword">await</span> client.fetch&lt;SanityCountry[]&gt;(query)
  <span class="hljs-keyword">return</span> countries.map(<span class="hljs-function">(<span class="hljs-params">country</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> localization = {
      <span class="hljs-attr">name</span>: country?.name[lang],
    }
    <span class="hljs-keyword">return</span> { ...country, ...localization }
  })
}

<span class="hljs-comment">//Products</span>
<span class="hljs-keyword">const</span> sanityGetProduct = <span class="hljs-keyword">async</span> (slug: string, locale = <span class="hljs-string">'en-US'</span>) =&gt; {
  <span class="hljs-keyword">const</span> lang = parseLocale(locale, <span class="hljs-string">'_'</span>, <span class="hljs-string">'-'</span>, <span class="hljs-string">'lowercase'</span>)
  <span class="hljs-keyword">const</span> query = <span class="hljs-string">`*[_type == "product" &amp;&amp; slug["<span class="hljs-subst">${lang}</span>"].current == "<span class="hljs-subst">${slug}</span>"]{
    name,
    description,
    reference,
    slug,
    'images': images[]-&gt;{
      'url': images.asset-&gt;url
    },
    'variants': variants[]-&gt;{
      label,
      code,
      name,
      size-&gt;,
      'images': images[]-&gt;{
        'url': images.asset-&gt;url
      }
    }    
  }`</span>
  <span class="hljs-keyword">const</span> item: any[] = <span class="hljs-keyword">await</span> client.fetch(query)
  <span class="hljs-keyword">return</span> parsingProduct(_.first(item), lang)
}
</code></pre>
<p>You can explore all our queries for the Commerce Layer starter project <a target="_blank" href="https://github.com/commercelayer/sanity-template-commercelayer/blob/main/utils/sanity/api.ts">here</a> in the GitHub repository. Also, here’s the <a target="_blank" href="https://github.com/commercelayer/sanity-template-commercelayer/blob/main/pages/%5BcountryCode%5D/%5Blang%5D/%5Bproduct%5D.tsx">major code</a> powering our frontend alongside some hooks, utils, components, and dependencies.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649682886239/Z6Fxba_BQ.png" alt /></p>
<p>Now that you have a Sanity Starter set up, we’ll show you the foundation principles needed to integrate commerce data from Commerce Layer. This is where you will begin to see the powerful combination of Commerce Layer data with Sanity content. And by the end of the tutorial, you will see not only the benefits of this integration, but you will also be able to experiment with your commerce data next to Sanity to see the power of both tools together.</p>
<h3 id="heading-5-get-commerce-layer-api-credentials">5️⃣  Get Commerce Layer API Credentials</h3>
<p>In the starter we built, transactional functionalities of your ecommerce platform are managed by Commerce Layer, while the content is managed by Sanity studio. This will result in better order management and content management. To get started with using Commerce Layer, you will need to create an organization, perform some configurations and settings for your business, seed some demo data, and get your API credentials. The API credentials will allow you to interact with Commerce Layer in your presentation layer (frontend) and the CLI. To get the required credentials, kindly follow the steps below:</p>
<ol>
<li>Create a free developer account <a target="_blank" href="https://dashboard.commercelayer.io/sign_up">here</a>. If you already have an account, kindly skip to Step 3.</li>
<li>Upon successful sign-up, skip the onboarding tutorial for the purposes of this article (we'll set up and seed the organization manually through the CLI shortly).</li>
<li>Create a new <a target="_blank" href="https://commercelayer.io/docs/data-model/users-and-organizations">organization</a> for your business.</li>
<li>In the Commerce Layer dashboard, click on the <strong>Sales channels</strong> tab and create an application, with the name: <code>Website</code>. Upon successful creation, you'll get a <code>CLIENT ID</code> , <code>BASE ENDPOINT</code>, and <code>ALLOWED SCOPES</code>. Kindly remember to save that as we'll use it to interact with our application later.</li>
<li>In the Commerce Layer dashboard, click on the <strong>Integrations</strong> tab and create an application, with the name: <code>CLI</code> and role: <code>Admin</code>. Upon successful creation, you'll get a <code>CLIENT ID</code>, <code>CLIENT SECRET</code>, and <code>BASE ENDPOINT</code>. Kindly remember to save that as we'll use it to interact with the CLI later.</li>
</ol>
<h3 id="heading-6-seed-organization-with-test-data">6️⃣  Seed Organization with Test Data</h3>
<p>The official <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli">Commerce Layer CLI</a> helps you to manage your Commerce Layer applications right from the terminal. Installing the CLI provides access to the <code>commercelayer</code> command. You can set it up using the following steps:</p>
<ul>
<li>Install the CLI using your favorite package manager:</li>
</ul>
<pre><code class="lang-bash">//npm
npm install -g @commercelayer/cli

//yarn
yarn global add @commercelayer/cli
</code></pre>
<ul>
<li>Log into your application via the CLI using the previously created <a target="_blank" href="https://docs.commercelayer.io/developers/roles-and-permissions#integration">integration</a> application credentials like so:</li>
</ul>
<pre><code class="lang-bash">commercelayer applications:login -o &lt;organizationSlug&gt; -i &lt;clientId&gt; -s &lt;clientSecret&gt; -a &lt;applicationAlias&gt;
</code></pre>
<p>Now, with the steps below, you can use the CLI to import three demo <a target="_blank" href="https://data.commercelayer.app/seed/markets.json">markets</a> (UK, USA, and Europe), a set of <a target="_blank" href="https://data.commercelayer.app/seed/skus.json">product SKUs</a>, related <a target="_blank" href="https://data.commercelayer.app/seed/price_lists.json">price lists</a>, related <a target="_blank" href="https://data.commercelayer.app/seed/prices.json">prices</a>, <a target="_blank" href="https://data.commercelayer.app/seed/stock_locations.json">stock locations</a>, and <a target="_blank" href="https://data.commercelayer.app/seed/stock_items.json">inventory</a> into your organization using the multi_market <a target="_blank" href="https://commercelayer.io/docs/data-model/markets-and-business-models">business model</a>.</p>
<ul>
<li>Install the <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli-plugin-seeder">seeder plugin</a> using the command below:</li>
</ul>
<pre><code class="lang-bash">commercelayer plugins:install seeder
</code></pre>
<ul>
<li>Seed your organization using the command below:</li>
</ul>
<pre><code class="lang-bash">commercelayer seed -b multi_market
</code></pre>
<h3 id="heading-7-final-checklist-and-netlify-deployment-configuration">7️⃣  Final checklist and Netlify deployment configuration</h3>
<ul>
<li>In order for a starter to be validated and used through <a target="_blank" href="https://www.sanity.io/starters">sanity.io/starters</a>, it needs to follow the project name must start with <code>sanity-template-</code>.</li>
<li>Configure your Sanity metadata in <code>sanity-template.json</code> and add deployment configuration for the frontend web application and Sanity studio like so:</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"version"</span>: <span class="hljs-number">2.0</span>,
  <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Commerce Layer Starter"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A multi-country ecommerce starter built with Sanity Studio, Commerce Layer, Next.js, and deployed to Netlify."</span>,
  <span class="hljs-attr">"previewMedia"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"image"</span>,
    <span class="hljs-attr">"src"</span>: <span class="hljs-string">".sanity-template/assets/preview.jpg"</span>,
    <span class="hljs-attr">"alt"</span>: <span class="hljs-string">"Preview image with Commerce Layer, Nextjs, and Netlify's logo"</span>
  },
  <span class="hljs-attr">"technologies"</span>: [
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"nextjs"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Next.js"</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://nextjs.org"</span>
    },
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"commercelayer"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Commerce Layer"</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://commercelayer.io"</span>
    },
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"netlify"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Netlify"</span>,
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://netlify.com"</span>
    }
  ],
  <span class="hljs-attr">"deployment"</span>: {
    <span class="hljs-attr">"provider"</span>: <span class="hljs-string">"netlify"</span>,
    <span class="hljs-attr">"sites"</span>: [
      {
        <span class="hljs-attr">"id"</span>: <span class="hljs-string">"studio"</span>,
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"studio"</span>,
        <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Commerce Layer Starter Studio"</span>,
        <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A multi-country ecommerce starter built with Sanity Studio, Commerce Layer, Next.js, and deployed to Netlify."</span>,
        <span class="hljs-attr">"dir"</span>: <span class="hljs-string">"./studio"</span>,
        <span class="hljs-attr">"previewMedia"</span>: {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"image"</span>,
          <span class="hljs-attr">"src"</span>: <span class="hljs-string">".sanity-template/assets/studio.png"</span>,
          <span class="hljs-attr">"alt"</span>: <span class="hljs-string">"A preview image of the Sanity studio."</span>
        },
        <span class="hljs-attr">"buildSettings"</span>: {
          <span class="hljs-attr">"base"</span>: <span class="hljs-string">"studio"</span>,
          <span class="hljs-attr">"dir"</span>: <span class="hljs-string">"/dist"</span>,
          <span class="hljs-attr">"cmd"</span>: <span class="hljs-string">"npm run build"</span>
        }
      },
      {
        <span class="hljs-attr">"id"</span>: <span class="hljs-string">"web"</span>,
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"web"</span>,
        <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Commerce Layer Starter Web"</span>,
        <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A multi-country ecommerce starter built with Sanity Studio, Commerce Layer, Next.js, and deployed to Netlify."</span>,
        <span class="hljs-attr">"dir"</span>: <span class="hljs-string">"./web"</span>,
        <span class="hljs-attr">"previewMedia"</span>: {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"image"</span>,
          <span class="hljs-attr">"src"</span>: <span class="hljs-string">".sanity-template/assets/preview.jpg"</span>,
          <span class="hljs-attr">"alt"</span>: <span class="hljs-string">"A preview image of the web demo."</span>
        },
        <span class="hljs-attr">"buildSettings"</span>: {
          <span class="hljs-attr">"base"</span>: <span class="hljs-string">"/"</span>,
          <span class="hljs-attr">"dir"</span>: <span class="hljs-string">"/out"</span>,
          <span class="hljs-attr">"cmd"</span>: <span class="hljs-string">"npm run build"</span>
        }
      }
    ]
  }
}
</code></pre>
<p>The metadata information is primarily displayed on <a target="_blank" href="https://www.sanity.io/blog/a-new-way-to-get-started-with-a-sanity-powered-website">sanity.io/create</a> as described below by the visual explainer from <a target="_blank" href="https://www.sanity.io/docs/starter-templates">Sanity docs</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649682937904/QVBgRYBAv.png" alt /></p>
<ul>
<li>Test your <code>sanity-template.json</code> file for errors using the sanity-template command:</li>
</ul>
<pre><code class="lang-bash">sanity-template check
</code></pre>
<ul>
<li>Build your project with the configuration in <code>sanity-template.json</code> using the command<strong>:</strong></li>
</ul>
<pre><code class="lang-bash">sanity-template build
</code></pre>
<ul>
<li>You need to refactor your project's <code>name</code>,  <code>projectId</code> and <code>dataset</code> in <code>studio/sanity.json</code> to a dynamic variable so when a user installs your starter via <a target="_blank" href="https://www.sanity.io/starters">sanity.io/starters</a>, Sanity can populate it with new values. To this, you pass the string value in <code>&lt;#&lt; ... &gt;#&gt;</code> as seen in the snippet below:</li>
</ul>
<pre><code class="lang-json"> {
  <span class="hljs-attr">"root"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"project"</span>: {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"&lt;#&lt; sanity.projectTitle &gt;#&gt;"</span>,
    <span class="hljs-attr">"basePath"</span>: <span class="hljs-string">"/"</span>
  },
  <span class="hljs-attr">"api"</span>: {
    <span class="hljs-attr">"projectId"</span>: <span class="hljs-string">"&lt;#&lt; sanity.projectId &gt;#&gt;"</span>,
    <span class="hljs-attr">"dataset"</span>: <span class="hljs-string">"&lt;#&lt; sanity.dataset &gt;#&gt;"</span>
  }
}
</code></pre>
<ul>
<li>You can also set up <a target="_blank" href="https://github.com/renovatebot/renovate">Renovatebot</a> to automatically make and merge pull requests that bump the Sanity dependencies upgrades in <code>studio/package.json</code>. All you need to do is add a <code>renovate.json</code> to the root directory, with the following configuration:</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: [
    <span class="hljs-string">"github&gt;sanity-io/renovate-presets:sanity-template"</span>
  ]
}
</code></pre>
<ul>
<li>Run the command below to build the studio to a static bundle and deploy it to Sanity cloud on a <code>&lt;your-project&gt;.sanity.studio</code> URL. You can also deploy anytime you make any change to your studio.</li>
</ul>
<pre><code class="lang-bash">sanity deploy
</code></pre>
<p>PS: You can still host a studio on any cloud platform you choose too (here’s <a target="_blank" href="https://github.com/BolajiAyodeji/cl-jamstack-ecommerce-workshop#continous-deployment-on-netlify">how to deploy to Netlify</a>) so you don't have to manually deploy after every change.</p>
<ul>
<li>Lastly, push your finished code to GitHub and test it live by <em>**</em>deploying the starter on Sanity following the starter specification:</li>
</ul>
<pre><code>https:<span class="hljs-comment">//www.sanity.io/create?template=[githubhandle]/sanity-template-[starter-name]</span>
</code></pre><h2 id="heading-conclusion">Conclusion</h2>
<p>Now that you have built a Sanity starter and integrated Commerce Layer, you can start to add more items and product data stored in Commerce Layer so you can see how your products and prices show up within your content. The power of Commerce Layer is that you can really localize your commerce data to make it work for multiple markets, all of which likely have different prices, SKUs, promotions, and even simple things like item weights and measurements. Commerce Layer gives you tremendous flexibility to sell your products locally and paired with a powerful tool like Sanity, you will be on your way to building the best, most optimized shopping experience for your customers.</p>
<p>You can get started with the Commerce Layer starter by visiting <a target="_blank" href="https://www.sanity.io/create?template=commercelayer/sanity-template-commercelayer">this link</a>, creating a new project and following the instructions in the link. Feel free to join the <a target="_blank" href="https://slack.commercelayer.app">Commerce Layer Slack</a> community to share what you are able to build after reading this tutorial or <a target="_blank" href="https://community.sanity.tools/desk/contribution.starter%2Ctemplate%3Dcontribution.starter;WzXQvbBRsm9g2XBDjGSTY%2Ctemplate%3Dcontribution.starter">showcase the starter on Sanity</a>. For further knowledge, you can <a target="_blank" href="https://www.sanity.io/docs/starter-templates">learn the central concepts</a> needed to create a 1-click Sanity Starter, learn <a target="_blank" href="https://github.com/BolajiAyodeji/cl-jamstack-ecommerce-workshop">how to build headless commerce web experiences</a> with Commerce Layer, or learn how to <a target="_blank" href="https://commercelayer.io/blog/how-to-sell-internationally-with-a-single-shopify-store-and-commerce-layer">sell internationally with a single Shopify store and Commerce Layer</a>.</p>
<p>Thanks for reading! 🖤</p>
]]></content:encoded></item><item><title><![CDATA[How to Connect Google Colab to a Local Runtime]]></title><description><![CDATA[Google Colaboratory lets you build "Colab notebooks" on the browser by blending Python executable code with rich text (along with images, HTML, and LaTeX). It is one of the simplest ways to work with Python notebooks, with no configuration required, ...]]></description><link>https://blog.bolajiayodeji.com/how-to-connect-google-colab-to-a-local-runtime</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-connect-google-colab-to-a-local-runtime</guid><category><![CDATA[Python]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[google cloud]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Fri, 04 Mar 2022 21:30:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1646428401645/vB_dszAtw.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://research.google.com/colaboratory">Google Colaboratory</a> lets you build "Colab notebooks" on the browser by blending Python executable code with rich text (along with images, HTML, and LaTeX). It is one of the simplest ways to work with Python notebooks, with no configuration required, easy collaboration, and free access to GPUs and TPUs. Unfortunately, while Colab provides access to GPUs, these resources are limited and will eventually run out. For a monthly fee, you can upgrade to Colab Pro or Colab Pro+, which gives you access to more GPUs and memory resources. However, if you're running a minimal collaboratory notebook, you can utilize the alternative option; connecting Google Colab to a local Jupyter runtime to use your local CPU and file storage.  In this article, I'll show you how to set up a Jupyter notebook and everything you need to know about connecting to Colab.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646425263173/yJN81MlZt.png" alt="Screenshot of the TF Hub for TF2: Retraining an image classifier notebook" /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>Basic knowledge of the command-line interface.</li>
<li>A working computer with Python installed.</li>
<li>A notebook created on a registered Google Colab account.</li>
</ul>
<h2 id="heading-step-one">Step One</h2>
<p>The first step is to install Jupyter notebook. I recommend you <a target="_blank" href="https://www.anaconda.com/download">download and install Anaconda</a> for your operating system of choice. This will also install  Python, Jupyter Notebook, and other regularly used data engineering packages. Once Anaconda is installed, you automatically have Jupyter installed.</p>
<p>Alternatively, you can install Jupyter using PIP provided you have Python installed already using the command below:</p>
<pre><code class="lang-bash">pip3 install --upgrade pip
pip3 install jupyter
</code></pre>
<p>Upon a successful installation, you can launch Anaconda and start a Jupyter server using the GUI or use the command line.</p>
<h2 id="heading-step-two">Step Two</h2>
<p>Install the Colab <a target="_blank" href="https://github.com/googlecolab/jupyter_http_over_ws">Jupyter HTTP-over-WebSocket extension</a> that allows you to run Jupyter notebooks using a WebSocket to proxy HTTP traffic. You can achieve that using the command below:</p>
<pre><code class="lang-bash">pip install --upgrade jupyter_http_over_ws&gt;=0.0.7
</code></pre>
<p>Next, enable the extension:</p>
<pre><code class="lang-bash">jupyter serverextension <span class="hljs-built_in">enable</span> --py jupyter_http_over_ws
</code></pre>
<h2 id="heading-step-three">Step Three</h2>
<p>Start a new Jupyter server on port 8888, allowing origin to the Colab domain using the command below:</p>
<pre><code>jupyter notebook \
  <span class="hljs-operator">-</span><span class="hljs-operator">-</span>NotebookApp.allow_origin=<span class="hljs-string">'https://colab.research.google.com'</span> \
  <span class="hljs-operator">-</span><span class="hljs-operator">-</span>port<span class="hljs-operator">=</span><span class="hljs-number">8888</span> \
  <span class="hljs-operator">-</span><span class="hljs-operator">-</span>NotebookApp.port_retries=<span class="hljs-number">0</span>
</code></pre><p>Once the server has started, it will print a backend URL alongside a token for authentication. Kindly copy the entire URL that starts with <code>http://localhost</code> ahead for the next step.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646427011253/8qqF8u0yf.png" alt="Screenshot 2022-03-04 at 9.48.57 PM.png" /></p>
<h2 id="heading-step-four">Step Four</h2>
<p>In your Google Colab notebook, click on the toggle button at the top right corner showing the RAM and Disk status bar and select the option "Connect a local runtime" as seen in the screenshots below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646427242729/KW6E7e2im.png" alt="Screenshot 2022-03-04 at 9.53.19 PM.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646427296804/eiRB1Ul3b.png" alt="Screenshot 2022-03-04 at 9.54.21 PM.png" /></p>
<p>You'll then be asked to enter the backend URL you copied earlier. Enter it and click on the "Connect" button. Now you should be connected successfully to your local machine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646427893409/E2SeikYOv.png" alt="Screenshot 2022-03-04 at 10.04.31 PM.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646427507956/GwMEBGDm-.png" alt="Screenshot 2022-03-04 at 9.57.49 PM.png" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you're running your own personal notebook, you can as well run them locally using Jupyter notebook. However, suppose you're working on a collaborative notebook or want to take advantage of additional Google Colab features like mounting your Google drive directly into the notebook. In that case, the local runtime setup can be useful when your GPU usage reaches its limit. To avoid security issues, make sure you trust the notebook before connecting locally.</p>
<p>I hope you found this helpful; cheers! 💙</p>
]]></content:encoded></item><item><title><![CDATA[How to Make any Website Shoppable]]></title><description><![CDATA[Digital commerce has changed, and there is an increasing demand for faster and highly efficient solutions across multiple interaction channels. The rise of headless commerce allows for more creative flexibility, better performance, and efficient soft...]]></description><link>https://blog.bolajiayodeji.com/how-to-make-any-website-shoppable</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-make-any-website-shoppable</guid><category><![CDATA[ecommerce]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[software development]]></category><category><![CDATA[HTML5]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Wed, 16 Feb 2022 11:56:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1645009490294/v5qGeIfMa.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Digital commerce has changed, and there is an increasing demand for faster and highly efficient solutions across multiple interaction channels. The rise of <a target="_blank" href="https://commercelayer.io/docs/core-concepts/headless-commerce">headless commerce</a> allows for more creative flexibility, better performance, and efficient software development. Developers can now make any website shoppable, serve customers on several platforms using the same code and content with no interruption to the customer journey using tools like <a target="_blank" href="https://commercelayer.io">Commerce Layer</a>. As an API-first and stack agnostic platform, Commerce Layer provides commerce APIs for inventory, orders, shopping carts, prices, promotions, shipments, customers, and more. Ecommerce businesses can integrate Commerce Layer to manage the transactional part of their sales channel alongside any architecture or tool of their choice (be it WordPress, Drupal, Headless... anything!) and serve their storefront on multiple presentation layers (desktop, mobile, wearable devices, IoT, etc.).</p>
<p>In this article, you'll practice the basics of headless commerce by building a simple <a target="_blank" href="https://headless-swag-store.netlify.app">ecommerce product page</a> with static content, HTML5, and TailwindCSS as seen in the screenshot below. We will then integrate Commerce Layer to make the website shoppable using the <a target="_blank" href="https://github.com/commercelayer/commercelayer-js-dropin">Drop-in Javascript library</a> and deploy the application to the cloud. The Drop-in library is a minimal way to get started with headless commerce and Commerce Layer, but if you're building for your business you can opt for a deeper integration using the <a target="_blank" href="https://github.com/commercelayer/commercelayer-sdk">Commerce Layer SDK</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644978483946/-x5owL926.png" alt="A screenshot of the Demo application." /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>Some HTML, CSS, Javascript, and command-line knowledge.</li>
<li>A general understanding of how APIs work.</li>
<li>An IDE and Git installed.</li>
</ul>
<h2 id="heading-what-is-commerce-layer">What is Commerce Layer?</h2>
<p><a target="_blank" href="https://commercelayer.io/">Commerce Layer</a> is a multi-market commerce API and order management system that lets you add global shopping capabilities to any website, mobile app, chatbot, wearable device, or IoT device, with ease. You can compose your stack with the best-of-breed tools you want and make any experience shoppable, anywhere, through a blazing-fast, enterprise-grade, and secure API.</p>
<p>The diagram below shows the relationships in an example stack for building a solid ecommerce web application using Commerce Layer and other <a target="_blank" href="https://commercelayer.io/docs/core-concepts/composable-commerce">composable</a> tools.</p>
<p><img src="https://github.com/BolajiAyodeji/cl-jamstack-ecommerce-workshop/raw/master/assets/architecture-flow.png" alt="https://github.com/BolajiAyodeji/cl-jamstack-ecommerce-workshop/raw/master/assets/architecture-flow.png" /></p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Let's start by building the static website with HTML and TailwindCSS, all in a single <code>index.html</code> file. We will then integrate Commerce Layer using the Dropin library!</p>
<h3 id="heading-1-create-the-required-files">1️⃣ Create the required files</h3>
<ol>
<li>Create a new folder somewhere on your computer.</li>
<li>Create an <code>index.html</code> file in the root directory.</li>
<li>Create a <code>/css</code> directory with a <code>custom.css</code> file in it.</li>
</ol>
<h3 id="heading-2-get-your-api-credentials">2️⃣ Get your API Credentials</h3>
<p>You can create an account to get access to and integrate with Commerce Layer APIs following the steps below:</p>
<ol>
<li>Create a free developer account <a target="_blank" href="https://dashboard.commercelayer.io/sign_up">here</a>. If you already have an account, kindly skip to Step 3.</li>
<li>Upon successful sign-up, skip the onboarding tutorial for the purposes of this article (we'll set up and seed the organization manually through the CLI shortly).</li>
<li>Create a new <a target="_blank" href="https://commercelayer.io/docs/data-model/users-and-organizations">organization</a> for your business.</li>
<li>In the Commerce Layer dashboard, click on the <strong>Sales channels</strong> tab and create an application, with the name: <code>Website</code>. Upon successful creation, you'll get a <code>CLIENT ID</code> and <code>BASE ENDPOINT</code>. Kindly remember to save that as we'll use it to interact with our application later.</li>
<li>In the Commerce Layer dashboard, click on the <strong>Integrations</strong> tab and create an application, with the name: <code>CLI</code> and role: <code>Admin</code>. Upon successful creation, you'll get a <code>CLIENT ID</code>, <code>CLIENT SECRET</code>, and <code>BASE ENDPOINT</code>. Kindly remember to save that as we'll use it to interact with the CLI later.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644978604540/5h0uXQ-i_.jpeg" alt="screenshot-sales-channel-website.jpg" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644978627048/7cA7BWS2_.jpeg" alt="screenshot-integration-CLI.jpg" /></p>
<h3 id="heading-3-setup-commerce-layer-cli">3️⃣ Setup Commerce Layer CLI</h3>
<p>The <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli">Commerce Layer CLI</a> helps you to manage your Commerce Layer applications right from the terminal. Installing the CLI provides access to the <code>commercelayer</code> command. You can set it up using the following steps:</p>
<ol>
<li>Install the CLI using your favorite package manager:</li>
</ol>
<pre><code class="lang-bash">//npm
npm install -g @commercelayer/cli

//yarn
yarn global add @commercelayer/cli
</code></pre>
<ol>
<li>Log into your application via the CLI using the previously created CLI credentials like so:</li>
</ol>
<pre><code class="lang-bash">commercelayer applications:login -o &lt;organizationSlug&gt; -i &lt;clientId&gt; -s &lt;clientSecret&gt; -a &lt;applicationAlias&gt;
</code></pre>
<p>You will do the above for both applications (sales_channel and integration). The login command adds the application and sets it as the current one in session. You should log into the <code>sales_channel</code> first, and then the <code>integration</code>. This way, you'll have the integration as the current app (which is required by the seeder).</p>
<h3 id="heading-4-seed-organization-with-test-data">4️⃣ Seed organization with test data</h3>
<p>As we mentioned earlier, you can seed your organization with a merchant, a set of markets, products (SKUs), related prices, shipping methods, inventory, etc. via the CLI.</p>
<ol>
<li>Install the <a target="_blank" href="https://github.com/commercelayer/commercelayer-cli-plugin-seeder">seeder plugin</a> using the command below:</li>
</ol>
<pre><code class="lang-bash">commercelayer plugins:install seeder
</code></pre>
<ol>
<li>Seed your organization using the command below:</li>
</ol>
<pre><code class="lang-bash">commercelayer seed
</code></pre>
<p>The CLI will use Commerce Layer's <a target="_blank" href="https://data.commercelayer.app/seeder">default seed data</a> to populate your organization with a set of products (SKUs), related prices, and inventory information.</p>
<h2 id="heading-build-the-static-product-page">Build the Static Product Page</h2>
<p>Now let's build our product page in a single <code>index.html</code> file which will contain the markup for product details, prices, available variants, available quantity, item availability, shopping bag, and checkout. You can find the full code on <a target="_blank" href="https://github.com/commercelayer/headless-swag-store/blob/main/index.html">GitHub</a>. In this tutorial, we will only highlight the important HTML elements and related data attributes we need for dynamic data. We will replace all style classes with the dummy content <code>[...]</code> so you can focus on the utility classes required to interact with the Drop-in library.</p>
<p>We will be using some <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">data attributes</a> to store extra information that doesn't have any visual representation like <code>data-sku-code</code> to store the SKU code of an item or <code>data-add-to-bag-id</code> to add a selected variant to the shopping bag when the "Add to bag" button is clicked. The Dropin library requires some data attributes to add dynamic data to a static web page. With this mechanism, you're building ecommerce with almost no code required (just HTML tags). Now let's proceed!</p>
<h3 id="heading-1-create-static-content">1️⃣ Create Static Content</h3>
<p>You should add some content to the page, such as product names, descriptions, reviews and images, and more. This content can be created with any CMS and rendered with any language. For this tutorial we're using just plain HTML.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Headless Swag Store<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>
          <span class="hljs-attr">href</span>=<span class="hljs-string">"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"</span>
          <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
        /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-10"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-between"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm text-indigo-600 font-medium"</span>&gt;</span>
                    Choose a size
                  <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-4 flex -mx-2"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 px-2"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">select</span>
                      <span class="hljs-attr">name</span>=<span class="hljs-string">"variant"</span>
                      <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant-select block appearance-none border-2 border-gray-500 text-base-700 py-3 px-4 pr-8 rounded"</span>
                      <span class="hljs-attr">data-sku-reference</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000"</span>
                      <span class="hljs-attr">data-price-container-id</span>=<span class="hljs-string">"price"</span>
                      <span class="hljs-attr">data-availability-message-container-id</span>=<span class="hljs-string">"availability-message"</span>
                      <span class="hljs-attr">data-add-to-bag-id</span>=<span class="hljs-string">"add-to-bag"</span>
                      <span class="hljs-attr">data-add-to-bag-quantity-id</span>=<span class="hljs-string">"add-to-bag-quantity"</span>
                    &gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">""</span> <span class="hljs-attr">disabled</span> <span class="hljs-attr">selected</span>&gt;</span>Select variant<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
                        <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
                        <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000SXXX"</span>
                        <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (S)"</span>
                        <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
                      &gt;</span>
                        Small
                      <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
                        <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
                        <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000MXXX"</span>
                        <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (M)"</span>
                        <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
                      &gt;</span>
                        Medium
                      <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
                        <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
                        <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000LXXX"</span>
                        <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (L)"</span>
                        <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
                      &gt;</span>
                        Large
                      <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
                        <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
                        <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000XLXX"</span>
                        <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (XL)"</span>
                        <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
                      &gt;</span>
                        Extra Large
                      <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                  <span class="hljs-comment">&lt;!-- Add to bag quantity --&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 px-2"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"add-to-bag-quantity"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>
                      &gt;</span>Add to bag quantity<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>
                    &gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
                      <span class="hljs-attr">id</span>=<span class="hljs-string">"add-to-bag-quantity"</span>
                      <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span>
                      <span class="hljs-attr">min</span>=<span class="hljs-string">"1"</span>
                      <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-add-to-bag-quantity block appearance-none border-2 border-gray-500 text-base-700 py-3 px-4 pr-2 rounded"</span>
                    /&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">link</span>
          <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span>
          <span class="hljs-attr">href</span>=<span class="hljs-string">"https://data.commercelayer.app/assets/images/favicons/favicon.ico"</span>
      /&gt;</span>   
     <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content --&gt;</span>

      <span class="hljs-comment">&lt;!-- Image gallery --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6 max-w-2xl mx-auto sm:px-6 lg:max-w-7xl lg:px-8 lg:grid lg:grid-cols-3 lg:gap-x-8"</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden aspect-w-3 aspect-h-4 rounded-lg overflow-hidden lg:block"</span> &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
               <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/img/01.png"</span>
               <span class="hljs-attr">alt</span>=<span class="hljs-string">"Model wearing the gray men hoodie"</span>
               <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full h-full object-center object-cover"</span>
               /&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden lg:grid lg:grid-cols-1 lg:gap-y-8"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"aspect-w-3 aspect-h-2 rounded-lg overflow-hidden"</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
                  <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/img/02.png"</span>
                  <span class="hljs-attr">alt</span>=<span class="hljs-string">"The gray men hoodie"</span>
                  <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full h-full object-center object-cover"</span>
                  /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"aspect-w-3 aspect-h-2 rounded-lg overflow-hidden"</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
                  <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/img/03.png"</span>
                  <span class="hljs-attr">alt</span>=<span class="hljs-string">"The gray men hoodie"</span>
                  <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full h-full object-center object-cover"</span>
                  /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"aspect-w-4 aspect-h-5 sm:rounded-lg sm:overflow-hidden lg:aspect-w-3 lg:aspect-h-4"</span> &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
               <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/img/04.png"</span>
               <span class="hljs-attr">alt</span>=<span class="hljs-string">"The gray men hoodie"</span>
               <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full h-full object-center object-cover"</span>
               /&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Product info --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"max-w-2xl mx-auto pt-10 pb-8 px-4 sm:px-6 lg:max-w-7xl lg:pt-16 lg:px-8 lg:grid lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8"</span> &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"lg:col-span-2 lg:border-r lg:border-gray-200 lg:pr-8"</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-2xl font-extrabold tracking-tight text-gray-900 sm:text-3xl"</span> &gt;</span>
            Sport Grey Unisex Hoodie Sweatshirt
         <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

         <span class="hljs-comment">&lt;!-- Description and details --&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"py-4"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>&gt;</span>Description<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"space-y-6"</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-base text-gray-900"</span>&gt;</span>
                  This comfortable hoodie is made of 100% combed ring-spun
                  cotton except for heather black logo, which contains
                  polyester. Our premium Unisex Sport Hoodie Sweatshirt is
                  everything you could ask for: it's warm and cozy,
                  heavyweight, unique, roomy, and it's built to last. Sign up
                  for our subscription service and be the first to get new,
                  exciting colors, like our upcoming "Charcoal Gray" limited
                  release.
               <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h3 id="heading-2-add-configuration">2️⃣ Add Configuration</h3>
<p>Add the code below to <code>&lt;body&gt;</code> section of the <code>index.html</code> file just before the <code>&lt;/body&gt;</code> tag and add the required publishable configuration data.</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- Config --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"clayer-config"</span>
  <span class="hljs-attr">data-base-url</span>=<span class="hljs-string">"&lt;yourOrganizationUrl&gt;"</span>
  <span class="hljs-attr">data-cache</span>=<span class="hljs-string">"true"</span>
  <span class="hljs-attr">data-client-id</span>=<span class="hljs-string">"&lt;yourClientId&gt;"</span>
  <span class="hljs-attr">data-market-id</span>=<span class="hljs-string">"&lt;yourMarketId&gt;"</span>
  <span class="hljs-attr">data-country-code</span>=<span class="hljs-string">"US"</span>
  <span class="hljs-attr">data-language-code</span>=<span class="hljs-string">"en"</span>
  <span class="hljs-attr">data-cart-url</span>=<span class="hljs-string">"&lt;https://example.com/cart&gt;"</span>
  <span class="hljs-attr">data-return-url</span>=<span class="hljs-string">"&lt;https://example.com/return&gt;"</span>
  <span class="hljs-attr">data-privacy-url</span>=<span class="hljs-string">"&lt;https://example.com/privacy&gt;"</span>
  <span class="hljs-attr">data-terms-url</span>=<span class="hljs-string">"&lt;https://example.com/terms&gt;"</span>
  <span class="hljs-attr">data-dev-settings-debug</span>=<span class="hljs-string">"true"</span>
  <span class="hljs-attr">data-dev-settings-console</span>=<span class="hljs-string">"true"</span>
  <span class="hljs-attr">data-dev-settings-trace</span>=<span class="hljs-string">"true"</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

<span class="hljs-comment">&lt;!-- JS Library --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"&lt;https://cdn.jsdelivr.net/npm/@commercelayer/js-dropin@1.5.12/lib/index.js&gt;"</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<ul>
<li><code>clayer-config</code> — stores all the data attributes with your credentials and page preferences.</li>
<li><code>data-base-url</code> — stores your organization base endpoint as defined on Commerce Layer (something like <code>https://yourdomain.commercelayer.io</code>).</li>
<li><code>data-client-id</code> — stores your sales channel application's client ID.</li>
<li><code>data-market-id</code> stores the <em>number</em> of the market you want to work on as defined in Commerce Layer.</li>
<li><code>data-country-code</code> — stores the country code of the country assigned to an order (e.g <code>US</code>)</li>
<li><code>data-language-code</code> — stores the language assigned to an order (e.g <code>en-US</code>).</li>
<li><code>data-cart-url</code>, <code>data-return-url</code>, <code>data-privacy-url</code>, <code>data-terms-url</code> — store the links to external pages that Commerce Layer should redirect your user to upon certain requests (we don't need these for this tutorial, so we'll add some dummy links).</li>
</ul>
<h3 id="heading-3-add-a-price">3️⃣ Add a Price</h3>
<p>Add the markup below to fetch the selling price (amount) and original price (compare-at-amount) of an SKU. You should add an element with <code>clayer-price</code> class and <code>data-sku-code</code> data attribute wherever you want to show a product price. The child element with class <code>amount</code> gets populated with the price that has been defined in Commerce Layer for that SKU code (for the market defined in the configuration step above). The child element with class <code>compare-at-amount</code> gets populated only if compare-at-amount is defined and is higher than the amount. The <code>price</code> ID is used to match the <code>data-price-container-id</code> in the variant options (as seen in the next section).</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Price --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>Product information<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
         <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-price"</span>
         <span class="hljs-attr">id</span>=<span class="hljs-string">"price"</span>
         <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000XLXX"</span>
         &gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... amount"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... compare-at-amount"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Config [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- JS Library [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h3 id="heading-4-add-variants-selection">4️⃣ Add Variants Selection</h3>
<p>If your product has many variants, add an element with <code>clayer-variant</code> class and <code>data-sku-code</code> data attribute for each variant option. This will fetch the SKU for each variant from Commerce Layer. You can use either a select tag or a list of radio buttons (appending the class <code>clayer-variant-select</code> or <code>clayer-variant-radio</code> respectively). You can also add other data attributes that will be used in the shopping bag based on a user's variant selection such as:</p>
<ul>
<li><code>data-sku-name</code> — the name of the selected SKU to be displayed in the shopping bag.</li>
<li><code>data-sku-reference</code> — the reference of the selected SKU to be displayed in the shopping bag.</li>
<li><code>data-sku-image-url</code> — the URL to an image of the SKU to be displayed in the shopping bag.</li>
<li><code>data-price-container-id</code> — the ID of the DOM element that contains the price for this SKU, in the current market (potentially different variants can have different prices; when selecting a new variant, the price will be updated accordingly).</li>
<li><code>data-availability-message-container-id</code> — the ID of the DOM element that contains the availability message for this SKU, in the current market (more on this later).</li>
<li><code>data-add-to-bag-id</code>: the ID of the "Add to bag" DOM element (when clicking the "Add to bag" button with the specified ID, the selected variant will be added to bag).</li>
<li><code>data-add-to-bag-quantity-id</code> — the ID of the "Add to bag quantity" DOM element (when clicking the "Add to bag" button with the specified ID, the selected quantity of the variant will be added to bag).</li>
</ul>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Price  [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Variants (select sizes) --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>Choose a size<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-4 flex -mx-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 px-2"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">select</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"variant"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant-select ..."</span>
            <span class="hljs-attr">data-sku-reference</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000"</span>
            <span class="hljs-attr">data-price-container-id</span>=<span class="hljs-string">"price"</span>
            <span class="hljs-attr">data-availability-message-container-id</span>=<span class="hljs-string">"availability-message"</span>
            <span class="hljs-attr">data-add-to-bag-id</span>=<span class="hljs-string">"add-to-bag"</span>
            <span class="hljs-attr">data-add-to-bag-quantity-id</span>=<span class="hljs-string">"add-to-bag-quantity"</span>
          &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">""</span> <span class="hljs-attr">disabled</span> <span class="hljs-attr">selected</span>&gt;</span>Select variant<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
              <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000SXXX"</span>
              <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (S)"</span>
              <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
            &gt;</span>Small
            <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
              <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000MXXX"</span>
              <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (M)"</span>
              <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
            &gt;</span>Medium
            <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
              <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000LXXX"</span>
              <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (L)"</span>
              <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
            &gt;</span>Large
            <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-variant"</span>
              <span class="hljs-attr">data-sku-code</span>=<span class="hljs-string">"SWEETHMUB7B7B7000000XLXX"</span>
              <span class="hljs-attr">data-sku-name</span>=<span class="hljs-string">"Sport Grey Unisex Hoodie Sweatshirt with Black Logo (XL)"</span>
              <span class="hljs-attr">data-sku-image-url</span>=<span class="hljs-string">"https://img.commercelayer.io/skus/SWEETHMUB7B7B7000000.png?fm=jpg&amp;q=90"</span>
            &gt;</span>Extra Large
            <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

           <span class="hljs-comment">&lt;!-- Other variant input elements [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Config [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- JS Library [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<h3 id="heading-5-add-to-bag">5️⃣ Add to Bag</h3>
<p>When an option (variant) is selected and the "Add to bag" button is clicked, the selected variant is added to the shopping bag using the <code>clayer-add-to-bag</code> class and ID that matches the variants' <code>data-add-to-bag-id</code>. You can also optionally add a quantity field to let the customer add more than one item to the shopping bag using the <code>clayer-add-to-bag-quantity</code> attribute.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Price  [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Variants (select sizes) --&gt;</span>

      <span class="hljs-comment">&lt;!-- Add to bag quantity --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"add-to-bag-quantity"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>Add to bag quantity<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">id</span>=<span class="hljs-string">"add-to-bag-quantity"</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span>
              <span class="hljs-attr">min</span>=<span class="hljs-string">"1"</span>
              <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-add-to-bag-quantity ..."</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Add to bag button --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
         <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span>
         <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-add-to-bag ..."</span>
         <span class="hljs-attr">id</span>=<span class="hljs-string">"add-to-bag"</span>
         <span class="hljs-attr">data-availability-message-container-id</span>=<span class="hljs-string">"availability-message"</span>
         &gt;</span>
      Add to bag
      <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Config [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- JS Library [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h3 id="heading-6-add-availability-message-templates">6️⃣ Add Availability Message Templates</h3>
<p>Add an element with <code>clayer-availability-message-available-template</code> ID as the template tag to be displayed when the selected SKU is available. All the child elements will be populated with the delivery lead time and shipping method information related to the selected variant.</p>
<p>Add an element with <code>clayer-availability-message-unavailable-template</code> ID as the template tag to be displayed when the selected SKU is not available. This element will be appended to a specific container when customers will try to add an SKU to the shopping bag and that SKU’s stock item has a quantity of zero or doesn’t exist at all. If you try to create a line item, you will get an "out of stock" error. The <code>clayer-availability-message-unavailable-template</code> is used to store an "out of stock" message.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Price  [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Variants (select sizes) --&gt;</span>

      <span class="hljs-comment">&lt;!-- Add to bag quantity --&gt;</span>

      <span class="hljs-comment">&lt;!-- Availability message templates --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
         <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-availability-message-container"</span>
         <span class="hljs-attr">id</span>=<span class="hljs-string">"availability-message"</span>
         &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"clayer-availability-message-available-template"</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
            Available in
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-availability-message-available-min-days"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            -
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-availability-message-available-max-days"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            days with
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-availability-message-available-shipping-method-name"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            (<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-availability-message-available-shipping-method-price"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>)
         <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"clayer-availability-message-unavailable-template"</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>The selected SKU is not available.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Config [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- JS Library [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h3 id="heading-7-add-shopping-bag-summary">7️⃣ Add Shopping Bag Summary</h3>
<p>The shopping bag includes a list of all items added to the bag alongside a summary of the current shopping bag details. Add an element with <code>clayer-shopping-bag-container</code> ID wherever you want to show the shopping bag. The <code>clayer-shopping-bag-items-container</code> is used as the parent element of the shopping bag line items, built from the <code>clayer-shopping-bag-item-template</code> template tag. The lists below explain what each child data attribute does.</p>
<p><strong>The line item template elements:</strong></p>
<ul>
<li><code>clayer-shopping-bag-item-name</code> — will be populated with the name of the SKU.</li>
<li><code>clayer-shopping-bag-item-unit-amount</code> — displays the unit amount of the SKU.</li>
<li><code>clayer-shopping-bag-item-qty-container</code> — displays an input field with the quantity of the SKU added to the bag (you can use this to update the quantity too).</li>
<li><code>clayer-shopping-bag-item-availability-message-container</code> — displays an “out of stock” message (contained in <code>clayer-availability-message-unavailable-template</code>) when you change a quantity and that quantity is not available.</li>
<li><code>clayer-shopping-bag-item-total-amount</code> — displays the total amount of a line item.</li>
<li><code>clayer-shopping-bag-item-remove</code> — displays a link to remove an item from the shopping bag.</li>
</ul>
<p><strong>The order summary elements:</strong></p>
<ul>
<li><code>clayer-shopping-bag-subtotal</code> — displays the subtotal amount of the entire shopping bag.</li>
<li><code>clayer-shopping-bag-shipping</code> — displays the shipping cost of the order.</li>
<li><code>clayer-shopping-bag-payment</code> — displays the selected payment method charge (if any).</li>
<li><code>clayer-shopping-bag-discount</code> — displays the discount amount of the order (if applied).</li>
<li><code>clayer-shopping-bag-taxes</code> — displays the cost of tax (if present).</li>
<li><code>clayer-shopping-bag-total</code> — displays the grand total summing up all the above costs for the order.</li>
</ul>
<p>By default, the shopping bag container is always closed. The <code>clayer-shopping-bag-toggle</code> element toggles an open class (attached to a button) on the shopping bag container.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Price  [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Variants (select sizes) --&gt;</span>

      <span class="hljs-comment">&lt;!-- Add to bag quantity --&gt;</span>

      <span class="hljs-comment">&lt;!-- Availability message templates --&gt;</span>

      <span class="hljs-comment">&lt;!-- Shopping bag --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>Shopping bag<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
         Your shopping bag contains
         <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-items-count"</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
         items worth
         <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-total"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-toggle"</span>&gt;</span>
      Toggle Bag <span class="hljs-symbol">&amp;#8645;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"clayer-shopping-bag-container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">table</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">thead</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">th</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
                  SKU
               <span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">th</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
                  Unit price
               <span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">th</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
                  Q.ty
               <span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">th</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
                  Total
               <span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">th</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               <span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"clayer-shopping-bag-items-container"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"clayer-shopping-bag-item-template"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-item-name"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
               <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-item-unit-amount"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-item-qty-container"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
                     <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span>
                     <span class="hljs-attr">min</span>=<span class="hljs-string">"1"</span>
                     <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>
                     /&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">span</span>
                     <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-item-availability-message-container"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
               <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-item-total-amount"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-item-remove"</span>&gt;</span>❌<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
               <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               Subtotal:
               <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-subtotal"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               Shipping:
               <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-shipping"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               Payment:
               <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-payment"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               Discount:
               <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-discount"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               Taxes:
               <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-taxes"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"..."</span>&gt;</span>
               Total:
               <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"clayer-shopping-bag-total"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Config [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- JS Library [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h3 id="heading-8-add-checkout-button">8️⃣ Add Checkout Button</h3>
<p>Add an element with <code>clayer-shopping-bag-checkout</code> class wherever you want to show the checkout button. This button will link to a dedicated checkout page where a customer can complete their purchase and place the order. We'll explain how this works soon.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- Static content [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Price  [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- Variants (select sizes) --&gt;</span>

      <span class="hljs-comment">&lt;!-- Add to bag quantity --&gt;</span>

      <span class="hljs-comment">&lt;!-- Availability message templates --&gt;</span>

      <span class="hljs-comment">&lt;!-- Shopping bag --&gt;</span>

      <span class="hljs-comment">&lt;!--Checkout button--&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"... clayer-shopping-bag-checkout"</span>&gt;</span>
      Proceed to checkout
      <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>

      <span class="hljs-comment">&lt;!-- Config [...] --&gt;</span>

      <span class="hljs-comment">&lt;!-- JS Library [...] --&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Coupled together, we get a nice-looking product page with some static content and some elements updated dynamically. In summary, once a user selects a variant and clicks the "Add to bag" button, we pass the data down with help from the specified data attributes and create an order associated with some line items. If there are any errors, we display them in the availability message container. The customer can then decide to check out the order.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644978689499/DiKqjiPMn.jpeg" alt="headless-swag-store-demo-cart.jpg" /></p>
<h2 id="heading-checkout-functionality">Checkout Functionality</h2>
<p>The Drop-in library uses the <a target="_blank" href="https://github.com/commercelayer/commercelayer-react-checkout">Commerce Layer checkout application</a> to provide a PCI-compliant, PSD2-compliant, and production-ready checkout flow powered by Commerce Layer APIs. The orders API will return the checkout URL using the format <code>&lt;your organization slug&gt;.checkout.commercelayer.app/:order_id?accessToken=&lt;token&gt;</code>. The Drop-in library will then automatically pass that URL to the checkout button (with class <code>clayer-shopping-bag-checkout</code>) alongside the order ID and required access token. When you click on the checkout button, you will be redirected to the checkout application. Upon successful checkout, the order will be sent to and can be managed in the orders management system (OMS), in the Commerce Layer dashboard. So cool, right 🙃?</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://commercelayer.io/blog/introducing-our-hosted-checkout-application">https://commercelayer.io/blog/introducing-our-hosted-checkout-application</a></div>
<h2 id="heading-deploying-the-final-project">Deploying the Final Project</h2>
<p>Now let's push our project to GitHub and deploy it to Netlify or Vercel. Whenever you push any change to GitHub, both tools will run your build and re-deploy the repository again. You can follow the guidelines in the documentation for <a target="_blank" href="https://docs.netlify.com/site-deploys/create-deploys/">Netlify</a> or <a target="_blank" href="https://vercel.com/docs/concepts/deployments/overview">Vercel</a> to get set up for either tool.</p>
<h2 id="heading-additional-resources">Additional resources</h2>
<p>An order can consist of a customer, line items (SKUs, cost of shipping method, cost of payment method, taxes, or gift cards), a billing address, a shipping address, discount(s) calculated from active promotions, redeemed gift card(s), a payment method, and a payment source type. You can <a target="_blank" href="https://commercelayer.io/docs/data-model/anatomy-of-an-order">read this guide</a> to learn more about the anatomy of an order and check out the <a target="_blank" href="https://docs.commercelayer.io/api">Commerce Layer API reference</a>. Also, check out the <a target="_blank" href="https://commercelayer.io/docs/data-model">Commerce Layer data model</a> for an overview of the most relevant API entities, their mutual relationships, and common usage (including E-R diagrams).</p>
<p>Here are some more resources to learn about headless commerce, Commerce Layer, and data attributes:</p>
<ul>
<li><a target="_blank" href="https://commercelayer.io/why">Why Commerce Layer?</a></li>
<li><a target="_blank" href="https://commercelayer.io/guides/headless-commerce">Comprehensive Guide to Headless Commerce</a></li>
<li><a target="_blank" href="https://commercelayer.io/docs/core-concepts">Commerce Layer Core Concepts</a></li>
<li><a target="_blank" href="https://commercelayer.io/developers">Commerce Layer Developer Resources</a></li>
<li><a target="_blank" href="https://commercelayer.io/jamstack">Jamstack Whitepaper</a></li>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">Using data attributes</a></li>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset">HTMLOrForeignElement.dataset</a></li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Yes! You have successfully turned a website into a <a target="_blank" href="https://headless-swag-store.netlify.app/">shoppable website</a> using Commerce Layer and the Drop-in Library; how cool is that?! 😃. </p>
<p>With <a target="_blank" href="https://commercelayer.io/">Commerce Layer</a>, you can manage all transactional parts of your ecommerce business, integrate with any CMS of your choice to manage content, build your user interface/experience with your favorite technologies, and build your business logic without friction. In addition, this will result in more flexibility and autonomy between sub-teams in your engineering team, thereby helping you scale your business globally.</p>
<p>This is a great way to quickly start building with Commerce Layer and can be used for specific use cases such as adding ecommerce to a Jamstack website or transforming a Shopify theme into a multi-market store. To create a comprehensive global shopping platform for your brand, check out our <a target="_blank" href="https://docs.commercelayer.io">documentation</a> and <a target="_blank" href="https://slack.commercelayer.app">join our Slack community</a> to learn about the possibilities.</p>
]]></content:encoded></item><item><title><![CDATA[How to Setup RStudio for Data Visualization]]></title><description><![CDATA[RStudio is the best integrated development environment (IDE) for data science teams that use the R programming language. It includes a syntax-highlighting editor and console alongside plotting, debugging, and workspace management tools. In addition, ...]]></description><link>https://blog.bolajiayodeji.com/how-to-setup-rstudio-for-data-visualization</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-setup-rstudio-for-data-visualization</guid><category><![CDATA[R Language]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[#data visualisation]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Wed, 15 Sep 2021 07:20:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1631689210625/7Jce70iW7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>RStudio is the best integrated development environment (IDE) for data science teams that use the R programming language. It includes a syntax-highlighting editor and console alongside plotting, debugging, and workspace management tools. In addition, R Studio makes working with R easier by providing a much more friendly and robust GUI environment for working with R. In this article; I'll show you how to install the R programming language and R Studio on your most preferred operating system.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631685662544/UX-jnGlg2.png" alt="RStudio products catalog" /></p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Background knowledge of R programming language.</li>
<li>A computer machine running Windows, Linux, or macOS operating system.</li>
<li>Ability to use the web browser and command-line interface.</li>
<li>A little smile? :)</li>
</ul>
<h2 id="r-installation-guide">R Installation Guide</h2>
<p>To use RStudio, you need to install the R programming language first. R is a free software environment for statistical computing and graphics. Next, you have to choose your preferred CRAN (Comprehensive R Archive Network) mirror from the available options to download R. The Comprehensive R Archive Network (CRAN) is a network of FTP and web servers around the world that store identical, up-to-date versions of R and acts as the primary web service distributing R sources, binaries, extension packages, and documentation.</p>
<p>To save yourself some stress, you can choose the <code>0-Cloud</code> option, as that will automatically redirect you to the best and closest server.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631599646261/xE--02zOq.png" alt="A screenshot of the CRAN mirrors page" /></p>
<h3 id="installing-on-windows">Installing on Windows</h3>
<p>Download the latest version of the base distribution of R <a target="_blank" href="https://cloud.r-project.org/bin/windows/base">here</a>. This should download a file with the name <code>R-4.1.1-win.exe</code> (version name would change in the future) that will work on 32 and 64 bits. You can then run the installer file and install as you normally <a target="_blank" href="https://www.wikihow.com/Open-EXE-Files">install .exe files</a>. If you want, you can read the Windows <a target="_blank" href="https://cloud.r-project.org/bin/windows/base/README.R-4.1.1">installation guide</a> for more instructions.</p>
<h3 id="installing-on-linux">Installing on Linux</h3>
<p>Download the latest version of R for Debian, Fedora, Redhat, Suse, and Ubuntu <a target="_blank" href="https://cloud.r-project.org/bin/linux">here</a>. Alternatively, you can use the commands below to install from your terminal.</p>
<h4 id="for-debian">For Debian</h4>
<pre><code class="lang-bash">apt-get update
apt-get install r-base r-base-dev
</code></pre>
<p>If you want, you can read the Debian <a target="_blank" href="https://cloud.r-project.org/bin/linux/debian/">installation guide</a> for more instructions.</p>
<h4 id="for-fedora">For Fedora</h4>
<pre><code class="lang-bash">sudo dnf install R
</code></pre>
<p>If you want, you can read the Fedora <a target="_blank" href="https://cloud.r-project.org/bin/linux/fedora/">installation guide</a> for more instructions.</p>
<h4 id="for-redhat">For Redhat</h4>
<pre><code class="lang-bash">sudo dnf install R
</code></pre>
<p>If you want, you can read the Redhat <a target="_blank" href="https://cloud.r-project.org/bin/linux/redhat/">installation guide</a> for more instructions.</p>
<h4 id="for-suse">For Suse</h4>
<pre><code class="lang-bash">VERSION=$(grep <span class="hljs-string">"^PRETTY_NAME"</span> /etc/os-release | tr <span class="hljs-string">" "</span> <span class="hljs-string">"_"</span> | sed -e <span class="hljs-string">'s/PRETTY_NAME=//'</span> | sed -e <span class="hljs-string">'s/"//g'</span>)
zypper addrepo -f http://download.opensuse.org/repositories/devel\:/languages\:/R\:/patched/<span class="hljs-variable">$VERSION</span>/ R-base
</code></pre>
<pre><code class="lang-bash">zypper install R-base R-base-devel
</code></pre>
<p>If you want, you can read the Suse <a target="_blank" href="https://cloud.r-project.org/bin/linux/suse/README.html">installation guide</a> for more instructions.</p>
<h4 id="for-ubuntu">For Ubuntu</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># update indices</span>
apt update -qq

<span class="hljs-comment"># install required helper packages</span>
apt install --no-install-recommends software-properties-common dirmngr

<span class="hljs-comment"># add the signing key</span>
wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | sudo tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc

<span class="hljs-comment"># add the R 4.0 repo from CRAN</span>
add-apt-repository <span class="hljs-string">"deb https://cloud.r-project.org/bin/linux/ubuntu <span class="hljs-subst">$(lsb_release -cs)</span>-cran40/"</span>
</code></pre>
<pre><code class="lang-bash">apt install --no-install-recommends r-base
</code></pre>
<p>If you want, you can read the Ubuntu <a target="_blank" href="https://cloud.r-project.org/bin/linux/ubuntu/">installation guide</a> for more instructions.</p>
<h3 id="installing-on-macos">Installing on macOS</h3>
<p>Download the latest version of the base distribution of R <a target="_blank" href="https://cloud.r-project.org/bin/macosx/">here</a>. This should download a file with the name <code>R-4.1.1.pkg</code> (version name would change in the future). You can then run the installer file and install as you normally <a target="_blank" href="https://www.laptopmag.com/articles/install-unininstall-mac-software">install .pkg files</a>.</p>
<p>If you want, you can read the macOS <a target="_blank" href="https://cloud.r-project.org/bin/macosx/">installation guide</a> for more instructions.</p>
<h2 id="r-studio-installation-guide">R Studio Installation Guide</h2>
<p>There are two versions of RStudio; the RStudio Desktop (the desktop app) and RStudio Server (the server running on a web browser). Both have free and paid editions with several additional features, as seen in the screenshots taken from the R Studio official website below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631686827205/GfW9Ow7Jw.png" alt="RStudio Desktop features" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631686844085/mr017Mq7B.png" alt="RStudio Server features" /></p>
<p>To download either of the versions, head to the <a target="_blank" href="https://www.rstudio.com/products/rstudio/download/">download page</a> and click on the "download" button for RStudio Desktop or RStudio Server. Alternatively, you can click on the "buy" button if you want to purchase the other editions. Once you click on "download," you will be redirected to the download page, which will automatically detect your operating system and recommend the file to download (this works only for RStudio Desktop. You can find the RStudio Server <a target="_blank" href="https://www.rstudio.com/products/rstudio/download-server/">installation guide</a> here). Below that, you will also find a list of other installers for different operating systems. So, pick what you want and download the file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631687450808/nz6dN-bxg.png" alt="RStudio Desktop download page" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>Upon successful installation, you can now use RStudio Desktop to write your R programs whilst enjoying all the benefits of R Studio. As seen in the screenshot below from my setup, you'll get the editor at the left-corner, console, terminal, and jobs at the bottom-left-corner, environment at the right-corner, and plots/packages, at the bottom-right corner of the application window. Of course, you can customize the arrangement, change your theme, download R packages, and other settings as you deem fit.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631688076708/BlLt7aW7wp.png" alt="RStudio Desktop demo" /></p>
<p>That's all for this tutorial, and I hope it was helpful. I'll be publishing an introductory guide to the R programming language for beginners soon, so ensure to <a target="_blank" href="https://bawd.bolajiayodeji.com/">subscribe to my newsletter</a> to get updated. For RStudio product guides and helpful tutorials, you can check out the <a target="_blank" href="https://docs.rstudio.com/">RStudio documentation page</a>. Cheers! 💙</p>
<h2 id="attributions">Attributions</h2>
<ul>
<li><a target="_blank" href="https://www.r-project.org">R Project</a></li>
<li><a target="_blank" href="https://www.rstudio.com">RStudio product website</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=_V8eKsto3Ug">R Programming Tutorial - Learn the Basics of Statistical Computing</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Career Update: Joining the Commerce Layer Team]]></title><description><![CDATA[Hey, awesome 👋,
I trust you're doing well and staying safe.
The past couple of weeks have been challenging and tiring from the fight against police brutality and protests to #EndSars ✊🏾 in Nigeria 🇳🇬. 
Amidst all the struggles, I'm excited to sha...]]></description><link>https://blog.bolajiayodeji.com/career-update-joining-the-commerce-layer-team</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/career-update-joining-the-commerce-layer-team</guid><category><![CDATA[General Programming]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[JAMstack]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Mon, 26 Oct 2020 10:21:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1603705287115/7XKCn8oFW.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey, awesome 👋,</p>
<p>I trust you're doing well and staying safe.</p>
<p>The past couple of weeks have been challenging and tiring from the fight against police brutality and protests to <a target="_blank" href="https://en.wikipedia.org/wiki/End_SARS">#EndSars ✊🏾</a> in Nigeria 🇳🇬. </p>
<p>Amidst all the struggles, I'm excited to share some good news and announce that I have joined the distinguished team at <a target="_blank" href="https://commercelayer.io">Commerce Layer</a> as a Developer Advocate.</p>
<h2 id="quick-intro-to-commerce-layer">Quick Intro to Commerce Layer</h2>
<p>Web development today has shifted from the traditional ways and is completely different from what it was a few years ago. Commerce Layer is a purely transactional engine based platform that integrates with any CMS of your choice, adding international shopping capabilities to ANY presentation layer — the perfect solution to build e-commerce on the JAMstack, providing the highest performance and reducing the cost of infrastructure.</p>
<p>As a headless commerce platform and order management system, Commerce Layer lets you add global shopping capabilities to any website, mobile app, chatbot, or IoT device, with ease via our commerce APIs.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/commercelayer/status/1289153056811003905">https://twitter.com/commercelayer/status/1289153056811003905</a></div>
<h2 id="okay">Okay?</h2>
<p>I'm passionate about the JAMstack ecosystem and can't wait to do amazing stuff with the community of JAMstack and e-commerce developers at Commerce Layer. You'll find me creating, amplifying &amp; looking for the right examples, messaging, and languages for audiences using our <a target="_blank" href="https://commercelayer.io/developers">developer's documentation</a>, <a target="_blank" href="https://github.com/commercelayer">open-source project's</a>, and a bunch of random DevRel stuff.</p>
<p>If you have any questions about Commerce Layer or any of our services, feel free to DM me on <a target="_blank" href="https://twitter.com/iambolajiayo">Twitter</a> or join our Slack by requesting an invite <a target="_blank" href="https://commercelayer.io/developers">here</a>.</p>
<p>Keep trailblazing, and stay safe! 💙</p>
]]></content:encoded></item><item><title><![CDATA[How to Create File Archives Using the Linux Tape Archive (TAR) Command]]></title><description><![CDATA[We manage many files and directories on our computer, and most times, it's a lot. You can organize and sort these files in folders for easier access and management. However, what do you do when you have to share these files with another user or compu...]]></description><link>https://blog.bolajiayodeji.com/how-to-create-file-archives-using-the-linux-tape-archive-tar-command</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-create-file-archives-using-the-linux-tape-archive-tar-command</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[Bash]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Fri, 18 Sep 2020 05:29:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1601527504498/z-pnLN8rT.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We manage many files and directories on our computer, and most times, it's a lot. You can organize and sort these files in folders for easier access and management. However, what do you do when you have to share these files with another user or computer?</p>
<p>In this article, I'll show you how to combine files or directories in an archive, compress these archives (reducing the final archive file size) alongside how to extract the collected files from the archive using the CLI.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Before you begin this tutorial, you'll need the following:</p>
<ul>
<li>A shell running on your computer (CommandPromt, Bash, Zsh e.t.c)</li>
<li>Bunch of files and directories on your computer. If you don't have some, you can create some files and directories (if you want to) with the command below:</li>
</ul>
<pre><code class="lang-bash">mkdir folder{1..10}
touch file{1..10}
</code></pre>
<ul>
<li>How about a small smile? 😊</li>
</ul>
<h2 id="introduction-to-cli">Introduction to CLI</h2>
<p>As opposed to the GUI (Graphical User Interface), which provides users with a user interface with which they can interact with a computer, the CLI (Command Line Interface) allows users to input in some commands meant to initiate some action on the computer in the form of text via a terminal. These commands will run via a shell with the desired result displayed back on the terminal.</p>
<p>One fantastic feature of the operating system is the Command Line Interface (CLI), which allows users to interact with their computer from a shell. This shell is a REPL (Read, Evaluate, Print, Loop) environment where users can enter a command, and the shell runs it and returns a result.</p>
<h2 id="how-to-create-an-archive-file-or-directory-with-tar">How to Create an Archive (File or Directory) with Tar</h2>
<p>Tar (an abbrevation for <strong>T</strong>ape <strong>Ar</strong>chive) is a Linux command used to create a <code>.tar</code>, <code>.tar.gz</code>, or <code>.tar.bz2</code> archive file (also known as "tarballs").</p>
<p>You can combine files or directories into an archive with the command below like so:</p>
<pre><code class="lang-bash">tar -cf archive-name.tar /path/to/file-or-directory
</code></pre>
<p>You should now see a file called <code>archive-name.tar</code> containing the files or directories in <code>/path/to/file-or-directory</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600242874584/dioQXxd4H.png" alt="My CLI.png" /></p>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-c</code> is the flag that <strong>c</strong>reates the archive file.</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
</ul>
<p>Let's run <code>ls -lshS</code> to list the files in our current directory with their file sizes in a readable format and sort the files from biggest to smallest.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600243800156/zHDCc3ptm.png" alt="My CLI.png" /></p>
<p>The contents of the <strong>node_modules</strong> directory with the initial <strong>32K</strong> size compressed into the archive will result in a <strong>174M</strong> size.</p>
<h2 id="how-to-create-an-archive-and-compress-with-gzip">How to Create an Archive and Compress with GZIP</h2>
<p>By default, we want our archive files to be compressed and produce lesser file sizes (I do, don't you? 😌); however, the previous command only collected the files into the archive. To compress your final archive, you'll append the <code>-z</code> flag, which will use GZIP to automatically compress the archive and produce a <code>.tar.gz</code> file.</p>
<blockquote>
<p>gzip is a file format and a software application used for file compression and decompression. The program was created by Jean-loup Gailly and Mark Adler as a free software replacement for the compress program used in early Unix systems, and intended for use by GNU ~ <a target="_blank" href="https://en.wikipedia.org/wiki/Gzip">Wikepedia</a></p>
</blockquote>
<p>Let's test this with the same <strong>node_modules</strong> folder like so:</p>
<pre><code class="lang-bash">tar -zcf archive-name.tar.gz node_modules
</code></pre>
<p>You should now see a file called <code>archive-name.tar.gz</code> containing the files and directories in <code>node_modules</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600244635172/Jrmrmbq-S.png" alt="My CLI.png" /></p>
<p>Let's run <code>ls -lshS</code> to list the files in our current directory with their file sizes in a readable format and sort the files from biggest to smallest.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600245283232/xB1F214-u.png" alt="My CLI.png" /></p>
<p>The contents of the <strong>node_modules</strong> directory with the initial <strong>32K</strong> size compressed into the archive will result in a <strong>34M</strong> size.</p>
<p>Notice that the <code>archive-name.tar.gz</code> is significantly smaller than <code>archive-name.tar</code>? This is because we used gzip compression.</p>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-z</code> is the flag that compresses the archive with g<strong>z</strong>ip.</li>
<li><code>-c</code> is the flag that <strong>c</strong>reates the archive file.</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
</ul>
<h2 id="how-to-create-an-archive-and-compress-with-bzip2">How to Create an Archive and Compress with BZIP2</h2>
<p>GZIP is the most frequently used compression method; however, there's a BZIP2 method that compresses more than GZIP but takes more time to execute. GZIP is faster, but it generally compresses a bit less (larger file) while BZIP2 is slower, but it compresses a bit more (smaller file). </p>
<blockquote>
<p>bzip2 is a free and open-source file compression program that uses the Burrows-Wheeler algorithm. It only compresses single files and is not a file archiver. It is developed by Julian Seward and maintained by Federico Mena. Seward made the first public release of bzip2, version 0.15, in July 1996. ~ <a target="_blank" href="https://en.wikipedia.org/wiki/Bzip2">Wikipedia</a></p>
</blockquote>
<p>Let's test this with the same <strong>node_modules</strong> folder like so:</p>
<pre><code class="lang-bash">tar -jcf archive-name.tar.bz2 node_modules
</code></pre>
<p>You should now see a file called <code>archive-name.tar.bz2</code> containing the files and directories in <code>node_modules</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600248812087/2viU9PtTl.png" alt="My CLI.png" /></p>
<p>Let's run <code>ls -lshS</code> to list the files in our current directory with their file sizes in a readable format and sort the files from biggest to smallest.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600248915728/f7_fZzAub.png" alt="My CLI.png" /></p>
<p>The contents of the <strong>node_modules</strong> directory with the initial <strong>32K</strong> size compressed into the archive will result in a <strong>27M</strong> size.</p>
<p>Notice that the <code>archive-name.tar.bz2</code> is significantly smaller than <code>archive-name.tar.gz</code>? This is because we used bzip2 compression.</p>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-j</code> is the flag that compresses the archive with bzip2.</li>
<li><code>-c</code> is the flag that <strong>c</strong>reates the archive file.</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
</ul>
<h2 id="how-to-compress-multiple-files-or-directories-at-once">How to Compress Multiple Files or Directories at Once</h2>
<p>You can compress multiple files or directories at once using the same command and specifying the paths of the files or directories with a space separating each path.</p>
<pre><code class="lang-bash">tar -cf archive-name.tar /path/to/file /path/to/directory /path/to/file-or-directory
</code></pre>
<h2 id="how-to-list-the-contents-of-an-archive-file">How to List the Contents of an Archive File</h2>
<p>You can list the content of an <code>archive-name.tar</code>, <code>archive-name.tar.gz</code>, or <code>archive-name.tar.bz2</code> file by adding the <code>-t</code> flag to their respective <code>tar</code> command.</p>
<blockquote>
<p>If you're familiar with <a target="_blank" href="https://www.vim.org/">Vim</a>, you can easily list out the contents using <code>vim archive-name.tar</code>. However, let's learn how to do this with the <code>Tar</code> command.</p>
</blockquote>
<h3 id="uncompressed-archive">Uncompressed Archive</h3>
<pre><code class="lang-bash">tar -tf archive-name.tar
</code></pre>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-t</code> is the flag that lists the content of the archive</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
</ul>
<h3 id="compressed-archive-gzip">Compressed Archive (GZIP)</h3>
<pre><code class="lang-bash">tar -ztf archive-name.tar.gz
</code></pre>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-z</code> is the flag that compresses the archive with g<strong>z</strong>ip.</li>
<li><code>-t</code> is the flag that lists the content of the archive</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
</ul>
<h3 id="compressed-archive-bzip2">Compressed Archive (BZIP2)</h3>
<pre><code class="lang-bash">tar -jtf archive-name.tar.bz2
</code></pre>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-j</code> is the flag that compresses the archive with bzip2.</li>
<li><code>-t</code> is the flag that lists the content of the archive</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
</ul>
<h2 id="how-to-extract-the-contents-of-an-archive-file">How to Extract the Contents of an Archive File</h2>
<p>Now you've learned how to create an archive file, compress, and list out its content. Let's extract the archive file (which should be your final destination, yeah?).</p>
<pre><code class="lang-bash">tar -xf archive-name.tar
</code></pre>
<p>By default, the archive content will be extracted into the current working directory. You can then specify a directory to extract to by appending an additional <code>-C</code> flag and the destination path like so:</p>
<pre><code class="lang-bash">tar -xf archive-name.tar -C /destination-path/
</code></pre>
<p>Here's a breakdown of the command we used:</p>
<ul>
<li><code>tar</code> is the tape archive command.</li>
<li><code>-x</code> is the flag that extracts the file.</li>
<li><code>-f</code> is the flag that allows you to specify the <strong>f</strong>ilename for the archive.</li>
<li><code>-C</code> is the flag that specifies the destination folder to extract the archive.</li>
</ul>
<h2 id="how-to-display-archive-progress">How to Display Archive Progress</h2>
<p>Want to see the progress of the archive process? You can add the <code>-v</code> verbose mode flag, which will display the archive progress in the terminal while creating the archive.</p>
<p>Let's test this like so:</p>
<pre><code class="lang-bash">tar -cvf archive-name.tar /path/to/file-or-directory
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1600406905365/YmAyd5sWr.png" alt="My CLI.png" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>If you're continually using or contributing to open-source, you'll come across many <code>.tar.gz</code> files, which is mostly used to bundle and compress the installable file of the project. With this article, I hope you'll harness these files effectively, like the boss that you are.</p>
<p>If you forget any command or are unsure about what to do, you can run <code>tar --help</code> or <code>man tar</code> which will display a manual with all possible options for the tar command.</p>
<p>Here are some useful <strong>Tar</strong> flags for your reference:</p>
<table>
<thead>
<tr>
<td>Flag</td><td>Meaning</td><td></td><td></td></tr>
</thead>
<tbody>
<tr>
<td>-c</td><td>Creates the archive</td><td></td><td></td></tr>
<tr>
<td>-f</td><td>List the content is the archive</td><td></td><td></td></tr>
<tr>
<td>-z</td><td>Compress the archive with GZIP</td><td></td><td></td></tr>
<tr>
<td>-j</td><td>Compress the archive with BZIP2</td><td></td><td></td></tr>
<tr>
<td>-v</td><td>Show archive progress in verbose mode</td><td></td><td></td></tr>
<tr>
<td>-x</td><td>Extract the content of an archive</td><td></td><td></td></tr>
<tr>
<td>-C</td><td>Specify the archive extraction destination path</td><td></td></tr>
</tbody>
</table>
<p>Thanks for reading! 💙</p>
]]></content:encoded></item><item><title><![CDATA[Introduction to Headless Content Management Systems]]></title><description><![CDATA[Content management has been around since 300 BC to about AD 273 and has been improving since then using non-digital techniques. The birth of the World Wide Web in the '90s brought about even more advancement to the existing traditional content manage...]]></description><link>https://blog.bolajiayodeji.com/introduction-to-headless-content-management-systems</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/introduction-to-headless-content-management-systems</guid><category><![CDATA[headless cms]]></category><category><![CDATA[cms]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[Content management system]]></category><category><![CDATA[headless]]></category><category><![CDATA[software development]]></category><category><![CDATA[content]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Mon, 03 Aug 2020 17:31:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1595943087148/gpYKiEZVg.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Content management has been around since 300 BC to about AD 273 and has been improving since then using non-digital techniques. The birth of the World Wide Web in the '90s brought about even more advancement to the existing traditional content management processes. Since then, we've been improving the processes we use to manage content.</p>
<p>In this article, I'll show you an overview of how content management can be optimized using the headless content management system(CMS) to ensure the reusability of content across several digital platforms or presentation layers (web, mobile, smartwatches, VR headsets, or home assistants).</p>
<h2 id="heading-introduction-to-content-management">Introduction to Content Management</h2>
<p>The <a target="_blank" href="https://www.britannica.com/topic/Library-of-Alexandria">Library of Alexandria</a> is one of the world's largest and most significant ancient libraries. (300 BC to about AD 273) and was the first attempt at managing content. Librarians preserved content in the form of scrolls and codices. They were the first content managers who also controlled access to this preserved content.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1595932890024/-hnCJzl74.jpeg" alt="dia-scroll-codex.jpg" /></p>
<blockquote>
<p>Image source: <a target="_blank" href="https://www.google.com/search?q=scrolls+and+codices&amp;rlz=1C5CHFA_enNG906NG906&amp;source=lnms&amp;tbm=isch&amp;sa=X&amp;ved=2ahUKEwjWz8vTxv_qAhWox4UKHQ4tCcUQ_AUoAXoECA4QAw&amp;biw=1280&amp;bih=721#imgrc=aCqaoTmiAcrL4M">Google search</a></p>
</blockquote>
<p>Content is information produced through some editorial process intended to be distributed to and consumed by humans. This content is created and managed; then, it is published and delivered to end-users for consumption. Content management (CM), on the other hand, is some set of tools and processes (from content creation to consumption) that support the collection, structuring, managing, preserving, and publishing of information (content) in any form or type.</p>
<p>Some of these processes can include:</p>
<ul>
<li>Editing existing content</li>
<li>Creating new content</li>
<li>Creating categories</li>
<li>Creating tags</li>
<li>Assigning content to a category or tag</li>
<li>Sorting content</li>
<li>Creating permissions and roles</li>
<li>Assign permissions and roles</li>
<li>Defining content structures and workflow</li>
<li>Version control</li>
<li>Publishing content</li>
</ul>
<h2 id="heading-the-big-four-of-content-management">The Big Four of Content Management</h2>
<ul>
<li><p><strong>Web Content Management (WCM)</strong>: This is the management of content meant to be distributed as text, images, sounds, videos, or animations on websites.</p>
</li>
<li><p><strong>Enterprise Content Management (ECM)</strong>: This is the management of business content not intended for distribution via websites (e.g. business and employee records).</p>
</li>
<li><p><strong>Digital Asset Management (DAM)</strong>: This is the management of media files (images, audio, video).</p>
</li>
<li><p><strong>Records Management (RM)</strong>: This is the management of transactional records from business operations consciously retained as evidence.</p>
</li>
</ul>
<h2 id="heading-introduction-to-content-management-systems">Introduction to Content Management Systems</h2>
<p>According to Wikipedia, a content management system (CMS) manages the creation and modification of digital content. These systems support multiple users in a collaborative environment, allowing documents to be managed with different styles of governance and workflows.</p>
<p>A CMS usually allows multiple users to interact with content stored in a repository. This repository might be located on the same server powering the application or in a separate cloud storage service. Content editors can then edit existing content, create new content, perform some editorial processes on content, and make the processed content available for consumption by end-users.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1595932775565/Iit0hBPVx.png" alt="Demo of the popular WordPress CMS" /></p>
<blockquote>
<p>Demo of the popular WordPress CMS</p>
</blockquote>
<h2 id="heading-components-of-a-cms">Components of a CMS</h2>
<p>A content management system typically has two major components:</p>
<ul>
<li><p><strong>Content management application (CMA)</strong>: This is the user interface that allows an editor to add, modify, remove and manage content from a website.</p>
</li>
<li><p><strong>Content delivery application (CDA)</strong>: This is the component that compiles the managed content and distributes it to the website or final front-end layer.</p>
</li>
</ul>
<h2 id="heading-introduction-to-traditional-cms">Introduction to Traditional CMS</h2>
<p>Traditional content management systems are "coupled"; the content management application (CMA) and the content delivery application (CDA) come together in a single software application. Here, managing and delivering content to the front-end layer is done on one application, making it difficult for developers and content managers to work independently. The CMS's backend and database are tightly bound within the same system that delivers and presents content to the front-end layer.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1595940531964/GDzyV7-vB.gif" alt="how traditional cms works" /></p>
<p>Content is created, managed, and stored along with all digital assets on the site’s back-end while providing a content delivery layer on the frontend.</p>
<h2 id="heading-introduction-to-headless-cms">Introduction to Headless CMS</h2>
<p>Traditional CMS mixes content with the front-end code (which is usually stack-specific), limiting the content to just the application upon which it is built. With the headless CMS, the front-end component of the CMS is decoupled from the application, content becomes independent, and you don't have to worry about how and where the content gets displayed but instead just storage, structuring, and delivery of the content.</p>
<p>The headless content management system is built as a content repository that makes structured content accessible via a RESTful API, GraphQL API, or the Git Workflow for display on any device while maintaining the speed, security, and scalability of <a target="_blank" href="https://blog.bolajiayodeji.com/introducing-jamstack-the-architecture-of-the-modern-web">the JAMstack architecture</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1595941572761/EM5HdBR0r.png" alt="Screenshot 2020-07-28 at 2.05.41 PM.png" /></p>
<ul>
<li><p>You create and structure your content in the Headless CMS.</p>
</li>
<li><p>You fetch data from your API endpoint to your website or application.</p>
</li>
<li><p>You query your content to your website or applications</p>
</li>
</ul>
<pre><code>{
  products {
    id
    name
    date
    tags
    image {
      url
      fileName
    }
  }
}
</code></pre><ul>
<li>You can then render the fetched data in your website or application however you want and on several platforms.</li>
</ul>
<h2 id="heading-benefits-of-headless-cms">Benefits of Headless CMS</h2>
<p>Wondering why you should delve into headless content management systems? Here are some reasons:</p>
<ul>
<li><p>The content created in a headless CMS is "raw" and can be reused across multiple platforms, making it easier for you to scale your application and still satisfy all your users on all platforms.</p>
</li>
<li><p>Content managers can work in their preferred choice of headless CMS, and developers can build with any front-end framework of their choice in their preferred programming language.</p>
</li>
<li><p>Headless CMSs are cheaper to set up and easier to use with even less technical expertise after setup.</p>
</li>
<li><p>A headless CMS promotes an agile way of working since content managers and developers can work independently and simultaneously, resulting in faster development.</p>
</li>
<li><p>Headless CMS secures your content by providing it in high-performance CDN rather than directly from the database, thereby reducing the risk of distributed denial-of-service attacks (DDoS).</p>
</li>
</ul>
<h2 id="heading-categories-of-headless-cms">Categories of Headless CMS</h2>
<p>You can find a comprehensive list of all Content Management Systems for Jamstack Sites <a target="_blank" href="https://headlesscms.org">here</a>. However, headless CMSs are generally based on two categories described below.</p>
<h3 id="heading-git-based-headless-cms">Git-based Headless CMS</h3>
<p>With a git-based CMS, you make and push changes to a git repository on a service like GitHub or GitLab. This service will then serve as your content delivery network, trigger a new build and update your website. Here are some of my favorites:</p>
<ul>
<li><a target="_blank" href="https://www.netlifycms.org/">Netlify CMS</a></li>
<li><a target="_blank" href="https://getpublii.com/">Publii</a></li>
<li><a target="_blank" href="https://forestry.io/">Forestry</a></li>
<li><a target="_blank" href="https://jaredforsyth.com/hexo-admin/">Hexo Admin</a></li>
<li><a target="_blank" href="https://jekyll.github.io/jekyll-admin/">Jekyll Admin</a></li>
</ul>
<h3 id="heading-api-driven-headless-cms">API-driven Headless CMS</h3>
<p>With an API-driven CMS, you manage your content on the CMS and deliver these contents to your application’s front-end with an intermediary API. Here are some of my favorites:</p>
<ul>
<li><a target="_blank" href="https://strapi.io/">Strapi</a></li>
<li><a target="_blank" href="https://www.contentful.com/">Contentful</a></li>
<li><a target="_blank" href="https://hygraph.com/">Hygraph</a></li>
<li><a target="_blank" href="https://www.cosmicjs.com/">Cosmic</a></li>
<li><a target="_blank" href="https://www.sanity.io/">Sanity</a></li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Content management is revolving, and there is an increasing demand for more flexible, scalable, and faster ways to deliver content with the right experience to the right user on the right platform. The future is headless!</p>
<p>You should consider checking out the slide of my recent talk: <strong>Optimizing Content Management with the Headless CMS</strong>.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://slides.com/bolajiayodeji/optimizing-cm-with-headless-cms">https://slides.com/bolajiayodeji/optimizing-cm-with-headless-cms</a></div>
<h2 id="heading-attributions">Attributions</h2>
<ul>
<li><a target="_blank" href="https://www.google.com/">Google</a></li>
<li><a target="_blank" href="https://www.wikipedia.org/">Wikipedia</a></li>
<li><a target="_blank" href="https://www.britannica.com/">Encyclopædia Britannica</a></li>
</ul>
<h2 id="heading-further-resources">Further Resources</h2>
<ul>
<li><a target="_blank" href="https://graphcms.com/academy/headless-cms">What Is A Headless CMS</a></li>
<li><a target="_blank" href="https://headlesscms.org/">A List of Content Management Systems for Jamstack Sites</a></li>
<li><a target="_blank" href="https://www.storyblok.com/tp/headless-cms-explained#headless-cms-explained-in-5-minutes">Headless CMS explained in 5 minutes</a></li>
<li><a target="_blank" href="https://graphcms.com/blog/headless-cms-technical-seo-best-practices">Headless CMS and SEO: Technical Best Practices</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[My First Interview Experience 👨🏾‍💻]]></title><description><![CDATA[Hashnode recently started some writing challenges and series to encourage developer writers to share knowledge, define their writing goals, understand writing standards, become consistent at writing, and build their career.
This week's challenge is t...]]></description><link>https://blog.bolajiayodeji.com/my-first-interview-experience</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/my-first-interview-experience</guid><category><![CDATA[DevLife]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Tue, 14 Jul 2020 13:25:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1594723533485/6mnZgDAn8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hashnode recently started some writing challenges and series to encourage developer writers to share knowledge, define their writing goals, understand writing standards, become consistent at writing, and build their career.</p>
<p>This week's challenge is the <a target="_blank" href="https://hashnode.com/n/myfirstinterviewexperience">#MyFirstInterviewExperience</a> where developers get to share their first interview experience and lessons learned from it. While this is my own story, I look forward to reading yours and hope developers preparing for interviews will learn from these stories.</p>
<h2 id="the-beginning">The Beginning</h2>
<p>My first interview experience was for an internship role at <a target="_blank" href="http://upnepa.ng">Upnepa NG</a>, a company in Lagos, Nigeria. Upnepa is a data-driven platform that gives real-time (On/Off) information and total hours of electricity supply in selected areas, communities, and homes across Nigeria.</p>
<p>I saw the advert for the role of Web Developer Intern in October 2018 posted on Facebook by one of Upnepa's co-founders. As of then, Upnepa just got started and just joined an acceleration program in collaboration with  Facebook and CCHUB at NG_HUB from Facebook.</p>
<p>I so much loved their mission to monitor, aggregate, and analyze power supply, power outages, and power distribution in order to improve productivity in young people and students, provide power supply rates insight for SMEs, and foresight for governmental institutions, energy distribution companies, and energy solution projects. </p>
<p>I prepared my resume and applied for the job. This was my first time applying for a job, I was very inexperienced and wasn't sure how it all works. I just kept calm and waited for whatever comes next 🧘🏾‍♂️.</p>
<h2 id="the-interview">The Interview</h2>
<p>Eventually, I got an email that my application was accepted and it was interview time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594718355180/xCshdJ7nT.png" alt="Interview update email" /></p>
<p>The interview was to evaluate my:</p>
<ul>
<li>Communication skill</li>
<li>Personal Development </li>
<li>Commitment</li>
<li>Level of expertise in web development</li>
</ul>
<p>I prepared, read tons of resources online around preparing for interviews and hoped I was ready for it. Fast forward to the interview day, I joined the call (I can't remember all the details now 😬) and answered tons of technical and non-technical questions alongside some test web projects.</p>
<p>After some more time and patiently waiting, I got another email after the interview.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594718712888/McxVBK6Zo.png" alt="Interview update email" /></p>
<p>Yayy, I got the job and was super excited! 🤩</p>
<h2 id="lessons-learned">Lessons Learned</h2>
<ul>
<li>Build a killer brand for yourself. Check out Gift's <a target="_blank" href="https://lauragift21.hashnode.dev/building-a-killer-personal-brand-cjww7wzdj000m1ws1o42r58hm">guide</a> on personal branding for developers.</li>
<li>Prepare a good resume before applying. Here's a <a target="_blank" href="https://www.freecodecamp.org/news/how-to-write-a-great-resume-for-software-engineers-75d514dd8322/">great guide</a> for this.</li>
<li>You don't need to "know it all", you just need to "know it and know-how and where to find more knowledge."</li>
<li>Ensure you have good people-skills aside from technical skills.</li>
<li>Do some research about the company before applying and interviewing.</li>
<li>Get a good internet connection before joining the interview.</li>
<li>Invest in quality webcams, microphones, or noise cancellation headphones.</li>
</ul>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/iambolajiayo/status/1255694731482365952">https://twitter.com/iambolajiayo/status/1255694731482365952</a></div>
<h2 id="conclusion">Conclusion</h2>
<p>Don't spend your entire life waiting for the best time making that move, write that article, build that side-project, apply for that job, or ask that question today. The best time is always now!</p>
<p>Join Hashnode's writing challenges like the <a target="_blank" href="https://hashnode.com/2articles1week">#2Articles1Week</a> and <a target="_blank" href="https://hashnode.com/n/myfirstinterviewexperience">#MyFirstInterviewExperience</a> to write, share knowledge, become a better writer, learn in public and win some Hashnode badges.</p>
]]></content:encoded></item><item><title><![CDATA[How to Add a README file to your GitHub Profile]]></title><description><![CDATA[GitHub recently introduced a special feature for developers, that allows you to showcase yourself by pinning a README.md containing information about you, your work, portfolio, and anything else on your GitHub profile.
Here are some of my favorites:
...]]></description><link>https://blog.bolajiayodeji.com/how-to-add-a-readme-file-to-your-github-profile</link><guid isPermaLink="true">https://blog.bolajiayodeji.com/how-to-add-a-readme-file-to-your-github-profile</guid><category><![CDATA[2Articles1Week]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Developer]]></category><category><![CDATA[portfolio]]></category><dc:creator><![CDATA[Bolaji Ayodeji]]></dc:creator><pubDate>Thu, 09 Jul 2020 08:49:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1594284915289/MyHJcZTlC.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>GitHub recently introduced a special feature for developers, that allows you to showcase yourself by pinning a <code>README.md</code> containing information about you, your work, portfolio, and anything else on your GitHub profile.</p>
<p>Here are some of my favorites:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594275726875/mR2D0-XUr.png" alt="Jason's GitHub.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594284277681/UcyCBJhu7.png" alt="Prosper's GitHub.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594275804931/gjnaiNY4q.png" alt="Cassidy's GitHub.png" /></p>
<p>In this article, I'll show you how to create a new GitHub repository and pin your special README file to your profile. That's it 🌚</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>A GitHub account</li>
<li>Basic markdown knowledge</li>
<li>Smile dear friend :)</li>
</ul>
<h2 id="heading-step-one">STEP ONE</h2>
<ul>
<li>Create a new <a target="_blank" href="https://github.com/new">✨special✨ repository</a> with your username. The special repository is case sensitive, ensure to use the same case as your account's username.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594277554831/g90tgFMHm.png" alt="GitHub screenshot.png" /></p>
<ul>
<li>Click on the checkbox: <strong>Initialize this repository with a README</strong>. This will create a <code>README.md</code> file inside your <strong>/</strong> repository.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594277960735/ckdAt2xy0.png" alt="Screenshot 2020-07-09 at 7.58.56 AM.png" /></p>
<h2 id="heading-step-two">STEP TWO</h2>
<blockquote>
<p>A README file contains information about other files in a directory or archive of computer software. A form of documentation, it is usually a simple plain text file called <code>READ.ME</code>, <code>README.TXT</code>, <code>README.md</code>, <code>README.1ST</code> – or simply README. The file's name is generally written in uppercase letters. ~ <a target="_blank" href="https://en.wikipedia.org/wiki/README">Wikepedia</a></p>
</blockquote>
<p>Now you have the <code>README.md</code> file in your <em>✨special✨ repository</em>, that's all you need. Edit the file as you deem fit (add images, text, tables, lists, embeds and anything else markdown supports). The README file will automatically appear on your public GitHub profile!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594278115318/8hu4yN9mQ.png" alt="GitHub screenshot.png" /></p>
<p>You also get a free template out of the box, cool right?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594278175857/jatORpexT.png" alt="GitHub screenshot.png" /></p>
<p>Here's my own finished profile page from the <a target="_blank" href="https://github.com/BolajiAyodeji/BolajiAyodeji">special repository</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1594284314953/e3DjMMFt_.png" alt="My GitHub Profile.png" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The special repository is a great way to showcase and summarize yourself and work for potential opportunities and engagements. Get creative and showcase everything about yourself to your friends and GitHub stalkers.</p>
<p>Got your special repository live now? Drop a link to your GitHub account in the comments and let's see how amazing yours look. ✌🏾🙂</p>
]]></content:encoded></item></channel></rss>