<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Guillaume Sere</title>
    <description>The latest articles on DEV Community by Guillaume Sere (@guillaumesere).</description>
    <link>https://dev.to/guillaumesere</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1382485%2F328dcc95-8834-499b-bfed-557f03b54a8f.jpg</url>
      <title>DEV Community: Guillaume Sere</title>
      <link>https://dev.to/guillaumesere</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guillaumesere"/>
    <language>en</language>
    <item>
      <title>🧩 Building an Interactive 3D Rubik's Cube with Three.js</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:33:41 +0000</pubDate>
      <link>https://dev.to/guillaumesere/building-an-interactive-3d-rubiks-cube-with-threejs-gf0</link>
      <guid>https://dev.to/guillaumesere/building-an-interactive-3d-rubiks-cube-with-threejs-gf0</guid>
      <description>&lt;p&gt;3D graphics on the web have become much more accessible thanks to modern libraries like &lt;strong&gt;Three.js&lt;/strong&gt;. As a personal challenge, I decided to build an interactive &lt;strong&gt;3D Rubik's Cube&lt;/strong&gt; that runs entirely in the browser.&lt;/p&gt;

&lt;p&gt;The objective wasn't just to render a cube, but to recreate the mechanics of a real Rubik's Cube with smooth animations, realistic rotations, and an intuitive interface.&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Live Demo:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://ruby-cube-60.vercel.app/" rel="noopener noreferrer"&gt;https://ruby-cube-60.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopysmzt5fgw68bsux3cb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopysmzt5fgw68bsux3cb.png" alt=" " width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  🚀 Why This Project?
&lt;/h1&gt;

&lt;p&gt;This project allowed me to explore several areas of modern web development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time 3D rendering&lt;/li&gt;
&lt;li&gt;Matrix transformations&lt;/li&gt;
&lt;li&gt;Object hierarchy management&lt;/li&gt;
&lt;li&gt;Smooth animations&lt;/li&gt;
&lt;li&gt;Interactive controls&lt;/li&gt;
&lt;li&gt;Performance optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a Rubik's Cube is an excellent exercise because every move affects multiple pieces while preserving the cube's overall structure.&lt;/p&gt;




&lt;h1&gt;
  
  
  🛠️ Tech Stack
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Three.js&lt;/li&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;GSAP&lt;/li&gt;
&lt;li&gt;dat.GUI&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  ✨ Features
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;🧩 Fully interactive 3D Rubik's Cube&lt;/li&gt;
&lt;li&gt;🎮 Individual face rotations&lt;/li&gt;
&lt;li&gt;🎥 Free camera rotation&lt;/li&gt;
&lt;li&gt;⚡ Smooth animations with GSAP&lt;/li&gt;
&lt;li&gt;⏱️ Integrated timer&lt;/li&gt;
&lt;li&gt;🔢 Move counter&lt;/li&gt;
&lt;li&gt;🎲 Random shuffle&lt;/li&gt;
&lt;li&gt;🔄 Reset cube&lt;/li&gt;
&lt;li&gt;📜 Move history&lt;/li&gt;
&lt;li&gt;🎨 Responsive interface&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  🧠 Cube Architecture
&lt;/h1&gt;

&lt;p&gt;The Rubik's Cube is composed of &lt;strong&gt;27 smaller cubes (Cubies)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each cubie is an independent &lt;strong&gt;Three.js Mesh&lt;/strong&gt; positioned inside a virtual 3×3×3 grid.&lt;/p&gt;

&lt;p&gt;Instead of modifying a single object, every rotation updates only the cubies belonging to the selected face.&lt;/p&gt;

&lt;p&gt;This architecture closely reproduces the behavior of a physical Rubik's Cube.&lt;/p&gt;




&lt;h1&gt;
  
  
  🔄 Face Rotation Logic
&lt;/h1&gt;

&lt;p&gt;When a player rotates a face, the application performs several steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the cubies belonging to the selected layer.&lt;/li&gt;
&lt;li&gt;Group them temporarily.&lt;/li&gt;
&lt;li&gt;Animate the rotation using GSAP.&lt;/li&gt;
&lt;li&gt;Update each cubie's position.&lt;/li&gt;
&lt;li&gt;Update each cubie's orientation.&lt;/li&gt;
&lt;li&gt;Remove the temporary group.&lt;/li&gt;
&lt;li&gt;Wait for the next move.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps the cube perfectly synchronized after every animation.&lt;/p&gt;




&lt;h1&gt;
  
  
  🎨 Rendering with Three.js
&lt;/h1&gt;

&lt;p&gt;Three.js handles the entire 3D scene, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Perspective Camera&lt;/li&gt;
&lt;li&gt;Scene&lt;/li&gt;
&lt;li&gt;WebGL Renderer&lt;/li&gt;
&lt;li&gt;Lights&lt;/li&gt;
&lt;li&gt;Meshes&lt;/li&gt;
&lt;li&gt;Materials&lt;/li&gt;
&lt;li&gt;Camera Controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using hardware acceleration through WebGL allows the cube to remain fluid even during continuous rotations.&lt;/p&gt;




&lt;h1&gt;
  
  
  ⚡ Smooth Animations
&lt;/h1&gt;

&lt;p&gt;Every face rotation is animated with &lt;strong&gt;GSAP&lt;/strong&gt;, providing fluid and natural movements.&lt;/p&gt;

&lt;p&gt;The animation system also prevents overlapping rotations, ensuring that one move is completed before another begins.&lt;/p&gt;

&lt;p&gt;This greatly improves the overall user experience.&lt;/p&gt;




&lt;h1&gt;
  
  
  🚧 Technical Challenges
&lt;/h1&gt;

&lt;p&gt;Although the interface looks simple, implementing the mechanics required solving several interesting problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correctly identifying cubies on each face&lt;/li&gt;
&lt;li&gt;Preserving cube orientation after rotations&lt;/li&gt;
&lt;li&gt;Synchronizing animations with cube state&lt;/li&gt;
&lt;li&gt;Preventing simultaneous moves&lt;/li&gt;
&lt;li&gt;Updating positions without losing precision&lt;/li&gt;
&lt;li&gt;Maintaining smooth rendering performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These challenges made the project much more interesting than a simple 3D rendering demo.&lt;/p&gt;




&lt;h1&gt;
  
  
  📚 What I Learned
&lt;/h1&gt;

&lt;p&gt;This project helped me improve my knowledge of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three.js&lt;/li&gt;
&lt;li&gt;WebGL rendering&lt;/li&gt;
&lt;li&gt;3D transformations&lt;/li&gt;
&lt;li&gt;Rotation matrices&lt;/li&gt;
&lt;li&gt;Object hierarchy&lt;/li&gt;
&lt;li&gt;GSAP animations&lt;/li&gt;
&lt;li&gt;Interactive UI design&lt;/li&gt;
&lt;li&gt;Vite project architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building interactive 3D applications is a great way to better understand graphics programming and object transformations.&lt;/p&gt;




&lt;h1&gt;
  
  
  💡 Future Improvements
&lt;/h1&gt;

&lt;p&gt;Some ideas for future versions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🤖 Automatic solving algorithm&lt;/li&gt;
&lt;li&gt;📖 Beginner solving tutorial&lt;/li&gt;
&lt;li&gt;🎯 Scramble generator&lt;/li&gt;
&lt;li&gt;💾 Save and restore cube state&lt;/li&gt;
&lt;li&gt;🌙 Additional visual themes&lt;/li&gt;
&lt;li&gt;📱 Improved mobile controls&lt;/li&gt;
&lt;li&gt;🏆 Speedcubing statistics&lt;/li&gt;
&lt;li&gt;🎵 Sound effects&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  🎉 Conclusion
&lt;/h1&gt;

&lt;p&gt;Building this Rubik's Cube was an exciting challenge that combined mathematics, 3D graphics, animations, and front-end development.&lt;/p&gt;

&lt;p&gt;Rather than relying on a game engine, the entire experience was built using &lt;strong&gt;Three.js&lt;/strong&gt;, &lt;strong&gt;GSAP&lt;/strong&gt;, and &lt;strong&gt;Vite&lt;/strong&gt;, resulting in a lightweight and interactive web application.&lt;/p&gt;

&lt;p&gt;If you're interested in &lt;strong&gt;Three.js&lt;/strong&gt;, &lt;strong&gt;WebGL&lt;/strong&gt;, or interactive 3D experiences on the web, I'd love to hear your feedback!&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Try it here:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://ruby-cube-60.vercel.app/" rel="noopener noreferrer"&gt;https://ruby-cube-60.vercel.app/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏷️ Tags
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;threejs&lt;/code&gt; &lt;code&gt;javascript&lt;/code&gt; &lt;code&gt;vite&lt;/code&gt; &lt;code&gt;webgl&lt;/code&gt; &lt;code&gt;3d&lt;/code&gt; &lt;code&gt;frontend&lt;/code&gt; &lt;code&gt;gamedev&lt;/code&gt; &lt;code&gt;gsap&lt;/code&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>javascript</category>
      <category>gamedev</category>
      <category>css</category>
    </item>
    <item>
      <title>🍬 Building a Candy Crush-Inspired Match-3 Game with React &amp; TypeScript</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:27:57 +0000</pubDate>
      <link>https://dev.to/guillaumesere/building-a-candy-crush-inspired-match-3-game-with-react-typescript-7k7</link>
      <guid>https://dev.to/guillaumesere/building-a-candy-crush-inspired-match-3-game-with-react-typescript-7k7</guid>
      <description>&lt;p&gt;Games are an excellent way to improve front-end development skills. Unlike traditional CRUD applications, they require algorithms, real-time state management, animations, and user interactions to work together seamlessly.&lt;/p&gt;

&lt;p&gt;To challenge myself, I built a &lt;strong&gt;Candy Crush-inspired Match-3 game&lt;/strong&gt; entirely with &lt;strong&gt;React&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt;, without using a game engine.&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Live Demo:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://candy-crush-gratuit.vercel.app/" rel="noopener noreferrer"&gt;https://candy-crush-gratuit.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1xot83j02wup97ch55a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1xot83j02wup97ch55a.png" alt=" " width="799" height="384"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🚀 Why This Project?
&lt;/h2&gt;

&lt;p&gt;The goal was to recreate the core mechanics of a Match-3 game while keeping the project lightweight, responsive, and enjoyable on both desktop and mobile devices.&lt;/p&gt;

&lt;p&gt;Rather than focusing only on the visual aspect, I wanted to implement all the gameplay mechanics from scratch.&lt;/p&gt;


&lt;h2&gt;
  
  
  🛠️ Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React 18&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Create React App&lt;/li&gt;
&lt;li&gt;HammerJS (touch gestures)&lt;/li&gt;
&lt;li&gt;CSS3&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🍬 Drag &amp;amp; Drop gameplay&lt;/li&gt;
&lt;li&gt;📱 Mobile touch support with HammerJS&lt;/li&gt;
&lt;li&gt;🎯 Automatic match detection&lt;/li&gt;
&lt;li&gt;↔️ Horizontal and vertical combinations&lt;/li&gt;
&lt;li&gt;⬇️ Gravity system&lt;/li&gt;
&lt;li&gt;🎲 Random candy generation&lt;/li&gt;
&lt;li&gt;🔥 Combo detection&lt;/li&gt;
&lt;li&gt;📊 Live score tracking&lt;/li&gt;
&lt;li&gt;📈 Progress indicator&lt;/li&gt;
&lt;li&gt;🔀 Shuffle board&lt;/li&gt;
&lt;li&gt;🔄 Restart game&lt;/li&gt;
&lt;li&gt;📱 Fully responsive interface&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🧠 Game Logic
&lt;/h2&gt;

&lt;p&gt;The game board is represented as a two-dimensional grid where each cell contains a candy.&lt;/p&gt;

&lt;p&gt;Every move follows the same sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Swap two candies&lt;/li&gt;
&lt;li&gt;Validate the move&lt;/li&gt;
&lt;li&gt;Detect all horizontal and vertical matches&lt;/li&gt;
&lt;li&gt;Remove matching candies&lt;/li&gt;
&lt;li&gt;Apply gravity so candies fall into empty spaces&lt;/li&gt;
&lt;li&gt;Generate new candies at the top&lt;/li&gt;
&lt;li&gt;Scan the board again for new combinations&lt;/li&gt;
&lt;li&gt;Repeat until the board becomes stable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This recursive process creates the chain reactions that make Match-3 games so satisfying.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔍 Match Detection
&lt;/h2&gt;

&lt;p&gt;After each move, the board is scanned to find groups of three or more identical candies.&lt;/p&gt;

&lt;p&gt;The algorithm checks both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal matches&lt;/li&gt;
&lt;li&gt;Vertical matches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every detected match is removed simultaneously before the gravity phase begins.&lt;/p&gt;

&lt;p&gt;This also allows multiple combinations and combo chains to occur naturally.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;
    &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="nx"&gt;horizontal&lt;/span&gt;
    &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="nx"&gt;vertical&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;
    &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="nx"&gt;candies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📱 Mobile Support
&lt;/h2&gt;

&lt;p&gt;To provide a better experience on smartphones and tablets, the project integrates &lt;strong&gt;HammerJS&lt;/strong&gt; for touch gesture recognition.&lt;/p&gt;

&lt;p&gt;Desktop users can play with the mouse, while mobile users simply swipe candies with their fingers.&lt;/p&gt;

&lt;p&gt;Both input methods use exactly the same game logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Performance
&lt;/h2&gt;

&lt;p&gt;A Match-3 game continuously updates the board after every move.&lt;/p&gt;

&lt;p&gt;To keep gameplay smooth, I focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Efficient board updates&lt;/li&gt;
&lt;li&gt;Lightweight React state management&lt;/li&gt;
&lt;li&gt;Minimal unnecessary re-renders&lt;/li&gt;
&lt;li&gt;Optimized array manipulation&lt;/li&gt;
&lt;li&gt;Smooth gameplay during long combo cascades&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚧 Technical Challenges
&lt;/h2&gt;

&lt;p&gt;Although the gameplay looks simple, implementing the mechanics required solving several challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preventing invalid moves&lt;/li&gt;
&lt;li&gt;Detecting multiple simultaneous matches&lt;/li&gt;
&lt;li&gt;Handling combo cascades&lt;/li&gt;
&lt;li&gt;Applying gravity efficiently&lt;/li&gt;
&lt;li&gt;Synchronizing animations with state updates&lt;/li&gt;
&lt;li&gt;Supporting both mouse and touch interactions&lt;/li&gt;
&lt;li&gt;Keeping rendering performant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These mechanics represent the most interesting part of the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What I Learned
&lt;/h2&gt;

&lt;p&gt;This project allowed me to improve my skills in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;State management&lt;/li&gt;
&lt;li&gt;Grid-based algorithms&lt;/li&gt;
&lt;li&gt;Recursive game logic&lt;/li&gt;
&lt;li&gt;Performance optimization&lt;/li&gt;
&lt;li&gt;Mobile interactions&lt;/li&gt;
&lt;li&gt;Interactive UI development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building games is a fun way to practice algorithms while creating engaging user experiences.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Future Improvements
&lt;/h2&gt;

&lt;p&gt;Some ideas for future versions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🍭 Special candies&lt;/li&gt;
&lt;li&gt;💥 Bomb combinations&lt;/li&gt;
&lt;li&gt;🎯 Level objectives&lt;/li&gt;
&lt;li&gt;🔊 Sound effects&lt;/li&gt;
&lt;li&gt;🎵 Background music&lt;/li&gt;
&lt;li&gt;🏆 Online leaderboard&lt;/li&gt;
&lt;li&gt;💾 Save progression&lt;/li&gt;
&lt;li&gt;🌍 Additional game modes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎉 Conclusion
&lt;/h2&gt;

&lt;p&gt;Building this Match-3 game was a rewarding challenge that combined front-end development with algorithm design.&lt;/p&gt;

&lt;p&gt;It demonstrates how React can be used to build highly interactive applications beyond traditional business interfaces.&lt;/p&gt;

&lt;p&gt;If you're interested in React, TypeScript, or browser game development, I'd love to hear your feedback!&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Play the game here:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://candy-crush-gratuit.vercel.app/" rel="noopener noreferrer"&gt;https://candy-crush-gratuit.vercel.app/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏷️ Tags
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;react&lt;/code&gt; &lt;code&gt;typescript&lt;/code&gt; &lt;code&gt;javascript&lt;/code&gt; &lt;code&gt;gamedev&lt;/code&gt; &lt;code&gt;frontend&lt;/code&gt; &lt;code&gt;css&lt;/code&gt; &lt;code&gt;webdev&lt;/code&gt; &lt;code&gt;algorithms&lt;/code&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>css</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Nouveau lien pour le site</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Sat, 18 Jul 2026 14:13:45 +0000</pubDate>
      <link>https://dev.to/guillaumesere/nouveau-lien-pour-le-site-2ike</link>
      <guid>https://dev.to/guillaumesere/nouveau-lien-pour-le-site-2ike</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/guillaumesere/world-radio-60-explorer-les-radios-du-monde-entier-depuis-votre-navigateur-3p44" class="crayons-story__hidden-navigation-link"&gt;World Radio 60 : explorer les radios du monde entier depuis votre navigateur 🎧🌍&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/guillaumesere" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1382485%2F328dcc95-8834-499b-bfed-557f03b54a8f.jpg" alt="guillaumesere profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/guillaumesere" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Guillaume Sere
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Guillaume Sere
                
              
              &lt;div id="story-author-preview-content-3116015" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/guillaumesere" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1382485%2F328dcc95-8834-499b-bfed-557f03b54a8f.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Guillaume Sere&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/guillaumesere/world-radio-60-explorer-les-radios-du-monde-entier-depuis-votre-navigateur-3p44" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Dec 19 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/guillaumesere/world-radio-60-explorer-les-radios-du-monde-entier-depuis-votre-navigateur-3p44" id="article-link-3116015"&gt;
          World Radio 60 : explorer les radios du monde entier depuis votre navigateur 🎧🌍
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nextjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nextjs&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/guillaumesere/world-radio-60-explorer-les-radios-du-monde-entier-depuis-votre-navigateur-3p44#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Nouvelle URL pour IPTV 60</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:44:46 +0000</pubDate>
      <link>https://dev.to/guillaumesere/nouvelle-url-pour-iptv-60-1am6</link>
      <guid>https://dev.to/guillaumesere/nouvelle-url-pour-iptv-60-1am6</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/guillaumesere/itv-60-mon-lecteur-iptv-personnel-en-ligne-1imh" class="crayons-story__hidden-navigation-link"&gt;📺✨ IPTV 60 fait peau neuve !&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/guillaumesere" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1382485%2F328dcc95-8834-499b-bfed-557f03b54a8f.jpg" alt="guillaumesere profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/guillaumesere" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Guillaume Sere
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Guillaume Sere
                
              
              &lt;div id="story-author-preview-content-3061203" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/guillaumesere" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1382485%2F328dcc95-8834-499b-bfed-557f03b54a8f.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Guillaume Sere&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/guillaumesere/itv-60-mon-lecteur-iptv-personnel-en-ligne-1imh" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 26 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/guillaumesere/itv-60-mon-lecteur-iptv-personnel-en-ligne-1imh" id="article-link-3061203"&gt;
          📺✨ IPTV 60 fait peau neuve !
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nextjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nextjs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/guillaumesere/itv-60-mon-lecteur-iptv-personnel-en-ligne-1imh" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/guillaumesere/itv-60-mon-lecteur-iptv-personnel-en-ligne-1imh#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>J’ai créé Agenda Festivals : une carte interactive des festivals en France avec Next.js et data.gouv</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:36:01 +0000</pubDate>
      <link>https://dev.to/guillaumesere/jai-cree-agenda-festivals-une-carte-interactive-des-festivals-en-france-avec-nextjs-et-datagouv-2f6f</link>
      <guid>https://dev.to/guillaumesere/jai-cree-agenda-festivals-une-carte-interactive-des-festivals-en-france-avec-nextjs-et-datagouv-2f6f</guid>
      <description>&lt;h1&gt;
  
  
  J’ai créé Agenda Festivals : une carte interactive des festivals en France avec Next.js et data.gouv
&lt;/h1&gt;

&lt;p&gt;J’ai récemment développé &lt;strong&gt;Agenda Festivals&lt;/strong&gt;, un site web qui permet d’explorer les festivals en France à partir de données publiques.&lt;/p&gt;

&lt;p&gt;👉 Démo : &lt;a href="https://agenda-festivals.vercel.app/" rel="noopener noreferrer"&gt;https://agenda-festivals.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;L’idée est simple : au lieu d’avoir une liste statique ou difficile à parcourir, je voulais créer une expérience plus visuelle, avec une &lt;strong&gt;carte interactive&lt;/strong&gt;, des &lt;strong&gt;filtres&lt;/strong&gt;, une &lt;strong&gt;recherche&lt;/strong&gt;, des &lt;strong&gt;fiches détaillées&lt;/strong&gt; et une interface responsive.&lt;/p&gt;

&lt;p&gt;Le site permet aujourd’hui d’explorer plus de &lt;strong&gt;7 000 festivals&lt;/strong&gt; en France, avec des informations comme la ville, la région, le département, la discipline, la période et la localisation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfpi931v1lcv1epzd6xt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfpi931v1lcv1epzd6xt.png" alt=" " width="799" height="385"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi ce projet ?
&lt;/h2&gt;

&lt;p&gt;Je voulais travailler sur un projet qui combine plusieurs sujets que j’aime bien :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;l’open data&lt;/li&gt;
&lt;li&gt;la cartographie web&lt;/li&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;l’UX autour de la recherche et des filtres&lt;/li&gt;
&lt;li&gt;la visualisation de données culturelles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La France a énormément de festivals, mais les données sont souvent dispersées ou peu agréables à explorer.&lt;br&gt;&lt;br&gt;
Le dataset de data.gouv donne une très bonne base pour créer un explorateur national.&lt;/p&gt;

&lt;p&gt;L’objectif n’était pas seulement d’afficher des points sur une carte, mais de créer un vrai outil de découverte.&lt;/p&gt;




&lt;h2&gt;
  
  
  La source des données
&lt;/h2&gt;

&lt;p&gt;Le projet utilise le dataset public :&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liste des festivals en France&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Source : data.gouv.fr / Ministère de la Culture&lt;br&gt;&lt;br&gt;
URL : &lt;a href="https://www.data.gouv.fr/datasets/liste-des-festivals-en-france" rel="noopener noreferrer"&gt;https://www.data.gouv.fr/datasets/liste-des-festivals-en-france&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le dataset est disponible dans plusieurs formats :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSV&lt;/li&gt;
&lt;li&gt;JSON&lt;/li&gt;
&lt;li&gt;GeoJSON&lt;/li&gt;
&lt;li&gt;ZIP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pour une carte interactive, le format le plus pratique est évidemment le &lt;strong&gt;GeoJSON&lt;/strong&gt;, car il contient directement les coordonnées géographiques des festivals.&lt;/p&gt;

&lt;p&gt;Dans le projet, je récupère les données côté serveur, puis je les normalise dans un format interne plus simple à utiliser côté front.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack technique
&lt;/h2&gt;

&lt;p&gt;Le projet est construit avec :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shadcn/ui&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Leaflet&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenStreetMap&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zod&lt;/strong&gt; pour sécuriser la donnée&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; pour le déploiement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L’idée était de garder une stack moderne, rapide à itérer, mais assez robuste pour gérer plusieurs milliers de points sur une carte.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture générale
&lt;/h2&gt;

&lt;p&gt;Le projet est séparé en plusieurs couches :&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
txt
app/
  page.tsx
  festivals/
    [slug]/
      page.tsx

components/
  festivals/
    FestivalCard.tsx
    FestivalFilters.tsx
    FestivalList.tsx
    FestivalCover.tsx

  map/
    FestivalMap.tsx
    MapPopup.tsx
    MapControls.tsx

lib/
  data-gouv.ts
  normalize-festival.ts
  filters.ts
  distance.ts
  slugify.ts

types/
  festival.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>nextjs</category>
      <category>opendata</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>J’ai créé Plageo : une carte moderne pour explorer les plages et sites de baignade en France</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:25:26 +0000</pubDate>
      <link>https://dev.to/guillaumesere/jai-cree-plageo-une-carte-moderne-pour-explorer-les-plages-et-sites-de-baignade-en-france-5aa9</link>
      <guid>https://dev.to/guillaumesere/jai-cree-plageo-une-carte-moderne-pour-explorer-les-plages-et-sites-de-baignade-en-france-5aa9</guid>
      <description>&lt;h1&gt;
  
  
  J’ai créé Plageo : une carte moderne pour explorer les plages et sites de baignade en France
&lt;/h1&gt;

&lt;p&gt;Je viens de mettre en ligne &lt;strong&gt;Plageo&lt;/strong&gt;, une application web qui permet d’explorer les plages et sites de baignade officiels en France.&lt;/p&gt;

&lt;p&gt;👉 Démo : &lt;a href="https://localisation-plages.vercel.app/" rel="noopener noreferrer"&gt;https://localisation-plages.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;L’idée est simple : partir de données publiques fiables, les rendre plus accessibles, puis proposer une expérience moderne autour de la recherche, de la carte, de la géolocalisation et des itinéraires.&lt;/p&gt;

&lt;p&gt;Aujourd’hui, Plageo référence &lt;strong&gt;3 409 sites officiels&lt;/strong&gt;, avec &lt;strong&gt;3 409 points cartographiés&lt;/strong&gt; et &lt;strong&gt;18 régions couvertes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy2i14gnb1cll0xcz1t53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy2i14gnb1cll0xcz1t53.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi ce projet ?
&lt;/h2&gt;

&lt;p&gt;Quand on cherche une plage ou un site de baignade, on tombe souvent sur des listes peu pratiques, des cartes incomplètes ou des résultats très orientés tourisme.&lt;/p&gt;

&lt;p&gt;Je voulais créer une interface plus directe :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;chercher une plage, une commune ou un département ;&lt;/li&gt;
&lt;li&gt;visualiser les sites sur une carte ;&lt;/li&gt;
&lt;li&gt;distinguer les ambiances : mer, lac, rivière ;&lt;/li&gt;
&lt;li&gt;accéder rapidement à une fiche ;&lt;/li&gt;
&lt;li&gt;lancer un itinéraire depuis sa position ;&lt;/li&gt;
&lt;li&gt;utiliser des données publiques plutôt qu’un listing fermé.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le but n’est pas seulement de faire un annuaire, mais de transformer un dataset public en vraie expérience utilisateur.&lt;/p&gt;




&lt;h2&gt;
  
  
  La source des données
&lt;/h2&gt;

&lt;p&gt;Le projet utilise le jeu de données public suivant :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.data.gouv.fr/datasets/donnees-de-rapportage-de-la-saison-balneaire-1" rel="noopener noreferrer"&gt;https://www.data.gouv.fr/datasets/donnees-de-rapportage-de-la-saison-balneaire-1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ce dataset correspond aux &lt;strong&gt;données de rapportage de la saison balnéaire&lt;/strong&gt;. Il contient notamment :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;les sites de baignade ;&lt;/li&gt;
&lt;li&gt;les coordonnées géographiques ;&lt;/li&gt;
&lt;li&gt;l’origine de l’eau ;&lt;/li&gt;
&lt;li&gt;des informations sanitaires ;&lt;/li&gt;
&lt;li&gt;des classements ou données de qualité lorsque disponibles ;&lt;/li&gt;
&lt;li&gt;des informations liées à la saison balnéaire.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Point important : ce dataset ne contient pas uniquement des plages de bord de mer. Il référence des &lt;strong&gt;sites de baignade officiels&lt;/strong&gt;, ce qui inclut aussi des lacs, rivières, plans d’eau et bases de loisirs.&lt;/p&gt;

&lt;p&gt;C’est pour cela que l’interface parle de &lt;strong&gt;plages et sites de baignade&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack technique
&lt;/h2&gt;

&lt;p&gt;Pour cette première version, j’ai choisi une stack moderne et assez classique pour un produit web interactif :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; avec App Router ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;composants UI modernes ;&lt;/li&gt;
&lt;li&gt;carte interactive basée sur &lt;strong&gt;OpenStreetMap&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;déploiement sur &lt;strong&gt;Vercel&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;données issues de &lt;strong&gt;data.gouv.fr&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;logique de géolocalisation côté navigateur ;&lt;/li&gt;
&lt;li&gt;liens d’itinéraire vers Google Maps, Apple Plans et OpenStreetMap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L’objectif était d’avoir une application rapide, responsive et agréable à utiliser, sans dépendre d’une API payante pour la carte.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture générale
&lt;/h2&gt;

&lt;p&gt;L’application est organisée autour de trois grands blocs :&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
txt
/app
  page.tsx
  plages/
    page.tsx
    [id]/
      page.tsx

/components
  layout/
  beaches/
  map/
  ui/

/lib
  data-gouv.ts
  beaches.ts
  geo.ts
  routes.ts
  images.ts
  utils.ts

/types
  beach.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>opendata</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building ArchiveFlix: A Free Movie Streaming Platform with Next.js 15</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Thu, 11 Jun 2026 07:57:10 +0000</pubDate>
      <link>https://dev.to/guillaumesere/building-archiveflix-a-free-movie-streaming-platform-with-nextjs-15-1g1k</link>
      <guid>https://dev.to/guillaumesere/building-archiveflix-a-free-movie-streaming-platform-with-nextjs-15-1g1k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fist1dazf5klv0rfv6pkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fist1dazf5klv0rfv6pkw.png" alt=" " width="800" height="384"&gt;&lt;/a&gt;# Building ArchiveFlix: A Free Movie Streaming Platform with Next.js 15&lt;/p&gt;

&lt;p&gt;Recently, I built &lt;strong&gt;ArchiveFlix&lt;/strong&gt;, a web application that allows users to discover and watch public-domain movies available through Internet Archive.&lt;/p&gt;

&lt;p&gt;The goal was to create a modern streaming experience inspired by popular video platforms while relying entirely on freely accessible content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Browse a catalog of public-domain movies&lt;/li&gt;
&lt;li&gt;Search movies instantly&lt;/li&gt;
&lt;li&gt;Save favorites&lt;/li&gt;
&lt;li&gt;Watch movies directly in the browser&lt;/li&gt;
&lt;li&gt;Responsive design&lt;/li&gt;
&lt;li&gt;SEO optimization with sitemap, robots.txt, and metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Internet Archive API&lt;/li&gt;
&lt;li&gt;Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Retrieving Movie Data
&lt;/h2&gt;

&lt;p&gt;One of the most interesting parts of the project was working with Internet Archive's APIs.&lt;/p&gt;

&lt;p&gt;To retrieve the movie catalog, I used the Advanced Search API:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://archive.org/advancedsearch.php?q=mediatype:movies&amp;amp;fl[]=identifier&amp;amp;fl[]=title&amp;amp;rows=24&amp;amp;output=json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Each movie can then be queried individually using the metadata endpoint:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://archive.org/metadata/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Description&lt;/li&gt;
&lt;li&gt;Creator&lt;/li&gt;
&lt;li&gt;Runtime&lt;/li&gt;
&lt;li&gt;Release year&lt;/li&gt;
&lt;li&gt;Video files&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building the Video Player
&lt;/h2&gt;

&lt;p&gt;After retrieving metadata, the application automatically extracts the available MP4 video file and displays it through a native HTML5 player.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;video&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the implementation simple while supporting all modern browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO Optimization
&lt;/h2&gt;

&lt;p&gt;Since the project contains many movie pages, SEO was an important consideration.&lt;/p&gt;

&lt;p&gt;The application includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic metadata&lt;/li&gt;
&lt;li&gt;Open Graph tags&lt;/li&gt;
&lt;li&gt;Twitter cards&lt;/li&gt;
&lt;li&gt;Dynamic sitemap generation&lt;/li&gt;
&lt;li&gt;robots.txt&lt;/li&gt;
&lt;li&gt;Google Search Console verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ArchiveFlix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Watch public-domain movies online for free.&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;h2&gt;
  
  
  Deployment Challenges
&lt;/h2&gt;

&lt;p&gt;While everything worked perfectly in development, I encountered timeout issues after deploying to Vercel.&lt;/p&gt;

&lt;p&gt;The issue came from requests to Internet Archive's metadata endpoints occasionally timing out.&lt;/p&gt;

&lt;p&gt;To improve reliability, I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error handling with try/catch&lt;/li&gt;
&lt;li&gt;ISR caching using &lt;code&gt;revalidate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fallback states when requests fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This significantly reduced the number of external requests and improved performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;ArchiveFlix was a fun project that allowed me to work with external APIs, dynamic content, SEO optimization, and video streaming in Next.js.&lt;/p&gt;

&lt;p&gt;The project demonstrates how publicly available content can be transformed into a modern user experience using today's web technologies.&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts and feedback.&lt;/p&gt;

&lt;p&gt;🚀 Live Demo: &lt;a href="https://archiveflix-60.vercel.app" rel="noopener noreferrer"&gt;https://archiveflix-60.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  nextjs #react #typescript #webdev #opensource #javascript #frontend #seo
&lt;/h1&gt;

</description>
      <category>nextjs</category>
      <category>api</category>
      <category>webdev</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>🎬 I Built a Universal Video Player for HLS, DASH &amp; MP4 (Now with Subtitles, Fullscreen &amp; Quality Control)</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Mon, 04 May 2026 12:08:44 +0000</pubDate>
      <link>https://dev.to/guillaumesere/i-built-a-universal-video-player-that-supports-hls-dash-mp4-works-on-smart-tvs-2io6</link>
      <guid>https://dev.to/guillaumesere/i-built-a-universal-video-player-that-supports-hls-dash-mp4-works-on-smart-tvs-2io6</guid>
      <description>&lt;p&gt;Working with video on the web sounds simple…&lt;/p&gt;

&lt;p&gt;Until you actually try it.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  😩 The Problem
&lt;/h2&gt;

&lt;p&gt;If you've ever built a video player, you know this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MP4 works natively&lt;/li&gt;
&lt;li&gt;HLS (.m3u8) needs special handling&lt;/li&gt;
&lt;li&gt;DASH (.mpd) requires another library&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you want to support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💻 Desktop&lt;/li&gt;
&lt;li&gt;📱 Mobile&lt;/li&gt;
&lt;li&gt;📺 Smart TVs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 it quickly becomes a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 So I Built My Own Solution
&lt;/h2&gt;

&lt;p&gt;Yesterday, I created:&lt;/p&gt;

&lt;p&gt;👉 universal-video-player&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/universal-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/universal-video-player&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lightweight JavaScript/TypeScript video player that supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MP4&lt;/li&gt;
&lt;li&gt;HLS&lt;/li&gt;
&lt;li&gt;DASH&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All with one simple API.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ The Goal
&lt;/h2&gt;

&lt;p&gt;Instead of juggling multiple libraries…&lt;/p&gt;

&lt;p&gt;You just do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;VideoPlayer&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;universal-video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoPlayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video.m3u8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;autoplay&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="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💥 That’s it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎥 Multi-Format Support
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  ⚡ Simple Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoPlayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;autoplay&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="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 New Features Added
&lt;/h2&gt;

&lt;p&gt;I’ve now added several advanced features to make the player production-ready:&lt;/p&gt;

&lt;p&gt;✅ WebVTT subtitles support&lt;br&gt;
✅ Fullscreen API&lt;br&gt;
✅ Events system (play, pause, etc.)&lt;br&gt;
✅ Custom controls support&lt;br&gt;
✅ HLS quality selector&lt;/p&gt;

&lt;h2&gt;
  
  
  🎥 HLS Streaming Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoPlayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;autoplay&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="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📝 Subtitles Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubtitles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subtitles.vtt&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;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📺 Fullscreen Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullscreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎮 Events API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;play&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Video started&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;h2&gt;
  
  
  ⚡ Quality Selector (HLS)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQuality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📺 Smart TV Compatible
&lt;/h2&gt;

&lt;p&gt;One of the main goals was compatibility.&lt;/p&gt;

&lt;p&gt;👉 This player works on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Samsung Tizen (browser)&lt;/li&gt;
&lt;li&gt;LG WebOS&lt;/li&gt;
&lt;li&gt;Android TV&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As long as a browser is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 How It Works
&lt;/h2&gt;

&lt;p&gt;Internally, the player automatically detects the format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.mp4 → native HTML5 video&lt;/li&gt;
&lt;li&gt;.m3u8 → HLS via hls.js&lt;/li&gt;
&lt;li&gt;.mpd → DASH via dash.js&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 You don’t have to care about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Why This Matters
&lt;/h2&gt;

&lt;p&gt;If you're building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming apps&lt;/li&gt;
&lt;li&gt;dashboards&lt;/li&gt;
&lt;li&gt;media platforms&lt;/li&gt;
&lt;li&gt;IPTV tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 this saves a huge amount of development time.&lt;/p&gt;

&lt;p&gt;No more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;switching libraries&lt;/li&gt;
&lt;li&gt;rewriting code&lt;/li&gt;
&lt;li&gt;handling edge cases manually&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 Current API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullscreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubtitles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQuality&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔥 Next Features
&lt;/h2&gt;

&lt;p&gt;Planned next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chromecast support&lt;/li&gt;
&lt;li&gt;AirPlay support&lt;/li&gt;
&lt;li&gt;Netflix-style UI&lt;/li&gt;
&lt;li&gt;Picture-in-picture&lt;/li&gt;
&lt;li&gt;React/Vue wrappers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📦 Try It
&lt;/h2&gt;

&lt;p&gt;npm install universal-video-player&lt;/p&gt;

&lt;h2&gt;
  
  
  💬 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;GIT: &lt;a href="https://github.com/GuillaumeSere/universal-video-player" rel="noopener noreferrer"&gt;https://github.com/GuillaumeSere/universal-video-player&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/universal-video-player" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/universal-video-player&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Video on the web shouldn't be this complicated.&lt;/p&gt;

&lt;p&gt;This is my attempt to make it simple.&lt;/p&gt;

&lt;p&gt;If you try it, I’d love your feedback 🙌&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>npm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>🚀 I Got Tired of AI APIs… So I Built My Own SDK in 24h (multi-ai-client)</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Thu, 23 Apr 2026 09:56:24 +0000</pubDate>
      <link>https://dev.to/guillaumesere/i-got-tired-of-ai-apis-so-i-built-my-own-sdk-in-24h-multi-ai-client-2ldl</link>
      <guid>https://dev.to/guillaumesere/i-got-tired-of-ai-apis-so-i-built-my-own-sdk-in-24h-multi-ai-client-2ldl</guid>
      <description>&lt;p&gt;Working with AI APIs&lt;br&gt;
 is powerful…&lt;br&gt;
but let’s be honest:&lt;/p&gt;

&lt;p&gt;👉 it’s also a mess.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI → one format&lt;/li&gt;
&lt;li&gt;Claude → another&lt;/li&gt;
&lt;li&gt;Gemini → completely different&lt;/li&gt;
&lt;li&gt;Mistral → again something else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time you switch provider, you rewrite everything.&lt;/p&gt;

&lt;p&gt;So yesterday, I decided to fix that.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 The Idea
&lt;/h2&gt;

&lt;p&gt;What if you could use one single API for all AI providers?&lt;br&gt;
No matter if it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI&lt;/li&gt;
&lt;li&gt;Claude (Anthropic)&lt;/li&gt;
&lt;li&gt;Gemini&lt;/li&gt;
&lt;li&gt;Mistral&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Same code. Same interface. Same logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚡ Meet multi-ai-client
&lt;/h2&gt;

&lt;p&gt;A lightweight unified AI SDK for JavaScript &amp;amp; TypeScript.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔥 The Goal
&lt;/h2&gt;

&lt;p&gt;Instead of this 👇&lt;/p&gt;

&lt;p&gt;// different code for each provider 😩&lt;/p&gt;

&lt;p&gt;You do this 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AIClient&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;multi-ai-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AIClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
 &lt;span class="p"&gt;})&lt;/span&gt; 

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Explain AI simply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💥 Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔌 Switching Providers = 1 Line
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AIClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&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 rewrite. No headache.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ Streaming (Real-Time Output)
&lt;/h2&gt;

&lt;p&gt;Modern AI apps need streaming.&lt;br&gt;
Here’s how simple it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tell me a story&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;h2&gt;
  
  
  🧠 Clean &amp;amp; Unified Output
&lt;/h2&gt;

&lt;p&gt;Every provider returns the same format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="err"&gt;content:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="err"&gt;done:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;👉 No parsing nightmares.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Why This Matters
&lt;/h2&gt;

&lt;p&gt;If you’ve built with AI APIs, you know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;switching providers = pain&lt;/li&gt;
&lt;li&gt;maintaining multiple integrations = worse&lt;/li&gt;
&lt;li&gt;streaming = inconsistent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 This SDK removes all that friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🤖 Multi-provider support (OpenAI, Claude, Gemini, Mistral)&lt;/li&gt;
&lt;li&gt;⚡ Unified API (chat() + stream())&lt;/li&gt;
&lt;li&gt;🔄 Real-time streaming (AsyncIterator)&lt;/li&gt;
&lt;li&gt;🧠 TypeScript support&lt;/li&gt;
&lt;li&gt;📦 Lightweight&lt;/li&gt;
&lt;li&gt;🔌 Extensible architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚙️ Advanced Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Explain AI&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;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; 
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏗️ Under the Hood
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AIClient 
├── OpenAI
├── Anthropic
├── Gemini
└── Mistral
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Each provider is isolated but exposed through one clean interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔥 Why I Built This
&lt;/h2&gt;

&lt;p&gt;I wanted something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple&lt;/li&gt;
&lt;li&gt;fast&lt;/li&gt;
&lt;li&gt;clean&lt;/li&gt;
&lt;li&gt;reusable across projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Basically: a tool I wouldn’t hate using&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Try It Yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;multi-ai-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧪 Real Use Case
&lt;/h2&gt;

&lt;p&gt;Switch providers dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt; &lt;span class="o"&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;openai&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;anthropic&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;gemini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

 &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;providers&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;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AIClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
 &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Explain quantum computing&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;h2&gt;
  
  
  🔮 What’s Coming Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;smarter streaming (token-level)&lt;/li&gt;
&lt;li&gt;retry system&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;plugin system&lt;/li&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  💬 Final Thought
&lt;/h2&gt;

&lt;p&gt;AI is evolving fast.&lt;/p&gt;

&lt;p&gt;But the developer experience? Not always.&lt;/p&gt;

&lt;p&gt;👉 multi-ai-client is my attempt to fix that.&lt;/p&gt;

&lt;p&gt;Link: &lt;a href="https://www.npmjs.com/package/multi-ai-client" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/multi-ai-client&lt;/a&gt;&lt;br&gt;
Git: &lt;a href="https://github.com/GuillaumeSere/multi-ai-client" rel="noopener noreferrer"&gt;https://github.com/GuillaumeSere/multi-ai-client&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find this useful, feedback is welcome 🙌&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>npm</category>
    </item>
    <item>
      <title>🧩 Building Mosaic Grid — A Rotation-Based Puzzle Game with Next.js</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Fri, 10 Apr 2026 14:58:44 +0000</pubDate>
      <link>https://dev.to/guillaumesere/building-mosaic-grid-a-rotation-based-puzzle-game-with-nextjs-4chb</link>
      <guid>https://dev.to/guillaumesere/building-mosaic-grid-a-rotation-based-puzzle-game-with-nextjs-4chb</guid>
      <description>&lt;p&gt;I recently built Mosaic Grid, a browser-based puzzle game focused on placement and rotation mechanics. In this post, I want to walk through the technical decisions, challenges, and architecture behind it.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://mosaic-grid.vercel.app/" rel="noopener noreferrer"&gt;https://mosaic-grid.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Concept
&lt;/h2&gt;

&lt;p&gt;The game is played on a 6x6 grid, where players must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag &amp;amp; drop pieces onto the board&lt;/li&gt;
&lt;li&gt;Rotate them in 90° increments&lt;/li&gt;
&lt;li&gt;Avoid overlaps&lt;/li&gt;
&lt;li&gt;Match an expected solution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At its core, it’s a mix of spatial reasoning and constraint solving.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js (App Router)&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;CSS Modules / Tailwind (depending on your setup)&lt;/li&gt;
&lt;li&gt;Custom state management (no heavy external libs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧠 Core Architecture
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Grid Representation
&lt;/h2&gt;

&lt;p&gt;The board is represented as a 2D array:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
&lt;span class="na"&gt;occupied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="nl"&gt;pieceId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="p"&gt;};&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cell&lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&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;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;occupied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Piece Modeling
&lt;/h2&gt;

&lt;p&gt;Each piece is defined as a matrix:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Piece&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[][];&lt;/span&gt;
  &lt;span class="nl"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0, 90, 180, 270&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nc"&gt;Example &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;x2&lt;/span&gt; &lt;span class="nx"&gt;block&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Rotation Logic
&lt;/h2&gt;

&lt;p&gt;Instead of storing multiple orientations, I compute rotations dynamically:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rotateMatrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[][]):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;matrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;reverse&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;Each click applies a 90° rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Drag &amp;amp; Drop System
&lt;/h2&gt;

&lt;p&gt;I implemented a custom drag &amp;amp; drop system instead of relying on libraries.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More control over snapping behavior&lt;/li&gt;
&lt;li&gt;Easier grid alignment&lt;/li&gt;
&lt;li&gt;Better performance for small UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track mouse position&lt;/li&gt;
&lt;li&gt;Translate piece preview in real-time&lt;/li&gt;
&lt;li&gt;Snap to nearest valid grid cell on drop&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Placement Validation
&lt;/h2&gt;

&lt;p&gt;Before placing a piece:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check boundaries&lt;/li&gt;
&lt;li&gt;Check collisions&lt;/li&gt;
&lt;li&gt;Apply special rules (e.g. 2x2 pieces must align on even coordinates)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;j&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;false&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;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;occupied&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;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Game State
&lt;/h2&gt;

&lt;p&gt;State is split into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grid&lt;/li&gt;
&lt;li&gt;pieces (with position + rotation)&lt;/li&gt;
&lt;li&gt;progress (correct vs incorrect placements)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I avoided global state libraries and used React state + lifting state up, which was sufficient for this scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 UX Details
&lt;/h2&gt;

&lt;p&gt;Some small but important features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Double-click to rotate placed pieces&lt;/li&gt;
&lt;li&gt;Click to rotate pieces in the palette&lt;/li&gt;
&lt;li&gt;Visual feedback:

&lt;ol&gt;
&lt;li&gt;🟢 Correct placement&lt;/li&gt;
&lt;li&gt;🔴 Incorrect placement&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Automatic “snap” to valid positions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧩 Challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Rotation + Collision Combined&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Handling rotation while ensuring valid placement required recalculating bounding boxes dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Snapping System&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Making the drag feel natural without jitter or misalignment took several iterations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Edge Cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partial out-of-bounds placement&lt;/li&gt;
&lt;li&gt;Rotated shapes exceeding grid limits&lt;/li&gt;
&lt;li&gt;Special constraints for certain pieces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 What’s Next
&lt;/h2&gt;

&lt;p&gt;Possible improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Procedural level generation&lt;/li&gt;
&lt;li&gt;Mobile touch support improvements&lt;/li&gt;
&lt;li&gt;Animations (Framer Motion)&lt;/li&gt;
&lt;li&gt;Score / timer system&lt;/li&gt;
&lt;li&gt;Multiplayer or daily challenges&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  💬 Feedback
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mosaic-grid.vercel.app/" rel="noopener noreferrer"&gt;https://mosaic-grid.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re into puzzle games or frontend architecture, I’d love your feedback!&lt;/p&gt;

&lt;p&gt;What would you improve in this kind of system?&lt;br&gt;
How would you handle state or performance differently?&lt;/p&gt;

&lt;h1&gt;
  
  
  webdev #nextjs #react #gamedev #javascript #indiedev
&lt;/h1&gt;

</description>
      <category>nextjs</category>
      <category>ui</category>
      <category>ux</category>
      <category>javascript</category>
    </item>
    <item>
      <title>🌍 Building “Ma Zone” — A Geolocated Events Platform with Next.js</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Fri, 06 Feb 2026 11:51:02 +0000</pubDate>
      <link>https://dev.to/guillaumesere/building-ma-zone-a-geolocated-events-platform-with-nextjs-2b6k</link>
      <guid>https://dev.to/guillaumesere/building-ma-zone-a-geolocated-events-platform-with-nextjs-2b6k</guid>
      <description>&lt;p&gt;I recently built Ma Zone, a web application that helps users discover events near their location and around the world.&lt;br&gt;
The goal was to work on real-world challenges like geolocation, third-party APIs, maps, and SEO in a modern full-stack React environment.&lt;/p&gt;

&lt;p&gt;Here’s a breakdown of the technical side of the project 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7a1iddd6slisa8t8pwz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7a1iddd6slisa8t8pwz.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ma-zone-sortie.vercel.app/" rel="noopener noreferrer"&gt;https://ma-zone-sortie.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🧱 Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Next.js (App Router)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React + TypeScript&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Leaflet + React-Leaflet for interactive maps&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;External Event APIs (geolocated event data)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Server-side data fetching with caching control&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SEO optimization using Next.js Metadata API&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🗺️ 1. Handling Maps in Next.js (SSR Challenge)
&lt;/h2&gt;

&lt;p&gt;Leaflet depends on the window object, which doesn’t exist during server-side rendering.&lt;/p&gt;

&lt;p&gt;To avoid the classic:&lt;/p&gt;

&lt;p&gt;ReferenceError: window is not defined&lt;/p&gt;

&lt;p&gt;I isolated the map into a Client Component and loaded it dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;dynamic&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;next/dynamic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./Map&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;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;MapWrapper&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt; &lt;span class="o"&gt;/&amp;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;h2&gt;
  
  
  📍 2. User Geolocation
&lt;/h2&gt;

&lt;p&gt;The app requests the user’s position via the browser API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&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="nf"&gt;setCoords&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// fallback location (Paris)&lt;/span&gt;
    &lt;span class="nf"&gt;setCoords&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.8566&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.3522&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;These coordinates are then used to fetch nearby events.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 3. Fetching Geolocated Events from APIs
&lt;/h2&gt;

&lt;p&gt;On the server side, I aggregate event data from multiple providers and normalize the structure:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&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;Data is fetched with cache: "no-store" to ensure fresh event listings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧭 4. Displaying Events on the Map
&lt;/h2&gt;

&lt;p&gt;Each event becomes a marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Popup&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Popup&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This creates a visual discovery experience instead of a simple list.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ 5. SEO with Next.js Metadata API
&lt;/h2&gt;

&lt;p&gt;Even though the app is dynamic and map-heavy, SEO still matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ma Zone – Events near you&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Discover concerts, festivals and local events around you.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ma Zone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Find events near you&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;website&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;This ensures proper indexing and rich previews when shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Key Challenges Solved
&lt;/h2&gt;

&lt;p&gt;✔ Mixing client-only libraries (Leaflet) with SSR&lt;br&gt;
✔ Handling browser geolocation safely&lt;br&gt;
✔ Normalizing data from different event APIs&lt;br&gt;
✔ Building a map-based UX instead of a traditional feed&lt;br&gt;
✔ Keeping performance + SEO in a hybrid Next.js app&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 What’s Next
&lt;/h2&gt;

&lt;p&gt;Planned improvements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Event detail pages with dynamic metadata&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filters (date, category, free/paid)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Favorites system&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Smarter location-based suggestions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link: &lt;a href="https://ma-zone-60.vercel.app/" rel="noopener noreferrer"&gt;https://ma-zone-60.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbel4ff9hpr20mhorgdk7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbel4ff9hpr20mhorgdk7.png" alt=" " width="690" height="916"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fkjal6d0uczarcpu6bm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fkjal6d0uczarcpu6bm.png" alt=" " width="687" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This project was a great way to explore real-world geospatial data, API aggregation, and modern Next.js architecture.&lt;/p&gt;

&lt;p&gt;If you're building something similar or working with maps + Next.js, I’d love to exchange ideas!&lt;/p&gt;

&lt;h1&gt;
  
  
  nextjs #react #typescript #webdev #frontend #maps #geolocation #seo
&lt;/h1&gt;

</description>
      <category>nextjs</category>
      <category>api</category>
      <category>webdev</category>
      <category>map</category>
    </item>
    <item>
      <title>World Radio 60 : explorer les radios du monde entier depuis votre navigateur 🎧🌍</title>
      <dc:creator>Guillaume Sere</dc:creator>
      <pubDate>Fri, 19 Dec 2025 17:02:24 +0000</pubDate>
      <link>https://dev.to/guillaumesere/world-radio-60-explorer-les-radios-du-monde-entier-depuis-votre-navigateur-3p44</link>
      <guid>https://dev.to/guillaumesere/world-radio-60-explorer-les-radios-du-monde-entier-depuis-votre-navigateur-3p44</guid>
      <description>&lt;p&gt;J’ai récemment terminé un projet personnel que je voulais partager : World Radio 60, un site web qui permet d’écouter et de découvrir des radios du monde entier, avec une interface simple et intuitive.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Pourquoi ce projet ?
&lt;/h2&gt;

&lt;p&gt;Je voulais créer une plateforme où l’utilisateur peut :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;parcourir les radios par pays et voir le drapeau correspondant ;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;filtrer par ville, pays ou nom de radio ;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;écouter un flux audio en direct directement depuis le site ;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;bénéficier d’une navigation fluide et rapide, même avec des centaines de radios.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack et choix techniques
&lt;/h2&gt;

&lt;p&gt;Pour ce projet, j’ai utilisé:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Next.js 13 (App Router) pour le rendu côté serveur et la génération de pages dynamiques.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React pour construire une interface réactive et interactive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TypeScript pour plus de sécurité sur les types et éviter les erreurs courantes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API interne pour contourner les problèmes de CORS et récupérer les flux des radios.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tailwind CSS pour un style moderne, responsive et rapide à mettre en place.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comment ça fonctionne ?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Le site récupère les lieux (places) depuis l’API Radio.Garden.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Les lieux sont regroupés par pays, avec une limite de villes pour optimiser les performances.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pour chaque lieu, les channels sont récupérés via un endpoint interne Next.js.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;L’utilisateur peut filtrer les radios par pays ou rechercher une station spécifique.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Un lecteur audio permet d’écouter le flux en direct.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Optimisations
&lt;/h2&gt;

&lt;p&gt;🔒 Verrou anti-double fetch pour éviter les requêtes infinies en React Strict Mode.&lt;/p&gt;

&lt;p&gt;⚡ Lazy loading et limites par pays pour réduire le temps de chargement.&lt;/p&gt;

&lt;p&gt;✅ Gestion des erreurs et fallback pour les flux indisponibles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Résultat
&lt;/h2&gt;

&lt;p&gt;World Radio 60 est maintenant en ligne et permet de parcourir facilement les radios de tous les continents, tout en offrant une interface simple et rapide.&lt;/p&gt;

&lt;p&gt;🌐 Découvrez-le ici : &lt;a href="https://world-radio-6z.vercel.app/" rel="noopener noreferrer"&gt;https://world-radio-6z.vercel.app/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
