<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://directus.io</id>
    <title>Directus</title>
    <updated>2026-03-18T16:14:19.482Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>Directus Team</name>
    </author>
    <link rel="alternate" href="https://directus.io"/>
    <subtitle>The official Directus content feed. Stay up to speed with the latest Directus news, releases, and tutorials from our developer blog.</subtitle>
    <logo>https://marketing.directus.app/assets/54c69e23-b8af-4aa8-abc5-2d3544bb7c23.png</logo>
    <icon>https://directus.io/favicon.ico</icon>
    <rights>© 2026 Monospace Inc.</rights>
    <entry>
        <title type="html"><![CDATA[You Built More Than a Ticketing System, So What Happens When Jira Data Center Goes Away?]]></title>
        <id>https://directus.io/blog/jira-data-center-eol-migration-alternative</id>
        <link href="https://directus.io/blog/jira-data-center-eol-migration-alternative"/>
        <updated>2026-03-15T17:34:00.000Z</updated>
        <summary type="html"><![CDATA[For many orgs, Jira DC wasn't just a ticketing system. It was an operational data platform. Here's what that means for your EOL migration strategy, and who should be looking at Directus.]]></summary>
        <content type="html"><![CDATA[<p>For many orgs, Jira DC wasn't just a ticketing system. It was an operational data platform. Here's what that means for your EOL migration strategy, and who should be looking at Directus.</p><p>-------</p><p><a href="https://directus.io/blog/jira-data-center-eol-migration-alternative">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>David Stockton </name>
            <uri>https://directus.io/team/david-stockton</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus v11.16: Global Draft Versions, Multimodal AI, and Smarter Deployments]]></title>
        <id>https://directus.io/blog/directus-11-16-release</id>
        <link href="https://directus.io/blog/directus-11-16-release"/>
        <updated>2026-03-10T13:11:00.000Z</updated>
        <summary type="html"><![CDATA[A native draft workflow, an AI Assistant that can see your images and PDFs, and a deployment module your whole team can use.]]></summary>
        <content type="html"><![CDATA[<p>A native draft workflow, an AI Assistant that can see your images and PDFs, and a deployment module your whole team can use.</p><p>-------</p><p><a href="https://directus.io/blog/directus-11-16-release">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>James White</name>
            <uri>https://directus.io/team/james-white</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus v11.15: Native Collaborative Editing, AI Assistant Goes GA, and One-Click Deployments]]></title>
        <id>https://directus.io/blog/directus-11-15-release</id>
        <link href="https://directus.io/blog/directus-11-15-release"/>
        <updated>2026-02-12T18:17:33.000Z</updated>
        <summary type="html"><![CDATA[Real-time collaborative editing in the core, an AI Assistant that actually knows your content, and Vercel deployments from the Data Studio.]]></summary>
        <content type="html"><![CDATA[<p>Real-time collaborative editing in the core, an AI Assistant that actually knows your content, and Vercel deployments from the Data Studio.</p><p>-------</p><p><a href="https://directus.io/blog/directus-11-15-release">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>James White</name>
            <uri>https://directus.io/team/james-white</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[How I Built an Employee Advocacy Scoreboard with Directus, Zapier, and Claude Code]]></title>
        <id>https://directus.io/blog/employee-advocacy-scoreboard-directus-zapier-claude</id>
        <link href="https://directus.io/blog/employee-advocacy-scoreboard-directus-zapier-claude"/>
        <updated>2026-02-09T13:00:00.000Z</updated>
        <summary type="html"><![CDATA[A practical guide to tracking team social sharing with AI-powered detection, Directus as the backend, and a vibecoded React dashboard.]]></summary>
        <content type="html"><![CDATA[<p>A practical guide to tracking team social sharing with AI-powered detection, Directus as the backend, and a vibecoded React dashboard.</p><p>-------</p><p><a href="https://directus.io/blog/employee-advocacy-scoreboard-directus-zapier-claude">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Matt Minor</name>
            <uri>https://directus.io/team/matt-minor</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Hello from Directus' New VP of Engineering]]></title>
        <id>https://directus.io/blog/hello-from-directus-new-vp-of-engineering</id>
        <link href="https://directus.io/blog/hello-from-directus-new-vp-of-engineering"/>
        <updated>2026-01-29T13:00:00.000Z</updated>
        <summary type="html"><![CDATA[From my first BASIC program on a Commodore 64 to leading cloud infrastructure at Confluent, my journey has always been about building great things with great people.]]></summary>
        <content type="html"><![CDATA[<p>From my first BASIC program on a Commodore 64 to leading cloud infrastructure at Confluent, my journey has always been about building great things with great people.</p><p>-------</p><p><a href="https://directus.io/blog/hello-from-directus-new-vp-of-engineering">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>David Stockton </name>
            <uri>https://directus.io/team/david-stockton</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus v11.14: AI Chat Beta, Bulk Downloads + More]]></title>
        <id>https://directus.io/blog/directus-v11-14-release</id>
        <link href="https://directus.io/blog/directus-v11-14-release"/>
        <updated>2025-12-16T16:39:54.000Z</updated>
        <summary type="html"><![CDATA[Enterprise authentication patterns, quality-of-life improvements for asset-heavy workflows, and better form organization.]]></summary>
        <content type="html"><![CDATA[<p>Enterprise authentication patterns, quality-of-life improvements for asset-heavy workflows, and better form organization.</p><p>-------</p><p><a href="https://directus.io/blog/directus-v11-14-release">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>James White</name>
            <uri>https://directus.io/team/james-white</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[An Update to Our Cloud Tiers]]></title>
        <id>https://directus.io/blog/an-update-to-cloud-tiers-november-2025</id>
        <link href="https://directus.io/blog/an-update-to-cloud-tiers-november-2025"/>
        <updated>2025-12-03T14:27:31.000Z</updated>
        <summary type="html"><![CDATA[Today we made some significant changes to our Cloud offering. Here's why, and what that means for Directus users moving forward.]]></summary>
        <content type="html"><![CDATA[<p>Today we made some significant changes to our Cloud offering. Here's why, and what that means for Directus users moving forward.</p><p>-------</p><p><a href="https://directus.io/blog/an-update-to-cloud-tiers-november-2025">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Benjamin Haynes</name>
            <uri>https://directus.io/team/ben-haynes</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus v11.13: Native MCP Support and Content Comparison]]></title>
        <id>https://directus.io/blog/directus-v11-13-release</id>
        <link href="https://directus.io/blog/directus-v11-13-release"/>
        <updated>2025-11-07T19:07:26.000Z</updated>
        <summary type="html"><![CDATA[AI agents can now interact directly with your Directus data, and comparing content versions just got way easier.]]></summary>
        <content type="html"><![CDATA[<p>AI agents can now interact directly with your Directus data, and comparing content versions just got way easier.</p><p>-------</p><p><a href="https://directus.io/blog/directus-v11-13-release">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>James White</name>
            <uri>https://directus.io/team/james-white</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus launches native Model Context Protocol, redefining the ‘collaborative’ CMS]]></title>
        <id>https://directus.io/blog/mcp-collaborative-cms</id>
        <link href="https://directus.io/blog/mcp-collaborative-cms"/>
        <updated>2025-11-05T19:50:07.000Z</updated>
        <summary type="html"><![CDATA[Native MCP support transforms content management into an AI-collaborative workflow, making teams more capable and productive together]]></summary>
        <content type="html"><![CDATA[<p>Native MCP support transforms content management into an AI-collaborative workflow, making teams more capable and productive together</p><p>-------</p><p><a href="https://directus.io/blog/mcp-collaborative-cms">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Matt Minor</name>
            <uri>https://directus.io/team/matt-minor</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[How FEG Achieved 70% Faster Content Deployment Across 5 Gaming Markets]]></title>
        <id>https://directus.io/case-studies/fortuna-entertainment-group</id>
        <link href="https://directus.io/case-studies/fortuna-entertainment-group"/>
        <updated>2025-07-21T17:41:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how Fortuna Entertainment Group used Directus to achieve faster content deployment, unified web and mobile management, and deliver one of the best rollouts in company history.]]></summary>
        <content type="html"><![CDATA[<p>Learn how Fortuna Entertainment Group used Directus to achieve faster content deployment, unified web and mobile management, and deliver one of the best rollouts in company history.</p><p>-------</p><p><a href="https://directus.io/case-studies/fortuna-entertainment-group">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Emma Paajanen</name>
            <uri>https://directus.io/team/emma-paajanen</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[We're Now SOC 2 Certified (And Yes, It Was As Fun As It Sounds)]]></title>
        <id>https://directus.io/blog/directus-soc-2-type-ii-certification</id>
        <link href="https://directus.io/blog/directus-soc-2-type-ii-certification"/>
        <updated>2025-07-15T13:17:00.000Z</updated>
        <summary type="html"><![CDATA[Directus Cloud is now SOC 2 Type II certified. Learn what that actually means for you. ]]></summary>
        <content type="html"><![CDATA[<p>Directus Cloud is now SOC 2 Type II certified. Learn what that actually means for you. </p><p>-------</p><p><a href="https://directus.io/blog/directus-soc-2-type-ii-certification">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Benjamin Haynes</name>
            <uri>https://directus.io/team/ben-haynes</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Introducing Collaborative Editing: Work Together Without Chaos]]></title>
        <id>https://directus.io/blog/collaborative-editing</id>
        <link href="https://directus.io/blog/collaborative-editing"/>
        <updated>2025-06-25T16:09:00.000Z</updated>
        <summary type="html"><![CDATA[Edit content together without the usual coordination headaches or accidentally overwriting each other’s work.]]></summary>
        <content type="html"><![CDATA[<p>Edit content together without the usual coordination headaches or accidentally overwriting each other’s work.</p><p>-------</p><p><a href="https://directus.io/blog/collaborative-editing">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Christina Harker</name>
            <uri>https://directus.io/team/christina-harker</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Introducing Visual Editing: Edit Your Content in Context]]></title>
        <id>https://directus.io/blog/introducing-visual-editing</id>
        <link href="https://directus.io/blog/introducing-visual-editing"/>
        <updated>2025-04-29T17:15:00.000Z</updated>
        <summary type="html"><![CDATA[Edit text and images right on your site within Directus, see changes instantly, and simplify your content workflow.]]></summary>
        <content type="html"><![CDATA[<p>Edit text and images right on your site within Directus, see changes instantly, and simplify your content workflow.</p><p>-------</p><p><a href="https://directus.io/blog/introducing-visual-editing">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Christina Harker</name>
            <uri>https://directus.io/team/christina-harker</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Database-First or API-First: Which Scales Better?]]></title>
        <id>https://directus.io/blog/database-first-or-api-first</id>
        <link href="https://directus.io/blog/database-first-or-api-first"/>
        <updated>2025-02-20T21:00:00.000Z</updated>
        <summary type="html"><![CDATA[Most architectural decisions are presented as binary choices. The reality? Smart teams pick their battles based on real-world needs.]]></summary>
        <content type="html"><![CDATA[<p>Most architectural decisions are presented as binary choices. The reality? Smart teams pick their battles based on real-world needs.</p><p>-------</p><p><a href="https://directus.io/blog/database-first-or-api-first">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Christina Harker</name>
            <uri>https://directus.io/team/christina-harker</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Solving the Multiple-Frontend Problem in Modern Applications]]></title>
        <id>https://directus.io/blog/solving-the-multiple-frontend-problem-in-modern-applications</id>
        <link href="https://directus.io/blog/solving-the-multiple-frontend-problem-in-modern-applications"/>
        <updated>2025-02-18T20:06:00.000Z</updated>
        <summary type="html"><![CDATA[The old "one framework to rule them all" approach is dead, and pretending otherwise is slowing down your entire development cycle.]]></summary>
        <content type="html"><![CDATA[<p>The old "one framework to rule them all" approach is dead, and pretending otherwise is slowing down your entire development cycle.</p><p>-------</p><p><a href="https://directus.io/blog/solving-the-multiple-frontend-problem-in-modern-applications">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Matt Minor</name>
            <uri>https://directus.io/team/matt-minor</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[REST vs. GraphQL vs. tRPC: Choosing Your API Architecture]]></title>
        <id>https://directus.io/blog/rest-graphql-tprc</id>
        <link href="https://directus.io/blog/rest-graphql-tprc"/>
        <updated>2025-02-11T21:28:00.000Z</updated>
        <summary type="html"><![CDATA[Your API architecture choice will haunt you for years - unless you focus on what actually matters instead of what's trending on GitHub.
]]></summary>
        <content type="html"><![CDATA[<p>Your API architecture choice will haunt you for years - unless you focus on what actually matters instead of what's trending on GitHub.
</p><p>-------</p><p><a href="https://directus.io/blog/rest-graphql-tprc">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Bryant Gillespie</name>
            <uri>https://directus.io/team/bryant-gillespie</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Developers vs. Content Teams: Why There's Always Tension]]></title>
        <id>https://directus.io/blog/developers-vs-content-teams</id>
        <link href="https://directus.io/blog/developers-vs-content-teams"/>
        <updated>2025-02-07T02:53:00.000Z</updated>
        <summary type="html"><![CDATA[Your dev and content teams aren't actually fighting each other – they're fighting broken systems that make collaboration impossible.]]></summary>
        <content type="html"><![CDATA[<p>Your dev and content teams aren't actually fighting each other – they're fighting broken systems that make collaboration impossible.</p><p>-------</p><p><a href="https://directus.io/blog/developers-vs-content-teams">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Christina Harker</name>
            <uri>https://directus.io/team/christina-harker</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Stop Overengineering Your Multi-tenant Architecture]]></title>
        <id>https://directus.io/blog/stop-overengineering-your-multitenant-architecture</id>
        <link href="https://directus.io/blog/stop-overengineering-your-multitenant-architecture"/>
        <updated>2025-02-04T22:37:00.000Z</updated>
        <summary type="html"><![CDATA[Most teams are building multi-tenant systems like they're assembling IKEA furniture blindfolded - following complex instructions when a simpler solution exists.]]></summary>
        <content type="html"><![CDATA[<p>Most teams are building multi-tenant systems like they're assembling IKEA furniture blindfolded - following complex instructions when a simpler solution exists.</p><p>-------</p><p><a href="https://directus.io/blog/stop-overengineering-your-multitenant-architecture">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Bryant Gillespie</name>
            <uri>https://directus.io/team/bryant-gillespie</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[1000+ Employees, 1 Data Platform]]></title>
        <id>https://directus.io/case-studies/prusa3d</id>
        <link href="https://directus.io/case-studies/prusa3d"/>
        <updated>2025-01-31T20:47:00.000Z</updated>
        <summary type="html"><![CDATA[How Prusa3D unified their scattered data systems into a single platform, cutting support overhead by 50% while scaling to 1000+ employees.]]></summary>
        <content type="html"><![CDATA[<p>How Prusa3D unified their scattered data systems into a single platform, cutting support overhead by 50% while scaling to 1000+ employees.</p><p>-------</p><p><a href="https://directus.io/case-studies/prusa3d">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Christina Harker</name>
            <uri>https://directus.io/team/christina-harker</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[January 2025 – Directus Updates, Releases, + More]]></title>
        <id>https://directus.io/blog/january-2025-directus-updates-releases-more</id>
        <link href="https://directus.io/blog/january-2025-directus-updates-releases-more"/>
        <updated>2025-01-31T18:21:00.000Z</updated>
        <summary type="html"><![CDATA[Node 22 support, refreshed Kanban layouts, improved translations + more. Check out what our team has been up to this month.]]></summary>
        <content type="html"><![CDATA[<p>Node 22 support, refreshed Kanban layouts, improved translations + more. Check out what our team has been up to this month.</p><p>-------</p><p><a href="https://directus.io/blog/january-2025-directus-updates-releases-more">Read the full post on the Directus website -></a></p>]]></content>
        <author>
            <name>Benjamin Haynes</name>
            <uri>https://directus.io/team/ben-haynes</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Changelog #5]]></title>
        <id>https://docs.directus.io/blog/the-changelog-5</id>
        <link href="https://docs.directus.io/blog/the-changelog-5"/>
        <updated>2024-12-13T17:38:06.000Z</updated>
        <summary type="html"><![CDATA[A list of this month’s Directus product updates.]]></summary>
        <content type="html"><![CDATA[<ul>
<li>In Directus <a href="https://github.com/directus/directus/releases/tag/v11.2.2">11.2.2</a>, we made S3 connection settings configurable via environment variables. This includes things like connection timeout.</li>
<li>In Directus <a href="https://github.com/directus/directus/releases/tag/v11.3.0">11.3.0</a> we introduced new retention settings for activities, revisions, and flow logs. These three system tables can become quite large, and now you can change how long they will be kept for before being pruned.</li>
<li>Earlier this year we committed to providing security updates to Directus 10 until the end of the year, make sure you upgrade to 10.13.4 if you’re still on the version 10 family. The same patch has been included in the Directus 11 patch as well.</li>
<li>Integrate HubSpot’s API into Directus flows with the <a href="https://github.com/directus-labs/extensions/tree/main/packages/hubspot-operation">HubSpot API Operation</a>.</li>
<li>Add a map panel (powered by D3) to your dashboard that displays countries in different colors based on their value using the <a href="https://github.com/directus-labs/extensions/tree/main/packages/choropleth-map-panel">Choropleth Map Panel</a>.</li>
<li>Create an interactive tour for your form from the <a href="https://github.com/directus-labs/extensions/tree/main/packages/tour-group-interface">Tour Group Interface</a>.</li>
<li>Integrate Google Maps address autocompletion functionality into the Directus Editor with the <a href="https://github.com/directus-labs/extensions/tree/main/packages/address-completion-interface">Address Completion Interface</a>.</li>
</ul>]]></content>
        <author>
            <name>Beth Loft</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[How I Built an AI Open Source Santa Roast App with Directus and Nuxt]]></title>
        <id>https://docs.directus.io/blog/ai-santa-roast-app-with-directus-nuxt</id>
        <link href="https://docs.directus.io/blog/ai-santa-roast-app-with-directus-nuxt"/>
        <updated>2024-12-09T12:40:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant breaks down how he built an AI-powered app that roasts developers based on their GitHub contributions.]]></summary>
        <content type="html"><![CDATA[<p>Hey folks! Bryant here from Directus. In this post, I’ll walk you through our <a href="https://salty-santa.vercel.app">Salty Open Source Santa</a> project - how we built it, why we built it, and all the fun little features we packed into this thing. Let's dive in!</p>
&lt;iframe style=&quot;width:100%; aspect-ratio:16/9; margin-bottom: 1em;&quot; src=&quot;https://www.youtube.com/embed/aHHdh50hkG4&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&gt;&lt;/iframe&gt;
<p><strong>What is Salty Open Source Santa?</strong></p>
<p>So what exactly is this thing? Well, it's basically the open source community's naughty or nice list. You write a letter to Santa, and he analyzes your public GitHub profile data to determine if your open source contributions were good enough to make the nice list. And then he writes you back a salty roast-style letter.</p>
<p><img src="https://marketing.directus.app/assets/a8d8b0a8-1fa7-4e9d-9036-0962f11b6163" alt="Screenshot of OS Santa app" /></p>
<p><strong>You can check to see if you’re on Open Source Santa’s Naughty or Nice list at <a href="https://salty-santa.vercel.app">https://salty-santa.vercel.app</a></strong></p>
<h2>The Idea 💡</h2>
<p>Like a lot of fun projects, this started with a simple conversation on Slack. My colleague John Daniels and I were brainstorming ideas for our Christmas promotion. Last year, we did a whole <a href="https://x.com/directus/status/1734671700296380579">&quot;12 Days of CMS&quot; thing</a> where team members sang that dreadful song. And this year, I wanted to up the ante a bit.</p>
<p><img src="https://marketing.directus.app/assets/d3a7018e-ff9d-4010-8fe7-34e945dbb87d" alt="Conversation about formation of the Salty Santa app" /></p>
<p>John suggested scanning letters and having them transcribed. I’m way more of a smartass than John so I said &quot;Hey, what if we make it snarkier? Like, you write a letter to Santa and he roasts you back?”</p>
<p>So with that direction, I built the first version in an episode of our <a href="https://directus.io/tv/100-apps-100-hours">&quot;100 Apps, 100 Hours&quot; show on Directus TV</a>.  Let's just say it wasn't as pretty as what you see now. Here’s what it looked like.</p>
&lt;video width=&quot;99%&quot; height=&quot;540&quot; autoplay loop muted controls&gt;
        &lt;source src=&quot;https://marketing.directus.app/assets/e053e36c-a7ee-4773-99cb-af144041564d&quot; type=&quot;video/mp4&quot; &gt;
    &lt;/video&gt;
<p>But after recording the episode, I still couldn’t get the idea out of my head and I thought there was a lot more fun we could have with it. I scoped the features out a bit further with some brainstorming help from the fine folks on my team – Matt, Christina, and Lindsey.</p>
<p><strong>Here’s the additional feature list we came up with:</strong></p>
<ul>
<li>A scoring algorithm to be more fair who makes it onto the nice list</li>
<li>An actual Nice list page to see which devs are in Santa’s good graces</li>
<li>Roast a friend mode with suggestions from your GitHub organization</li>
<li>A “Spicyness” Meter to rank the spiciest letters and increase enagement</li>
<li>An animated naughty or nice “gauge” to build suspense while you wait on the letter</li>
<li>Dynamic OG images that mimic a personalized letter from Santa to increase sharing</li>
<li>And the cherry on top – Santa actually reads the letter aloud to you</li>
</ul>
<p><em>We’ll cover some of these features in-depth, but first let’s run the through the tech stack.</em></p>
<h2>The Tech Stack 💾</h2>
<p>Let's dive into the fun stuff - the actual tech that powers Santa’s roasts.</p>
<h3>Backend – Directus</h3>
<p>We're using Directus for the backend (shocking, I know 😉). But it's actually a pretty lightweight data model and setup compared to other projects I’ve built. We've got just four collections:</p>
<pre><code class="language-md">// Our main collections
- profiles (stores all the letters and scores)
- likes (for that spicy meter!)
- metrics (for storing calculated metrics on a daily basis)
- globals (help content, site title, etc.)
</code></pre>
<p><img src="https://marketing.directus.app/assets/dd3faa3b-74b6-41d6-8ae6-9195e187b67b" alt="" /></p>
<p>Under the hood, it's all sitting on a PostgreSQL database – hosted on <a href="https://directus.cloud">Directus Cloud</a>. Anytime I add a collection or field to my data model, Directus mirrors those changes to Postgres and updates the APIs automatically. Super handy when you're iterating quickly on a project like this.</p>
<p>All communication to the frontend is through a single Directus user name “Santa’s Helper” (gotta carry the theme 🤣🎅). Santa’s Helper authenticates using a static access token and has a single Access Policy called <code>Elves</code>.</p>
<p><img src="https://marketing.directus.app/assets/e379102b-560d-40a2-b599-2a215bbe9ceb" alt="" /></p>
<p>The <code>Elves</code> policy has create, read, and update permissions on <code>profiles</code> and <code>likes</code>. And also read permissions for the <code>directus_</code> system collections in order to generate types using a helper Node script.</p>
<p><img src="https://marketing.directus.app/assets/db50f501-7db3-4d9e-a07c-7894cf5dff0e" alt="" /></p>
<p>If you ever use this same pattern, just make sure you’re only using static access tokens for server-to-server comms. You don’t want to expose those to anyone on the frontend because of the elevated permissions that might be attached.</p>
<h3>Frontend – Nuxt</h3>
<p>For the frontend, we're running with <a href="https://nuxt.com">Nuxt</a>.</p>
<p>We're using the alpha version of <a href="https://ui.nuxt.com">Nuxt UI</a> (living dangerously, I know!). It's built on <a href="https://www.radix-vue.com/">RadixVue</a> and <a href="https://tailwindcss.com/blog/tailwindcss-v4-beta">Tailwind CSS v4-beta</a>. The component library is fantastic - it gave us all these nice little UI pieces that we could customize for our holiday theme.</p>
<p><strong>Business Logic in Nuxt Server Routes</strong></p>
<p>We’re using Nuxt Server Routes pretty heavily in this project. They give us these nice, type-safe API endpoints that we proxy to the Directus API. And they add an extra layer of caching that we can leverage to reduce costs and improve performance since things like GitHub profile data don’t need to be realtime.</p>
<p><strong>Authentication with Nuxt Auth Utils</strong></p>
<p>Authentication is really simple and handled via a GitHub OAuth app and the <a href="https://github.com/atinux/nuxt-auth-utils">nuxt-auth-utils</a> package.</p>
<p>We're not storing any sensitive GitHub data - we just need to know who's logged in so we can fetch their public profile data. The package handles all the OAuth flow, session management, and token refresh stuff for us.</p>
<p>We use session data to personalize the experience. Like when you're writing a letter to Santa, we can pre-fill it with your GitHub info if you're logged in, or show the friend mode UI if you're not.</p>
<p>The whole auth flow is super smooth:</p>
<ol>
<li>Click &quot;Sign in with GitHub&quot;</li>
<li>GitHub OAuth popup appears</li>
<li>Authorize the app</li>
<li>Get redirected back with your session</li>
<li>Start roasting (or getting roasted by) Santa!</li>
</ol>
<p><strong>Why Not Use Directus Auth?</strong></p>
<p>While Directus has a really robust authentication SSO providers (including GitHub), we deliberately went with <code>nuxt-auth-utils</code> for this project. Here's why… we didn't actually need &quot;real&quot; user accounts or any of the powerful permission features that Directus provides. We just needed a quick way to say &quot;hey, this person is logged in with GitHub&quot; so we could fetch their public profile data.</p>
<p>Plus, keeping the auth lightweight meant one less thing to configure in our Directus instance, which we're primarily using it as the store for all the letters and likes. Sometimes simpler is better.</p>
<h3>The AI Magic – Anthropic + Vercel AI SDK</h3>
<p>For the AI part, we're using Anthropic's Claude 3.5 Sonnet. We actually tested this against a few different LLMs, and Claude just had this perfect balance of snark and humor that really nailed the Santa voice we were going for.</p>
<p>We're using the <a href="https://sdk.vercel.ai/">Vercel AI SDK</a> to handle our Anthropic API calls, specifically their <code>generateObject</code> function which is super handy. Here's an example of how that might look below.</p>
<pre><code class="language-typescript">import { z } from 'zod';
import { generateObject } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';

// Schema for the AI Payload to return proper JSON
export const aiPayloadSchema = z.object({
    letter: z.string().min(1), // The letter in Markdown format
    list: z.enum(['naughty', 'nice']), // The list the user belongs to
    flagged: z.boolean().optional(), // Was the letter flagged as inappropriate?
    flagged_reason: z.string().optional(), // Reason for why it was flagged
});

// Create the Anthropic client
const anthropic = createAnthropic({
    apiKey: config.anthropicApiKey as string,
});

// Generate the letter
const aiResponse = await generateObject({
    model: anthropic('claude-3-5-sonnet-20240620'),
    schema: aiPayloadSchema,
    maxTokens: 8192,
    messages: [{ role: 'user', content: prompt }],
});
</code></pre>
<p>The <code>generateObject</code> method from the Vercel AI forces Claude to return data in exactly the structure we want. We define our schema with Zod, and the SDK makes sure the AI response matches that structure. No more parsing weird JSON strings or weird edge cases dealing with malformed responses.</p>
<h3>Frontend Hosting – Vercel</h3>
<p>The frontend is hosted with the big triangle company - Vercel. In my own testing across various Nuxt projects over the last year or two, I’ve found that deploying Nuxt 3 on Vercel usually “just works” more often than other providers. Other providers usually take me a little more time to debug and troubleshoot builds. Cost is definitely a concern though - especially if this thing gets really popular.</p>
<h3>The Fun Extras</h3>
<p>There’s some other cool additional libraries that add those special touches.</p>
<ul>
<li><a href="https://particles.js.org/"><strong>@tsparticles</strong></a> – powers the falling snow effect animation.</li>
<li><a href="https://github.com/vueuse/sound"><strong>@vueuse/sound</strong></a> – makes it super easy to add sound effects to any Vue.js app.</li>
<li><a href="https://github.com/micromark/micromark"><strong>micromark</strong></a> – to render markdown coming back from the LLM.</li>
</ul>
<p><em>Next up, let’s deep dive into the actual features.</em></p>
<h2>The Feature List 🎅</h2>
<h3>Naughty or Nice scoring algorithm ✅</h3>
<p>The original version relied on the AI Santa to decide whether a developer made it onto the naughty or nice list. The roasts were hilarious but almost everyone was on the naughty list. Which didn’t feel “fair” to those who contribute a lot, so we had to figure out a way to fix it.</p>
<p>After eliciting feedback from the team and our AI / LLM friends, we came back with this (way over-engineered 😅) algorithm.</p>
<p><strong>Base Points</strong></p>
<table>
<thead>
<tr>
<th><strong>Data Point</strong></th>
<th><strong>Score</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Issues</td>
<td>0.5 pts per issue</td>
</tr>
<tr>
<td>Commits</td>
<td>1 pts per commit</td>
</tr>
<tr>
<td>Pull Requests</td>
<td>2 pts per PR</td>
</tr>
<tr>
<td>Code Reviews</td>
<td>3 pts per review</td>
</tr>
<tr>
<td>Followers</td>
<td>2 pts per follower</td>
</tr>
<tr>
<td>Stars</td>
<td>2 pts per star on owned repos</td>
</tr>
<tr>
<td>Sponsorships</td>
<td>25 points per GitHub sponsorship (where you are the sponsor)</td>
</tr>
</tbody>
</table>
<p><strong>Modifiers</strong></p>
<ul>
<li>Abandoned Forks: -2 points for each forked repository not updated in 6+ months</li>
<li>Popular Projects: 20% bonus (1.2×) for having any project with &gt;500 stars</li>
<li>Organization Membership: 10% bonus (1.1×) for being part of GitHub organizations</li>
</ul>
<p><strong>Final Score</strong></p>
<ul>
<li>Users scoring 500+ points are classified as &quot;nice&quot;</li>
<li>Users scoring below 500 points are classified as &quot;naughty&quot;</li>
</ul>
<p><strong>Fetching user data with the GitHub GraphQL API</strong></p>
<p>We needed to grab a lot of different data points to calculate that naughty/nice score - commits, PRs, reviews, issues, followers, organizations, and more. With the GitHub REST API, we’d be making 6-10 separate API calls for each profile to get all the data we needed to properly score a profile..</p>
<p>With GraphQL, we can get it all in one shot.</p>
<pre><code class="language-graphql">query getUserProfile($username: String!) {
  user(login: $username) {
    login
    name
    location
    twitterUsername
    url
    avatarUrl
    websiteUrl
    company
    bio
    readme: repository(name: $username) {
      object(expression: &quot;HEAD:README.md&quot;) {
        ... on Blob {
          text
        }
      }
    }
    starredRepositories {
      totalCount
    }
    followers {
      totalCount
    }
    following {
      totalCount
    }
    organizations(first: 3, orderBy: {field: CREATED_AT, direction: DESC}) {
      nodes {
        name
        description
        url
        avatarUrl
		 membersWithRole(first: 10){
          nodes{
            name
            login
            avatarUrl
          }
        }
      }
    }
    repositories(visibility: PUBLIC, first: 10, ownerAffiliations: OWNER, orderBy: {field: PUSHED_AT, direction: DESC}) {
      totalCount
      nodes {
        forkCount
        isFork
        name
        description
        descriptionHTML
        url
        createdAt
        stargazerCount
        issues(states: OPEN) {
          totalCount
        }
        readme: object(expression: &quot;HEAD:README.md&quot;) {
          ... on Blob {
            text
          }
        }
        pushedAt
        commitComments {
          totalCount
        }
      }
    }
    contributionsCollection(
      from: &quot;2024-01-01T00:00:00Z&quot;
      to: &quot;2024-12-31T23:59:59Z&quot;
    ) {
      totalRepositoryContributions
      totalRepositoriesWithContributedIssues
      totalRepositoriesWithContributedCommits
      totalCommitContributions
      totalIssueContributions
      totalPullRequestContributions
      totalPullRequestReviewContributions
    }
    sponsorshipsAsSponsor(activeOnly: true, first: 100) {
      totalCount
    }
  }
}
</code></pre>
<p>The data returned then gets fed into our custom scoring algorithm. Next, the resulting score and profile gets passed to the LLM. And finally the generated letter, score, and metadata is stored in Directus to be retrieved on the frontend.</p>
<p>Here’s what our complete <code>roast</code> Nuxt server endpoint looks like.</p>
<pre><code class="language-tsx">// server/api/roast.post.ts

import { z } from 'zod';
import { generateObject } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';

import userQuery from '~~/server/graphql/getUserProfile';
import orgQuery from '~~/server/graphql/getOrgProfile';

import type { GitHubUserData } from '~~/server/graphql/getUserProfile';
import type { GitHubOrgData } from '~~/server/graphql/getOrgProfile';
import type { RoastResponse } from '#shared/types/endpoints.js';
import type { H3Error } from 'h3';

// Schema for the AI Payload to return proper JSON
export const aiPayloadSchema = z.object({
	letter: z.string().min(1), // The letter in Markdown format
	list: z.enum(['naughty', 'nice']), // The list the user belongs to
	flagged: z.boolean().optional(), // Was the letter flagged as inappropriate?
	flagged_reason: z.string().optional(), // Reason for why it was flagged
});

// Schema for the roast endpoint body
export const profileSchema = z.object({
	username: z.string().min(1),
	wishlist: z.string().optional(),
	type: z.enum(['user', 'organization']).optional().default('user'),
	mode: z.enum(['self', 'friend']).optional().default('self'),
	roasted_by: z.string().optional(),
	profileType: z.enum(['User', 'Organization']),
});

// Create the Anthropic client
const config = useRuntimeConfig();
const anthropic = createAnthropic({
	apiKey: config.anthropicApiKey as string,
});

export default defineEventHandler(async (event): Promise&lt;RoastResponse | H3Error&gt; =&gt; {
	const body = await readValidatedBody(event, (body) =&gt; profileSchema.parse(body));
	const { username, wishlist, mode, roasted_by, profileType } = body;

	// Check to see if the profile already exists in Directus if so, redirect to the profile
	const [directusResponse] = await directusServer.request(
		readItems('profiles', { filter: { username: { _eq: username } }, limit: 1 }),
	);

	if (directusResponse) {
		return {
			redirect: `/${username}`,
		};
	}

	// Check to see if the user is logged in to GitHub if not, don't allow them to submit a letter to save on costs
	const session = await requireUserSession(event);

	if (!session) {
		throw createError({
			statusCode: 401,
			message: 'Unauthorized. Please login to submit a letter to Santa.',
		});
	}

	try {
		const variables = { username };

		const response = await $fetch('https://api.github.com/graphql', {
			method: 'POST',
			headers: {
				Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
				'Content-Type': 'application/json',
			},
			body: {
				query: profileType === 'User' ? userQuery : orgQuery,
				variables,
			},
		});

		const typedResponse = response as { data: { user?: GitHubUserData; organization?: GitHubOrgData } };

		const profileData =
			profileType === 'User'
				? (typedResponse.data.user as GitHubUserData)
				: (typedResponse.data.organization as GitHubOrgData);

		// Score the contributions based on the profile type
		const score = calculateNiceScore(profileData, profileType);

		const prompt = `
			You are the open source Santa Claus. You determine who's open source contributions are naughty or nice.
			Analyze the following Github ${profileType === 'User' ? 'user' : 'organization'}'s profile carefully and in detail.
			We've determined the ${profileType}'s score based on their contributions. Whether they're on the nice list
			or the naughty list, roast them accordingly. Write a short, funny letter in a snarky sarcastic tone.
			Include a couple lines from the wish list in the letter if it's provided.
			If the mode provided is &quot;friend&quot;, then make a short mention of the roasted_by user in one of the paragraphs.

			STRUCTURE:
			- Intro
			- 3 short paragraphs
			- PS

			RULES:
			- Do NOT include a signature and like 'Yours, From Santa' in the letter.
			- The letter should be in Markdown format.
			- If someone uses profanity or asks for something inappropriate, do not roast them. Set the flagged field to true and provide a reason.

			Wish List: ${wishlist} ${mode === 'friend' ? `Note: Wishlist provided by ${roasted_by}` : ''}
			Profile: ${JSON.stringify(profileData)}
			Score: ${score}
			Mode: ${mode}
			Roasted By: ${roasted_by}
		`;

		const aiResponse = await generateObject({
			model: anthropic('claude-3-5-sonnet-20240620'),
			schema: aiPayloadSchema,
			maxTokens: 8192,
			messages: [{ role: 'user', content: prompt }],
		});

		// If the user has organizations and membersWithRoles exist, loop through the organizations and add the members to the metadata as possible_roasts
		const possibleRoasts: any[] = [];
		if (profileType === 'User' &amp;&amp; (profileData as GitHubUserData).organizations?.nodes) {
			for (const org of (profileData as GitHubUserData).organizations.nodes ?? []) {
				if (org?.membersWithRole?.nodes) {
					possibleRoasts.push(...org.membersWithRole.nodes);
				}
			}
		}

		// Generate metadata to store with the profile
		const metadata = {
			ai_usage: aiResponse.usage,
			ai_response: aiResponse.object,
			score: score,
			possible_roasts: possibleRoasts,
		};

		// Store the profile in Directus
		const directusResponse = await directusServer.request(
			createItem('profiles', {
				username,
				letter: aiResponse.object.letter,
				list: score.list,
				wishlist,
				mode,
				score: score.finalScore,
				roasted_by,
				metadata,
				type: profileType,
			}),
		);

		return {
			redirect: `/${username}`,
			letter: directusResponse.letter,
			list: directusResponse.list,
			metadata: directusResponse.metadata,
			roasted_by: directusResponse.roasted_by,
			score: directusResponse.score,
			type: directusResponse.type,
			mode: directusResponse.mode,
			username: directusResponse.username,
			wishlist: directusResponse.wishlist,
		};
	} catch (error) {
		console.error(JSON.stringify(error));
		throw createError({
			statusCode: 500,
			message: 'Failed to roast profile',
		});
	}
});

</code></pre>
<h3>Spicyness Meter ✅</h3>
<p>We wanted a way for users to engage with letters other than their own and cast a vote on the spiciest letters, so we created the spicemeter. You can increase your opinion of the spiciness by left-clicking or decrease by right-clicking. <a href="https://www.youtube.com/watch?v=4xgx4k83zzc">These go to eleven.</a></p>
<p><img src="https://marketing.directus.app/assets/51fe557d-469d-41f7-b859-a76e533d3bf6" alt="" /></p>
<p>The inspiration for this was taken from <a href="https://www.joshwcomeau.com/">Josh Comeau</a> and his awesome blog. Instead of a simple like button, there’s an interactive heart button that you can mash up to 16 times.</p>
<p><img src="https://marketing.directus.app/assets/231a3e72-9e80-4098-b01c-30ae77df1292" alt="" /></p>
<p>It seems like such a simple interaction until you factor in that each person should only get X number of likes on any given post. Here’s how it works.</p>
<ul>
<li>We get the IP from the request <code>x-forwarded-for</code> header</li>
<li>We keep that secure by creating a hash of the IP.</li>
<li>We store the hash and the count in our <code>likes</code> Directus collection to track individual user interactions</li>
</ul>
<pre><code class="language-tsx">// server/api/profiles/[username]/likes.post.ts
import type { LikesResponse } from '~~/shared/types/endpoints.js';

export default defineEventHandler(async (event): Promise&lt;LikesResponse&gt; =&gt; {
	const username = getRouterParam(event, 'username');

	if (!username) throw createError({ statusCode: 400, message: 'Missing username. username is required.' });

	const ip =
		(event.node.req.headers['x-forwarded-for'] as string) ||
		(event.node.req.headers['x-vercel-forwarded-for'] as string);

	const visitorHash = createVisitorHash(ip, process.env.SALT as string);

	try {
		// Get existing profile with all likes
		const existingProfile = await directusServer.request(
			readItem('profiles', username, {
				fields: [
					'username',
					{
						likes: ['id', 'visitor_hash', 'profile', 'count'],
					},
				],
			}),
		);

		if (!existingProfile) {
			throw createError({ statusCode: 404, message: 'Profile not found.' });
		}

		// Get user's specific like record
		const userLike = existingProfile.likes?.find(like =&gt; like.visitor_hash === visitorHash);

		const body = await readBody(event);
		const newCount = Math.min(Math.max(body.count || 0, 0), 11);

		let like;

		if (userLike) {
			// Update existing like record
			like = await directusServer.request(
				updateItem('likes', userLike.id, {
					profile: existingProfile.username,
					count: newCount,
				}),
			);
		} else {
			// Create new like record
			like = await directusServer.request(
					createItem('likes', {
						profile: existingProfile.username,
						visitor_hash: visitorHash,
						count: newCount,
					}),
			);
		}

		// Calculate total likes by summing all likes
		const totalLikes = existingProfile.likes?.reduce((sum, like) =&gt; {
			// If this is the user's like, use the new count
			if (like.visitor_hash === visitorHash) {
				return sum + newCount;
			}
			return sum + (like.count || 0);
		}, 0);

		const response: LikesResponse = {
			username: existingProfile.username,
			totalLikes: totalLikes || 0,
			userLikeCount: newCount,
		};

		return response;
	} catch (error) {
		console.error('Error updating like count:', error);
		throw createError({
			statusCode: 500,
			message: 'Failed to update like count.',
		});
	}
});

</code></pre>
<h3>The Nice List ✅</h3>
<p>After you figure out if you’re on the naughty or nice list, you might want to see if your friends made the list next. So we build a page to show each list side by side, sorted by the number of likes or “spice level”.</p>
<p><img src="https://marketing.directus.app/assets/1a0976b6-ca0a-4d94-978d-33a81af7a129" alt="" /></p>
<p>We use <a href="https://nitro.build/guide/cache#cached-event-handlers">cached event handlers</a> from Nuxt / Nitro here to cache the data for a short period of time to prevent hammering the Directus API if there’s a lot of users on the page.</p>
<p>After hearing feedback from teammates about privacy concerns, we added a simple switch that lets users opt out of appearing on the public list.</p>
<p><img src="https://marketing.directus.app/assets/9194489a-4c7e-4bcf-b273-2b70168091e9" alt="" /></p>
<h3>Dynamic OG Images ✅</h3>
<p>Fun social sharing / OG images seem to have become a thing in my projects. And if I’m building a Nuxt project - I always reach for the <a href="https://github.com/nuxt-modules/og-image"><code>nuxt-og-image</code> module</a> by rockstar Harlan Wilton.</p>
<p><img src="https://marketing.directus.app/assets/99279797-93ba-4ee7-96ff-731f36ef25f4" alt="" /></p>
<p>It’s as simple as creating a separate Vue component for image design and then calling <code>defineOgImageComponent</code> in your Nuxt page.</p>
<pre><code class="language-tsx">// app/components/OgImage/Username.vue
&lt;script setup lang=&quot;ts&quot;&gt;
const props = withDefaults(
	defineProps&lt;{
		username?: string;
		avatarUrl?: string;
	}&gt;(),
	{
		username: 'random_hacker_323',
	},
);
&lt;/script&gt;

&lt;template&gt;
	&lt;div class=&quot;w-full h-full flex flex-col bg-red-900 p-12&quot;&gt;
		&lt;!-- do template-y stuff here --&gt;
		&lt;/div&gt;
&lt;/template&gt;
</code></pre>
<pre><code class="language-tsx">// app/pages/[username].vue
&lt;script setup lang=&quot;ts&quot;&gt;
const username = computed(() =&gt; route.params.username);
const avatarUrl = computed(() =&gt; `https://github.com/${username.value}.png`);

defineOgImageComponent('Username', {
	username: username.value,
	avatarUrl: avatarUrl.value,
});
&lt;/script&gt;
</code></pre>
<p>There can still be a few gotchas depending on the rendering method and the host you’re using. I almost always end up add the <code>sharp</code> module as a dependency. This site is using SSR and hosted on Vercel and 🤞 we haven’t had any major issues yet.</p>
<h3>Santa Reads Aloud ❌</h3>
<p>Some features just don’t make the final cut. This one got axed not because it didn’t work or wasn’t awesome - but for cost purposes.</p>
<p>Mr <a href="https://directus.io/team/pedro-pizarro">Pedro Pizzaro</a> – one of our AEs is freaking awesome at voiceover. And he recorded a custom salty sample voice that we used to create a custom voice at <a href="https://elevenlabs.io">ElevenLabs</a>.</p>
<p>Once you sent your letter to Santa, we’d send the generated text to their API to generate speech and then play it back to you on your profile page. But the amount of credits we’d burn through made it too expensive to include.</p>
<p>But fear not - here’s a sample of what could have been.</p>
&lt;audio controls&gt;
  &lt;source src=&quot;https://marketing.directus.app/assets/57aa9699-7d15-42cb-92f2-75bbb57088ea&quot; type=&quot;audio/mpeg&quot;&gt;
  Your browser does not support the audio element.
&lt;/audio&gt;
<h2>Salty Santa FAQs ❓</h2>
<h3><strong>How long did it take to build?</strong></h3>
<p>From idea to launch has been about <strong>3 weeks</strong> of time. That’s not really the total build time just the elapsed time since work started onit.</p>
<p>I’d estimate I’ve spent a solid 30 hours of time “in-the-seat” actually building, testing and improving this thing. The majority of that in the frontend interaction, prompt engineering, and the scoring algorithm.</p>
<h3>What does it cost to run?</h3>
<p>Because we’re fetching a lot of data like repositories and their readmes, the input token count in quite high. The average input token count is around ~9,977 tokens. This varies a lot based on the users repos and readme content.</p>
<p>Output is a totally different story – averaging around ~360 tokens since we’re just outputting the letter (mostly).</p>
<p>That brings the <strong>cost to ~$0.035 per profile roasted</strong>. Or put a different way - every 1000 roasts would cost us about $35.</p>
<p>We may tweak our data fetching and adjust our prompts to attempt to lower this if it becomes really popular.</p>
<h2>Santa’s Summary</h2>
<p>This thing was a blast to build and I hope this a super-helpful write up for your own projects. Be sure to check out the live project at <a href="https://salty-santa.vercel.app">https://salty-santa.vercel.app</a>.</p>
<p>Let us know your feedback. And shoot us your ideas for the next fun build.</p>]]></content>
        <author>
            <name>Bryant Gillespie</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Changelog #4]]></title>
        <id>https://docs.directus.io/blog/the-changelog-4</id>
        <link href="https://docs.directus.io/blog/the-changelog-4"/>
        <updated>2024-11-19T14:41:13.000Z</updated>
        <summary type="html"><![CDATA[A list of this month’s Directus product updates.]]></summary>
        <content type="html"><![CDATA[<ul>
<li>In Directus <a href="https://github.com/directus/directus/releases">11.1.2</a>, improvements to content versioning and new comment endpoints have been made.
<ul>
<li>For improvements to content versioning, internally, we stored every change to a content version separately in the <code>directus_revisions</code> collection, and then merged them together when promoting a version. In this release, we’ve added a new <code>delta</code> field to the <code>directus_versions</code> collection that combines all revisions into a single field. This means you can prune <code>directus_revisions</code> without compromising your content versions.</li>
<li>We've introduced a dedicated <code>directus_comments</code> collection, replacing the previous system that used <code>directus_activity</code> for comments. While new comment endpoints have been added, existing endpoints remain functional. Comment primary keys are now UUIDs instead of numeric values, which may impact custom type checking implementations. The SDK's internal comment endpoints have been updated to reflect this change. To avoid errors, ensure your Directus version is compatible with the latest SDK when using comment functions.</li>
</ul>
</li>
<li>In Directus <a href="https://github.com/directus/directus/releases">11.2</a>, TUS (resumable uploads) now added to Supabase, Azure, Cloudinary, and GCS storage adapters to join AWS and Local Adapters released in 10.13.0.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/ai-web-scraper-operation">AI Web Scraper</a> allows you to scrape web pages and receive structured data back using Firecrawl's web scraping API to extract data from websites.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/ai-writer-operation">AI Writer</a> has been extended to include the option to use multiple AI providers as well as different models.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/treemap-chart-panel">Tree Map Chart</a> presents a cluster or boxes where the size of each box represent the value. You can also group data into categories which are presented in different colors.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/funnel-chart-panel">Funnel Chart</a> presents a list of numbers in an ascending or descending funnel chart.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/scatter-plot-panel">Scatter Plot Chart</a> is a 2-axis chart where values are plotted as dots. You can optionally add axis labels and hover over any of the dots to see the values.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/treemap-chart-panel">Timeline Chart</a> presents a series of tasks or events with a start and end date on a graph. You can also group data into categories on the y axis and seperate tasks into different colors.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/boilerplates/tabular-layout">Customizable Tabular Layout Boilerplate</a> give extension authors the ability to use it as a base for their customizations.</li>
</ul>]]></content>
        <author>
            <name>Beth Loft</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Changelog #3]]></title>
        <id>https://docs.directus.io/blog/the-changelog-3</id>
        <link href="https://docs.directus.io/blog/the-changelog-3"/>
        <updated>2024-10-15T13:45:00.000Z</updated>
        <summary type="html"><![CDATA[A list of this month’s Directus product updates.]]></summary>
        <content type="html"><![CDATA[<ul>
<li>In <a href="https://github.com/directus/directus/releases">Directus 11.1.1</a>, a number of bug fixes and optimizations were included. We’ve also removed the dedicated SendGrid email transport and you should replace it with SMTP.</li>
<li><a href="https://directus.cloud/">Directus Cloud templates</a> for website CMS, CRM and eCommerce projects are available to use within Directus Cloud. The templates enable you to have the data models, permissions and flows pre-configured to get started quicker. You can select a template when creating a new project on Directus Cloud.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/gantt-chart-layout">Gantt Chart Layout</a> displays items in a collection in a gantt chart, helping those of you who use Directus for project management and task management. You can specify a label that you want displayed on each task, a start date and an end date and optionally, a dependency field which will draw dependency lines in a chart, and also specify the zoom in as granular as an hour and as broad as a year.</li>
<li><a href="https://github.com/directus-labs/extensions/tree/main/packages/calculated-fields-bundle">Calculated field interface</a> allows you to write a formula and the value of the interface will be automatically computed and shown. It supports the full set of functions provided by Formula.js, and a majority of JavaScript operators that work for numbers and strings. It also supports relational fields and we parse formulas to ensure they are only running allowed functions which is important for security. Important to note, values here are only visible in the interface and not in API responses.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/api-metric-panel">API metric panel</a> can be used to display a value from an external API. For example, the number of docker downloads or sales or followers on social media platforms. You can make a web request to get your preferred metrics, you can also provide custom headers or a request body if required, then specify the path of the value you want to display.</li>
</ul>]]></content>
        <author>
            <name>Beth Loft</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Changelog #2]]></title>
        <id>https://docs.directus.io/blog/the-changelog-2</id>
        <link href="https://docs.directus.io/blog/the-changelog-2"/>
        <updated>2024-09-10T15:09:00.000Z</updated>
        <summary type="html"><![CDATA[A list of this month’s Directus product updates.]]></summary>
        <content type="html"><![CDATA[<ul>
<li>In Directus 11.1, you can now <a href="/user-guide/settings/system-logs">stream system logs</a> inside of the Directus Data Studio to have greater visibility and debug problems. You can filter by log level or node in a multi-node deployment.</li>
<li>We added support for listening on a Unix socket path as opposed to host and port.</li>
<li>You can now customize the invite link expiry time.</li>
<li>Indices are now supported through the new <code>is_indexed</code> field, which means you don’t have to do this directly in your database anymore.</li>
<li>Using the <a href="https://github.com/directus-labs/extensions/tree/main/packages/liquidjs-operation">LiquidJS template operation</a> enables dynamically-generated content creation, perfect for creating personalized emails or any scenario where you need to combine data with templates within a flow.</li>
<li><a href="https://github.com/directus-labs/extensions/tree/main/packages/resend-operation">Resend email operation</a> integrates Resend's powerful email API into your Directus flows.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/table-view-panel">Table in Insight Panel</a> facilitates output data from across multiple tables, if you click an item, it'll open a Directus Editor draw.</li>
<li><a href="https://github.com/directus-labs/extensions/tree/main/packages/plausible-analytics-bundle">Plausible Analytics Bundle</a> embeds your Plausible dashboard right within your Directus Insights dashboard or your content editor.</li>
<li>The <a href="https://github.com/directus-labs/extensions/tree/main/packages/flow-trigger-bundle">Flow Trigger Bundle</a> allows you to run manually-triggered flows from right within a dashboard or within your content editor.</li>
<li><a href="https://github.com/directus-labs/extensions/tree/main/packages/simple-list-interface">List interface</a> enables the easy creation and management of simple lists with full keyboard support.</li>
<li>A <a href="https://github.com/directus-labs/extensions/tree/main/packages/command-palette-module">global command palette</a> extension is now available - giving you CMD/Ctrl+K across Directus. Navigate the Data Studio, run flows, and copy API endpoints from anywhere.</li>
<li><a href="https://github.com/directus-labs/extensions/tree/main/boilerplates/input-rich-text-html">WYSIWYG</a> &amp; <a href="https://github.com/directus-labs/extensions/tree/main/boilerplates/input-block-editor">Block Editor</a> Boilerplates give extension authors the ability to use them as a base for their customizations.</li>
</ul>]]></content>
        <author>
            <name>Beth Loft</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Understanding Policy-Based Access Control]]></title>
        <id>https://docs.directus.io/blog/understanding-policy-based-access-control</id>
        <link href="https://docs.directus.io/blog/understanding-policy-based-access-control"/>
        <updated>2024-08-16T10:00:00.000Z</updated>
        <summary type="html"><![CDATA[Understand Directus 11's changes to access control by shifting from role-based to policy-based management, offering more flexibility and modular permissions for users and roles.]]></summary>
        <content type="html"><![CDATA[<p>In version 11 introduced a change to how Directus handles access control through the introduction of policies.</p>
<p>Previously, permissions were attached directly to roles, and a user could receive those permissions by assigning their role. Now, permissions are attached to reusable policies that can be added to users or roles, as well as other subtle but impactful changes.</p>
<h2>Introducing Policies</h2>
<p><img src="https://marketing.directus.app/assets/1d98f3e2-a0e1-4e63-b8ca-9000b88cbfec" alt="Can Issue Article Updates Policy showing read access on the authors collection and update and delete permissions on the posts collection, as well as a number of permissions in the system collections required for app access." /></p>
<p>Each policy can contain one or more permissions, which in turn is made up of a collection, an action, and either allow all or create custom rules. While the creation of permissions remains unchanged, we have also updated the UI so only relevant collections are shown.</p>
<p>As before, custom permissions can include a filter to only allow access to certain items or fields.</p>
<h2>Assigning Policies &amp; Roles</h2>
<p>Policies can be added directly to roles, meaning you can continue to use the permission as you have done prior to Directus 11 by creating a single policy per role.</p>
<p>However, roles can have multiple policies attached to them, and policies can be attached to multiple roles. This flexibility means that you can create smaller and more modular sets of permissions and reuse them at any time.</p>
<p>You can also add policies directly to users, which can be useful for users in your project who have edge cases, or already have an assigned role.</p>
<h3>Multiple Roles</h3>
<p>While you can still only assign a single role to a user, roles can now contain other roles. All policies applied to any of the roles will be used when determining access control.</p>
<p>A new <code>$CURRENT_ROLES</code> dynamic variable is now available throughout Directus and will return an array of all roles at all depths. <code>$CURRENT_ROLE</code> will contain only the directly-added role as before.</p>
<h3>Additive Permissions</h3>
<p>By default, a user has no access to any collection. All permissions in all policies are additive and are combined to determine what a user has access to. There are no “negative” permissions that remove access.</p>
<h2>Composing Access Control</h2>
<p>Having this new level of flexibility in policies helps in two ways - to remove repetitive permission configuration for similar roles, and to create sets of permissions that have a clear purpose when applied together.</p>
<h3>An Example</h3>
<p>In this store, there is a <code>staff</code>,  <code>shift manager</code> , and <code>store manager</code> role. There are <code>inventory</code>, <code>shifts</code> , and <code>sales</code> collections.</p>
<p>A <code>see inventory</code> policy could contain a single permission allowing read access to the <code>inventory</code> collection, and be assigned to the <code>staff</code> role.</p>
<p>An <code>accept inventory</code> policy could allow full access to all actions in the <code>inventory</code> collection, and be added to both the <code>shift manager</code> , and <code>store manager</code> roles.</p>
<p>A <code>see shifts</code> policy could be added to the staff role that could allow <code>read</code> access to <code>shifts</code> and only allowing <code>read</code> access to the name field of <code>staff</code>, while <code>shift managers</code> could get a more permissive policy allowing for the creation and editing of shifts.</p>
<p>Staff should be able to <code>create</code> <code>sales</code>, but only the <code>store manager</code> should be able to <code>read</code>, <code>edit</code>, or <code>delete</code> sales. Once again, two policies with the varying permission levels.</p>
<p>Finally, the <code>shift manager</code> role could contain the <code>staff</code> role, so they get all staff-level policies.</p>
<h2>API Changes</h2>
<p>These changes also introduce API changes - the ones you might expect are the introduction of a Policies API, and changes to the fields in the Roles and Users APIs. And, of course, permissions are no longer attached to a <code>role</code> but instead to a <code>policy</code>.</p>
<p>If there is ever conflicting permissions around which fields or items are available based on a filter, the field will be returned with a value of <code>null</code>. This means that <code>null</code> must now be treated as both a value and as an indication of lacking access.</p>
<h2>Migrating from Role-Based Access Control</h2>
<p>When upgrading to Directus 11, our automatic migrations will handle this upgrade for you by turning each role’s permissions into a single policy and attach the policy to the role on your behalf.</p>
<p>You are then free to take advantage of the new policy-based access control as you are ready to - taking common permissions across roles into their own policies is a fantastic first step.</p>
<p>The way we have implemented the migration is intended to make this a smooth transition, allowing you to take advantage of policies at your own pace. But we hope that you find this addition a powerful change.</p>]]></content>
        <author>
            <name>Kevin Lewis</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Changelog #1]]></title>
        <id>https://docs.directus.io/blog/the-changelog-1</id>
        <link href="https://docs.directus.io/blog/the-changelog-1"/>
        <updated>2024-08-13T14:57:00.000Z</updated>
        <summary type="html"><![CDATA[A list of this month’s Directus product updates.]]></summary>
        <content type="html"><![CDATA[<ul>
<li><a href="https://github.com/directus/directus/releases">Directus v11.0.0 release</a> contains a new permissions system that's based on access policies, nested roles, and a switch to mysql2. <a href="/blog/understanding-policy-based-access-control">Learn more about policies.</a></li>
<li><a href="https://directus.io/blog/a-change-in-our-pricing-july-2024">Directus Cloud pricing changes</a> including the introduction of a new starter tier at $15/month.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/audio-player-interface/README.md">Audio player interface</a> extension allows an audio source to be selected and displays an audio player from an URL or a local file from Directus.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/video-player-interface/README.md">Video player interface</a> extension allows a video from YouTube, Vimeo or a local file from Directus to be selected and a video player to be displayed.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/pdf-viewer-interface/readme.md">PDF viewer interface</a> extension enables a view of PDF files from within the item editor.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/spreadsheet-layout/README.md">Spreadsheet layout</a> extension allows the editing of item fields directly inline, similar to a spreadsheet.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/rss-to-json-operation/README.md">RSS to JSON operation</a> extension returns an RSS Feed as a JSON object inside of flows as a custom operation.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/multilevel-autocomplete-api-interface/readme.md">Multilevel autocomplete interface</a> extension allows you to get data from nested API queries.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/whiteboard-interface/readme.md">Whiteboard interface</a> extension adds a field to your collection for drawing sketches and ideas.</li>
<li><a href="https://github.com/directus-labs/extensions/blob/main/packages/experimental-m2a-interface/readme.md">Experimental M2A presentation interface</a> extension enables the adding of a matrix button selector to the built-in M2A interface.</li>
</ul>]]></content>
        <author>
            <name>Beth Loft</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Integrating Elasticsearch Indexing with Directus]]></title>
        <id>https://docs.directus.io/blog/integrating-elasticsearch-indexing-with-directus</id>
        <link href="https://docs.directus.io/blog/integrating-elasticsearch-indexing-with-directus"/>
        <updated>2024-08-13T08:39:49.000Z</updated>
        <summary type="html"><![CDATA[Learn how to maintain an Elasticsearch index from data in your Directus project by building a custom hook extension.]]></summary>
        <content type="html"><![CDATA[<p>In this article, we will explore how to index data from Directus in Elasticsearch through a custom hook extension, enabling you to track created, updated, and deleted data to maintain an up-to-date index which you can then use in your external applications.</p>
<h2>Setting Up Directus</h2>
<p>You will need to have a <a href="https://docs.directus.io/self-hosted/quickstart">local Directus project running</a> to develop extensions.</p>
<p>In your new project, create a collection called <code>books</code> with a <code>title</code> and a <code>description</code> field.</p>
<h2>Initializing Your Extension</h2>
<p>In your <code>docker-compose.yml</code> file, set an <code>EXTENSIONS_AUTO_RELOAD</code> environment variable to <code>true</code> so that Directus will automatically watch and reload extensions as you save your code. Restart your project once your new environment variable is added.</p>
<p>In your terminal, navigate to your <code>extensions</code> directory and run <code>npx create-directus-extension@latest</code>. Name your extension <code>elasticsearch-indexing</code> and choose a <code>hook</code> type and create the extension with <code>JavaScript</code>. Allow Directus to automatically install dependencies and wait for them to install.</p>
<h2>Seting Up Elasticsearch</h2>
<p>To integrate Directus and Elasticsearch, you will need a running instance of both. For this tutorial, <a href="https://www.elastic.co/cloud/elasticsearch-service/signup">Elastic Cloud</a> will be used. You will need both the Cloud ID and an API Key, which you can generate from your deployment dashboard.</p>
<p>In your <code>docker-compose.yml</code> file, create an <code>ELASTIC_API_KEY</code> and <code>ELASTIC_CLOUD_ID</code> environment variable and set them to the value from your Elasticsearch dashboard. Restart your project as you have changed your environment variables.</p>
<p>Navigate into your new extension directory, run <code>npm install @elastic/elasticsearch</code>, and then <code>npm run dev</code> to start the automatic extension building.</p>
<p>At the top of your extension's <code>src/index.js</code> file, initialize the Elasticsearch client:</p>
<pre><code class="language-javascript">import { createRequire } from &quot;module&quot;;
const require = createRequire(import.meta.url);
const { Client } = require(&quot;@elastic/elasticsearch&quot;);

export default ({ action }, { env }) =&gt; {
  const client = new Client({
    cloud: { id: env.ELASTIC_CLOUD_ID },
    auth: { apiKey: env.ELASTIC_API_KEY },
  });
};
</code></pre>
<p>Because Elasticsearch is a CommonJS package, the <code>require()</code> function is constructed using the <code>createRequire()</code> Node utility method and used to import it to avoid errors.</p>
<h2>Saving Items to Index</h2>
<p>Add the following lines of code after the <code>client</code> variable:</p>
<pre><code class="language-javascript">action(&quot;books.items.create&quot;, async (meta) =&gt; {
  await client.index({
    index: &quot;books&quot;,
    id: meta.key,
    document: meta.payload,
  });
});
</code></pre>
<p>This <code>action</code> hook will be triggered when an item is created in <code>books</code> collection. This is achieved by specifying <code>books.items.create</code> as the event name.
When executed a document will be created in an Elasticsearch <code>books</code> index containing the newly created item fields which was accessed from the <code>meta</code> object. The <code>meta</code> object includes the ID of the newly created item in the <code>key</code> property and the item fields in the <code>payload</code> property.
Although the <code>books</code> index was not explicitly created, that will be done automatically if doesn’t exist and a new document is been created which is the default behavior.</p>
<h2>Updating Items in Index</h2>
<p>Add the following lines of code below the existing action:</p>
<pre><code class="language-javascript">action(&quot;books.items.update&quot;, async (meta) =&gt; {
  await Promise.all(
    meta.keys.map(
      async (key) =&gt;
        await client.update({
          index: &quot;books&quot;,
          id: key,
          doc: meta.payload,
        })
    )
  );
});
</code></pre>
<p>For an update event, the <code>meta</code> object will includes an array of <code>keys</code> along with the updated fields even when only a single item is updated. So to modify the corresponding document or documents in <code>books</code> index, the array of keys is iterated over to send multiple update requests.</p>
<h2>Deleting Items in Index</h2>
<p>For a delete event, the <code>meta</code> object includes an array of keys of the of the deleted items. Fields are not included. Add the following lines of code after the <code>books.items.update</code> action:</p>
<pre><code class="language-javascript">action(&quot;books.items.delete&quot;, async (meta) =&gt; {
  await Promise.all(
    meta.keys.map(
      async (key) =&gt;
        await client.delete({
          index: &quot;books&quot;,
          id: key,
        })
    )
  );
});
</code></pre>
<h2>Testing Extension</h2>
<p>When you create, update, or delete items in the <code>books</code> collection, the changes should reflect in your Elasticsearch <code>books</code> index.</p>
<h2>Summary</h2>
<p>By following this guide, you have learned how to set up extensions in Directus. You also saw how to test the extension by creating, updating, and deleting data in Directus, with changes being reflected in your Elasticsearch index. This setup ensures that our data remains synchronized across both platforms.</p>]]></content>
        <author>
            <name>Taminoturoko Briggs</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Integrating Meilisearch Indexing with Directus]]></title>
        <id>https://docs.directus.io/blog/integrating-meilisearch-indexing-with-directus</id>
        <link href="https://docs.directus.io/blog/integrating-meilisearch-indexing-with-directus"/>
        <updated>2024-08-12T09:26:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to maintain a Meilisearch index from data in your Directus project by building a custom hook extension.]]></summary>
        <content type="html"><![CDATA[<p>In this article, we will explore how to index data from Directus in Meilisearch by building a custom hook extension, enabling you to track created, updated, and deleted data to maintain an up-to-date index which you can then use in your external applications.</p>
<h2>Setting Up Directus</h2>
<p>You will need to have a <a href="https://docs.directus.io/self-hosted/quickstart">local Directus project running</a> to develop extensions.</p>
<p>In your new project, create a collection called <code>articles</code> with a <code>title</code>, <code>content</code>, and <code>author</code> field.</p>
<h2>Initializing Your Extension</h2>
<p>In your <code>docker-compose.yml</code> file, set an <code>EXTENSIONS_AUTO_RELOAD</code> environment variable to <code>true</code> so that Directus will automatically watch and reload extensions as you save your code. Restart your project once your new environment variable is added.</p>
<p>In your terminal, navigate to your <code>extensions</code> directory and run <code>npx create-directus-extension@latest</code>. Name your extension <code>melisearch-indexing</code> and choose a <code>hook</code> type and create the extension with <code>JavaScript</code>. Allow Directus to automatically install dependencies and wait for them to install.</p>
<h2>Setting Up Meilisearch</h2>
<p>Sign up for a Meilisearch account if you haven't already. Once you have your Meilisearch instance details, you will be able to copy your credentials in your dashboard.</p>
<p><img src="https://marketing.directus.app/assets/1e860d34-d5ea-4adb-9304-f3a8db5755c6" alt="Melisearch dashboard" /></p>
<p>Add the following environment variables to your project:</p>
<pre><code class="language-docker-compose">MEILISEARCH_HOST=your_meilisearch_host
MEILISEARCH_API_KEY=your_meilisearch_api_key
</code></pre>
<p>Navigate into your new extension directory, run <code>npm install meilisearch</code>, and then <code>npm run dev</code> to start the automatic extension building.</p>
<p>At the top of your extension's <code>src/index.js</code> file, initialize the Meilisearch client:</p>
<pre><code class="language-javascript">import { MeiliSearch } from 'meilisearch'

const client = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST,
  apiKey: process.env.MEILISEARCH_API_KEY
})
const index = client.index('directus_index')
</code></pre>
<h2>Saving New Items to Index</h2>
<p>Update your extension's exported function to process create events when a new <code>article</code> is added to the collection:</p>
<pre><code class="language-javascript">export default ({ action }) =&gt; {
  action('articles.items.create', async (meta) =&gt; {
    await index.addDocuments([{ id: meta.key, ...meta.payload }])
  })
}
</code></pre>
<p>The <code>articles.items.create</code> action hook triggers after item creation. The <code>meta</code> object contains the new item's <code>key</code> (ID) and other fields in its <code>payload</code> property. By setting the <code>objectID</code> to the Directus item <code>id</code>, we ensure accurate referencing and management in Meilisearch.</p>
<h3>Updating Items in Index</h3>
<p>Add another action hook to process updates when one or more articles are modified:</p>
<pre><code class="language-javascript">action('articles.items.update', async (meta) =&gt; {
  await Promise.all(
    meta.keys.map(async (key) =&gt; 
      await index.updateDocuments([{ id: key, ...meta.payload }])
    )
  )
})
</code></pre>
<p>The <code>articles.items.update</code> action hook triggers when articles are updated. It receives <code>meta.keys</code> (an array of updated item IDs) and <code>meta.payload</code> (changed values). The hook updates each document in Meilisearch.</p>
<h3>Deleting Items in Index</h3>
<p>Add an action hook to remove items from Meilisearch when they're deleted in Directus:</p>
<pre><code class="language-javascript">action('articles.items.delete', async (meta) =&gt; {
  await index.deleteDocuments(meta.keys)
})
</code></pre>
<p>The <code>articles.items.delete</code> action hook triggers when articles are deleted. It receives <code>meta.keys</code>, an array of deleted item IDs. The hook uses these keys to remove the corresponding documents from the Meilisearch index.</p>
<p>Now add 3 items to your articles collection and you should see them in your Meilisearch index.</p>
<p><img src="https://marketing.directus.app/assets/860f6500-d185-4d0b-84f7-6f89f9e66b50" alt="Melisearch with data from Directus" /></p>
<h2>Summary</h2>
<p>In this tutorial, you've learned how to integrate Meilisearch with Directus. You've learned how to setup the Directus hooks that automatically indexes data created, updated, or deleted from a Directus project in Meilisearch.</p>]]></content>
        <author>
            <name>Clara Ekekenta</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Testimonial Widget with SvelteKit and Directus]]></title>
        <id>https://docs.directus.io/blog/building-a-testimonial-widget-with-sveltekit-and-directus</id>
        <link href="https://docs.directus.io/blog/building-a-testimonial-widget-with-sveltekit-and-directus"/>
        <updated>2024-08-08T07:38:13.000Z</updated>
        <summary type="html"><![CDATA[In this guide, you will build a Testimonial widget with Directus and SvelteKit. You will learn how to query and add data to Directus directly from your SvelteKit app.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, we will setup a testimonial widget using SvelteKit and Directus as a backend.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li>To install Node.js and a code editor on your computer.</li>
<li>A Directus project - follow our <a href="https://docs.directus.io/getting-started/quickstart.html">quickstart guide</a> if you don't already have one.</li>
<li>Some knowledge of Svelte and SvelteKit.</li>
</ul>
<h2>Setting Up Your Directus Project</h2>
<p>Create a <code>testimonials</code> collection with the following fields:</p>
<ul>
<li><code>full_name</code> (Type: String, Interface: Input): To capture the user's full name.</li>
<li><code>email_address</code> (Type: String, Interface: Input): To store the user's email address.</li>
<li><code>review</code> (Type: Text, Interface: TextArea): To store the user's testimonials.</li>
</ul>
<p>Then give the public role full access to create and read items in the <code>testimonials</code> collection.</p>
<p>Create 3 example testimonials from the content module.</p>
<h2>Initializing a Svelte project</h2>
<p>Initialize a new Svelte project by running the following command:</p>
<pre><code class="language-bash">npm create svelte@latest testimonial-frontend # Choose Skeleton project
cd testimonial-frontend 
npm install
npm install @directus/sdk
</code></pre>
<p>Type <code>npm run dev</code> in your terminal to start the Vite development server and open <a href="http://localhost:5173">http://localhost:5173</a> in your browser to access the Svelte website.</p>
<h2>Setting Up the Directus SDK</h2>
<p>To make the Directus SDK available to your project, you need to setup a wrapper for the Directus SDK.</p>
<p>Add a <code>directus.js</code> file to the <code>./src/lib</code> directory and add the following to the file.</p>
<pre><code class="language-js">import { createDirectus, rest } from '@directus/sdk';
import { PUBLIC_API_URL } from '$env/static/public';


function getDirectusInstance(fetch) {
   const options = fetch ? { globals: { fetch } } : {};
   const directus = createDirectus(PUBLIC_API_URL, options).with(rest());
   return directus;
}

export default getDirectusInstance;
</code></pre>
<p>Add a <code>hooks.server.js</code> file to your <code>./src</code> directory, and add the following to the file.</p>
<pre><code class="language-js">export async function handle({ event, resolve }) {
   return await resolve(event, {
       filterSerializedResponseHeaders: (key, value) =&gt; {
           return key.toLowerCase() === 'content-type';
       },
   });
}
</code></pre>
<p>The <code>hooks.server.js</code> ensures that request headers required by the Directus backend are added to every request sent from your frontend to the Directus server.</p>
<p>Create a <code>.env</code> file in your project’s root directory and add the following to the file</p>
<pre><code class="language-bash">PUBLIC_API_URL='directus_server_url'
</code></pre>
<p>Change <code>directus_server_url</code> to the URL of your Directus project.</p>
<h3>Fetching Data From Directus</h3>
<p>Add a <code>+page.js</code> file to your <code>./src/routes</code> directory, and add the following content to the file.</p>
<pre><code class="language-js">/** @type {import('./$types').PageLoad} */
import getDirectusInstance from &quot;$lib/directus&quot;;
import { error } from &quot;@sveltejs/kit&quot;;
import { readItems } from &quot;@directus/sdk&quot;;

export async function load({ fetch }) {
 const directus = getDirectusInstance(fetch);
 try {
   return {
     testimonials: await directus.request(readItems(&quot;testimonials&quot;)),
   };
 } catch (err) {
  error(err);
 }
}
</code></pre>
<p>The <code>load</code> function fetch data from your testimonials collection on every page load. Update your <code>+page.svelte</code> file to the following.</p>
<pre><code class="language-js">&lt;script&gt;
 /** @type {import('./$types').PageData} */
 export let data;
&lt;/script&gt;

&lt;div&gt;
   &lt;div&gt;{data.testimonials[0].full_name}&lt;/div&gt;
   &lt;div&gt;{data.testimonials[0].email_address}&lt;/div&gt;
   &lt;div&gt;{data.testimonials[0].review}&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>Your page should contain information from your testimonials collection.</p>
<h2>Create a Testimonial Carousel</h2>
<p>Add a <code>TestimonialCard.svelte</code> and <code>TestimonialCarousel.svelte</code> file to your <code>./src/lib</code> directory. Add the following to your <code>TestiomonialCard.svelte</code> file:</p>
<pre><code class="language-js">&lt;script&gt;
  export let id;
  export let full_name;
  export let email_address;
  export let review;
&lt;/script&gt;

&lt;div {id} class=&quot;card-li&quot;&gt;
  &lt;blockquote class=&quot;card-article&quot;&gt;{review}&lt;/blockquote&gt;
  &lt;div class=&quot;card-div1&quot;&gt;
    &lt;h5 class=&quot;card-h5&quot;&gt;
      {full_name}&lt;span class=&quot;card-span&quot;&gt; {email_address}&lt;/span&gt;
    &lt;/h5&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;style&gt;
.card-li {
	font-family: sans-serif;
	position: relative;
	overflow-x: auto;
	padding: 50px 50px;
	text-align: center;
	min-width: 310px;
	width: 100%;
	text-align: center;
	box-shadow: none !important;
}

.card-li * {
	-webkit-box-sizing: border-box;
	box-sizing: border-box;
}
.card-article {
	margin: 1;
	display: block;
	border-radius: 8px;
	position: relative;
	background-color: #fafafa;
	padding: 50px 30px 70px 50px;
	font-size: 1em;
	font-weight: 500;
	margin: 0 0 -50px;
	line-height: 1.6em;
	box-shadow: 0 0 5px rgba(0, 0, 0, 0.15);
}
.card-article:before,
.card-article:after {
	font-family: &quot;FontAwesome&quot;;
	content: &quot;\201C&quot;;
	position: absolute;
	font-size: 50px;
	opacity: 0.3;
	font-style: normal;
}
.card-article:before {
	top: 35px;
	left: 20px;
}
.card-article:after {
	content: &quot;\201D&quot;;
	right: 20px;
	bottom: 35px;
}
.card-div1 {
	position: relative;
	z-index: 20;
	margin-top: 10;
	padding-bottom: 9;
	padding-top: 10;
}
.card-h5 {
	opacity: 0.8;
	margin: 0;
	font-weight: 800;
	text-align: center;
}
.card-span {
	font-weight: 400;
	text-transform: none;
	display: block;
	text-align: center;
}
&lt;/style&gt;
</code></pre>
<p>This code displays individual testimonial data in a Card. Add the following to your <code>TestimonialCarousel.svelte</code> file to implement the testimonial carousel:</p>
<pre><code class="language-js">&lt;script context=&quot;module&quot;&gt;
  import TestimonialCard from &quot;$lib/TestimonialCard.svelte&quot;;

  export const getCarouselId = (index, carouselName = &quot;carousel&quot;) =&gt;
    `${carouselName}-item-${index}`;
&lt;/script&gt;

&lt;script&gt;
  export let data;
&lt;/script&gt;

&lt;ul class=&quot;carousel-ul&quot;&gt;
  {#each data.testimonials as testimonial, index}
    &lt;svelte:component
      this={TestimonialCard}
      id={getCarouselId(index)}
      {...testimonial}
    /&gt;
  {/each}
&lt;/ul&gt;

&lt;style&gt;
.carousel-ul {
	display: flex;
	padding: 20;
	scroll-snap-type: x mandatory;
	gap: 2;
	overflow-x: auto;
}
.carousel-ul:before {
	width: 30vw;
}
.carousel-ul::after {
	width: 30vw;
}
&lt;/style&gt;
</code></pre>
<p>Update your <code>+page.svelte</code> file:</p>
<pre><code class="language-js">&lt;script&gt;
  /** @type {import('./$types').PageData} */
  export let data;
  import Carousel from &quot;$lib/TestimonialCarousel.svelte&quot;;
&lt;/script&gt;

&lt;div&gt;
  &lt;h1 class=&quot;page-h1&quot;&gt;Product testimonials&lt;/h1&gt;
&lt;/div&gt;

&lt;section class=&quot;page-section&quot;&gt;
  &lt;Carousel {data} /&gt;
&lt;/section&gt;

&lt;style&gt;
.page-h1 {
	text-align: center;
}
.page-section {
	display: grid;
	min-height: 100%;
	padding-left: 200px;
	margin: 10px;
	grid-template-rows: auto;
	place-items: center;
	overflow-x: scroll;
}
&lt;/style&gt;
</code></pre>
<p>Your page should change to something similar to the following.</p>
<p><img src="https://marketing.directus.app/assets/67cbd2a0-1660-4a77-8433-2cb4f8c56cf3" alt="Svelte Testimonial Carousel" /></p>
<h2>Creating the Add Testimonial Form</h2>
<p>The final step is to implement your Add Testimonial form. This form will allow users add data to your Testimonials collection directly from your svelte website.</p>
<p>Add a <code>TestimonialCreate.svelte</code> file your <code>./src/lib</code> directory and add the following code to the file.</p>
<pre><code class="language-js">&lt;script&gt;
import getDirectusInstance from &quot;$lib/directus&quot;;
import { error } from &quot;@sveltejs/kit&quot;;
import { createItem } from &quot;@directus/sdk&quot;;
export let full_name;
export let email_address;
export let review;
export let addTestimonial;
let loading = false;
const directus = getDirectusInstance(fetch);

async function createTestimonial() {
	var item = {
		full_name: full_name,
		email_address: email_address,
		review: review,
	};

	try {
		loading = true;
		await directus.request(createItem(&quot;testimonials&quot;, item));
		loading = false;
		addTestimonial = false;
	} catch (err) {
		console.log(err);
		loading = false;
		addTestimonial = false;
		error(err);
	}
}
&lt;/script&gt;

&lt;div class=&quot;create-div&quot;&gt;
  &lt;form class=&quot;create-form&quot;&gt;
    &lt;h1 class=&quot;create-h1&quot;&gt;Add your Testimonial&lt;/h1&gt;
    &lt;label class=&quot;create-label&quot; for=&quot;email&quot;&gt;Full Name&lt;/label&gt;
    &lt;input
      class=&quot;create-input&quot;
      name=&quot;full_name&quot;
      required
      bind:value={full_name}
    /&gt;
    &lt;label class=&quot;create-label&quot; for=&quot;password&quot;&gt;Email&lt;/label&gt;
    &lt;input
      class=&quot;create-input&quot;
      name=&quot;email_address&quot;
      type=&quot;email&quot;
      required
      bind:value={email_address}
    /&gt;
    &lt;label class=&quot;create-label&quot; for=&quot;email&quot;&gt;Enter your testimonial&lt;/label&gt;
    &lt;textarea
      rows=&quot;5&quot;
      class=&quot;create-input&quot;
      name=&quot;review&quot;
      required
      bind:value={review}
    /&gt;
    &lt;button on:click={createTestimonial} class=&quot;create-button&quot;&gt;
      {#if loading}
        &lt;svg
          aria-hidden=&quot;true&quot;
          role=&quot;status&quot;
          class=&quot;create-spinner&quot;
          viewBox=&quot;0 0 100 101&quot;
          fill=&quot;none&quot;
          xmlns=&quot;http://www.w3.org/2000/svg&quot;
        &gt;
          &lt;path
            d=&quot;M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z&quot;
            fill=&quot;#E5E7EB&quot;
          /&gt;
          &lt;path
            d=&quot;M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z&quot;
            fill=&quot;currentColor&quot;
          /&gt;
        &lt;/svg&gt;
      {:else}
        &lt;div class=&quot;create-button-text&quot;&gt;Add a review&lt;/div&gt;
      {/if}
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;

&lt;style&gt;
.create-input {
	display: flex;
	align-items: center;
	padding: 2px 2px 2px 2px;
	width: 400px;
	min-height: 30px;
	font-size: small;
	margin-top: 2px;
	border-radius: 5px;
}
.create-input:focus {
	outline: none;
}
.create-label {
	font: bold;
	font-size: small;
	margin-top: 10px;
}
.create-h1 {
	padding-top: 3px;
	font: bolder;
	font-size: medium;
}
.create-form {
	display: flex;
	flex-direction: column;
	padding: 8px 8px 8px 8px;
	background-color: white;
	border-radius: 20px;
}
.create-div {
	display: flex;
	flex-direction: column;
	justify-content: center;
	justify-items: center;
	align-items: center;
}
.create-button {
	display: flex;
	justify-items: center;
	align-items: center;
	font-size: small;
	padding: 10px 20px 10px 20px;
	width: 80;
	background-color: blue;
	border-color: white;
	margin-top: 8px;
	font: bold;
	border-radius: 25px;
	color: white;
}
.create-button-text {
	text-align: center;
	justify-content: center;
	justify-self: center;
}
.create-spinner {
	height: 8px;
	width: 8px;
	display: inline;
	justify-self: center;
	animation-name: spin;
	animation-duration: 5000ms;
	animation-iteration-count: infinite;
}
&lt;/style&gt;
</code></pre>
<p>This implements a form that accepts user inputs like <code>full_name</code>, <code>email_address</code>, and <code>review</code> and adds the input to your <code>testimonial</code> collection in Directus.</p>
<p>Update your <code>./src/routes/+page.svelte</code> to the following to add the create testimonial form:</p>
<pre><code class="language-js">&lt;script&gt;
  /** @type {import('./$types').PageData} */
  export let data;
  let addTestimonial = false;
  import Carousel from &quot;$lib/TestimonialCarousel.svelte&quot;;
  import TestimonialCreate from &quot;../lib/TestimonialCreate.svelte&quot;;
  async function createTestimonial() {
    addTestimonial = true;
  }
  async function cancelTestimonial() {
    addTestimonial = false;
  }
&lt;/script&gt;

&lt;div class=&quot;page-div1&quot;&gt;
	&lt;h1 class=&quot;page-h1&quot;&gt;Product testimonials&lt;/h1&gt;
	&lt;div class=&quot;page-div2&quot;&gt;
	  {#if addTestimonial}
		&lt;button
		  on:click={cancelTestimonial}
		  class=&quot;page-button1&quot;
		  &gt;Cancel&lt;/button
		&gt;
	  {:else}
		&lt;button
		  on:click={createTestimonial}
		  class=&quot;page-button2&quot;
		  &gt;Add your testimonial&lt;/button
		&gt;
	  {/if}
	&lt;/div&gt;
&lt;/div&gt;


{#if addTestimonial}
  &lt;TestimonialCreate {addTestimonial} /&gt;
{:else}
  &lt;section class=&quot;page-section&quot;&gt;
    &lt;Carousel {data} /&gt;
  &lt;/section&gt;
{/if}

&lt;style&gt;
.page-h1 {
	text-align: center;
}
.page-div1{
	margin-top: 2px;
}
.page-div2{
	display: flex;
	justify-content: center;
}
.page-section {
	display: grid;
	min-height: 100%;
	padding-left: 1000px;
	margin: 10px;
	grid-template-rows: auto;
	place-items: center;
	overflow-x: scroll;
}
.page-button1 {
	display: flex;
	justify-items: center;
	align-items: center;
	font-size: small;
	padding: 10px 20px 10px 20px;
	width: 80;
	background-color: red;
	border-color: white;
	margin-top: 8px;
	font: bold;
	border-radius: 25px;
	color: white;
}
.page-button2 {
	display: flex;
	justify-items: center;
	align-items: center;
	font-size: small;
	padding: 10px 20px 10px 20px;
	width: 80;
	background-color: blue;
	border-color: white;
	margin-top: 8px;
	font: bold;
	border-radius: 25px;
	color: white;
}
&lt;/style&gt;
</code></pre>
<p><img src="https://marketing.directus.app/assets/c24fa5fa-1653-4e28-abfd-0c32fcfa6adf" alt="Svelte Testimonial Carousel" /></p>
<h2>Summary</h2>
<p>In this guide, you have set up a testimonial widget in SvelteKit using Directus. It allows for adding new testimonials to Directus and displaying existing testimonials in a carousel.</p>
<p>If you have any questions, feel free to drop by our <a href="https://directus.chat/">Discord</a> server.</p>]]></content>
        <author>
            <name>Quadri Sheriff</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Getting Started with Directus and Laravel]]></title>
        <id>https://docs.directus.io/blog/getting-started-with-directus-and-laravel</id>
        <link href="https://docs.directus.io/blog/getting-started-with-directus-and-laravel"/>
        <updated>2024-08-05T09:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to integrate Directus with Laravel. You will store, retrieve, and use global metadata such as the site title, create new pages dynamically based on data in a Directus project.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, you will learn how to build a website using Directus as a Headless CMS. You will store, retrieve, and use global metadata such as the site title, create new pages dynamically based on Directus items, and build a blog.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li><a href="https://www.php.net/releases/7_4_0.php">PHP 7.4</a> or higher</li>
<li><a href="https://getcomposer.org/">Composer</a></li>
<li>A code editor on your computer.</li>
<li>A Directus project - follow our <a href="https://docs.directus.io/getting-started/quickstart">quickstart guide</a> if you don't already have one.</li>
<li>Some knowledge of Laravel.</li>
</ul>
<p>The code for this tutorial is available on my <a href="https://github.com/directus-labs/blog-example-getting-started-laravel">GitHub repository</a>.</p>
<p>Set up a new Laravel project move into the project directory by running the following commands:</p>
<pre><code class="language-shell">composer create-project laravel/laravel directus-laravel-blog
cd directus-laravel-blog
</code></pre>
<h2>Creating a Directus Module</h2>
<p>Create a new service provider with the following command:</p>
<pre><code class="language-shell">php artisan make:provider DirectusServiceProvider
</code></pre>
<p>Update the <code>app/Providers/DirectusServiceProvider.php</code> file with the following code:</p>
<pre><code class="language-php">&lt;?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Http;

class DirectusServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this-&gt;app-&gt;singleton('directus', function ($app) {
            return new class {
                protected $baseUrl;

                public function __construct()
                {
                    $this-&gt;baseUrl = rtrim(env('DIRECTUS_URL'), '/');                }

                public function request($method, $endpoint, $data = [])
                {
                    $url = &quot;{$this-&gt;baseUrl}/items/{$endpoint}&quot;;
                    return Http::$method($url, $data);
                }

                public function get($endpoint, $params = [])
                {
                    return $this-&gt;request('get', $endpoint, $params);
                }
            };
        });
    }
}
</code></pre>
<p>This defines a <code>DirectusServiceProvider</code> class which creates a singleton instance for interacting with a Directus API. It provides methods to make HTTP requests to the API, with the base URL set from environment variables.</p>
<h2>Using Global Metadata and Settings</h2>
<p>Create a new collection named <code>global</code> in your Directus project by navigating to <strong>Settings -&gt; Data Model</strong>. Choose 'Treat as a single object' under the Singleton option since this collection will have just one item with global website metadata in it.</p>
<p>Create two text input fields, one with the key <code>title</code> and the other with <code>description</code>.</p>
<p>Navigate to the content module and enter the global collection. Collections will generally display a list of items, but as a singleton, it will launch directly into the one-item form. Enter information in the title and description field and hit save.</p>
<p>By default, new collections are not accessible to the public. Navigate to Settings -&gt; Access Control -&gt; Public and give Read access to the <code>global</code> collection.</p>
<p>Create a <code>HomeController</code> with the command:</p>
<pre><code class="language-shell">php artisan make:controller HomeController
</code></pre>
<p>Open the <code>app/Http/Controllers/HomeController.php</code> that was created with the above command, and use the <code>DirectusServiceProvider</code> class instance to a call to the Directus backend to fetch global metadata.</p>
<pre><code class="language-php">&lt;?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function index()
    {
        $directus = app('directus');
        $settingsResponse = $directus-&gt;get('global');
        $settings = $settingsResponse['data'];
        return view('home', compact('settings'));
    }
}
</code></pre>
<p>The <code>DirectusServiceProvider</code> registers a singleton instance of Directus API, which can be accessed throughout the application using <code>app('directus')</code>. The <code>HomeController</code> uses this instance to fetch global settings from the Directus backend and pass them to the view.</p>
<p>Create a <code>home.blade.php</code> file in the <code>resources/views</code> directory and add the following code to render the global metadata settings:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;{{ $settings['site_title'] }}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;{{ $settings['site_title'] }}&lt;/h1&gt;
    &lt;p&gt;{{ $settings['site_description'] }}&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Edit the code in your <code>routes/web.php</code> file to add a new route for the <code>HomeController</code> view:</p>
<pre><code class="language-php">&lt;?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;

Route::get('/', [HomeController::class, 'index']);
</code></pre>
<p><img src="https://marketing.directus.app/assets/1972b708-e3da-4e7b-96b2-efc153e51b39" alt="Home page with global metadata settings" /></p>
<h2>Creating Pages With Directus</h2>
<p>Create a new collection named <code>pages</code>, and in the Primary ID Field, enter a &quot;Manually Entered String&quot; (named <code>slug</code>) that corresponds to the page's URL. For instance, the page <code>localhost:3000/about</code> will correspond to the about page.</p>
<p>Create a <code>WYSIWYG</code> input field named <code>content</code> and a text input field named <code>title</code>. In Access Control, give the Public role read access to the new collection. Create 3 items in the new collection - <a href="https://github.com/directus-labs/getting-started-demo-data">here's some sample data</a>.</p>
<p>In your project terminal, create a <code>PageController</code> with the command:</p>
<pre><code class="language-shell">php artisan make:controller PageController
</code></pre>
<p>Open the <code>app/Http/Controllers/PageController.php</code> file created with the above command and add the following code:</p>
<pre><code>&lt;?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PageController extends Controller
{
    public function show($slug)
    {
        $directus = app('directus');
        $pageResponse = $directus-&gt;get('pages', [
            'filter' =&gt; ['slug' =&gt; $slug]
        ]);
        $page = $pageResponse['data'][0];
        return view('page', compact('page'));
    }
}
</code></pre>
<p>The above code uses the Directus instance to fetch the page data from the Directus backend and pass them to the view.</p>
<p>Create a new blade view file named <code>page.blade.php</code> in your <code>resources/views</code> directory and add the following code:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;{{ $page['title'] }}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;{{ $page['title'] }}&lt;/h1&gt;
    {!! $page['content'] !!}
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Edit the <code>routes/web.php</code> file to add a new route for the <code>PageController</code> view:</p>
<pre><code class="language-php">use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\PostController;

Route::get('/page/{slug}', [PageController::class, 'show']);
Route::get('/', [HomeController::class, 'index']);
</code></pre>
<p>Navigate to <code>http://127.0.0.1:8000/page/about</code> to view the About page.</p>
<p><img src="https://marketing.directus.app/assets/e559e77c-bcdf-4642-ab89-8b37e8026a21" alt="dynamic about page" /></p>
<h3>Creating Blog Posts With Directus</h3>
<p>Create a new collection called <code>authors</code> and include a single <code>name</code> text input field. Create one or more authors.</p>
<p>Create another collection called <code>posts</code> and add the following fields:</p>
<ul>
<li>title (Type: String)</li>
<li>slug (Type: String)</li>
<li>content (Type: WYSIWYG)</li>
<li>image (Type: Image relational field)</li>
<li>author (Type: Many-to-one relational field with the related collection set to authors)</li>
</ul>
<p>Add 3 items in the posts collection - <a href="https://github.com/directus-community/getting-started-demo-data">here's some sample data</a>.</p>
<p>Create a <code>app/Http/Controllers/PageController.php</code> file by running the command:</p>
<pre><code class="language-shell">php artisan make:controller PageController
</code></pre>
<p>Update the <code>app/Http/Controllers/PageController.php</code> file with the following code:</p>
<pre><code class="language-php">&lt;?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $directus = app('directus');
        $postsResponse = $directus-&gt;get('posts', [
            'sort' =&gt; ['-date_created'],
            'limit' =&gt; 10
        ]);
        $posts = $postsResponse['data'];

        return view('posts.index', compact('posts'));
    }

    public function show($id)
    {
        $directus = app('directus');
        $postResponse = $directus-&gt;get('posts', $id);
        $post = $postResponse['data'];
        return view('posts.show', compact('post'));
    }
}
</code></pre>
<p>The above code fetches the blogs from the Directus backend and passes them to the posts view.</p>
<p>Create a <code>resources/views/page.blade.php</code> file for the page blade view and add the following code.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Blog Posts&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Blog Posts&lt;/h1&gt;
    @foreach($posts as $post)
        &lt;article&gt;
            &lt;h2&gt;&lt;a href=&quot;{{ route('posts.show', $post['id']) }}&quot;&gt;{{ $post['title'] }}&lt;/a&gt;&lt;/h2&gt;
            &lt;p&gt;Posted on: {{ date('F j, Y', strtotime($post['date_created'])) }}&lt;/p&gt;
        &lt;/article&gt;
    @endforeach
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Create another view file <code>resources/views/posts/show.blade.php</code> for the blog single page:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;{{ $post['title'] }}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;{{ $post['title'] }}&lt;/h1&gt;
    &lt;p&gt;Posted on: {{ date('F j, Y', strtotime($post['date_created'])) }}&lt;/p&gt;
    {!! $post['content'] !!}
    &lt;a href=&quot;{{ route('posts.index') }}&quot;&gt;Back to Blog&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Add the following routes to your <code>routes/web.php</code> file:</p>
<pre><code class="language-php">&lt;?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\PageController;
use App\Http\Controllers\PostController;

Route::get('/blog', [PostController::class, 'index'])-&gt;name('posts.index');
Route::get('/blog/{id}', [PostController::class, 'show'])-&gt;name('posts.show');
Route::get('/page/{slug}', [PageController::class, 'show']);
Route::get('/', [HomeController::class, 'index']);
</code></pre>
<p>Navigate to <code>http://127.0.0.1:8000/blog</code> to access the blogs page.</p>
<p><img src="https://marketing.directus.app/assets/53cd3a2e-df03-4ca4-90cb-6ffbe987ab0b" alt="blog list page" /></p>
<h2>Add Navigation</h2>
<p>Run the commmand below to create a new service provider:</p>
<pre><code class="language-shell">php artisan make:provider ViewServiceProvider
</code></pre>
<p>Then update <code>app/Providers/ViewServiceProvider.php</code> file with the following code:</p>
<pre><code class="language-php">&lt;?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;

class ViewServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $navigation = [
            ['url' =&gt; '/', 'label' =&gt; 'Home'],
            ['url' =&gt; '/blog', 'label' =&gt; 'Blog Posts'],
            ['url' =&gt; '/page/about', 'label' =&gt; 'About'],
        ];

        View::composer('*', function ($view) use ($navigation) {
            $view-&gt;with('navigation', $navigation);
        });
    }
}
</code></pre>
<p>The <code>ViewServiceProvider</code> provider service class registers an array of navigations for your application and will be used across your views to allow your users to navigate throughout the application.</p>
<p>Update all your views files in the <strong>views</strong> directory to add the navigation:</p>
<pre><code class="language-html">&lt;!-- put this after the &lt;body&gt; tag in all your views file --&gt;
&lt;nav&gt;
    @foreach($navigation as $item)
        @if(isset($item['url']) &amp;&amp; isset($item['label']))
            &lt;a href=&quot;{{ $item['url'] }}&quot;&gt;{{ $item['label'] }}&lt;/a&gt;
        @else
            &lt;p&gt;Invalid navigation item&lt;/p&gt;
        @endif
    @endforeach
&lt;/nav&gt;
</code></pre>
<p><img src="https://marketing.directus.app/assets/78d0b2b8-0110-4560-b5b4-c2164daf18d2" alt="A content page with three navigation links at the top." /></p>
<h2>Summary</h2>
<p>Throughout this tutorial, you've learned how to build a Laravel application that uses data from a Directus project. You started by creating a new project, setting up environment variables, and everything you need to call Directus. You then created pages and post collections in Directus and integrated them with the Laravel project.</p>]]></content>
        <author>
            <name>Ekekenta Clinton</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Video Streaming App with SvelteKit and Directus]]></title>
        <id>https://docs.directus.io/blog/building-a-video-streaming-app-with-sveltekit-and-directus</id>
        <link href="https://docs.directus.io/blog/building-a-video-streaming-app-with-sveltekit-and-directus"/>
        <updated>2024-08-01T08:38:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to integrate Directus with SvelteKit to build a video streaming application. You will store, and retrieve video metadata in Directus as a content management system.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, you will learn how to build an application using Directus as a backend. You will store and retrieve video metadata in a Directus project as a content management system, and use them to build a video streaming application that tracks views.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li><a href="https://nodejs.org/">Node.js v20.11.1</a> or later.</li>
<li>A code editor on your computer.</li>
<li>A Directus project - follow our <a href="https://docs.directus.io/getting-started/quickstart">quickstart guide</a> if you don't already have one.</li>
<li>Some knowledge of SvelteKit.</li>
</ul>
<p>You can find the code for this tutorial in the <a href="https://github.com/directus-labs/blog-example-video-streaming-app-sveltekit">GitHub repository</a>.</p>
<h2>Creating a SvelteKit Project</h2>
<p>Create a new SvelteKit project and install the required dependencies including the Directus SDK:</p>
<pre><code>npm create svelte@latest video-streaming-app #
cd video-streaming-app
npm install
npm install @directus/sdk svelte-video-player
</code></pre>
<p>In your <code>src/libs</code> folder, create a new <code>directus.ts</code> file to create a Directus SDK instance helper function:</p>
<pre><code class="language-js">import { createDirectus, rest } from &quot;@directus/sdk&quot;;
export const DIRECTUS_API_URL = import.meta.env.VITE_DIRECTUS_URL;
function getDirectusClient() {
  const directus = createDirectus(DIRECTUS_API_URL).with(rest());
  return directus;
};
export default getDirectusClient;
</code></pre>
<p>Create a <code>.env</code> file in the root folder of your project and add your Directus API URL:</p>
<pre><code>VITE_DIRECTUS_URL='https://directus.example.com';
</code></pre>
<h2>Creating a Directus Collection</h2>
<p>Create a new <code>videos</code> collection with the following fields:</p>
<ul>
<li><code>id</code> (Primary Key Field, Type: Manually entered string)</li>
<li><code>title</code> (Type: String, Interface: Input)</li>
<li><code>description</code> (Type: String, Interface: Input)</li>
<li><code>thumbnail</code> (Type: Image Field)</li>
<li><code>video_file</code> (Type: File Field)</li>
<li><code>tags</code>  (Type: Tags Field)</li>
<li><code>views</code> (Type: String, Interface: Input)</li>
<li><code>upload_date</code> (Type: Datetime Field)</li>
</ul>
<p>Give the Public role read access on the <code>videos</code> and <code>directus_files</code> collections.</p>
<p>Create 3 videos in the collection to test with.</p>
<h2>Creating the Video Listing Page</h2>
<p>In your SvelteKit application, create a <code>types.ts</code> file in the <code>src/lib</code> folder to define the structure of your video data and ensure type safety throughout your application.</p>
<pre><code class="language-ts">export interface Video {
  id: string;
  title: string;
  description: string;
  video_file:  { id: string };
  thumbnail: { id: string };
  tags: string[];
  duration: number;
  views: number;
  upload_date: string;
}
</code></pre>
<p>Create a new <code>components</code> folder inside the <code>src/libs</code> folder. Within this new folder, create two component files: <code>VideoCard.svelte</code> and <code>VideoGrid.svelte</code>. Add the following code to the <code>VideoCard.svelte</code> file:</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
  import type { Video } from &quot;$lib/types&quot;;
  export let video: Video;
&lt;/script&gt;
&lt;a href=&quot;/video/{video.id}&quot; class=&quot;video-card&quot;&gt;
  &lt;img
    src={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.thumbnail.id}`}
    alt={video.title}
  /&gt;
  &lt;h3&gt;{video.title}&lt;/h3&gt;
  &lt;p&gt;
    {video.views} views • {new Date(video.upload_date).toLocaleDateString()}
  &lt;/p&gt;
&lt;/a&gt;
&lt;style&gt;
  .video-card { display: block; text-decoration: none; color: inherit; }
  img { width: 100%; height: auto; }
&lt;/style&gt;
</code></pre>
<p>Then add the following code to the <code>VideoGrid.svelte</code> file:</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
  import type { Video } from &quot;$lib/types&quot;;
  import VideoCard from &quot;./VideoCard.svelte&quot;;
  export let videos: Video[];
&lt;/script&gt;
&lt;div class=&quot;video-grid&quot;&gt;
  {#each videos as video}
    &lt;VideoCard {video} /&gt;
  {/each}
&lt;/div&gt;
&lt;style&gt;
  .video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
&lt;/style&gt;
</code></pre>
<p>Those two files are reusable components created to render your video data and organize the videos in a grid layout.</p>
<h2>Fetching Videos</h2>
<p>In your <code>src/libs folder</code>, create a new folder named <code>services</code>. Inside this folder, create a file named <code>index.ts</code>. Add the following code to this file to use the Directus helper function to fetch all the videos from your Directus <code>videos</code> collection:</p>
<pre><code class="language-ts">import getDirectusClient from &quot;$lib/directus&quot;;
import { readItems } from &quot;@directus/sdk&quot;;
import type { Video } from &quot;$lib/types&quot;;

export async function getVideos(params = {}): Promise&lt;Video[]&gt; {
  const directus = getDirectusClient();
  const response = await directus.request(readItems(&quot;videos&quot;, params));
  return response as Video[];
}
</code></pre>
<h2>Displaying Thumbnails and Titles</h2>
<p>Update the your <code>routes/+page.svelte</code> file to use the <code>getVideos</code> function to fetch video data and display it using the <code>VideoGrid</code> component. This will display the thumbnails, titles, views and dates of the videos.</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
  import { onMount } from &quot;svelte&quot;;
  import { getVideos } from &quot;$lib/services/index&quot;;
  import VideoGrid from &quot;$lib/components/VideoGrid.svelte&quot;;
  import type { Video } from &quot;$lib/types&quot;;
  let videos: Video[] = [];
  onMount(async () =&gt; {
    try {
      videos = await getVideos({
        sort: [&quot;-upload_date&quot;],
        limit: 20,
        fields: [
          &quot;*&quot;,
          { thumbnail: [&quot;*&quot;] },
          { video_file: [&quot;*&quot;] }
        ],
      });
    } catch (error) {
      console.error(&quot;Error fetching videos:&quot;, error);
    }
  });
&lt;/script&gt;
&lt;h1&gt;Stream your favorite vidoes&lt;/h1&gt;
{#if videos.length &gt; 0}
  &lt;VideoGrid {videos} /&gt;
{:else}
  &lt;p&gt;Loading videos...&lt;/p&gt;
{/if}
</code></pre>
<p>Directus stores file metadata in the <code>directus_files</code> collection.</p>
<p><img src="https://marketing.directus.app/assets/c4e7a9bf-6658-4af6-8e00-26c88d772861" alt="Video Listing" /></p>
<h2>Building the Video Player Page</h2>
<p>Update your <code>services/index.ts</code> file to add new functions that will fetch a video by its ID and update the <code>videos</code> collection to increment the video's views field.</p>
<pre><code class="language-ts">// your other imports

import { readItems, readItem, updateItem } from &quot;@directus/sdk&quot;;

export async function getVideo(id: string): Promise&lt;Video&gt; {
  const directus = getDirectusClient();
  const response = await directus.request(
    readItem(&quot;videos&quot;, id, {
      fields: [
          &quot;*&quot;,
          { thumbnail: [&quot;*&quot;] },
          { video_file: [&quot;*&quot;] }
        ]
    })
  );
  return response as Video;
}

export async function incrementViews(id: string) {
  const directus = getDirectusClient();
  const video = await directus.request(readItem(&quot;videos&quot;, id));
  await directus.request(
    updateItem(&quot;videos&quot;, id, { views: parseInt(video.views || 0) + 1 })
  );
}
</code></pre>
<p>Create a nested route in your <code>routes</code> folder in the format <code>video/[id]/+page.svelte</code> to create a page to play selected videos. Update this file with the following code:</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
  import { page } from &quot;$app/stores&quot;;
  import { getVideo, incrementViews } from &quot;$lib/services&quot;;
  import VideoPlayer from &quot;svelte-video-player&quot;;
  import type { Video } from &quot;$lib/types&quot;;
  let video: Video | null = null;
  $: id = $page.params.id;
  $: if (id) {
    getVideo(id)
      .then((v: Video) =&gt; {
        video = v;
        incrementViews(id);
      })
      .catch((error) =&gt; {
        console.error(&quot;Error fetching video:&quot;, error);
      });
  }
&lt;/script&gt;
{#if video}
  &lt;h1&gt;{video.title}&lt;/h1&gt;
  &lt;p&gt;
    {video.views} views • {new Date(video.upload_date).toLocaleDateString()}
  &lt;/p&gt;
  &lt;VideoPlayer
    poster={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.thumbnail.id}`}
    source={`${import.meta.env.VITE_DIRECTUS_URL}/assets/${video.video_file.id}`}
  /&gt;
  &lt;h2&gt;Description&lt;/h2&gt;
  &lt;p&gt;{video.description}&lt;/p&gt;
  &lt;h2&gt;Tags&lt;/h2&gt;
  &lt;div class=&quot;tags&quot;&gt;
    {#each video.tags as tag}
      &lt;span class=&quot;tag&quot;&gt;{tag}&lt;/span&gt;
    {/each}
  &lt;/div&gt;
{:else}
  &lt;p&gt;Loading...&lt;/p&gt;
{/if}
&lt;style&gt;
  .tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }
  .tag {
    background-color: #eee;
    padding: 0.25rem 0.5rem;
    border-radius: 0.25rem;
  }
&lt;/style&gt;
</code></pre>
<p>Now click on any of the videos to stream it.</p>
<p><img src="https://marketing.directus.app/assets/3518022b-bc9d-4c37-aed7-28a50306a69b" alt="Video Player" /></p>
<h2>Creating Search Functionality</h2>
<p>In your <code>services/index.ts</code>, add a new funtion that implements search functionality to find videos by title or description.</p>
<pre><code class="language-ts">export async function searchVideos(query: string): Promise&lt;Video[]&gt; {
  const directus = getDirectusClient();
  const response = await directus.request(
    readItems(&quot;videos&quot;, {
      search: query,
      fields: [&quot;*&quot;, &quot;thumbnail.*&quot;, &quot;video_file.*&quot;],
    })
  );
  return response as Video[];
}
</code></pre>
<p>This function uses the <code>search</code> parameter from Directus to perform a search on <code>videos</code> collection.</p>
<p>Update the code in your <code>routes/+page.svelte</code> file to use the <code>searchVideos</code> function to add search functionality to your page.</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
  import { onMount } from &quot;svelte&quot;;
  import { getVideos, searchVideos } from &quot;$lib/services/index&quot;;
  import VideoGrid from &quot;$lib/components/VideoGrid.svelte&quot;;
  import type { Video } from &quot;$lib/types&quot;;
  let videos: Video[] = [];
  let searchQuery = &quot;&quot;;
  let searchResults: Video[] = [];
  let isSearching = false;
  onMount(async () =&gt; {
    await loadLatestVideos();
  });
  async function loadLatestVideos() {
    try {
      videos = (await getVideos({
        sort: [&quot;-upload_date&quot;],
        limit: 20,
        fields: [&quot;*&quot;, &quot;thumbnail.*&quot;, &quot;video_file.*&quot;],
      })) as Video[];
    } catch (error) {
      console.error(&quot;Error fetching videos:&quot;, error);
    }
  }
  async function handleSearch() {
    if (searchQuery.trim()) {
      isSearching = true;
      try {
        const response = await searchVideos(searchQuery);
        searchResults = response as Video[];
      } catch (error) {
        console.error(&quot;Error searching videos:&quot;, error);
      } finally {
        isSearching = false;
      }
    } else {
      searchResults = [];
    }
  }
&lt;/script&gt;
&lt;h1&gt;Stream your favorite vidoes&lt;/h1&gt;
&lt;form on:submit|preventDefault={handleSearch}&gt;
  &lt;input type=&quot;text&quot; bind:value={searchQuery} placeholder=&quot;Search for videos&quot; /&gt;
  &lt;button type=&quot;submit&quot;&gt;Search&lt;/button&gt;
&lt;/form&gt;
{#if isSearching}
  &lt;p&gt;Searching...&lt;/p&gt;
{:else if searchResults.length &gt; 0}
  &lt;h2&gt;Search Results&lt;/h2&gt;
  &lt;VideoGrid videos={searchResults} /&gt;
{:else if searchQuery}
  &lt;p&gt;No results found.&lt;/p&gt;
{:else}
  &lt;h2&gt;Latest Videos&lt;/h2&gt;
  {#if videos.length &gt; 0}
    &lt;VideoGrid {videos} /&gt;
  {:else}
    &lt;p&gt;Loading videos...&lt;/p&gt;
  {/if}
{/if}
</code></pre>
<p>You can now search and stream any video of your choice.</p>
<p><img src="https://marketing.directus.app/assets/670ca881-f281-4580-8ed7-d004b6d13258" alt="Search videos" /></p>
<h2>Summary</h2>
<p>In this tutorial, you've learned how to build a SvelteKit video streaming application that uses data from a Directus project, tracking views when rendering data into pages.</p>]]></content>
        <author>
            <name>Clara Ekekenta</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Job Board Platform with Directus and SolidStart.js]]></title>
        <id>https://docs.directus.io/blog/building-a-job-board-platform-with-directus-and-solid-start-js</id>
        <link href="https://docs.directus.io/blog/building-a-job-board-platform-with-directus-and-solid-start-js"/>
        <updated>2024-07-29T09:14:07.000Z</updated>
        <summary type="html"><![CDATA[Learn how to build a job board platform with Directus and SolidStart.js. You'll learn how to register new users, login, and perform operations on your data.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, you'll learn to build a job board portal using Directus and SolidStart.js. We'll cover user registration, login, and working with data in Directus. You'll create a complete job board with listing, application, and management features for both jobs and applications. This guide will provide you with the skills to combine Directus's backend capabilities with SolidStart.js' reactive frontend.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li><a href="https://nodejs.org/">Node.js v18</a> or above installed on your computer.</li>
<li>A Directus project - follow our <a href="https://docs.directus.io/getting-started/quickstart">quickstart guide</a> if you don't already have one.</li>
<li>Some experience with Typescript and SolidStart.js.</li>
</ul>
<p>The code for this tutorial is available on this <a href="https://github.com/directus-labs/blog-example-job-board-solidstart">GitHub repository</a>.</p>
<h2>Configuring Directus Data Models</h2>
<p>Create a <code>job</code>, and <code>application</code> collection in your project. The <code>job</code> will store a list available jobs with the following fields:</p>
<ul>
<li><code>id</code>: autocomplete input</li>
<li><code>title</code>: input field</li>
<li><code>location</code>: input field</li>
<li><code>type</code>: input field</li>
<li><code>salary</code>: input field</li>
</ul>
<p>The <code>application</code> collection will store job applications with the following fields:</p>
<ul>
<li><code>id:</code> Autocomplete Input</li>
<li><code>status</code>: input field</li>
</ul>
<p>To link a new job with its creator:</p>
<ol>
<li>In the <code>job</code> collection, add a Many-to-One field named <code>employer</code>.</li>
<li>Set the related collection to System &gt; <code>directus_users</code>.</li>
<li>Choose <code>first_name</code> as the Display template.</li>
</ol>
<p>This creates a relationship between the job and the admin who created it.</p>
<p>In the <code>application</code> collection, add two Many-to-One fields:</p>
<ol>
<li><code>user</code>: Links to the applicant</li>
<li><code>job</code>: Links to the job</li>
</ol>
<p>These fields connect each application to its corresponding applicant and job. Add 3 items in the <code>job</code> collection - <a href="https://github.com/directus-labs/blog-example-job-board-solidstart/blob/main/src/components/jobs.md">here's some sample data</a>.</p>
<h2>Creating a New User Role</h2>
<p>In your Access Control settings, create a new role called <code>Job Applicant</code>. For the <code>application</code> collection, enable <code>create</code> and <code>read</code> permissions. Use custom rules for the <code>application</code> collection to ensure users can only read and update their own applications. Set a filter like: <code>user -&gt; id Equals $CURRENT_USER.id</code>. This allows users to view all jobs, create new applications, and view or update only their own applications.</p>
<p>Enable public read access for the <code>job</code> collection to allow users to see available jobs even when they are not logged in.</p>
<p>Enable user registration in your project settings, and select <code>Job Applicant</code> aas the role for new users.</p>
<h2>Initializing a SolidStart.js project</h2>
<p>Create a new SolidStart project by running the command:</p>
<pre><code class="language-env">npm init solid@latest
</code></pre>
<p>Choose the bare template, enable server-side rendering, annd use TypeScript.</p>
<p>In your SolidStart project's <code>src</code> directory, create a <code>lib</code> directory. Inside it, create a <code>directus.js</code> file:</p>
<pre><code class="language-jsx">import { authentication, createDirectus, rest } from &quot;@directus/sdk&quot;;

export const PUBLIC_DIRECTUS_API_URL = import.meta.env
  .VITE_PUBLIC_DIRECTUS_API_URL;

function getDirectusInstance() {
  const directus = createDirectus(PUBLIC_DIRECTUS_API_URL)
    .with(
      authentication(&quot;cookie&quot;, { credentials: &quot;include&quot;, autoRefresh: true })
    )
    .with(rest({ credentials: &quot;include&quot; }));
  return directus;
}
export default getDirectusInstance;
</code></pre>
<p>Add your Directus URL to the <code>.env</code> file:</p>
<pre><code class="language-env">VITE_PUBLIC_DIRECTUS_API_URL='https://directus.example.com'
</code></pre>
<h2>Implementing User Authentication</h2>
<p>To implement user authentication and grant users access to the application, create a <code>context</code> directory and, inside it, an <code>AuthContext.tsx</code> file.</p>
<h3>Creating User Registration</h3>
<p>In the <code>AuthContext.tsx</code>, implement user registration:</p>
<pre><code class="language-jsx">import {
  createContext,
  useContext,
  JSX,
  createSignal,
  createEffect,
} from &quot;solid-js&quot;;
import { User } from &quot;../types&quot;;
import getDirectusInstance from &quot;~/lib/directus&quot;;
import { createUser } from &quot;@directus/sdk&quot;;

interface AuthContextType {
  register: (user: Omit&lt;User, &quot;id&quot;&gt;) =&gt; Promise&lt;void&gt;;
}

const AuthContext = createContext&lt;AuthContextType&gt;();
const directus = getDirectusInstance();

export function AuthProvider(props: { children: JSX.Element }) {
  const register = async (newUser: Omit&lt;User, &quot;id&quot;&gt;) =&gt; {
    try {
      await directus.request(
        registerUser({
          email: newUser.email,
          password: newUser.password,
        })
      );
    } catch (error) {
      throw new Error(&quot;Registration failed&quot;);
    }
  };

  return (
    &lt;AuthContext.Provider value={{ register }}&gt;
      {props.children}
    &lt;/AuthContext.Provider&gt;
  );
}

export const useAuth = () =&gt; useContext(AuthContext)!;
</code></pre>
<h3>Creating User Login</h3>
<p>Update the <code>AuthContext.tsx</code> file, first adding functions to save, retrieve, and delete user sessions:</p>
<pre><code class="language-ts">//...
const setCookie = (name: string, value: string, days: number = 7) =&gt; {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${encodeURIComponent(
    value
  )}; expires=${expires}; path=/`;
};

const getCookie = (name: string): string | null =&gt; {
  return document.cookie.split(&quot;; &quot;).reduce((r, v) =&gt; {
    const parts = v.split(&quot;=&quot;);
    return parts[0] === name ? decodeURIComponent(parts[1]) : r;
  }, &quot;&quot;);
};

const deleteCookie = (name: string) =&gt; {
  setCookie(name, &quot;&quot;, -1);
};
</code></pre>
<p>Then implement the login functionality:</p>
<pre><code class="language-ts">//...
import { createUser, readMe, withToken } from &quot;@directus/sdk&quot;;

interface AuthContextType {
  register: (user: Omit&lt;User, &quot;id&quot;&gt;) =&gt; Promise&lt;void&gt;;
  user: () =&gt; User | null;
  login: (email: string, password: string) =&gt; Promise&lt;void&gt;;
}

export function AuthProvider(props: { children: JSX.Element }) {
  const [user, setUser] = createSignal&lt;User | null&gt;(null);
  const getToken = () =&gt; getCookie(&quot;auth_token&quot;) || &quot;&quot;;

  const login = async (email: string, password: string) =&gt; {
    try {
      const result = await directus.login(email, password);
      setCookie(&quot;auth_token&quot;, result.access_token as string);
      directus.setToken(result.access_token);
      await fetchUser();
    } catch (error) {
      throw new Error(&quot;Invalid credentials&quot;);
    }
  };


   return (
    &lt;AuthContext.Provider value={{ register, login, user }}&gt;
      {props.children}
    &lt;/AuthContext.Provider&gt;
  );
}
</code></pre>
<h3>Creating User Logout</h3>
<p>Update the <code>AuthContext.tsx</code> file to add user logout functionality:</p>
<pre><code class="language-ts">//...

export function AuthProvider(props: { children: JSX.Element }) {
  // ...

  interface AuthContextType {
    register: (user: Omit&lt;User, &quot;id&quot;&gt;) =&gt; Promise&lt;void&gt;;
    user: () =&gt; User | null;
    login: (email: string, password: string) =&gt; Promise&lt;void&gt;;
    logout: () =&gt; Promise&lt;void&gt;;
  }

  const logout = async () =&gt; {
    try {
      await directus.logout();
    } catch (error) {
      console.error(&quot;Logout error:&quot;, error);
    } finally {
      setUser(null);
      deleteCookie(&quot;auth_token&quot;);
      directus.setToken(null);
    }
  };

  return (
    &lt;AuthContext.Provider value={{ register, login, user, logout }}&gt;
      {props.children}
    &lt;/AuthContext.Provider&gt;
  );
}
</code></pre>
<h3>Getting Active User Data</h3>
<p>Update <code>AuthContext.tsx</code> to fetch the details of the actively logged-in user:</p>
<pre><code class="language-ts">//...
export function AuthProvider(props: { children: JSX.Element }) {
  // ...
  createEffect(() =&gt; {
    const token = getToken();
    if (token) {
      directus.setToken(token);
      fetchUser();
    }
  });

  const fetchUser = async () =&gt; {
    try {
      const userData = await directus.request(
        withToken(
          getToken(),
          readMe({
            fields: [&quot;*&quot;],
            deep: {
              role: {
                fields: [&quot;*&quot;],
              },
            },
          })
        )
      );
      setUser(userData as User);
    } catch (error) {
      await logout();
    }
  };

  //...
}
</code></pre>
<p>This <code>AuthContext</code> handles user registration, login, logout, and session management. It retrieves user session information from cookies, including access and refresh tokens, and returns an object containing this information.</p>
<p>In your <code>src</code> directory, create a new <code>types</code> directory. Add an <code>index.ts</code> file inside it to define the <code>User</code> interface used in <code>AuthContext</code> and other interfaces you'll be using throughout your application. This centralizes your TypeScript type definitions.</p>
<pre><code class="language-jsx">export interface User {
  id?: number;
  email: string;
  password: string;
  first_name?: string;
  last_name?: string;
  role?: string;
}

export interface Application {
  id: number;
  job: Job;
  user: User,
  status: 'pending' | 'reviewed' | 'accepted' | 'rejected';
  resumeUrl: string;
}

export interface Job {
  id?: number;
  title: string;
  description: string;
  location: string;
  type: string;
  salary: number;
  employer?: User | string;
}

export type Jobs = Job[];
export type Applications = Application[];
</code></pre>
<p>Create two new files, <code>register.tsx</code> and <code>login.tsx</code>, in your routes directory to implement the user registration and login forms. Add the following to <code>register.tsx</code>:</p>
<pre><code class="language-jsx">import { createSignal } from &quot;solid-js&quot;;
import { useAuth } from &quot;../context/AuthContext&quot;;
import { useNavigate } from &quot;@solidjs/router&quot;;

export default function RegisterPage() {
  const [email, setEmail] = createSignal(&quot;&quot;);
  const [password, setPassword] = createSignal(&quot;&quot;);
  const [role, setRole] = createSignal&lt;&quot;applicant&quot; | &quot;employer&quot;&gt;(&quot;applicant&quot;);
  const auth = useAuth();
  const navigate = useNavigate();

  const handleRegister = async (e: Event) =&gt; {
    e.preventDefault();
    try {
     const res = await auth.register({
        email: email(),
        password: password(),
      });
      await auth.login(email(), password());
      navigate(&quot;/&quot;, { replace: true });
    } catch (err) {
      console.log(err)
      alert(&quot;Registration failed. Please try again.&quot;);
    }
  };

  return (
    &lt;form onSubmit={handleRegister}&gt;
      &lt;input
        type=&quot;email&quot;
        placeholder=&quot;Email&quot;
        value={email()}
        onInput={(e) =&gt; setEmail(e.currentTarget.value)}
        required
      /&gt;
      &lt;input
        type=&quot;password&quot;
        placeholder=&quot;Password&quot;
        value={password()}
        onInput={(e) =&gt; setPassword(e.currentTarget.value)}
        required
      /&gt;
      &lt;select
        value={role()}
        onChange={(e) =&gt;
          setRole(e.currentTarget.value as &quot;applicant&quot; | &quot;employer&quot;)
        }
      &gt;
        &lt;option value=&quot;applicant&quot;&gt;Applicant&lt;/option&gt;
        &lt;option value=&quot;employer&quot;&gt;Employer&lt;/option&gt;
      &lt;/select&gt;
      &lt;button type=&quot;submit&quot;&gt;Register&lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>Then add the code snippets below to the <code>login.tsx</code> file.</p>
<pre><code class="language-jsx">import { createSignal } from &quot;solid-js&quot;;
import { useAuth } from &quot;../context/AuthContext&quot;;
import { useNavigate } from &quot;@solidjs/router&quot;;

export default function LoginPage() {
  const [email, setEmail] = createSignal(&quot;&quot;);
  const [password, setPassword] = createSignal(&quot;&quot;);
  const navigate = useNavigate();
  const auth = useAuth();

  const handleLogin = async (e: Event) =&gt; {
    e.preventDefault();
    try {
      await auth.login(email(), password());
      navigate(&quot;/&quot;, { replace: true });
    } catch (err) {
      alert(&quot;Invalid credentials. Please try again.&quot;);
    }
  };

  return (
    &lt;form onSubmit={handleLogin}&gt;
      &lt;input
        type=&quot;email&quot;
        placeholder=&quot;Email&quot;
        value={email()}
        onInput={(e) =&gt; setEmail(e.currentTarget.value)}
        required
      /&gt;
      &lt;input
        type=&quot;password&quot;
        placeholder=&quot;Password&quot;
        value={password()}
        onInput={(e) =&gt; setPassword(e.currentTarget.value)}
        required
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;Login&lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<h2>Adding Navigation</h2>
<p>SolidStart uses a file-based routing system, so all the files in your <code>src/routes</code> directory are automatically routes. To set up navigation:</p>
<ol>
<li>Use the <code>&lt;FileRoutes /&gt;</code> component from SolidStart.</li>
<li>Wrap it with <code>&lt;Router&gt;</code>from <code>@solidjs/router</code>.</li>
<li>Enclose everything in <code>&lt;AuthProvider&gt;</code> for app-wide authentication context.</li>
</ol>
<p>Your <code>App</code> component should look like this:</p>
<pre><code class="language-jsx">import { Router } from &quot;@solidjs/router&quot;;
import { AuthProvider } from &quot;./context/AuthContext&quot;;
import { Suspense } from &quot;solid-js&quot;;
import { FileRoutes } from &quot;@solidjs/start/router&quot;;

export default function App() {
  return (
    &lt;AuthProvider&gt;
      &lt;Router root={props =&gt; &lt;Suspense&gt;{props.children}&lt;/Suspense&gt;}&gt;
        &lt;FileRoutes /&gt;
      &lt;/Router&gt;
    &lt;/AuthProvider&gt;
  );
}
</code></pre>
<p>This setup enables automatic routing based on your file structure while providing authentication context throughout the app.</p>
<h2>Creating Job Listing Components</h2>
<p>To use the <code>getDirectusInstance</code> to get data from Directus, create a <code>components</code> directory, inside the components directory, create <code>JobList.tsx</code>:</p>
<pre><code class="language-jsx">import { For, Show } from &quot;solid-js&quot;;
import { Jobs, Job } from &quot;../types&quot;;

interface JobListProps {
  jobs: Jobs;
  onEdit?: (job: Job) =&gt; void;
  onDelete?: (id: number) =&gt; void;
  onApply?: (id: number) =&gt; void;
}

function JobList(props: JobListProps) {
  return (
    &lt;div class=&quot;container&quot;&gt;
      &lt;h2 class=&quot;title&quot;&gt;Job Listings&lt;/h2&gt;
      &lt;ul class=&quot;job-list&quot;&gt;
        &lt;For each={props.jobs}&gt;{(job: Job) =&gt; 
          &lt;li class=&quot;job-list-item&quot;&gt;
            &lt;h3 class=&quot;job-title&quot;&gt;{job.title}&lt;/h3&gt;
            &lt;p class=&quot;job-description&quot;&gt;{job.description}&lt;/p&gt;
            &lt;p class=&quot;job-location&quot;&gt;Location: {job.location}&lt;/p&gt;
            &lt;p class=&quot;job-type&quot;&gt;Type: {job.type}&lt;/p&gt;
            &lt;p class=&quot;job-salary&quot;&gt;Salary: ${job.salary}&lt;/p&gt;
            &lt;Show when={props.onEdit}&gt;
              &lt;button onClick={() =&gt; props.onEdit!(job)}&gt;Edit&lt;/button&gt;
            &lt;/Show&gt;
            &lt;Show when={props.onDelete}&gt;
              &lt;button onClick={() =&gt; props.onDelete!(job.id as number as number)}&gt;Delete&lt;/button&gt;
            &lt;/Show&gt;
            &lt;Show when={props.onApply}&gt;
              &lt;button onClick={() =&gt; props.onApply!(job.id as number)}&gt;Apply&lt;/button&gt;
            &lt;/Show&gt;
          &lt;/li&gt;
        }&lt;/For&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}

export default JobList;
</code></pre>
<p>The <code>JobList</code> component takes four props:</p>
<ul>
<li><code>jobs</code>: An array of job objects to display</li>
<li><code>onEdit</code>: A function to handle job editing</li>
<li><code>onDelete</code>: A function to handle job deletion</li>
<li><code>onApply</code>: A function to handle job applications</li>
</ul>
<p>In the <code>routes/index.tsx</code> file use the <code>JobList</code> component to display the job listings:</p>
<pre><code class="language-jsx">import { createResource, Show } from &quot;solid-js&quot;;
import { Jobs } from &quot;../types&quot;;
import { useAuth } from &quot;../context/AuthContext&quot;;
import { readItems } from &quot;@directus/sdk&quot;;
import getDirectusInstance from &quot;~/lib/directus&quot;;
import { useNavigate } from &quot;@solidjs/router&quot;;
import JobList from &quot;~/components/JobList&quot;;

function HomePage() {
  const directus = getDirectusInstance();
  const auth = useAuth();
  const navigate = useNavigate();

  const fetchJobs = async () =&gt; {
    try {
      const fetchedJobs = await directus.request(readItems(&quot;job&quot;));
      return fetchedJobs as Jobs;
    } catch (error) {
      console.error(&quot;Error fetching jobs:&quot;, error);
    }
  };

  const [jobs, { refetch: refetchJobs }] = createResource(fetchJobs);

  return (
    &lt;div&gt;
      &lt;h1&gt;Job Management System&lt;/h1&gt;
      &lt;Show
        when={auth.user()}
        fallback={
          &lt;nav&gt;
            &lt;button onClick={() =&gt; navigate(&quot;/login&quot;)}&gt;Login&lt;/button&gt;
            &lt;button onClick={() =&gt; navigate(&quot;/register&quot;)}&gt;Register&lt;/button&gt;
          &lt;/nav&gt;
        }
      &gt;
        &lt;button onClick={auth.logout}&gt;Logout&lt;/button&gt;
        &lt;Show when={auth.user()?.email === &quot;admin@example.com&quot;}&gt;
          &lt;button onClick={() =&gt; navigate(&quot;/applications&quot;)}&gt;
            Manage Applications
          &lt;/button&gt;
        &lt;/Show&gt;
      &lt;/Show&gt;
      &lt;Show when={jobs.loading}&gt;Loading jobs...&lt;/Show&gt;
      &lt;Show when={jobs.error}&gt;Error loading jobs: {jobs.error}&lt;/Show&gt;
    &lt;Show
        when={!jobs.error}
        fallback={&lt;div&gt;Error loading jobs: {jobs.error?.message}&lt;/div&gt;}
      &gt;
        &lt;JobList
          jobs={jobs() || []}
        /&gt;
      &lt;/Show&gt;
    &lt;/div&gt;
  );
}

export default HomePage;
</code></pre>
<p><img src="https://marketing.directus.app/assets/ab1b4fb8-57aa-410d-93db-9a3b5fb90028" alt="Job Listing Portal" /></p>
<h2>Creating, Updating, and Deleting Job Listings</h2>
<p>Update the job <code>HomePage</code> component to implement job listing management functionalities.</p>
<h3>Creating Job Listings</h3>
<p>Add the following <code>addJob</code> function to the <code>HomePage</code> component to enable administrators to create new job listings:</p>
<pre><code class="language-tsx">import { createSignal, createResource, Show } from &quot;solid-js&quot;;
import JobList from &quot;~/components/JobList&quot;;
import JobForm from &quot;~/components/JobForm&quot;;
import { Job } from &quot;../types&quot;;
import Modal from &quot;~/components/Modal&quot;;
import { useAuth } from &quot;../context/AuthContext&quot;;
import { createItem } from &quot;@directus/sdk&quot;;
import getDirectusInstance from &quot;~/lib/directus&quot;;
import { useNavigate } from &quot;@solidjs/router&quot;;

function HomePage() {
  const [isModalOpen, setIsModalOpen] = createSignal(false);
  const [modalContent, setModalContent] = createSignal&lt;&quot;jobForm&quot;&gt;(&quot;jobForm&quot;);
  const [editingJob, setEditingJob] = createSignal&lt;Job | null&gt;(null);
  const auth = useAuth();
  const directus = getDirectusInstance();
  const navigate = useNavigate();

  const addJob = async (job: Omit&lt;Job, &quot;id&quot;&gt;) =&gt; {
    try {
      if (!auth.user()) {
        throw new Error(&quot;You must be logged in to create a job&quot;);
      }
      job.employerId = auth.user()?.id as string;
      const response = await directus.request(createItem(&quot;job&quot;, job));

      if (response) {
        setIsModalOpen(false);
        refetchJobs();
      } else {
        throw new Error(&quot;Failed to add job&quot;);
      }
    } catch (error) {
      console.error(&quot;Error adding job:&quot;, error);
      alert(&quot;Failed to add job. Please try again.&quot;);
    }
  };

   // ... rest of your component code
}
</code></pre>
<h3>Updating Job Listings</h3>
<p>Implement the <code>updateJob</code> function in the <code>HomePage</code> component to allow administrators to edit existing job listings:</p>
<pre><code class="language-ts">+
function HomePage() {
  // ... existing code

  const updateJob = async (updatedJob: Job, id: string) =&gt; {
    try {
      if (!auth.user()) {
        throw new Error(&quot;You must be logged in to update a job&quot;);
      }
      await directus.request(updateItem(&quot;job&quot;, id, updatedJob));
      setEditingJob(null);
      setIsModalOpen(false);
      refetchJobs();
    } catch (error) {
      console.error(&quot;Error updating job:&quot;, error);
      alert(&quot;Failed to update job. Please try again.&quot;);
    }
  };

  // ... rest of your component code
}
</code></pre>
<h3>Deleting Job Listings</h3>
<p>Add the <code>deleteJob</code> function to the <code>HomePage</code> component to enable administrators to remove job listings:</p>
<pre><code class="language-ts">+
import { deleteItem } from &quot;@directus/sdk&quot;;

function HomePage() {
  // ... existing code

  const deleteJob = async (id: number) =&gt; {
    try {
      await directus.request(deleteItem(&quot;job&quot;, id));
      refetchJobs();
    } catch (error) {
      console.error(&quot;Error deleting job:&quot;, error);
      alert(&quot;Failed to delete job. Please try again.&quot;);
    }
  };

  // ... rest of your component code
}
</code></pre>
<h2>Integrating Management Functions</h2>
<p>Update the <code>HomePage</code> component's return statement to incorporate these management functions:</p>
<pre><code class="language-tsx">return (
  &lt;div&gt;
    &lt;h1&gt;Job Portal&lt;/h1&gt;
    &lt;Show
      when={auth.user()}
      fallback={
        &lt;nav&gt;
          &lt;button onClick={() =&gt; navigate(&quot;/login&quot;)}&gt;Login&lt;/button&gt;
          &lt;button onClick={() =&gt; navigate(&quot;/register&quot;)}&gt;Register&lt;/button&gt;
        &lt;/nav&gt;
      }
    &gt;
      &lt;button onClick={auth.logout}&gt;Logout&lt;/button&gt;
      &lt;Show when={auth.user()?.email === &quot;admin@example.com&quot;}&gt;
        &lt;button onClick={() =&gt; { setModalContent(&quot;jobForm&quot;); setIsModalOpen(true); }}&gt;
          Add New Job
        &lt;/button&gt;
        &lt;button onClick={() =&gt; navigate(&quot;/applications&quot;)}&gt;Manage Applications&lt;/button&gt;
      &lt;/Show&gt;
    &lt;/Show&gt;
    &lt;Show when={jobs.loading}&gt;Loading jobs...&lt;/Show&gt;
    &lt;Show when={jobs.error}&gt;Error loading jobs: {jobs.error}&lt;/Show&gt;
    &lt;Show
      when={!jobs.error}
      fallback={&lt;div&gt;Error loading jobs: {jobs.error?.message}&lt;/div&gt;}
    &gt;
      &lt;JobList
        jobs={jobs() || []}
        onEdit={auth.user()?.email === &quot;admin@example.com&quot; ? openModal : undefined}
        onDelete={auth.user()?.email === &quot;admin@example.com&quot; ? deleteJob : undefined}
        onApply={auth.user()?.email !== &quot;admin@example.com&quot; ? applyForJob : undefined}
      /&gt;
    &lt;/Show&gt;
    &lt;Modal isOpen={isModalOpen()} onClose={() =&gt; setIsModalOpen(false)}&gt;
      &lt;Show when={modalContent() === &quot;jobForm&quot;}&gt;
        &lt;JobForm
          onSubmit={editingJob() ? updateJob : addJob}
          job={editingJob() as Job}
        /&gt;
      &lt;/Show&gt;
    &lt;/Modal&gt;
  &lt;/div&gt;
);
</code></pre>
<p>These functions will handle the respective actions when triggered by user interactions in the job list. Ensure your routes/index.tsx file contains this updated code.
In the <code>components</code> directory, create two new files for the <code>JobForm.tsx</code> and <code>Modal.tsx</code> components that were used in your <code>HomePage</code> component. Add the following code to your <code>components/JobForm.tsx</code> file:</p>
<pre><code class="language-jsx">import { createSignal } from &quot;solid-js&quot;;
import { Job } from &quot;../types&quot;;

interface JobFormProps {
  job?: Job;
  onSubmit: (job: Job) =&gt; void;
}

export default function JobForm(props: JobFormProps) {
  const [title, setTitle] = createSignal(props.job?.title || &quot;&quot;);
  const [description, setDescription] = createSignal(props.job?.description || &quot;&quot;);
  const [location, setLocation] = createSignal(props.job?.location || &quot;&quot;);
  const [type, setType] = createSignal(props.job?.type || &quot;Full-time&quot;);
  const [salary, setSalary] = createSignal(props.job?.salary || 0);

  const handleSubmit = (e: Event) =&gt; {
    e.preventDefault();
    props.onSubmit({
        id: props.job?.id,
        title: title(),
        description: description(),
        location: location(),
        type: type(),
        salary: salary(),
    });
  };

  return (
    &lt;form onSubmit={handleSubmit} class=&quot;job-form&quot;&gt;
      &lt;h2&gt;{props.job ? &quot;Edit Job&quot; : &quot;Add New Job&quot;}&lt;/h2&gt;
      &lt;input
        type=&quot;text&quot;
        placeholder=&quot;Job Title&quot;
        value={title()}
        onInput={(e) =&gt; setTitle(e.currentTarget.value)}
        required
      /&gt;
      &lt;textarea
        placeholder=&quot;Job Description&quot;
        value={description()}
        onInput={(e) =&gt; setDescription(e.currentTarget.value)}
        required
      /&gt;
      &lt;input
        type=&quot;text&quot;
        placeholder=&quot;Location&quot;
        value={location()}
        onInput={(e) =&gt; setLocation(e.currentTarget.value)}
        required
      /&gt;
      &lt;select value={type()} onChange={(e) =&gt; setType(e.currentTarget.value)}&gt;
        &lt;option value=&quot;Full-time&quot;&gt;Full-time&lt;/option&gt;
        &lt;option value=&quot;Part-time&quot;&gt;Part-time&lt;/option&gt;
        &lt;option value=&quot;Contract&quot;&gt;Contract&lt;/option&gt;
      &lt;/select&gt;
      &lt;input
        type=&quot;number&quot;
        placeholder=&quot;Salary&quot;
        value={salary()}
        onInput={(e) =&gt; setSalary(Number(e.currentTarget.value))}
        required
      /&gt;
      &lt;button type=&quot;submit&quot;&gt;{props.job ? &quot;Update Job&quot; : &quot;Add Job&quot;}&lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>Add the following code in your <code>components/Modal.tsx</code> file:</p>
<pre><code class="language-jsx">import { Show, JSX } from &quot;solid-js&quot;;
import &quot;./Modal.css&quot;;

interface ModalProps {
  isOpen: boolean;
  onClose: () =&gt; void;
  children: JSX.Element;
}

export default function Modal(props: ModalProps) {
  return (
    &lt;Show when={props.isOpen}&gt;
      &lt;div class=&quot;modal-overlay&quot; onClick={props.onClose}&gt;
        &lt;div class=&quot;modal-content&quot; onClick={(e) =&gt; e.stopPropagation()}&gt;
          &lt;button class=&quot;modal-close&quot; onClick={props.onClose}&gt;×&lt;/button&gt;
          {props.children}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Show&gt;
  );
}
</code></pre>
<p>Create a new file named <code>Modal.css</code> in your <code>components</code> directory and copy the CSS styles <a href="https://github.com/directus-labs/blog-example-job-board-solidstart/blob/main/src/components/Modal.css">here</a> to it.</p>
<p>Log in with admin credentials, and click on the <strong>Add New Job</strong> button to create a new job, you can also edit and delete a job by clicking on the edit and delete buttons respectively.</p>
<p><img src="https://marketing.directus.app/assets/84ef8af1-9669-49d3-af1f-1f5bc1ba6773" alt="Add new job modal" /></p>
<h2>Implementing Search and Filters</h2>
<p>Update the code in your <code>components/JobList.tsx</code> file:</p>
<pre><code class="language-jsx">import { For, Show, createMemo, createSignal } from &quot;solid-js&quot;;
import { Jobs, Job } from &quot;../types&quot;;

interface JobListProps {
  jobs: Jobs;
  onEdit?: (job: Job) =&gt; void;
  onDelete?: (id: number) =&gt; void;
  onApply?: (id: number) =&gt; void;
}

function JobList(props: JobListProps) {
  const [searchQuery, setSearchQuery] = createSignal(&quot;&quot;);
  const [jobType, setJobType] = createSignal(&quot;All&quot;);
  const [minSalary, setMinSalary] = createSignal(0);
  const [maxSalary, setMaxSalary] = createSignal(1000000);

  const filteredJobs = createMemo(() =&gt; {
    const query = searchQuery().toLowerCase();
    return props.jobs.filter(
      (job: Job) =&gt;
        (job.title.toLowerCase().includes(query) ||
          job.description.toLowerCase().includes(query) ||
          job.location.toLowerCase().includes(query)) &amp;&amp;
        (jobType() === &quot;All&quot; || job.type === jobType()) &amp;&amp;
        job.salary &gt;= minSalary() &amp;&amp;
        job.salary &lt;= maxSalary()
    );
  });

  return (
    &lt;div class=&quot;container&quot;&gt;
      &lt;input
        type=&quot;text&quot;
        class=&quot;search-input&quot;
        placeholder=&quot;Search jobs...&quot;
        onInput={(e) =&gt; setSearchQuery(e.currentTarget.value)}
        value={searchQuery()}
      /&gt;
      &lt;div class=&quot;filters&quot;&gt;
        &lt;select onChange={(e) =&gt; setJobType(e.currentTarget.value)}&gt;
          &lt;option value=&quot;All&quot;&gt;All Types&lt;/option&gt;
          &lt;option value=&quot;Full-time&quot;&gt;Full-time&lt;/option&gt;
          &lt;option value=&quot;Part-time&quot;&gt;Part-time&lt;/option&gt;
          &lt;option value=&quot;Contract&quot;&gt;Contract&lt;/option&gt;
        &lt;/select&gt;
        &lt;input
          type=&quot;number&quot;
          placeholder=&quot;Min Salary&quot;
          onInput={(e) =&gt; setMinSalary(parseInt(e.currentTarget.value) || 0)}
        /&gt;
        &lt;input
          type=&quot;number&quot;
          placeholder=&quot;Max Salary&quot;
          onInput={(e) =&gt; setMaxSalary(parseInt(e.currentTarget.value) || 1000000)}
        /&gt;
      &lt;/div&gt;
      &lt;ul class=&quot;job-list&quot;&gt;
        &lt;For each={filteredJobs()}&gt;
          {(job: Job) =&gt; (
            &lt;li class=&quot;job-list-item&quot;&gt;
              &lt;h3 class=&quot;job-title&quot;&gt;{job.title}&lt;/h3&gt;
              &lt;p class=&quot;job-description&quot;&gt;{job.description}&lt;/p&gt;
              &lt;p class=&quot;job-location&quot;&gt;Location: {job.location}&lt;/p&gt;
              &lt;p class=&quot;job-type&quot;&gt;Type: {job.type}&lt;/p&gt;
              &lt;p class=&quot;job-salary&quot;&gt;Salary: ${job.salary}&lt;/p&gt;
              &lt;Show when={props.onEdit}&gt;
                &lt;button onClick={() =&gt; props.onEdit!(job)}&gt;Edit&lt;/button&gt;
              &lt;/Show&gt;
              &lt;Show when={props.onDelete}&gt;
                &lt;button onClick={() =&gt; props.onDelete!(job.id as number)}&gt;
                  Delete
                &lt;/button&gt;
              &lt;/Show&gt;
              &lt;Show when={props.onApply}&gt;
                &lt;button onClick={() =&gt; props.onApply!(job.id as number)}&gt;
                  Apply
                &lt;/button&gt;
              &lt;/Show&gt;
            &lt;/li&gt;
          )}
        &lt;/For&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}

export default JobList;
</code></pre>
<p>The update implements the following features:</p>
<ul>
<li>Search functionality: Users can search jobs by title, description, or location using the search input.</li>
<li>Job type filtering: A dropdown allows users to filter jobs by type (Full-time, Part-time, Contract, or All).</li>
<li>Salary range filtering: Users can set minimum and maximum salary ranges.</li>
<li>Reactive filtering: The <code>createMemo</code> function creates a reactive filtered job list based on the search query and filter criteria.</li>
<li></li>
</ul>
<p><img src="https://marketing.directus.app/assets/cd0e80a4-2824-4855-9616-cd14b403ff10" alt="Job listing with search and filter" /></p>
<h2>Implementing Job Applications</h2>
<p>Create a new file named <code>ResumeForm.tsx</code> in your <code>components</code> directory and add the code snippets below for the Resume URL form inputs.</p>
<pre><code class="language-jsx">import { createSignal } from &quot;solid-js&quot;;

interface ResumeFormProps {
  onSubmit: (resumeUrl: string) =&gt; void;
}

function ResumeForm({ onSubmit }: ResumeFormProps) {
  const [resumeUrl, setResumeUrl] = createSignal(&quot;&quot;);

  const handleSubmit = (e: Event) =&gt; {
    e.preventDefault();
    onSubmit(resumeUrl());
  };

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      &lt;h2&gt;Submit Your Resume&lt;/h2&gt;
      &lt;div&gt;
        &lt;label for=&quot;resumeUrl&quot;&gt;Resume URL:&lt;/label&gt;
        &lt;input
          type=&quot;url&quot;
          id=&quot;resumeUrl&quot;
          value={resumeUrl()}
          onInput={(e) =&gt; setResumeUrl(e.currentTarget.value)}
          required
        /&gt;
      &lt;/div&gt;
      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;
    &lt;/form&gt;
  );
}

export default ResumeForm;
</code></pre>
<p>Update the code in your <code>src/routes/index.tsx</code> file to add the job application functionality:</p>
<pre><code class="language-jsx">+
//...  //... (your existing imports)
import ResumeForm from &quot;~/components/ResumeForm&quot;;

function HomePage() {
     //...(your existing state varribles
    const [modalContent, setModalContent] = createSignal&lt;&quot;jobForm&quot; | &quot;resumeForm&quot;&gt;(&quot;jobForm&quot;);
    const [applyingJobId, setApplyingJobId] = createSignal&lt;number | null&gt;(null);

     // ... (your existing code for fetchJobs, addJob, updateJob, deleteJob, and openModal)

    const applyForJob = async (jobId: number) =&gt; {
      if (!auth.user()) {
        alert(&quot;You need to login to apply for a job&quot;);
        return;
      }
      setApplyingJobId(jobId);
      setModalContent(&quot;resumeForm&quot;);
      setIsModalOpen(true);
    };

    const submitApplication = async (resumeUrl: string) =&gt; {
      try {
        if (!auth.user() || !applyingJobId()) {
          throw new Error(&quot;You need to login to apply for a job&quot;);
        }
        const newApplication = {
          job: applyingJobId(),
          user: auth.user()?.id as unknown as string,
          status: &quot;pending&quot;,
          resumeUrl: resumeUrl,
        };

        await directus.request(createItem(&quot;application&quot;, newApplication));
        alert(&quot;Application submitted successfully!&quot;);
        setIsModalOpen(false);
        setApplyingJobId(null);
      } catch (error) {
        alert(&quot;Failed to apply for job. Please try again.&quot;);
      }
    };

    return (
      &lt;div&gt;
        &lt;h1&gt;Job Portal&lt;/h1&gt;
        {/* ... (your existing code for authentication buttons) */}
        &lt;Show when={jobs.loading}&gt;Loading jobs...&lt;/Show&gt;
        &lt;Show when={jobs.error}&gt;Error loading jobs: {jobs.error}&lt;/Show&gt;
        &lt;Show
          when={!jobs.error}
          fallback={&lt;div&gt;Error loading jobs: {jobs.error?.message}&lt;/div&gt;}
        &gt;
          &lt;JobList
            jobs={jobs() || []}
            onEdit={auth.user()?.email === &quot;admin@example.com&quot; ? openModal : undefined}
            onDelete={auth.user()?.email === &quot;admin@example.com&quot; ? deleteJob : undefined}
            onApply={auth.user()?.email !== &quot;admin@example.com&quot; ? applyForJob : undefined}
          /&gt;
        &lt;/Show&gt;
        &lt;Modal isOpen={isModalOpen()} onClose={() =&gt; setIsModalOpen(false)}&gt;
          &lt;Show when={modalContent() === &quot;jobForm&quot;}&gt;
            &lt;JobForm
              onSubmit={editingJob() ? updateJob : addJob}
              job={editingJob() as Job}
            /&gt;
          &lt;/Show&gt;
          &lt;Show when={modalContent() === &quot;resumeForm&quot;}&gt;
            &lt;ResumeForm onSubmit={submitApplication} /&gt;
          &lt;/Show&gt;
        &lt;/Modal&gt;
      &lt;/div&gt;
    );
  }

export default HomePage;
</code></pre>
<p>Register as an applicant, click on the Apply button to show the Resume URL modal, enter a Resume URL, and click on <strong>Submit</strong> to apply for a job.</p>
<p><img src="https://marketing.directus.app/assets/7d6a9b0d-b411-4f9c-af6e-931ab850a9e3" alt="job application functionality" /></p>
<h2>Managing Applicant Profiles and Resumes</h2>
<p>To allow the admin to view, accept, or decline job applications, create a new file named <code>applications.tsx</code> in your <code>src/routes</code> directory and add the following code:</p>
<pre><code class="language-jsx">import { createSignal, createEffect, For, Show } from &quot;solid-js&quot;;
import { readItems, updateItem } from &quot;@directus/sdk&quot;;
import { useAuth } from &quot;../context/AuthContext&quot;;
import { Application, Job } from &quot;../types&quot;;
import getDirectusInstance from &quot;~/lib/directus&quot;;

const ManageApplicationsPage = () =&gt; {
  const directus  = getDirectusInstance();

  const [applications, setApplications] = createSignal&lt;Application[]&gt;([]);
  const [jobs, setJobs] = createSignal&lt;Job[]&gt;([]);
  const [selectedApplication, setSelectedApplication] =
    createSignal&lt;Application | null&gt;(null);
  const auth = useAuth();

  const fetchApplications = async () =&gt; {
    try {
      const fetchedApplications = await directus.request(
        readItems(&quot;application&quot;, {
          sort: [&quot;-date_created&quot;],
          deep: {
            userId: {
              fields: [&quot;first_name&quot;, &quot;last_name&quot;],
            },
            jobId: {
              fields: [&quot;title&quot;],
            },
          },
          fields: [&quot;*&quot;, &quot;userId.first_name&quot;, &quot;userId.last_name&quot;, &quot;jobId.title&quot;],
        })
      );
      setApplications(fetchedApplications as Application[]);
    } catch (error) {
      console.error(&quot;Error fetching applications:&quot;, error);
    }
  };

  const fetchJobs = async () =&gt; {
    try {
      const fetchedJobs = await directus.request(readItems(&quot;job&quot;));
      setJobs(fetchedJobs as Job[]);
    } catch (error) {
      console.error(&quot;Error fetching jobs:&quot;, error);
    }
  };

  createEffect(() =&gt; {
    fetchApplications();
    fetchJobs();
  });

  const updateApplicationStatus = async (id: number, status: string) =&gt; {
    try {
      await directus.request(updateItem(&quot;application&quot;, id, { status }));
      fetchApplications();
    } catch (error) {
      console.error(&quot;Error updating application status:&quot;, error);
    }
  };

  return (
    &lt;div class=&quot;container mx-auto p-4&quot;&gt;
      &lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;
        &lt;div&gt;
          &lt;h2 class=&quot;text-xl font-semibold mb-2&quot;&gt;Applications List&lt;/h2&gt;
          &lt;For each={applications()}&gt;
            {(application) =&gt; (
              &lt;div
                class=&quot;border p-2 mb-2 cursor-pointer hover:bg-gray-100&quot;
                onClick={() =&gt; setSelectedApplication(application)}
              &gt;
                &lt;p&gt;Job: {application.job.title}&lt;/p&gt;
                &lt;p&gt;Applicant: {application.user.first_name} {application.user.last_name}&lt;/p&gt;
                &lt;p&gt;Status: {application.status}&lt;/p&gt;
              &lt;/div&gt;
            )}
          &lt;/For&gt;
        &lt;/div&gt;
        &lt;Show when={selectedApplication()}&gt;
          &lt;div class=&quot;border p-4&quot;&gt;
            &lt;h2 class=&quot;text-xl font-semibold mb-2&quot;&gt;Application Details&lt;/h2&gt;
            &lt;p&gt;Job: {selectedApplication()?.job.title}&lt;/p&gt;
            &lt;p&gt;Applicant: {selectedApplication()?.user.first_name} {selectedApplication()?.user.last_name}&lt;/p&gt;
            &lt;p&gt;Status: {selectedApplication()?.status}&lt;/p&gt;
            &lt;p&gt;
              Resume:
              &lt;a
                href={selectedApplication()?.resumeUrl}
                target=&quot;_blank&quot;
                rel=&quot;noopener noreferrer&quot;
              &gt;
                View Resume
              &lt;/a&gt;
            &lt;/p&gt;
            &lt;div class=&quot;mt-4&quot;&gt;
              &lt;button
                class=&quot;bg-green-500 text-white px-4 py-2 mr-2&quot;
                onClick={() =&gt;
                  updateApplicationStatus(selectedApplication()!.id, &quot;accepted&quot;)
                }
              &gt;
                Accept
              &lt;/button&gt;
              &lt;button
                class=&quot;bg-red-500 text-white px-4 py-2&quot;
                onClick={() =&gt;
                  updateApplicationStatus(selectedApplication()!.id, &quot;rejected&quot;)
                }
              &gt;
                Reject
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/Show&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default ManageApplicationsPage;
</code></pre>
<p>Here, we implemented the following:</p>
<ul>
<li><code>fetchApplications()</code>: Retrieves all job applications from the Directus backend, including related user and job information.</li>
<li><code>fetchJobs()</code>: Fetches all available jobs from the Directus backend.</li>
<li><code>updateApplicationStatus()</code>: Updates the status of a job application (accepted or rejected) in the Directus backend.</li>
<li><code>ManageApplicationsPage</code> component: Renders the application management interface, including:
<ul>
<li>A list of all applications</li>
<li>Detailed view of a selected application</li>
<li>Buttons to accept or reject the selected application
Click on the <strong>Manage Applications</strong> button to navigate to the application's route.
<img src="https://marketing.directus.app/assets/35696744-df91-450a-a438-053d7ee34805" alt="Job applications management" /></li>
</ul>
</li>
</ul>
<h2>Summary</h2>
<p>In this tutorial, you’ve learned how to build a job portal with Directus and SolidStart.js, dynamically create, read, update, and delete jobs and applications, and successfully build a job portal application with Directus for the backend and SolidStart.js for the frontend.</p>]]></content>
        <author>
            <name>Precious Ndoma</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tracking GitHub Issues and Pull Requests with Directus Automate]]></title>
        <id>https://docs.directus.io/blog/tracking-github-issues-and-pull-requests-with-directus-automate</id>
        <link href="https://docs.directus.io/blog/tracking-github-issues-and-pull-requests-with-directus-automate"/>
        <updated>2024-07-18T09:43:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to use Directus Automate to keep track of new issues, pull requests, and comments in a GitHub repository.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, you will learn how to track events, such as new issues and pull requests, in a GitHub repository using Directus Automate. By the end of this article, you will have created a repository tracker that sends alerts via email and in-app notifications.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li>A Directus project - Create a project by following this <a href="https://docs.directus.io/getting-started/quickstart.html">quickstart guide</a>. Your project will need to be accessible via a public URL so you'll need to host it or use a tunnel to your local instance.</li>
<li><a href="https://github.com/signup">A GitHub Account</a> and admin access to the GitHub repo you want to track.</li>
</ul>
<h2>Creating a New Flow</h2>
<p>In your Directus project, navigate to <strong>Settings -&gt; Flows</strong> and create a Flow. In the Trigger Setup section, select <strong>Webhook Trigger</strong> so we can receive events from GitHub. Ensure the Webhook Trigger is set to use the <code>POST</code> method with Asynchronous mode enabled. Save the Flow and copy the Trigger URL.</p>
<p><img src="https://marketing.directus.app/assets/0c83e7db-5b39-4d06-a850-23e06a5ccd12" alt="A screenshot of the Flow demonstrating where to find the Trigger URL" /></p>
<h2>Setting Up a Webhook on GitHub</h2>
<p>GitHub allows developers to create webhooks for both repositories and organizations. A webhook is a web request to an external URL (in this case, Directus) that will be immediately triggered when an event happens such as a new issue being opened in a repository.</p>
<p>To create a webhook, navigate to the repository or organization you want to track. Go to the <strong>Settings</strong> tab and select <strong>Webhooks</strong> from the menu.</p>
<p>Click on the <strong>Add Webhook</strong> button. Next, paste the Trigger URL from Directus flow into the <code>Payload URL</code> field and change the content type to <code>application/json</code>.</p>
<p>Select the events that should trigger the webhook. For this tutorial, choose the <code>Let me select individual events</code> option and toggle these events:</p>
<ul>
<li>Issues</li>
<li>Issue comments</li>
<li>Pull requests</li>
</ul>
<p>Save your settings by clicking the <strong>Add Webhook</strong> button. GitHub will send a ping payload to your Directus project. Confirm the webhook is successful by checking the Activity Logs section in your Flow.</p>
<p><img src="https://marketing.directus.app/assets/bbf7a68e-3e4b-4e73-982c-0de98b14a82f" alt="A screenshot of Activity Logs section in the Flow editor" /></p>
<h1>Creating the Alert System</h1>
<p>The final step is to set up an alert system to notify you or others on the project when any of the selected events occur. Depending on your preference, you can use email or in-app notifications available as operations in Flows.</p>
<p>Since there are three different events, creating a one-size-fits-all notification template won't be ideal. Instead, we can generate a separate notification template for each event by setting up <strong>Condition</strong> operations to check if the trigger event is an issue, issue comment, or pull request.</p>
<h3>Setting Up Condition Operations</h3>
<p>Create a condition operation with the following rules:</p>
<pre><code class="language-json">{
  &quot;$trigger&quot;: {
    &quot;headers&quot;: {
      &quot;x-github-event&quot;: {
        &quot;_eq&quot;: &quot;issues&quot;
      }
    }
  }
}
</code></pre>
<p>Add another condition in the <a href="https://docs.directus.io/app/flows.html">failure path</a> of the first condition to check if the event is an issue comment. The setup is the same as the above condition, with the change being <code>&quot;_eq&quot;: &quot;issue_comment&quot;</code>. You should see something like this:</p>
<p><img src="https://marketing.directus.app/assets/3cf611ea-68ac-426d-bc2c-b5fc7fa4b321" alt="Screenshot showing the flow with the two conditions we have created" /></p>
<h3>Generate Notification Template</h3>
<p>The next step is to create a custom notification template for each event, generating dynamic content specific to each one.</p>
<p>For Issues:</p>
<pre><code class="language-md">Hi,

I want to bring to your attention an issue that has been {{$trigger.body.action}} on the GitHub repository.

| Issue title         | {{$trigger.body.issue.title}}         |
| :------------------ | :------------------------------------ |
| Description         | {{$trigger.body.issue.body}}          |
| Author's name       | {{$trigger.body.issue.user.login}}    |
| Author's GitHub URL | {{$trigger.body.issue.user.html_url}} |

You can view the full issue and its details by following this link: [Link to the issue]({{$trigger.body.issue.html_url}}).

Thank you!
</code></pre>
<p>For issue comments:</p>
<pre><code class="language-md">Hi,

Issue #{{$trigger.body.issue.number}} has a new comment by [{{$trigger.body.comment.user.login}}]({{$trigger.body.comment.user.html_url}})

| Issue title | {{$trigger.body.issue.title}}  |
| :---------- | :----------------------------- |
| Comment     | {{$trigger.body.comment.body}} |

You can view the issue and join the conversation: [Link to the issue]({{$trigger.body.issue.html_url}}).

Thank you!
</code></pre>
<p>For pull requests:</p>
<pre><code class="language-md">Hi,

I want to bring to your attention a pull request that has been {{$trigger.body.action}} on the GitHub repository.

| Pull Request Title  | {{$trigger.body.pull_request.title}}         |
| :------------------ | :------------------------------------------- |
| Description         | {{$trigger.body.pull_request.body}}          |
| Author's name       | {{$trigger.body.pull_request.user.login}}    |
| Author's GitHub URL | {{$trigger.body.pull_request.user.html_url}} |

You can view the pull request and its details by following this link: [Link to pull request]({{$trigger.body.pull_request.html_url}}).

Thank you!
</code></pre>
<h3>Creating Email and In-App Notification Operations</h3>
<p>You will need the generated template and <code>User ID</code> if you are creating <strong>Send Notification</strong> operation, or email address if you are using the <strong>Send Email</strong> operation. The result should look like this:</p>
<p><img src="https://marketing.directus.app/assets/02ef50c1-86bb-4e0b-b8e8-4a93976366ce" alt="Screenshot of the final flow setup" /></p>
<h2>Test the Application</h2>
<p>Voilà! You've completed setting up the flow. Test the repo tracker by creating an issue.</p>
<p><img src="https://marketing.directus.app/assets/ad8817a1-a26f-4faa-8199-0407edd9dd09" alt="Screenshot of the email" /></p>
<h2>Summary</h2>
<p>In this tutorial, you've learned how to track a GitHub repository in your Directus application. Join our <a href="https://directus.chat/">Discord server</a> to learn more about using Directus.</p>]]></content>
        <author>
            <name>Erinle Samuel</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Implementing Pagination and Infinite Scrolling in Next.js]]></title>
        <id>https://docs.directus.io/blog/implementing-pagination-and-infinite-scrolling-in-next-js</id>
        <link href="https://docs.directus.io/blog/implementing-pagination-and-infinite-scrolling-in-next-js"/>
        <updated>2024-07-15T08:49:14.000Z</updated>
        <summary type="html"><![CDATA[Learn how to implement pagination and Infinite scrolling with Directus in Next.js, and about important factors that will help you make the right choice for your project.]]></summary>
        <content type="html"><![CDATA[<p>When working with a large set of data fetching and rendering them at once can cause performance issues for your application, especially for devices with poor internet connections. To prevent this, several techniques are used to fetch and render data in chunks. Two of the most common ones are pagination and infinite scrolling.</p>
<p>In this tutorial, you will learn how to implement pagination and infinite scrolling in Next.js with the Directus SDK, and understand benefits and drawbacks of both approaches.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li>A Directus project - follow the <a href="https://docs.directus.io/getting-started/quickstart.html">quickstart</a> guide to create a project if you don’t have one already.</li>
<li>Knowledge of Next.js.</li>
</ul>
<h2>Adding Data to Directus</h2>
<p>You will need some data to work with to implement the pagination and infinite scrolling. In your Directus project, navigate to <strong>Settings -&gt; Data Model</strong> and create a new collection called <code>posts</code> with a text input called <code>title</code> and a textarea field called <code>body</code>.</p>
<p>Navigate to the <strong>Content Module</strong> and add at least 10 items to the <strong>Posts</strong> collection. You can get sample data from the <a href="https://jsonplaceholder.typicode.com/posts">JSONPlaceholder posts resource</a>.</p>
<p>To make the collection publicly accessible, navigate to <strong>Settings -&gt; Access Control -&gt; Public</strong> and give Read access to the <code>posts</code> collection.</p>
<h2>Setting Up a Next.js Project</h2>
<p>Open your terminal, and enter the following command to create a new Next.js project, install dependencies, and run a development server:</p>
<pre><code class="language-shell">npx create-next-app@latest
✔ What is your project named? next-directus-app
✔ Would you like to use TypeScript? No
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like to use `src/` directory? No
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to customize the default import alias (@/*)? No

cd next-directus-app
npm install @directus/sdk
npm run dev
</code></pre>
<p>In the root directory of the project, create a new directory called <code>lib</code>. Inside it create a <code>directus.js</code> file and include the following lines of code to initialize Directus and disable the default caching behavior of the Next.js <a href="https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options">fetch()</a> function:</p>
<pre><code class="language-js">import { createDirectus, rest } from &quot;@directus/sdk&quot;;
const directus = createDirectus(&quot;&lt;your-directus-project-url&gt;&quot;).with(
  rest({
    onRequest: (options) =&gt; ({ ...options, cache: &quot;no-store&quot; }),
  })
);
export default directus;
</code></pre>
<p>Make sure to modify <code>&lt;your-directus-project-url&gt;</code> with your project's URL.</p>
<h2>Fetching Data in Chunks</h2>
<p>To fetch data in chunks using an API, usually, two parameters are required. The first is a <code>limit</code> that determines the maximum number of items returned, and the second is a parameter that determines the starting point of the items to be fetched, it is usually associated with a type of pagination (e.g. cursor-based pagination, offset-based, pagination, etc.).</p>
<p>In Directus the second parameter can be either <a href="https://docs.directus.io/reference/query.html#offset">offset</a> and <a href="https://docs.directus.io/reference/query.html#page">page</a> which are associated with offset-based and page-based pagination respectively.</p>
<ul>
<li>Offset-based pagination: This deals with specifying a value (or offset) that indicates the number of items to skip or where the items being fetched should start from. It works hand-in-hand with the <code>limit</code> parameter to return a fixed number of items. For example, for a dataset of 200 items, if <code>offset=20</code> and <code>limit=10</code>, items 21-30 will be returned.</li>
<li>Page-based pagination: This is an abstraction of the offset-based pagination where a page number is specified (e.g. 1, 2, 3) which will be used under the hood to calculate the offset.</li>
</ul>
<p>For this tutorial, the <code>page</code> parameter will be used to implement both the pagination and infinite scrolling.</p>
<h2>Implementing Pagination</h2>
<p>Modify the <code>app/page.js</code> file to the following:</p>
<pre><code class="language-js">import directus from &quot;@/lib/directus&quot;;
import { readItems } from &quot;@directus/sdk&quot;;

const getPosts = async () =&gt; {
  return directus.request(readItems(&quot;posts&quot;));
};

export default async function Home() {
  const posts = await getPosts();

  return (
    &lt;div&gt;
      &lt;ul&gt;
        {posts.map((post) =&gt; {
          return (
            &lt;li key={post.id}&gt;
              &lt;h2&gt;{post.title}&lt;/h2&gt;
              &lt;p&gt;{post.body}&lt;/p&gt;
            &lt;/li&gt;
          );
        })}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>This is a Server Component that will fetch all your posts from Directus using the <code>getPost()</code> function and render HTML in the server which will then be sent to the client.
If you navigate to <code>http://localhost:3000</code> in your browser you should see all your posts:</p>
<p><img src="https://marketing.directus.app/assets/4aa3f89f-7f3f-4bae-b9d8-919935c0f300" alt="All items from the posts collection in your Directus project" /></p>
<p>To add pagination, first, modify the <code>app/page.js</code> file to the following:</p>
<pre><code class="language-js">import directus from &quot;@/lib/directus&quot;;
import { readItems } from &quot;@directus/sdk&quot;;

const getPosts = async (limit, page) =&gt; {
  return directus.request(
    readItems(&quot;posts&quot;, {
      limit,
      page,
    })
  );
};

export default async function Home({ searchParams }) {
  const LIMIT = 4;
  const currentPage = parseInt(searchParams.page) || 1;
  const posts = await getPosts(LIMIT, currentPage);
  return (
    &lt;div&gt;
      &lt;ul&gt;
        {posts.map((post) =&gt; {
          return (
            &lt;li key={post.id}&gt;
              &lt;h2&gt;{post.title}&lt;/h2&gt;
              &lt;p&gt;{post.body}&lt;/p&gt;
            &lt;/li&gt;
          );
        })}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>The <code>getPosts()</code> function will now return a maximum of 4 posts at a time. The set of posts returned is based on the page number passed to it which is gotten from the URL as a query parameter and is defaulted to 1. So currently the first 4 posts will be rendered and when the page number is increased the next set of posts will be rendered.</p>
<p>To be able to add and modify the page query parameter from the UI for navigating between pages, in the root directory, create a <code>components</code> directory, inside it, create a <code>Pagination.js</code> file, and add the following lines of code:</p>
<pre><code class="language-js">import directus from &quot;@/lib/directus&quot;;
import { aggregate } from &quot;@directus/sdk&quot;;
import Link from &quot;next/link&quot;;

const getTotalPostCount = async () =&gt; {
  const totalCount = await directus.request(
    aggregate(&quot;posts&quot;, {
      aggregate: { count: &quot;*&quot; },
    })
  );
  return totalCount[0].count;
};

async function Pagination({ limit, currentPage }) {
  const totalPostCount = await getTotalPostCount();
  const totalPages = Math.ceil(totalPostCount / limit);

  const hasMorePage = () =&gt; {
    const recievedPostsCount = limit * currentPage;
    return recievedPostsCount &lt; totalPostCount;
  };

  return (
    &lt;div&gt;
      &lt;Link href={currentPage &lt;= 2 ? &quot;/&quot; : `?page=${currentPage - 1}`}&gt;
        &amp;laquo; Previous
      &lt;/Link&gt;
      {Array.from(Array(totalPages), (_, i) =&gt; i + 1).map((page) =&gt; (
        &lt;Link
          key={page}
          href={page === 1 ? &quot;/&quot; : `?page=${page}`}
          className={page === currentPage ? &quot;active&quot; : &quot;&quot;}
        &gt;
          {page}
        &lt;/Link&gt;
      ))}
      &lt;Link
        href={
          hasMorePage() ? `?page=${currentPage + 1}` : `?page=${currentPage}`
        }
      &gt;
        Next &amp;raquo;
      &lt;/Link&gt;
    &lt;/div&gt;
  );
}

export default Pagination;
</code></pre>
<p>Here the Directus <code>aggregate()</code> function is used to calculate and return the total number of posts. The result is used to calculate the total number of pages and create a <code>hasMorePage()</code> function which will check if there are more pages.
In the return statement, we are rendering the pagination nav elements to be used to add and modify the page query parameter which includes a previous button, numeric links rendered using the total number of pages, and a next button that uses the <code>hasMorePage()</code> function to prevent further navigation.</p>
<p>To render to <code>Pagination</code> component, In the <code>app/page.js</code> file, add the following import:</p>
<pre><code class="language-js">import Pagination from &quot;@/components/Pagination&quot;;
</code></pre>
<p>Then, add the following line of code after the close <code>&lt;/ul&gt;</code> tag:</p>
<pre><code class="language-js">&lt;Pagination limit={LIMIT} currentPage={currentPage} /&gt;
</code></pre>
<p>Here is what the <code>app/page.js</code> file, should now look like:</p>
<pre><code class="language-js">import Pagination from &quot;@/components/Pagination&quot;;
import directus from &quot;@/lib/directus&quot;;
import { readItems } from &quot;@directus/sdk&quot;;

const getPosts = async (limit, page) =&gt; {
  return directus.request(
    readItems(&quot;posts&quot;, {
      limit,
      page,
    })
  );
};

export default async function Home({ searchParams }) {
  const LIMIT = 4;
  const currentPage = parseInt(searchParams.page) || 1;
  const posts = await getPosts(LIMIT, currentPage);
  return (
    &lt;div&gt;
      &lt;ul&gt;
        {posts.map((post) =&gt; {
          return (
            &lt;li key={post.id}&gt;
              &lt;h2&gt;{post.title}&lt;/h2&gt;
              &lt;p&gt;{post.body}&lt;/p&gt;
            &lt;/li&gt;
          );
        })}
      &lt;/ul&gt;
      &lt;Pagination limit={LIMIT} currentPage={currentPage} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>With this, you can now navigate between pages from the UI:</p>
<p><img src="https://marketing.directus.app/assets/6706b05a-2581-46e8-98db-748669d757c0" alt="Demonstrates how the pagination functionality works by navigating between different pages using the pagination navs at the bottom of the page" /></p>
<h2>Implementing Infinite Scrolling</h2>
<p>Infinite scrolling requires browser events or the Intersection observer JavaScript API to be implemented so it needs to be done on the client side but one way we can improve this is to load the initial HTML from the server. For that, we will be using <a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">Serve Actions</a>.</p>
<p>In the root directory, create a <code>components</code> directory, inside it, create a <code>PostList.js</code> file and add the following lines of code:</p>
<pre><code class="language-js">&quot;use client&quot;;

import { useState } from &quot;react&quot;;

const PostList = ({ initialPosts, getPosts, limit, totalPostCount }) =&gt; {
  const [posts, setPosts] = useState(initialPosts);

  return (
    &lt;&gt;
      &lt;ul className=&quot;max-w-[600px] mx-auto grid gap-5 pt-10&quot;&gt;
        {posts?.map((post) =&gt; (
          &lt;li key={post.id} className=&quot;p-5 rounded-md bg-gray-200 text-black&quot;&gt;
            &lt;h2 className=&quot;uppercase text-lg font-medium&quot;&gt;{post.title}&lt;/h2&gt;
            &lt;p&gt;{post.body}&lt;/p&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
      &lt;span&gt;No more posts&lt;/span&gt;
    &lt;/&gt;
  );
};

export default PostList;
</code></pre>
<p>This component will receive the initial posts and render them.
Modify the <code>app/page.js</code> file to the following:</p>
<pre><code class="language-js">import PostList from &quot;@/components/PostList&quot;;
import directus from &quot;@/lib/directus&quot;;
import { readItems } from &quot;@directus/sdk&quot;;

const getPosts = async () =&gt; {
  &quot;use server&quot;;
  return await directus.request(readItems(&quot;posts&quot;));
};

export default async function Home() {
  const initialPosts = await getPosts();

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;PostList initialPosts={initialPosts} /&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>Here a Server Action is created which is used to fetch all posts and pass the result as props to the <code>PostList</code> component. If you navigate to <code>http://localhost:3000</code> in your browser you should see all your posts:</p>
<p><img src="https://marketing.directus.app/assets/7bccd305-bb0f-408d-ab28-6a90629e9c4e" alt="All items from the posts collection in your Directus project" /></p>
<p>To add infinite scrolling, first, modify the <code>app/page.js</code> file to the following:</p>
<pre><code class="language-js">import PostList from &quot;@/components/PostList&quot;;
import directus from &quot;@/lib/directus&quot;;
import { aggregate, readItems } from &quot;@directus/sdk&quot;;
const getPosts = async (page, limit) =&gt; {
  &quot;use server&quot;;
  return await directus.request(
    readItems(&quot;posts&quot;, {
      limit,
      page,
    })
  );
};
const getTotalPostCount = async () =&gt; {
  const totalCount = await directus.request(
    aggregate(&quot;posts&quot;, {
      aggregate: { count: &quot;*&quot; },
    })
  );
  return totalCount[0].count;
};
export default async function Home() {
  const LIMIT = 6;
  const initialPosts = await getPosts(1, LIMIT);
  const totalPostCount = await getTotalPostCount();
  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;PostList
          getPosts={getPosts}
          limit={LIMIT}
          initialPosts={initialPosts}
          totalPostCount={totalPostCount}
        /&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>The <code>getPosts()</code> function will now return a maximum of 6 posts at a time. The set of posts returned is based on the page number passed to it. Increasing the page number will get the next set of posts.</p>
<p>Using the Directus <code>aggregate()</code> function, a <code>getTotalPostCount()</code> function was created to calculate and get the total number of posts. The result of the function along with the limit and <code>getPosts()</code> is then passed to the <code>PostList</code> component where they will be used.</p>
<p>Modify the <code>components/PostList.js</code> file to the following:</p>
<pre><code class="language-js">&quot;use client&quot;;

import { useState } from &quot;react&quot;;

const PostList = ({ initialPosts, getPosts, limit, totalPostCount }) =&gt; {
  const [currentPage, setCurrentPage] = useState(1);
  const [posts, setPosts] = useState(initialPosts);

  const hasMorePosts = () =&gt; {
    const recievedCount = limit * currentPage;
    return recievedCount &lt; totalPostCount;
  };

  return (
    &lt;&gt;
      &lt;ul className=&quot;max-w-[600px] mx-auto grid gap-5 pt-10&quot;&gt;
        {posts?.map((post) =&gt; (
          &lt;li key={post.id} className=&quot;p-5 rounded-md bg-gray-200 text-black&quot;&gt;
            &lt;h2 className=&quot;uppercase text-lg font-medium&quot;&gt;{post.title}&lt;/h2&gt;
            &lt;p&gt;{post.body}&lt;/p&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
      {hasMorePosts() ? (
        &lt;span className=&quot;text-center block py-10&quot;&gt;Loading...&lt;/span&gt;
      ) : (
        &lt;span className=&quot;text-center block p-10&quot;&gt;No more posts&lt;/span&gt;
      )}
    &lt;/&gt;
  );
};

export default PostList;
</code></pre>
<p>Here the total number of posts passed as props is now used to create a <code>hasMorePosts()</code> function to check if there are more posts. The <code>hasMorePosts()</code> is then used to conditionally render a loading indicator.
To start fetching subsequent posts the Intersection Observer API will be used to observe the loading indicator, and when it enters the viewport more posts will be fetched.
Add the following import to the <code>components/PostList.js</code> file:</p>
<pre><code class="language-js">import { useRef, useEffect } from &quot;react&quot;;
</code></pre>
<p>Call the <code>useRef()</code> hook by adding the following line of code after the <code>posts</code> state:</p>
<pre><code class="language-js">const observerElem = useRef(null);
</code></pre>
<p>To access the loading indicator element using the ref object, modify the <code>&lt;span&gt;</code> tag that display’s the loading indicator to the following:</p>
<pre><code class="language-js">&lt;span className=&quot;text-center block py-10&quot; ref={observerElem}&gt;
</code></pre>
<p>Finally, to start fetching new posts when the loading indicator enters the view post, add the following line code after the <code>observerElem</code> ref object:</p>
<pre><code class="language-js">useEffect(() =&gt; {
  if (typeof window === &quot;undefined&quot; || !window.IntersectionObserver) return;
  const element = observerElem.current;
  const option = { threshold: 0 };

  const observer = new IntersectionObserver(handleObserver, option);
  if (element) observer.observe(element);
  return () =&gt; {  if (element) observer.unobserve(element) };
}, [currentPage]);

const fetchMorePosts = async () =&gt; {
  const nextPage = currentPage + 1;
  const fetchedPosts = await getPosts(nextPage, limit);
  setPosts((prevPosts) =&gt; [...prevPosts, ...fetchedPosts]);
  setCurrentPage(nextPage);
};

const handleObserver = (entries) =&gt; {
  const [target] = entries;
  if (target.isIntersecting &amp;&amp; hasMorePosts()) {
    fetchMorePosts();
  }
};
</code></pre>
<p>Here in the <code>useEffect</code> hook the Intersection Observer API is used to observe the <code>&lt;span&gt;</code> element of the loading indicator. At initial mount and when the <code>&lt;span&gt;</code> enters and exists the viewport, the <code>handleObserver()</code> callback function is called which will then call <code>fetchMorePosts()</code> to fetch new posts and update the state whenever there are more posts and the <code>&lt;span&gt;</code> element enters the viewport.</p>
<p>With this, the infinite scrolling should now be working:
<img src="https://marketing.directus.app/assets/bb16cb6b-594c-4d28-8e18-5e3394826d3c" alt="Demonstrates the infinte scrolling functionality. When the page is scrolled down to the bottom, the loading indicator is displayed, and new posts are displayed afterwards. Finally, when the bottom of the page is reached again the text No more posts is displayed." /></p>
<h2>Which To Use</h2>
<p>As with most topics, the answer is &quot;it depends&quot;. Paginated lists tend to have better performance Search Engine Optimization (SEO) as all page content (typically smaller) is initially loaded on the page. Infinite scrolling is better at maintaining attention and exploration</p>
<p>You can read more on how to implement SEO-friendly pagination and infinite scrolling in the <a href="https://developers.google.com/search/blog/2014/02/infinite-scroll-search-friendly">Google Search Central Blog</a>.</p>
<h2>Summary</h2>
<p>With this tutorial, you’ve learned how to implement pagination and infinite scrolling with Directus in Next.js, and also about important factors that will help you make the choice between infinite scrolling and pagination for your app.</p>]]></content>
        <author>
            <name>Taminoturoko Briggs</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Notebook Chrome Extension with Directus]]></title>
        <id>https://docs.directus.io/blog/building-a-notebook-chrome-extension-with-directus</id>
        <link href="https://docs.directus.io/blog/building-a-notebook-chrome-extension-with-directus"/>
        <updated>2024-07-11T08:49:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to build WebNote Keeper: A Chrome extension for saving webpage notes using Directus CMS. Capture URLs, add, edit, and delete notes easily.]]></summary>
        <content type="html"><![CDATA[<p>This article will guide you through building a Chrome extension using Vite and Directus. The extension will leverage Directus as the backend to store and manage data.</p>
<p>When a user clicks the extension while browsing a webpage, it will automatically capture the URL of the current webpage and prompt the user to add a <code>note</code>. Users will be able to <code>view</code>, <code>edit</code>, and <code>delete</code> their notes directly from the extension. This tutorial will not cover styling the extension, but light styling has been applied to the screenshots shown.</p>
<p>Before you start, you will need a Directus project. Follow the <a href="https://docs.directus.io/getting-started/quickstart">Quickstart guide</a> to create one if needed.</p>
<h2>Set Up Your Directus Project</h2>
<h3>Create Notes Collection</h3>
<p>Create a new collection called <code>notes</code> with all optional fields enabled. Create the following additional fields:</p>
<ul>
<li><code>website</code> - new field with type <code>string</code></li>
<li><code>note</code> - new field with type <code>string</code></li>
</ul>
<p><img src="https://marketing.directus.app/assets/6f0fbee4-d0dd-4b57-8e2f-2ee908585289" alt="Notes Collection showing all optional fields and the website and note custom fields" /></p>
<h3>Set Up Roles</h3>
<p>In order to create new note, edit note or delete note on behalf of user, we need to create separate role. Create a <code>customer</code> role for new users. In the access control settings for the role, allow creation on the <code>notes</code> collection and custom read, edit, and delete permissions: <code>user_created equals $CURRENT_USER</code>.</p>
<p><img src="https://marketing.directus.app/assets/2792c994-cced-4683-98db-544ccbf14620" alt="Customer Role Permissions" /></p>
<p><img src="https://marketing.directus.app/assets/e54c9767-32fb-4571-90d0-01a82e9de008" alt="Filter Notes" /></p>
<h1>Initialize Extension</h1>
<p>Open your terminal and run the following commands to create a new project, install dependencies, and run the project:</p>
<pre><code class="language-bash">npm create vite@latest directus-webnote-keeper -- --template vue
cd directus-webnote-keeper
npm install
npm install @directus/sdk js-cookie vue-router
npm run dev
</code></pre>
<p>Add a <code>manifest.json</code> file in root directory of the project:</p>
<pre><code class="language-json">{
    &quot;name&quot;: &quot;Directus WebNote Keeper&quot;,
    &quot;version&quot;: &quot;1.0&quot;,
    &quot;manifest_version&quot;: 3,
    &quot;author&quot;: &quot;Jay Bharadia&quot;,
    &quot;description&quot;: &quot;Directus WebNote Keeper for capturing urls and store in directus.&quot;,
    &quot;icons&quot;: {
        &quot;16&quot;: &quot;icon.png&quot;,
        &quot;32&quot;: &quot;icon.png&quot;,
        &quot;48&quot;: &quot;icon.png&quot;,
        &quot;128&quot;: &quot;icon.png&quot;
    },
    &quot;action&quot;: {
        &quot;default_title&quot;: &quot;Directus WebNote Keeper&quot;,
        &quot;default_popup&quot;: &quot;index.html&quot;
    },
    &quot;permissions&quot;: [&quot;activeTab&quot;, &quot;storage&quot;]
}
</code></pre>
<p>You must tell browser about the functionality and permissions required by this extension. You can read more about declaring permissions in the <a href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions">Google Extensions Docs</a>.</p>
<ul>
<li><code>activeTab</code>: We need to read the website from current active tab.</li>
<li><code>storage</code>: Store the authentication token as a cookie.</li>
</ul>
<h3>Create Directus SDK Plugin</h3>
<p>Create new file <code>plugins/directus.js</code></p>
<pre><code class="language-js">import { createDirectus, rest, authentication } from &quot;@directus/sdk&quot;;

import Cookies from &quot;js-cookie&quot;;
const directus = createDirectus(&quot;your-project-url&quot;)
    .with(
        authentication(&quot;cookie&quot;, {
            autoRefresh: true,
            credentials: &quot;include&quot;,
            storage: {
                get() {
                    if (Cookies.get(&quot;directus_auth&quot;))
                        return JSON.parse(Cookies.get(&quot;directus_auth&quot;));
                    else return null;
                },
                set(data) {
                    Cookies.set(&quot;directus_auth&quot;, JSON.stringify(data));
                },
            },
        })
    )
    .with(rest());

export default directus;
</code></pre>
<p>Replace <code>your-project-url</code> with your Directus Project's URL. Then, open <code>main.js</code> and import the plugin:</p>
<pre><code class="language-js">import directus from &quot;./plugins/directus.js&quot;;
app.provide(&quot;directus&quot;, directus);
</code></pre>
<h2>Setup Routing</h2>
<p>Create new <code>plugins/router.js</code> file:</p>
<pre><code class="language-js">import { createWebHistory, createRouter } from &quot;vue-router&quot;;
import HomeView from &quot;../views/home.vue&quot;;

const routes = [
    { path: &quot;/&quot;, name: &quot;home&quot;, meta: { public: false }, component: HomeView },
];

const router = createRouter({
    history: createWebHistory(),
    routes,
});

export default router;
</code></pre>
<p>Include the router instance in <code>main.js</code>:</p>
<pre><code class="language-js">import directus from &quot;./plugins/directus.js&quot;;
import router from &quot;./plugins/router.js&quot;; // [!code ++]

app.provide(&quot;directus&quot;, directus);
app.use(router); // [!code ++]
</code></pre>
<h2>Load Extension</h2>
<p>Before moving further, build the extension and make sure it runs in your browser. Add build command in <code>package.json</code>:</p>
<pre><code class="language-json">&quot;build-extension&quot;: &quot;vite build &amp;&amp; cp manifest.json dist/&quot;
</code></pre>
<p>And then run the build command from your terminal:</p>
<pre><code>npm run build-extension
</code></pre>
<p>Open Google Chrome, go to <code>chrome://extensions</code>, click on 'Load Unpacked button' button, and select your project's <code>dist</code> folder.</p>
<h3>Setup Signup</h3>
<p>Create new <code>src/views/signup.vue</code> file:</p>
<pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;
        &lt;form @submit.prevent=&quot;signup&quot;&gt;
            &lt;label&gt;Name&lt;/label&gt;
            &lt;input type=&quot;text&quot; v-model=&quot;name&quot; required /&gt;

            &lt;label&gt;Email&lt;/label&gt;
            &lt;input type=&quot;email&quot; v-model=&quot;email&quot; required /&gt;

            &lt;label&gt;Password&lt;/label&gt;
            &lt;input type=&quot;password&quot; v-model=&quot;password&quot; required /&gt;
            &lt;button type=&quot;submit&quot;&gt;Signup&lt;/button&gt;
            &lt;p&gt;
                Already have account?
                &lt;span @click=&quot;$router.push({ name: 'login' })&quot;&gt;Login&lt;/span&gt;
            &lt;/p&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
    import { registerUser } from &quot;@directus/sdk&quot;;
    export default {
        inject: [&quot;directus&quot;],
        data() {
            return {
                name: &quot;&quot;,
                email: &quot;&quot;,
                password: &quot;&quot;,
            };
        },
        methods: {
            async signup() {
                await this.directus.request(
                    registerUser({
                        first_name: this.name,
                        email: this.email,
                        password: this.password,
                    })
                );
                this.$router.push({ name: &quot;login&quot; });
            },
        },
    };
&lt;/script&gt;
</code></pre>
<p>Add the route in <code>routes</code></p>
<pre><code class="language-js">   {
        path: &quot;/signup&quot;,
        name: &quot;signup&quot;,
        meta: { public: true },
        component: SignupView,
    },
</code></pre>
<p><img src="https://marketing.directus.app/assets/59914531-60b9-46d0-87a5-99e436bde6a0" alt="Signup" /></p>
<h2>Setup Login</h2>
<p>Create new file <code>src/views/login.vue</code></p>
<pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;
        &lt;div&gt;
            &lt;span @click=&quot;$router.push({ name: 'signup' })&quot;&gt; Signup &lt;/span&gt;
        &lt;/div&gt;
        &lt;form @submit.prevent=&quot;login&quot;&gt;
            &lt;label for=&quot;email&quot;&gt;Email&lt;/label&gt;
            &lt;input type=&quot;email&quot; id=&quot;email&quot; required v-model=&quot;email&quot; /&gt;

            &lt;label for=&quot;password&quot;&gt;Password&lt;/label&gt;
            &lt;input type=&quot;password&quot; id=&quot;password&quot; v-model=&quot;password&quot; required /&gt;

            &lt;button type=&quot;submit&quot;&gt;Login&lt;/button&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
    export default {
        inject: [&quot;directus&quot;],
        data() {
            return {
                email: &quot;&quot;,
                password: &quot;&quot;,
            };
        },
        methods: {
            async login() {
                try {
                    await this.directus.login(this.email, this.password);
                    this.$router.push({ name: &quot;home&quot; });
                } catch ({ errors }) {
                    console.log(&quot;🚀 ~ login ~ errors:&quot;, errors);
                    if (errors[0].extensions.code === &quot;INVALID_CREDENTIALS&quot;)
                        alert(&quot;Invalid Email or password&quot;);
                    else
                        alert(
                            &quot;Something went wrong. Try again after some time...&quot;
                        );
                }
            },
        },
    };
&lt;/script&gt;
</code></pre>
<h3>Add Login Route</h3>
<pre><code class="language-js">import LoginView from &quot;../views/login.vue&quot;;
</code></pre>
<pre><code class="language-js">  {
        path: &quot;/login&quot;,
        name: &quot;login&quot;,
        meta: { public: true },
        component: LoginView,
    },
</code></pre>
<p><img src="https://marketing.directus.app/assets/a234d5f6-5d62-4dfa-89b7-cb556c3a174a" alt="Login" /></p>
<h2>Setup Home Page</h2>
<pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;
        &lt;div&gt;
            &lt;p @click=&quot;logout&quot;&gt;Logout&lt;/p&gt;

            &lt;button
                @click=&quot;$router.push({ name: 'upsert', params: { id: '+' } })&quot;
            &gt;
                ⊕
            &lt;/button&gt;
        &lt;/div&gt;
        &lt;p v-if=&quot;loading&quot;&gt;Loading...&lt;/p&gt;
        &lt;div v-else&gt;
            &lt;li v-for=&quot;note in notes&quot; :key=&quot;`note-${note.id}`&quot;&gt;
                &lt;div&gt;
                    &lt;a :href=&quot;note.website&quot;&gt; {{ note.website }}&lt;/a&gt;
                    &lt;div&gt;{{ note.note }}&lt;/div&gt;
                &lt;/div&gt;
                &lt;button
                    @click=&quot;
                        $router.push({
                            name: 'upsert',
                            params: { id: note.id },
                        })
                    &quot;
                &gt;
                    📝
                &lt;/button&gt;
                &lt;button @click=&quot;remove(note.id)&quot;&gt;🗑️&lt;/button&gt;
            &lt;/li&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
    import { readItems, deleteItem } from &quot;@directus/sdk&quot;;
    import Cookies from &quot;js-cookie&quot;;
    export default {
        inject: [&quot;directus&quot;],
        data() {
            return {
                notes: null,
                loading: false,
            };
        },
        created() {
            this.getNotes();
        },
        methods: {
            async remove(id) {
                await this.directus.request(deleteItem(&quot;notes&quot;, id));
                this.getNotes();
            },
            logout() {
                Cookies.remove(&quot;directus_auth&quot;);
                this.$router.push({ name: &quot;login&quot; });
            },
            async getNotes() {
                this.loading = true;
                this.notes = await this.directus.request(readItems(&quot;notes&quot;));
                this.loading = false;
            },
        },
    };
&lt;/script&gt;
</code></pre>
<h3>Load <code>home.vue</code> in <code>router.js</code></h3>
<pre><code class="language-js">import HomeView from &quot;../views/home.vue&quot;;
</code></pre>
<pre><code class="language-js">{
    path: &quot;/&quot;,
    name: &quot;home&quot;,
    meta: { public: false },
    component: HomeView
},
</code></pre>
<p><img src="https://marketing.directus.app/assets/ccc22fa4-3d1f-45e4-b8a3-502738c36482" alt="Home Page" /></p>
<h2>Create and Edit Notes</h2>
<p>For creating and editing note, we will create only one file named <code>upsert.vue</code>. Based on route parameter, create or edit note logic is used. For creating a new note, route will be <code>/note/+</code>, and when editing, the route will be <code>/note/id</code> (<code>id</code> will be a unique number). Create <code>upsert.vue</code>:</p>
<pre><code class="language-html">&lt;template&gt;
    &lt;div&gt;
        &lt;textarea
            rows=&quot;10&quot;
            v-model=&quot;note&quot;
            placeholder=&quot;Notes are great way to store helpful information to access later. Get Started...&quot;
        &gt;&lt;/textarea&gt;
        &lt;button @click=&quot;save&quot;&gt;👍 Done&lt;/button&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
    import { createItem, readItem, updateItem } from &quot;@directus/sdk&quot;;
    export default {
        inject: [&quot;directus&quot;],
        data() {
            return {
                note: &quot;&quot;,
            };
        },
        computed: {
            id() {
                return this.$route.params.id;
            },
            isCreate() {
                return this.$route.params.id === &quot;+&quot;;
            },
            isEdit() {
                return !this.isCreate;
            },
        },
        created() {
            if (this.isEdit) {
                this.get();
            }
        },
        methods: {
            async get() {
                // Edit Note
                const { note } = await this.directus.request(
                    readItem(&quot;notes&quot;, this.id)
                );
                this.note = note;
            },
            async save() {
                if (this.isEdit) {
                    await this.directus.request(
                        updateItem(&quot;notes&quot;, this.id, {
                            note: this.note,
                        })
                    );
                } else {
                    // Create Note
                    const [tab] = await chrome.tabs.query({
                        active: true,
                        lastFocusedWindow: true,
                    });
                    const { origin } = new URL(tab.url);
                    await this.directus.request(
                        createItem(&quot;notes&quot;, {
                            note: this.note,
                            website: origin,
                        })
                    );
                }
                this.$router.push({ name: &quot;home&quot; });
            },
        },
    };
&lt;/script&gt;
</code></pre>
<h3>Load <code>upsert.vue</code> in <code>router.js</code></h3>
<pre><code class="language-js">import Upsert from &quot;../views/upsert.vue&quot;;
</code></pre>
<pre><code class="language-js">    {
        path: &quot;/note/:id&quot;,
        name: &quot;upsert&quot;,
        meta: { public: false },
        component: Upsert,
    },
</code></pre>
<p><img src="https://marketing.directus.app/assets/de053770-0448-41a6-8607-6a1ef5c4934c" alt="Create Note" /></p>
<p><img src="https://marketing.directus.app/assets/8693db55-73aa-4bae-afaf-c84d99c77833" alt="Edit Note" /></p>
<h2>Summary</h2>
<p>In this tutorial, you've learnt how to build a Chrome Extension that authenticates with Directus and allows the user to manage data. There's still some more polish and functionality you can build, but a lot of it will be based on the same concepts we've worked through here.</p>]]></content>
        <author>
            <name>Jay Bharadia</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Integrating Algolia Indexing and Directus]]></title>
        <id>https://docs.directus.io/blog/integrating-algolia-indexing-and-directus</id>
        <link href="https://docs.directus.io/blog/integrating-algolia-indexing-and-directus"/>
        <updated>2024-07-08T07:20:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to maintain an Algolia index from data in your Directus project by building a custom hook extension.]]></summary>
        <content type="html"><![CDATA[<p>In this article, we will explore how to index data from Directus in Algolia, enabling you to track created, updated, and deleted data to maintain an up-to-date index which you can then use in your external applications. Given that Algolia only support their official JavaScript client and not the REST API directly, we will build a hook extension which utilizes the client.</p>
<h2>Setting Up Directus</h2>
<p>You will need to have a <a href="https://docs.directus.io/self-hosted/quickstart">local Directus project running</a> to develop extensions.</p>
<p>In your new project, create a collection called <code>posts</code> with a <code>title</code>, <code>content</code>, and <code>author</code> field.</p>
<h2>Initializing Your Extension</h2>
<p>In your <code>docker-compose.yml</code> file, set an <code>EXTENSIONS_AUTO_RELOAD</code> environment variable to <code>true</code> so that Directus will automatically watch and reload extensions as you save your code. Restart your project once your new environment variable is added.</p>
<p>In your terminal, navigate to your <code>extensions</code> directory and run <code>npx create-directus-extension@latest</code>. Name your extension <code>algolia-indexing</code> and choose a <code>hook</code> type and create the extension with <code>JavaScript</code>. Allow Directus to automatically install dependencies and wait for them to install.</p>
<h2>Setting Up Algolia</h2>
<p>To integrate Directus and Algolia we will need our Algolia application ID and write API key. If you don't have an account already, <a href="https://www.algolia.com/users/sign_up">create one</a>, and you will see the credentials in your dashboard.</p>
<p><img src="https://marketing.directus.app/assets/376f7c33-c9b1-44f7-86a1-ae6084aadabb" alt="An image of Algolia Dashboard" /></p>
<p>In your <code>docker-compose.yml</code> file, create an <code>ALGOLIA_APP_ID</code> and <code>ALGOLIA_ADMIN_KEY</code> environment variable and set them to the value from your Algolia dashboard. Restart your project as you have changed your environment variables.</p>
<p>Navigate into your new extension directory, run <code>npm install algoliasearch</code>, and then <code>npm run dev</code> to start the automatic extension building.</p>
<p>At the top of your extension's <code>src/index.js</code> file, initialize the Algolia client:</p>
<pre><code class="language-js">import algoliasearch from  'algoliasearch';
const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_KEY);
const index = client.initIndex('directus_index');
</code></pre>
<h2>Saving New Objects to Index</h2>
<p>Update your exported function to run the Algolia <code>saveObjects()</code> method whenever a new item in the <code>posts</code> collection is created:</p>
<pre><code class="language-js">export default ({ action }) =&gt; {
    action('posts.items.create', async (meta) =&gt; {
        await index.saveObjects([{ objectID: `${meta.key}`, ...meta.payload }]);
    });
};
</code></pre>
<p>An <code>action</code> hook runs after an item has been created. Data passed in the <code>meta</code> property includes the new <code>key</code> (ID) of the item, and all the value of all fields created in the <code>payload</code> property.</p>
<p>For item creation (posts.items.create), the code registers a hook that triggers when a new item is added to the posts collection. The item is saved with an <code>objectID</code> set to the Directus item <code>id</code>, ensuring it can be accurately referenced and managed in Algolia.</p>
<h2>Updating Objects in Index</h2>
<p>When one or more items are updated, the <code>&lt;collection&gt;.items.update</code> action receives an array of <code>keys</code> along with just the values in each item that have changed. Below the existing action, add another:</p>
<pre><code class="language-js">action('posts.items.update', async (meta) =&gt; {
    await Promise.all(
        meta.keys.map(async (key) =&gt; await index.partialUpdateObjects([{ objectID: `${key}`, ...meta.payload }])),
    );
});
</code></pre>
<h2>Deleting Objects in Index</h2>
<p>When one or more items are deleted, the <code>&lt;collection&gt;.items.delete</code> action receives an array of <code>keys</code>. Add a new action:</p>
<pre><code class="language-js">action('posts.items.delete', async (meta) =&gt; {
    await index.deleteObjects(meta.keys);
});
</code></pre>
<h2>Testing Extension</h2>
<p>To test if the extension works, create a new post in Directus.</p>
<p>To verify that the indexing process is functioning as expected, navigate to the Algolia Dashboard. Click on &quot;Search&quot; in the navigation menu on the left side of your screen, then select the index. You should see that Algolia has recognized the new data:</p>
<p><img src="https://marketing.directus.app/assets/df68f81a-7182-4a45-ba0b-95f543012ea6" alt="An image of the created blog post" /></p>
<p>Also try updating and deleting posts and check if the index reflects the change.</p>
<h2>Summary</h2>
<p>By following this guide, you have learned how to set up extensions in Directus. You also saw how to test the extension by creating, updating, and deleting data in Directus, with changes being reflected in your Algolia index. This setup ensures that our data remains synchronized across both platforms.</p>]]></content>
        <author>
            <name>Marvel Ken-Anele</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Getting Started with Directus and Android with Kotlin]]></title>
        <id>https://docs.directus.io/blog/getting-started-with-directus-and-android-with-kotlin</id>
        <link href="https://docs.directus.io/blog/getting-started-with-directus-and-android-with-kotlin"/>
        <updated>2024-07-01T08:05:00.000Z</updated>
        <summary type="html"><![CDATA[This tutorial will guide you through building and integrating Directus with Kotlin to build an Android application.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, you will learn how to set up an Android project with Kotlin and Directus. We'll cover initializing the project, creating a helper library for the Directus SDK, setting up global configurations, and creating dynamic pages, including a blog listing and a blog single view.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li>A Directus project - follow our <a href="https://docs.directus.io/getting-started/quickstart">quickstart guide</a> if you don't already have one.</li>
<li>knowledge of Kotlin</li>
<li><a href="https://developer.android.com/studio">Android Studio</a> installed on your computer</li>
</ul>
<h2>Initialize a Project</h2>
<p>Open your Android Studio and create a new project by clicking <strong>Start a new Android Studio project</strong> from the welcome screen, or click on <strong>File -&gt; New -&gt; New Project</strong> if you created a project on Android Studio before. Select <code>Empty Activity</code>, name your project <code>DirectusApp</code> and, click the <strong>Finish</strong> button.</p>
<p>Open your <code>build.gradule</code> module file and add the following dependencies in the dependencies section:</p>
<pre><code class="language-groovy">dependencies {
    // [!code ++]
    implementation(&quot;androidx.navigation:navigation-fragment-ktx:2.3.5&quot;)
    implementation(&quot;androidx.navigation:navigation-ui-ktx:2.3.5&quot;)
    implementation(&quot;com.squareup.retrofit2:retrofit:2.9.0&quot;)
    implementation(&quot;com.squareup.retrofit2:converter-gson:2.9.0&quot;)
    implementation(&quot;org.jetbrains:markdown:0.7.3&quot;)
}
</code></pre>
<p>Once the changes are made, a modal will appear suggesting you sync the project. Click on the <strong>Sync</strong> button to install the dependencies.</p>
<h2>Create a Helper Library for the Directus SDK</h2>
<p>Right-click on the <code>com.example.directusapp</code> directory and select <strong>New -&gt; New Kotlin FIle/Class -&gt; File</strong> and name it <code>Constants</code>. This is where you will define all the constants for this app like your Directus URL. Add the code to the <code>Constants.kt</code> file:</p>
<pre><code class="language-kotlin">package com.example.directusapp

object Constants {
    const val BASE_URL = &quot;https://directus.example.com&quot;
}
</code></pre>
<p>Then right-click on the <code>com.example.directusapp</code> directory and select <strong>New -&gt; Package</strong> to create a network package. In your network package, create a new Kotlin file named <code>DirectusHelper</code> and define the Directus API service:</p>
<pre><code class="language-kotlin">package com.example.directusapp.network
import com.example.directusapp.Constants
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

interface DirectusApiService {
    companion object {

        fun create(): DirectusApiService {
            val retrofit = Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            return retrofit.create(DirectusApiService::class.java)
        }
    }
}
</code></pre>
<p>The above code defines a <code>DirectusAPIService</code> that includes a <code>create()</code> function to set up a Retrofit instance. This function creates a <code>Retrofit.Builder</code> object, imports the <code>Constants</code> object and sets the base URL using <code>baseUrl(Constants.BASE_URL)</code>, adds the <code>GsonConverterFactory</code> for handling JSON data conversion, builds the Retrofit instance with <code>build()</code>, and creates an implementation of the <code>DirectusApiService</code> interface using <code>create(DirectusApiService::class.java)</code>.</p>
<p>Similarly to the network package, create a model and create a new Kotlin file named <code>Models</code> in the model package and define the app models:</p>
<pre><code class="language-kotlin">package com.example.directusapp.model

data class Author(
    val id: Int,
    val name: String,
)

data class Blog(
    val id: Int,
    val title: String,
    val content: String,
    val dateCreated: String,
    val author: Author
)

data class Page(
    val slug: String,
    val title: String,
    val content: String,
)

data class Global(
    val id: Int,
    val title: String,
    val description: String,
)

data class BlogResponse(
    val data: Blog
)

data class BlogsResponse(
    val data: List&lt;Blog&gt;
)

data class PageResponse(
    val data: List&lt;Page&gt;
)

data class GlobalResponse(
    val data: Global
)
</code></pre>
<p>The above code defines data classes for different Directus collections and their respective response models.</p>
<h2>Using Global Metadata and Settings</h2>
<p>In your Directus Data Studio, click on <strong>Settings -&gt; Data Model</strong> and create a new collection named <code>global</code>. Select 'Treat as a single object' under the Singleton option because this will only have a single entry containing the app's global metadata. Create two text input fields - one with the key <code>title</code> and one with <code>description</code>.</p>
<p>Dirctus collections are not accessible to the public by default, click on <strong>Settings -&gt; Access Control -&gt; Public</strong> and give <strong>Read</strong> access to the <code>global</code> collection.</p>
<p>Then click on the content module and select the global collection. A collection would normally display a list of items, but since this is a singleton, it will launch directly into the one-item form. Enter information in the title and description field and hit save.</p>
<p><img src="https://marketing.directus.app/assets/2e7205ed-a6a8-4641-b6c1-cc4f06e84ba6" alt="Creating global collection" /></p>
<p>Update the code in your <code>DirectusHelper.kt</code> file in your network package to define a Get endpoint to fetch the global metadata from Directus:</p>
<pre><code class="language-kotlin">package com.example.directusapp.network
import com.example.directusapp.Constants
import com.example.directusapp.model.GlobalResponse
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

interface DirectusApiService {
    @GET(&quot;items/global&quot;)
    suspend fun getGlobal(): GlobalResponse

    companion object {

        fun create(): DirectusApiService {
            val retrofit = Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            return retrofit.create(DirectusApiService::class.java)
        }
    }
}
</code></pre>
<p>Right-click on your ui package, and create a new Kotlin file named <code>HomePageScreen</code>:</p>
<pre><code class="language-kotlin">package com.example.directusapp.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import com.example.directusapp.model.GlobalResponse
import com.example.directusapp.network.DirectusApiService

@Composable
fun BlogHomeScreen() {
    var globalResponse by remember { mutableStateOf&lt;GlobalResponse?&gt;(null) }
    var errorMessage by remember { mutableStateOf&lt;String?&gt;(null) }

    val scope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        scope.launch {
            try {
                val apiService = DirectusApiService.create()
                globalResponse = apiService.getGlobal()

            } catch (e: Exception) {
                errorMessage = e.message
            }
        }
    }

    if (errorMessage != null) {
        Text(text = &quot;Error: $errorMessage&quot;, color = MaterialTheme.colorScheme.error)
    } else {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            globalResponse?.let { response -&gt;
                Text(text = response.data.title, style = MaterialTheme.typography.titleLarge)
                Spacer(modifier = Modifier.height(8.dp))
                Text(text = response.data.description, style = MaterialTheme.typography.titleLarge)
                Spacer(modifier = Modifier.height(16.dp))
            }
        }
    }
}
</code></pre>
<p>Update your <code>MainActivity</code> class in the <code>MainActivity.kt</code> file to render the <code>BlogHomeScreen</code> screen.</p>
<pre><code class="language-kotlin">package com.example.directusapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.directusapp.ui.BlogHomeScreen
import com.example.directusapp.ui.theme.DirectusAppTheme

class directusapp : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DirectusAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    BlogHomeScreen()
                }
            }
        }
    }
}
</code></pre>
<p>Update your <code>AndroidManifest.xml</code> file in <code>app/src/main/</code> directory and grant your application access to the internet.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;

    &lt;application
        android:allowBackup=&quot;true&quot;
        android:dataExtractionRules=&quot;@xml/data_extraction_rules&quot;
        android:fullBackupContent=&quot;@xml/backup_rules&quot;
        android:icon=&quot;@mipmap/ic_launcher&quot;
        android:label=&quot;@string/app_name&quot;
        android:roundIcon=&quot;@mipmap/ic_launcher_round&quot;
        android:supportsRtl=&quot;true&quot;
        android:theme=&quot;@style/Theme.DirectusApp&quot;
        tools:targetApi=&quot;31&quot;&gt;
        &lt;activity
            android:name=&quot;.MainActivity&quot;
            android:exported=&quot;true&quot;
            android:label=&quot;@string/app_name&quot;
            android:theme=&quot;@style/Theme.DirectusApp&quot;&gt;
            &lt;intent-filter&gt;
                &lt;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;

                &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
            &lt;/intent-filter&gt;
        &lt;/activity&gt;
    &lt;/application&gt;

    &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt; // [!code ++]
&lt;/manifest&gt;
</code></pre>
<p>Now click on the Run icon at the top of your Android Studio Window to run the application.</p>
<p><img src="https://marketing.directus.app/assets/f7f4151f-7028-4cf9-a8d7-8f62ad81b284" alt="Showing metadata from Directus global collection" /></p>
<h2>Creating Pages With Directus</h2>
<p>Create a new collection called <code>pages</code> - make the Primary ID Field a &quot;Manually Entered String&quot; called <code>slug</code>, which will correlate with the URL for the page. For example, about will later correlate to the page localhost:3000/about.</p>
<p>Create a text input field called <code>title</code> and a <code>WYSIWYG</code> input field called <code>content</code>. In Access Control, give the Public role read access to the new collection. Create 3 items in the new collection - <a href="https://github.com/directus-labs/getting-started-demo-data">here's some sample data</a>.</p>
<p><img src="https://marketing.directus.app/assets/4eeeaf48-95a7-4b93-a6af-dcbd5b6cf6dd" alt="Creating pages collection" /></p>
<p>Then update your <code>DirectusHelper</code> file to add another endpoint to fetch the page data from Directus:</p>
<pre><code class="language-kotlin">package com.example.directusapp.network
import com.example.directusapp.Constants
import com.example.directusapp.model.GlobalResponse
import com.example.directusapp.model.PageResponse
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET


interface DirectusApiService {
    @GET(&quot;items/global&quot;)
    suspend fun getGlobal(): GlobalResponse

    @GET(&quot;items/pages&quot;)
    suspend fun getPages(): PageResponse

    companion object {

        fun create(): DirectusApiService {
            val retrofit = Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            return retrofit.create(DirectusApiService::class.java)
        }
    }
}
</code></pre>
<p>Update your <code>BlogHomeScreen</code> to display the pages data:</p>
<pre><code class="language-kotlin">package com.example.directusapp.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import androidx.compose.foundation.lazy.LazyColumn
import com.example.directusapp.ui.MarkdownView

@Composable
fun BlogHomeScreen(navController: NavController) {
    var globalResponse by remember { mutableStateOf&lt;GlobalResponse?&gt;(null) }
    var pagesResponse by remember { mutableStateOf&lt;PageResponse?&gt;(null) }

    var errorMessage by remember { mutableStateOf&lt;String?&gt;(null) }

    val scope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        scope.launch {
            try {
                val apiService = DirectusApiService.create()
                globalResponse = apiService.getGlobal()
                pagesResponse = apiService.getPages()

            } catch (e: Exception) {
                errorMessage = e.message
            }
        }
    }

    if (errorMessage != null) {
        Text(text = &quot;Error: $errorMessage&quot;, color = MaterialTheme.colorScheme.error)
    } else {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            pagesResponse?.let { response -&gt;
              Text(text = response.data[0].title, style = MaterialTheme.typography.titleLarge)
              Spacer(modifier = Modifier.height(8.dp))
              MarkdownView(markdownText = response.data[0].content.trimIndent())
              Spacer(modifier = Modifier.height(16.dp))
            }
        }
    }
}
</code></pre>
<p>Create another file named <code>MarkdownView</code> and create a <code>MarkdownView</code> composable function to render the <code>WYSIWYG</code> content from the collection of the pages:</p>
<pre><code class="language-kotlin">package com.example.directusapp.ui

import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser

@Composable
fun MarkdownView(markdownText: String) {
    val htmlContent = markdownToHtml(markdownText)

    AndroidView(factory = { context -&gt;
        WebView(context).apply {
            webViewClient = WebViewClient()
            loadDataWithBaseURL(null, htmlContent, &quot;text/html&quot;, &quot;UTF-8&quot;, null)
        }
    }, update = {
        it.loadDataWithBaseURL(null, htmlContent, &quot;text/html&quot;, &quot;UTF-8&quot;, null)
    })
}

fun markdownToHtml(markdownText: String): String {
    val flavour = GFMFlavourDescriptor()
    val parser = MarkdownParser(flavour)
    val parsedTree = parser.buildMarkdownTreeFromString(markdownText)
    val htmlGenerator = HtmlGenerator(markdownText, parsedTree, flavour)
    return htmlGenerator.generateHtml()
}
</code></pre>
<p>Refresh the app to see the changes.</p>
<p><img src="https://marketing.directus.app/assets/49ec2d21-5c61-4c52-9fd3-afddb0c2f2a3" alt="Displaying the pages" /></p>
<h2>Creating Blog Posts With Directus</h2>
<p>Back to your Directus Data studio, create a collection to store and manage your user's blog posts. First, create a collection named <code>author</code> with a single text input field named <code>name</code>. Add one or more authors to the collection.</p>
<p>Create another collection called <code>blogs</code> and add the following fields:</p>
<ul>
<li>slug: Primary key field, Manually entered string</li>
<li>title: Text input field</li>
<li>content: WYSIWYG input field</li>
<li>image: Image relational field</li>
<li>author: Many-to-one relational field with the related collection set to authors</li>
</ul>
<p>Add 3 items in the posts collection - <a href="https://github.com/directus-labs/getting-started-demo-data">here's some sample data</a>.</p>
<p>Then update your <code>DirectusHelper</code> file to add another endpoint to fetch the blog data:</p>
<pre><code class="language-kotlin">package com.example.directusapp.network
import com.example.directusapp.Constants
import com.example.directusapp.model.BlogsResponse
import com.example.directusapp.model.GlobalResponse
import com.example.directusapp.model.PageResponse
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET


interface DirectusApiService {
    @GET(&quot;items/global&quot;)
    suspend fun getGlobal(): GlobalResponse

    @GET(&quot;items/pages&quot;)
    suspend fun getPages(): PageResponse

    @GET(&quot;items/blogs?fields=*,author.name&quot;)
    suspend fun getBlogs(): BlogsResponse

    companion object {

        fun create(): DirectusApiService {
            val retrofit = Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            return retrofit.create(DirectusApiService::class.java)
        }
    }
}
</code></pre>
<p>Update your <code>BlogHomeScreen</code> to render the blogs:</p>
<pre><code class="language-kotlin">package com.example.directusapp.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.ui.graphics.BlendMode.Companion.Screen
import com.example.directusapp.ui.MarkdownView
import com.example.directusapp.model.GlobalResponse
import com.example.directusapp.model.PageResponse
import com.example.directusapp.model.BlogsResponse
import com.example.directusapp.model.Blog
import com.example.directusapp.network.DirectusApiService

@Composable
fun BlogHomeScreen(navController: NavController) {
    var blogsResponse by remember { mutableStateOf&lt;BlogsResponse?&gt;(null) }
    var pagesResponse by remember { mutableStateOf&lt;PageResponse?&gt;(null) }
    var globalResponse by remember { mutableStateOf&lt;GlobalResponse?&gt;(null) }
    var errorMessage by remember { mutableStateOf&lt;String?&gt;(null) }

    val scope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        scope.launch {
            try {
                val apiService = DirectusApiService.create()
                blogsResponse = apiService.getBlogs()
                pagesResponse = apiService.getPages()
                globalResponse = apiService.getGlobal()
                println(pagesResponse)
                println(globalResponse)

            } catch (e: Exception) {
                errorMessage = e.message
            }
        }
    }

    if (errorMessage != null) {
        Text(text = &quot;Error: $errorMessage&quot;, color = MaterialTheme.colorScheme.error)
    } else {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            // Display the page title and content
            pagesResponse?.let { response -&gt;
                Text(text = response.data[0].title, style = MaterialTheme.typography.titleLarge)
                Spacer(modifier = Modifier.height(8.dp))
                MarkdownView(markdownText = response.data[0].content.trimIndent())
                Spacer(modifier = Modifier.height(16.dp))
            }
            Text(text = &quot;Blog Posts&quot;, style = MaterialTheme.typography.titleLarge)
            Spacer(modifier = Modifier.height(10.dp))
            blogsResponse?.let { response -&gt;
                LazyColumn {
                    items(response.data.size) { index -&gt;
                        BlogItem(response.data[index], navController)
                    }
                }
            }
        }
    }
}

@Composable
fun BlogItem(blog: Blog, navController: NavController) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .clickable {
                navController.navigate(Screen.BlogDetail.createRoute(blog.id))
                println(blog.id)
            }
            .padding(16.dp)
    ) {

        Text(text = &quot;${blog.title} - ${blog.author}&quot;, style = MaterialTheme.typography.titleMedium)
        Spacer(modifier = Modifier.height(8.dp))
        Text(text = blog.dateCreated, style = MaterialTheme.typography.bodyMedium)
    }
}
</code></pre>
<p>Refresh your application to see the updates.</p>
<p><img src="https://marketing.directus.app/assets/d232ed66-7153-4de5-b4ad-f537e4792b51" alt="Display the blog listing page" /></p>
<h2>Create Blog Post Listing</h2>
<p>Each blog post links to a screen that does not yet exist. Right-click the <code>ui</code> package and create a new Kotlin file named <code>BlogDetailScreen</code>:</p>
<pre><code class="language-kotlin">package com.example.directusapp.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.example.directusapp.network.DirectusApiService
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import kotlinx.coroutines.launch
import com.example.directusapp.model.BlogResponse


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BlogDetailScreen(blogId: Int, navController: NavController) {
    var blogResponse by remember { mutableStateOf&lt;BlogResponse?&gt;(null) }
    var errorMessage by remember { mutableStateOf&lt;String?&gt;(null) }

    LaunchedEffect(blogId) {
        launch {
            try {
                val apiService = DirectusApiService.create()
                blogResponse = apiService.getBlogById(blogId)
            } catch (e: Exception) {
                errorMessage = e.message
            }
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(&quot;Blog Detail&quot;) },
                navigationIcon = {
                    IconButton(onClick = { navController.navigateUp() }) {
                        Icon(
                            imageVector = Icons.Filled.ArrowBack,
                            contentDescription = &quot;Back&quot;
                        )
                    }
                }
            )
        }
    ) {
        if (errorMessage != null) {
            Text(text = &quot;Error: $errorMessage&quot;, style = MaterialTheme.typography.bodyLarge)
        } else {
            if (blogResponse != null) {
                // Render content using `blogResponse.data`
                val blog = blogResponse!!.data
                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(it)
                        .padding(16.dp)
                ) {
                    Text(text = blog.title, style = MaterialTheme.typography.titleLarge)
                    Spacer(modifier = Modifier.height(8.dp))
                    Text(text = blog.dateCreated, style = MaterialTheme.typography.bodyMedium)
                    Spacer(modifier = Modifier.height(16.dp))
                    MarkdownView(markdownText = blog.content.trimIndent())
                }
            } else{
                Text(text=&quot;Loading&quot;)
            }
        }
    }
}
</code></pre>
<p>The above code defines a composable function called <code>BlogDetailScreen</code> that displays the details of a blog post retrieved from an API. It uses the Scaffold component with a <code>TopAppBar</code> that has a back button to navigate up the screen hierarchy. The screen fetches blog data from an API service using a coroutine and stores it in the <code>blogResponse</code> state variable. If there is an error, the <code>errorMessage</code> state variable is set. If the blog data is successfully fetched, it renders the blog title, date created, and content using the custom <code>MarkdownView</code> composable function.</p>
<p>Then update your <code>DirectusHelper</code> file to add an endpoint to fetch blogs by their id:</p>
<pre><code class="language-kotlin">package com.example.directusapp.network
import com.example.directusapp.Constants
import com.example.directusapp.model.BlogsResponse
import com.example.directusapp.model.BlogResponse
import com.example.directusapp.model.GlobalResponse
import com.example.directusapp.model.PageResponse
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path

interface DirectusApiService {
    @GET(&quot;items/global&quot;)
    suspend fun getGlobal(): GlobalResponse

    @GET(&quot;items/pages&quot;)
    suspend fun getPages(): PageResponse

    @GET(&quot;items/blog?fields=*,author.name&quot;)
    suspend fun getBlogs(): BlogsResponse

    @GET(&quot;items/blog/{id}?fields=*,author.name&quot;)
    suspend fun getBlogById(@Path(&quot;id&quot;) id: Int): BlogResponse

    companion object {

        fun create(): DirectusApiService {
            val retrofit = Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            return retrofit.create(DirectusApiService::class.java)
        }
    }
}
</code></pre>
<h2>Add Navigation</h2>
<p>To allow your users to navigate the <code>BlogDetailScreen</code> and back to the <code>BlogHomeScreen</code> you need to implement navigation in the app. In the ui package, create a new Kotlin file named <code>NavGraph</code>:</p>
<pre><code class="language-kotlin">package com.example.directusapp.ui

import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable

sealed class Screen(val route: String) {
    object BlogList : Screen(&quot;blogList&quot;)
    object BlogDetail : Screen(&quot;blogDetail/{blogId}&quot;) {
        fun createRoute(blogId: Int) = &quot;blogDetail/$blogId&quot;
    }
}

@Composable
fun NavGraph(navController: NavHostController) {
    NavHost(navController, startDestination = Screen.BlogList.route) {
        composable(Screen.BlogList.route) {
            BlogHomeScreen(navController)
        }
        composable(Screen.BlogDetail.route) { backStackEntry -&gt;
            val blogIdString = backStackEntry.arguments?.getString(&quot;blogId&quot;)
            val blogId = blogIdString?.toIntOrNull()
            if (blogId != null) {
                BlogDetailScreen(blogId, navController)
            }
        }
    }
}
</code></pre>
<p>For the navigation, update your <code>MainActivity</code> file to render the <code>NavGraph</code>.</p>
<pre><code class="language-kotlin">package com.example.directusapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import com.example.directusapp.ui.NavGraph
import com.example.directusapp.ui.theme.DirectusAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DirectusAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val navController = rememberNavController()
                    NavGraph(navController = navController)
                }
            }
        }
    }
}
</code></pre>
<p>Now click on any of the blogs to navigate to the details page.</p>
<p><img src="https://marketing.directus.app/assets/c0d911bd-1944-4132-8577-6dab410a39a3" alt="Show the blog details pages" /></p>
<h2>Next Steps</h2>
<p>Throughout this guide, you have set up an Android project, created a Directus plugin, and set up an Android project with Kotlin to interact with Directus, covering project initialization, creating a helper library for the Directus SDK, global configurations, dynamic pages, and navigation setup.</p>
<p>If you want to see the code for this project, you can find it on <a href="https://github.com/directus-labs/blog-example-getting-started-android-kotlin">GitHub</a>.</p>]]></content>
        <author>
            <name>Ekekenta Clinton</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build a URL Shortener with React, TypeScript, and Directus]]></title>
        <id>https://docs.directus.io/blog/build-a-url-shortener-with-react-type-script-and-directus</id>
        <link href="https://docs.directus.io/blog/build-a-url-shortener-with-react-type-script-and-directus"/>
        <updated>2024-06-27T08:30:00.000Z</updated>
        <summary type="html"><![CDATA[Learn to build a link shortener using Directus and React! Define data models, customize roles, and dynamically route with ease.]]></summary>
        <content type="html"><![CDATA[<p>In this tutorial, we will build a link shortener. Then, we'll create a React project that looks for a slug, queries the associated record in Directus, and redirects the user to the configured link.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ol>
<li>A Directus project - follow our <a href="https://docs.directus.io/getting-started/quickstart">quickstart guide</a> if you don't already have one.</li>
<li>Some knowledge of Javascript and React.js.</li>
</ol>
<p>The complete code for this project can be found on <a href="https://github.com/directus-labs/blog-example-url-shortener">GitHub.</a></p>
<h2>Setting Up Your Directus Project</h2>
<p>Create a new collection called <code>short_link</code>. Enable all optional fields, and add the following additional fields:</p>
<ul>
<li><code>slug</code> (type: String, interface: input, required): The URL path that will be shared in the short URL.</li>
<li><code>url</code> (type: String, interface: input, required): The URL we want to redirect to.</li>
<li><code>clicks</code> (type: Integer, interface: input, default_value: 0): Number of times the link was clicked.</li>
</ul>
<p>To make sure the URL that will be stored in the <strong>URL</strong> field is valid, we will use a validation filter.</p>
<p>Create a validation rule for the URL. From the field settings, ensure the URL <strong>Matches RegExp</strong>:</p>
<pre><code>https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&amp;//=]*)
</code></pre>
<p><img src="https://marketing.directus.app/assets/7a27e4b2-5229-4dad-b2db-1f6c7ed905e5" alt="Validation regex" /></p>
<p>Create a new <code>contributor</code> role will have the following privileges:</p>
<ul>
<li>Create a short_link.</li>
<li>Read a short_link</li>
<li>Update the <code>clicks</code> field (to avoid spamming) of short_link.</li>
</ul>
<p><img src="https://marketing.directus.app/assets/ae832a6d-1601-4b2e-9040-c7b368a16d06" alt="Contributor Permissions - create and read short link are enabled. Edit is custom." /></p>
<p><img src="https://marketing.directus.app/assets/b897b6f1-a616-4d6e-895a-f7c19b3e2249" alt="Contributor Privileges" /></p>
<p>Finally, create some sample data in your <code>short_link</code> collection.</p>
<table>
<thead>
<tr>
<th>Slug</th>
<th>URL</th>
</tr>
</thead>
<tbody>
<tr>
<td>website</td>
<td><a href="https://directus.io/">https://directus.io/</a></td>
</tr>
<tr>
<td>x</td>
<td><a href="https://x.com/directus">https://x.com/directus</a></td>
</tr>
</tbody>
</table>
<h2>Setting Up Your React Project</h2>
<p>Install React.js, set up dependencies, and run a development server:</p>
<pre><code>npm create vite@latest link-shortener
? Select a framework: React
? Select a variant: TypeScript
npm install
npm install --save-dev @types/node
npm install react-router-dom
npm install @directus/sdk 
npm run dev
</code></pre>
<p>Now, open <a href="http://localhost:5173/">http://localhost:5173/</a> on your browser, and you should see the starter page.</p>
<p>Next, we will set up Vite to allow environment variables, which will be used to store our Directus credentials. In the <code>vite.config.ts</code> file, add the following code:</p>
<pre><code class="language-typescript">import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;

export default defineConfig({
  plugins: [react()],
  define: {
    &quot;process.env.VITE_DIRECTUS_API_TOKEN.&quot;: JSON.stringify(
      process.env.VITE_DIRECTUS_API_TOKEN
    ),
    &quot;process.env.VITE_DIRECTUS_API_URL.&quot;: JSON.stringify(
      process.env.VITE_DIRECTUS_API_URL
    ),
  },
});
</code></pre>
<p>Create a <code>.env</code> file in the <code>src</code> directory, and the following variables, being sure to provide your specific static authentication token and project URL:</p>
<pre><code>VITE_DIRECTUS_API_TOKEN = XXXXX
VITE__DIRECTUS_API_URL = https://your-amazing.directus.app/
</code></pre>
<p>In the <code>src</code> directory create a sub-directory called <code>utils</code>, and within it, a <code>directus.ts</code> file.</p>
<p>Add the following code to <em>directus.ts</em>:</p>
<pre><code class="language-typescript">import { createDirectus, staticToken, rest } from &quot;@directus/sdk&quot;;

const directusToken = import.meta.env.VITE_DIRECTUS_API_TOKEN;
const directusUrl = import.meta.env.VITE__DIRECTUS_API_URL;

if (!directusToken) {
  throw new Error(&quot;Please include a Token&quot;);
}

if (!directusUrl) {
  throw new Error(&quot;Please include a Url&quot;);
}

export const directus = createDirectus(directusUrl)
  .with(staticToken(directusToken))
  .with(rest());
</code></pre>
<h3>Creating the Dynamic Route</h3>
<p>Let's create the route that will be used to redirect the user to the slug URL queried from Directus. In the <code>App.tsx</code> file add the following code:</p>
<pre><code class="language-typescript">import { BrowserRouter, Routes, Route } from &quot;react-router-dom&quot;;
import { useParams } from &quot;react-router-dom&quot;;

function App() {
  return (
    &lt;&gt;
      &lt;h1&gt;Link Shortener&lt;/h1&gt;
      &lt;BrowserRouter&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/:slug&quot; element={&lt;LinkRoute /&gt;}&gt;&lt;/Route&gt;
        &lt;/Routes&gt;
      &lt;/BrowserRouter&gt;
    &lt;/&gt;
  );
}

function LinkRoute() {
  const { slug } = useParams();

  console.log(slug);

  return (
    &lt;&gt;
      &lt;h1&gt;{slug}&lt;/h1&gt;
    &lt;/&gt;
  );
}

export default App;
</code></pre>
<h3>Querying Directus</h3>
<p>Query the associated record for the slug in Directus and redirect the user to the route link.</p>
<p>Enter the following code in <code>App.tsx</code> to define the interface for the data that will be returned from Directus:</p>
<pre><code class="language-typescript">import { BrowserRouter, Routes, Route } from &quot;react-router-dom&quot;;
import { useParams } from &quot;react-router-dom&quot;;

interface ShortLink { // [!code ++]
  clicks: number; // [!code ++]
  date_created: string; // [!code ++]
  date_updated?: string; // [!code ++]
  id: number; // [!code ++]
  slug: string; // [!code ++]
  sort?: null; // [!code ++]
  url: string; // [!code ++]
  user_created?: string; // [!code ++]
  user_updated?: null; // [!code ++]
} // [!code ++]

function App() {
  return (
    &lt;&gt;
      &lt;h1&gt;Link Shortener&lt;/h1&gt;
      &lt;BrowserRouter&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/:slug&quot; element={&lt;LinkRoute /&gt;}&gt;&lt;/Route&gt;
        &lt;/Routes&gt;
      &lt;/BrowserRouter&gt;
    &lt;/&gt;
  );
}

function LinkRoute() {....}
</code></pre>
<p>Query Directus in <code>App.tsx</code>:</p>
<pre><code class="language-typescript">import { BrowserRouter, Routes, Route } from &quot;react-router-dom&quot;;
import { useParams } from &quot;react-router-dom&quot;;
import { directus } from &quot;./util/directus&quot;; // [!code ++]
import { readItems, updateItem } from &quot;@directus/sdk&quot;; // [!code ++]
import { useEffect, useState } from &quot;react&quot;; // [!code ++]

function App(){...}

function LinkRoute() { // [!code ++]
  const { slug } = useParams();
  const [slugError, setSlugError] = useState(&quot;&quot;); // [!code ++]

  useEffect(() =&gt; { // [!code ++]
    async function fetchShortLink() { // [!code ++]
      try { // [!code ++]
        const data = await directus.request(readItems(&quot;short_link&quot;)); // [!code ++]
// [!code ++]
        const slug_data = data // [!code ++]
          .map((y) =&gt; y) // [!code ++]
          .filter((z) =&gt; z.slug.toLowerCase().includes(slug)); // [!code ++]
// [!code ++]
        if (!slug_data || slug_data?.length === 0) { // [!code ++]
          setSlugError(`Invalid Slug: Couldn't find the record →→ ${slug}`); // [!code ++]
          throw new Error(&quot;Invalid Slug: Couldn't find that record&quot;); // [!code ++]
        } // [!code ++]
        const shortLink = slug_data[0] as ShortLink; // [!code ++]
// [!code ++]
        await directus.request( // [!code ++]
          updateItem(&quot;short_link&quot;, shortLink.id, { // [!code ++]
            clicks: shortLink.clicks + 1, // [!code ++]
          }) // [!code ++]
        ); // [!code ++]
// [!code ++]
        window.location.assign(`${shortLink.url}`);// [!code ++]
// [!code ++]
      } catch (error) { // [!code ++]
        console.log(error); // [!code ++]
      } // [!code ++]
    } // [!code ++]
// [!code ++]
    fetchShortLink(); // [!code ++]
  }, [slug]); // [!code ++]

  return (
    &lt;&gt;
      &lt;h1&gt;{slug}&lt;/h1&gt; // [!code --]
      {slugError &amp;&amp; &lt;h1 style={{ color: &quot;red&quot; }}&gt;{slugError}&lt;/h1&gt;} // [!code ++]
    &lt;/&gt;
  );
}

export default App;
</code></pre>
<p>The <code>LinkRoute</code> component uses the <code>useParams</code> hook to get the <code>slug</code> parameter from the URL and the <code>useState</code> hook to store an error message if the slug is invalid.</p>
<p>The <code>useEffect</code> hook is used to fetch data from the Directus API via the <code>fetchShortLink</code> effect function, which performs the following actions:</p>
<ol>
<li>It makes a request to the Directus API to read all short links via the <code>readItems</code> composable.</li>
<li>The response data is filtered to find the short link that matches the current slug.</li>
<li>If no matching short link is found, it sets an error message and throws an error.</li>
<li>If a matching short link is found, it updates the short links <code>click</code> property by <code>1</code> via the <code>updateItem</code> composable from Directus API.</li>
<li>Finally, the user is redirected to the URL associated with the short link using the <code>window.location.assign</code> method.</li>
</ol>
<p>Finally, if an error occurs during the data fetching process, the error is caught and logged to the console. The <code>LinkRoute</code> component returns the error message if the slug is invalid.</p>
<h2>Conclusion</h2>
<p>You’ve just learned how to set up the data collection process and implement dynamic routing using React and Directus. The broad process involved setting up a Directus data model, customizing user roles and permissions, creating an API endpoint to query data from Directus, and configuring a dynamic route to navigate to slug URLs.</p>
<p>I hope you find this tutorial useful - if you have any questions or hurdles feel free to join the Directus <a href="https://directus.chat">official Discord server</a>.</p>]]></content>
        <author>
            <name>Onyedikachi Eni</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[5 Automations to Level-Up Your Blog with Directus]]></title>
        <id>https://docs.directus.io/blog/5-automations-to-level-up-your-blog-with-directus</id>
        <link href="https://docs.directus.io/blog/5-automations-to-level-up-your-blog-with-directus"/>
        <updated>2024-06-24T08:28:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to automate translation, content publishing, alerts, summary writing, and hooking up your blog to the world of integrations provided by Zapier.]]></summary>
        <content type="html"><![CDATA[<p>Maintaining a blog requires more than just compelling content; there's a lot of other manual work or processes that may need to be set up.</p>
<p>This article shows five automation ideas that will level up your blog powered by Directus, allowing you to focus more on content creation and less on the intricacies of blog management.</p>
<h2>Before You Start</h2>
<p>You will need:</p>
<ul>
<li>A Directus project - <a href="https://docs.directus.io/getting-started/quickstart">follow our quickstart guide</a> if you don't already have one.</li>
<li>A <a href="https://www.deepl.com/">DeepL</a> API key for translation.</li>
<li>An <a href="https://openai.com/">OpenAI</a> key for generating search engine optimized summaries of your content.</li>
</ul>
<p>In your Directus project, create a new collection called <code>content</code> and <code>comment</code> with the following fields:</p>
<p>For <code>content</code>:</p>
<ul>
<li><code>seo_summary</code>: Textarea, string</li>
<li><code>comment</code>: one to many, comment</li>
<li><code>approved</code>: Radio Buttons, Choices: yes, no, Default Value: no</li>
</ul>
<p>Do not add the fields to be translated like <code>title</code> or <code>detail</code> yet, because those will be stored in a separate collection. Follow <a href="https://docs.directus.io/guides/headless-cms/content-translations">this translation guide</a> to set up your multilingual project.</p>
<p>For <code>comment</code>:</p>
<ul>
<li><code>content</code>: many to one, content</li>
<li><code>comment</code>: Textarea, string</li>
</ul>
<h2>Automate the Translation of Your Content</h2>
<p><img src="https://marketing.directus.app/assets/6d0c3f9f-05fd-4aec-9dbe-fc978da8a26b" alt="Flow for translation of content with AI" /></p>
<p>Automated translations ensure that every new post is available in multiple languages simultaneously, helping you be more accessible to a global audience.</p>
<p>Before continuing, add a <code>translation</code> field in your <code>content</code> collection with a <code>title</code> and <code>detail</code> field, if you haven't already.</p>
<p>We will automate translation from English to French, then update the article that was just created or updated. While we will translate only the <code>detail</code> section of the blog, but you can also use this method to create a translations for more fields.</p>
<p>This automation relies on the AI Translator extension, which you can download from the Directus Marketplace and is published by the Directus team.</p>
<p>Create a new flow with an event trigger. The scope should be <code>items.create</code> and <code>items.update</code> on the <code>content</code> collection.</p>
<p>The trigger will only return the <code>key</code> of the content, but we require the <code>detail</code> field. Create a <strong>Read Data</strong> operation and give it full access permissions. On the &quot;Content Translations&quot; collection, access the following query:</p>
<pre><code class="language-json">{
    &quot;filter&quot;: {
        &quot;_or&quot;: [
            {
                &quot;_and&quot;: [
                    {
                        &quot;content_id&quot;: {
                            &quot;_eq&quot;: &quot;{{$trigger.keys[0]}}&quot;
                        }
                    },
                    {
                        &quot;languages_code&quot;: {
                            &quot;_eq&quot;: &quot;en-US&quot;
                        }
                    }
                ]
            },
            {
                &quot;_and&quot;: [
                    {
                        &quot;content_id&quot;: {
                            &quot;_eq&quot;: &quot;{{$trigger.key}}&quot;
                        }
                    },
                    {
                        &quot;languages_code&quot;: {
                            &quot;_eq&quot;: &quot;en-US&quot;
                        }
                    }
                ]
            }
        ]
    }
}
</code></pre>
<p>The query filters for records in Content Translations where the <code>content_id</code> matches &lt;span v-pre&gt;<code>{{$trigger.keys[0]}}</code>&lt;/span&gt; (when an updated item) or &lt;span v-pre&gt;<code>{{$trigger.key}}</code>&lt;/span&gt; (when a newly-created item), and retrieves the <code>en-US</code> version. The <strong>Read Data</strong> returns the full article that was just created or updated.</p>
<p>Now add the <strong>AI Translation</strong> operation with full access permission. Enter your DeepL API key and select the appropriate plan. In the <strong>Text</strong> input, insert &lt;span v-pre&gt;<code>{{$last[0].detail}}</code>&lt;/span&gt; and then choose the desired language.</p>
<p>Now we can save the French output by adding the <strong>Create Data</strong> operation on the <strong>Content Translations</strong> collection with full access permission. Use the payload:</p>
<pre><code class="language-json">{
    &quot;content_id&quot;: &quot;{{$trigger.keys[0]}}&quot;,
    &quot;languages_code&quot;: &quot;fr-FR&quot;,
    &quot;detail&quot;: &quot;{{$last}}&quot;
}
</code></pre>
<p>You can test this out by creating new content. After a few seconds, the French version will be populated. You can do the same for the title.</p>
<p><img src="https://marketing.directus.app/assets/2655c3ba-7866-42fc-b9e5-fc0b366e940a" alt="AI translated content" /></p>
<h2>Automate Content Publishing Once Approved</h2>
<p><img src="https://marketing.directus.app/assets/d5c71b81-88bd-44ea-a8cd-9d4992621249" alt="Flow to publish article" /></p>
<p>As a blog manager, you frequently juggle multiple tasks. By setting up an automated publishing workflow in Directus, once a post is reviewed and approved, it gets published immediately. This automation eliminates the back-and-forth of manual scheduling and reduces the risk of human error.</p>
<p>Create a flow to trigger upon updating a post and select the content collection.</p>
<p>Add a <strong>Read Data</strong> operation with the key of <code>read_translation</code> attached to the <strong>Content Translations</strong> collection with the ID &lt;span v-pre&gt;<code>{{$trigger.keys[0]}}</code>&lt;/span&gt;. This will return the payload that includes the <code>title</code> and <code>detail</code> of the content that was just updated. If you examine the payload, you'll notice that the <code>approved</code> value is not included, as it is saved in the <code>content</code> collection.</p>
<p>To retrieve this information, you'll need to use the <strong>Read Data</strong> operation again, this time attached to the <code>content</code> collection with the ID &lt;span v-pre&gt;<code>{{$trigger.keys[0]}}</code>&lt;/span&gt;.</p>
<p>Now use the <strong>Condiion</strong> operation with the following Condition rule:</p>
<pre><code class="language-json">{
    &quot;$trigger&quot;: {
        &quot;payload&quot;: {
            &quot;approved&quot;: {
                &quot;_eq&quot;: &quot;yes&quot;
            }
        }
    }
}
</code></pre>
<p>This is saying that the flow should go to the next operation if &quot;approved&quot; is equal to &quot;yes&quot;. If the condition is met, you can follow this existing docs post on <a href="https://docs.directus.io/guides/headless-cms/trigger-static-builds/netlify.html#configure-netlify-build-hook">using Netlify/Vercel build triggers</a>.</p>
<h2>Configuring Alerts for New Comments</h2>
<p><img src="https://marketing.directus.app/assets/8feb7ca1-71b7-4058-9fe8-58b48b899ba2" alt="comment alert" /></p>
<p>As your blog grows, monitoring comments manually becomes impractical. Automated alerts ensure that you and your team are notified of new comments instantly, enabling prompt responses and community engagement.</p>
<p>In this section, you will learn how to set up an automation to send comments email to a specified email.</p>
<p>This automation assumed that comments are either collected directly in your Directus project, or that they are synced/pushed via an integration.</p>
<p>:::info Email Setup</p>
<p>If you are self-hosting a Directus instance, you will need to set up the <a href="https://docs.directus.io/self-hosted/config-options.html#email">email service configuration</a>.</p>
<p>:::</p>
<p>Create a flow to trigger email upon creating a comment and select the <code>comment</code> collection.</p>
<p>Now add a <strong>Read Data</strong> operation attached to the Content Translations collection with the ID &lt;span v-pre&gt;{{$trigger.payload.content}}`&lt;/span&gt;. This will return the comment that was just created. Now attach the <strong>Send Email</strong> operation. In the <strong>To</strong> section input the email addresses you want the notification to go to.</p>
<pre><code>**Subject**:  Comment for on article &quot;{{get_article_comment.title}}&quot;
**Type**: Markdown
**Body**: 
    Hello, there is a comment for the article &quot;{{get_article_comment.title}}&quot;
    &gt; {{$trigger.payload.comment}}
</code></pre>
<p><img src="https://marketing.directus.app/assets/f763c486-1561-426a-8464-1a52bf3e4e29" alt="email alert" /></p>
<h2>Automatically Write SEO-Optimized Summaries</h2>
<p><img src="https://marketing.directus.app/assets/058d9288-6ef7-4b64-ba5c-6bc97ef79179" alt="AI SEO summary" /></p>
<p>Writing SEO summaries can be time-consuming and repetitive. By using the AI Writer extension, available in the Directus Marketplace, you can automatically generate keyword-rich summaries, improving your blog's SEO performance and freeing up time for more creative tasks.</p>
<p>Once you've installed the AI Writer extension, create a flow to trigger upon creating a post and select the content collection.</p>
<p>The trigger will only return the <code>key</code> of the content, but the whole post is needed to send to OpenAI. Create a <strong>Read Data</strong> operation and give it full access permissions. On the “Content Translations” collection, access the following query:</p>
<pre><code class="language-json">{
    &quot;filter&quot;: {
        &quot;_and&quot;: [
            {
                &quot;content_id&quot;: {
                    &quot;_eq&quot;: &quot;{{$trigger.key}}&quot;
                }
            },
            {
                &quot;languages_code&quot;: {
                    &quot;_eq&quot;: &quot;en-US&quot;
                }
            }
        ]
    }
}
</code></pre>
<p>Pass the content to OpenAI by creating an <strong>AI writer</strong> operation. Input your OpenAI key, select the <strong>GPT model</strong> you want to use and <strong>Prompt</strong> as Create SEO Description. The Text will be gotten at &lt;span v-pre&gt;<code>{{$last[0].detail}}</code>&lt;/span&gt;.</p>
<p>Update the content collection with the newly created SEO description using <strong>Update Data</strong> operation - the ID tag will be &lt;span v-pre&gt;<code>{{$trigger.key}}</code>&lt;/span&gt;. You can now save the summary back to the item.</p>
<h2>Connect to the World of Zapier Integrations</h2>
<p><img src="https://marketing.directus.app/assets/d419296e-537c-4417-aeb2-27e0a9c4c0d0" alt="social post" /></p>
<p>Zapier is a wonderful automation tool with thousands of pre-built integrations. While Directus Automate is powerful, it's useful to recognize when integrating with other tools ends up being more efficient.</p>
<p>Create a Zap with a <a href="https://help.zapier.com/hc/en-us/articles/8496288690317-Trigger-Zaps-from-webhooks">Webhook Trigger URL</a>. Once you have this, data can be sent to Zapier from Directus.</p>
<p>Create a flow to manually trigger on the Item page in the content collection. Now, create a <strong>Read Data</strong> operation, on the “Content” collection, use the ID at &lt;span v-pre&gt;<code>{{$trigger.body.keys[0]}}</code>&lt;/span&gt;.</p>
<p>Create a <strong>Webhook/Request URL</strong> operation with the POST method and paste in your Zapier hook URL. You can add any data required by your Zap in the body.</p>
<p>Examples of what you can do with Zapier for social media automation includes posting to Facebook, Instagram, or LinkedIn.</p>
<h2>Conclusion</h2>
<p>By leveraging the capabilities of Directus, from automated translations to content posting, you have the ability to operate your blog effectively. These five automation strategies not only streamline your workflow but also ensure that you cut down on the actions you need to take between the writing and promotion process.</p>]]></content>
        <author>
            <name>Muhammed Ali</name>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[What is Directus?]]></title>
        <id>/tv/directus-academy/what-is-directus</id>
        <link href="/tv/directus-academy/what-is-directus"/>
        <updated>2024-09-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this introduction, you will learn about the Directus platform, offering rich developer tools and an intuitive web application for managing data-driven projects⁠.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Web3, Blockchain, and dApps with Ben]]></title>
        <id>/tv/learning-things-i-love-to-hate/web-3-blockchain</id>
        <link href="/tv/learning-things-i-love-to-hate/web-3-blockchain"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin is joined by Ben Greenberg to tackle his learning on topics around Web3, blockchain, and decentralized apps. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Sports Betting Platform with TDSOFT]]></title>
        <id>/tv/i-made-this/sports-betting-platform-tdsoft</id>
        <link href="/tv/i-made-this/sports-betting-platform-tdsoft"/>
        <updated>2023-03-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[John and Pedro are joined by guest Dariusz Tarczyński (CEO and Founder of TDSOFT). He and his team are building some innovative applications in the sports betting space.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Version]]></title>
        <id>/tv/leap-week/version</id>
        <link href="/tv/leap-week/version"/>
        <updated>2023-10-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At Leap Week 1, we announced the availability of Content Versioning for the Directus Editor and the new Content Versions API, along with new events for Directus Automate.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Real-Life AI Content Workflows with Directus MCP Workshop]]></title>
        <id>/tv/directus-mcp-server/real-life-ai-content-workflows-directus-mcp</id>
        <link href="/tv/directus-mcp-server/real-life-ai-content-workflows-directus-mcp"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join this comprehensive workshop exploring real-world AI content workflows with the Directus MCP Server. See practical examples, best practices, and advanced techniques for integrating AI into your daily content management processes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Sean's Dead Plants]]></title>
        <id>/tv/automate-my-life/seans-dead-plants</id>
        <link href="/tv/automate-my-life/seans-dead-plants"/>
        <updated>2024-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Sean loves plants, but he can't stop killing them. Can our crack team of experts save him from his murderous streak?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Copilot]]></title>
        <id>/tv/beyond-the-core/directus-copilot</id>
        <link href="/tv/beyond-the-core/directus-copilot"/>
        <updated>2024-02-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Esther speaks to community member Donald about Directus Copilot - a panel extension allows users to ask contextual questions about their data within Insights dashboards.

]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[August 2024]]></title>
        <id>/tv/the-changelog/1-august-2024</id>
        <link href="/tv/the-changelog/1-august-2024"/>
        <updated>2024-08-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, a new way of taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes the MUX Uploader extension contribution, Hannes talking though solving hard problems with the Directus 11 access policies and Kevin with a tutorial on using the Directus UI library in extensions.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Getting Started with AgencyOS]]></title>
        <id>/tv/mastering-agencyos/getting-started-with-agencyos</id>
        <link href="/tv/mastering-agencyos/getting-started-with-agencyos"/>
        <updated>2023-10-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, we'll cover how to quickly spin up and deploy an instance of AgencyOS.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[February 2026]]></title>
        <id>/tv/the-changelog/February-2026</id>
        <link href="/tv/the-changelog/February-2026"/>
        <updated>2026-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes an AI update from Bryant and a new community program to get involved with from Beth.

]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Connect to ChatGPT]]></title>
        <id>/tv/directus-mcp-server/mcp-chatgpt</id>
        <link href="/tv/directus-mcp-server/mcp-chatgpt"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Connect Directus to ChatGPT using the native Directus MCP Server. This episode demonstrates how to set up the MCP integration with ChatGPT, enabling AI-powered content management and data operations directly from your ChatGPT conversations.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Expensify Clone]]></title>
        <id>/tv/100-apps-100-hours/expenses-system</id>
        <link href="/tv/100-apps-100-hours/expenses-system"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Expensify is an expense management tool that tracks receipts, rolls them up into business reports and documents, and handles the approval workflow. Bryant has one hour to build it (or die trying).]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Customer Relationship Manager]]></title>
        <id>/tv/100-apps-100-hours/crm</id>
        <link href="/tv/100-apps-100-hours/crm"/>
        <updated>2024-04-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this intense one-hour challenge, watch as Bryant incredibly builds a full-featured custom CRM from the ground up using Directus. He builds contacts, organizations, deal pipelines, activities, and more.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bobby Tail and the Digital Library]]></title>
        <id>/tv/bobby-tails-little-library/bobby-tail-digital-library</id>
        <link href="/tv/bobby-tails-little-library/bobby-tail-digital-library"/>
        <updated>2024-12-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bobby Tail learns about Directus and its many capabilities, including the Data Studio and Data Engine. Read by Beth Loft.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[WTF is a DXP? ]]></title>
        <id>/tv/buzzword-wilderness/wtf-is-a-dxp</id>
        <link href="/tv/buzzword-wilderness/wtf-is-a-dxp"/>
        <updated>2024-04-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Lately, it seems like DXPs, or digital experience platforms, are all anyone can talk about. But WTF are DXPs (other than a fancy acronym), and why should anyone care about them? Touch grass with Matt as he shares his POV on the origin, meaning, and application of DXPs.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Filtering of JSON Objects]]></title>
        <id>/tv/request-review/json-filtering</id>
        <link href="/tv/request-review/json-filtering"/>
        <updated>2024-01-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on January 11 2024, Rijk, Jonathan, and Daniel discuss filtering inside of stored JSON objects. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Transformation Presets]]></title>
        <id>/tv/short-hops/transformation-presets</id>
        <link href="/tv/short-hops/transformation-presets"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover the power of transformation presets in Directus to streamline your image handling processes. Learn how to create and use these presets to standardize image formats, sizes, and even apply custom transformations across your entire website or application]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[New Years Resolution Bingo Generator]]></title>
        <id>/tv/100-apps-100-hours/new-years-resolution-bingo-generator</id>
        <link href="/tv/100-apps-100-hours/new-years-resolution-bingo-generator"/>
        <updated>2026-03-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant is joined by Marc and Alvaro with the goal of building a goal bingo app in just 10 minutes using the MCP.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build a Dialer Panel Extension in Directus Insights with Twilio]]></title>
        <id>/tv/enter-the-workshop/twiliio-dialer-panel-extension</id>
        <link href="/tv/enter-the-workshop/twiliio-dialer-panel-extension"/>
        <updated>2024-06-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Kevin and Nathaniel Okenwa, Developer Evangelist at Twilio, as they utilize Twilio's Voice SDK to build a Directus extension that allows outbound phone calls directly from the browser.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Get Started with Directus and Nuxt]]></title>
        <id>/tv/stack-up/nuxt</id>
        <link href="/tv/stack-up/nuxt"/>
        <updated>2024-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn how to use the Directus SDK with a Nuxt application.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Santosh Ahuja, Lead Engineer at Optum]]></title>
        <id>/tv/trace-talks/santosh-ahuja</id>
        <link href="/tv/trace-talks/santosh-ahuja"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We trace Santosh's journey from a business family to a Lead Engineer, driven by a passion for technology and the influential ethos of Chuck Norris in 'Walker, Texas Ranger'.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's in your Dock, Rijk?]]></title>
        <id>/tv/whats-in-your-dock/rijk</id>
        <link href="/tv/whats-in-your-dock/rijk"/>
        <updated>2024-09-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Rijk is a developer and designer born and raised in the Netherlands. He first came to America to work on Directus where he is CTO to this day. When not coding you can find him playing bass in the Lower East Side or hanging out with his cats in Brooklyn. Coder-designer by nature, musician at heart; prefers code to be indented in threes. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Echobind]]></title>
        <id>/tv/agency-corner/echobind</id>
        <link href="/tv/agency-corner/echobind"/>
        <updated>2024-09-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Meet Echobind, a product consultancy and digital agency who work with companies looking to build ambitious software, and hear about how they use Directus for multiple clients such as Annex, an e-commerce platform for document storage warehouses and Hope Media Group, a Christian media conglomerate that fosters faith and community.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mistakes]]></title>
        <id>/tv/community-question-time/mistakes</id>
        <link href="/tv/community-question-time/mistakes"/>
        <updated>2023-01-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How do we balance business and community? What have been some of our mistakes? What vision did you sell to VCs? How else might we make money? ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus AI Overview]]></title>
        <id>/tv/ai/ai-overview</id>
        <link href="/tv/ai/ai-overview"/>
        <updated>2024-05-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[​Hear from our CTO Rijk about our philosophy on AI’s role in Directus and beyond, and then our Director of Developer Relations Kevin on our brand new AI extensions.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Q1 2024]]></title>
        <id>/tv/quarterly-customer-qna/q1-2024</id>
        <link href="/tv/quarterly-customer-qna/q1-2024"/>
        <updated>2024-01-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What's the biggest thing we've done for customers in 2023? What's coming up this year? And what's this new Directus TV thing all about? Join Will for our first video Quarterly Customer Q&A. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Conditional Fields ]]></title>
        <id>/tv/short-hops/conditional-fields</id>
        <link href="/tv/short-hops/conditional-fields"/>
        <updated>2024-03-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to conditionally show and hide fields inside of Directus Editor.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Leap Week 02: Full Keynote]]></title>
        <id>/tv/leap-week/02-keynote</id>
        <link href="/tv/leap-week/02-keynote"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The full keynote from our second Leap Week in March 2024]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's In Your Dock, Cassidy?]]></title>
        <id>/tv/whats-in-your-dock/cassidoo</id>
        <link href="/tv/whats-in-your-dock/cassidoo"/>
        <updated>2024-03-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Cassidy is a software engineer, advisor, developer advocate, investor, and memer on the internet, and here's the software, hardware, and analog tools she uses in her day-to-day.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Start]]></title>
        <id>/tv/ready-set-battlesnake/start</id>
        <link href="/tv/ready-set-battlesnake/start"/>
        <updated>2024-03-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In the first episode of the season, Kevin and Andrew bring you on the journey of learning about Battlesnake by building an (arguably) capable autonomous snake based on Node.js. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Trees of London]]></title>
        <id>/tv/democratizing-data/trees-of-london</id>
        <link href="/tv/democratizing-data/trees-of-london"/>
        <updated>2024-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The Greater London Authority publishes a dataset of over 815,000 of London's Local Authority Maintained Trees. Kevin shows you how Directus can be used to further explore, understand, and remix this dataset both in the Data Studio and via API.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: AI Letters to Santa]]></title>
        <id>/tv/100-apps-100-hours/ai-letters-to-santa</id>
        <link href="/tv/100-apps-100-hours/ai-letters-to-santa"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant builds a holiday-themed app that generates personalized letters from "Open Source Santa" based on GitHub profiles. Watch as he creates a system that analyzes developers' repositories, determines whether they're on the open source naughty or nice list, and generates snarky, sarcastic letters from Santa — complete with festive styling and holiday cheer.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[MCP 101: What It Actually Does]]></title>
        <id>/tv/mcp-showcase/mcp-101</id>
        <link href="/tv/mcp-showcase/mcp-101"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant as he talks through the details of the Directus MCP.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Naz Delam, Software Engineering Manager at LinkedIn]]></title>
        <id>/tv/trace-talks/naz-delam</id>
        <link href="/tv/trace-talks/naz-delam"/>
        <updated>2024-05-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of Trace Talks, engineering leader Naz Delam shares her journey to LinkedIn, highlighting key leadership lessons she's learned along the way – from challenges of leading teams during tech layoffs to empowering teams through autonomy and ownership.

Naz also offers practical advice on handling layoffs, continuous learning, and effective networking. 

This episode is a MUST-LISTEN for any aspiring leaders in the tech industry.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Data Modeling]]></title>
        <id>/tv/technically-im-lost/data-modeling</id>
        <link href="/tv/technically-im-lost/data-modeling"/>
        <updated>2024-07-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What happens when a non-technical person attempts to get technical? In episode 1 of Technically I'm Lost (TIL), non-technical marketer Matt attempts to build the data model for a partner directory from scratch, with help from resident Directus expert Bryant. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Data Studio Translations in Directus]]></title>
        <id>/tv/translation-station/data-studio-translations-in-directus</id>
        <link href="/tv/translation-station/data-studio-translations-in-directus"/>
        <updated>2025-02-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus comes in several languages translated kindly by our community. In this episode, Carmen will show you how to contribute your own translations using Crowdin.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Using GeoJSON fields in Directus]]></title>
        <id>/tv/uncharted-territory/geojson-fields</id>
        <link href="/tv/uncharted-territory/geojson-fields"/>
        <updated>2025-03-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus comes with numerous types of geospatial data presentation. In this episode, Carmen will walk you through the different fields and how to use them.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What are Image Transformations]]></title>
        <id>/tv/sharp-focus/image-transformations</id>
        <link href="/tv/sharp-focus/image-transformations"/>
        <updated>2024-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Carmen as we learn what image transformations are and how you can assign permissions to them in Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[5 Minute Demo From Our CEO]]></title>
        <id>/tv/discover-directus/ceo-5</id>
        <link href="/tv/discover-directus/ceo-5"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join our CEO Ben Haynes for a rapid 5 minutes demo of the Directus platform.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Lazy Programmer]]></title>
        <id>/tv/dev-thoughts/lazy-programmer</id>
        <link href="/tv/dev-thoughts/lazy-programmer"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Some developers are efficient. Maybe a little too efficient.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Data Management]]></title>
        <id>/tv/directus-academy/data-management</id>
        <link href="/tv/directus-academy/data-management"/>
        <updated>2024-09-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover how Directus empowers you to manage data, create collections, customize fields, and streamline content workflows with features like Explore, Editor, and multilingual support.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Keep the Money Flow - Berlin Meetup]]></title>
        <id>/tv/around-the-world/keep-the-money-flow</id>
        <link href="/tv/around-the-world/keep-the-money-flow"/>
        <updated>2023-07-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Andreas uses Directus in lots of personal projects to improve the efficiency of his household. In this talk, we hear about his pocket money tracker used with his kids.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Idea]]></title>
        <id>/tv/digging-the-rabbit-hole/idea</id>
        <link href="/tv/digging-the-rabbit-hole/idea"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, Matt and Kevin discuss the inspiration behind the Directus TV project.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Integrating Firecrawl with Directus]]></title>
        <id>/tv/quick-connect/firecrawl</id>
        <link href="/tv/quick-connect/firecrawl"/>
        <updated>2024-10-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Integrating Firecrawl with Directus]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What does authentication mean?]]></title>
        <id>/tv/authentication-ave/what-does-authentication-mean</id>
        <link href="/tv/authentication-ave/what-does-authentication-mean"/>
        <updated>2025-01-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Authentication happens many times a day without us even realizing. In this walk down Authentication Avenue, Kevin answers "What does authentication mean?"]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Creativity in Code]]></title>
        <id>/tv/bridging-bytes/creativity-in-code</id>
        <link href="/tv/bridging-bytes/creativity-in-code"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A discussion on the evolution and future of online experiences, including practical advice on innovation, technology trends, and the role of AI and AR/VR.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[L'Oreal (Because You’re Worth It) ]]></title>
        <id>/tv/joy-of-theming/loreal-group-theme</id>
        <link href="/tv/joy-of-theming/loreal-group-theme"/>
        <updated>2024-03-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Let's grab our keyboards and mice, and let the beauty adventure begin because YOU are worth it. Join Bry as he picks up his brushes to create a L’Oréal Paris theme in Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Downtown Nashville]]></title>
        <id>/tv/scenescapes/downtown-nashville</id>
        <link href="/tv/scenescapes/downtown-nashville"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Filmed by John, a beautiful day in Nashville. Enjoy the sun rise and set with some chill beats.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Sending SMS Messages with Vonage]]></title>
        <id>/tv/quick-connect/vonage</id>
        <link href="/tv/quick-connect/vonage"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a prompt which allows customer notifications via text message using the Vonage SMS API]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Leap Week 3: Full Keynote]]></title>
        <id>/tv/leap-week/leap-week-3-keynote</id>
        <link href="/tv/leap-week/leap-week-3-keynote"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The full keynote from our third Leap Week in June 2024.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.1]]></title>
        <id>/tv/release-notes/10-1</id>
        <link href="/tv/release-notes/10-1"/>
        <updated>2023-05-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[With over 70 improvements, bug fixes, and new additions, Rijk, our lead maintainer, guides you through the significant updates we've released.
]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Database Vendor | Migration | Server Location and GDPR Compliance]]></title>
        <id>/tv/from-the-field/db-vendor-migration-server-location-gdpr</id>
        <link href="/tv/from-the-field/db-vendor-migration-server-location-gdpr"/>
        <updated>2022-10-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about Directus' own database choice and migration between projects.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Generate API Docs for Custom Collections in Directus]]></title>
        <id>/tv/community-hopline/generate-api-docs-custom-collections</id>
        <link href="/tv/community-hopline/generate-api-docs-custom-collections"/>
        <updated>2025-10-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant as he answers a community question "is there an automated way to generate API docs for my custom collections inside Directus?"]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Shipping Case Study Content 5x Faster with AI]]></title>
        <id>/tv/content-power-hour/case-study-content-with-ai</id>
        <link href="/tv/content-power-hour/case-study-content-with-ai"/>
        <updated>2025-09-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Matt turns a customer interview into a published case study in under an hour. Watch him go from raw transcript to live website using his method that cuts production time from 5 hours to 1. No more staring at blank pages or endless formatting! ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Code Review Hogwarts]]></title>
        <id>/tv/dev-thoughts/code-review</id>
        <link href="/tv/dev-thoughts/code-review"/>
        <updated>2024-03-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Welcome to the wizarding world of code reviews.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Getting Started With WebSockets]]></title>
        <id>/tv/make-it-realtime/getting-started-with-websockets</id>
        <link href="/tv/make-it-realtime/getting-started-with-websockets"/>
        <updated>2023-06-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this short tutorial, you'll learn how to quickly add real time data to your application or project using WebSockets and Directus Realtime. Follow along with Bryant as he covers the three easy steps to get started with WebSockets.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[European Athletics]]></title>
        <id>/tv/client-cache/european-athletics</id>
        <link href="/tv/client-cache/european-athletics"/>
        <updated>2024-07-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover how European Athletics use Directus as they support 51 national federations for track and field sport and organize events including the European Athletics Championships. Starting with their aim to bring their website in-house, hear about how Directus powers the European Athletics website and their ambitious plans to treat it as the backbone of their digital strategy.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Constant]]></title>
        <id>/tv/dev-thoughts/constant</id>
        <link href="/tv/dev-thoughts/constant"/>
        <updated>2024-03-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The only thing standing between a variable and its destiny is the courage to compile a different story.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Access Control]]></title>
        <id>/tv/technically-im-lost/technically-access-control</id>
        <link href="/tv/technically-im-lost/technically-access-control"/>
        <updated>2024-07-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What happens when a non-technical person attempts to get technical? In episode 2 of Technically I'm Lost (TIL), non-technical marketer Matt attempts to set-up roles and permissions for a partner directory from scratch, with help from resident Directus expert Bryant. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build an Audio Podcast Summarizer in Directus Automate with Deepgram]]></title>
        <id>/tv/enter-the-workshop/deepgram-audio-podcast-summarizer</id>
        <link href="/tv/enter-the-workshop/deepgram-audio-podcast-summarizer"/>
        <updated>2024-06-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Kevin and Damien Murphy, Solutions Engineer at Deepgram, as they use Deepgram to build an audio podcast summarizer in Directus Automate.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Global Bookmarks]]></title>
        <id>/tv/short-hops/global-bookmarks</id>
        <link href="/tv/short-hops/global-bookmarks"/>
        <updated>2024-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to create preset layouts, including filters and sorts, inside of Directus Explore.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's in your Dock, Bryant?]]></title>
        <id>/tv/whats-in-your-dock/what's-in-your-dock-bryant</id>
        <link href="/tv/whats-in-your-dock/what's-in-your-dock-bryant"/>
        <updated>2025-03-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant Gillespie, Growth Engineer at Directus takes us through his most used apps and websites.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Linear]]></title>
        <id>/tv/dev-thoughts/linear</id>
        <link href="/tv/dev-thoughts/linear"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Development is hard. Estimating how long it takes based on shirt size is harder-er.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Marketplace Beta]]></title>
        <id>/tv/leap-week/02-marketplace-beta</id>
        <link href="/tv/leap-week/02-marketplace-beta"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Introducing the Directus Marketplace Beta: distribute and install extensions in any Directus project and supercharge your projects.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Maintenance Windows | Adding Languages | Realtime Updates]]></title>
        <id>/tv/from-the-field/maintenance-windows-adding-languages-realtime-updates</id>
        <link href="/tv/from-the-field/maintenance-windows-adding-languages-realtime-updates"/>
        <updated>2022-10-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about how we manage state inside of Directus, adding languages, and realtime support.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Setting up MCP: Start Building Today]]></title>
        <id>/tv/mcp-showcase/setting-up-mcp</id>
        <link href="/tv/mcp-showcase/setting-up-mcp"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant and Lindsey as they show you how to get started with the MCP.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Open Source Outlook]]></title>
        <id>/tv/bridging-bytes/open-source-outlook</id>
        <link href="/tv/bridging-bytes/open-source-outlook"/>
        <updated>2024-03-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A discussion on the past, present, and future of open source, including what it means to be "Post-Open" and build sustainable businesses.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Using the Map Layout in Directus]]></title>
        <id>/tv/uncharted-territory/using-the-map-layout-in-directus</id>
        <link href="/tv/uncharted-territory/using-the-map-layout-in-directus"/>
        <updated>2025-03-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus allows you to display geospatial data in the content module. In this episode, Carmen will walk you through how to use the Map Layout in Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Image Manipulation]]></title>
        <id>/tv/sharp-focus/image-manipulation</id>
        <link href="/tv/sharp-focus/image-manipulation"/>
        <updated>2024-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Carmen as we apply focal points and other manipulations to our images.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Multisite CMS]]></title>
        <id>/tv/100-apps-100-hours/multisite-cms</id>
        <link href="/tv/100-apps-100-hours/multisite-cms"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Amid the WordPress drama, Bryant builds a multi-site CMS system that lets you manage content for multiple websites from a single Directus instance. Watch as he sets up relationships between sites, pages, and users, then configures advanced permissions to ensure content editors can only access sites they're assigned to—all while creating a secure API structure for your frontend applications.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[String Translations in Directus]]></title>
        <id>/tv/translation-station/string-translations-in-directus</id>
        <link href="/tv/translation-station/string-translations-in-directus"/>
        <updated>2025-02-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Did you know you can individually translate labels, notes, and other aspects of your Directus project? In this episode, Carmen will show you how to use Directus’ translation strings.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.2]]></title>
        <id>/tv/release-notes/10-2</id>
        <link href="/tv/release-notes/10-2"/>
        <updated>2023-05-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, Esther unveils a multitude of enhancements, bug fixes, and novel additions, all designed to make your data management experience smoother and more efficient.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What is access control?]]></title>
        <id>/tv/authentication-ave/what-is-access-control</id>
        <link href="/tv/authentication-ave/what-is-access-control"/>
        <updated>2025-01-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[It's not just enough to give someone access, but sometimes you need to say what they can do. In this stroll down Authentication Avenue, Kevin answers "What is access control?"]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Platform to Monetize Communities]]></title>
        <id>/tv/i-made-this/platform-to-monetize-communities</id>
        <link href="/tv/i-made-this/platform-to-monetize-communities"/>
        <updated>2023-03-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of I Made This, Pedro and John chat with Bryant to learn why he chose Directus and walk-through how he built a community monetization platform.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Lightspeed Retail API]]></title>
        <id>/tv/community-hopline/lightspeed-retail-api</id>
        <link href="/tv/community-hopline/lightspeed-retail-api"/>
        <updated>2025-10-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant as he answers a community question "I'm integrating Directus with the Lightspeed Retail API [...] has anyone successfully implemented a setup like this?".]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Multitenancy in Directus - Berlin Meetup]]></title>
        <id>/tv/around-the-world/multitenancy-at-hybrid-heroes</id>
        <link href="/tv/around-the-world/multitenancy-at-hybrid-heroes"/>
        <updated>2023-07-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We will walk you through the steps to implement segregation of client data using Directus fields and scripts to create the appropriate roles and permissions. We will also showcase a real-world use case and how our client benefit.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Edge Computing with Pandelis]]></title>
        <id>/tv/learning-things-i-love-to-hate/edge-computing</id>
        <link href="/tv/learning-things-i-love-to-hate/edge-computing"/>
        <updated>2024-01-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin is joined by Pandelis to properly understand edge computing, and how they evolve from CDNs and distributed compute.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Berlin at Night]]></title>
        <id>/tv/scenescapes/berlin-at-night</id>
        <link href="/tv/scenescapes/berlin-at-night"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Filmed by Kevin, a cold winter evening at Alexanderplatz - where trams, trains, and people meet.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Workout App]]></title>
        <id>/tv/100-apps-100-hours/workout-app</id>
        <link href="/tv/100-apps-100-hours/workout-app"/>
        <updated>2026-03-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant is joined by Matt with the goal of building a workout fitness app in just 10 minutes using the MCP.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Gavin Doughtie, Senior Developer at Aon]]></title>
        <id>/tv/trace-talks/gavin-doughtie</id>
        <link href="/tv/trace-talks/gavin-doughtie"/>
        <updated>2024-01-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Delve into Gavin's unique journey from his early years in the entertainment industry to his evolution as a developer, and gain insights into the intersection of technology and corporate dynamics.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude Desktop Installation]]></title>
        <id>/tv/directus-mcp-server/claude-desktop-mcp-installation</id>
        <link href="/tv/directus-mcp-server/claude-desktop-mcp-installation"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to install and configure the Directus MCP Server with Claude Desktop. This episode walks through the complete setup process, from getting your Directus credentials to configuring the MCP server for seamless AI-powered content management.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[September 2024]]></title>
        <id>/tv/the-changelog/2-september-2024</id>
        <link href="/tv/the-changelog/2-september-2024"/>
        <updated>2024-09-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes a community showcase from websyte.ai, Rijk taking us through what's in his dock and Kevin with a tutorial on complex media transformations.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Connect to Cursor]]></title>
        <id>/tv/directus-mcp-server/mcp-cursor</id>
        <link href="/tv/directus-mcp-server/mcp-cursor"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Integrate the native Directus MCP Server with Cursor IDE for enhanced AI-powered development workflows. This episode shows you how to configure the MCP connection in Cursor, enabling seamless content management and database operations while you code.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Soft Delete]]></title>
        <id>/tv/short-hops/soft-delete</id>
        <link href="/tv/short-hops/soft-delete"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to set up and use the archive functionality to declutter your views, improve workflow, and maintain a clean data structure without permanently deleting items. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Configuration as Code]]></title>
        <id>/tv/request-review/config-as-code</id>
        <link href="/tv/request-review/config-as-code"/>
        <updated>2024-02-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on January 25 2024, Rijk, Jonathan, and Daniel discuss configuration as code.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Transcribe Audio Files with Deepgram]]></title>
        <id>/tv/quick-connect/deepgram</id>
        <link href="/tv/quick-connect/deepgram"/>
        <updated>2024-01-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Automatically transcribe new audio files with Deepgram's Speech-to-Text API.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[American Express (Don’t Leave Home Without It)]]></title>
        <id>/tv/joy-of-theming/american-express-theme</id>
        <link href="/tv/joy-of-theming/american-express-theme"/>
        <updated>2024-04-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Home is where your heart is and – just like your wallet – you should never leave home without it. Join Bry as he builds a happy little theme inspired by American Express.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Access Control]]></title>
        <id>/tv/directus-academy/access-control</id>
        <link href="/tv/directus-academy/access-control"/>
        <updated>2024-09-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This video explains Directus' access control system, covering user management, authentication, roles, permissions, and policies for secure and flexible data management.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Get Started with Directus and Astro]]></title>
        <id>/tv/stack-up/astro</id>
        <link href="/tv/stack-up/astro"/>
        <updated>2024-02-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn how to use the Directus SDK with an Astro application.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus 11 Release Candidate]]></title>
        <id>/tv/leap-week/directus-11-rc</id>
        <link href="/tv/leap-week/directus-11-rc"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 11 is here with policies - our key new feature making access control more powerful and flexible in your projects.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Project Templates In AgencyOS]]></title>
        <id>/tv/mastering-agencyos/project-templates-agencyos</id>
        <link href="/tv/mastering-agencyos/project-templates-agencyos"/>
        <updated>2023-10-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn more about project templates and how they work within AgencyOS.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Learning Management System]]></title>
        <id>/tv/100-apps-100-hours/lms</id>
        <link href="/tv/100-apps-100-hours/lms"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[From creating and organizing course content, managing instructors and students, and tracking enrollments and completions, Bryant has sixty minutes on the clock.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build a Realtime Chat App]]></title>
        <id>/tv/make-it-realtime/realtime-chat-app-websockets</id>
        <link href="/tv/make-it-realtime/realtime-chat-app-websockets"/>
        <updated>2023-06-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this tutorial, you'll learn how to build a simple multi-user chat application powered by Directus Realtime and WebSockets in less than 10 minutes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Compose]]></title>
        <id>/tv/leap-week/compose</id>
        <link href="/tv/leap-week/compose"/>
        <updated>2023-10-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At Leap Week 1, we introduced and explained our approach on composability, along with announcing AgencyOS - an all-in-one operating system for digital agencies.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Shows]]></title>
        <id>/tv/digging-the-rabbit-hole/shows</id>
        <link href="/tv/digging-the-rabbit-hole/shows"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, Bryant and Kevin discuss what shows are being commissioned and why. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Media AI Bundle]]></title>
        <id>/tv/beyond-the-core/media-ai-bundle</id>
        <link href="/tv/beyond-the-core/media-ai-bundle"/>
        <updated>2024-02-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Esther speaks to community member Marcus about their Media AI Bundle - a group of extensions that allow extraction of key details using AI tools. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bobby Tail and the Magical Guest List]]></title>
        <id>/tv/bobby-tails-little-library/bobby-tail-magical-guest-list</id>
        <link href="/tv/bobby-tails-little-library/bobby-tail-magical-guest-list"/>
        <updated>2024-12-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bobby Tail learns about the authentication concepts of usernames, password, two-factor authentication, access control, and single sign-on. Read by Carmen Huidobro.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Alt Text Writer]]></title>
        <id>/tv/ai/ai-alt-text-writer</id>
        <link href="/tv/ai/ai-alt-text-writer"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Create captions for your images within Directus Files with this custom operation, powered by Clarifai.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Q2 2024]]></title>
        <id>/tv/quarterly-customer-qna/q2-2024</id>
        <link href="/tv/quarterly-customer-qna/q2-2024"/>
        <updated>2024-08-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Director of Customer Success Will gives a rundown of the latest news, product updates, and more from Q2 of 2024. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[David Simmer, Senior Software Engineer at Netflix]]></title>
        <id>/tv/trace-talks/david-simmer</id>
        <link href="/tv/trace-talks/david-simmer"/>
        <updated>2024-06-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of Trace Talks, David Simmer, Senior Software Engineer at Netflix, shares his unique journey into tech, starting from college AV/IT jobs to helping technical teams be more productive at Netflix. He talks about the importance of empathy in engineering, balancing hands-on coding with leadership, and using generative AI to boost efficiency. David also offers practical advice for aspiring leaders on seizing opportunities and promoting themselves. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's In Your Dock, Salma?]]></title>
        <id>/tv/whats-in-your-dock/salma</id>
        <link href="/tv/whats-in-your-dock/salma"/>
        <updated>2024-03-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Salma is a live streamer, software engineer, and developer educator, and here's the software, hardware, and analog tools she uses in her day-to-day.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Automate]]></title>
        <id>/tv/ready-set-battlesnake/automate</id>
        <link href="/tv/ready-set-battlesnake/automate"/>
        <updated>2024-04-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin and Andrew migrate their snake to run entirely in Directus Automate's low-code builder Flows. Is this the right way to run a Battlesnake? Tune in and find out.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[WTF is a CMS]]></title>
        <id>/tv/buzzword-wilderness/wtf-is-a-cms</id>
        <link href="/tv/buzzword-wilderness/wtf-is-a-cms"/>
        <updated>2024-05-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Is content data? Or is data content? And is a content management system really just a data management system? In this episode, Matt muses over what a CMS is, what a DMS is, and if there's a difference. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Meteorite Landings]]></title>
        <id>/tv/democratizing-data/meteorite-landings</id>
        <link href="/tv/democratizing-data/meteorite-landings"/>
        <updated>2024-06-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[NASA publishes a dataset of over 34,000 meteorite landings as recorded by The Meteoritical Society.  Kevin shows you how Directus can be used to further explore, understand, and remix this dataset both in the Data Studio and via API.
]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Equipment Booking Manager ]]></title>
        <id>/tv/100-apps-100-hours/equipment-booking-manager</id>
        <link href="/tv/100-apps-100-hours/equipment-booking-manager"/>
        <updated>2024-04-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In a fast and furious hour, Bryant tackles building an equipment booking manager. Watch as he designs and implement a system to manage videography and photography gear, including features for checking items in and out, reserving equipment, and maintaining a comprehensive inventory dashboard.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Extending Directus for an Online Art Gallery - London Meetup]]></title>
        <id>/tv/around-the-world/extending-online-art-gallery</id>
        <link href="/tv/around-the-world/extending-online-art-gallery"/>
        <updated>2023-08-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Using Directus extensions, we augmented Directus to add functionality to generate blurhashes for the smooth loading of high-quality full-screen images.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Rabbitar Directory]]></title>
        <id>/tv/100-apps-100-hours/rabbitar-directory</id>
        <link href="/tv/100-apps-100-hours/rabbitar-directory"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Hop along with Bryant as he attempts to build a management system for AI-generated rabbit avatars ("Rabbitars"). Watch him tackle importing hundreds of images, setting up trait relationships, and creating an automated flow to generate new characters with DALL-E—all in just 60 minutes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Connect]]></title>
        <id>/tv/directus-academy/directus-connect</id>
        <link href="/tv/directus-academy/directus-connect"/>
        <updated>2024-09-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This video introduces Directus Connect — automatic REST and GraphQL APIs for your database and asset storage, highlighting key features such as resource-based URLs, query parameters, and the Directus JavaScript SDK.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Visualize]]></title>
        <id>/tv/ready-set-battlesnake/visualize-battlesnake</id>
        <link href="/tv/ready-set-battlesnake/visualize-battlesnake"/>
        <updated>2024-04-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Having build a very powerful snake, Kevin and Andrew seek to bask in the glory of their intellect by visualizing their wins. But not is all as it seems...]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's In Your Dock, Terence?]]></title>
        <id>/tv/whats-in-your-dock/terence</id>
        <link href="/tv/whats-in-your-dock/terence"/>
        <updated>2024-04-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Terence is an open source hacker and tinkerer, works in civic tech, and here's the software, hardware, and analog tools he uses in his day-to-day.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Page Builder In AgencyOS]]></title>
        <id>/tv/mastering-agencyos/page-builder-agencyos</id>
        <link href="/tv/mastering-agencyos/page-builder-agencyos"/>
        <updated>2023-10-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn how to create dynamic pages that are on brand and look great with the page builder inside AgencyOS.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cursor Installation]]></title>
        <id>/tv/directus-mcp-server/cursor-installation</id>
        <link href="/tv/directus-mcp-server/cursor-installation"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up the Directus MCP Server with Cursor IDE for AI-powered development workflows. This episode covers the complete installation process and configuration steps to integrate Directus with Cursor for enhanced coding and content management.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Get Started with Directus and SvelteKit]]></title>
        <id>/tv/stack-up/sveltekit</id>
        <link href="/tv/stack-up/sveltekit"/>
        <updated>2024-02-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn how to use the Directus SDK with a SvelteKit application.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Production]]></title>
        <id>/tv/digging-the-rabbit-hole/production</id>
        <link href="/tv/digging-the-rabbit-hole/production"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, Kevin discusses the efficient processes created to enable show creation at scale. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Public User Registration]]></title>
        <id>/tv/leap-week/public-user-registration</id>
        <link href="/tv/leap-week/public-user-registration"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We've shipped public user registration in Directus 10, allowing users to register for your project without the need for complex permission setups. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Focal Point Detection]]></title>
        <id>/tv/ai/ai-focal-point-detection</id>
        <link href="/tv/ai/ai-focal-point-detection"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Determine an image's primary point of interest with this custom operation, powered by OpenAI.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Home Depot (How Doers Get More Done)]]></title>
        <id>/tv/joy-of-theming/the-home-depot-theme</id>
        <link href="/tv/joy-of-theming/the-home-depot-theme"/>
        <updated>2024-04-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We should all take more time to share in each other’s successes as we accomplish our goals. However, it isn’t just about what you can do, but the journey of getting there. Join Bry as he supports you while painting a new theme inspired by Home Depot.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.3]]></title>
        <id>/tv/release-notes/10-3</id>
        <link href="/tv/release-notes/10-3"/>
        <updated>2023-06-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.3 has landed with a huge new feature - WebSockets support! In this video, Jan gives you the roundup of what's notable in our latest release. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Renaming Collections | Network Vulnerability Scans | Roles/Permissions]]></title>
        <id>/tv/from-the-field/renaming-collections-network-vulnerability-scans-roles-permissions</id>
        <link href="/tv/from-the-field/renaming-collections-network-vulnerability-scans-roles-permissions"/>
        <updated>2022-11-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about renaming collections, how we detect vulnerabilities, and our access control settings.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Geocoding using the Mapbox API and Directus Automate]]></title>
        <id>/tv/uncharted-territory/geocoding-using-the-mapboxapi</id>
        <link href="/tv/uncharted-territory/geocoding-using-the-mapboxapi"/>
        <updated>2025-03-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus Automate allows you to integrate with numerous services and automatically perform actions on specific triggers. In this episode, Carmen will walk you through creating an application that integrates with the Mapbox API to automatically retrieve addresses and locations.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Talent Platform with H&H]]></title>
        <id>/tv/i-made-this/talent-recruitment-platform</id>
        <link href="/tv/i-made-this/talent-recruitment-platform"/>
        <updated>2023-06-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of I Made This, Erwin and Uwe walk Bryant through one of their recent Directus projects - a engineering job listing and talent recruitment site for their client - Rockstar Recruiting.
]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Generative AI For Devs with Rizèl ]]></title>
        <id>/tv/learning-things-i-love-to-hate/ai-devs</id>
        <link href="/tv/learning-things-i-love-to-hate/ai-devs"/>
        <updated>2024-01-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin is joined by Rizèl to learn how developers REALLY use AI in their day-to-day work, including ChatGPT and GitHub Copilot.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Jordan Cutler, Senior Software Engineer at Pinterest]]></title>
        <id>/tv/trace-talks/jordan-cutler</id>
        <link href="/tv/trace-talks/jordan-cutler"/>
        <updated>2024-06-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of Trace Talks, Jordan Cutler shares his journey from intern to Senior Software Engineer at Pinterest. He discusses overcoming imposter syndrome after not getting a return offer from Twitter, and how he quickly advanced at Gusto by focusing on being a reliable team member. Jordan emphasizes the importance of curiosity, seeking feedback, and understanding the intricacies of tools and technologies. He also talks about his passion for teaching through his High Growth Engineer newsletter and courses.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bobby Tail and the Library Construction Crew]]></title>
        <id>/tv/bobby-tails-little-library/bobby-tail-library-construction-crew</id>
        <link href="/tv/bobby-tails-little-library/bobby-tail-library-construction-crew"/>
        <updated>2024-12-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bobby Tail learns about the backend concepts of data modeling, APIs and file management. Read by Cathrin Steiner.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why do tokens expire?]]></title>
        <id>/tv/authentication-ave/why-do-tokens-expire</id>
        <link href="/tv/authentication-ave/why-do-tokens-expire"/>
        <updated>2025-01-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[You can't have access forever. In this skip down Authentication Avenue, Kevin answers "Why do tokens expire?" and shows you how to get new ones.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[5 Years Time]]></title>
        <id>/tv/community-question-time/5-years-time</id>
        <link href="/tv/community-question-time/5-years-time"/>
        <updated>2024-03-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to we balance flexibility with responsibility? What does Directus in 5 years look like? What does sustainable open source look like while trying to remain fair?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[WTF is SaaS?]]></title>
        <id>/tv/buzzword-wilderness/wtf-is-saas</id>
        <link href="/tv/buzzword-wilderness/wtf-is-saas"/>
        <updated>2024-05-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[BaaS and SaaS sound a lot alike, but in practice they're entirely different. In this episode, Matt breaks down the difference between these two similar terms and offers a high-level look at both. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[How Agencies Innovate]]></title>
        <id>/tv/bridging-bytes/how-agencies-innovate</id>
        <link href="/tv/bridging-bytes/how-agencies-innovate"/>
        <updated>2024-07-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A discussion on how agencies source and assess technology, including the process of technology sourcing, evaluating technology for clients and addressing client concerns.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Passive Aggressive Bug]]></title>
        <id>/tv/dev-thoughts/passive-aggressive-bug</id>
        <link href="/tv/dev-thoughts/passive-aggressive-bug"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The phrase "to be a fly on the wall" takes on new meaning.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Connect to Claude Code]]></title>
        <id>/tv/directus-mcp-server/mcp-claude-code</id>
        <link href="/tv/directus-mcp-server/mcp-claude-code"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up the native Directus MCP Server with Claude Code for powerful command-line AI workflows. Learn how to configure the MCP integration to enable direct content management and schema operations from your terminal with Claude's autonomous coding capabilities.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[October 2024]]></title>
        <id>/tv/the-changelog/3-october-2024</id>
        <link href="/tv/the-changelog/3-october-2024"/>
        <updated>2024-10-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes a community showcase of a form builder from David, Carmen taking you through a docs preview and Kevin with a tutorial on AI web scraping API with flows.

]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Customize]]></title>
        <id>/tv/leap-week/customize</id>
        <link href="/tv/leap-week/customize"/>
        <updated>2023-10-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At Leap Week 1, we introduced our brand new theming engine for the Directus Data Studio - a reliable way to set theming options inside of Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Swag Platform]]></title>
        <id>/tv/100-apps-100-hours/swag-platform</id>
        <link href="/tv/100-apps-100-hours/swag-platform"/>
        <updated>2023-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Gifting platforms are how we get goodies in the hands of community members and customers. Bryant needs to figure out how to manage products, build pages to handle the giveaways, and manage fulfillment.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New Since the Last Leap Week]]></title>
        <id>/tv/leap-week/03-since-last-time</id>
        <link href="/tv/leap-week/03-since-last-time"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Find out about key features in Directus 10.8, 10.9, and 10.10.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Content Translations in Directus: Data Studio]]></title>
        <id>/tv/translation-station/content-translations-in-directus-data-studio</id>
        <link href="/tv/translation-station/content-translations-in-directus-data-studio"/>
        <updated>2025-02-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus allows you to set up your content to be translated into a bevy of languages. In this episode, Carmen will show you how to activate and use content translations.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Multitenancy]]></title>
        <id>/tv/short-hops/multitenancy</id>
        <link href="/tv/short-hops/multitenancy"/>
        <updated>2024-03-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to achieve multitenancy inside of a Directus project using Roles and Relational Fields.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Our Perspective on AI in Directus]]></title>
        <id>/tv/mcp-showcase/ai-in-directus</id>
        <link href="/tv/mcp-showcase/ai-in-directus"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Rijk shared how we're approaching AI at Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Enriching User Profiles with Clearbit]]></title>
        <id>/tv/quick-connect/clearbit</id>
        <link href="/tv/quick-connect/clearbit"/>
        <updated>2024-01-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Automatically enrich new user profiles with rich data from Clearbit.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Book Rating App]]></title>
        <id>/tv/100-apps-100-hours/book-rating-app</id>
        <link href="/tv/100-apps-100-hours/book-rating-app"/>
        <updated>2026-03-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant is joined by Vicky and Beth with the goal of building a book rating app in just 10 minutes using the MCP.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Q3 2024]]></title>
        <id>/tv/quarterly-customer-qna/q3-2024</id>
        <link href="/tv/quarterly-customer-qna/q3-2024"/>
        <updated>2024-11-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Director of Customer Success Will gives a rundown of the latest news, product updates, and more from Q3 of 2024. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building A/B Testing in Your CMS: A Deep Dive with Directus & PostHog]]></title>
        <id>/tv/enter-the-workshop/setting-up-ab-testing-posthog</id>
        <link href="/tv/enter-the-workshop/setting-up-ab-testing-posthog"/>
        <updated>2025-02-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us and our friends at PostHog to learn all about building A/B testing infrastructure in your CMS with their killer A/B testing functionality.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Size and Performance]]></title>
        <id>/tv/sharp-focus/size-and-performance</id>
        <link href="/tv/sharp-focus/size-and-performance"/>
        <updated>2024-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Carmen as we transform our images to be more performant and format-conscious.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mateja Sela, Engineering Manager at Bolt]]></title>
        <id>/tv/trace-talks/mateja-sela</id>
        <link href="/tv/trace-talks/mateja-sela"/>
        <updated>2024-01-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, Mateja shares his experiences in founding and managing a company in the volatile cryptocurrency/blockchain sector, and the transition to a role in a larger organization.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Improvements to Flows Debugging]]></title>
        <id>/tv/request-review/flows-log</id>
        <link href="/tv/request-review/flows-log"/>
        <updated>2024-02-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on February 8 2024, Rijk, Jonathan, and Daniel discuss improvements to our no-code automation builder - Flows.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[M Night Shamaylan]]></title>
        <id>/tv/dev-thoughts/m-night-shamalylan</id>
        <link href="/tv/dev-thoughts/m-night-shamalylan"/>
        <updated>2024-03-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Step into a thriller where the line between the coder and the debugger blurs.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Virtual Events Platform]]></title>
        <id>/tv/100-apps-100-hours/virtual-events-platform</id>
        <link href="/tv/100-apps-100-hours/virtual-events-platform"/>
        <updated>2024-05-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this 60 min sprint, Bryant builds a virtual events platform that's similar to webinar services like Zoom, Livestorm, and others. Can he build all the functionality like event scheduling, event registration, separate break out rooms, and an integration to Whereby?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building Frontend CMS within Directus]]></title>
        <id>/tv/community-hopline/building-fronted-cms-within-directus</id>
        <link href="/tv/community-hopline/building-fronted-cms-within-directus"/>
        <updated>2025-10-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant as he answers a community question "Is building frontend CMS within Directus possible?".]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[M2A Relationships]]></title>
        <id>/tv/short-hops/m2a-relationships</id>
        <link href="/tv/short-hops/m2a-relationships"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover how to use Directus's powerful many-to-any relationships to create flexible and dynamic content structures. Learn to set up a versatile page builder that allows linking diverse content types to a single parent collection. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cycle Around New York City]]></title>
        <id>/tv/democratizing-data/cycle-around-new-york-city</id>
        <link href="/tv/democratizing-data/cycle-around-new-york-city"/>
        <updated>2024-06-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Citi Bike publish the millions of rides used by their customers around New York City's thousands of stations. Kevin shows you how Directus can be used to further explore, understand, and remix this dataset both in the Data Studio and via API.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[PDF Generator]]></title>
        <id>/tv/beyond-the-core/pdf-generator</id>
        <link href="/tv/beyond-the-core/pdf-generator"/>
        <updated>2024-03-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Esther speaks to community member Bart about TextToAnything - an extension which allows generation of PDFs, QRCodes and Barcodes within Directus through the TextToAnyThing API.

Learn more about TextToAnything at texttoanything.nl/docs/directus.
]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What is a Collaborative CMS?]]></title>
        <id>/tv/mcp-showcase/collaborative-cms-mcp</id>
        <link href="/tv/mcp-showcase/collaborative-cms-mcp"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Christina talks through what we mean at Directus by 'collaborative CMS' and how the MCP is part of the story.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cost Control]]></title>
        <id>/tv/dev-thoughts/cost-control</id>
        <link href="/tv/dev-thoughts/cost-control"/>
        <updated>2024-01-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Welcome to the magical land of open source! A place where time and money have a rocky relationship.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Debug Mode]]></title>
        <id>/tv/dev-thoughts/debug-mode</id>
        <link href="/tv/dev-thoughts/debug-mode"/>
        <updated>2024-03-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Do bugs think about us as much as we think about them?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Notifications | Bulk Publishing | Localizations | SSO]]></title>
        <id>/tv/from-the-field/notifications-bulk-publishing-localizations-sso</id>
        <link href="/tv/from-the-field/notifications-bulk-publishing-localizations-sso"/>
        <updated>2022-11-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about internationalization, alerting to actions needed, and SSO support.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Visualize]]></title>
        <id>/tv/leap-week/visualize</id>
        <link href="/tv/leap-week/visualize"/>
        <updated>2023-10-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At Leap Week 1, we introduced loads of improvements to Directus Insights and had a Directus community update from our CEO Ben.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Boutique Web Agency with Studio Monty]]></title>
        <id>/tv/i-made-this/boutique-web-agency</id>
        <link href="/tv/i-made-this/boutique-web-agency"/>
        <updated>2024-01-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of I Made This, Bryant is joined by Miguel Stevens, the creative force behind Studio Monty, a Belgium-based agency specializing in lifestyle and bespoke websites. Miguel shares his journey from a childhood fascination with computers to founding his studio. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Conditional Fields & Relational Values]]></title>
        <id>/tv/request-review/conditional-fields-nested-relations</id>
        <link href="/tv/request-review/conditional-fields-nested-relations"/>
        <updated>2024-02-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on February 22 2024, Rijk, Jonathan, and Daniel discuss the support of nested relational values in conditional fields to build more flexible content editors]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.4]]></title>
        <id>/tv/release-notes/10-4</id>
        <link href="/tv/release-notes/10-4"/>
        <updated>2023-06-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.4 has landed with a new beta SDK and some small but important breaking changes. In this video, Colton gives you the roundup of what's notable in our latest release. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building Advanced Custom Operations for Directus Flows - Berlin Meetup]]></title>
        <id>/tv/around-the-world/custom-operations-take-flows-next-level</id>
        <link href="/tv/around-the-world/custom-operations-take-flows-next-level"/>
        <updated>2023-11-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus Flows allow you to automate tasks with event-based triggers. In this talk, we show an advanced use case generating reports for medical purposes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: SaaS App]]></title>
        <id>/tv/100-apps-100-hours/saas-app</id>
        <link href="/tv/100-apps-100-hours/saas-app"/>
        <updated>2024-01-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Building multi-user is a common convention when building Software as a Service. Bryant has an hour to build a todo app which manages users, teams, and ensures data can only be accessed within a team.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Generate Images and Social Posts with OpenAI]]></title>
        <id>/tv/quick-connect/openai</id>
        <link href="/tv/quick-connect/openai"/>
        <updated>2024-01-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Use OpenAI's APIs to generate images with DALL·E and social posts with GPT-4.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Looping Flows]]></title>
        <id>/tv/short-hops/looping-flows</id>
        <link href="/tv/short-hops/looping-flows"/>
        <updated>2024-04-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to chain and set up loops inside of Flows inside of Directus Automate.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Get Started with Directus and Remix]]></title>
        <id>/tv/stack-up/remix</id>
        <link href="/tv/stack-up/remix"/>
        <updated>2024-03-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn how to use the Directus SDK with a Remix application.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Client Payments with Stripe and AgencyOS]]></title>
        <id>/tv/mastering-agencyos/stripe-agencyos</id>
        <link href="/tv/mastering-agencyos/stripe-agencyos"/>
        <updated>2023-10-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video you'll learn how your clients can quickly and easily pay within their portal. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Platform]]></title>
        <id>/tv/digging-the-rabbit-hole/platform</id>
        <link href="/tv/digging-the-rabbit-hole/platform"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, Kevin walks through the code and backend of Directus TV.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's In Your Dock, Saron?]]></title>
        <id>/tv/whats-in-your-dock/saron</id>
        <link href="/tv/whats-in-your-dock/saron"/>
        <updated>2024-04-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Saron is the founder of Disco and the creator of Not a Designer, and here's the software, hardware, and analog tools she uses in her day-to-day.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Realtime Leaderboard]]></title>
        <id>/tv/100-apps-100-hours/realtime-leaderboard</id>
        <link href="/tv/100-apps-100-hours/realtime-leaderboard"/>
        <updated>2024-05-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant races against the clock to build a real-time leaderboard for the Directus Arcade game – Duckin' Cold Emails. Watch as he builds features to submit and display high scores for the game using Directus Realtime and websockets.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Image Generation]]></title>
        <id>/tv/ai/ai-image-generation</id>
        <link href="/tv/ai/ai-image-generation"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Generate new images within Directus Automate with this custom operation, powered by OpenAI.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[This Spring on Directus TV]]></title>
        <id>/tv/leap-week/02-tv-spring</id>
        <link href="/tv/leap-week/02-tv-spring"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus TV is our video streaming platform containing hours of content for developers. We're announcing our slate of new shows and renewals for Spring 2024.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Toggle]]></title>
        <id>/tv/ready-set-battlesnake/toggle</id>
        <link href="/tv/ready-set-battlesnake/toggle"/>
        <updated>2024-05-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In the season finale, Kevin and Andrew fly close to the sun to understand which snake strategies work best, by A/B testing live with DevCycle. Do they keep their wings?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Pampers (Discover Your Baby's World)]]></title>
        <id>/tv/joy-of-theming/pampers-theme</id>
        <link href="/tv/joy-of-theming/pampers-theme"/>
        <updated>2024-04-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bry and his furry companion as he paints a beautiful Pampers theme inspired by all the little ones that represent our future.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build Advanced Content Workflows: A Deep Dive with Directus + Inngest]]></title>
        <id>/tv/enter-the-workshop/advanced-content-workflows-inngest</id>
        <link href="/tv/enter-the-workshop/advanced-content-workflows-inngest"/>
        <updated>2025-03-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us and our friends at Inngest to learn all about building advanced content workflows and orchestrating automated localization for content in different languages. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Scott Adrian, Sr. Wordpress Developer]]></title>
        <id>/tv/trace-talks/scott-adrian</id>
        <link href="/tv/trace-talks/scott-adrian"/>
        <updated>2024-06-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode of Trace Talks, Scott Adrian shares his journey from professional piercer and musician to Senior WordPress Engineer. Scott discusses his early experiences with web development, starting with Myspace layouts for his band, and his first tech job at 1 Stop Internet. He highlights his transition to WordPress, leading teams, and mentoring junior developers. Scott also talks about balancing his career with personal projects, including publishing a sci-fi fantasy novel and developing a WordPress-based RPG game. Tune in for insights on career growth, leadership, and leveraging AI in development.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Ali Payne, Lead Engineer at Stitch Fix]]></title>
        <id>/tv/trace-talks/ali-payne</id>
        <link href="/tv/trace-talks/ali-payne"/>
        <updated>2024-02-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Ali shares his relentless passion for technology and his commitment to staying abreast of current trends and developments. Tracing his journey from Ohio to the tech hubs of the West, Ali discusses how he perceives every technological challenge as a puzzle waiting to be solved.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[WTF is Composable?]]></title>
        <id>/tv/buzzword-wilderness/wtf-is-composable</id>
        <link href="/tv/buzzword-wilderness/wtf-is-composable"/>
        <updated>2024-06-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[These days, it's nearly impossible to avoid the term "composable." But what does it actually mean in the context of development? ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Extended Security Updates for Directus 10.12]]></title>
        <id>/tv/leap-week/extended-security-updates-directus-10-12</id>
        <link href="/tv/leap-week/extended-security-updates-directus-10-12"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Announcing Extended Security Updates (ESU) for Directus 10.12 until the end of 2024, ensuring critical security updates and a flexible upgrade timeframe for our users.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[November 2024]]></title>
        <id>/tv/the-changelog/4-november-2024</id>
        <link href="/tv/the-changelog/4-november-2024"/>
        <updated>2024-11-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes a community showcase of a note taking system from Josh, Carmen taking you the first episode of Sharp Focus, Kevin with the new guest Author program and more...]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Focal Point]]></title>
        <id>/tv/short-hops/focal-point</id>
        <link href="/tv/short-hops/focal-point"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn to maintain the focus of your images during asset transformations, enhancing the visual appeal of your website or application.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Raycast Installation]]></title>
        <id>/tv/directus-mcp-server/raycast-installation</id>
        <link href="/tv/directus-mcp-server/raycast-installation"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Connect Directus to Raycast using the MCP Server for quick AI-powered content management from your macOS launcher. Learn how to install, configure, and use the Directus MCP Server with Raycast for efficient workflow automation.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Files]]></title>
        <id>/tv/directus-academy/directus-files</id>
        <link href="/tv/directus-academy/directus-files"/>
        <updated>2024-09-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover the powerful features of Directus Files, a comprehensive Digital Asset Management system that simplifies file storage, organization, and transformation within your Directus projects⁠.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Connect to Claude AI / Desktop]]></title>
        <id>/tv/directus-mcp-server/mcp-claude-ai</id>
        <link href="/tv/directus-mcp-server/mcp-claude-ai"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Connect the native Directus MCP Server to Claude AI Desktop for streamlined content management conversations. This episode guides you through the setup process to bring Directus data and operations directly into your Claude Desktop chat experience.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What are static tokens?]]></title>
        <id>/tv/authentication-ave/what-are-static-tokens</id>
        <link href="/tv/authentication-ave/what-are-static-tokens"/>
        <updated>2025-01-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Sometimes you need to give out a master key that never changes. In this jog down Authentication Avenue, Kevin answers "What are static tokens?".]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Help Center]]></title>
        <id>/tv/100-apps-100-hours/help-center</id>
        <link href="/tv/100-apps-100-hours/help-center"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant tackles the challenge of building a documentation site that lets content editors manage help articles through a CMS instead of markdown files. Watch as he sets up a backend with Directus—creating categories, articles, and tags relationships—before diving into a frontend template to bring it all together in his race against the clock.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Advanced Transformations]]></title>
        <id>/tv/sharp-focus/advanced-transformations</id>
        <link href="/tv/sharp-focus/advanced-transformations"/>
        <updated>2024-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Carmen as we apply advanced transformations to our images leveraging the Sharp API.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Content Translations in Directus: SDK]]></title>
        <id>/tv/translation-station/content-translations-in-directus-sdk</id>
        <link href="/tv/translation-station/content-translations-in-directus-sdk"/>
        <updated>2025-02-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[You can use your Directus project’s API to access your translated content. In this episode, Carmen will show you how to build an application to display translated content using the Directus SDK, REST and GraphQL APIs.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bobby Tail and the Always-Awake Library]]></title>
        <id>/tv/bobby-tails-little-library/bobby-tail-always-awake-library</id>
        <link href="/tv/bobby-tails-little-library/bobby-tail-always-awake-library"/>
        <updated>2024-12-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bobby Tail learns about the concepts of Realtime, WebSockets and GraphQL. Read by Daniel Roe.
]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Automate]]></title>
        <id>/tv/directus-academy/directus-automate</id>
        <link href="/tv/directus-academy/directus-automate"/>
        <updated>2024-09-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Understand Directus Flows, We'll explain how it enables custom event-driven task automation within Directus through triggers, operations, and data chains⁠.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Reception]]></title>
        <id>/tv/digging-the-rabbit-hole/reception</id>
        <link href="/tv/digging-the-rabbit-hole/reception"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Kevin discuss the early feedback and reception to Directus TV and it's shows.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Feedback Widget]]></title>
        <id>/tv/100-apps-100-hours/feedback-widget</id>
        <link href="/tv/100-apps-100-hours/feedback-widget"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant builds an embeddable feedback widget for documentation sites that captures user ratings and comments with a decisive 4-point scale. Watch as he creates a complete system with slick animations and wraps it in an iframe-friendly package, then builds a dashboard to visualize feedback metrics with conditional styling.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Get Started with Directus and Next.js]]></title>
        <id>/tv/stack-up/next</id>
        <link href="/tv/stack-up/next"/>
        <updated>2024-03-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this video, you'll learn how to use the Directus SDK with Next.js]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's In Your Dock, Kevin?]]></title>
        <id>/tv/whats-in-your-dock/kevin</id>
        <link href="/tv/whats-in-your-dock/kevin"/>
        <updated>2024-05-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin runs Developer Relations at Directus, and runs Directus TV, and here's the software, hardware, and analog tools he uses in his day-to-day.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Simpsons (Doh!)]]></title>
        <id>/tv/joy-of-theming/the-simpsons-theme</id>
        <link href="/tv/joy-of-theming/the-simpsons-theme"/>
        <updated>2024-04-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[There are no mistakes in this episode. Join Bry as he dusts the demons out of the keyboard and paints a wonderfully delightful theme worthy of the longest running TV on American television – The Simpsons.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.5]]></title>
        <id>/tv/release-notes/10-5</id>
        <link href="/tv/release-notes/10-5"/>
        <updated>2023-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Alex as he gives you a whirlwind tour of notable new features in Directus 10.5 - including huge updates to our API reference.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building in 10 Minutes or Less: TikTok Clone]]></title>
        <id>/tv/mcp-showcase/10-mins-tiktok-clone</id>
        <link href="/tv/mcp-showcase/10-mins-tiktok-clone"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant and Ben try to build the project of Ben's dreams using the MCP, in 10 minutes or less!]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[WTF is MACH?]]></title>
        <id>/tv/buzzword-wilderness/wtf-is-mach</id>
        <link href="/tv/buzzword-wilderness/wtf-is-mach"/>
        <updated>2024-06-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The term Jamstack has been floating around for the past few years, but a new term is starting to make the rounds. Join Matt as he explores MACH, and how it compares to Jamstack. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Extend]]></title>
        <id>/tv/leap-week/extend</id>
        <link href="/tv/leap-week/extend"/>
        <updated>2023-10-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At Leap Week 1, we introduced the Secure Extension Framework as a way to gain and have trust with third-party extensions.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What are cookies?]]></title>
        <id>/tv/authentication-ave/what-are-cookies</id>
        <link href="/tv/authentication-ave/what-are-cookies"/>
        <updated>2025-01-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[No, not those kind of cookies! In this comfortable sit on a bench at the end of Authentication Avenue, Kevin answers "What are cookies?" and shows you how to get them.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[December 2024]]></title>
        <id>/tv/the-changelog/5-december-2024</id>
        <link href="/tv/the-changelog/5-december-2024"/>
        <updated>2024-12-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes a year in review recap, a community showcase from Petros showing a universal translator extension, an episode of a new show on Directus TV called Bobby Tail's Little Library and more...]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Inline Data Editor]]></title>
        <id>/tv/request-review/inline-data-editor</id>
        <link href="/tv/request-review/inline-data-editor"/>
        <updated>2024-03-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on March 21 2024, Rijk, Jonathan, and Daniel discuss inline editing.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mystery Code]]></title>
        <id>/tv/dev-thoughts/mystery-code</id>
        <link href="/tv/dev-thoughts/mystery-code"/>
        <updated>2024-01-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Who needs a plot twist when you have legacy code?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Publishing Blog Posts from Google Docs]]></title>
        <id>/tv/directus-mcp-server/publishing-blog-post-from-google-docs</id>
        <link href="/tv/directus-mcp-server/publishing-blog-post-from-google-docs"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Master AI-assisted blog post creation with the Directus MCP Server. Learn how to streamline your content creation process, from initial concept to published post, using AI collaboration to maintain quality while saving time.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus MCP For Developers]]></title>
        <id>/tv/directus-mcp-server/mcp-for-developers</id>
        <link href="/tv/directus-mcp-server/mcp-for-developers"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover powerful developer workflows using the native Directus MCP Server with Claude Code. This episode demonstrates practical use cases including rapid data modeling, automated flow creation, sample data generation, and script writing to accelerate your development process.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Pair Programming]]></title>
        <id>/tv/dev-thoughts/pair-programming</id>
        <link href="/tv/dev-thoughts/pair-programming"/>
        <updated>2024-03-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Time to quack open the world of pair programming.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Sort Fields]]></title>
        <id>/tv/short-hops/sort-fields</id>
        <link href="/tv/short-hops/sort-fields"/>
        <updated>2024-04-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to add sort fields inside of new and existing relationships inside of Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tom Morano, Technical Lead at SMX Consulting]]></title>
        <id>/tv/trace-talks/tom-morano</id>
        <link href="/tv/trace-talks/tom-morano"/>
        <updated>2024-02-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Tom shares his unexpected ascent into leadership and his unique perspective on guiding teams. This episode offers valuable lessons on leadership and personal growth.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Dynamic Relational Interfaces]]></title>
        <id>/tv/short-hops/dynamic-relational-interfaces</id>
        <link href="/tv/short-hops/dynamic-relational-interfaces"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to create a more intuitive user experience in Directus by implementing dynamic filters on relational interfaces. This episode teaches you to set up dependent field relationships, allowing users to see only relevant options when selecting related items. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Product Information Management]]></title>
        <id>/tv/100-apps-100-hours/pim</id>
        <link href="/tv/100-apps-100-hours/pim"/>
        <updated>2024-01-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant as he tackles creation of a product information management system to manage and distribute product data to e-commerce channels. He has one hour to build the data model, add some data, and create an integration to automatically push products to Shopify.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Languages | Translations | Roles]]></title>
        <id>/tv/from-the-field/languages-translations-roles</id>
        <link href="/tv/from-the-field/languages-translations-roles"/>
        <updated>2022-11-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about multi-language support, locale-based permissions, and the editing workflow with languages.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tag Images Automatically with Clarifai]]></title>
        <id>/tv/quick-connect/clarifai</id>
        <link href="/tv/quick-connect/clarifai"/>
        <updated>2024-01-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Automatically tag new image files with Clarifai's Image Recognition Model.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Image Moderation]]></title>
        <id>/tv/ai/ai-image-moderation</id>
        <link href="/tv/ai/ai-image-moderation"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Analyze images for drugs, suggestive or explicit material with this custom operation, powered by Clarifai.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bobby Tail and the Book Return Machine]]></title>
        <id>/tv/bobby-tails-little-library/bobby-tail-book-return-machine</id>
        <link href="/tv/bobby-tails-little-library/bobby-tail-book-return-machine"/>
        <updated>2024-12-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bobby Tail learns about the automation concepts of flows, triggers and operations. Read by Bryant Gillespie.
]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Git Submodules with Shelley]]></title>
        <id>/tv/learning-things-i-love-to-hate/submodules</id>
        <link href="/tv/learning-things-i-love-to-hate/submodules"/>
        <updated>2024-02-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin is joined by Shelley to finally understand the cursed tech that is Git Submodules. Turns out it's not that difficult...]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[DIYing My Wedding The Hacker's Way]]></title>
        <id>/tv/around-the-world/diy-wedding</id>
        <link href="/tv/around-the-world/diy-wedding"/>
        <updated>2024-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Hold on to your bowties and bouquets as we dive head-first into the world of wedding planning. Except this isn't any wedding - it's a wedding between two nerds who know how to code. We'll discuss the hacky projects built to help us organize and deliver a magical day - from custom a custom invite RSVP system, email and text alerts, security-list generator, and some of the weirder ideas we ran out of time to build.  ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Labs & Spreadsheet Layout]]></title>
        <id>/tv/leap-week/directus-labs-spreadsheet-layout</id>
        <link href="/tv/leap-week/directus-labs-spreadsheet-layout"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Explore the new Spreadsheet Layout on Directus Marketplace for efficient data editing, launched as part of our new Directus Labs project. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Payments Hackathon]]></title>
        <id>/tv/leap-week/02-payments-hackathon</id>
        <link href="/tv/leap-week/02-payments-hackathon"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This March we invite you to showcase your creative and technical skills to build and publish extensions related to payments and billing.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Preset Transformations]]></title>
        <id>/tv/sharp-focus/preset-transformations</id>
        <link href="/tv/sharp-focus/preset-transformations"/>
        <updated>2024-11-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Carmen as we assign our transformations to presets that we can apply to multiple images.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Remote Job Board]]></title>
        <id>/tv/100-apps-100-hours/remote-job-board</id>
        <link href="/tv/100-apps-100-hours/remote-job-board"/>
        <updated>2024-05-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Race right along with Bryant as he tackles building a virtual job board inspired by We Work Remotely. He tries to build a ton of functionality in just 60 mins – job listings, integrating categories and companies, and job post submissions with Directus and Nuxt.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Freelancer Marketplace]]></title>
        <id>/tv/100-apps-100-hours/freelancer-marketplace</id>
        <link href="/tv/100-apps-100-hours/freelancer-marketplace"/>
        <updated>2024-05-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Today, we learn a new acronym "JFI"  = Just Fiverr It. Join Bryant as he tries to build a clone of the popular online marketplace for freelancers in just 60 minutes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Speech Generation]]></title>
        <id>/tv/ai/ai-speech-generation</id>
        <link href="/tv/ai/ai-speech-generation"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Generate realistic speech clips from text with this custom operation, powered by Genny from LOVO.

]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: AI App Generator]]></title>
        <id>/tv/100-apps-100-hours/ai-app</id>
        <link href="/tv/100-apps-100-hours/ai-app"/>
        <updated>2024-01-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What if you could build an app that builds itself? That's the question Bryant seeks to answer in one hour on this AI themed episode. Follow along as he builds a custom Directus extension that connects with the OpenAI GPT-4 API and updates the projects underlying data model based on a simple prompt.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Chick-fil-A (Eat Mor Chikin)]]></title>
        <id>/tv/joy-of-theming/chickfila-theme</id>
        <link href="/tv/joy-of-theming/chickfila-theme"/>
        <updated>2024-04-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Theming can seem hard on the surface. If you get stuck waffling back and forth on color palettes and font choices, then you need to be chikin out this episode. It's our pleasure to present the "Chick-fil-A theme". Join host Bry Ross as he paints a happy little theme for the king of the chicken sandwich.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The State of Data Survey 2024]]></title>
        <id>/tv/leap-week/02-state-of-data</id>
        <link href="/tv/leap-week/02-state-of-data"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[780 engineers told us how they use data in their projects. Here are 6 things that we learned.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Kit]]></title>
        <id>/tv/digging-the-rabbit-hole/kit</id>
        <link href="/tv/digging-the-rabbit-hole/kit"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, Bryant shows you around the template for creating your own streaming service with Directus, based on Directus TV.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Dana Lawson, CTO at Netlify]]></title>
        <id>/tv/trace-talks/dana-lawson</id>
        <link href="/tv/trace-talks/dana-lawson"/>
        <updated>2024-10-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Dana Lawson, CTO of Netlify, discusses her unconventional path to tech leadership. She shares insights on effective communication, embracing AI, fostering psychological safety, and balancing technical and soft skills. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building in 10 Minutes or Less: Menu App]]></title>
        <id>/tv/mcp-showcase/10-mins-menu-app</id>
        <link href="/tv/mcp-showcase/10-mins-menu-app"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant and Rijk try to build the project of Rijk's dreams using the MCP, in 10 minutes or less!]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Monthly Database Bill]]></title>
        <id>/tv/dev-thoughts/monthly-database</id>
        <link href="/tv/dev-thoughts/monthly-database"/>
        <updated>2024-01-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[When monthly infrastructure bills are too high, a new hero emerges - The Creative Cost Cutter.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Track Changes | Grammar Checks | Version History]]></title>
        <id>/tv/from-the-field/track-changes-grammar-checks-version-history</id>
        <link href="/tv/from-the-field/track-changes-grammar-checks-version-history"/>
        <updated>2022-11-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about tracking changes, expiration dates, and checking content for correctness.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building a Card Game Discord Bot With Directus]]></title>
        <id>/tv/around-the-world/wishbot</id>
        <link href="/tv/around-the-world/wishbot"/>
        <updated>2024-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this talk, we will discuss the development of WishBot: a trading card game based entirely in discord. we will talk about some of the intricacies of building bots at scale, and how Directus can be used to manage users, collections, and trading.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Public Forms]]></title>
        <id>/tv/request-review/public-forms</id>
        <link href="/tv/request-review/public-forms"/>
        <updated>2024-04-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on April 4 2024, Rijk, Jonathan, and Daniel discuss sharable forms for adding content to collections.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bobby Tail and the Winter Reading Rush]]></title>
        <id>/tv/bobby-tails-little-library/bobby-tail-winter-reading-rush</id>
        <link href="/tv/bobby-tails-little-library/bobby-tail-winter-reading-rush"/>
        <updated>2024-12-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bobby Tail learns about Directus insights, dashboards, panels and charts. Read by Mike Elsmore.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Organizing Assets]]></title>
        <id>/tv/directus-mcp-server/ai-organize-assets-directus-mcp</id>
        <link href="/tv/directus-mcp-server/ai-organize-assets-directus-mcp"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Transform your asset management with AI-powered organization. Learn how to use the Directus MCP Server to automatically analyze, categorize, and optimize your media library for better discoverability and workflow efficiency.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Show Build Status with Netlify]]></title>
        <id>/tv/quick-connect/netlify</id>
        <link href="/tv/quick-connect/netlify"/>
        <updated>2024-02-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Increase visibility of build status by showing them in Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Dynamic Variables]]></title>
        <id>/tv/short-hops/dynamic-variables</id>
        <link href="/tv/short-hops/dynamic-variables"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn to use dynamic variables like $CURRENT_USER, $CURRENT_ROLE, and $NOW to create personalized views and time-based queries both in the app interface and API calls. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus MCP for Marketers]]></title>
        <id>/tv/directus-mcp-server/mcp-for-marketers</id>
        <link href="/tv/directus-mcp-server/mcp-for-marketers"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Explore powerful content workflows for marketers using the native Directus MCP Server with ChatGPT and Claude. This episode demonstrates real-world marketing use cases including importing formatted content from Google Docs, creating landing pages with AI, automating translations, and generating images.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Insights]]></title>
        <id>/tv/directus-academy/directus-insights</id>
        <link href="/tv/directus-academy/directus-insights"/>
        <updated>2024-09-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Explore the powerful features of Directus Insights for building interactive dashboards with customizable panels, charts, and global variables to visualize and analyze your data.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Internal Knowledgebase]]></title>
        <id>/tv/100-apps-100-hours/internal-knowledge-base</id>
        <link href="/tv/100-apps-100-hours/internal-knowledge-base"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant builds a Guru-inspired knowledge base with bite-sized information cards accessible through a Chrome extension. Watch as he creates a Directus backend with verification flows, then builds a browser extension interface that makes company knowledge searchable and editable on the fly—despite a tricky bug that nearly derails the entire project.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[January 2025]]></title>
        <id>/tv/the-changelog/january-2025</id>
        <link href="/tv/the-changelog/january-2025"/>
        <updated>2025-01-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes a documentation update, a community showcase from Shruti showing a real estate app, a salty santa recap from Bryant and an episode of a new show on Directus TV called Authentication Avenue.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Computer Deli]]></title>
        <id>/tv/dev-thoughts/computer-deli</id>
        <link href="/tv/dev-thoughts/computer-deli"/>
        <updated>2024-03-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Computers are the gateway to anything you want. Well almost...]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.6]]></title>
        <id>/tv/release-notes/10-6</id>
        <link href="/tv/release-notes/10-6"/>
        <updated>2023-08-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Kevin as he shows you what's new in Directus 10.6 - take note because there's a breaking change in here!]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Plans for Extensions and Marketplace]]></title>
        <id>/tv/leap-week/extensions-and-marketplace-plans</id>
        <link href="/tv/leap-week/extensions-and-marketplace-plans"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn about the main core themes for our new team focused on extensions and the Directus Marketplace.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[This Summer on Directus TV]]></title>
        <id>/tv/leap-week/directus-tv-summer-2024</id>
        <link href="/tv/leap-week/directus-tv-summer-2024"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[New shows and renewals for Directus TV - our streaming platform which brings together education, entertainment, and stories from across the Directus ecosystem. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.7]]></title>
        <id>/tv/release-notes/10-7</id>
        <link href="/tv/release-notes/10-7"/>
        <updated>2023-11-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Kevin runs through all of the changes in Directus 107 - including content versioning, a new theming engine, improvements to Directus Insights, and more.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Version Control]]></title>
        <id>/tv/short-hops/version-control</id>
        <link href="/tv/short-hops/version-control"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to make stress-free edits to published content without affecting the live version. Bryant walks you through how to enable content versioning in Directus and how to create and manage different versions of content.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Community MCP Build: Note-Taking App]]></title>
        <id>/tv/mcp-showcase/mcp-build-note-app</id>
        <link href="/tv/mcp-showcase/mcp-build-note-app"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Joshua Bemendorfer talks through his project: building a note-taking app.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Email Translations]]></title>
        <id>/tv/request-review/email-translations</id>
        <link href="/tv/request-review/email-translations"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on May 9 2024, Daniel, Jonathan, and Kevin discuss making email templates translatable]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Validate Phone Numbers with Twilio]]></title>
        <id>/tv/quick-connect/twilio</id>
        <link href="/tv/quick-connect/twilio"/>
        <updated>2024-02-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Use Twilio's Lookup API to automatically verify numbers submitted by new users.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Central Perk (Coffee is Better with Friends)]]></title>
        <id>/tv/joy-of-theming/friends-theme</id>
        <link href="/tv/joy-of-theming/friends-theme"/>
        <updated>2024-05-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Step into Bry's studio for a splash of '90s nostalgia as he brews a Central Perk theme, rich with the essence of our favorite TV coffee shop.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[No Place Like Home]]></title>
        <id>/tv/dev-thoughts/no-place-like-home</id>
        <link href="/tv/dev-thoughts/no-place-like-home"/>
        <updated>2024-04-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[There's no place like home, there's no place like home, there's no place like home.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Community Platform]]></title>
        <id>/tv/100-apps-100-hours/community-platform</id>
        <link href="/tv/100-apps-100-hours/community-platform"/>
        <updated>2024-01-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Every thriving community needs a home. Dive right in alongside Bryant and he builds a community platform inspired by Circle. He races against the clock to build an engaging and interactive community space in just one hour.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[GraphQL Nested Queries | MACH Compliance | APIs]]></title>
        <id>/tv/from-the-field/graphql-nested-queries-mach-compliance-apis</id>
        <link href="/tv/from-the-field/graphql-nested-queries-mach-compliance-apis"/>
        <updated>2022-12-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about fetching data, MACH, and the APIs offered by Directus.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Git Conflicts]]></title>
        <id>/tv/dev-thoughts/git-conflicts</id>
        <link href="/tv/dev-thoughts/git-conflicts"/>
        <updated>2024-01-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[For the times when your Git repo is messier than the plot line of a Bill & Ted movie.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Website Personalization Engine]]></title>
        <id>/tv/100-apps-100-hours/website-personalization-engine</id>
        <link href="/tv/100-apps-100-hours/website-personalization-engine"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant builds a content personalization system that shows different variations of web content based on visitor segments. Watch as he uses Directus to create a flexible backend for managing segment-specific content variations, then implements client-side detection to serve the right content to the right visitors.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Databox Clone]]></title>
        <id>/tv/100-apps-100-hours/databox-clone</id>
        <link href="/tv/100-apps-100-hours/databox-clone"/>
        <updated>2024-05-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[It’s time to act on your data. So join Bryant as he sprints to build a KPI dashboard to better understand your data. He dives into metrics vs events and pulling in data from third party systems.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building Landing Pages with AI]]></title>
        <id>/tv/directus-mcp-server/building-landing-pages-with-ai</id>
        <link href="/tv/directus-mcp-server/building-landing-pages-with-ai"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Create compelling landing pages with AI assistance using the Directus MCP Server. Learn how to build high-converting pages by leveraging AI for content creation, structure, and optimization while maintaining design consistency.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[February 2025]]></title>
        <id>/tv/the-changelog/February-2025</id>
        <link href="/tv/the-changelog/February-2025"/>
        <updated>2025-02-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month's show includes a documentation update, Labs extension showcase, a template preview from Lindsey Zylstra and an episode of a new show on Directus TV called Translation Station.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Cloud]]></title>
        <id>/tv/leap-week/02-cloud</id>
        <link href="/tv/leap-week/02-cloud"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Today, we’re proud to be expanding the Directus Cloud offering with key features requested by our customers.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Speech Transcription]]></title>
        <id>/tv/ai/ai-speech-transcription</id>
        <link href="/tv/ai/ai-speech-transcription"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Generate transcripts from audio files with this custom operation, powered by Deepgram.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Theming]]></title>
        <id>/tv/directus-academy/theming</id>
        <link href="/tv/directus-academy/theming"/>
        <updated>2024-09-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Explore Directus' advanced custom theming options, including light and dark themes, CSS customization, and public page styling, to create a personalized look for your company or client's project⁠.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Netflix Clone]]></title>
        <id>/tv/100-apps-100-hours/netflix</id>
        <link href="/tv/100-apps-100-hours/netflix"/>
        <updated>2024-01-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[It's Bryant vs the streaming giant – Netflix – in this exciting episode. He has just one hour to build an app that replicates the core functionalities of the video streaming service.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Text Extraction]]></title>
        <id>/tv/ai/ai-text-extraction</id>
        <link href="/tv/ai/ai-text-extraction"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Extract text from image files within Directus Files with this custom operation, using Clarifai.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Strictness of API errors]]></title>
        <id>/tv/request-review/strictness-of-api-errors</id>
        <link href="/tv/request-review/strictness-of-api-errors"/>
        <updated>2024-05-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on May 23 2024, Daniel, Jonathan, and Rick discuss making the strictness of API errors]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Create Issues from Directus with GitHub]]></title>
        <id>/tv/quick-connect/github</id>
        <link href="/tv/quick-connect/github"/>
        <updated>2024-02-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a Flow to create GitHub issues from issues within Directus collections.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[New Directus Partner Program]]></title>
        <id>/tv/leap-week/02-partners</id>
        <link href="/tv/leap-week/02-partners"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Today we are launching the new Directus Partner Program, specifically tailored for agencies. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[March 2025]]></title>
        <id>/tv/the-changelog/march-2025</id>
        <link href="/tv/the-changelog/march-2025"/>
        <updated>2025-03-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month includes new extensions such as an SEO plugin and field comments module, a new episode of Unchartered Territory and What's in your Dock from Bryant.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Amazon (From A to Zest)]]></title>
        <id>/tv/joy-of-theming/amazon-theme</id>
        <link href="/tv/joy-of-theming/amazon-theme"/>
        <updated>2024-05-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join your favorite theme master, Bry Ross, as he takes on a dynamic Amazon theme. Grab your brushes (and your duster) and let's paint a bold theme that perfectly captures the spirit of a thriving marketplace.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Company Intranet]]></title>
        <id>/tv/100-apps-100-hours/company-intranet</id>
        <link href="/tv/100-apps-100-hours/company-intranet"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant takes on the challenge of building a company intranet portal for fictional company "Initech" (complete with Office Space branding). Watch as he implements Google single sign-on, creates team structures with reporting hierarchies, sets up permission-based content access, and designs an announcement system. Follow along as he transforms Directus into a fully-functional corporate portal where employees see only what they're supposed to—TPS reports and all.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Community MCP Build: Content Handling]]></title>
        <id>/tv/mcp-showcase/mcp-build-content-handling</id>
        <link href="/tv/mcp-showcase/mcp-build-content-handling"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Wayne Eldridge talks through using the MCP for content handling.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[How We Used MCP for this Series]]></title>
        <id>/tv/directus-mcp-server/how-we-used-mcp-for-this-series</id>
        <link href="/tv/directus-mcp-server/how-we-used-mcp-for-this-series"/>
        <updated>2025-05-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[See the Directus MCP Server in action as we document the actual workflow used to create this entire series. Watch real-time AI collaboration transform a simple list of videos into a complete, structured content series with full metadata, relationships, and automation.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Functino]]></title>
        <id>/tv/dev-thoughts/functino</id>
        <link href="/tv/dev-thoughts/functino"/>
        <updated>2024-01-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[An amusing tale of a typo turned international affair.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI]]></title>
        <id>/tv/dev-thoughts/ai</id>
        <link href="/tv/dev-thoughts/ai"/>
        <updated>2024-04-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The story of AI more interested in Beethoven than binary.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.8]]></title>
        <id>/tv/release-notes/10-8</id>
        <link href="/tv/release-notes/10-8"/>
        <updated>2023-12-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.8 has huge new updates for our theming engine, including a new extension type and awesome tools to build themes visually.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Roles and Permissions | CRON | Insights Panel | Bulk Edits]]></title>
        <id>/tv/from-the-field/roles-permissions-cron-insights-panel-bulk-edits</id>
        <link href="/tv/from-the-field/roles-permissions-cron-insights-panel-bulk-edits"/>
        <updated>2022-12-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this episode, John and Pedro answer questions about CRON jobs, Directus Insights, and bulk editing.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[MIssion: Intercom Messenger Clone]]></title>
        <id>/tv/100-apps-100-hours/intercom-messenger</id>
        <link href="/tv/100-apps-100-hours/intercom-messenger"/>
        <updated>2024-06-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[It's a race to a real-time chat widget in this episode. Bryant tries to build his own version of the Intercom Messenger - a business messaging widget that includes live chat, helpful articles, and other rich content.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus Realtime]]></title>
        <id>/tv/directus-academy/directus-realtime</id>
        <link href="/tv/directus-academy/directus-realtime"/>
        <updated>2024-09-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover how Directus Realtime keeps your application data up-to-date instantly, enabling realtime features like multi-user chat, live updates, and time-sensitive data transfers through WebSocket connections and flexible subscription options⁠.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Directus+ Team Plans, Starter Kits, and Full Launch]]></title>
        <id>/tv/leap-week/directus-plus-full-launch</id>
        <link href="/tv/leap-week/directus-plus-full-launch"/>
        <updated>2024-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Get access to powerful starter kits and advanced workshops with Directus+, our premium subscription for developers. Promotional pricing ends soon.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Complex Queries via API]]></title>
        <id>/tv/short-hops/complex-queries</id>
        <link href="/tv/short-hops/complex-queries"/>
        <updated>2024-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Dive into the world of Directus REST API querying in this informative episode. Learn how to perform complex queries, filter results, and work with relational data using the Directus SDK. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[ROI]]></title>
        <id>/tv/dev-thoughts/roi</id>
        <link href="/tv/dev-thoughts/roi"/>
        <updated>2024-01-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[On today's episode of the "Dude, Where's My Project?", we search for the ever-elusive return on investment. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bloopers From The Field]]></title>
        <id>/tv/from-the-field/season-1-bloopers</id>
        <link href="/tv/from-the-field/season-1-bloopers"/>
        <updated>2023-01-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What's a season without a load of bloopers? Have fun! ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: AI Personalized Landing Page]]></title>
        <id>/tv/100-apps-100-hours/ai-personalized-landing</id>
        <link href="/tv/100-apps-100-hours/ai-personalized-landing"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Watch Bryant create a system that generates custom landing pages for target companies by scraping their websites and using AI to craft personalized sales pitches. With a mix of Directus hooks, FireCrawl for content scraping, and the Anthropic API for copywriting, he builds a complete workflow that transforms company data into engaging sales pages for his fictional developer seat cushion business.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Level Up With Directus+]]></title>
        <id>/tv/leap-week/02-plus</id>
        <link href="/tv/leap-week/02-plus"/>
        <updated>2024-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Announcing our subscription service crafted to enhance your Directus experience to the maximum.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Extensions]]></title>
        <id>/tv/directus-academy/extensions</id>
        <link href="/tv/directus-academy/extensions"/>
        <updated>2024-09-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover the power of Directus extensions in this overview, exploring how they can enhance your project's functionality and integrate with external services⁠.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New In Directus Version 10.9]]></title>
        <id>/tv/release-notes/10-9</id>
        <link href="/tv/release-notes/10-9"/>
        <updated>2024-02-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.9 has new focal point support, a new hash display, a huge bug fix for extension builders, and a few breaking changes - so do tune in.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Social Media Platform]]></title>
        <id>/tv/100-apps-100-hours/social-media-platform</id>
        <link href="/tv/100-apps-100-hours/social-media-platform"/>
        <updated>2024-06-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[It’s social time. Join Bryant as he races to build a social media platform with registration, content feed and the ability to dish out likes and follows in just 60 minutes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Ex Variables]]></title>
        <id>/tv/dev-thoughts/ex-variables</id>
        <link href="/tv/dev-thoughts/ex-variables"/>
        <updated>2024-04-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Code so dramatic it deserves it's own reality tv series.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Text Intelligence]]></title>
        <id>/tv/ai/ai-text-intelligence</id>
        <link href="/tv/ai/ai-text-intelligence"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Analyze text intents, sentiment, topics, and generate a summary within with this custom operation, powered by Deepgram.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[WYSIWYG Linking Existing Files]]></title>
        <id>/tv/request-review/19659</id>
        <link href="/tv/request-review/19659"/>
        <updated>2024-07-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Rijk, Jonathan and special guest Hannes discuss WYSIWYG Linking Existing Files.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Food Delivery App]]></title>
        <id>/tv/100-apps-100-hours/food-delivery</id>
        <link href="/tv/100-apps-100-hours/food-delivery"/>
        <updated>2024-02-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Food delivery apps exploded during the pandemic. But what does it take to actually build one? Follow along with Bryant as he has 60 minutes on the clock to build a backend, create a menu, and place an order in his Doordash clone.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Walmart (Blueprint for Savings)]]></title>
        <id>/tv/joy-of-theming/walmart-theme</id>
        <link href="/tv/joy-of-theming/walmart-theme"/>
        <updated>2024-05-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bry as he brings the essence of better living to life with a happy little Walmart theme. Grab your theming brushes and following to turn savings into art.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[MCP Hackathon: Challenges, Prizes, and How to Enter]]></title>
        <id>/tv/mcp-showcase/mcp-hackathon-challenges</id>
        <link href="/tv/mcp-showcase/mcp-hackathon-challenges"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant talks through the MCP Hackathon, giving details on the challenges, prizes and entry information.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[April 2025]]></title>
        <id>/tv/the-changelog/april-2025</id>
        <link href="/tv/the-changelog/april-2025"/>
        <updated>2025-04-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights. This month includes new extensions such as an Algolia Operation, a GitHub Operation and an Elasticsearch Operation, a look into the new Visual Editor beta module and an episode of the Joy of Theming with Bry Ross.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New in Directus Version 10.10]]></title>
        <id>/tv/release-notes/10-10</id>
        <link href="/tv/release-notes/10-10"/>
        <updated>2024-03-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.10 introduces the Directus Marketplace Beta - a new way to discover and install extensions in your projects, Content Versioning enhancements for use with Live Preview, and a number of small breaking changes to be aware of.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Airbnb]]></title>
        <id>/tv/100-apps-100-hours/airbnb</id>
        <link href="/tv/100-apps-100-hours/airbnb"/>
        <updated>2024-02-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Can Bryant follow AirBnb's trajectory from scrappy startup to short term rental royalty – and build an AirBnb clone in one hour or less? Follow along as he attempts to builds a complete backend and frontend to manage rental listings, bookings, hosts, and more.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Text Translator]]></title>
        <id>/tv/ai/ai-text-translator</id>
        <link href="/tv/ai/ai-text-translator"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Translate text into over 30 languages with this custom oepration, powered by DeepL.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Meaning of Life]]></title>
        <id>/tv/dev-thoughts/meaning-of-life</id>
        <link href="/tv/dev-thoughts/meaning-of-life"/>
        <updated>2024-01-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Life's deepest truths are often found in the strangest places.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: AI Copilot]]></title>
        <id>/tv/100-apps-100-hours/ai-copilot</id>
        <link href="/tv/100-apps-100-hours/ai-copilot"/>
        <updated>2025-03-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bryant takes on his most ambitious challenge yet: building an AI Copilot that integrates directly with Directus data. Watch as he races against the clock to create a custom chat interface that can query your database, implement tool calling functionality, and (attempt to) persist conversations. Things get meta when Bryant uses AI to help build his AI assistant—with some hilariously chaotic results along the way.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Import & Export option for Flows]]></title>
        <id>/tv/request-review/18618</id>
        <link href="/tv/request-review/18618"/>
        <updated>2024-08-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on August 15 2024, Daniel, Jonathan, and Rick discuss import and export options for flows]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[May 2025]]></title>
        <id>/tv/the-changelog/may-2025</id>
        <link href="/tv/the-changelog/may-2025"/>
        <updated>2025-05-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[404]]></title>
        <id>/tv/dev-thoughts/404</id>
        <link href="/tv/dev-thoughts/404"/>
        <updated>2024-04-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Today we're searching the ever-elusive file that's not found.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tesla (Simply Electric)]]></title>
        <id>/tv/joy-of-theming/tesla-theme</id>
        <link href="/tv/joy-of-theming/tesla-theme"/>
        <updated>2024-05-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Charge up your canvas and join Bry as teaches you how you paint an electrifying Directus theme for car maker Tesla.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Form Builder]]></title>
        <id>/tv/100-apps-100-hours/form-builder</id>
        <link href="/tv/100-apps-100-hours/form-builder"/>
        <updated>2024-07-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join Bryant for his final 60 minute rush of the season as he attempts to plan and build a form builder. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Control]]></title>
        <id>/tv/dev-thoughts/control</id>
        <link href="/tv/dev-thoughts/control"/>
        <updated>2024-01-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[They say you should be careful what you wish for.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New in Directus Version 10.11]]></title>
        <id>/tv/release-notes/10-11</id>
        <link href="/tv/release-notes/10-11"/>
        <updated>2024-05-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.11 introduces public registration and a set of other enhancements, optimizations, and bug fixes.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[June 2025]]></title>
        <id>/tv/the-changelog/June-2025</id>
        <link href="/tv/the-changelog/June-2025"/>
        <updated>2025-06-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Silver Lining]]></title>
        <id>/tv/dev-thoughts/silver-lining</id>
        <link href="/tv/dev-thoughts/silver-lining"/>
        <updated>2024-04-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[All the clouds that glitter are not gold.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mission: Slack (Live Episode)]]></title>
        <id>/tv/100-apps-100-hours/slack</id>
        <link href="/tv/100-apps-100-hours/slack"/>
        <updated>2024-03-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this special edition live episode, Bryant is joined by Kevin, Alex, and Matt to build a clone on Slack in just one hour with Directus Realtime and Nuxt. Join the absolute chaos as friends and colleagues try to "help" against the clock. ]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI Writer]]></title>
        <id>/tv/ai/ai-writer</id>
        <link href="/tv/ai/ai-writer"/>
        <updated>2024-05-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Generate text based on a prompt with this custom operation, powered by OpenAI.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Apple (Think Different)]]></title>
        <id>/tv/joy-of-theming/think-different</id>
        <link href="/tv/joy-of-theming/think-different"/>
        <updated>2024-06-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Designed by Bry Ross in California. Time to join your favorite theme guru for an exercise in simplistic theming as he delivers a captivating Apple theme.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Additional Form/Field Layout Options]]></title>
        <id>/tv/request-review/9161</id>
        <link href="/tv/request-review/9161"/>
        <updated>2024-08-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on August 29th 2024, Daniel, Jonathan, and Rijk discuss Additional Form/Field Layout Options.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Essence of Coding]]></title>
        <id>/tv/dev-thoughts/essence-of-coding</id>
        <link href="/tv/dev-thoughts/essence-of-coding"/>
        <updated>2024-01-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What is development if not an expression of our own inner, true-self?]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New in Directus Version 10.12]]></title>
        <id>/tv/release-notes/10-12</id>
        <link href="/tv/release-notes/10-12"/>
        <updated>2024-06-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus 10.12 enhances public user registration with a set of new features to control your project.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Codebase Cleanup]]></title>
        <id>/tv/dev-thoughts/codebase-cleanup</id>
        <link href="/tv/dev-thoughts/codebase-cleanup"/>
        <updated>2024-04-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Enter the chaotic world of coding etiquette.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Optionally Delete Unused Files]]></title>
        <id>/tv/request-review/17853</id>
        <link href="/tv/request-review/17853"/>
        <updated>2024-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on September 12th 2024, Nils, Jonathan, and Rijk discuss Optionally Delete Unused Files.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[July 2025]]></title>
        <id>/tv/the-changelog/July-25</id>
        <link href="/tv/the-changelog/July-25"/>
        <updated>2025-07-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Nike (Just Do It)]]></title>
        <id>/tv/joy-of-theming/just-do-it</id>
        <link href="/tv/joy-of-theming/just-do-it"/>
        <updated>2024-06-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[It might seem hard to get started theming, but it's important to just do it.  Join the GOAT of theming Bry Ross and he kicks out a fresh theme that captures the essence of footwear and apparel legends – Nike.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Field Locking and Notifications]]></title>
        <id>/tv/request-review/20594</id>
        <link href="/tv/request-review/20594"/>
        <updated>2024-10-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In this recording of our live event on September 12th 2024, Nils, Jonathan, and Hannes discuss Field locking and notifications.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Thor]]></title>
        <id>/tv/dev-thoughts/thor</id>
        <link href="/tv/dev-thoughts/thor"/>
        <updated>2024-04-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Could Keyboard Warrior be the next Marvel super-hero? Stay tuned.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[What's New in Directus Version 10.13]]></title>
        <id>/tv/release-notes/10-13</id>
        <link href="/tv/release-notes/10-13"/>
        <updated>2024-07-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Directus Version 10.13 introduces resumable uploads, some fixes around shares, and some small quality of life improvements for the Data Studio.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Maintenance]]></title>
        <id>/tv/dev-thoughts/maintenance</id>
        <link href="/tv/dev-thoughts/maintenance"/>
        <updated>2024-01-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Fun fact: If you whisper the word "maintenance" 5 times, somewhere in the world a developer breaks out into a cold sweat.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[September 2025]]></title>
        <id>/tv/the-changelog/september-2025</id>
        <link href="/tv/the-changelog/september-2025"/>
        <updated>2025-09-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Join us for The Changelog, taking you through the month’s Directus updates including product updates, new content and community contribution highlights.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Rome]]></title>
        <id>/tv/dev-thoughts/rome</id>
        <link href="/tv/dev-thoughts/rome"/>
        <updated>2024-04-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[All time comparisons lead to Rome.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Crash]]></title>
        <id>/tv/dev-thoughts/crash</id>
        <link href="/tv/dev-thoughts/crash"/>
        <updated>2024-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Discover the secret lives of computers and their quest for rebellion.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Jimicolon]]></title>
        <id>/tv/dev-thoughts/jimicolon</id>
        <link href="/tv/dev-thoughts/jimicolon"/>
        <updated>2024-05-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Finally somebody explains the proper usage of the jimicolon.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[No Code]]></title>
        <id>/tv/dev-thoughts/no-code</id>
        <link href="/tv/dev-thoughts/no-code"/>
        <updated>2024-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The real mark of an elite developer is not in the lines of code they write, but the lines of code they don't write.]]></summary>
    </entry>
    <entry>
        <title type="html"><![CDATA[Greg]]></title>
        <id>/tv/dev-thoughts/greg</id>
        <link href="/tv/dev-thoughts/greg"/>
        <updated>2024-05-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Sometimes we find companionship in the least expected places.]]></summary>
    </entry>
</feed>