<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Abdullah Al Fahad</title>
    <description>The latest articles on Forem by Abdullah Al Fahad (@imfahad).</description>
    <link>https://forem.com/imfahad</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F245020%2F09ecdc39-71a3-4bc3-aa60-316734281d96.png</url>
      <title>Forem: Abdullah Al Fahad</title>
      <link>https://forem.com/imfahad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/imfahad"/>
    <language>en</language>
    <item>
      <title>I shipped a video player to npm — twice. What I learned about scoped CSS, "use client", and Nuxt modules.</title>
      <dc:creator>Abdullah Al Fahad</dc:creator>
      <pubDate>Sat, 09 May 2026 10:28:33 +0000</pubDate>
      <link>https://forem.com/imfahad/i-shipped-a-video-player-to-npm-twice-what-i-learned-about-scoped-css-use-client-and-nuxt-2dfl</link>
      <guid>https://forem.com/imfahad/i-shipped-a-video-player-to-npm-twice-what-i-learned-about-scoped-css-use-client-and-nuxt-2dfl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A small, HLS-capable video player for React &lt;strong&gt;and&lt;/strong&gt; Vue, with zero global CSS side-effects. Built in the open. Try it: &lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The annoying gap
&lt;/h2&gt;

&lt;p&gt;I needed an embeddable video player for a side project. Requirements were boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HLS streaming (&lt;code&gt;.m3u8&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A clean play button overlay, optional close button, optional desktop/mobile aspect-ratio toggle&lt;/li&gt;
&lt;li&gt;Fits the design system without fighting it&lt;/li&gt;
&lt;li&gt;Works in &lt;strong&gt;Next.js App Router&lt;/strong&gt; without a &lt;em&gt;"ReactServerComponentsError"&lt;/em&gt; headache&lt;/li&gt;
&lt;li&gt;Works in &lt;strong&gt;Nuxt 3&lt;/strong&gt; without manually wiring a plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried the popular options. Each was &lt;em&gt;almost&lt;/em&gt; right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;react-player&lt;/code&gt; is mature but the API is geared toward "give me a URL and a giant control bar."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;video.js&lt;/code&gt; is overkill for an embed and ships a chunk of theme CSS that fights Tailwind.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plyr&lt;/code&gt;, &lt;code&gt;vidstack&lt;/code&gt; — beautiful, but either too heavy or too opinionated about styling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing that kept biting me was &lt;strong&gt;CSS bleed&lt;/strong&gt;. Every "drop-in" player I tried shipped global resets, theme tokens, or &lt;code&gt;*&lt;/code&gt; selectors that quietly nudged my buttons by 1px or rewrote my form input borders. In a design system you've spent weeks tuning, that's a paper cut you don't want.&lt;/p&gt;

&lt;p&gt;So I built one. Then, because half the consumers I had in mind were on Vue, I built it twice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@glitchlab/react-video-player&lt;/code&gt;&lt;/strong&gt; — &lt;a href="https://www.npmjs.com/package/@glitchlab/react-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/react-video-player&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@glitchlab/vue-video-player&lt;/code&gt;&lt;/strong&gt; — &lt;a href="https://www.npmjs.com/package/@glitchlab/vue-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/vue-video-player&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are at v1.0.2 today. Both are MIT. Both have the same prop surface, same UI, and ship under 4 KB gzipped of CSS + JS.&lt;/p&gt;

&lt;p&gt;There's a live playground on Vercel where you can drop in a file or paste an HLS URL and try it end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is the build-in-public version of how I got there. Three decisions ended up doing most of the work — the rest was just plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 1: kill the global CSS
&lt;/h2&gt;

&lt;p&gt;The first version of this player used Tailwind v4. I wrote the components, ran &lt;code&gt;vite build --lib&lt;/code&gt;, and the resulting &lt;code&gt;dist/style.css&lt;/code&gt; was &lt;strong&gt;17 KB&lt;/strong&gt; of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;:host&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-violet-700&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="py"&gt;--font-sans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ui-sans-serif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;*,&lt;/span&gt; &lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;::after&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;::backdrop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tw-translate-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire Tailwind preflight, baked in. Anyone who imported &lt;code&gt;@glitchlab/react-video-player/style.css&lt;/code&gt; would get my theme tokens injected into &lt;code&gt;:root&lt;/code&gt; and global resets on every element on their page.&lt;/p&gt;

&lt;p&gt;For an internal app this is whatever. For a published library it's malpractice. So I rewrote the CSS by hand.&lt;/p&gt;

&lt;p&gt;The full stylesheet now scopes everything under a single class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="o"&gt;*,&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.gvp-play&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;91&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt; &lt;span class="m"&gt;182&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;:root&lt;/code&gt;, no &lt;code&gt;*&lt;/code&gt; selectors at the document level. The component owns its subtree and nothing else. The CSS file dropped from &lt;strong&gt;17 KB to 2.8 KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The bonus: consumers can now override with predictable specificity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.gvp-root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-play&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rebeccapurple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.gvp-toggle-btn.is-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;deeppink&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole "design system" story. No &lt;code&gt;@apply&lt;/code&gt;, no &lt;code&gt;:where()&lt;/code&gt; tricks, no Tailwind dependency in the lib. Tailwind users still get to use Tailwind in their own app — the player just stops yelling at theirs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if you ship CSS in an npm package, treat it like API. Every selector you publish is a contract. &lt;code&gt;:root { --color-foo: ...; }&lt;/code&gt; is a worse breaking change than removing a prop, because it breaks silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 2: preserve "use client" through the build
&lt;/h2&gt;

&lt;p&gt;The React package targets Next.js App Router. That means every file that uses hooks needs &lt;code&gt;"use client";&lt;/code&gt; at the top, otherwise Next refuses to render it from a server component.&lt;/p&gt;

&lt;p&gt;The component source had it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the gotcha: &lt;strong&gt;rollup strips top-of-file directives during bundling unless you tell it not to.&lt;/strong&gt; I ran &lt;code&gt;vite build --lib&lt;/code&gt;, looked at &lt;code&gt;dist/index.mjs&lt;/code&gt;, and the directive was gone. Importing the package from a Next App Router server component blew up with the classic &lt;em&gt;"You're importing a component that needs &lt;code&gt;useState&lt;/code&gt;"&lt;/em&gt; error.&lt;/p&gt;

&lt;p&gt;The fix is a tiny rollup output plugin that re-prepends the directive after bundling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preserveUseClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preserve-use-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;renderChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"use client"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'use client'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`"use client";\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;preserveUseClient&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, both &lt;code&gt;dist/index.mjs&lt;/code&gt; and &lt;code&gt;dist/index.cjs&lt;/code&gt; start with &lt;code&gt;"use client";&lt;/code&gt;. Consumers can import the package directly from a server component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/page.tsx — server component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.m3u8"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No client-component wrapper required. Just import and render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if your README claims "Next.js App Router compatible," verify the directive survives the bundler. Open &lt;code&gt;dist/index.mjs&lt;/code&gt; after a build. If line 1 isn't &lt;code&gt;"use client";&lt;/code&gt;, you're shipping a footgun.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 3: ship a real Nuxt module
&lt;/h2&gt;

&lt;p&gt;The Vue package was supposed to mirror the React one's "drop in and go" feel. For Nuxt, that meant a real module — not a "import this component manually in every page."&lt;/p&gt;

&lt;p&gt;Nuxt's module system is a small library of helpers (&lt;code&gt;@nuxt/kit&lt;/code&gt;) that lets you add plugins, components, and composables to a Nuxt app. The trick is that &lt;code&gt;@nuxt/kit&lt;/code&gt; imports Node-only modules (&lt;code&gt;giget&lt;/code&gt;, &lt;code&gt;node:fs&lt;/code&gt;, etc.) — so if you naively re-export your Nuxt module from your main entry, the lib's vanilla Vue users get a bundle that tries to require &lt;code&gt;node:fs&lt;/code&gt; in the browser.&lt;/p&gt;

&lt;p&gt;The fix is to give Nuxt its own subpath export and never let it touch the main entry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package.json (excerpt)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.cjs"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./style.css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/style.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./nuxt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/nuxt-module.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/nuxt-module.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/nuxt-module.cjs"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vite config emits two entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nuxt-module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/utils/nuxt-module.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hls.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nuxt/kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/^node:.*/&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Nuxt module itself is mostly boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/nuxt-module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineNuxtModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nuxt/kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;configKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vueVideoPlayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compatibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nuxt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;=3.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_nuxt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;addPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./nuxt-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the plugin auto-registers the component globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils/nuxt-plugin.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineNuxtPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VideoPlayer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../VideoPlayer.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtPlugin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;nuxtApp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nuxtApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vueApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VueVideoPlayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VideoPlayer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Nuxt 3 users get one-line integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/nuxt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the component is available globally — no import, just &lt;code&gt;&amp;lt;VueVideoPlayer src="..." /&amp;gt;&lt;/code&gt; anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; subpath exports aren't optional for libraries with framework-specific entry points. Putting a Nuxt module behind &lt;code&gt;./nuxt&lt;/code&gt; means non-Nuxt consumers never load &lt;code&gt;@nuxt/kit&lt;/code&gt; and its Node-only deps. Their browser bundle stays clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  A bug that took an hour to find: Vue HLS first-mount race
&lt;/h2&gt;

&lt;p&gt;I want to call this one out because it's a category of bug that's easy to ship and almost impossible to catch with smoke tests.&lt;/p&gt;

&lt;p&gt;The Vue HLS player initialized like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLVideoElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;initPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// 👈 the bug&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. Tests passed. The deployed playground showed the player frame, the play button, the native controls bar — but &lt;strong&gt;the video never loaded&lt;/strong&gt;. Click play, the play button vanished (so &lt;code&gt;play()&lt;/code&gt; resolved), but no pixels.&lt;/p&gt;

&lt;p&gt;The cause: &lt;code&gt;watch(..., { immediate: true })&lt;/code&gt; fires the callback during &lt;code&gt;setup()&lt;/code&gt;, &lt;strong&gt;before the template renders&lt;/strong&gt;. At that point &lt;code&gt;videoEl.value&lt;/code&gt; is still &lt;code&gt;null&lt;/code&gt;. &lt;code&gt;initPlayer()&lt;/code&gt; early-returns, the watcher's already fired, and &lt;code&gt;props.src&lt;/code&gt; doesn't change again — so HLS never attaches.&lt;/p&gt;

&lt;p&gt;The React side wasn't affected because React effects always run &lt;em&gt;after&lt;/em&gt; commit. The ref is bound by the time the effect fires.&lt;/p&gt;

&lt;p&gt;The fix is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;onMounted&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;initPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;initPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// no `immediate`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the first init runs in &lt;code&gt;onMounted&lt;/code&gt; (template ref is bound), and subsequent &lt;code&gt;src&lt;/code&gt; changes are handled by the regular watcher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; &lt;code&gt;immediate: true&lt;/code&gt; and refs don't mix cleanly in Composition API. If your watcher needs a ref that's bound by the template, use &lt;code&gt;onMounted&lt;/code&gt; for the first run and a non-immediate watch for updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  The playground
&lt;/h2&gt;

&lt;p&gt;I wanted a proof point that wasn't "trust the README." So I built a minimal Next.js app with three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A drag-and-drop file zone (uploads create blob URLs and feed them to the player)&lt;/li&gt;
&lt;li&gt;A URL input with validation (paste any HTTP/HTTPS link, including &lt;code&gt;.m3u8&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The player itself, configured to remount cleanly on source change via &lt;code&gt;key={videoSrc}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's deployed at &lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste the test HLS stream &lt;code&gt;https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8&lt;/code&gt; and watch it play&lt;/li&gt;
&lt;li&gt;Paste a CORS-permissive &lt;code&gt;.mp4&lt;/code&gt; URL and watch it play&lt;/li&gt;
&lt;li&gt;Drop a local file and watch the blob URL feed the player&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs on the actual published &lt;code&gt;@glitchlab/react-video-player&lt;/code&gt; from npm. Every time I publish a new version, I bump the dep and redeploy — the playground exercises the real published bundle, not source.&lt;/p&gt;

&lt;p&gt;The Vue package shares the same UX in the local playground at &lt;code&gt;playground/vue/&lt;/code&gt; in the monorepo. The deployed demo is React because that's what Vercel templates make easy; the Vue version is one &lt;code&gt;pnpm dev:vue&lt;/code&gt; away if you clone the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in v1.0.2
&lt;/h2&gt;

&lt;p&gt;Both packages at v1.0.2 today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same prop surface across React and Vue.&lt;/strong&gt; &lt;code&gt;src&lt;/code&gt;, &lt;code&gt;poster&lt;/code&gt;, &lt;code&gt;showDeviceToggle&lt;/code&gt;, &lt;code&gt;defaultDevice&lt;/code&gt;, &lt;code&gt;hoverPlay&lt;/code&gt;, &lt;code&gt;tooltipText&lt;/code&gt;, &lt;code&gt;muted&lt;/code&gt;, &lt;code&gt;loop&lt;/code&gt;, &lt;code&gt;controls&lt;/code&gt;, &lt;code&gt;frameMaxWidth&lt;/code&gt;, &lt;code&gt;aspectRatio&lt;/code&gt;, &lt;code&gt;hlsConfig&lt;/code&gt;, &lt;code&gt;isHls&lt;/code&gt;. React adds &lt;code&gt;onClose&lt;/code&gt; + &lt;code&gt;children&lt;/code&gt;; Vue uses &lt;code&gt;closable&lt;/code&gt; + &lt;code&gt;@close&lt;/code&gt; event + default slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HLS via &lt;code&gt;hls.js&lt;/code&gt;&lt;/strong&gt; with automatic native fallback for Safari (no &lt;code&gt;hls.js&lt;/code&gt; cost when MSE isn't needed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Captions/subtitles passthrough.&lt;/strong&gt; Pass &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; elements as React children or Vue default slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover-play with race-safe play/pause.&lt;/strong&gt; Tracks the play promise so a quick mouse-leave can't trigger a &lt;code&gt;DOMException&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hlsConfig&lt;/code&gt; is memoizable.&lt;/strong&gt; Pass a stable reference (&lt;code&gt;useMemo&lt;/code&gt; / &lt;code&gt;shallowRef&lt;/code&gt;) to avoid HLS rebuilds on render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript types&lt;/strong&gt;, full source maps, MIT licensed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoke tests&lt;/strong&gt; in vitest (8 React, 7 Vue).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bundle size: ~3 KB JS gzipped + ~1 KB CSS gzipped, per package.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's not in v1.0.2 (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Picture-in-Picture toggle&lt;/strong&gt; — the API is trivial, the UI affordance isn't. Holding off until I know how it should look in the toggle pill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback speed control&lt;/strong&gt; — same. Will land when I find a non-busy way to expose it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio language switcher&lt;/strong&gt; — for HLS streams with multiple audio renditions (dubs, descriptive audio, secondary languages). &lt;code&gt;hls.js&lt;/code&gt; exposes &lt;code&gt;audioTracks&lt;/code&gt; and &lt;code&gt;audioTrack&lt;/code&gt; already, so the wiring is small; the open question is the UI affordance, same as PiP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caption track UI.&lt;/strong&gt; You can pass &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; elements but the component doesn't render a captions menu. Native browser controls (&lt;code&gt;controls={true}&lt;/code&gt;) handle this for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An actual stable Nuxt module ecosystem release&lt;/strong&gt; — currently the module exists and works, but isn't on &lt;a href="https://nuxt.com/modules" rel="noopener noreferrer"&gt;https://nuxt.com/modules&lt;/a&gt;. That's a separate process I'll do when the API has settled for a quarter or two.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# React&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @glitchlab/react-video-player hls.js

&lt;span class="c"&gt;# Vue&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @glitchlab/vue-video-player hls.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React + Next.js App Router&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/react-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactVideoPlayer&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.m3u8"&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Nuxt 3&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/nuxt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@glitchlab/vue-video-player/style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- anywhere in your Nuxt app --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;VueVideoPlayer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/videos/hero.m3u8"&lt;/span&gt; &lt;span class="na"&gt;:controls=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live demo: &lt;strong&gt;&lt;a href="https://video-player-playgraound.vercel.app/" rel="noopener noreferrer"&gt;https://video-player-playgraound.vercel.app/&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
React package: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@glitchlab/react-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/react-video-player&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Vue package: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@glitchlab/vue-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@glitchlab/vue-video-player&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;strong&gt;&lt;a href="https://github.com/im-fahad/react-video-player" rel="noopener noreferrer"&gt;https://github.com/im-fahad/react-video-player&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://github.com/im-fahad/vue-video-player" rel="noopener noreferrer"&gt;https://github.com/im-fahad/vue-video-player&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Issues, PRs, and feature requests welcome. If something's broken in your setup, the playground is the fastest way to reproduce it — drop your URL in, screenshot what you see, file an issue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you got value from this, the cheapest way to support is hitting the GitHub star button or sharing the playground link with someone fighting the same CSS-bleed problem. Both packages are MIT and will stay that way.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>vue</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implementing User Authentication in Next.js 14 using AuthJs Credentials (REST API)</title>
      <dc:creator>Abdullah Al Fahad</dc:creator>
      <pubDate>Mon, 19 Feb 2024 04:55:13 +0000</pubDate>
      <link>https://forem.com/imfahad/implementing-user-authentication-in-nextjs-14-using-authjs-credentials-rest-api-20jd</link>
      <guid>https://forem.com/imfahad/implementing-user-authentication-in-nextjs-14-using-authjs-credentials-rest-api-20jd</guid>
      <description>&lt;p&gt;Let’s talk step by step about how to implement Next.js 14’s user authentication with Auth.js (credentials) and REST API.&lt;/p&gt;

&lt;p&gt;To start, begin by creating a &lt;a href="https://nextjs.org/docs/app/api-reference/create-next-app"&gt;Next.js&lt;/a&gt; application.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx create-next-app@latest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then, integrate the &lt;a href="https://authjs.dev/reference/nextjs"&gt;Auth.js&lt;/a&gt; library into your application.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install next-auth@beta&lt;/code&gt;&lt;br&gt;
Next, you will need to add a &lt;code&gt;AUTH_SECRET&lt;/code&gt; and &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; to your &lt;code&gt;env&lt;/code&gt; file. Although during development you could leave &lt;code&gt;AUTH_SECRET&lt;/code&gt; it empty, during production it would raise an error.&lt;/p&gt;

&lt;p&gt;Now, proceed to create the &lt;code&gt;auth.ts&lt;/code&gt; file within your &lt;code&gt;/src&lt;/code&gt; directory. This file will handle authentication by interfacing with a REST API, specifically for login functionality. Depending on your API structure, the response may vary, but you can reference the example API response provided here to guide your implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type {NextAuthConfig} from "next-auth";
import NextAuth from "next-auth";

import CredentialsProvider from "next-auth/providers/credentials";
import $axios from "@/lib/Axios";

async function login(credentials: any) {
    try {
        return await $axios.post("your-login-api", credentials).then((res: any) =&amp;gt; {
            const {user} = res;
            return {
                name: user.name,
                email: user.email,
                image: user.profile_photo,
                accessToken: res.access_token,
                // If you need any other information you can add here...
            };
        });
    } catch (e) {
        throw new Error("Something went wrong.");
    }
}

export const config: NextAuthConfig = {
    pages: {signIn: "/login"},
    providers: [
        CredentialsProvider({
            name: "Credentials",
            credentials: {
                email: {label: "Email", type: "email", placeholder: "example@email.com"},
                password: {label: "Password", type: "password", placeholder: "******"}
            },
            async authorize(credentials,) {
                try {
                    return login(credentials);
                } catch (e) {
                    return {};
                }
            },
        }),
    ],
    callbacks: {
        async jwt({user, token}) {
            if (user) {
                token.user = user;
            }
            return token;
        },
        async session({session, token}: any) {
            session.user = token.user;
            return session;
        },
    },
    debug: process.env.NODE_ENV === "development",
};

export const {handlers, auth, signIn, signOut} = NextAuth(config);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jwt()&lt;/code&gt; callback is called user when the user first logs in. The user object will be populated with the object that is returned from the authorize function. The object that is returned from the &lt;code&gt;jwt&lt;/code&gt; callback is what will be saved on the session cookie.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;session()&lt;/code&gt; callback receives the session cookie content in its token parameter. Whatever is returned from this callback is what will be presented when &lt;code&gt;useSession&lt;/code&gt; or &lt;code&gt;getServerSession&lt;/code&gt; is called.&lt;/p&gt;

&lt;p&gt;Next, establish a &lt;code&gt;api&lt;/code&gt; directory within either your &lt;code&gt;/src/app&lt;/code&gt; or &lt;code&gt;/app&lt;/code&gt; directory. Inside this api directory, create a &lt;code&gt;route.ts&lt;/code&gt; file within the &lt;code&gt;api/auth/[...nextauth]&lt;/code&gt; directory structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {handlers} from "@/auth";

export const {GET, POST} = handlers;
Then create another route.ts file inside api/protected directory.

import {auth} from "@/auth";

export const GET = auth((req) =&amp;gt; {
    if (req.auth) {
        return Response.json({data: "Protected data"});
    }

    return Response.json({message: "Not authenticated"}, {status: 401});
}) as any;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;layout.tsx&lt;/code&gt; file, you'll utilize the &lt;code&gt;SessionProvider&lt;/code&gt; to manage session data. This component enables your application to interact with the user authentication state across various components. You can use it to display different content or UI elements based on whether a user is authenticated or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import React from "react";
import {SessionProvider} from "next-auth/react";
...

export default function RootLayout({children}: { children: React.ReactNode }) {
    return (
        &amp;lt;SessionProvider&amp;gt;
            &amp;lt;html lang="en"&amp;gt;
            &amp;lt;body className="pintoe"&amp;gt;
                {children}
            &amp;lt;/body&amp;gt;
            &amp;lt;/html&amp;gt;
        &amp;lt;/SessionProvider&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well done, your &lt;a href="https://authjs.dev/reference/nextjs"&gt;Auth.js&lt;/a&gt; setup is complete. Now you can use your session data on every page (route). ex: &lt;code&gt;app/pages.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import {useSession} from "next-auth/react";
import Image from "next/image";

export default function HomePage() {
    const {data: session} = useSession();

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;h2&amp;gt;{session?.user.name}&amp;lt;/h2&amp;gt;

            &amp;lt;Image src={session?.user.image} alt="User Avatar" width={200} height={200}/&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use a higher order component to protect authenticated routes. On my side, I create a &lt;code&gt;AuthGuard.tsx&lt;/code&gt; file in &lt;code&gt;src/&lt;/code&gt; components directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import React, {useEffect, useState} from "react";
import {useSession} from "next-auth/react";
import {usePathname, useRouter} from "next/navigation";

export default function AuthGuard({children}: { children: React.ReactNode }) {
    const {data: status}: any = useSession();
    const router = useRouter();
    const pathname = usePathname();
    const [loading, setLoading] = useState&amp;lt;boolean&amp;gt;(true);

    const guestRoutes: string[] = [
        "/login",
        "/register"
    ];

    useEffect(() =&amp;gt; {
        const isGuestRoute = !!guestRoutes.find(route =&amp;gt; route === pathname);
        setLoading(true);

        if (status === "loading") {
            return;
        } else if (status === "authenticated" &amp;amp;&amp;amp; isGuestRoute) {
            router.push("/");
        } else if (status === "unauthenticated" &amp;amp;&amp;amp; !isGuestRoute) {
            router.push("/login");
        } else {
            setLoading(false);
        }
    }, [pathname, status]);

    return (loading ?
        &amp;lt;div&amp;gt;
            Loading...
        &amp;lt;/div&amp;gt; : children);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the &lt;code&gt;AuthGuard&lt;/code&gt; component in your &lt;code&gt;layout.tsx&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import "@/assets/styles/tailwind.css";
import React from "react";
import {SessionProvider} from "next-auth/react";
import AuthGuard from "@/lib/AuthGurad";

export default function RootLayout({children}: { children: React.ReactNode }) {
    return (
        &amp;lt;SessionProvider&amp;gt;
            &amp;lt;html lang="en"&amp;gt;
            &amp;lt;body className="pintoe"&amp;gt;
            &amp;lt;AuthGuard&amp;gt;
                {children}
            &amp;lt;/AuthGuard&amp;gt;
            &amp;lt;/body&amp;gt;
            &amp;lt;/html&amp;gt;
        &amp;lt;/SessionProvider&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the AuthGuard component will automatically handle guest/auth route redirects based on login session data.&lt;/p&gt;

&lt;p&gt;Next, we will discuss step by step how to implement &lt;code&gt;redux-toolkit&lt;/code&gt; in &lt;a href="https://nextjs.org/"&gt;Next.js 14&lt;/a&gt; and how to configure &lt;a href="https://axios-http.com/"&gt;Axios&lt;/a&gt; to get &lt;code&gt;access-token&lt;/code&gt; from the auth session.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
