<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bryant Gillespie</title>
    <description>The latest articles on DEV Community by Bryant Gillespie (@bryantgillespie).</description>
    <link>https://dev.to/bryantgillespie</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1101395%2F7dbd95d6-fea8-4c73-8b35-d8d89d0d3ffe.jpeg</url>
      <title>DEV Community: Bryant Gillespie</title>
      <link>https://dev.to/bryantgillespie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bryantgillespie"/>
    <language>en</language>
    <item>
      <title>How I shipped an event registration site in just 1 week with Nuxt, Directus, OpenAI, and TailwindCSS</title>
      <dc:creator>Bryant Gillespie</dc:creator>
      <pubDate>Fri, 07 Jun 2024 16:47:03 +0000</pubDate>
      <link>https://dev.to/bryantgillespie/how-i-shipped-an-event-registration-site-in-just-1-week-with-nuxt-directus-openai-and-tailwindcss-123n</link>
      <guid>https://dev.to/bryantgillespie/how-i-shipped-an-event-registration-site-in-just-1-week-with-nuxt-directus-openai-and-tailwindcss-123n</guid>
      <description>&lt;p&gt;I recently shipped a event registration site in 1 week that would take some companies 1 year. And I'm definitely not a 10x developer. &lt;/p&gt;

&lt;p&gt;When I was first learning to code, I always appreciated behind-the-curtain looks at how projects were made, soooo… here’s the story and how it’s built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TLDR;
&lt;/h2&gt;

&lt;p&gt;Adding this here, just to get the “What’s your stack?” questions out of the way 🤣&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frontend - &lt;a href="https://nuxt.com"&gt;Nuxt&lt;/a&gt; / Vue&lt;/li&gt;
&lt;li&gt;backend / CMS - &lt;a href="https://directus.io"&gt;Directus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;styling - &lt;a href="https://ui.nuxt.com"&gt;Nuxt UI&lt;/a&gt; and &lt;a href="https://tailwindcss.com/"&gt;Tailwind CSS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;hosting - Netlify&lt;/li&gt;
&lt;li&gt;avatar generation - OpenAI &lt;a href="https://openai.com/index/dall-e-3/"&gt;Dall•E 3&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the gory details are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Story &lt;strong&gt;🐰&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Meet Leapweek&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;LeapWeek.dev is our week long launch celebration for developers at Directus. There are product announcements, workshops, giveaways, and more. &lt;/p&gt;

&lt;p&gt;The live events are typically hosted via the Directus TV website (&lt;a href="https://directus.io/tv"&gt;https://directus.io/tv&lt;/a&gt;) but the registration has typically been powered by other tools like &lt;a href="https://lu.ma/"&gt;Lu.ma&lt;/a&gt; and a few others.&lt;/p&gt;

&lt;p&gt;Using third-party tools obviously meant additional costs which add up, but that wasn’t the main concern. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The big headaches to solve were:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a very disconnected experience for users&lt;/li&gt;
&lt;li&gt;an in-efficient (pronounced “💩-y”) workflow for our team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So with this third Leap Week, we made the call to build our own “platform” that is tightly integrated with our existing stack and could be re-used for future Leap Week events. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Goals 🥅&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Aside from supporting registration for the event, there were a few important boxes to check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Own” our own event property.&lt;/li&gt;
&lt;li&gt;Build a growth loop to incentivize shares.&lt;/li&gt;
&lt;li&gt;Leverage AI for something “different” to get attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Concept 👨‍🚀🚀&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our previous Leap Week events were spaced themed. We knew we wanted to carry that same theme so we didn’t have to whip up a ton of brand new supporting creative.&lt;/p&gt;

&lt;p&gt;Aside from that though, the event registration site was mostly a blank canvas.&lt;/p&gt;

&lt;p&gt;We took some inspiration from Vercel Ship and their user registration badge concept. I’d also seen other companies do similar things in the past. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53pk4hfmu3pdvxfpcblz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53pk4hfmu3pdvxfpcblz.png" alt="Image description" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I really loved the personalization, but I definitely wanted to take the concept “up a notch”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mission Patch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sharing loop was critical for the project and our first idea for it was a mission patch.&lt;/p&gt;

&lt;p&gt;It looked nice, dynamically added a name when you filled out the form, and included a parallax effect to make it feel "3D". It would be fine, but it just didn’t feel right. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc83byb4z09kel5dpqs6a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc83byb4z09kel5dpqs6a.png" alt="Image description" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We kept iterating though. At some point fairly early in the process, I had snuck in a rabbit astronaut on the landing page as an accent piece.&lt;/p&gt;

&lt;p&gt;My next thought was “ok, let’s use that to make the patch concept even more interesting”. So I added the ability to upload an avatar and drop it into the astronaut suit and that appeared behind the custom patch. This felt a little more interesting.&lt;/p&gt;

&lt;p&gt;And then….I wanted to customize the astronaut even further by adding custom patches for the person’s country and company.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jud6wdc9luavdkt9ezs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jud6wdc9luavdkt9ezs.png" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few GPT-fueled jam sessions later and quick demo that left the whole team smiling, the astronaut became the focal point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And Rabbitars were born.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdvnkx9j321kejr6dmhus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdvnkx9j321kejr6dmhus.png" alt="Image description" width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So basically “Rabbitars” are personalized rabbit astronauts. It’s our version of the registration badge and it’s definitely over the top. They include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI generated rabbit headshot&lt;/li&gt;
&lt;li&gt;Company logo patch&lt;/li&gt;
&lt;li&gt;Name patch&lt;/li&gt;
&lt;li&gt;Country patch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when you share your unique referral link – we use that avatar to create a personalized social sharing image as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkoc1xsuesmnn84ro54z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkoc1xsuesmnn84ro54z.png" alt="Image description" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Backend &lt;strong&gt;🧱&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The backend is powered by &lt;a href="https://directus.io"&gt;Directus&lt;/a&gt; - a data platform that is a hybrid of BaaS and CMS. It pairs up with most SQL databases. &lt;/p&gt;

&lt;p&gt;It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;instantly ready-to-go REST APIs (or GraphQL if that’s your thing)&lt;/li&gt;
&lt;li&gt;asset / file storage&lt;/li&gt;
&lt;li&gt;authentication and permissions&lt;/li&gt;
&lt;li&gt;admin interface where you can manage and edit data, build or adjust your data model without writing code&lt;/li&gt;
&lt;li&gt;no-code dashboards&lt;/li&gt;
&lt;li&gt;low code automation tool to build simple or complex flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Directus runs the whole backend from ticketing and registration to serving data for the landing page.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Data Model&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I created the data model via the UI inside Directus. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gv7d6nooj5ci8xtssw1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gv7d6nooj5ci8xtssw1.png" alt="Image description" width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means it was easy to see what the editing experience would look like for my team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlkc702j3vlfz6rfhdgh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlkc702j3vlfz6rfhdgh.png" alt="Image description" width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also put together a nice dashboard for the team to track sign ups and view all the different countries users were from. This is baked into Directus and took me all of like 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7g083l4vnz3lzb0lt7nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7g083l4vnz3lzb0lt7nt.png" alt="Image description" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Frontend &lt;strong&gt;🧱&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The frontend runs on Nuxt - the popular full stack meta-framework on top of Vue.js. I’m a fan of Nuxt and I’ve been using it for several years in various projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routes
&lt;/h3&gt;

&lt;p&gt;Nuxt’s file based routing is a helpful pattern to speed along projects. Chuck a Vue component into the &lt;code&gt;/pages&lt;/code&gt; directory and you get a route.&lt;/p&gt;

&lt;p&gt;And there’s really only a handful of routes for this project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; - the landing page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/tickets&lt;/code&gt;  - the registration page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/tickets/customize&lt;/code&gt; - the (logged in only)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ticket/[ticket]&lt;/code&gt; - the personalized rabbitar page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/auth/login&lt;/code&gt; - login if you switch devices or logout&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/auth/reset&lt;/code&gt; - if you somehow misplace the initial email with a confirmation code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/terms&lt;/code&gt; - terms and conditions for the giveaway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs54ta66sxtev4cm6ttvm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs54ta66sxtev4cm6ttvm.png" alt="Image description" width="800" height="1079"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nuxt Route Rules keep the site speedy by allowing different rendering modes based on specific routes – an uncommon feature for other frameworks. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faretg3e84vknu8ds3m3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faretg3e84vknu8ds3m3j.png" alt="Image description" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, the landing page data is fetched from the Directus backend, but uses a stale while revalidate caching pattern for performance.&lt;/p&gt;

&lt;p&gt;I also setup a proxy for the Clearbit Logo API to prevent hounding their server all the time. The site uses their API to fetch the logos for companies based on the website you enter. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Landing Page&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The event landing page uses a “page builder” concept where anyone on our marketing team can update the layout on the page and add new components like card groups or faqs.&lt;/p&gt;

&lt;p&gt;On the backend, this is setup using Directus’ &lt;a href="https://docs.directus.io/app/data-model/relationships.html#many-to-any-m2a"&gt;Many-to-Any (M2A) relationships&lt;/a&gt;. Each block can has different schema.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftjw523z0sd3kt6c1ym88.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftjw523z0sd3kt6c1ym88.png" alt="Image description" width="800" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It all comes together on the Nuxt side. The data is fetched from the Directus backend, and then passed to a &lt;code&gt;PageBuilder&lt;/code&gt; component that is responsible for looping through an array of blocks and rendering the components dynamically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl98pgck1a129nln36soy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl98pgck1a129nln36soy.png" alt="Image description" width="800" height="934"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;UI&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The site uses the &lt;a href="https://ui.nuxt.com/"&gt;Nuxt UI&lt;/a&gt; library for a lot of the basic components like buttons and form inputs. Nuxt UI in turn uses libraries like TailwindCSS and Headless UI. It’s pretty easy to theme and uses tailwind-merge to manage class conflicts. &lt;/p&gt;

&lt;p&gt;It really saved me a lot of time by not to re-create some of the more “rich” components like comboboxes or dropdown menus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating AI Rabbitars &lt;strong&gt;🧱&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The actual rabbitar images are generated using OpenAI’s Dall•E 3. Currently, the average user generates ~1.52 avatars costing us a total of ~$0.0608 per registrant. We have set a hard limit of 3 generations to prevent any crazy scary OpenAI bills.&lt;/p&gt;

&lt;p&gt;There is a Nuxt server route that calls the OpenAI API, saves the generated image to the Directus instance, and updates the avatars generated by the user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j6tjmmaewlnz9r0sy48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j6tjmmaewlnz9r0sy48.png" alt="Image description" width="800" height="883"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Challenges 🪨
&lt;/h1&gt;

&lt;p&gt;There were more than a few challenges I faced with this thing. 😅&lt;/p&gt;

&lt;h3&gt;
  
  
  Referral Tracking
&lt;/h3&gt;

&lt;p&gt;We wanted to offer more chances in the giveaway for referrals so we needed to build a mechanism to control that. &lt;/p&gt;

&lt;p&gt;Once you generate your personalized rabbitar - you can share it to increase your odds of winning. Each person your refer earns you another entry in the giveaway. &lt;/p&gt;

&lt;p&gt;To track this, we tag the visitor with a &lt;code&gt;referral_ticket_id&lt;/code&gt; cookie whenever they visit a registrant personal url. Whenever a visitor registers for the event, we check for the cookie, and update a &lt;code&gt;referred_by&lt;/code&gt; field inside our Directus backend.&lt;/p&gt;

&lt;p&gt;This is surfaced to the registrant as a “Swag-O-Meter” on their personalized ticket page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9orpd12patec33e07pw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9orpd12patec33e07pw6.png" alt="Image description" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Function Timeouts&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="http://Leapweek.dev"&gt;Leapweek.dev&lt;/a&gt; is hosted on Netlify. We’ve got a number of our other projects hosted there and I’m pretty familiar with the workflow. With Nuxt, there’s not really much configuration to be done, aside from connecting your repo and adding your ENV variables. &lt;/p&gt;

&lt;p&gt;But Dall•E 3 currently takes roughly between ~15-21 seconds to generate a rabbitar for the site. In local development this wasn’t a problem, but once deployed to Netlify, we were getting timeouts on the serverless functions because the default timeout is 10 secs. &lt;/p&gt;

&lt;p&gt;The Netlify support team was right there to help us out. They increased our limit to 26 secs and we’ve not had anymore issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Long URLs&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Originally we wanted to run this off a subdomain of the site. But &lt;a href="https://leapweek.directus.io/tickets/bryant-gillespie"&gt;&lt;code&gt;https://leapweek.directus.io/tickets/bryant-gillespie&lt;/code&gt;&lt;/a&gt; eats up a lot of characters and shorter urls are better for sharing. We’re really digging Dub.co for sharing our content on socials, but it just wasn’t a fit here for generating links.&lt;/p&gt;

&lt;p&gt;So we chose the &lt;a href="http://leapweek.dev"&gt;&lt;code&gt;leapweek.dev&lt;/code&gt;&lt;/a&gt; domain over &lt;code&gt;leapweek.directus.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But we could do better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nuxt Alias&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The alias property within Nuxt’s definePageMeta makes it super easy to generate aliases for a specific route. So the page at &lt;code&gt;/tickets/bryant-gillespie&lt;/code&gt; can also be rendered at &lt;code&gt;/t/bryant-gillespie&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F452830sy9193dpvako81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F452830sy9193dpvako81.png" alt="Image description" width="798" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which gives us a final url like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://leapweek.dev/t/bryant-gillespie&lt;/code&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Dynamic OG Images and Caching&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Dynamically generated OG images are really freaking cool, but it’s hard to ensure they render perfectly on different social media platforms. Each platform tends to have it’s own cache for OG images, making it harder to figure out than the Water Temple in Ocarina of Time.&lt;/p&gt;

&lt;p&gt;For actually generating the dynamic social share images and caching them, we use the &lt;a href="https://nuxt.com/modules/og-image"&gt;Nuxt OG-Image module&lt;/a&gt; by Harlan Wilton. It abstracts away a lot of the complexities of serving up dynamic social images. &lt;/p&gt;

&lt;p&gt;Under the hood, it uses &lt;a href="https://github.com/vercel/satori"&gt;Satori by Vercel&lt;/a&gt; to render the images from a Vue component. But because of that there are some caveats about component structure and how you can style your images. &lt;/p&gt;

&lt;p&gt;When someone updates their avatar, we also need to purge the cached image so we don’t show the previous one. That’s handled inside a Nuxt server route as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk8xtnkh4vbd3x1xtmmw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk8xtnkh4vbd3x1xtmmw.png" alt="Image description" width="800" height="1335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Results
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzw55xo5cqnuc8d28hf9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzw55xo5cqnuc8d28hf9.png" alt="Image description" width="800" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm pretty happy with the results so far.&lt;/p&gt;

&lt;p&gt;The site just launched on Monday this week and we already have over 300+ registrants and 475 rabbitars generated.&lt;/p&gt;

&lt;p&gt;There's been 0 promotion aside from a few tweets from our team and a single LinkedIn post.&lt;/p&gt;

&lt;p&gt;And as far as I know, we now have the world's largest collection of rabbit avatars. &lt;/p&gt;

&lt;p&gt;So if you ever need 100s or 1000s of rabbit headshots, consider me your guy 🤣. &lt;/p&gt;

&lt;p&gt;If you're interested and want to poke around the site and generate your rabbitar - go for it. You can check out the site at &lt;a href="https://leapweek.dev"&gt;https://leapweek.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>openai</category>
      <category>directus</category>
      <category>nuxt</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Directus and SEO: Tips &amp; Tricks</title>
      <dc:creator>Bryant Gillespie</dc:creator>
      <pubDate>Thu, 02 Nov 2023 04:01:00 +0000</pubDate>
      <link>https://dev.to/directus/directus-and-seo-tips-tricks-2klc</link>
      <guid>https://dev.to/directus/directus-and-seo-tips-tricks-2klc</guid>
      <description>&lt;p&gt;Search engine optimization (SEO) is an ever-changing but super important part of getting your website in front of visitors.&lt;/p&gt;

&lt;p&gt;When using Directus as a Headless CMS, it is incredibly un-opinionated about what you do with your data and content on the frontend, leaving how you build your website up to you. &lt;/p&gt;

&lt;p&gt;But if you’re just starting out, being so open-ended can leave you wondering about the best way to handle things like SEO.&lt;/p&gt;

&lt;p&gt;In this post, I’ll share a few tips for managing SEO with Directus. I’ll also share some links and resources for some of the most popular frontend frameworks.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Important Assumptions&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You understand basic SEO strategy and terminology.&lt;/li&gt;
&lt;li&gt;You’re familiar with collections, fields, and fetching data from Directus.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Create a Separate Collection for SEO Data
&lt;/h2&gt;

&lt;p&gt;At the end of the day, page titles and meta tags are just data. And you have to store that SEO data for your pages and blog posts somewhere. &lt;/p&gt;

&lt;p&gt;For simple sites, you could easily manage all your meta tag data by duplicating fields like &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;meta_description&lt;/code&gt; from one collection to another. &lt;/p&gt;

&lt;p&gt;But beyond a couple collections, this approach becomes cumbersome and potentially dangerous. What if you forget to copy one field? What if there’s a typo and the names become inconsistent? These oversights could break your SEO practices and ,at worst, your whole site.&lt;/p&gt;

&lt;p&gt;One better option would be creating a single collection to standardize all the SEO data for your content collections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create an &lt;code&gt;seo&lt;/code&gt; collection&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;seo
&lt;span class="p"&gt;
-&lt;/span&gt; id (Type: uuid)
&lt;span class="p"&gt;-&lt;/span&gt; title (Type: String, Interface: Input, Note: This item's title, defaults to item.title. Max 70 characters including the site name.)
&lt;span class="p"&gt;-&lt;/span&gt; meta_description (Type: Text, Interface: Textarea, Note: This item's meta description. Max 160 characters.)
&lt;span class="p"&gt;-&lt;/span&gt; canonical_url (Type: String, Interface: Input, Note: Where should the canonical URL for this entry point to.)
&lt;span class="p"&gt;-&lt;/span&gt; no_index (Type: Boolean, Interface: Toggle, Note: Instruct crawlers not to index this item.)
&lt;span class="p"&gt;-&lt;/span&gt; no_follow (Type: Boolean, Interface: Toggle, Note: Instruct crawlers not to follow links on this item.)
&lt;span class="p"&gt;-&lt;/span&gt; og_image (Type: Image, Note: This item's OG image. Defaults to global site OG image. The recommended size is 1200px x 630px. The image will be focal cropped to this dimension.)
&lt;span class="p"&gt;-&lt;/span&gt; sitemap_change_frequency (Type: String, Interface: Input, Note: How often to instruct search engines to crawl.)
&lt;span class="p"&gt;-&lt;/span&gt; sitemap_priority (Type: Decimal, Interface: Input, Note: Valid values range from 0.0 to 1.0. This value does not affect how your pages are compared to pages on other sites, it only lets the search engines know which pages you deem most important for the crawlers.)

&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nab5g3mf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/4124a39c-3a9d-4bbd-bc43-9464f024c658" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nab5g3mf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/4124a39c-3a9d-4bbd-bc43-9464f024c658" alt="Screenshot of the SEO collection data model inside settings. Several fields are displayed like title, meta_description, canonical_url, and others related to the sitemap." width="800" height="1012"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond the basic &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;meta_description&lt;/code&gt; , the other fields you add to your SEO collection are totally up to you. &lt;/p&gt;

&lt;p&gt;Adding the fields within Directus is only one half of the work - you need to use these fields in your frontend within your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tags and sitemap.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For each of your content collections that have a route on your frontend, inside Directus –  create a many-to-one (M2O) relationship with the SEO collection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--upcrS9z6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/8ed6695f-0090-4531-a7c9-4ab4c5cf59cd" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--upcrS9z6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/8ed6695f-0090-4531-a7c9-4ab4c5cf59cd" alt="Screenshot of adding a new many-to-one relationship within the Directus pages collection." width="800" height="904"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When fetching your content on the frontend, use the &lt;code&gt;fields&lt;/code&gt; parameter to expand the SEO data within a single API call.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@directus/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;directus_project_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;234ee-3fdsafa-dfadfa-dfada&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;readItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta_description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canonical_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no_index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no_follow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og_image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sitemap_change_frequency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sitemap_priority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then pass that to your frameworks specific method of add SEO metadata within your &lt;/p&gt; tags.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Frontend Framework Metadata Documentation&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata#dynamic-metadata"&gt;Next.js - Dynamic Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/getting-started/seo-meta#seo-and-meta"&gt;Nuxt - SEO and Meta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/core-concepts/layouts/"&gt;Astro - Layouts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kit.svelte.dev/docs/seo"&gt;SvelteKit - SEO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://remix.run/docs/en/main/route/meta"&gt;Remix - meta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.io/api/platform-browser/Meta"&gt;Angular - Meta&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Don’t Use Slugs as a Primary Key
&lt;/h2&gt;

&lt;p&gt;The actual url for a page on your website is a key factor for search engine rankings. Because of that, your content editors will want to experiment and adjust urls and slugs for items as needed. &lt;/p&gt;

&lt;p&gt;When adding a new collection Directus lets you choose from several types of primary keys for your collection. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-incremented integer&lt;/li&gt;
&lt;li&gt;Auto-incremented big integer&lt;/li&gt;
&lt;li&gt;Generated UUID&lt;/li&gt;
&lt;li&gt;Manually entered string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve seen lots of folks name the primary key field &lt;code&gt;slug&lt;/code&gt; and use the &lt;code&gt;Manually entered string&lt;/code&gt; . It’s easy to understand. It makes fetching that item easier because you can get the item by its id (the slug) directly instead of constructing a query to match the slug. &lt;/p&gt;

&lt;p&gt;But an item’s p*&lt;em&gt;rimary key value can not be changed after they are created, while t&lt;/em&gt;*his protects your database and all of your relationships, it makes it hard to experiment or change URLs if, for example, a product name changes. &lt;/p&gt;

&lt;p&gt;To avoid this, use the auto-incremented ids or UUIDs for the primary key &lt;code&gt;id&lt;/code&gt; and create a separate field input for the &lt;code&gt;slug&lt;/code&gt; . You can require uniqueness which means only one item in a collection can have a given slug.&lt;/p&gt;

&lt;p&gt;You can no longer use API endpoints or SDK functions for reading a single item, as this relies on using the primary key. Instead, query the whole collection and use &lt;a href="https://docs.directus.io/reference/filter-rules.html"&gt;Filter Rules&lt;/a&gt; to get the single item that has the slug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readItems&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@directus/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;directus_project_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slugFromYourFrontEndFramework&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="c1"&gt;// whatever your convention your framework follows;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;_eq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slugFromYourFrontEndFramework&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Use Relationships for Internal Linking
&lt;/h2&gt;

&lt;p&gt;When building a website, you’ll need both links for internal content and links for external content.&lt;/p&gt;

&lt;p&gt;One common pattern I’ve noticed is creating string inputs for internal links to other content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZxayUG72--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/a6364bb5-74fb-4403-a57d-c0f8ace66d4a" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZxayUG72--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/a6364bb5-74fb-4403-a57d-c0f8ace66d4a" alt="Screenshot of a form within Directus. Two fields are shown. Label and Href. The Href field value is a string /contact-us" width="800" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But this can be surprisingly brittle. As soon as the slug for the Contact page changes from &lt;code&gt;/contact-us&lt;/code&gt; to &lt;code&gt;/contact-directus-team&lt;/code&gt; , the link will break and this can really crash your search engine rankings. &lt;/p&gt;

&lt;p&gt;Luckily Directus makes a more dynamic approach possible with relationships.&lt;/p&gt;

&lt;p&gt;When creating your data model for links to other items from the same or different collections, try using the conditional fields and &lt;a href="https://docs.directus.io/app/data-model/relationships.html#many-to-one-m2o"&gt;many to one relationships&lt;/a&gt; to build a powerful, resilient way to link items.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Within your content collection, add the following fields for linking. (Note: This example is extremely simplified so you can learn the logic involved. - name these depending on what makes the most sense to you and your use case.)&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;your_content_collection
&lt;span class="p"&gt;
-&lt;/span&gt; link_type (Type: String, Interface: Dropdown, Note: Choices: [
    {
        "text": "Internal - Page",
        "value": "pages"
    },
    {
        "text": "Internal - Post",
        "value": "posts"
    },
    {
        "text": "External - URL",
        "value": "external"
    }
])
&lt;span class="p"&gt;-&lt;/span&gt; link_label (Type: String, Interface: Input, Note: What label or title is displayed for this link?)
&lt;span class="p"&gt;-&lt;/span&gt; link_page (Type: M20 Relationship, Related Collection: pages, Hidden On Detail, Conditions: [
    {
        "name": "IF link_type === page",
        "rule": {
            "_and": [
                {
                    "link_type": {
                        "_eq": "pages"
                    }
                }
            ]
        },
        "hidden": false,
    }
])
&lt;span class="p"&gt;-&lt;/span&gt; link_post (Type: M20 Relationship, Related Collection: post, Hidden On Detail, Conditions: [
    {
        "name": "IF link_type === post",
        "rule": {
            "_and": [
                {
                    "link_type": {
                        "_eq": "post"
                    }
                }
            ]
        },
        "hidden": false,
    }
])
&lt;span class="p"&gt;-&lt;/span&gt; link_external_url (Type: String, Interface: Input, Conditions: [
    {
        "name": "IF link_type === external",
        "rule": {
            "_and": [
                {
                    "link_type": {
                        "_eq": "external"
                    }
                }
            ]
        },
        "hidden": false,
    }
])

&lt;/code&gt;&lt;/pre&gt;



  


&lt;p&gt;Fields for page, post, and external url are only visible when the related type is selected so there is no confusion about which fields to enter data for. And it also allows you to use that relationship to fetch the proper slug or permalink for posts and pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the frontend, when you are fetching data via the API, use the &lt;code&gt;[fields&lt;/code&gt; parameter](&lt;a href="https://docs.directus.io/reference/query.html#fields"&gt;https://docs.directus.io/reference/query.html#fields&lt;/a&gt;) to get the related posts and pages in a single API call. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then make sure you’re getting the proper url based on the &lt;code&gt;link_type&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```tsx
const post = await client.request(
    readItem('your_content_collection', 'your_content_item_id', {
        fields: [
            // Fetch all the other root level fields for your collection **In production, you should only fetch the fields you need**
            '*',
            'link_type',
            'link_label',
            'external_url',
            // Use object syntax to fetch fields from a relation
            {
                page: ['id', 'title', 'permalink'],
                post: ['id', 'title', 'slug'],
            },
        ],
    }),
)

function getUrl(item) {
    if (item.link_type === 'pages') {
        return item.page.permalink ?? ''
    } else if (item.link_type === 'posts') {
        return `/blog/${item.post.slug}` ?? ''
    } else if (item.link_type === 'external') {
        return item.external_url ?? ''
    }
    return undefined
}

// ~~~ //
// Inside your template
// Make sure to use the correct syntax for your framework
&amp;lt;a href={getUrl(item)}&amp;gt;{item.link_label}&amp;lt;/a&amp;gt;
// ~~~ //
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frontend Framework Link Documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/components/link"&gt;Next.js - &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/api/components/nuxt-link#nuxtlink"&gt;Nuxt - &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/core-concepts/astro-pages/#link-between-pages"&gt;Astro - Link between pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kit.svelte.dev/docs/link-options"&gt;Svelte - Link options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://remix.run/docs/en/main/components/link#link"&gt;Remix - &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.io/api/router/RouterLink"&gt;Angular - RouterLink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Add Fields to Control Semantic Elements When Using Dynamic Page Builder
&lt;/h2&gt;

&lt;p&gt;The structure of your content matters a lot for SEO. Crawlers like well structured pages with a clear hierarchy. &lt;/p&gt;

&lt;p&gt;For educational items like blog posts or documentation, semantic hierarchy and design usually align well. Most of these items also have a well defined “template” or “layout” on the frontend.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag contains the title of the article and is the visually the largest header on the page. Other header tags like &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; get smaller visually and have less priority for SEO.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PQRe9-SG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/08e38f04-6bc4-4275-ad95-41ca06c0caf7" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PQRe9-SG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/08e38f04-6bc4-4275-ad95-41ca06c0caf7" alt="A blog page on the Directus website. The page title is highlighted and labeled as H1. Another headline within the blog post is also highlighted and labeled H2." width="800" height="904"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But for other items like that are more dynamic like landing pages or homepages, the &lt;a href="https://docs.directus.io/app/data-model/fields/relational.html#builder-m2a"&gt;Builder (Many To Any Relationships)&lt;/a&gt; really shine inside Directus. You can let your marketing or content teams build pages on their own with predefined collections or &lt;code&gt;blocks&lt;/code&gt; without involving a developer at all. It also pairs beautifully with the &lt;a href="https://docs.directus.io/app/data-model/collections.html#set-up-live-preview-for-a-collection"&gt;Live Preview feature&lt;/a&gt; to allow them to see exactly what the site will look like before publishing. &lt;/p&gt;

&lt;p&gt;However, handling the semantic markup you need for proper SEO versus the fact that blocks  could be placed anywhere on a page can be a real challenge when developing your site.&lt;/p&gt;

&lt;p&gt;Let’s take header tags for example. You need proper semantic hierarchy for SEO. But our hierarchy for SEO doesn’t always equal our visual hierarchy required for good design.&lt;/p&gt;

&lt;p&gt;Having the keyword optimized &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag be the largest visually - is not always ideal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aojK-FaI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/1b78cb7a-df47-4e6f-a2de-6e0ba4cec0eb" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aojK-FaI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/1b78cb7a-df47-4e6f-a2de-6e0ba4cec0eb" alt="An events page on the Directus website that highlights the difference in size between the H1 and H2 tags. A small badge with the text Events is the H1. A large headline is the H2." width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But in other cases, the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag should be the largest visually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ORLzRzwq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/2c08d6c3-fb25-4e0c-8fa2-35b1a60b8217" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ORLzRzwq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/2c08d6c3-fb25-4e0c-8fa2-35b1a60b8217" alt="Screenshot of the Directus website with the page heading highlighted. Callouts are pointing to the H2 and H1 elements within the page heading. The H2 tag is above the H1 tag and much smaller." width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A great solution to this problem can be to create separate fields within the collection that allow the content editor to choose both the proper header tag and the visual size. &lt;/p&gt;

&lt;p&gt;Here’s an example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gEs-KAhJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/558c32e5-428c-43dd-889a-9a76f6bf2839" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gEs-KAhJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/558c32e5-428c-43dd-889a-9a76f6bf2839" alt="Content editing form within Directus collection. Several fields are highlighted - Preheading Tag, Heading Size, and Heading Tag. Preheading Tag has a value of H1, Heading Size has a value of X-Large, and Heading Tag has a value of H2." width="800" height="989"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This provides a ton of flexibility. And with just a little training, editors can create pages that look great on screen and also perform well for SEO.&lt;/p&gt;

&lt;p&gt;To render this on the frontend, you’d use dynamic components. Most frontend tools support the concept of dynamic components. The syntax will vary though so consult your frontend framework’s documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend Framework Component Documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/learn/seo/improve/dynamic-import-components"&gt;Next.js - Dynamic Imports for Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/guide/directory-structure/components#dynamic-components"&gt;Nuxt - Dynamic Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.astro.build/en/core-concepts/framework-components/"&gt;Astro - Components&lt;/a&gt; - Astro is a bit unique in that you can use components from other frameworks&lt;/li&gt;
&lt;li&gt;[Svelte - &lt;a&gt;svelte:component&lt;/a&gt;](&lt;a href="https://svelte.dev/docs/special-elements#svelte-component"&gt;https://svelte.dev/docs/special-elements#svelte-component&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Remix - I couldn’t find anything on dynamic components within Remix’s documentation.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.io/guide/dynamic-component-loader"&gt;Angular - Dynamic component loader&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implement a Sitemap
&lt;/h2&gt;

&lt;p&gt;Sitemaps are important tools for crawlers like Googlebot to index your site properly. It’s easy to skip over this step when launching a new site, but it’s an important step that makes sure all the pages on your site can be found in search engines.&lt;/p&gt;

&lt;p&gt;There’s not much to really manage inside Directus for a sitemap beyond properly &lt;a href="https://docs.directus.io/app/data-model/collections.html#create-a-collection"&gt;creating your content collections&lt;/a&gt;. The heavy lifting for a sitemap is on the frontend. &lt;/p&gt;

&lt;p&gt;Some frontend frameworks have an official or community-supported sitemap module / plugin. Others have instructions on how to generate a sitemap without the need to another package. &lt;/p&gt;

&lt;p&gt;The exact implementation details will vary based on your selected framework but the general approach looks like this.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a function that fetches the items for each collection that has a route on your frontend.&lt;/li&gt;
&lt;li&gt;Loop through those items formatting each as proper xml (or as the specific syntax your plugin requires).&lt;/li&gt;
&lt;li&gt;Create a route like &lt;code&gt;/sitemap.xml&lt;/code&gt; that returns the XML file. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s an example that’s specific to Nuxt but the logic could be extracted to other frameworks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```jsx
// This example is based on Nuxt and uses a third party package. 
// Consult your own frontend framework's documentation for how to properly generate a sitemap in their ecosystem.
// /server/routes/sitemap.xml.ts

import { SitemapStream, streamToPromise } from 'sitemap'
import { createDirectus, readItems, rest } from '@directus/sdk'

const directus = createDirectus(directusUrl).with(rest())

export default defineEventHandler(async (event) =&amp;gt; {
    // Fetch all the collections you want to include in your sitemap
    const pages = await directus.request(
        readItems('pages', {
            fields: ['permalink'],
            limit: -1, // Be careful using -1, it will fetch all the items in the collection and could cause performance issues if you have a lot of items
        }),
    )

    const posts = await directus.request(
        readItems('posts', { fields: ['slug', { type: ['slug'] }], limit: -1 }),
    )

    // Create an array of objects with the url you want to include in your sitemap
    const urls = []

    urls.push(...pages.data.map((page) =&amp;gt; ({ url: page.permalink })))

    urls.push(
        ...posts.data.map((post) =&amp;gt; ({
            url: `/blog/${post.slug}`,
        })),
    )

    const sitemap = new SitemapStream({
        hostname: 'https://example.com',
    })

    // Add each url to the sitemap
    for (const item of urls) {
        sitemap.write({
            url: item.url,
            changefreq: 'monthly',
        })
    }

    sitemap.end()
    return streamToPromise(sitemap)
})
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frontend Framework Sitemap Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/learn/seo/crawling-and-indexing/xml-sitemaps"&gt;Next.js - XML Sitemap Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/modules/simple-sitemap"&gt;Nuxt - Nuxt Simple Sitemap Module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/integrations-guide/sitemap/"&gt;Astro - @astro/sitemap Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bartholomej/svelte-sitemap"&gt;Svelte - Sitemap Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/balavishnuvj/remix-seo"&gt;Remix - Remix SEO Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Angular - I struggled to find a helpful tutorial or sitemap package for Angular.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Give Your Team Control of Redirects
&lt;/h2&gt;

&lt;p&gt;It sucks having to pull yourself away from a fun (or important) project to manually add some redirects for a page. Do yourself a favor and let your content team add and manage redirects within the CMS.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;code&gt;redirects&lt;/code&gt; collection&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;redirects
&lt;span class="p"&gt;
-&lt;/span&gt; id (Type: uuid)
&lt;span class="p"&gt;-&lt;/span&gt; url_old (Type: String, Interface: Input)
&lt;span class="p"&gt;-&lt;/span&gt; url_new (Type: Integer, Interface: Slider)
&lt;span class="p"&gt;-&lt;/span&gt; response_code (Type: String, Interface: Dropdown, Choices: [
    {
        "text": "Permanent (301)",
        "value": "301"
    },
    {
        "text": "Temporary (302)",
        "value": "302"
    }
])
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hjChTBG2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/d6d5c9e8-de1a-4cbe-8700-15a2f14f3367" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hjChTBG2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/d6d5c9e8-de1a-4cbe-8700-15a2f14f3367" alt="Screenshot of the Redirects collection data model within Directus settings. Fields included in the data model are url_old, url_new, and response_code." width="800" height="822"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add redirects dynamically when building your frontend. This is often done by creating a function to fetch the redirects from your Directus API and then passing those redirects to your frontend framework using a specific syntax inside a config file, plugin, or module.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Though it depends on your specific framework, this logic would probably be called during build time&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@directus/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directusUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;responseCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response_code&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;

    &lt;span class="c1"&gt;// If response code doesn't match what we expect, use 301&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;responseCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;responseCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Add Logic here to add the redirects to a config file or tell your frontend framework how to handle them&lt;/span&gt;
    &lt;span class="c1"&gt;// ** Your Logic **&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Frontend Framework Redirect Documentation&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/pages/api-reference/next-config-js/redirects"&gt;Next.js Redirects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering"&gt;Nuxt Redirects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/core-concepts/routing/#configured-redirects"&gt;Astro Redirects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kit.svelte.dev/docs/load#redirects"&gt;Svelte Redirects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://remix.run/docs/en/main/utils/redirect#redirect"&gt;Remix Redirects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.io/guide/router#setting-up-redirects"&gt;Angular Redirects&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Don’t Forget Image Alt Text
&lt;/h2&gt;

&lt;p&gt;Often overlooked, image alt text is important for SEO and critical for proper accessibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/image.png"&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"An example image of how alt text works"&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"500"&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"500"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good way to manage image alt tags is on the files themselves as you upload them within Directus.&lt;/p&gt;

&lt;p&gt;I prefer using the &lt;code&gt;description&lt;/code&gt; field on files to store alt text. This way you don’t have to input alt text every single time you use this same image in different contexts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ECDgsLFF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/768c1a1f-08c9-495a-8a4c-0d7dc52405a8" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ECDgsLFF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/768c1a1f-08c9-495a-8a4c-0d7dc52405a8" alt="Screenshot of a File library item form that shows a large image of Directus Flows. Within the form there are two fields - Title and Description." width="800" height="779"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For items like blog posts or articles you might have an &lt;code&gt;image&lt;/code&gt; or &lt;code&gt;featured_image&lt;/code&gt;  field so you can have a nice hero image or a thumbnail if shown inside a card.&lt;/p&gt;

&lt;p&gt;If you store alt text inside of the image item, you will want to fetch these nested title and description fields within the same API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;_eq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slugFromYourFrontEndFramework&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date_published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}]&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ~~~ //&lt;/span&gt;
&lt;span class="c1"&gt;// Inside your template&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that the position of the image &lt;code&gt;id&lt;/code&gt; will change from &lt;code&gt;item.image&lt;/code&gt; to &lt;code&gt;[item.image.id](http://item.image.id)&lt;/code&gt; when you fetch nested fields.&lt;/p&gt;

&lt;p&gt;And remember – the actual alt text itself is just as important as rendering it on the page. I won’t dive into details because Moz has a great &lt;a href="https://moz.com/learn/seo/alt-text"&gt;tutorial on how to write proper alt text here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if you’ve got 100s of images without alt text and you’re dreading doing all that work manually, try a boost from this extension - &lt;a href="https://github.com/Arood/directus-extension-media-ai-bundle"&gt;Directus Media AI Bundle&lt;/a&gt; - a winner in a recent &lt;a href="https://docs.directus.io/blog/directus-ai-hackathon-vote.html"&gt;AI Hackathon&lt;/a&gt; submitted by community member &lt;a href="https://github.com/Arood"&gt;Arood&lt;/a&gt;. It uses several different AI tools to write image alt text or descriptions for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend Framework Image Documentation&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/pages/api-reference/components/image"&gt;Next.js Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://image.nuxt.com/"&gt;Nuxt Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/images/"&gt;Astro Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kit.svelte.dev/docs/assets"&gt;Svelte Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Remix - I couldn’t locate any documentation about images on the Remix site.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.io/guide/image-directive"&gt;Angular Images&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we’ve covered some SEO best practices when using Directus as a Headless CMS. Hopefully you’ve learnt something new, and can build more robust data models that serve both your users and search engine crawlers.&lt;/p&gt;

&lt;p&gt;If these tips were helpful or if you have some of your own you’d like to share, let us know inside &lt;a href="https://directus.chat"&gt;our Discord community&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>directus</category>
      <category>nuxt</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Building a User Feedback Widget with Vue.js and Directus</title>
      <dc:creator>Bryant Gillespie</dc:creator>
      <pubDate>Fri, 22 Sep 2023 04:25:28 +0000</pubDate>
      <link>https://dev.to/directus/building-a-user-feedback-widget-with-vuejs-and-directus-4bm1</link>
      <guid>https://dev.to/directus/building-a-user-feedback-widget-with-vuejs-and-directus-4bm1</guid>
      <description>&lt;p&gt;One of our DevRel initiatives at &lt;a href="https://directus.io"&gt;Directus&lt;/a&gt; is constantly improving our documentation. As a small team with finite time and resources, we rely a lot on user feedback to help guide our writing efforts. But we were missing the most important bit there – your feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We’re Building
&lt;/h2&gt;

&lt;p&gt;At the time of this post, the Directus Docs runs on VitePress (which in turn is based on Vue.js and Vite). Vitepress is a nice bit of kit for quickly generating a static documentation site, but sadly there’s no built-in feature for gathering user feedback. &lt;/p&gt;

&lt;p&gt;So I decided to build my own so our team could make better decisions on where to spend our precious time and attention. &lt;/p&gt;

&lt;p&gt;While this project was built in the context of Vitepress, this post will show you how to do it with Vue generally. Here’s what our finished product will look like. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_EtRp0f9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/13d9ddfe-8527-499a-b70d-2c20f08c7da4" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_EtRp0f9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/13d9ddfe-8527-499a-b70d-2c20f08c7da4" alt="Screenshot of Directus documentation article with a highlighted feedback widget at the bottom of the screen." width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Prerequisites&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before we hop 🐰 in , here’s what you’ll need to follow along:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowledge&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beginner knowledge of Javascript, Typescript, and Vue.js (Composition API)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tooling&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Directus instance up and running (either &lt;a href="https://docs.directus.io/self-hosted/quickstart.html"&gt;self-hosted&lt;/a&gt; or on &lt;a href="https://directus.cloud/"&gt;Directus Cloud&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A Vue.js project set up (&lt;a href="https://vuejs.org/guide/scaling-up/tooling.html#tooling"&gt;Using Vite is recommended&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preparing Directus Collection
&lt;/h2&gt;

&lt;p&gt;First off, we're going to need a place to store all this valuable feedback we'll be gathering. &lt;/p&gt;

&lt;p&gt;Create a &lt;strong&gt;&lt;code&gt;docs_feedback&lt;/code&gt;&lt;/strong&gt; collection with the following data model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;docs_feedback
&lt;span class="p"&gt;
-&lt;/span&gt; id (Type: uuid)
&lt;span class="p"&gt;-&lt;/span&gt; date_created (Type: Timestamp, Interface: Date/Time)
&lt;span class="p"&gt;-&lt;/span&gt; url (Type: String, Interface: Input)
&lt;span class="p"&gt;-&lt;/span&gt; rating (Type: Integer, Interface: Slider)
&lt;span class="p"&gt;-&lt;/span&gt; title (Type: String, Interface: Input)
&lt;span class="p"&gt;-&lt;/span&gt; comments (Type: Text, Interface: Textarea)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating Vue Component for Article Feedback
&lt;/h2&gt;

&lt;p&gt;Just as if it were the lone dev on a cross-functional team – we’re going to place a lot of different responsibilities on our hard-working little Vue component.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rating System&lt;/strong&gt;: We’ll use a 1 to 4 scale, with each value associated with a different message. These messages will help engage users and guide them through the feedback process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Ended Feedback&lt;/strong&gt;: We provide a text area for users to write their thoughts. This is where the gold is. We want users to share their ideas, suggestions, and insights – positive or constructive 😭.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Posting Data to Directus&lt;/strong&gt;: The component handles the submission of the feedback to Directus. It constructs the feedback object and makes a POST request to the feedback API endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scaffolding the Feedback Component
&lt;/h3&gt;

&lt;p&gt;Create a new file in our &lt;code&gt;components&lt;/code&gt; directory named &lt;code&gt;ArticleFeedback.vue&lt;/code&gt; . Then copy and paste the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrapper"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Step 1. Show Rating Buttons --&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"desc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;How can we improve?&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;How helpful was this article?&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Step 2. Ask for Comments --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Step 3. Show Success Message --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve got three different states (or steps as I’m calling them) we’ll need to build. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An initial state that shows the feedback prompt and rating buttons.&lt;/li&gt;
&lt;li&gt;Once a rating has been selected, a state which asks for comments and feedback.&lt;/li&gt;
&lt;li&gt;A success state once form submission is complete.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Adding Props and Reactive Logic
&lt;/h3&gt;

&lt;p&gt;Now let’s start adding our logic to control these three steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;&amp;lt;script setup lang="ts"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ import { ref, reactive } from 'vue';
+
+ const props = defineProps&amp;lt;{
+   title: string;
+   url: string
+ }&amp;gt;();
+
+ const feedback = reactive&amp;lt;{
+   id?: string;
+   rating?: number;
+   comments?: string;
+ }&amp;gt;({});
+
+ const success = ref(false);
&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt;/script&amp;gt;
&lt;/span&gt;
&amp;lt;template&amp;gt;
    &amp;lt;div class="wrapper"&amp;gt;
&lt;span class="gd"&gt;-       &amp;lt;div class="step"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+       &amp;lt;div v-if="!feedback.rating" class="step"&amp;gt;
&lt;/span&gt;            &amp;lt;!-- Step 1. Show Rating Buttons --&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;p class="desc"&amp;gt;How can we improve?&amp;lt;/p&amp;gt;
                &amp;lt;p class="heading"&amp;gt;How helpful was this article?&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;-       &amp;lt;div class="step"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+       &amp;lt;div v-else-if="feedback.rating &amp;amp;&amp;amp; !success" class="step"&amp;gt;
&lt;/span&gt;            &amp;lt;!-- Step 2. Ask for Comments --&amp;gt;
        &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;-       &amp;lt;div class="step"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+       &amp;lt;div v-else class="step"&amp;gt;
&lt;/span&gt;            &amp;lt;!-- Step 3. Show Success Message --&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;&amp;lt;/template&amp;gt;
&lt;/span&gt;
&amp;lt;style scoped&amp;gt;
&lt;span class="gd"&gt;&amp;lt;/style&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Import the &lt;code&gt;ref&lt;/code&gt; and &lt;code&gt;reactive&lt;/code&gt; functions from Vue.&lt;/li&gt;
&lt;li&gt;We’ll pass the &lt;code&gt;url&lt;/code&gt; and page &lt;code&gt;title&lt;/code&gt; as props from the parent component that contains this widget.&lt;/li&gt;
&lt;li&gt;Create a reactive object &lt;code&gt;feedback&lt;/code&gt; to manage our form submission data.&lt;/li&gt;
&lt;li&gt;Create a reactive &lt;code&gt;success&lt;/code&gt; variable to hold the success state.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;v-if&lt;/code&gt;, &lt;code&gt;v-else-if&lt;/code&gt;, and &lt;code&gt;v-else&lt;/code&gt; to control what step of the feedback process is shown.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With the logic roughed in, let’s add our rating buttons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the Rating Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;&amp;lt;script setup lang="ts"&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;import { ref, reactive } from 'vue';
&lt;/span&gt;
const props = defineProps&amp;lt;{ title: string; url: string }&amp;gt;();

const feedback = reactive&amp;lt;{
    id?: string;
    rating?: number;
    comments?: string;
&lt;span class="err"&gt;}&amp;gt;({});&lt;/span&gt;

&lt;span class="gi"&gt;+ const ratingOptions = [
+   { label: 'Worst Doc Ever 🗑️', value: 1, message: 'Woof! 🤦‍♂️ Sorry about that. How do we fix it?' },
+   { label: 'Not Helpful 😡', value: 2, message: '🧐 Help us do better. How can we improve this article?' },
+   { label: 'Helpful 😃', value: 3, message: 'Nice! 👍 Anything we can improve upon?' },
+   { label: 'Super Helpful 🤩', value: 4, message: `Awesome! The whole team is rejoicing in celebration! 🥳🎉🎊 Anything you'd like to say to them?` },
+ ];
+
+ function getRatingOption(rating: number) {
+   return ratingOptions.find((option) =&amp;gt; option.value === rating);
+ }
&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt;/script&amp;gt;
&lt;/span&gt;
&amp;lt;template&amp;gt;
    &amp;lt;div class="wrapper"&amp;gt;
        &amp;lt;div v-if="!feedback.rating" class="step"&amp;gt;
            &amp;lt;!-- Step 1. Show Rating Buttons --&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;p class="desc"&amp;gt;How can we improve?&amp;lt;/p&amp;gt;
                &amp;lt;p class="heading"&amp;gt;How helpful was this article?&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
&lt;span class="gi"&gt;+           &amp;lt;div class="button-container"&amp;gt;
+                   &amp;lt;!-- We'll add a function for handling button clicks while adding our submission logic --&amp;gt;
+                   &amp;lt;button v-for="item in ratingOptions" :key="item.value" class="btn"&amp;gt;
+                       &amp;lt;span&amp;gt;{{ item.label }}&amp;lt;/span&amp;gt;
+                   &amp;lt;/button&amp;gt;
+           &amp;lt;/div&amp;gt;
&lt;/span&gt;        &amp;lt;/div&amp;gt;
        &amp;lt;div v-else-if="feedback.rating &amp;amp;&amp;amp; !success" class="step"&amp;gt;
            &amp;lt;!-- Step 2. Ask for Comments --&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div v-else class="step"&amp;gt;
            &amp;lt;!-- Step 3. Show Success Message --&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;&amp;lt;/template&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rating options will be an array of objects that have a visible &lt;code&gt;label&lt;/code&gt;, a corresponding &lt;code&gt;value&lt;/code&gt; of 1-4, and a dynamic&lt;code&gt;message&lt;/code&gt; that we’ll display to encourage the user to leave comments after selecting a rating.&lt;/p&gt;

&lt;p&gt;We’ll also create a small helper function to return the rating object based when passing a number value. This will come in handy in the second step because we’re going to display the rating the user chose.&lt;/p&gt;

&lt;p&gt;Add a new div to Step 1 below the feedback prompt that will contain our rating options. Inside that, we’ll use &lt;code&gt;v-for&lt;/code&gt; to loop through the &lt;code&gt;ratingOptions&lt;/code&gt; array and render the individual buttons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asking for Comments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;&amp;lt;template&amp;gt;
&lt;/span&gt;    &amp;lt;div class="wrapper"&amp;gt;
        &amp;lt;div v-if="!feedback.rating" class="step"&amp;gt;
            &amp;lt;!-- Step 1. Show Rating Buttons --&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;p class="desc"&amp;gt;How can we improve?&amp;lt;/p&amp;gt;
                &amp;lt;p class="heading"&amp;gt;How helpful was this article?&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class="button-container"&amp;gt;
                &amp;lt;button v-for="item in ratingOptions" :key="item.value" class="btn"&amp;gt;
                    &amp;lt;span&amp;gt;{{ item.label }}&amp;lt;/span&amp;gt;
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div v-else-if="feedback.rating &amp;amp;&amp;amp; !success" class="step"&amp;gt;
            &amp;lt;!-- Step 2. Ask for Comments --&amp;gt;
&lt;span class="gi"&gt;+               &amp;lt;div&amp;gt;
+                   &amp;lt;p class="desc"&amp;gt;This article is&amp;lt;/p&amp;gt;
+                   &amp;lt;div&amp;gt;
+                       &amp;lt;span&amp;gt;{{ getRatingOption(feedback.rating)?.label }}&amp;lt;/span&amp;gt;
+                       &amp;lt;button class="btn" @click="feedback.rating = undefined"&amp;gt;
+                           ❌
+                       &amp;lt;/button&amp;gt;
+                   &amp;lt;/div&amp;gt;
+               &amp;lt;/div&amp;gt;
+               &amp;lt;p class="heading"&amp;gt;{{ getRatingOption(feedback.rating)?.message }}&amp;lt;/p&amp;gt;
+               &amp;lt;textarea v-model="feedback.comments" autofocus class="input" /&amp;gt;
+               &amp;lt;button class="btn btn-primary" :disabled="!feedback.comments"&amp;gt;
+                   Send Us Your Feedback
+               &amp;lt;/button&amp;gt;
&lt;/span&gt;        &amp;lt;/div&amp;gt;
        &amp;lt;div v-else class="step"&amp;gt;
            &amp;lt;!-- Step 3. Show Success Message --&amp;gt;
&lt;span class="gi"&gt;+           &amp;lt;p class="heading"&amp;gt;Thanks for your feedback!&amp;lt;/p&amp;gt;
&lt;/span&gt;        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;&amp;lt;/template&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Step 2 of the process, we’re showing the user the rating they chose using our &lt;code&gt;getRatingOption&lt;/code&gt; helper function we created. &lt;/p&gt;

&lt;p&gt;To improve the user experience, we’ll also let users go back and choose a different rating in case they picked the wrong one by mistake. Whenever they click the close button we’ll set the &lt;code&gt;feedback.rating&lt;/code&gt; property to &lt;code&gt;undefined&lt;/code&gt; which will take the user back to Step 1 based on the &lt;code&gt;v-if&lt;/code&gt; logic we created.&lt;/p&gt;

&lt;p&gt;Below that, we’ll show the proper message for the option they chose to encourage them to leave helpful comments in short form with a textarea input and a submit button.&lt;/p&gt;

&lt;p&gt;We’ll also prevent them from submitting from Step 2 when the comments are empty, so we pass the &lt;code&gt;:disabled="!feedback.comments"&lt;/code&gt; prop to the button element.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Styling
&lt;/h3&gt;

&lt;p&gt;Next, let’s add some basic styling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;// ^^ Rest of ArticleFeedback.vue Component ^^
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f6f6f7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.step&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.desc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.75&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;grid-gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-color&lt;/span&gt; &lt;span class="m"&gt;0.25s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;background-color&lt;/span&gt; &lt;span class="m"&gt;0.25s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;vertical-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;middle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt;&lt;span class="nd"&gt;:disabled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6644ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn-primary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6644ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6644ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn-primary&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4422dd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4422dd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#ccc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;768px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.button-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding Submission Logic Inside The Vue Component
&lt;/h3&gt;

&lt;p&gt;We’re going to write a handler function to actually submit our data to our Directus &lt;code&gt;docs_feedback&lt;/code&gt; collection.&lt;/p&gt;

&lt;p&gt;At the end of our &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, let’s add our submission handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleSubmission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Replace this with your own Directus URL&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directusBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourdirectusurl.directus.app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// If we've already created a feedback record, we'll update it with the new rating or comments.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;directusBaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items/docs_feedback/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;directusBaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items/docs_feedback/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// If the reponse has comments, we can assume they've completed the second step. So we'll show the success message.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function &lt;code&gt;handleSubmission&lt;/code&gt; accepts an optional rating and then conditionally creates a new feedback item or updates depending on which step the user completed.&lt;/p&gt;

&lt;p&gt;We also need to update our template to call our handler using the &lt;code&gt;@click&lt;/code&gt; directive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;&amp;lt;template&amp;gt;
&lt;/span&gt;    &amp;lt;div class="wrapper"&amp;gt;
        &amp;lt;Transition name="fade" mode="out-in"&amp;gt;
            &amp;lt;div v-if="!feedback.rating" class="step"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;div&amp;gt;
                        &amp;lt;p class="desc"&amp;gt;How can we improve?&amp;lt;/p&amp;gt;
                        &amp;lt;p class="heading"&amp;gt;How helpful was this article?&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="button-container"&amp;gt;
                    &amp;lt;button v-for="item in ratingOptions"
                        :key="item.value"
                        class="btn"
&lt;span class="gi"&gt;+                       @click="handleSubmission(item.value)"&amp;gt;
&lt;/span&gt;                        &amp;lt;span&amp;gt;{{ item.label }}&amp;lt;/span&amp;gt;
                    &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div v-else-if="feedback.rating &amp;amp;&amp;amp; !success" class="step"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;p class="desc"&amp;gt;This article is&amp;lt;/p&amp;gt;
                    &amp;lt;div&amp;gt;
                        &amp;lt;span&amp;gt;{{ getRatingOption(feedback.rating)?.label }}&amp;lt;/span&amp;gt;
                        &amp;lt;button style="margin-left: 0.5rem" class="btn" @click="feedback.rating = undefined"&amp;gt;
                            &amp;lt;span mi icon&amp;gt;close&amp;lt;/span&amp;gt;
                        &amp;lt;/button&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;p class="heading"&amp;gt;{{ getRatingOption(feedback.rating)?.message }}&amp;lt;/p&amp;gt;
                &amp;lt;textarea v-model="feedback.comments" autofocus class="input" /&amp;gt;
                &amp;lt;button
                    class="btn btn-primary"
                    :disabled="!feedback.comments"
&lt;span class="gi"&gt;+                   @click="handleSubmission()"&amp;gt;
&lt;/span&gt;                    Send Us Your Feedback
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div v-else class="step"&amp;gt;
                &amp;lt;p class="heading"&amp;gt;Thanks for your feedback!&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/Transition&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;span class="gd"&gt;&amp;lt;/template&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sweet! Now there’s just one last step before we have a working component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating Permissions inside Directus
&lt;/h2&gt;

&lt;p&gt;Right now, if we try to submit some feedback, we’re probably to going receive an &lt;code&gt;Permission denied&lt;/code&gt; error from Directus. &lt;/p&gt;

&lt;p&gt;This is because all collections have zero public permissions by default. While this is great for security, it’s not so great if we want to store our feedback data. &lt;/p&gt;

&lt;p&gt;Open up the Public role with the &lt;a href="https://docs.directus.io/user-guide/user-management/permissions.html"&gt;Roles &amp;amp; Permission settings&lt;/a&gt;. Then scroll to find the &lt;code&gt;docs_feedback&lt;/code&gt; collection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--i0KEolQ3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/5586bb04-6628-4f6f-81d5-a6aae901b8dd" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i0KEolQ3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/5586bb04-6628-4f6f-81d5-a6aae901b8dd" alt="Directus Roles and Permissions settings page, the docs_feedback collection is highlighted and all CRUD permission settings are set to not allowed" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create and Update Operations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click the &lt;span&gt;block&lt;/span&gt; button inside each column and choose &lt;span&gt;check&lt;/span&gt; All Access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read Operation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We might not want any prying eyes to be able to read the actual feedback ratings and content, so we’ll use some custom permissions to restrict the fields that anyone can ‘read’.&lt;/p&gt;

&lt;p&gt;Click the button for the Read column, and choose Custom Permissions.&lt;/p&gt;

&lt;p&gt;On the Field Permissions tab, check only the &lt;code&gt;id&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6GwwvE6c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/d9788785-96b4-4eac-9dc4-a249882b0c5c" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6GwwvE6c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/d9788785-96b4-4eac-9dc4-a249882b0c5c" alt="Custom permission settings screen for docs_feedback Read operation. List of fields with checkboxes but only the id field is checked." width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you’re all done, it should look like this screenshot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XwGOKlvq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/89fa63c3-05bc-4cc4-ac4f-a871d92597a9" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XwGOKlvq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/89fa63c3-05bc-4cc4-ac4f-a871d92597a9" alt="Directus Roles and Permissions settings page, the docs_feedback collection is highlighted, Create and Update operation permissions are set to Allowed, Read operation has custom permissions" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Awesome! Now on to testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Feedback Widget
&lt;/h2&gt;

&lt;p&gt;Let’s open this up our Vue app and our Directus instance to test that everything is working as intended. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w_NOFG2o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/67a4bd6b-f44d-4290-8504-0a5355c54955" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w_NOFG2o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/67a4bd6b-f44d-4290-8504-0a5355c54955" alt="Demo of the Vue feedback widget being used" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure you check that the form submissions are correct inside Directus.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AFGAntIC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/cf5944ff-4f29-45fc-8b6d-6e2da6147e5a" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AFGAntIC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://marketing.directus.app/assets/cf5944ff-4f29-45fc-8b6d-6e2da6147e5a" alt="A Directus detail page for the an item in docs_feedback collection" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Here’s a few of the next steps you may want to explore beyond this tutorial.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analyze the Data Using Directus Insights
&lt;/h3&gt;

&lt;p&gt;Collecting feedback is just one half of the equation. Analyzing and taking action on the data you receive is the more important part.&lt;/p&gt;

&lt;p&gt;Our module for creating &lt;a href="https://docs.directus.io/user-guide/insights/dashboards.html"&gt;no-code dashboards - Directus Insights&lt;/a&gt; - can help you understand the data you collect much easier and faster than browsing through a list of feedback. &lt;/p&gt;

&lt;h3&gt;
  
  
  Secure form submissions
&lt;/h3&gt;

&lt;p&gt;To post our form submissions, we just enabled Public create and update access for the &lt;code&gt;docs_feedback&lt;/code&gt; collection inside Directus. &lt;/p&gt;

&lt;p&gt;There’s not a lot to gain by spamming documentation feedback submissions but you never know with folks these days. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security wise - we could do better.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Here’s a few options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Flow with using an incoming webhook trigger that processes the incoming requests.&lt;/li&gt;
&lt;li&gt;Obscure our Directus instance URL by using a proxy or serverless function to make the call to the Directus API. Netlify, Vercel, and other hosting static site hosting platforms simplify this process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Add a session identifier to track feedback from the same user
&lt;/h3&gt;

&lt;p&gt;It could be very handy to know if feedback across different articles is coming from the same user.  We don’t really need full blown user sessions stored in the database for this. We could implement it client-side by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding a field to for &lt;code&gt;session_id&lt;/code&gt; or &lt;code&gt;visitor_id&lt;/code&gt; to our collection inside Directus&lt;/li&gt;
&lt;li&gt;generating a random ID on a first visit or feedback submission inside the Vue app&lt;/li&gt;
&lt;li&gt;storing the ID within the browser using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"&gt;localStorage&lt;/a&gt; or &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage"&gt;sessionStorage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;grabbing that ID and passing it in the API call to Directus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you find this post useful - if you have any questions feel free to join &lt;a href="https://directus.chat"&gt;our Directus Discord server&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>directus</category>
      <category>vue</category>
    </item>
  </channel>
</rss>
