<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>aabidk.dev –</title><link>https://aabidk.dev/</link><description>Recent content on aabidk.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sat, 24 Jan 2026 20:48:02 +0530</lastBuildDate><atom:link href="https://aabidk.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>Dockerizing a Laravel and Inertia App</title><link>https://aabidk.dev/blog/dockerizing-a-laravel-and-inertia-app/</link><pubDate>Sun, 21 Dec 2025 22:10:00 +0530</pubDate><guid>https://aabidk.dev/blog/dockerizing-a-laravel-and-inertia-app/</guid><description>
&lt;h3&gt;Introduction&lt;span class="hx:absolute hx:-mt-20" id="introduction"&gt;&lt;/span&gt;
&lt;a href="#introduction" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;This post assumes you are familiar with Laravel, Inertia and have a basic understanding of Docker. To follow along, you can create a new blank Laravel project with React. There are some complexities around file permissions, health checks, and volumes that we will address in this post.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/&gt;&lt;/svg&gt;Note&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;Reddit discussion around this post is available &lt;a href="https://www.reddit.com/r/PHP/comments/1psds71/a_guide_on_dockerizing_a_laravel_inertia_react_app/"target="_blank" rel="noopener"&gt;here&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. There were some suggestions, I&amp;rsquo;ve updated the post to reflect those changes. See &lt;a href="https://www.reddit.com/r/PHP/comments/1psds71/comment/nv8si0v/"target="_blank" rel="noopener"&gt;this comment&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; thread for more details.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Prerequisites&lt;span class="hx:absolute hx:-mt-20" id="prerequisites"&gt;&lt;/span&gt;
&lt;a href="#prerequisites" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Docker installed on your machine. You can download it from &lt;a href="https://www.docker.com/"target="_blank" rel="noopener"&gt;Docker&amp;rsquo;s official site&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Laravel installed on your machine. You can follow the official installation guide &lt;a href="https://laravel.com/docs/12.x/installation"target="_blank" rel="noopener"&gt;here&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Basic knowledge of Laravel and Inertia.js.&lt;/li&gt;
&lt;li&gt;A Laravel Inertia application. If you already have Laravel installed, you can create a new project using the following commands(Skip the &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt; commands when prompted):&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;laravel new demo-project --using&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;laravel/react-starter-kit:^1.0.1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The fixed version is merely used for stability here, you can use the latest version as well. After creating the project, we will not install anything else locally. Let&amp;rsquo;s dockerize this application step by step.&lt;/p&gt;
&lt;h3&gt;Create a Dockerfile&lt;span class="hx:absolute hx:-mt-20" id="create-a-dockerfile"&gt;&lt;/span&gt;
&lt;a href="#create-a-dockerfile" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;First, we will quickly get the application up and running, and then we will improve it with multi-stage builds. We will be using &lt;a href="https://hub.docker.com/r/serversideup/php"target="_blank" rel="noopener"&gt;serversideup/php&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; images as our base. These images are optimized for running PHP applications, with good defaults, easy configuration, and include some automation scripts for Laravel.
In the root of the Laravel project, create a file named &lt;code&gt;Dockerfile&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;Dockerfile&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Use a base image with PHP, Nginx, and Alpine Linux.&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Use fixed version tags for stability.&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;FROM&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;serversideup/php:8.4.11-fpm-nginx-alpine3.21-v3.6.0&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;AS&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;development&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Switch to root to install dependencies&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;root&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Install any needed PHP extensions&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# RUN install-php-extensions intl&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Defaults&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;ARG&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;USER_ID&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;ARG&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;GROUP_ID&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; docker-php-serversideup-set-id www-data &lt;span style="color:#f5e0dc"&gt;$USER_ID&lt;/span&gt;:&lt;span style="color:#f5e0dc"&gt;$GROUP_ID&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-php-serversideup-set-file-permissions --owner &lt;span style="color:#f5e0dc"&gt;$USER_ID&lt;/span&gt;:&lt;span style="color:#f5e0dc"&gt;$GROUP_ID&lt;/span&gt; --service nginx&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Install Node (for building assets)&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# This is quick and dirty, we&amp;#39;ll fix it in the multi-stage version&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; apk add --no-cache nodejs npm&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;www-data&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy composer files first&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --chown&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;www-data:www-data composer.json composer.lock /var/www/html/&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; composer install --optimize-autoloader --no-interaction&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy package files for frontend&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --chown&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;www-data:www-data package.json package-lock.json /var/www/html/&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; npm ci&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy rest of the app&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --chown&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;www-data:www-data . /var/www/html/&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Build frontend assets&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;root&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; npm run build&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;www-data&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Run the application&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;EXPOSE&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;8080&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;CMD&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;php&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;artisan&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;serve&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;--host=0.0.0.0&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;--port=8080&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Let&amp;rsquo;s break down what each part does:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FROM serversideup/php:8.4.11-fpm-nginx-alpine3.21-v3.6.0 AS base&lt;/code&gt;: This line specifies the base image we are using, which includes PHP 8.4 with FPM and Nginx on Alpine Linux v3.21. The version tag v3.6.0 is used by the serversideup image. We are using fixed versions as much as possible for stability. Using the Nginx variant of the image allows us to serve the static files, while PHP-FPM handles the PHP processing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;USER root&lt;/code&gt;: Switches to the root user to install necessary dependencies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID&lt;/code&gt;: This command sets the user and group IDs for the &lt;code&gt;www-data&lt;/code&gt; user inside the container to match those of your host system. This helps avoid permission issues when mounting volumes during development. The script is included in the &lt;code&gt;serversideup/php&lt;/code&gt; image itself. More information can be found &lt;a href="https://serversideup.net/open-source/docker-php/docs/guide/understanding-file-permissions"target="_blank" rel="noopener"&gt;here&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RUN docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx&lt;/code&gt;: This command sets the correct file permissions for the Nginx service to ensure it can read and serve the application files.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RUN apk add --no-cache nodejs npm&lt;/code&gt;: Installs Node.js and npm, which are required for building the frontend assets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;COPY --chown=www-data:www-data composer.json composer.lock /var/www/html/&lt;/code&gt;: Copies the Composer files to the container and sets the ownership to &lt;code&gt;www-data&lt;/code&gt;. This benefits from Docker&amp;rsquo;s layer caching . If the &lt;code&gt;composer.json&lt;/code&gt; or &lt;code&gt;composer.lock&lt;/code&gt; files have not changed, Docker can use the cached layer rather than reinstalling dependencies. Note that we are doing composer install in the image itself for now.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RUN composer install --optimize-autoloader --no-interaction&lt;/code&gt;: Installs PHP dependencies using Composer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;COPY --chown=www-data:www-data package.json package-lock.json /var/www/html&lt;/code&gt;: Copies the package files for the frontend and sets the ownership. Again, this is done before copying the rest of the application to benefit from caching.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RUN npm ci&lt;/code&gt;: Installs Node.js dependencies. Using &lt;code&gt;npm ci&lt;/code&gt; ensures a clean and consistent install based on the &lt;code&gt;package-lock.json&lt;/code&gt;, and is preferred in CI/CD environments over &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;COPY --chown=www-data:www-data . /var/www/html/&lt;/code&gt;: Copies the rest of the application files to the container.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RUN npm run build&lt;/code&gt;: Builds the frontend assets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 8080&lt;/code&gt;: Exposes port 8080 for the application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CMD [&amp;quot;php&amp;quot;, &amp;quot;artisan&amp;quot;, &amp;quot;serve&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=8080&amp;quot;]&lt;/code&gt;: Starts the Laravel development server.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can build and run the Docker container by using the following commands:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build -t demo-project .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run -p 8080:8080 demo-project:latest&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The application should now be accessible at &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Few things to note here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;By default the &lt;code&gt;serversideup/php&lt;/code&gt; images use port &lt;code&gt;8080&lt;/code&gt; for HTTP and &lt;code&gt;8443&lt;/code&gt; for HTTPS, which we have exposed in the Dockerfile.&lt;/li&gt;
&lt;li&gt;If you have not changed any environment variables, the application by default will be using SQLite as the database.&lt;/li&gt;
&lt;li&gt;Right now, the image is large (around 600mb in my case). You can use &lt;a href="https://github.com/wagoodman/dive"target="_blank" rel="noopener"&gt;dive&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; to analyze the image layers. At this point, if you inspect the image, you will notice that we have many unnecessary files included in the image such as &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;vendor&lt;/code&gt; directory, tests, git files and so on.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Add a .dockerignore File&lt;span class="hx:absolute hx:-mt-20" id="add-a-dockerignore-file"&gt;&lt;/span&gt;
&lt;a href="#add-a-dockerignore-file" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;To prevent unnecessary files from being copied into the Docker image, create a &lt;code&gt;.dockerignore&lt;/code&gt; file in the root of your project with the following content:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;.dockerignore&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vendor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tests
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.github
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;storage/logs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;storage/framework/cache/*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;!storage/framework/sessions/*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;storage/framework/views/*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;node_modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.env.*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.gitignore
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.gitattributes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.DS_Store
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.idea
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.vscode
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.editorconfig
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;compose.*.yml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm-debug.log
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;yarn-error.log
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;_ide_helper.php
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;_ide_helper_models.php
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stubs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;eslint.config.js
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;phpunit.xml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.phpactor.yml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;README.md&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We try to exclude all the unnecessary files and folders that are not needed in the Docker image :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vendor&lt;/code&gt; and &lt;code&gt;node_modules&lt;/code&gt; directories as they will be installed inside the container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.git&lt;/code&gt; directory to avoid copying version control data.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storage/logs&lt;/code&gt; and other cache directories to avoid copying log files and cached data.&lt;/li&gt;
&lt;li&gt;Environment files (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.*&lt;/code&gt;) to avoid exposing sensitive information.&lt;/li&gt;
&lt;li&gt;IDE and editor specific files and folders, log files, configuration files for linters, formatters, testing, etc.&lt;/li&gt;
&lt;li&gt;Docker-related files to avoid copying Dockerfiles and compose files. These are only needed on the host machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will reduce our docker image size, decrease build times, and improve security by not including sensitive files. You need to rebuild the image after adding this file.&lt;/p&gt;
&lt;h3&gt;Local Development with Docker Compose&lt;span class="hx:absolute hx:-mt-20" id="local-development-with-docker-compose"&gt;&lt;/span&gt;
&lt;a href="#local-development-with-docker-compose" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We will use Docker Compose to set up our dev environment. We will be using PostgreSQL as our database and Mailpit as the local mail server.
Create a file &lt;code&gt;compose.dev.yml&lt;/code&gt; in the &lt;code&gt;docker&lt;/code&gt; folder with the following content:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: postgres:17.6-alpine3.21
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: &lt;span style="color:#fab387"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;5432:5432&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;CMD-SHELL&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;pg_isready -U $POSTGRES_USER&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;interval&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;timeout&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;retries&lt;/span&gt;: &lt;span style="color:#fab387"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-data:/var/lib/postgresql/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;mailpit&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: axllent/mailpit:v1.27.7
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: always
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;${FORWARD_MAILPIT_PORT:-1025}:1025&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;app&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;context&lt;/span&gt;: .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;target&lt;/span&gt;: development
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;args&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;USER_ID&lt;/span&gt;: ${USER_ID:-1000}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;GROUP_ID&lt;/span&gt;: ${GROUP_ID:-1000}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: &lt;span style="color:#fab387"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;condition&lt;/span&gt;: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;# /up is Laravel&amp;#39;s built-in health check route&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;CMD-SHELL&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;curl -f http://localhost:8080/up || exit 1&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;interval&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;timeout&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;start_period&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;retries&lt;/span&gt;: &lt;span style="color:#fab387"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;80:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;443:8443&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .:/var/www/html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;queue&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;context&lt;/span&gt;: .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;target&lt;/span&gt;: development
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;args&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;USER_ID&lt;/span&gt;: ${USER_ID:-1000}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;GROUP_ID&lt;/span&gt;: ${GROUP_ID:-1000}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: &lt;span style="color:#fab387"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;condition&lt;/span&gt;: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - AUTORUN_ENABLED=false &lt;span style="color:#6c7086;font-style:italic"&gt;#prevents migrations from running again, they will already have run in app service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;command&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;php&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;/var/www/html/artisan&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;queue:work&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;--tries=3&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;stop_signal&lt;/span&gt;: SIGTERM
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;CMD&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;healthcheck-queue&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;start_period&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .:/var/www/html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;node&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: node:22.19.0-alpine3.21
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;working_dir&lt;/span&gt;: /app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .:/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;command&lt;/span&gt;: sh -c &amp;#34;npm ci --legacy-peer-deps &amp;amp;&amp;amp; npm run dev&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;5173:5173&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; demo-project-data:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Let&amp;rsquo;s break down the services defined in this file:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;db&lt;/code&gt; service : We are using the official Alpine based PostgreSQL image with fixed version tag. We expose port &lt;code&gt;5432&lt;/code&gt; for database connections and set up a health check to ensure the database is ready before other services depend on it. The database data is stored in a Docker volume named &lt;code&gt;demo-project-data&lt;/code&gt; to persist data across container restarts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mailpit&lt;/code&gt; service : This service uses the Mailpit image to provide a local SMTP server for testing email functionality. It exposes ports &lt;code&gt;1025&lt;/code&gt; for SMTP and &lt;code&gt;8025&lt;/code&gt; for the web dashboard.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;app&lt;/code&gt; service : This service builds the Laravel application using the Dockerfile we created earlier. It depends on the &lt;code&gt;db&lt;/code&gt; service and waits for it to be healthy before starting. It exposes ports &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt; for HTTP and HTTPS access respectively. We are using a bind mount to mount the current directory into the container for hot reloading during development. A health check is also defined to ensure the application is running.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;queue&lt;/code&gt; service : This service is responsible for running Laravel&amp;rsquo;s queue worker. It also builds from the same Dockerfile and also depends on the &lt;code&gt;db&lt;/code&gt; service. The command specified runs the queue worker with a maximum of 3 tries for each job. We disable automatic migrations here since they will already have run in the &lt;code&gt;app&lt;/code&gt; service. To gracefully stop the queue worker, we set the stop signal to &lt;code&gt;SIGTERM&lt;/code&gt;. For our health check, we are using the &lt;code&gt;healthcheck-queue&lt;/code&gt; command provided by the &lt;code&gt;serversideup/php&lt;/code&gt; image. More information can be found &lt;a href="https://serversideup.net/open-source/docker-php/docs/framework-guides/laravel/queue"target="_blank" rel="noopener"&gt;here&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node&lt;/code&gt; service : This service uses the official Node.js Alpine image to handle frontend asset compilation. It mounts the current directory into the container and runs &lt;code&gt;npm ci&lt;/code&gt; followed by &lt;code&gt;npm run dev&lt;/code&gt; to start the development server. Port &lt;code&gt;5173&lt;/code&gt; is exposed for Vite&amp;rsquo;s development server.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We are passing the &lt;code&gt;.env&lt;/code&gt; file to all services to ensure they have the necessary environment variables. We need to add the following variables to our &lt;code&gt;.env&lt;/code&gt; file to configure the database and migration settings:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;.env&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-env" data-lang="env"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# These are for Laravel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;APP_URL&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;http://localhost
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;APP_DEBUG&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#89dceb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;DB_CONNECTION&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;pgsql
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;DB_HOST&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;db
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;DB_PORT&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;5432&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;DB_DATABASE&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;laravel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;DB_USERNAME&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;postgres
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;DB_PASSWORD&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;postgres
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;#Migration settings (These are for serversideup/php image automation scripts)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;AUTORUN_ENABLED&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#89dceb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;AUTORUN_LARAVEL_MIGRATION_ISOLATION&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#89dceb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;#These are for db container, it expects database related variables with POSTGRES_ prefix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;POSTGRES_DB&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;${&lt;/span&gt;&lt;span style="color:#f5e0dc"&gt;DB_DATABASE&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;POSTGRES_USER&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;${&lt;/span&gt;&lt;span style="color:#f5e0dc"&gt;DB_USERNAME&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;${&lt;/span&gt;&lt;span style="color:#f5e0dc"&gt;DB_PASSWORD&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;With the above compose file, we can simplify our &lt;code&gt;Dockerfile&lt;/code&gt; a bit :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Since we now have a separate Node service for handling frontend assets, we can remove the Node installation and asset building steps from our Dockerfile.&lt;/li&gt;
&lt;li&gt;We can also remove the port expose and CMD instructions as they will be handled by Docker Compose.&lt;/li&gt;
&lt;li&gt;Since we are using bind mounts for the application code, there is no need to copy the &lt;code&gt;composer.json&lt;/code&gt; and &lt;code&gt;composer.lock&lt;/code&gt; files and run &lt;code&gt;composer install&lt;/code&gt; in the Dockerfile. They will be invalidated by the bind mount anyways. Instead, we will run &lt;code&gt;composer install&lt;/code&gt; from the host machine when needed. See the linked Reddit discussion above for more details.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Our updated &lt;code&gt;Dockerfile&lt;/code&gt; now looks like this:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;Dockerfile&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;FROM&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;serversideup/php:8.4.11-fpm-nginx-alpine3.21-v3.6.0&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;AS&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;development&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Switch to root to install dependencies / fix permissions&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;root&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;ARG&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;USER_ID&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;ARG&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;GROUP_ID&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; docker-php-serversideup-set-id www-data &lt;span style="color:#f5e0dc"&gt;$USER_ID&lt;/span&gt;:&lt;span style="color:#f5e0dc"&gt;$GROUP_ID&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-php-serversideup-set-file-permissions --owner &lt;span style="color:#f5e0dc"&gt;$USER_ID&lt;/span&gt;:&lt;span style="color:#f5e0dc"&gt;$GROUP_ID&lt;/span&gt; --service nginx&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;www-data&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy rest of the app&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --chown&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;www-data:www-data . /var/www/html/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Lastly, we need to update the &lt;code&gt;dev&lt;/code&gt; command in our &lt;code&gt;package.json&lt;/code&gt; and add the &lt;code&gt;--host&lt;/code&gt; flag to ensure Vite listens on all interfaces, allowing access from outside the container:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;package.json&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;dev&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;vite --host&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This should reduce our Docker image size significantly (around 110 MB in my case).&lt;/p&gt;
&lt;p&gt;We can now start our development environment by using Docker Compose with the following command:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose -f compose.dev.yml up --build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This command will build the images and start all the services defined in the &lt;code&gt;compose.dev.yml&lt;/code&gt; file. The Laravel application should now be accessible at &lt;code&gt;http://localhost&lt;/code&gt;, and the Mailpit dashboard at &lt;code&gt;http://localhost:8025&lt;/code&gt;. Note that you do not need to use &lt;code&gt;--build&lt;/code&gt; every time, only when you make changes to the Dockerfile or the compose file. Hot reloading should now work seamlessly.&lt;/p&gt;
&lt;h3&gt;Building a Production-Ready Image with Multi-Stage Builds&lt;span class="hx:absolute hx:-mt-20" id="building-a-production-ready-image-with-multi-stage-builds"&gt;&lt;/span&gt;
&lt;a href="#building-a-production-ready-image-with-multi-stage-builds" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;There are some differences when building for production. The permission fixes and the UID/GID mapping we used during development are not needed in production. We will also pre-build the frontend assets during image build, so we do not need a separate node service in production.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s update our &lt;code&gt;Dockerfile&lt;/code&gt; to add separate stages for development and production:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;Dockerfile&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-Dockerfile" data-lang="Dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Base image - common for both dev and prod&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;FROM&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;serversideup/php:8.4.11-fpm-nginx-alpine3.21-v3.6.0&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;AS&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;base&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;root&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;#Install any needed PHP extensions&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; install-php-extensions intl&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;www-data&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Node builder (for Inertia/Vite assets) - only for production&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;FROM&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;node:22.19.0-alpine3.21&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;AS&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;node-builder&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;/app&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy package files first for caching&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Install deps&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; npm ci --legacy-peer-deps&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy the rest of the frontend&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; resources/ resources/&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; vite.config.js ./&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Build frontend&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; npm run build&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Development stage (fixes host UID/GID permissions)&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;FROM&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;base&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;AS&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;development&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;root&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;ARG&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;USER_ID&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;ARG&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;GROUP_ID&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; docker-php-serversideup-set-id www-data &lt;span style="color:#f5e0dc"&gt;$USER_ID&lt;/span&gt;:&lt;span style="color:#f5e0dc"&gt;$GROUP_ID&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-php-serversideup-set-file-permissions --owner &lt;span style="color:#f5e0dc"&gt;$USER_ID&lt;/span&gt;:&lt;span style="color:#f5e0dc"&gt;$GROUP_ID&lt;/span&gt; --service nginx&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;www-data&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Production stage&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# ============================================================&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;FROM&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;base&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;AS&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;production&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;ENV&lt;/span&gt; &lt;span style="color:#f5e0dc"&gt;PHP_OPCACHE_ENABLE&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#fab387"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;USER&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;www-data&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --chown&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;www-data:www-data composer.json composer.lock /var/www/html/&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;#Exclude dev dependencies for production with --no-dev&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; composer install --no-dev --no-scripts --no-autoloader --no-interaction&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --chown&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;www-data:www-data . /var/www/html&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Copy only the built assets from node stage&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;COPY&lt;/span&gt; --from&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;node-builder /app/public/build /var/www/html/public/build&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Now run scripts and autoloader generation&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;RUN&lt;/span&gt; composer dump-autoload --optimize &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89b4fa"&gt;&lt;/span&gt; composer run-script post-autoload-dump&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In this updated Dockerfile, we have made the following changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Added a &lt;code&gt;base&lt;/code&gt; stage that has common steps for both development and production stages.&lt;/li&gt;
&lt;li&gt;Added a &lt;code&gt;node-builder&lt;/code&gt; stage that installs Node.js dependencies and builds the frontend assets. &lt;strong&gt;This stage is only used in the production build&lt;/strong&gt;. Our development setup uses a separate Node service as defined in the &lt;code&gt;compose.dev.yml&lt;/code&gt; file discussed earlier.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;development&lt;/code&gt; stage stays the same, still fixing permissions for local development and handling the UID/GID mapping. It still does not include composer install step as before.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;production&lt;/code&gt; stage installs only the necessary PHP dependencies without dev dependencies. Unlike development stage, the composer dependencies are installed during image build itself to have a self-contained image, as we will not be using bind mounts in production. It then copies the pre-built frontend assets from the &lt;code&gt;node-builder&lt;/code&gt; stage without including any build tools or source files from that stage.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can build the development image same as before. To build the production image locally use the following command:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build --target production -t demo-project:prod .&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;There will be a slight difference in production image size as compared to development since it includes composer. &lt;em&gt;Please note that the production stage for the Dockerfile is meant for building the production image both locally and in the deployment pipelines.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Testing The Production Image Locally With Docker Compose&lt;span class="hx:absolute hx:-mt-20" id="testing-the-production-image-locally-with-docker-compose"&gt;&lt;/span&gt;
&lt;a href="#testing-the-production-image-locally-with-docker-compose" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Let&amp;rsquo;s create a new compose file for production named &lt;code&gt;compose.prod.yml&lt;/code&gt; for building and running the production image locally :&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;compose.prod.yml&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: postgres:17.6-alpine3.21
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env.production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;CMD-SHELL&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;pg_isready -U $POSTGRES_USER&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;interval&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;timeout&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;retries&lt;/span&gt;: &lt;span style="color:#fab387"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-prod-db:/var/lib/postgresql/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;app&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;context&lt;/span&gt;: .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;target&lt;/span&gt;: production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;condition&lt;/span&gt;: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env.production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;CMD-SHELL&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;curl -f http://localhost:8080/up || exit 1&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;interval&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;timeout&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;start_period&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;retries&lt;/span&gt;: &lt;span style="color:#fab387"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;80:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#a6e3a1"&gt;&amp;#34;443:8443&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-prod-data:/var/www/html/storage
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;queue&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;context&lt;/span&gt;: .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;target&lt;/span&gt;: production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;condition&lt;/span&gt;: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env.production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - AUTORUN_ENABLED=false
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - PHP_FPM_POOL_NAME=app_queue
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;command&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;php&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;/var/www/html/artisan&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;queue:work&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;--tries=3&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;--timeout=90&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;stop_signal&lt;/span&gt;: SIGTERM
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#34;CMD&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;healthcheck-queue&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;start_period&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;# Same volume as app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-prod-data:/var/www/html/storage
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;demo-project-prod-db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; demo-project-prod-data:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Following are the key differences in this file compared to the development compose file:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;We are using the &lt;code&gt;production&lt;/code&gt; target from our Dockerfile to build the app and queue services.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We have added an additional env file &lt;code&gt;.env.production&lt;/code&gt; to separate production specific environment variables. If a variable is defined in both files, the one in &lt;code&gt;.env.production&lt;/code&gt; will take precedence as it is listed later.&lt;/p&gt;
&lt;p&gt;For now, my &lt;code&gt;.env.production&lt;/code&gt; only has the following variables :&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;.env.production&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-env" data-lang="env"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;APP_ENV&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;APP_DEBUG&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#89dceb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f5e0dc"&gt;NODE_ENV&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;production&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We are using named volumes for persisting database data and storage data rather than bind mounts. Also, the volumes are mounted on &lt;code&gt;/var/www/html/storage&lt;/code&gt; only and not the entire &lt;code&gt;var/www/html&lt;/code&gt; directory, since that is the only directory that needs to be writable by the application (for file uploads, cache, sessions, etc.).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The restart policy is set to &lt;code&gt;unless-stopped&lt;/code&gt; to ensure the containers restart automatically unless explicitly stopped.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In &lt;code&gt;.env.production&lt;/code&gt;, you can add other environment variables to replicate your production environment, such as mail server settings, logging configurations, etc.&lt;/p&gt;
&lt;p&gt;You can run the compose file by using the following command:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose -f compose.prod.yml up --build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Your application should now be accessible at &lt;code&gt;http://localhost&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Again note that during development, for live reloads, you need to use the &lt;code&gt;compose.dev.yml&lt;/code&gt; file, while for testing a production build locally, you should use &lt;code&gt;compose.prod.yml&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Adapting The Production Compose File For Deployment&lt;span class="hx:absolute hx:-mt-20" id="adapting-the-production-compose-file-for-deployment"&gt;&lt;/span&gt;
&lt;a href="#adapting-the-production-compose-file-for-deployment" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We can use the same Dockerfile for building the production image during deployment. However, we need to change few things in the compose:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Generally, in production deployments, we do not build the image on the server itself. Instead, we build it in a CI/CD pipeline and push it to a container registry. The deployment server then pulls the pre-built image from the registry. If the server builds the images from the repository, a compose file similar to our &lt;code&gt;compose.prod.yml&lt;/code&gt; can be used. But if we are pulling the pre-built image from a registry, we need to replace the &lt;code&gt;build&lt;/code&gt; section with &lt;code&gt;image&lt;/code&gt; in our compose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;We will not use &lt;code&gt;env_file&lt;/code&gt; in production deployments.&lt;/strong&gt; Instead, we will set the environment variables directly in the deployment platform or server, and pass them to the containers. &lt;strong&gt;The &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.production&lt;/code&gt; files should not be included in the image or the deployment compose file.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You might not want to expose ports for database and other services, based on the requirements you can remove the ones you do not need. If you are using a reverse proxy in front of your application, you might not need to expose ports from the app container as well.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This part largely depends on your deployment platform and strategy. Below is a sample deployment compose file &lt;code&gt;compose.deploy.yml&lt;/code&gt; assuming the above conditions :&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;compose.deploy.yml&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: postgres:17.6-alpine3.21
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-db-data:/var/lib/postgresql/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - POSTGRES_DB=${DB_DATABASE}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - POSTGRES_USER=${DB_USERNAME}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - POSTGRES_PASSWORD=${DB_PASSWORD}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#39;CMD-SHELL&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;pg_isready -U $POSTGRES_USER&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;interval&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;timeout&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;retries&lt;/span&gt;: &lt;span style="color:#fab387"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;app&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: ${REGISTRY_URL}/demo-project:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;pull_policy&lt;/span&gt;: always
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;condition&lt;/span&gt;: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_NAME&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_NAME}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_ENV&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_ENV}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_KEY&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_KEY}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_DEBUG&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_DEBUG}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_URL&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_URL}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ASSET_URL&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${ASSET_URL}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;# Other environment variables as needed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#39;CMD-SHELL&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;curl -f http://localhost:8080/up || exit 1&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;interval&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;timeout&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;start_period&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;retries&lt;/span&gt;: &lt;span style="color:#fab387"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-app-data:/var/www/html/storage
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;queue&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;image&lt;/span&gt;: ${REGISTRY_URL}/demo-project:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;pull_policy&lt;/span&gt;: always
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;db&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;condition&lt;/span&gt;: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;command&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#39;php&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;/var/www/html/artisan&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;queue:work&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;--tries=3&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;--timeout=90&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_NAME&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_NAME}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_ENV&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_ENV}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_KEY&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_KEY}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_DEBUG&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_DEBUG}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;APP_URL&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${APP_URL}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;ASSET_URL&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#39;${ASSET_URL}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;PHP_FPM_POOL_NAME&lt;/span&gt;: app_queue
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;# Other environment variables as needed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;healthcheck&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;test&lt;/span&gt;: [&lt;span style="color:#a6e3a1"&gt;&amp;#39;CMD&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#39;healthcheck-queue&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;start_period&lt;/span&gt;: 5s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - demo-project-app-data:/var/www/html/storage
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;demo-project-db-data&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; demo-project-app-data:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You can set the &lt;code&gt;REGISTRY_URL&lt;/code&gt; environment variable in your server environment to point to your container registry.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/&gt;&lt;/svg&gt;Note&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;ol&gt;
&lt;li&gt;The image above is using the &lt;code&gt;latest&lt;/code&gt; tag, however it is recommended to use specific version tags or commit SHAs for better control and rollback capabilities.&lt;/li&gt;
&lt;li&gt;If you face any errors related to static assets in production, ensure that the &lt;code&gt;ASSET_URL&lt;/code&gt; variable is set correctly (which should be the same as &lt;code&gt;APP_URL&lt;/code&gt; unless you are using a CDN or separate domain for assets).&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You can use Github Actions for building and pushing images to a registry. The following is a sample workflow you can use as a starting point :&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;.github/workflows/deploy.yml&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Build and Deploy to Production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;branches&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - main
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;runs-on&lt;/span&gt;: ubuntu-latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Checkout code
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;uses&lt;/span&gt;: actions/checkout@v4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Set up Docker Buildx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;uses&lt;/span&gt;: docker/setup-buildx-action@v3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Log in to Docker registry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;uses&lt;/span&gt;: docker/login-action@v3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;registry&lt;/span&gt;: ${{ secrets.REGISTRY_URL }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;username&lt;/span&gt;: ${{ secrets.REGISTRY_USERNAME }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;password&lt;/span&gt;: ${{ secrets.REGISTRY_PASSWORD }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Build and push Docker image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;uses&lt;/span&gt;: docker/build-push-action@v5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;context&lt;/span&gt;: .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;push&lt;/span&gt;: &lt;span style="color:#fab387"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;target&lt;/span&gt;: production
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;tags&lt;/span&gt;: |&lt;span style="color:#6c7086"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086"&gt; ${{ secrets.REGISTRY_URL }}/demo-project:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086"&gt; ${{ secrets.REGISTRY_URL }}/demo-project:${{ github.sha }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;release&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;runs-on&lt;/span&gt;: ubuntu-latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;needs&lt;/span&gt;: build
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;permissions&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;contents&lt;/span&gt;: write
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Create GitHub Release
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;uses&lt;/span&gt;: softprops/action-gh-release@v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;tag_name&lt;/span&gt;: release-${{ github.run_number }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Release ${{ github.run_number }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;body&lt;/span&gt;: |&lt;span style="color:#6c7086"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086"&gt; Commit: ${{ github.sha }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086"&gt; Message: ${{ github.event.head_commit.message }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086"&gt; Image: demo-project:${{ github.sha }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;draft&lt;/span&gt;: &lt;span style="color:#fab387"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;prerelease&lt;/span&gt;: &lt;span style="color:#fab387"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;deploy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;runs-on&lt;/span&gt;: ubuntu-latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;needs&lt;/span&gt;: release
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#cba6f7"&gt;name&lt;/span&gt;: Trigger Deployment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;run&lt;/span&gt;: |&lt;span style="color:#6c7086"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086"&gt; curl -X POST &amp;#39;${{ secrets.DEPLOY_WEBHOOK_URL }}&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You might need to adjust the workflow based on your requirements and configure the secrets in your repository settings. After building and pushing the image, the workflow triggers a deployment webhook.&lt;/p&gt;
&lt;h3&gt;Further Improvements&lt;span class="hx:absolute hx:-mt-20" id="further-improvements"&gt;&lt;/span&gt;
&lt;a href="#further-improvements" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;You can further improve this setup by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Using a &lt;code&gt;compose.base.yml&lt;/code&gt; file to define common services and configurations, and then extending it in &lt;code&gt;compose.dev.yml&lt;/code&gt; and &lt;code&gt;compose.prod.yml&lt;/code&gt; files. This will help reduce duplication and make it easier to manage changes across different environments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sending the commit SHA in the webhook payload and using it to pull specific image versions during deployment for better traceability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set up pre-production deployments using Github actions based on branches or tags.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set up cache in Github actions (registry cache works with docker/build-push-action) to speed up build times.&lt;/p&gt;
&lt;p&gt;If you have any questions or suggestions, feel free to reach out via email.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Building Modern Cross Browser Web Extensions: Core Functionality, Storage and Permissions (Part 5)</title><link>https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-core-functionality-storage-and-permissions/</link><pubDate>Tue, 04 Feb 2025 10:00:00 +0530</pubDate><guid>https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-core-functionality-storage-and-permissions/</guid><description>
&lt;p&gt;In the &lt;a href="https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-background-scripts-and-messaging/"&gt;previous post&lt;/a&gt;, we explored the background scripts and the messaging in extensions, as well as using &lt;a href="https://www.npmjs.com/package/@webext-core/messaging"target="_blank" rel="noopener"&gt;@webext-core/messaging&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for a robust and type-safe messaging system. In this part, we will use the same concepts to build the core functionality of our extension.&lt;/p&gt;
&lt;h3&gt;Basic Commands&lt;span class="hx:absolute hx:-mt-20" id="basic-commands"&gt;&lt;/span&gt;
&lt;a href="#basic-commands" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We want to add actions to the extension such as opening a new tab, a new window as well as state based actions such as muting/unmuting or pinning/unpinning a tab. Let&amp;rsquo;s follow the same pattern we explored in the previous post.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First, we will define the types for the actions we want to add to the extension.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/types.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; UserAction &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title: &lt;span style="color:#f38ba8"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: &lt;span style="color:#f38ba8"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt;; &lt;span style="color:#6c7086;font-style:italic"&gt;// To hide actions such as mute/unmute conditionally
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Next, we will set up messaging in &lt;code&gt;src/lib/messaging.ts&lt;/code&gt; as follows:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineExtensionMessaging } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@webext-core/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; userAction(data&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt; })&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; { sendMessage, onMessage } &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; defineExtensionMessaging&amp;lt;&lt;span style="color:#cba6f7"&gt;ProtocolMap&lt;/span&gt;&amp;gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Create a new file &lt;code&gt;src/lib/user.actions.ts&lt;/code&gt; to define the actions:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/user.actions.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; { UserAction } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; userActions: &lt;span style="color:#f38ba8"&gt;UserAction&lt;/span&gt;[] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;New Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;New Window&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Note that although this file is not explicitly marked as a content script, its actions will be utilized within the Command Palette, which itself is a part of our main content script.&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;We will also add the listeners in the background script to handle these actions:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { onMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, (userAction) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;switch&lt;/span&gt; (userAction.data.&lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.tabs.create({});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.windows.create({});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Finally, we will use these actions in the Command Palette component:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;*&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;as&lt;/span&gt; React &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Command,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandDialog,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandEmpty,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandInput,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandItem,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandList,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/command&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { userActions } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/user.actions&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; CommandPalette() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [open, setOpen] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; React.useState(&lt;span style="color:#fab387"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; React.useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; down &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; (e: &lt;span style="color:#f38ba8"&gt;KeyboardEvent&lt;/span&gt;) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (e.key &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;j&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (e.metaKey &lt;span style="color:#89dceb;font-weight:bold"&gt;||&lt;/span&gt; e.ctrlKey)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; e.preventDefault();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setOpen((open) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;open);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb"&gt;document&lt;/span&gt;.addEventListener(&lt;span style="color:#a6e3a1"&gt;&amp;#34;keydown&amp;#34;&lt;/span&gt;, down);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#89dceb"&gt;document&lt;/span&gt;.removeEventListener(&lt;span style="color:#a6e3a1"&gt;&amp;#34;keydown&amp;#34;&lt;/span&gt;, down);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, []);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandDialog&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;open&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{open} &lt;span style="color:#89b4fa"&gt;onOpenChange&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{setOpen}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Command&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;loop&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{&lt;span style="color:#fab387"&gt;true&lt;/span&gt;} &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;max-h-96 min-h-96 rounded-lg shadow-md&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandInput&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;placeholder&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;Type a command or search...&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandList&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandEmpty&lt;/span&gt;&amp;gt;No results found.&amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandEmpty&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {userActions.map((action) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandItem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;key&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{action.id}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;value&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{action.title}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;onSelect&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; action.handler();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setOpen(&lt;span style="color:#fab387"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex-1 truncate px-3 text-start&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {action.title}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandItem&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandList&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;Command&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandDialog&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;That&amp;rsquo;s all we need to do, and our actions should now be available in the Command Palette.&lt;/p&gt;
&lt;figure class="video"&gt;
&lt;video
controls
preload="metadata"
width="1080"
&gt;
&lt;source src="https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-core-functionality-storage-and-permissions/new-tab.mp4" type="video/mp4" /&gt;
&lt;/video&gt;
&lt;/figure&gt;
&lt;h3&gt;State Dependent Commands&lt;span class="hx:absolute hx:-mt-20" id="state-dependent-commands"&gt;&lt;/span&gt;
&lt;a href="#state-dependent-commands" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We can also add actions that are dependent on the state of the tab, such as muting/unmuting or pinning/unpinning. Let&amp;rsquo;s add these actions to the extension.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add the Tab type to &lt;code&gt;src/lib/types.ts&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/types.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; Tab &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; chrome.tabs.Tab;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;We will need the current tab to get the state as well as to perform actions on it. Let&amp;rsquo;s update our messaging setup for the same:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineExtensionMessaging } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@webext-core/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { Tab } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; userAction(data&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tab?: &lt;span style="color:#f38ba8"&gt;Tab&lt;/span&gt;; &lt;span style="color:#6c7086;font-style:italic"&gt;// We will need the tab info for updating its state
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; })&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getActiveTab()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;Tab&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; { sendMessage, onMessage } &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; defineExtensionMessaging&amp;lt;&lt;span style="color:#cba6f7"&gt;ProtocolMap&lt;/span&gt;&amp;gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We have added &lt;code&gt;muteTab&lt;/code&gt; and &lt;code&gt;unmuteTab&lt;/code&gt; actions, and an optional &lt;code&gt;tab&lt;/code&gt; parameter to the &lt;code&gt;userAction&lt;/code&gt; message. Whenever we want to use the browser APIs to update the state of a tab, we need to provide its unique id, which we can get from this &lt;code&gt;tab&lt;/code&gt; parameter. We have also added a &lt;code&gt;getActiveTab&lt;/code&gt; action to get the current active tab.&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Let&amp;rsquo;s update our background script to handle these actions:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { onMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, (userAction) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;switch&lt;/span&gt; (userAction.data.&lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// ...listeners for newTab and newWindow actions remain the same
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (userAction.data.tab&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.tabs.update(userAction.data.tab.id, { muted: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (userAction.data.tab&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.tabs.update(userAction.data.tab.id, { muted: &lt;span style="color:#f38ba8"&gt;false&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getActiveTab&amp;#34;&lt;/span&gt;, &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; tabs &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; browser.tabs.query({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; active: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentWindow: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; tabs[&lt;span style="color:#fab387"&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We have a separate listener for &lt;code&gt;getActiveTab&lt;/code&gt; to get the current active tab, while &lt;code&gt;muteTab&lt;/code&gt; and &lt;code&gt;unmuteTab&lt;/code&gt; actions are under the &lt;code&gt;userAction&lt;/code&gt; listener. This is in accordance with our messaging setup. Though we have not mentioned return type here for listener of &lt;code&gt;getActiveTab&lt;/code&gt;, its return type will be &lt;code&gt;Tab&lt;/code&gt;, which, again, is inferred from our messaging setup.&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;In our content script, instead of directly returning &lt;code&gt;userActions&lt;/code&gt; as an array, we will now use an async function, &lt;code&gt;getUserActions&lt;/code&gt;, which will return actions based on the state of the tab:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/user.actions.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; { UserAction } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; getUserActions &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; ()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;UserAction&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;[]&lt;/span&gt;&amp;gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// Note that when we don&amp;#39;t have any parameters, we have to pass undefined
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; activeTab &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getActiveTab&amp;#34;&lt;/span&gt;, &lt;span style="color:#fab387"&gt;undefined&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; isMuted: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; activeTab.mutedInfo&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.muted &lt;span style="color:#89dceb;font-weight:bold"&gt;||&lt;/span&gt; &lt;span style="color:#fab387"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; userActions: &lt;span style="color:#f38ba8"&gt;UserAction&lt;/span&gt;[] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// ...newTab and newWindow actions remain the same
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Mute Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;isMuted,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Unmute Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;isMuted&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; userActions.filter((action) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; action.visible);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Since &lt;code&gt;getUserActions&lt;/code&gt; is an async function, we need to update the Command Palette component to handle the promise:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;*&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;as&lt;/span&gt; React &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Command,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandDialog,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandEmpty,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandInput,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandItem,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CommandList,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/command&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { getUserActions } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/user.actions&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { UserAction } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; CommandPalette() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [open, setOpen] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; React.useState(&lt;span style="color:#fab387"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [userActions, setUserActions] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; React.useState&amp;lt;&lt;span style="color:#cba6f7"&gt;UserAction&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;[]&lt;/span&gt;&amp;gt;([]);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; React.useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; down &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; (e: &lt;span style="color:#f38ba8"&gt;KeyboardEvent&lt;/span&gt;) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (e.key &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;j&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (e.metaKey &lt;span style="color:#89dceb;font-weight:bold"&gt;||&lt;/span&gt; e.ctrlKey)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; e.preventDefault();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setOpen((open) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;open);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb"&gt;document&lt;/span&gt;.addEventListener(&lt;span style="color:#a6e3a1"&gt;&amp;#34;keydown&amp;#34;&lt;/span&gt;, down);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#89dceb"&gt;document&lt;/span&gt;.removeEventListener(&lt;span style="color:#a6e3a1"&gt;&amp;#34;keydown&amp;#34;&lt;/span&gt;, down);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, []);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; React.useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;open) &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; getUserActions().then(setUserActions);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }, [open]);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandDialog&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;open&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{open} &lt;span style="color:#89b4fa"&gt;onOpenChange&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{setOpen}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Command&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;loop&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{&lt;span style="color:#fab387"&gt;true&lt;/span&gt;} &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;max-h-96 min-h-96 rounded-lg shadow-md&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandInput&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;placeholder&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;Type a command or search...&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandList&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandEmpty&lt;/span&gt;&amp;gt;No results found.&amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandEmpty&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {userActions.map((action) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandItem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;key&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{action.id}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;value&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{action.title}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;onSelect&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; action.handler();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setOpen(&lt;span style="color:#fab387"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex-1 truncate px-3 text-start&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {action.title}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandItem&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandList&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;Command&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandDialog&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Now, the Command Palette will show the &lt;code&gt;Mute Tab&lt;/code&gt; and &lt;code&gt;Unmute Tab&lt;/code&gt; actions based on the state of the active tab. We can also filter visible actions in &lt;code&gt;useEffect&lt;/code&gt; instead of filtering them in the &lt;code&gt;getUserActions&lt;/code&gt; function, if desired. We can similarly add other conditional actions such as &lt;code&gt;Pin Tab&lt;/code&gt; and &lt;code&gt;Unpin Tab&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="video"&gt;
&lt;video
controls
preload="metadata"
width="1080"
&gt;
&lt;source src="https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-core-functionality-storage-and-permissions/conditional-actions.mp4" type="video/mp4" /&gt;
&lt;/video&gt;
&lt;/figure&gt;
&lt;h3&gt;Adding Keybindings To The Command Palette&lt;span class="hx:absolute hx:-mt-20" id="adding-keybindings-to-the-command-palette"&gt;&lt;/span&gt;
&lt;a href="#adding-keybindings-to-the-command-palette" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Let&amp;rsquo;s display keybindings for the actions. Later, we will provide a way for the user to toggle the visibility of keybindings using browser storage to store user preferences.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First update our &lt;code&gt;UserAction&lt;/code&gt; type to include a &lt;code&gt;keybinding&lt;/code&gt; property:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/types.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; UserAction &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title: &lt;span style="color:#f38ba8"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: &lt;span style="color:#f38ba8"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; keybinding?: &lt;span style="color:#f38ba8"&gt;string&lt;/span&gt;[]; &lt;span style="color:#6c7086;font-style:italic"&gt;//optional
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;We can detect the OS using the &lt;code&gt;browser.runtime.getPlatformInfo&lt;/code&gt; API and set the keybindings accordingly. This API can only be used in background script, so we will update our messaging setup, add listener to background script, and update the &lt;code&gt;user.actions.ts&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing protocol map
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; getPlatformInfo()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;chrome.runtime.PlatformInfo&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing listeners
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getPlatformInfo&amp;#34;&lt;/span&gt;, &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; browser.runtime.getPlatformInfo();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/user.actions.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; { UserAction } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; getUserActions &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; ()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;UserAction&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;[]&lt;/span&gt;&amp;gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; activeTab &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getActiveTab&amp;#34;&lt;/span&gt;, &lt;span style="color:#fab387"&gt;undefined&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; isMuted: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; activeTab.mutedInfo&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.muted &lt;span style="color:#89dceb;font-weight:bold"&gt;||&lt;/span&gt; &lt;span style="color:#fab387"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; isPinned: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; activeTab.pinned &lt;span style="color:#89dceb;font-weight:bold"&gt;||&lt;/span&gt; &lt;span style="color:#fab387"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; platform &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getPlatformInfo&amp;#34;&lt;/span&gt;, &lt;span style="color:#fab387"&gt;undefined&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; isMac &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; platform.os &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;mac&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; userActions: &lt;span style="color:#f38ba8"&gt;UserAction&lt;/span&gt;[] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;New Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;T&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;New Window&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;N&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Mute Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;isMuted,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; keybinding: &lt;span style="color:#f38ba8"&gt;isMac&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;Shift&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;] &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Unmute Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;isMuted&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; keybinding: &lt;span style="color:#f38ba8"&gt;isMac&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;Shift&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;] &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Pin Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;pinTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;pinTab&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;isPinned,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Unpin Tab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unpinTab&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unpinTab&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;isPinned&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; visibleActions &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; userActions.filter((action) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; action.visible);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// change &amp;#39;Command&amp;#39; to &amp;#39;⌘&amp;#39;, Alt to &amp;#39;⌥&amp;#39; and Shift to &amp;#39;⇧&amp;#39; on Mac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// &amp;#39;Command&amp;#39; is &amp;#39;Ctrl&amp;#39; on Windows/Linux
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; formattedActions &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; visibleActions.map((action) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; action.keybinding &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; action.keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.map((key) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (key &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; isMac &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;⌘&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Ctrl&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; } &lt;span style="color:#cba6f7"&gt;else&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (key &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Alt&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; isMac &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;⌥&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Alt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; } &lt;span style="color:#cba6f7"&gt;else&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (key &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Shift&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; isMac &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;⇧&amp;#34;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Shift&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; key;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; action;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; formattedActions;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Finally, render the keybindings in the Command Palette component:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{userActions.map((action) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandItem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;key&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{action.id}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;value&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{action.title}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;onSelect&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; action.handler();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setOpen(&lt;span style="color:#fab387"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex-1 truncate px-3 text-start&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {action.title}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; {action.keybinding &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex items-center space-x-2&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; {action.keybinding.map((key, i) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;kbd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;key&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{key}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;px-2 py-1 text-foreground bg-accent rounded
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#a6e3a1"&gt; text-sm text-center&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; {key}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;kbd&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; {action.keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.length &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; i &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;lt;&lt;/span&gt; action.keybinding.length &lt;span style="color:#89dceb;font-weight:bold"&gt;-&lt;/span&gt; &lt;span style="color:#fab387"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;+&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; ))}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;CommandItem&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;))}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The Command Palette will now show the keybindings based on the OS.
&lt;figure&gt;
&lt;img src="./regular-keybindings.png" title="On Windows/Linux" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;On Windows/Linux&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src="./mac-keybindings.png" title="On Mac" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;On Mac&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Note that the &lt;code&gt;muteTab&lt;/code&gt; and &lt;code&gt;unmuteTab&lt;/code&gt; actions have different keybindings for Mac and other platforms, which we have handled based on the &lt;code&gt;isMac&lt;/code&gt; variable.&lt;/p&gt;
&lt;h3&gt;Storage&lt;span class="hx:absolute hx:-mt-20" id="storage"&gt;&lt;/span&gt;
&lt;a href="#storage" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The browser&amp;rsquo;s asynchronous storage API provides persistent key-value storage for user preferences, extension state, and other application data. We can use the &lt;code&gt;local&lt;/code&gt; storage, which is persistent across browser sessions, or the &lt;code&gt;session&lt;/code&gt; storage, which is cleared when the browser is closed. We can also use the &lt;code&gt;sync&lt;/code&gt; storage, which is synced across devices if the user is signed in to their browser account. Each type of storage has different limits on the amount of data it can store. The &lt;code&gt;storage&lt;/code&gt; API is available in all extension contexts, including background scripts and content scripts. For more details, refer to the &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage"target="_blank" rel="noopener"&gt;MDN Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; or &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/storage"target="_blank" rel="noopener"&gt;Chrome Developer Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. Before proceeding, you are encouraged to read these docs and understand the differences between using storage APIs and other storage methods such as &lt;code&gt;localStorage&lt;/code&gt; or &lt;code&gt;IndexedDB&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;WXT provides a well designed &lt;a href="https://wxt.dev/storage.html"target="_blank" rel="noopener"&gt;wrapper&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; around the browser storage APIs, with lots of features such as type safety, versioning, metadata, bulk operations, watchers and more, which makes it easier to work with storage in extensions. These are thoroughly documented in the WXT docs, so we will not go into details here.&lt;/p&gt;
&lt;h4&gt;Adding A Toggle To Show/Hide Keybindings&lt;span class="hx:absolute hx:-mt-20" id="adding-a-toggle-to-showhide-keybindings"&gt;&lt;/span&gt;
&lt;a href="#adding-a-toggle-to-showhide-keybindings" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;To demonstrate the use of &lt;code&gt;storage&lt;/code&gt; API, we will add a toggle in the popup to show or hide keybindings in the Command Palette.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We need to add the &lt;code&gt;storage&lt;/code&gt; permission in our &lt;code&gt;wxt.config.ts&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineConfig } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;wxt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// See https://wxt.dev/api/config.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; manifest&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command Palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;A command palette to quickly perform actions&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;storage&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;We will add a new file &lt;code&gt;src/lib/storage.ts&lt;/code&gt; to handle storage operations. We will use the &lt;a href="https://wxt.dev/storage.html#defining-storage-items"target="_blank" rel="noopener"&gt;defined storage&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; from WXT:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/storage.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// Note the local: prefix, which is used to store the data in local storage
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; showKeybindings &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; storage.defineItem&amp;lt;&lt;span style="color:#cba6f7"&gt;boolean&lt;/span&gt;&amp;gt;(&lt;span style="color:#a6e3a1"&gt;&amp;#34;local:showKeybindings&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; defaultValue: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fallback: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; version: &lt;span style="color:#f38ba8"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; store &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showKeybindings,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;storage&lt;/code&gt; is auto imported here from &lt;code&gt;@wxt-dev/storage&lt;/code&gt;. We have defined a new item &lt;code&gt;showKeybindings&lt;/code&gt; with a default value of &lt;code&gt;true&lt;/code&gt;, and we are using local storage for this item. For convenience, we have exported a single object &lt;code&gt;store&lt;/code&gt; which contains all the storage items. If we have large number of storage items, instead of exporting them individually, we can export them as a single object.&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;We will add a switch (from Shadcn) in our &lt;code&gt;src/entrypoints/popup/App.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/popup/App.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { useState, useEffect } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { Switch } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/switch&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { Label } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/label&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { store } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/storage&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; App() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [showKeybindings, setShowKeybindings] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; useState(&lt;span style="color:#fab387"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store.showKeybindings.getValue().then(setShowKeybindings);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, []);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;m-3 w-72&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;h1&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;text-lg font-bold mb-3&amp;#34;&lt;/span&gt;&amp;gt;Settings&amp;lt;/&lt;span style="color:#cba6f7"&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex items-center justify-between mb-3&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Label&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;htmlFor&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;showKeybindings&amp;#34;&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;font-normal&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Show keybindings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;Label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Switch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;id&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;showKeybindings&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;checked&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{showKeybindings}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;onCheckedChange&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{(checked) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store.showKeybindings.setValue(checked);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setShowKeybindings(checked);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;&amp;lt;/&lt;span style="color:#cba6f7"&gt;Switch&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; App;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;In the Command Palette, all we have to do is check the value from storage and render the keybindings accordingly. We will update the existing &lt;code&gt;useEffect&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { store } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/storage&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; CommandPalette() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [showKeybindings, setShowKeybindings] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; React.useState(&lt;span style="color:#fab387"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing states and effects
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; React.useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;open) &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store.showKeybindings.getValue().then(setShowKeybindings);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getUserActions().then(setUserActions);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, [open]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Use the &lt;code&gt;showKeybindings&lt;/code&gt; state to conditionally render the keybindings in the Command Palette:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{action.keybinding &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; showKeybindings &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex items-center space-x-2&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {action.keybinding.map((key, i) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;kbd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;key&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{key}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;px-2 py-1 text-foreground bg-accent rounded text-sm text-center&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {key}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;kbd&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {action.keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.length &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; i &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;lt;&lt;/span&gt; action.keybinding.length &lt;span style="color:#89dceb;font-weight:bold"&gt;-&lt;/span&gt; &lt;span style="color:#fab387"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;+&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The value is read from the storage when the Command Palette is opened, so if the user changes the setting in the popup, they will need to close and reopen the Command Palette to see the changes. We can add a watcher to the storage item to update the state in real time:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;React.useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;open) &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store.showKeybindings.getValue().then(setShowKeybindings);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getUserActions().then(setUserActions);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; unwatch &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; store.showKeybindings.watch(setShowKeybindings);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; unwatch();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}, [open]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure class="video"&gt;
&lt;video
controls
preload="metadata"
width="1080"
&gt;
&lt;source src="https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-core-functionality-storage-and-permissions/storage.mp4" type="video/mp4" /&gt;
&lt;/video&gt;
&lt;/figure&gt;
&lt;p&gt;Since the storage API is available in all extension contexts, we can perform storage operations directly without needing to use message passing or adding listeners in the background script.&lt;/p&gt;
&lt;h4&gt;Viewing The Stored Data&lt;span class="hx:absolute hx:-mt-20" id="viewing-the-stored-data"&gt;&lt;/span&gt;
&lt;a href="#viewing-the-stored-data" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;We can view the stored data in the browser directly. Note that the storage data will be empty if the default value of &lt;code&gt;showKeybindings&lt;/code&gt; in the popup has not been changed.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In Chrome, open the service worker DevTools (the same way we accessed the console for the background script), and go to the &lt;code&gt;Application&lt;/code&gt; tab. You will see the &lt;code&gt;Extension Storage&lt;/code&gt; section on the left, which will has different storage areas such as &lt;code&gt;Local&lt;/code&gt;, &lt;code&gt;Session&lt;/code&gt;, and &lt;code&gt;Sync&lt;/code&gt;. Our stored data will be visible in the &lt;code&gt;Local&lt;/code&gt; section.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img src="./chrome-storage.png" title="Chrome Storage" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;Chrome Storage&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;In Firefox, open the DevTools by going to &lt;code&gt;about:debugging#/runtime/this-firefox&lt;/code&gt; and clicking on &lt;code&gt;Inspect&lt;/code&gt; for your extension. Under &lt;code&gt;Storage&lt;/code&gt; tab, and you will see the &lt;code&gt;Extension Storage&lt;/code&gt; section on the left, which has the stored data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img src="./firefox-storage.png" title="Firefox Storage" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;Firefox Storage&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;This is a very simple demonstration, and WXT offers many more storage related-features which are not covered here. You can refer to the &lt;a href="https://wxt.dev/storage.html"target="_blank" rel="noopener"&gt;WXT storage docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h3&gt;Permissions&lt;span class="hx:absolute hx:-mt-20" id="permissions"&gt;&lt;/span&gt;
&lt;a href="#permissions" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;So far, our actions were simple and did not require any permissions to be declared in the &lt;code&gt;manifest.json&lt;/code&gt; (besides the &lt;code&gt;storage&lt;/code&gt;). However, some actions might require additional permissions, such as adding or removing a bookmark. We have already seen an example of using the &lt;code&gt;history&lt;/code&gt; permission in the previous post, and we can similarly add &lt;code&gt;bookmark&lt;/code&gt; permission to our manifest. But this is not the best way of handling permissions, since the permissions listed in manifest are required to be granted during installation itself, which might put off the users if our extension needs sensitive permissions. A better way is to use minimum required permissions during installation, and add the rest as optional permissions in the manifest, which can be requested on runtime if needed. Let&amp;rsquo;s see how we can add actions that require permissions as well as requesting these permissions at runtime.&lt;/p&gt;
&lt;h4&gt;Adding And Removing Bookmarks With Install-Time Permissions&lt;span class="hx:absolute hx:-mt-20" id="adding-and-removing-bookmarks-with-install-time-permissions"&gt;&lt;/span&gt;
&lt;a href="#adding-and-removing-bookmarks-with-install-time-permissions" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Let&amp;rsquo;s quickly add new actions to add and remove the current tab as a bookmark. We will also add a new required permission &lt;code&gt;bookmarks&lt;/code&gt; in the &lt;code&gt;wxt.config.ts&lt;/code&gt; for now (which we will later update to runtime permission):&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineConfig } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;wxt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// See https://wxt.dev/api/config.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; manifest&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command Palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;A command palette to quickly perform actions&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;storage&amp;#34;&lt;/span&gt;,&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; userAction(data&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newTab&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;newWindow&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;muteTab&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unmuteTab&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;pinTab&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;unpinTab&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;addBookmark&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;|&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;removeBookmark&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tab?: &lt;span style="color:#f38ba8"&gt;Tab&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; isBookmarked(data&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; { tab: &lt;span style="color:#f38ba8"&gt;Tab&lt;/span&gt; })&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;boolean&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing protocol map
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/user.actions.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; isBookmarked &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;isBookmarked&amp;#34;&lt;/span&gt;, { tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; userActions: &lt;span style="color:#f38ba8"&gt;UserAction&lt;/span&gt;[] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing actions
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Add Bookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;addBookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;addBookmark&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;isBookmarked,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;D&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Remove Bookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;removeBookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;removeBookmark&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;isBookmarked&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;D&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, (userAction) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;switch&lt;/span&gt; (userAction.data.&lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//..existing cases
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;addBookmark&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (userAction.data.tab&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.bookmarks.create({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title: &lt;span style="color:#f38ba8"&gt;userAction.data.tab.title&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url: &lt;span style="color:#f38ba8"&gt;userAction.data.tab.url&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;removeBookmark&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (userAction.data.tab&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.bookmarks.search({ url: &lt;span style="color:#f38ba8"&gt;userAction.data.tab.url&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .then((bookmarks) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (bookmarks.length) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.bookmarks.remove(bookmarks[&lt;span style="color:#fab387"&gt;0&lt;/span&gt;].id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;isBookmarked&amp;#34;&lt;/span&gt;, &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; (message) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;message.data.tab&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.url) &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; bookmarks &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; browser.bookmarks.search({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url: &lt;span style="color:#f38ba8"&gt;message.data.tab.url&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; bookmarks.length &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#fab387"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Our add and remove bookmark actions should work now.&lt;/p&gt;
&lt;h4&gt;Conditionally Showing Actions Based On Permissions&lt;span class="hx:absolute hx:-mt-20" id="conditionally-showing-actions-based-on-permissions"&gt;&lt;/span&gt;
&lt;a href="#conditionally-showing-actions-based-on-permissions" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The visibility of certain actions depends on the specific permissions being granted. If we don&amp;rsquo;t have the &lt;code&gt;bookmarks&lt;/code&gt; permission, we should not show either of the &amp;lsquo;Add Bookmark&amp;rsquo; and &amp;lsquo;Remove Bookmark&amp;rsquo; actions, as they won&amp;rsquo;t work in that case. Before displaying such actions, we need to check the permissions, as well as provide a way for the user to grant them if they are listed as optional permissions.
Let&amp;rsquo;s update &lt;code&gt;wxt.config.ts&lt;/code&gt; to remove the &lt;code&gt;bookmarks&lt;/code&gt; permission from the required permissions and add it to optional permissions:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineConfig } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;wxt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// See https://wxt.dev/api/config.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; manifest&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command Palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;A command palette to quickly perform actions&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;storage&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; optional_permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Again we need to update types and the messaging setup:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/types.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; Permissions &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; chrome.permissions.Permissions;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing protocol map
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; getPermissions()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;Permissions&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getPermissions&amp;#34;&lt;/span&gt;, &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; browser.permissions.getAll();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Consider the scenarios under which we want the &amp;lsquo;Add Bookmark&amp;rsquo; and &amp;lsquo;Remove Bookmark&amp;rsquo; to be visible:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Has permission&lt;/th&gt;
&lt;th&gt;Is Bookmarked&lt;/th&gt;
&lt;th&gt;Show &amp;lsquo;Add&amp;rsquo; action&lt;/th&gt;
&lt;th&gt;Show &amp;lsquo;Remove&amp;rsquo; action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In simpler words, if we don&amp;rsquo;t have the permission, we won&amp;rsquo;t show either of the options. If we have the permission, we will check if the page is bookmarked or not and show the corresponding action, so we need to check both the parameters here. Update the &lt;code&gt;user.actions.ts&lt;/code&gt; file to include this logic:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/user.actions.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; { Tab, UserAction } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; getBookmarkStatus &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; tab: &lt;span style="color:#f38ba8"&gt;chrome.tabs.Tab&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;)&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;lt;&lt;/span&gt;[hasBookmarksPermission: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt;, isBookmarked: &lt;span style="color:#f38ba8"&gt;boolean&lt;/span&gt;]&lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; permissionsInfo &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getPermissions&amp;#34;&lt;/span&gt;, &lt;span style="color:#fab387"&gt;undefined&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; hasBookmarksPermission &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; permissionsInfo.permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.includes(&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// If we don&amp;#39;t have the permission, we won&amp;#39;t show either of the options,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;hasBookmarksPermission) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; [&lt;span style="color:#fab387"&gt;false&lt;/span&gt;, &lt;span style="color:#fab387"&gt;false&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; isBookmarked &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;isBookmarked&amp;#34;&lt;/span&gt;, { tab });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; [hasBookmarksPermission, isBookmarked];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; getUserActions &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; ()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;UserAction&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;[]&lt;/span&gt;&amp;gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing checks
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [hasBookmarksPermission, isBookmarked] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; getBookmarkStatus(activeTab);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; userActions: &lt;span style="color:#f38ba8"&gt;UserAction&lt;/span&gt;[] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing actions
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Add Bookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;addBookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;addBookmark&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;hasBookmarksPermission&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;isBookmarked,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;D&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Remove Bookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;removeBookmark&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;userAction&amp;#34;&lt;/span&gt;, { &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;removeBookmark&amp;#34;&lt;/span&gt;, tab: &lt;span style="color:#f38ba8"&gt;activeTab&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; visible: &lt;span style="color:#f38ba8"&gt;hasBookmarksPermission&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; isBookmarked,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; keybinding&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;Command&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;D&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We have added a new function &lt;code&gt;getBookmarkStatus&lt;/code&gt; to check the discussed conditions. However, both of our bookmark actions won&amp;rsquo;t be visible as of now, as we have &lt;code&gt;bookmarks&lt;/code&gt; as the optional permission and it hasn&amp;rsquo;t been granted by the user yet.&lt;/p&gt;
&lt;h4&gt;Requesting Permissions At Runtime&lt;span class="hx:absolute hx:-mt-20" id="requesting-permissions-at-runtime"&gt;&lt;/span&gt;
&lt;a href="#requesting-permissions-at-runtime" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Requesting permissions is quite simple – we just have to add a way for user to trigger the request. Let&amp;rsquo;s add a button in our popup through which user can request the permission:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/popup/App.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { useState, useEffect } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { Switch } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/switch&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { store } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/storage&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { Label } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/label&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; App() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [showKeybindings, setShowKeybindings] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; useState(&lt;span style="color:#fab387"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [hasBookmarkPermission, setHasBookmarkPermission] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; useState(&lt;span style="color:#fab387"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store.showKeybindings.getValue().then(setShowKeybindings);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, []);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; onload &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; permissionsInfo &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getPermissions&amp;#34;&lt;/span&gt;, &lt;span style="color:#fab387"&gt;undefined&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//permissionsInfo contains permissions as well as origins
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// we only need permissions here
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (permissionsInfo.permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.includes(&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; setHasBookmarkPermission(&lt;span style="color:#fab387"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; onload();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }, []);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; requestBookmarksPermission &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;hasBookmarkPermission) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; granted &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; browser.permissions.request({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (granted) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; setHasBookmarkPermission(&lt;span style="color:#fab387"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;m-3 w-72 flex flex-col&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;h1&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;text-lg font-bold mb-3&amp;#34;&lt;/span&gt;&amp;gt;Settings&amp;lt;/&lt;span style="color:#cba6f7"&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex items-center justify-between mb-3&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Label&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;htmlFor&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;showKeybindings&amp;#34;&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;font-normal&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Show keybindings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;Label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Switch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;id&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;showKeybindings&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;checked&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{showKeybindings}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;onCheckedChange&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{(checked) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store.showKeybindings.setValue(checked);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setShowKeybindings(checked);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;&amp;lt;/&lt;span style="color:#cba6f7"&gt;Switch&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; {hasBookmarkPermission &lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;text-sm text-wrap mr-2&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; Bookmarks permission granted
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; ) &lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;flex items-center justify-between&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;text-sm text-wrap mr-2&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; Request bookmarks permission(required &lt;span style="color:#cba6f7"&gt;for&lt;/span&gt; adding&lt;span style="color:#89dceb;font-weight:bold"&gt;/&lt;/span&gt;removing bookmarks)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Button&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;font-normal&amp;#34;&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;onClick&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{requestBookmarksPermission}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; Request
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;Button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; )}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; App;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We initially checked if the permission is already granted, and if it isn&amp;rsquo;t, we show a button to request the permission. On button click, we request the permission using &lt;code&gt;browser.permissions.request&lt;/code&gt; and update the state accordingly.&lt;/p&gt;
&lt;figure class="video"&gt;
&lt;video
controls
preload="metadata"
width="1080"
&gt;
&lt;source src="https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-core-functionality-storage-and-permissions/runtime-permissions.mp4" type="video/mp4" /&gt;
&lt;/video&gt;
&lt;/figure&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-amber-200 hx:bg-amber-100 hx:text-amber-900 hx:dark:border-amber-200/30 hx:dark:bg-amber-900/30 hx:dark:text-amber-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/&gt;&lt;/svg&gt;Warning&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;Important note for Firefox: Permissions can only be requested via direct user interaction, such as a button click, and not programmatically. This is to prevent extensions from requesting permissions without user consent. The permission request &lt;strong&gt;must&lt;/strong&gt; be the first thing on the button click. If you add any other code before the permission request, the request will be blocked. For example, try adding a simple timeout before the permission request and you should get an error on the background script&amp;rsquo;s console:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/popup/App.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; requestBookmarksPermission &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;new&lt;/span&gt; Promise((resolve) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; setTimeout(resolve, &lt;span style="color:#fab387"&gt;1000&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (&lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;hasBookmarkPermission) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; granted &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; browser.permissions.request({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (granted) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setHasBookmarkPermission(&lt;span style="color:#fab387"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img src="./firefox-permissions-error.png" title="Firefox Permissions Error" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;Firefox Permissions Error&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;This also means you cannot ask for multiple permissions at once on a single button click (this might also be problematic in chrome). If you need to ask for multiple permissions, the best way would be to ask for them one by one separately.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Making The Shortcut Configurable&lt;span class="hx:absolute hx:-mt-20" id="making-the-shortcut-configurable"&gt;&lt;/span&gt;
&lt;a href="#making-the-shortcut-configurable" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We have been listening to keyboard events in our Command Palette component to toggle its visibility. We can move this to extension level rather than having it in the UI component. This will allow us to make the shortcut configurable by the user.&lt;/p&gt;
&lt;p&gt;First we need to add the suggested keybinding to the manifest in &lt;code&gt;wxt.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineConfig } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;wxt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// See https://wxt.dev/api/config.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; manifest&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command Palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;A command palette to quickly perform actions&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;storage&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; optional_permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;bookmarks&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; commands&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; toggleMainDialog&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; { &lt;span style="color:#6c7086;font-style:italic"&gt;// This is name of the command which we will listen to
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; suggested_key&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Alt+J&amp;#34;&lt;/span&gt;, &lt;span style="color:#6c7086;font-style:italic"&gt;// Ctrl+J is taken by Chrome/Firefox
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Toggle the main dialog&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Next, add a message that can be sent from the background script to the content script to toggle the dialog:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing protocol map
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; toggleMainDialog()&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;void&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Add a listener for the command in the background script (the &lt;code&gt;command&lt;/code&gt; API is not accessible in content scripts), and send a message to the content script to toggle the dialog:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { onMessage, sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing listeners
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; browser.commands.onCommand.addListener((command) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (command &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;toggleMainDialog&amp;#34;&lt;/span&gt;) { &lt;span style="color:#6c7086;font-style:italic"&gt;// same name as in the manifest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// send the message to the content script of active tab
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; browser.tabs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .query({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; active: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentWindow: &lt;span style="color:#f38ba8"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .then((tabs) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (tabs[&lt;span style="color:#fab387"&gt;0&lt;/span&gt;]&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// tab id is required to send message to the content script
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;toggleMainDialog&amp;#34;&lt;/span&gt;, &lt;span style="color:#fab387"&gt;undefined&lt;/span&gt;, tabs[&lt;span style="color:#fab387"&gt;0&lt;/span&gt;].id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;On receiving the message in the Command Palette component, simply update the state to toggle the dialog:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/CommandPalette.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { onMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; CommandPalette() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...existing code
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; React.useEffect(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// onMessage returns a function to remove the listener, which we can call in the cleanup function
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; removeListener &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;toggleMainDialog&amp;#34;&lt;/span&gt;, () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setOpen((open) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#89dceb;font-weight:bold"&gt;!&lt;/span&gt;open);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; removeListener();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, []);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// We can remove the keyboard event listener
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// React.useEffect(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// const down = (e: KeyboardEvent) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// if (e.key === &amp;#34;j&amp;#34; &amp;amp;&amp;amp; (e.metaKey || e.ctrlKey)) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// e.preventDefault();
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// setOpen((open) =&amp;gt; !open);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// };
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// document.addEventListener(&amp;#34;keydown&amp;#34;, down);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// return () =&amp;gt; document.removeEventListener(&amp;#34;keydown&amp;#34;, down);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// }, []);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//.. rest of the code as is
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Our default shortcut is now &lt;code&gt;Alt+J&lt;/code&gt;, which user can change from the browser&amp;rsquo;s extension settings.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img src="shortcut-chrome.png" title="Configurable Shortcut in Chrome" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;Configurable Shortcut in Chrome&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src="shortcut-firefox.png" title="Configurable Shortcut in Firefox" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;Configurable Shortcut in Firefox&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;span class="hx:absolute hx:-mt-20" id="conclusion"&gt;&lt;/span&gt;
&lt;a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Through this series, we have covered the fundamentals of building modern web extensions using WXT. To recall, we have covered the following topics:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fundamentals of Web Extensions, the current state of extension development, and the need for a modern approach.&lt;/li&gt;
&lt;li&gt;Setting up a new project with WXT, Tailwind CSS, and Shadcn.&lt;/li&gt;
&lt;li&gt;Working with Content Scripts, building isolated UIs, and fixing some common issues with UI.&lt;/li&gt;
&lt;li&gt;Background Scripts, built in messaging APIs, and using an external wrapper for messaging.&lt;/li&gt;
&lt;li&gt;Using Storage API, Permissions / Runtime Permissions, and Commands.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This builds a strong foundation for building more complex extensions. We have covered a lot of ground, but there is still a lot more to explore:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://wxt.dev/guide/essentials/entrypoints.html"target="_blank" rel="noopener"&gt;Other Entrypoints&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;: Though we have used only the content script and Popup, there are other entrypoints like Sidebar, Devtools, and Options page which you can use in your extension.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wxt.dev/guide/essentials/publishing.html"target="_blank" rel="noopener"&gt;Publishing&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;: Different stores have different processes for publishing extensions. After initial publishing, WXT provides a way to automate the process of updating the extension. To use the extensions in release build of browser, you need a signed version which can be obtained from respective stores. You can also automate publishing using Github Actions.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wxt.dev/guide/essentials/unit-testing.html"target="_blank" rel="noopener"&gt;Testing&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;: WXT has support for Vitest for unit testing and suggests using Playwright for end-to-end testing.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wxt.dev/guide/essentials/i18n.html"target="_blank" rel="noopener"&gt;Internationalization&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;: WXT has a package &lt;a href="https://wxt.dev/guide/essentials/i18n.html"target="_blank" rel="noopener"&gt;@wxt-dev/i18n&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for internationalization, which can be used to localize the extension based on user&amp;rsquo;s preferred language.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, keep an eye on this space for future posts. If you have any questions or suggestions, feel free to reach out at &lt;a href="mailto:reply@aabidk.dev"&gt;reply@aabidk.dev&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Building Modern Cross Browser Web Extensions: Background Scripts and Messaging (Part 4)</title><link>https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-background-scripts-and-messaging/</link><pubDate>Wed, 29 Jan 2025 16:30:00 +0530</pubDate><guid>https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-background-scripts-and-messaging/</guid><description>
&lt;p&gt;In the &lt;a href="https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-content-scripts-and-ui/"&gt;previous post&lt;/a&gt;, we explored how to work with content scripts and build isolated UIs for our extension. In this post, we will learn how to work with background scripts as well as about the communication process between the content scripts and the background scripts.&lt;/p&gt;
&lt;h3&gt;Background Scripts&lt;span class="hx:absolute hx:-mt-20" id="background-scripts"&gt;&lt;/span&gt;
&lt;a href="#background-scripts" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Background scripts enable us to listen to browser events (such as opening/closing tabs, bookmarking a page, etc.) and use sensitive browser APIs (which cannot be accessed from content scripts directly) as long as the user has granted the necessary permissions. The term &amp;ldquo;background script&amp;rdquo; is used with Manifest V2, and with Manifest V3, the term &amp;ldquo;service worker&amp;rdquo; is used. In our project, we have a &lt;code&gt;src/entrypoints/background.ts&lt;/code&gt; file which is our background script. WXT automatically handles the registration of the background script in the manifest file as per Manifest V2 or Manifest V3. More information about background scripts can be found in &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts"target="_blank" rel="noopener"&gt;MDN Web Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; and &lt;a href="https://developer.chrome.com/docs/extensions/mv2/background-pages"target="_blank" rel="noopener"&gt;Chrome Extension Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Listening to Browser Events&lt;span class="hx:absolute hx:-mt-20" id="listening-to-browser-events"&gt;&lt;/span&gt;
&lt;a href="#listening-to-browser-events" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Recall from previous posts that we have a &lt;code&gt;browser&lt;/code&gt; global available via the auto imports that WXT provides, and that we can use the same for both Chrome and Firefox. Here is how we can listen to the &lt;code&gt;onInstalled&lt;/code&gt; event and perform some action when the extension is installed:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.runtime.onInstalled.addListener(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.tabs.create({ url&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;https://www.google.com&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;onInstalled&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This will open a new tab with google.com when the extension is installed. The log message will be visible in the service worker&amp;rsquo;s console. Similar other events such as &lt;code&gt;onStartup&lt;/code&gt;, &lt;code&gt;onInstalled&lt;/code&gt;, &lt;code&gt;onMessage&lt;/code&gt;, etc are available in &lt;code&gt;browser.runtime&lt;/code&gt;.
There is an extensive list of events provided by various APIs such as &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs#events"target="_blank" rel="noopener"&gt;tabs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/bookmarks#events"target="_blank" rel="noopener"&gt;bookmarks&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; etc. which can be used in background scripts.&lt;/p&gt;
&lt;h3&gt;Communication between Content Scripts and Background Scripts&lt;span class="hx:absolute hx:-mt-20" id="communication-between-content-scripts-and-background-scripts"&gt;&lt;/span&gt;
&lt;a href="#communication-between-content-scripts-and-background-scripts" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Communication is one of the most crucial and complex component of web extensions. Errors in communication logic are easy to introduce and difficult to debug. Hence, having a good understanding of communication fundamentals is essential.&lt;/p&gt;
&lt;p&gt;To communicate between content scripts and background scripts, we can use the messaging APIs provided by the browser. Using these APIs, we can send one-time messages, or use long-lived connections for continuous communication. Bidirectional communication is possible, so the background script can send messages to the content script and vice versa. Let&amp;rsquo;s try to send a message from the content script to the background script and log the message in the background script:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/test.content.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineContentScript({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; matches&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// The main function of the content script can be async
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; main(ctx) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; res &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; browser.runtime.sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;testMessage&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(res);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;and in the background script:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// The listener function passed to addListener CANNOT be async
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; browser.runtime.onMessage.addListener((message, sender, sendResponse) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (message &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;testMessage&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;got message from test.content.ts&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;testResponse&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Few important points to note here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;browser.runtime.sendMessage&lt;/code&gt; function is used to send a message from the content script to the background script. When sending from the background script to the content script, we need to use &lt;code&gt;browser.tabs.sendMessage&lt;/code&gt;, which also requires the &lt;code&gt;tabId&lt;/code&gt; of the target tab.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;browser.runtime.onMessage.addListener&lt;/code&gt; function is used to listen to messages in the background script, same can be used in content scripts as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Although we haven&amp;rsquo;t used the &lt;code&gt;sender&lt;/code&gt; parameter in the listener function, it can be used to get information about the tab that sent the message. Note that the &lt;code&gt;browser.runtime.onMessage&lt;/code&gt; event only receives messages from our own extension or its content scripts. By default, other extensions can send messages to our extension, but web pages cannot. To receive messages from web pages, we need to explicitly allow external sources in our manifest file by specifying the &lt;code&gt;externally_connectable&lt;/code&gt; property. In such cases, we can use the &lt;code&gt;browser.runtime.onMessageExternal&lt;/code&gt; event to receive messages from external sources, and the sender parameter will contain the URL of the page or the ID of the extension sending the message, which can be used to verify the authenticity of the message.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;sendResponse&lt;/code&gt; function here is used to send a &lt;strong&gt;synchronous&lt;/strong&gt; response back to the content script. To send an &lt;strong&gt;asynchronous&lt;/strong&gt; response with &lt;code&gt;sendResponse&lt;/code&gt;, we &lt;strong&gt;must&lt;/strong&gt; return a &lt;strong&gt;true&lt;/strong&gt; from the listener function. We cannot use an asynchronous listener function with &lt;code&gt;addListener&lt;/code&gt;. For example:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/enrtypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;browser.runtime.onMessage.addListener((message, sender, sendResponse) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (message &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;testMessage&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;got message from test.content.ts, sending async response&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setTimeout(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;testResponse async&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#fab387"&gt;1000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This way the message channel will be kept open until the response is sent back to the content script using &lt;code&gt;sendResponse&lt;/code&gt;. &lt;strong&gt;If we have multiple listeners for &lt;em&gt;same&lt;/em&gt; event, only the first one to call &lt;code&gt;sendResponse&lt;/code&gt; will send the response, and rest will be ignored.&lt;/strong&gt; Consider the following code:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;browser.runtime.onMessage.addListener((message, sender, sendResponse) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (message &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;testMessage&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;got message from test.content.ts, sending async response&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setTimeout(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;testResponse async&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#fab387"&gt;1000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;browser.runtime.onMessage.addListener((message, sender, sendResponse) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (message &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;testMessage&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;A second listener for the same message&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setTimeout(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;This will be sent first, the other listener will be ignored&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#fab387"&gt;500&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In the above example, the response from the second listener will be sent first, and the response from the first listener will be ignored as both are listening for the same message.&lt;/p&gt;
&lt;p&gt;For listening to multiple messages, a cleaner way would be to use a switch case:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;browser.runtime.onMessage.addListener((message, sender, sendResponse) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;switch&lt;/span&gt; (message) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;message_1&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;got message_1&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setTimeout(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;response_1&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#fab387"&gt;1000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;case&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;message_2&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;got message_2&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setTimeout(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;response_2&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#fab387"&gt;1000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//...more cases
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// on any unknown message
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;got unknown message:&amp;#34;&lt;/span&gt;, message);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//No async response here, so no need to return true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; sendResponse(&lt;span style="color:#a6e3a1"&gt;&amp;#34;unknown message&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You can test this by sending different messages from the content script and checking the console logs in both the consoles.&lt;/p&gt;
&lt;h3&gt;Using sensitive browser APIs in Background Scripts&lt;span class="hx:absolute hx:-mt-20" id="using-sensitive-browser-apis-in-background-scripts"&gt;&lt;/span&gt;
&lt;a href="#using-sensitive-browser-apis-in-background-scripts" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Let&amp;rsquo;s say we want to access the user&amp;rsquo;s browsing history in our extension. This is a sensitive API that requires the &lt;code&gt;history&lt;/code&gt; permission in the manifest file. We cannot access the &lt;code&gt;history&lt;/code&gt; API directly in our content scripts, so we will use messaging as discussed previously. We can add the &lt;code&gt;history&lt;/code&gt; permission to the manifest section in our &lt;code&gt;wxt.config.ts&lt;/code&gt; file as:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineConfig } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;wxt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// See https://wxt.dev/api/config.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; manifest&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command Palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;A command palette to quickly perform actions&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; permissions&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;history&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Let&amp;rsquo;s send a message from the content script to the background script to get the history and log it on response:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/enrtypoints/test.content.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// The HistoryItem type from chrome namespace should work for both Chrome and Firefox
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; HistoryItem &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; chrome.history.HistoryItem;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineContentScript({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; matches&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; main(ctx) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; history: &lt;span style="color:#f38ba8"&gt;HistoryItem&lt;/span&gt;[] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; browser.runtime.sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getHistory&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;history&amp;#34;&lt;/span&gt;, history);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;and in the background script:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/enrtypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.runtime.onMessage.addListener((message, sender, sendResponse) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;if&lt;/span&gt; (message &lt;span style="color:#89dceb;font-weight:bold"&gt;===&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;getHistory&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getting history&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser.history.search({ text&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;#34;&lt;/span&gt; }).then((history) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendResponse(history);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; &lt;span style="color:#fab387"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Our response should look something like this in the browser console:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;lastVisitTime&amp;#34;&lt;/span&gt;: &lt;span style="color:#fab387"&gt;1735732800000.0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;DuckDuckGo - Your protection, our priority.&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;typedCount&amp;#34;&lt;/span&gt;: &lt;span style="color:#fab387"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;https://duckduckgo.com/&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;visitCount&amp;#34;&lt;/span&gt;: &lt;span style="color:#fab387"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We have used the &lt;code&gt;HistoryItem&lt;/code&gt; type, allowing us to easily access the available attributes of the history item. The extensive list of browser APIs, their attributes and their events is available on MDN Web Docs and Chrome Developer Docs.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-amber-200 hx:bg-amber-100 hx:text-amber-900 hx:dark:border-amber-200/30 hx:dark:bg-amber-900/30 hx:dark:text-amber-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/&gt;&lt;/svg&gt;Warning&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;Although &lt;em&gt;most&lt;/em&gt; of the browser APIs are similar between the browsers, there are some minor differences, which you may encounter when developing browser specific features. For example, &lt;code&gt;browser.tabs.onActivated&lt;/code&gt; event provides an &lt;code&gt;activeInfo&lt;/code&gt; object, which has &lt;code&gt;previousTabId&lt;/code&gt;, &lt;code&gt;tabId&lt;/code&gt; and &lt;code&gt;windowId&lt;/code&gt; properties in Firefox&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, but only &lt;code&gt;tabId&lt;/code&gt; and &lt;code&gt;windowId&lt;/code&gt; in Chrome&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. As we are using &lt;code&gt;@types/chrome&lt;/code&gt; for types, Typescript will complain that &lt;code&gt;previousTabId&lt;/code&gt; is not available if we try to use it. We can add a check for the browser and a simple &lt;code&gt;//@ts-ignore&lt;/code&gt; above the line to ignore the error, when using such browser specific features. Another way is to use the &lt;a href="https://wxt.dev/guide/essentials/extension-apis.html#webextension-polyfill"target="_blank" rel="noopener"&gt;Webextension Polyfill&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; (which is by default disabled as of now), but it might further add complexity in managing types in the project. In most cases, you should try to use the widely available APIs only unless absolutely necessary.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;A better way of messaging&lt;span class="hx:absolute hx:-mt-20" id="a-better-way-of-messaging"&gt;&lt;/span&gt;
&lt;a href="#a-better-way-of-messaging" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The above approach of messaging using the browser APIs is quite error-prone and can get difficult to manage as the extension grows. We have very minimally used types in it, and the complexity of asynchronous operations only adds to the complication. WXT recommends using a wrapper around the built-in messaging APIs for these reasons. We will be using the &lt;a href="https://www.npmjs.com/package/@webext-core/messaging"target="_blank" rel="noopener"&gt;@webext-core/messaging&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; to simplify our messaging implementation and have a more robust and maintainable design.&lt;/p&gt;
&lt;p&gt;First, we need to install the package:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm add @webext-core/messaging&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We will keep our types in a separate file &lt;code&gt;src/lib/types.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/types.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;type&lt;/span&gt; HistoryItem &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; chrome.history.HistoryItem;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Our messaging setup will be in &lt;code&gt;src/lib/messaging.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { HistoryItem } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineExtensionMessaging } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@webext-core/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getHistory(data&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; { size: &lt;span style="color:#f38ba8"&gt;number&lt;/span&gt; })&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;HistoryItem&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;[]&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//or
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//getHistory(data: { size: number }): HistoryItem[];
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;// Both are same, as all messages are async. We don&amp;#39;t explicitly need to return a Promise, but it&amp;#39;s good for clarity.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; { sendMessage, onMessage } &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; defineExtensionMessaging&amp;lt;&lt;span style="color:#cba6f7"&gt;ProtocolMap&lt;/span&gt;&amp;gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We have defined a &lt;code&gt;ProtocolMap&lt;/code&gt; interface which will contain all the messages that we want to pass between the content script and the background script. We have defined a &lt;code&gt;getHistory&lt;/code&gt; message which will take a &lt;code&gt;size&lt;/code&gt; parameter and return an array of &lt;code&gt;HistoryItem&lt;/code&gt;. We have also defined the &lt;code&gt;sendMessage&lt;/code&gt; and &lt;code&gt;onMessage&lt;/code&gt; functions which will be used for message passing instead of the built-in browser APIs.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/&gt;&lt;/svg&gt;Note&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;We can skip the &lt;code&gt;data&lt;/code&gt; key if we have only one parameter in the message.&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getHistory(size: &lt;span style="color:#f38ba8"&gt;number&lt;/span&gt;)&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; HistoryItem[];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Protocol map functions expect only one parameter &lt;code&gt;data&lt;/code&gt;. If we have multiple parameters, we can pass them as an object with multiple keys:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/messaging.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;interface&lt;/span&gt; ProtocolMap {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getHistory(data&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; { size: &lt;span style="color:#f38ba8"&gt;number&lt;/span&gt;, query: &lt;span style="color:#f38ba8"&gt;string&lt;/span&gt; })&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; HistoryItem[];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Even though we have single parameter &lt;code&gt;size&lt;/code&gt; we are using &lt;code&gt;data&lt;/code&gt; for consistency. More information can be found on webext-core&amp;rsquo;s &lt;a href="https://webext-core.aklinker1.io/messaging/protocol-maps"target="_blank" rel="noopener"&gt;documentation&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Now in our background script, we can use the &lt;code&gt;onMessage&lt;/code&gt; function to listen to messages:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { onMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;//Notice that the function passed to defineBackground is still not async
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// The message handler function can be async
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; onMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getHistory&amp;#34;&lt;/span&gt;, &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; (message) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; history &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; browser.history.search({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; maxResults: &lt;span style="color:#f38ba8"&gt;message.data.size&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; history;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We do not need to use &lt;code&gt;sendResponse&lt;/code&gt; or return &lt;code&gt;true&lt;/code&gt; here. The &lt;code&gt;onMessage&lt;/code&gt; function will automatically handle the response. The message handler function can be async, and we can use &lt;code&gt;await&lt;/code&gt; to wait for the response from the browser APIs if we need to modify it before returning, or just directly return the Promise. We also have additional attributes such as &lt;code&gt;sender&lt;/code&gt; and &lt;code&gt;timestamp&lt;/code&gt; available on the &lt;code&gt;message&lt;/code&gt; parameter.&lt;/p&gt;
&lt;p&gt;In the content script, we can use the &lt;code&gt;sendMessage&lt;/code&gt; function to send messages:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/test.content.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineContentScript({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; matches&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; main(ctx) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; history &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getHistory&amp;#34;&lt;/span&gt;, { size: &lt;span style="color:#f38ba8"&gt;4&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(history);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Instead of passing the &lt;code&gt;size&lt;/code&gt; parameter directly, note that we are passing it as an object (the data object) with a key &lt;code&gt;size&lt;/code&gt;. You should now be able to see the history logged in the browser console.&lt;/p&gt;
&lt;p&gt;We now have a more robust and maintainable messaging system in place. We can easily add more messages to the &lt;code&gt;ProtocolMap&lt;/code&gt; interface and use them in our content and background scripts. Using this approach allows to catch errors early in development, as it checks for literal message names (e.g. &lt;code&gt;getHistory&lt;/code&gt;), making it easier to identify typing mistakes in message passing – for example, even &lt;code&gt;gethistory&lt;/code&gt; would not be recognized as valid message in this case.
We can further organize the code by creating helper functions in &lt;code&gt;src/lib/helpers.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/lib/helpers.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { HistoryItem } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/types&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { sendMessage } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/messaging&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; fetchHistory(size: &lt;span style="color:#f38ba8"&gt;number&lt;/span&gt;)&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; Promise&amp;lt;&lt;span style="color:#cba6f7"&gt;HistoryItem&lt;/span&gt;&lt;span style="color:#f38ba8"&gt;[]&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; history &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getHistory&amp;#34;&lt;/span&gt;, { size: &lt;span style="color:#f38ba8"&gt;size&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; history;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#cba6f7"&gt;catch&lt;/span&gt; (error) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.error(&lt;span style="color:#a6e3a1"&gt;&amp;#34;Error getting history&amp;#34;&lt;/span&gt;, error);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; Promise.resolve([]);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;and then use the helper functions in our content script:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/test.content.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { fetchHistory } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/lib/helpers&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineContentScript({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; matches&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; main(ctx) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; history &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; fetchHistory(&lt;span style="color:#fab387"&gt;4&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;history&amp;#34;&lt;/span&gt;, history);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In summary, our process for messaging will be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define the appropriate types in &lt;code&gt;src/lib/types.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Define the messages in &lt;code&gt;src/lib/messaging.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add the message handlers in the background script using &lt;code&gt;onMessage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;sendMessage&lt;/code&gt; function in the content script to send messages / use helper functions for more complex operations.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We can also send messages from the background script to the content script using the same messaging system, the only difference is that we have to pass the &lt;code&gt;tabId&lt;/code&gt; as the third argument:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sendMessage(&lt;span style="color:#a6e3a1"&gt;&amp;#34;getHistory&amp;#34;&lt;/span&gt;, { size: &lt;span style="color:#f38ba8"&gt;4&lt;/span&gt; }, tabId);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This will send the message to the content script of the tab with the given &lt;code&gt;tabId&lt;/code&gt;. We would also need to use &lt;code&gt;onMessage&lt;/code&gt; in the content script to listen to messages from the background script.&lt;/p&gt;
&lt;h3&gt;Using Proxy Service: an alternative to messaging&lt;span class="hx:absolute hx:-mt-20" id="using-proxy-service-an-alternative-to-messaging"&gt;&lt;/span&gt;
&lt;a href="#using-proxy-service-an-alternative-to-messaging" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;If majority of your extension&amp;rsquo;s logic needs to be run in the background script, you can look into &lt;a href="https://webext-core.aklinker1.io/proxy-service/installation"target="_blank" rel="noopener"&gt;@webext-core/proxy-service&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;, which allows you to register a service in background script and call its methods directly from other contexts without any message passing. We will continue to use messaging in our extension for now, but you can explore this option if it fits your use case better.&lt;/p&gt;
&lt;p&gt;In the next post, we will use the discussed concepts to build the core functionality of our extension, as well as explore storage and permissions in web extensions.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onActivated#activeinfo"target="_blank" rel="noopener"&gt;https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onActivated#activeinfo&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://developer.chrome.com/docs/extensions/reference/api/tabs#parameters_29"target="_blank" rel="noopener"&gt;https://developer.chrome.com/docs/extensions/reference/api/tabs#parameters_29&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Building Modern Cross Browser Web Extensions: Content Scripts and UI (Part 3)</title><link>https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-content-scripts-and-ui/</link><pubDate>Fri, 24 Jan 2025 10:48:00 +0530</pubDate><guid>https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-content-scripts-and-ui/</guid><description>
&lt;p&gt;In the &lt;a href="https://aabidk.dev/blog/building-modern-cross-web-extensions-project-setup/"&gt;previous post&lt;/a&gt;, we installed WXT, TailwindCSS and Shadcn and explored the project structure. In this post, we will explore how to work with content scripts and build isolated UIs for our extension as well as discuss some issues with UI and how to resolve them. We will build a Command Palette for quick browser actions, such as creating new tabs/windows, muting/unmuting tabs, and more over the next few posts.&lt;/p&gt;
&lt;h3&gt;Content Scripts&lt;span class="hx:absolute hx:-mt-20" id="content-scripts"&gt;&lt;/span&gt;
&lt;a href="#content-scripts" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Content scripts are JavaScript files that run in the context of web pages. They can read and modify the DOM of web pages the browser visits but have limited access to browser APIs. We need to use message passing to communicate and get information through background script, which has access to more browser APIs.&lt;/p&gt;
&lt;h3&gt;Injecting Content Scripts&lt;span class="hx:absolute hx:-mt-20" id="injecting-content-scripts"&gt;&lt;/span&gt;
&lt;a href="#injecting-content-scripts" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;In the previous post, we saw that our project has a content script at &lt;code&gt;src/entrypoints/content.ts&lt;/code&gt;. The WXT documentation provides information about how we can organize our content scripts in the &lt;a href="https://wxt.dev/guide/essentials/entrypoints#content-scripts"target="_blank" rel="noopener"&gt;entrypoints section&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. We will rename our content script to &lt;code&gt;main.content.tsx&lt;/code&gt;. The documentation has &lt;a href="https://wxt.dev/guide/essentials/content-scripts.html#ui"target="_blank" rel="noopener"&gt;3 methods&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for injecting UIs into the pages with well explained differences. We&amp;rsquo;ll go with the Shadow Root method as it is pretty straightforward and provides isolation for our CSS styling.&lt;/p&gt;
&lt;p&gt;First, let&amp;rsquo;s add the &lt;a href="https://ui.shadcn.com/docs/components/command"target="_blank" rel="noopener"&gt;Shadcn Command component&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; to our project:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm dlx shadcn@latest add command&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In the &lt;code&gt;src/components/&lt;/code&gt; create a new file &lt;code&gt;CommandPalette.tsx&lt;/code&gt; and add the &lt;a href="https://ui.shadcn.com/docs/components/command#dialog"target="_blank" rel="noopener"&gt;example code&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; from the Shadcn website for using Command with Dialog. Rename the default export to &lt;code&gt;CommandPalette&lt;/code&gt; from &lt;code&gt;CommandDialogDemo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In our &lt;code&gt;main.content.tsx&lt;/code&gt;, we will remove the existing code and add the following:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/main.content.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; ReactDOM &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react-dom/client&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { CommandPalette } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/CommandPalette&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/index.css&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineContentScript({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; matches&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cssInjectionMode&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;ui&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; main(ctx) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; ui &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; createShadowRootUi(ctx, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;command-palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; position&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;inline&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; anchor&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onMount&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; (container) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; app &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#89dceb"&gt;document&lt;/span&gt;.createElement(&lt;span style="color:#a6e3a1"&gt;&amp;#34;div&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.id &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;command-palette-root&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container.append(app);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; root &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; ReactDOM.createRoot(app);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; root.render(&amp;lt;&lt;span style="color:#cba6f7"&gt;CommandPalette&lt;/span&gt; /&amp;gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; root;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; onRemove&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; (root) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; root&lt;span style="color:#89dceb;font-weight:bold"&gt;?&lt;/span&gt;.unmount();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ui.mount();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Few things to note here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ctx&lt;/code&gt;, the parameter to the &lt;code&gt;main&lt;/code&gt; function, provides information about the context. When an extension is disabled or uninstalled, the content script is not removed. In such cases, the &amp;ldquo;context&amp;rdquo; of our content script becomes invalid. We can use &lt;code&gt;ctx&lt;/code&gt; to monitor if the context is still valid, for example to remove event listeners. The UI is removed automatically when the context becomes invalid, so we don&amp;rsquo;t need to use it explicitly here.&lt;/li&gt;
&lt;li&gt;We are using &lt;code&gt;createShadowRootUi&lt;/code&gt; to create a shadow root for our UI.(This code is same from the docs). In the DOM, our shadow root will be in a &lt;code&gt;command-palette&lt;/code&gt; element (the name we passed to &lt;code&gt;createShadowRootUi&lt;/code&gt;) like:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#cba6f7"&gt;command-palette&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;data-wxt-shadow-root&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;data-aria-hidden&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#89b4fa"&gt;aria-hidden&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; #shadow-root (open)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;head&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#cba6f7"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;id&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;command-palette-root&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#cba6f7"&gt;command-palette&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;main&lt;/code&gt; function in content scripts can be async, but not in background scripts. We are using async here.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Since we are using &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; in the &lt;code&gt;matches&lt;/code&gt; array, our content script will run on all pages. We can restrict it to specific pages by using a URL pattern.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Importing &lt;code&gt;@/index.css&lt;/code&gt; in the content script will inject the CSS into the shadow DOM, allowing our Shadcn components to use the styles.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We have used distinct names for the shadow root and the root element of our UI, so that they are easily identifiable in the DOM. You can inspect the DOM to see the shadow root and the root element structured as shown above.&lt;/p&gt;
&lt;p&gt;There are some issues with the dialog that we need to resolve though.&lt;/p&gt;
&lt;h3&gt;Resolving Issues With The UI&lt;span class="hx:absolute hx:-mt-20" id="resolving-issues-with-the-ui"&gt;&lt;/span&gt;
&lt;a href="#resolving-issues-with-the-ui" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;If you go to any webpage and inspect the elements, you will see the above element in the DOM. However, if we try to trigger the dialog, it will look broken like this:
&lt;figure&gt;
&lt;img src="broken-dialog.png" title="The broken command dialog at the bottom of the page" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;The broken command dialog at the bottom of the page&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The issue here is that by default on keypress, the dialog div is added to the main DOM and not the shadow DOM. Shadcn is built on RadixUI, hence we can fix this by using &lt;a href="https://www.radix-ui.com/primitives/docs/utilities/portal"target="_blank" rel="noopener"&gt;RadixUI portals&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Using RadixUI Portals To Move The Dialog To Shadow DOM&lt;span class="hx:absolute hx:-mt-20" id="using-radixui-portals-to-move-the-dialog-to-shadow-dom"&gt;&lt;/span&gt;
&lt;a href="#using-radixui-portals-to-move-the-dialog-to-shadow-dom" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;In our &lt;code&gt;main.content.tsx&lt;/code&gt;, create a portal context and a content root component:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/main.content.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; React &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; PortalContext &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; React.createContext&amp;lt;&lt;span style="color:#cba6f7"&gt;HTMLElement&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;|&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;null&lt;/span&gt;&amp;gt;(&lt;span style="color:#fab387"&gt;null&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; ContentRoot &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; () &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [portalContainer, setPortalContainer] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; useState&amp;lt;&lt;span style="color:#cba6f7"&gt;HTMLElement&lt;/span&gt; &lt;span style="color:#f38ba8"&gt;|&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;null&lt;/span&gt;&amp;gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#fab387"&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;React.StrictMode&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;PortalContext.Provider&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;value&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{portalContainer}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;ref&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{setPortalContainer} &lt;span style="color:#89b4fa"&gt;id&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;command-portal-container&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;CommandPalette&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;PortalContext.Provider&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;React.StrictMode&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;In the same file, update the &lt;code&gt;main&lt;/code&gt; function to use the &lt;code&gt;ContentRoot&lt;/code&gt; instead of &lt;code&gt;CommandPalette&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/main.content.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root.render(&amp;lt;&lt;span style="color:#cba6f7"&gt;ContentRoot&lt;/span&gt; /&amp;gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;
&lt;p&gt;Ensure that you have &lt;code&gt;@/index.css&lt;/code&gt; imported in your &lt;code&gt;src/entrypoints/main.content.tsx&lt;/code&gt; file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the &lt;code&gt;src/components/ui/dialog.tsx&lt;/code&gt; import the &lt;code&gt;PortalContext&lt;/code&gt; and &lt;code&gt;useContext&lt;/code&gt;, and update the &lt;code&gt;DialogContent&lt;/code&gt; component to use our portal:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/components/ui/dialog.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { useContext } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { PortalContext } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/entrypoints/main.content.tsx&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;//... rest of the code as is
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; DialogContent &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; React.forwardRef&lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;lt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; React.ElementRef&amp;lt;&lt;span style="color:#cba6f7"&gt;typeof&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;DialogPrimitive.Content&lt;/span&gt;&amp;gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; React.ComponentPropsWithoutRef&amp;lt;&lt;span style="color:#cba6f7"&gt;typeof&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;DialogPrimitive.Content&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;&amp;gt;&lt;/span&gt;(({ className, children, ...props }, ref) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;DialogPortal&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;container&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{useContext(PortalContext)}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;DialogOverlay&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;After this, the dialog should be rendered properly:
&lt;img src="dialog.gif" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;What we simply did here, is creating a &amp;lsquo;div&amp;rsquo; and using it as a portal where our dialog will be rendered, thus bringing it inside the Shadow DOM.&lt;/p&gt;
&lt;h4&gt;Isolating Events&lt;span class="hx:absolute hx:-mt-20" id="isolating-events"&gt;&lt;/span&gt;
&lt;a href="#isolating-events" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;By default, events in the Shadow DOM are not isolated from the main DOM. This means the events such as key presses and scroll can still affect the main page. To see this in action, try opening YouTube, playing a video, and then opening our Command Palette. If you press the &lt;code&gt;m&lt;/code&gt; key, the video will mute, showing that the event has bubbled up to the main DOM. Additionally, you&amp;rsquo;ll notice that scrolling doesn&amp;rsquo;t work as expected within the Command Palette. We can easily isolate events by updating our &lt;code&gt;createShadowRootUi&lt;/code&gt; function:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/main.content.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;async&lt;/span&gt; main(ctx) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; ui &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;await&lt;/span&gt; createShadowRootUi(ctx, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;command-palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; position&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;inline&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; anchor&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; isolateEvents&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;keydown&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;keyup&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;keypress&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;wheel&amp;#34;&lt;/span&gt;], &lt;span style="color:#6c7086;font-style:italic"&gt;// Add other events as needed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt; onMount&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; (container) &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6c7086;font-style:italic"&gt;//... rest of the code as is
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Once you add the &lt;code&gt;isolateEvents&lt;/code&gt; array, the events will be isolated to the shadow DOM, and both keypresses and scrolling will work as expected.&lt;/p&gt;
&lt;h4&gt;Configuring PostCSS To Convert &lt;code&gt;rem&lt;/code&gt; To &lt;code&gt;px&lt;/code&gt;&lt;span class="hx:absolute hx:-mt-20" id="configuring-postcss-to-convert-rem-to-px"&gt;&lt;/span&gt;
&lt;a href="#configuring-postcss-to-convert-rem-to-px" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;There is one other issue that is not be immediately visible, but you might encounter on some pages. In Shadcn and TailwindCSS, &lt;code&gt;rem&lt;/code&gt; units are used widely. &lt;code&gt;rem&lt;/code&gt; units are relative to root element&amp;rsquo;s font size, and in Shadow DOM, the rem unit is actually relative to the font size of the main document&amp;rsquo;s root element (i.e., the html element), not the shadow root, and this might affect our UI.&lt;/p&gt;
&lt;p&gt;The fix for this is rather simple: using other units like &lt;code&gt;px&lt;/code&gt;, such as with &lt;a href="https://github.com/TheDutchCoder/postcss-rem-to-px"target="_blank" rel="noopener"&gt;this package&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. We can do it as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the package:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# https://github.com/TheDutchCoder/postcss-rem-to-px&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm install --save-dev postcss @thedutchcoder/postcss-rem-to-px
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;#or&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm add -D postcss @thedutchcoder/postcss-rem-to-px&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Update our &lt;code&gt;postcss.config.js&lt;/code&gt; to use the plugin:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;postcss.config.js&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; plugins&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; autoprefixer&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@thedutchcoder/postcss-rem-to-px&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tailwindcss&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We can now safely use &lt;code&gt;rem&lt;/code&gt; units in our CSS, and they will be converted to &lt;code&gt;px&lt;/code&gt; units in the Shadow DOM automatically during build.&lt;/p&gt;
&lt;p&gt;Note that we only have the &lt;code&gt;main.content.tsx&lt;/code&gt; as a content script, and any UI component can be directly used by importing without marking them as content script separately (just as we don&amp;rsquo;t have &lt;code&gt;CommandPalette&lt;/code&gt; marked as content script explicitly anywhere).&lt;/p&gt;
&lt;h4&gt;Other Styling Fixes&lt;span class="hx:absolute hx:-mt-20" id="other-styling-fixes"&gt;&lt;/span&gt;
&lt;a href="#other-styling-fixes" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;You might also see some minor styling issues with the dialog such as z-index (again, noticeable on YouTube), which can easily fixed by adding custom CSS either in &lt;code&gt;src/index.css&lt;/code&gt; itself or using a separate file:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/index.css&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;role&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;dialog&amp;#34;&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;]&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;z-index&lt;/span&gt;: &lt;span style="color:#fab387"&gt;999999&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The ShadowDOM, event isolation and rem to px conversion are one time fixes, while you might need styling fixes for UI based on the components you use. We now have a properly isolated UI for our extension, and we can start adding more features to it.&lt;/p&gt;
&lt;p&gt;In the next post, we will explore background scripts, the communication process in extensions, and setting up a robust type-safe messaging system for our extension.&lt;/p&gt;</description></item><item><title>Building Modern Cross Browser Web Extensions: Project Setup (Part 2)</title><link>https://aabidk.dev/blog/building-modern-cross-web-extensions-project-setup/</link><pubDate>Mon, 20 Jan 2025 00:40:00 +0530</pubDate><guid>https://aabidk.dev/blog/building-modern-cross-web-extensions-project-setup/</guid><description>
&lt;p&gt;In the &lt;a href="https://aabidk.dev/blog/building-modern-cross-web-extensions-introduction/"&gt;previous post&lt;/a&gt;, we discussed the basics of Web Extensions and the tools we will be using to build our extension. Out of the three mentioned frameworks, we will be using &lt;a href="https://wxt.dev/"target="_blank" rel="noopener"&gt;WXT&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. In this post, we will set up the project and install the necessary dependencies and get an overview of the project structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edit 2025/02/03&lt;/strong&gt;: The source code for the extension we will be building in this series is available on &lt;a href="https://github.com/aabidk20/command-palette"target="_blank" rel="noopener"&gt;GitHub&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/&gt;&lt;/svg&gt;Note&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;The project uses &lt;code&gt;pnpm&lt;/code&gt; as the package manager. If you are using other package managers, adjust the commands accordingly.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Installing and setting up WXT&lt;span class="hx:absolute hx:-mt-20" id="installing-and-setting-up-wxt"&gt;&lt;/span&gt;
&lt;a href="#installing-and-setting-up-wxt" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The &lt;a href="https://wxt.dev/guide/installation.html"target="_blank" rel="noopener"&gt;documentation&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for WXT is pretty straightforward, and you should refer to it for more detailed information at any step. The commands below are directly taken from the documentation:
&lt;div class="hextra-scrollbar hx:overflow-x-auto hx:overflow-y-hidden hx:overscroll-x-contain"&gt;
&lt;div class="hx:mt-4 hx:flex hx:w-max hx:min-w-full hx:border-b hx:border-gray-200 hx:pb-px hx:dark:border-neutral-800"&gt;&lt;button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected"&gt;PNPM&lt;/button&gt;&lt;button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-1"&gt;Bun&lt;/button&gt;&lt;button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-2"&gt;NPM&lt;/button&gt;&lt;button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-3"&gt; Yarn&lt;/button&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-0"
role="tabpanel" tabindex="0" data-state="selected" &gt;&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm dlx wxt@latest init&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-1"
role="tabpanel"&gt;&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bunx wxt@latest init&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-2"
role="tabpanel"&gt;&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npx wxt@latest init&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-3"
role="tabpanel"&gt;&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;# Use NPM initially, but select Yarn when prompted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npx wxt@latest init&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Enter your project name when prompted. You will be asked to select a template, and a package manager. We will be choosing React and PNPM (or you can choose any other package manager) respectively. Once the command finishes, you will be asked to &lt;code&gt;cd&lt;/code&gt; into the project folder and install the packages.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-amber-200 hx:bg-amber-100 hx:text-amber-900 hx:dark:border-amber-200/30 hx:dark:bg-amber-900/30 hx:dark:text-amber-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/&gt;&lt;/svg&gt;Warning&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;WXT is in active development, and the commands/structure might change in the future. The version used in the guide is &lt;code&gt;0.19.23&lt;/code&gt; which you can pin in your &lt;code&gt;package.json&lt;/code&gt; file to follow along.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You are encouraged to go through the documentation once at this point to get a better understanding of the project structure and the commands available.&lt;/p&gt;
&lt;p&gt;We will &lt;a href="https://wxt.dev/guide/essentials/project-structure.html#adding-a-src-directory"target="_blank" rel="noopener"&gt;add a src directory&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for better project organization. Create a &lt;code&gt;src&lt;/code&gt; directory in your project&amp;rsquo;s root and move &lt;code&gt;assets&lt;/code&gt;, &lt;code&gt;entrypoints&lt;/code&gt; and &lt;code&gt;public&lt;/code&gt; folders inside it. In the &lt;code&gt;wxt.config.ts&lt;/code&gt; in root of your project, add the following:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Run &lt;code&gt;pnpm dev&lt;/code&gt; (or &lt;code&gt;pnpm dev:firefox&lt;/code&gt; for firefox) to ensure that the setup is successful. Your browser should open, and you will see the extension installed as below:
&lt;img src="initial.png" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;During development, it is recommended to test the extension on both Chrome and Firefox continuously to ensure that it works as expected on both the browsers, as well as to catch any potential browser-specific bugs early.&lt;/p&gt;
&lt;h3&gt;Installing TailwindCSS and Shadcn&lt;span class="hx:absolute hx:-mt-20" id="installing-tailwindcss-and-shadcn"&gt;&lt;/span&gt;
&lt;a href="#installing-tailwindcss-and-shadcn" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Although WXT uses Vite under the hood, the Shadcn theme instructions for Vite do not work directly with WXT. We will use the &lt;a href="https://ui.shadcn.com/docs/installation/manual"target="_blank" rel="noopener"&gt;Manual installation&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; steps with a few modifications.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The first few commands are for installing TailwindCSS and some other dependencies. Run the following commands:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm add -D tailwindcss postcss autoprefixer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm dlx tailwindcss init -p
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm add tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;In the &lt;code&gt;tsconfig.json&lt;/code&gt; file, add the following paths and baseUrl in the &lt;code&gt;compilerOptions&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;tsconfig.json&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;extends&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;./.wxt/tsconfig.json&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;compilerOptions&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;allowImportingTsExtensions&amp;#34;&lt;/span&gt;: &lt;span style="color:#fab387"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;jsx&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;react-jsx&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;baseUrl&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e3a1"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;paths&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;&amp;#34;@/*&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;./src/*&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-amber-200 hx:bg-amber-100 hx:text-amber-900 hx:dark:border-amber-200/30 hx:dark:bg-amber-900/30 hx:dark:text-amber-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/&gt;&lt;/svg&gt;Warning&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;WXT explicitly advises against adding paths directly to &lt;code&gt;tsconfig.json&lt;/code&gt;, and suggests using the &lt;code&gt;alias&lt;/code&gt; option in &lt;code&gt;wxt.config.ts&lt;/code&gt; &lt;a href="https://wxt.dev/guide/essentials/config/typescript.html#tsconfig-paths"target="_blank" rel="noopener"&gt;in their documentation&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. Doing so could cause issues in the future if WXT changes its configurations. However, Shadcn fails to resolve the paths correctly if we do not add them to &lt;code&gt;tsconfig.json&lt;/code&gt; directly. There is an &lt;a href="https://github.com/shadcn-ui/ui/issues/6020"target="_blank" rel="noopener"&gt;open issue&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; in Shadcn about the same. The manual addition is just a temporary workaround and you are advised to monitor the issue&amp;rsquo;s status and update your configuration accordingly in the future.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Copy the contents of the &lt;code&gt;tailwind.config.js&lt;/code&gt; file from the &lt;a href="https://ui.shadcn.com/docs/installation/manual#configure-tailwindconfigjs"target="_blank" rel="noopener"&gt;Shadcn documentation&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. Change the value of &lt;code&gt;content&lt;/code&gt; key as follows:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;content&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;./src/**/*.{html,js,ts,jsx,tsx}&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e3a1"&gt;&amp;#34;./index.html&amp;#34;&lt;/span&gt;],&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;
&lt;p&gt;Create a file &lt;code&gt;index.css&lt;/code&gt; in the &lt;code&gt;src/&lt;/code&gt; folder. Copy the style from the &lt;a href="https://ui.shadcn.com/docs/installation/manual#configure-styles"target="_blank" rel="noopener"&gt;Configure styles section&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; of the Shadcn documentation and paste them in the &lt;code&gt;index.css&lt;/code&gt; file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create &lt;code&gt;lib/&lt;/code&gt; folder in &lt;code&gt;src/&lt;/code&gt;. Inside &lt;code&gt;lib/&lt;/code&gt;, create a file utils.ts and add the &lt;a href="https://ui.shadcn.com/docs/installation/manual#add-a-cn-helper"target="_blank" rel="noopener"&gt;cn helper&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;code&gt;components.json&lt;/code&gt; file in the project&amp;rsquo;s root and add the &lt;a href="https://ui.shadcn.com/docs/installation/manual#create-a-componentsjson-file"target="_blank" rel="noopener"&gt;components.json&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try adding a button with the following command:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pnpm dlx shadcn@latest add button&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If everything is set up correctly, the button should be added in &lt;code&gt;src/components/ui/button.tsx&lt;/code&gt;.&lt;/p&gt;
&lt;ol start="8"&gt;
&lt;li&gt;Replace the contents of &lt;code&gt;src/entrypoints/popup/App.tsx&lt;/code&gt; with the following:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;src/entrypoints/popup/App.tsx&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { useState } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { Button } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;@/components/ui/button&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f38ba8"&gt;function&lt;/span&gt; App() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;const&lt;/span&gt; [count, setCount] &lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt; useState(&lt;span style="color:#fab387"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cba6f7"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;className&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6e3a1"&gt;&amp;#34;m-10&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#cba6f7"&gt;Button&lt;/span&gt; &lt;span style="color:#89b4fa"&gt;onClick&lt;/span&gt;&lt;span style="color:#89dceb;font-weight:bold"&gt;=&lt;/span&gt;{() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; setCount(count &lt;span style="color:#89dceb;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#fab387"&gt;1&lt;/span&gt;)}&amp;gt;Count&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {count}&amp;lt;/&lt;span style="color:#cba6f7"&gt;Button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#cba6f7"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; App;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;(The main goal is to remove the App.css import, but we also removed rest of the code and added a simple button to test the setup.)&lt;/p&gt;
&lt;ol start="9"&gt;
&lt;li&gt;
&lt;p&gt;In the &lt;code&gt;src/entrypoints/popup/main.tsx&lt;/code&gt; file, remove the &lt;code&gt;style.css&lt;/code&gt; import and add &lt;code&gt;@/index.css&lt;/code&gt; import.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;pnpm dev&lt;/code&gt; to start the server. You should see the Shadcn button in the popup window. We can now use TailwindCSS and Shadcn components (adding via CLI will also work).
&lt;img src="final_shadcn.png" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Also, we can delete the &lt;code&gt;src/entrypoints/popup/App.css&lt;/code&gt; file and the &lt;code&gt;src/entrypoints/popup/style.css&lt;/code&gt; file as they are not needed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Understanding The Project Structure&lt;span class="hx:absolute hx:-mt-20" id="understanding-the-project-structure"&gt;&lt;/span&gt;
&lt;a href="#understanding-the-project-structure" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Our directory structure should look something like this:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── components.json
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── node_modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── package.json
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── pnpm-lock.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── postcss.config.js
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── README.md
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── assets
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── react.svg
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── components
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── button.tsx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── ui
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── button.tsx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── entrypoints
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── background.ts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── content.ts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── popup
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── App.tsx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── index.html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── main.tsx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── index.css
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── lib
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── utils.ts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── public
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── icon
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── 128.png
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   :
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── wxt.svg
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── tailwind.config.js
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── tsconfig.json
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── wxt.config.ts&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;All our configuration files are in the project&amp;rsquo;s root, and all the source code in &lt;code&gt;src/&lt;/code&gt; directory. It is mostly similar to the one shown in the &lt;a href="https://wxt.dev/guide/essentials/project-structure.html#adding-a-src-directory"target="_blank" rel="noopener"&gt;WXT documentation&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;, with some differences due to adding Shadcn as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;components.json&lt;/code&gt; - Shadcn configuration file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;components/ui/&lt;/code&gt; - Shadcn components directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index.css&lt;/code&gt; - Our global CSS file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt; - TailwindCSS configuration file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postcss.config.js&lt;/code&gt; - PostCSS configuration file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main files we are interested are the ones in the &lt;code&gt;src/entrypoints/&lt;/code&gt; directory. These files are the entrypoints for our extension. We have three entrypoints initially:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;background.ts&lt;/code&gt; file is the entrypoint for the background script.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;content.ts&lt;/code&gt; file is the entrypoint for the content script.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;popup/&lt;/code&gt; directory contains the entrypoint for the popup.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;More information about these can be found &lt;a href="https://wxt.dev/guide/essentials/entrypoints.html#entrypoint-types"target="_blank" rel="noopener"&gt;here&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;. As of writing this guide, we can have only one background script in WXT, but multiple content scripts.&lt;/p&gt;
&lt;p&gt;On inspecting the contents of the &lt;code&gt;background.ts&lt;/code&gt; file, we can see the following code:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;background.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineBackground(() &lt;span style="color:#89dceb;font-weight:bold"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;Hello background!&amp;#34;&lt;/span&gt;, { id: &lt;span style="color:#f38ba8"&gt;browser.runtime.id&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;and in the &lt;code&gt;content.ts&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;content.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineContentScript({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; matches&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;*://*.google.com/*&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; main() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console.log(&lt;span style="color:#a6e3a1"&gt;&amp;#34;Hello content.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Notice that the default exports are wrapped in &lt;code&gt;defineBackground&lt;/code&gt; and &lt;code&gt;defineContentScript&lt;/code&gt; functions. These functions are provided by WXT and are used to define the entrypoints for the background and content scripts. The &lt;code&gt;matches&lt;/code&gt; property in the &lt;code&gt;defineContentScript&lt;/code&gt; function is used to specify the domains where the we want the content script to run (User is notified that we need access to these URLs). In the above example, the content script will run only on &lt;code&gt;*.google.com&lt;/code&gt; domains.&lt;/p&gt;
&lt;p&gt;The content script and background scripts use different browser consoles for logging. To see the above log statements, run &lt;code&gt;pnpm dev&lt;/code&gt; and wait for the browser to open. As the content script has match only for &lt;code&gt;*.google.com&lt;/code&gt; domain, the content script will only load when the domain matches. Navigate to &lt;code&gt;www.google.com&lt;/code&gt; and open the browser console by pressing &lt;code&gt;F12&lt;/code&gt;. You should see the &lt;code&gt;Hello content&lt;/code&gt; message in the console.&lt;/p&gt;
&lt;p&gt;To see the log from background script:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In chrome / chrome based browsers, navigate to &lt;code&gt;chrome://extensions/&lt;/code&gt; and click on the &lt;code&gt;service worker&lt;/code&gt; link for your extension.&lt;/li&gt;
&lt;li&gt;In firefox, navigate to &lt;code&gt;about:debugging#/runtime/this-firefox&lt;/code&gt; and click on the &lt;code&gt;Inspect&lt;/code&gt; link for your extension and go to Console.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This should open a console where you should see the &lt;code&gt;Hello background&lt;/code&gt; message along with the &lt;code&gt;browser.runtime.id&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Both these consoles are useful for debugging and logging messages. For our UI components, we can find logs in the browser console, while for background scripts they will be in the service worker / extension&amp;rsquo;s developer console.&lt;/p&gt;
&lt;h3&gt;Configurations&lt;span class="hx:absolute hx:-mt-20" id="configurations"&gt;&lt;/span&gt;
&lt;a href="#configurations" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;wxt.config.ts&lt;/code&gt; file is the main configuration file for WXT. It is used to configure the extension&amp;rsquo;s name, version, and other settings. The &lt;code&gt;wxt.config.ts&lt;/code&gt; file in our project looks like this:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;import&lt;/span&gt; { defineConfig } &lt;span style="color:#cba6f7"&gt;from&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;wxt&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;// See https://wxt.dev/api/config.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6c7086;font-style:italic"&gt;&lt;/span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In the first post, we discussed about the &lt;code&gt;manifest.json&lt;/code&gt; file, which contains the metadata for the extension. We have to list our content scripts with the URLs we want to run them on, as well as our background scripts in it for our extension to run. However, manually writing &lt;code&gt;manifest.json&lt;/code&gt; is complex, and along with that, the property names are different in MV2 and MV3. WXT abstracts away the manifest file and generates it based on our project structure. (Recall the &lt;code&gt;defineBackground&lt;/code&gt; and &lt;code&gt;defineContentScript&lt;/code&gt;?). Let&amp;rsquo;s add the name and description of our project in the manifest:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;wxt.config.ts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cba6f7"&gt;export&lt;/span&gt; &lt;span style="color:#cba6f7"&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; srcDir&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;src&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extensionApi&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;chrome&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modules&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e3a1"&gt;&amp;#34;@wxt-dev/module-react&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; manifest&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; name&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;Command Palette&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; description&lt;span style="color:#89dceb;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#a6e3a1"&gt;&amp;#34;A command palette to quickly perform actions&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#45475a"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Run the &lt;code&gt;pnpm dev&lt;/code&gt; command. The name and description of the extension should reflect in the browser. In the &lt;code&gt;.output/chrome-mv3/&lt;/code&gt; or &lt;code&gt;.output/firefox-mv2&lt;/code&gt; folder in your project&amp;rsquo;s root, you should see the &lt;code&gt;manifest.json&lt;/code&gt; generated by WXT.&lt;/p&gt;
&lt;div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200"&gt;
&lt;p class="hx:flex hx:items-center hx:font-medium"&gt;&lt;svg height=16px class="hx:inline-block hx:align-middle hx:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"&gt;&lt;path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/&gt;&lt;/svg&gt;Note&lt;/p&gt;
&lt;div class="hx:w-full hx:min-w-0 hx:leading-7"&gt;
&lt;div class="hx:mt-6 hx:leading-7 hx:first:mt-0"&gt;&lt;p&gt;WXT uses Manifest V3 for chrome and Manifest V2 for Firefox by default. In the &lt;code&gt;wxt.config.ts&lt;/code&gt;, we only have to add the manifest key names as per MV3, and WXT will automatically convert the keys based on MV2 or MV3.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Auto Imports And Extension APIs&lt;span class="hx:absolute hx:-mt-20" id="auto-imports-and-extension-apis"&gt;&lt;/span&gt;
&lt;a href="#auto-imports-and-extension-apis" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Notice that there is no import for &lt;code&gt;defineBackground&lt;/code&gt; in background script or &lt;code&gt;defineContentScript&lt;/code&gt; in the content script. By default WXT, auto imports its own APIs, as well as few source directories. The one we will be using most is the &lt;code&gt;browser&lt;/code&gt; global variable. It allows us to access the various browser APIs such as &lt;code&gt;browser.tabs&lt;/code&gt;, &lt;code&gt;browser.bookmarks&lt;/code&gt; and a lot more.&lt;/p&gt;
&lt;p&gt;Chrome use the &lt;code&gt;chrome&lt;/code&gt; global variable instead of &lt;code&gt;browser&lt;/code&gt;. &lt;a href="https://wxt.dev/guide/essentials/extension-apis.html"target="_blank" rel="noopener"&gt;WXT abstracts this away&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; and provides a consistent API across browsers. We only have to use the &lt;code&gt;browser&lt;/code&gt; global everywhere - however we have to check if the APIs are available, as WXT assumes all APIs exist for all browsers.&lt;/p&gt;
&lt;p&gt;For reference, a list of all available Firefox APIs can be found on &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API"target="_blank" rel="noopener"&gt;MDN Web Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; (along with compatibility details with different browsers) and for Chrome can be found on &lt;a href="https://developer.chrome.com/docs/extensions/reference/api"target="_blank" rel="noopener"&gt;Chrome Developer Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the next post, we will discuss about content scripts and UI for our extension.&lt;/p&gt;</description></item><item><title>Building Modern Cross Browser Web Extensions: Introduction (Part 1)</title><link>https://aabidk.dev/blog/building-modern-cross-web-extensions-introduction/</link><pubDate>Mon, 20 Jan 2025 00:20:00 +0530</pubDate><guid>https://aabidk.dev/blog/building-modern-cross-web-extensions-introduction/</guid><description>
&lt;p&gt;If you are already familiar with web extensions and want to jump directly to the implementation, you can skip this post and go to the &lt;a href="https://aabidk.dev/blog/building-modern-cross-web-extensions-project-setup/"&gt;next one&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Introduction&lt;span class="hx:absolute hx:-mt-20" id="introduction"&gt;&lt;/span&gt;
&lt;a href="#introduction" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Extensions have become an integral part of modern browsers. While web development has evolved vastly over the last few years, development of extensions has mostly remained the same. This multi-post series will explore the development of cross browser web extensions using modern tools and frameworks.&lt;/p&gt;
&lt;h3&gt;Why Build Web Extensions?&lt;span class="hx:absolute hx:-mt-20" id="why-build-web-extensions"&gt;&lt;/span&gt;
&lt;a href="#why-build-web-extensions" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Web extensions offer a unique blend of platform independence, a large user base, and feature rich APIs. They have become as significant as mobile apps in today&amp;rsquo;s world. Following are some of the reasons why you should consider building web extensions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Platform Independence: Web extensions are built using web technologies, which makes them platform independent. With modern tools, you can build an extension once and deploy it across multiple browsers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Large User Base: Browsers have a large user base, which means your extension can reach a large number of users. Chrome itself has over &lt;em&gt;3 billion&lt;/em&gt; active users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Feature Rich APIs: Browsers provide a rich set of APIs that can be used to build powerful extensions. You get a well-established and robust platform - no need to worry about the backend or the infrastructure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Persistent Availability: Extensions are installed by the user and stay with them (even across devices). This means you can build a personalized experience for the user. This is somewhat equivalent of an installed desktop app or a mobile app.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Browser Integration: Browser extensions can act as a window to your core software. Todoist and Grammarly are really good examples of this - users can easily access functionalities of the core software from the browser itself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Monetization: Extensions can be monetized in various ways — from ads to premium features, or as a paid addon to the core software.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Development Challenges&lt;span class="hx:absolute hx:-mt-20" id="development-challenges"&gt;&lt;/span&gt;
&lt;a href="#development-challenges" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Extensions have been around for a long time, but the development process has not changed much. It&amp;rsquo;s not a surprise that most of the material on the web for extension development focuses on building from scratch. This is not necessarily a bad thing, but it makes things difficult down the road. While it may work for simple extensions, building any sufficiently complex extensions can quickly become unmanageable without better tools and practices.&lt;/p&gt;
&lt;p&gt;The transition between manifest versions adds complexity. Chrome&amp;rsquo;s push toward Manifest V3 requires significant architectural changes - from replacing background scripts with service workers to adapting to stricter API limitations. With Firefox maintaining Manifest V2 support, developers must maintain compatibility across different specifications.&lt;/p&gt;
&lt;p&gt;Browser-specific implementations create additional hurdles. While most of the APIs are similar between the popular browsers, there might be some differences at some places, and each extension store has a different review process. An extension working perfectly in Chrome might need substantial modifications for Firefox, while Safari&amp;rsquo;s extension support is more limited. Cross browser support is often not considered from the initial phase - either making the transition difficult, or requiring multiple separate versions.&lt;/p&gt;
&lt;p&gt;Besides these, conveniences like hot reloading and automated publishing workflows are often missing from traditional extension development.&lt;/p&gt;
&lt;h3&gt;Moving Forward With Modern Frameworks&lt;span class="hx:absolute hx:-mt-20" id="moving-forward-with-modern-frameworks"&gt;&lt;/span&gt;
&lt;a href="#moving-forward-with-modern-frameworks" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Several modern frameworks are available for building extensions. These include &lt;a href="https://crxjs.dev/vite-plugin"target="_blank" rel="noopener"&gt;CRXJS&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;, which takes a vite-plugin approach, and supports React, Solid, Vue and Vanilla JavaScript. However, it requires more manual configuration than other available options, and provides just enough tooling to get started.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.plasmo.com/"target="_blank" rel="noopener"&gt;Plasmo&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; offers a more opinionated approach, with out of the box support for React, Typescript and other frameworks. It has a long list of features and is one of the good options to check out.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://wxt.dev/"target="_blank" rel="noopener"&gt;WXT&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;, which is a relatively newer framework, is quickly gaining popularity. WXT provides a good developer experience, with some wrappers around core browser APIs. We will be building our extension using WXT in this series. The comparison of these three can be found &lt;a href="https://wxt.dev/guide/resources/compare.html"target="_blank" rel="noopener"&gt;here&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While each framework offers its own advantages, WXT provides the best balance of features and flexibility for our needs. Its comprehensive tooling, framework-agnostic approach, and growing ecosystem make it an excellent choice for modern extension development. Let&amp;rsquo;s first understand some key terminology before diving into implementation.&lt;/p&gt;
&lt;h3&gt;Understanding The Terminology&lt;span class="hx:absolute hx:-mt-20" id="understanding-the-terminology"&gt;&lt;/span&gt;
&lt;a href="#understanding-the-terminology" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;There are a few terms that you should be familiar with before we dive into building extensions:&lt;/p&gt;
&lt;h4&gt;1. Manifest&lt;span class="hx:absolute hx:-mt-20" id="1-manifest"&gt;&lt;/span&gt;
&lt;a href="#1-manifest" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The manifest file contains the configuration and metadata of the extension. It includes the name, version, permissions, and other details of the extension. It is the only required file, and must have the name &lt;code&gt;manifest.json&lt;/code&gt;. MV2 (Manifest V2) and MV3 (Manifest V3) are successive versions of the browser extension manifest format, with MV3 being the latest iteration, introducing significant changes in security, permissions, and functionality.&lt;/p&gt;
&lt;h4&gt;2. Background Script / Service Worker&lt;span class="hx:absolute hx:-mt-20" id="2-background-script--service-worker"&gt;&lt;/span&gt;
&lt;a href="#2-background-script--service-worker" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;This is a script that runs in the background and can be used to listen to events, make network requests, and perform other tasks. Background scripts have access to sensitive browser APIs, although they do not have direct access to DOM. The term &amp;lsquo;Service workers&amp;rsquo; is used with Manifest V3, while &amp;lsquo;Background script&amp;rsquo; is used in Manifest V2.&lt;/p&gt;
&lt;h4&gt;3. Content Script&lt;span class="hx:absolute hx:-mt-20" id="3-content-script"&gt;&lt;/span&gt;
&lt;a href="#3-content-script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Content scripts are scripts that run in the context of web pages (as if injecting a script in page itself) . They can be used to manipulate the DOM, interact with the page, etc but have limited access to browser APIs. We have to use message passing to communicate with the rest of the extension. One important point to note is that content scripts run in an &amp;lsquo;isolated world&amp;rsquo; by default (can be run in &amp;lsquo;main&amp;rsquo; world too), meaning the javascript environment of the page and extension are different. Thus, the webpage as well as the content scripts of all the extensions run in isolation and cannot access other&amp;rsquo;s context.&lt;/p&gt;
&lt;h4&gt;4. Popup&lt;span class="hx:absolute hx:-mt-20" id="4-popup"&gt;&lt;/span&gt;
&lt;a href="#4-popup" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The popup is a small window that appears when the user clicks on the extension icon. It can be used to display information, settings, or other UI elements.&lt;/p&gt;
&lt;h4&gt;5. Options Page and Browser Action&lt;span class="hx:absolute hx:-mt-20" id="5-options-page-and-browser-action"&gt;&lt;/span&gt;
&lt;a href="#5-options-page-and-browser-action" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The options page is a full HTML page that is used to display the settings of the extension. It can be used to configure the extension, set preferences, etc. Browser Action is the button that the extension adds to the browser toolbar. It can be used to trigger actions, open the popup, etc.&lt;/p&gt;
&lt;p&gt;Both Chrome and Firefox have excellent documentation on building extensions. You can refer to the &lt;a href="https://developer.chrome.com/docs/extensions/get-started"target="_blank" rel="noopener"&gt;Chrome Extension Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions"target="_blank" rel="noopener"&gt;Firefox Extension Docs&lt;svg class="hx:inline hx:rtl:rotate-270 hx:align-baseline" height="1em" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;path d="m9.1716 7.7574h7.0711m0 0v7.0711m0-7.0711-8.4853 8.4853" stroke-linecap="round" stroke-linejoin="round"/&gt;
&lt;/svg&gt;&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h3&gt;Anatomy Of An Extension&lt;span class="hx:absolute hx:-mt-20" id="anatomy-of-an-extension"&gt;&lt;/span&gt;
&lt;a href="#anatomy-of-an-extension" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The following diagram shows the anatomy of an extension:&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;
&lt;img src="extension_anatomy.png" title="Anatomy of an extension" alt="" loading="lazy" /&gt;
&lt;figcaption&gt;Anatomy of an extension&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;It illustrates how the different components of an extension interact with each other, as previously discussed.&lt;/p&gt;
&lt;p&gt;In the next few posts, we will explore :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project setup using WXT, TailwindCSS and Shadcn and understanding the project structure and configuration&lt;/li&gt;
&lt;li&gt;Content Scripts and building isolated UIs for the extension&lt;/li&gt;
&lt;li&gt;Background scripts and messaging&lt;/li&gt;
&lt;li&gt;Storage and Permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We will build an extension along the way using the concepts as we learn.&lt;/p&gt;</description></item></channel></rss>