<?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: Younes Laaroussi</title>
    <description>The latest articles on DEV Community by Younes Laaroussi (@youneslaaroussi).</description>
    <link>https://dev.to/youneslaaroussi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F833086%2F2f9e14a5-ab79-422a-95bd-c7c267c319e7.png</url>
      <title>DEV Community: Younes Laaroussi</title>
      <link>https://dev.to/youneslaaroussi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/youneslaaroussi"/>
    <language>en</language>
    <item>
      <title>Augmenting Phantom With Auth0 Authority</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Fri, 03 Apr 2026 21:05:39 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/augmenting-phantom-with-auth0-authority-360a</link>
      <guid>https://dev.to/youneslaaroussi/augmenting-phantom-with-auth0-authority-360a</guid>
      <description>&lt;p&gt;Phantom already knew how to listen, see the browser, and act. The real challenge was turning that local agent into a system that could use connected accounts, delegate safely, and expose authority instead of hiding it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Younes Laaroussi&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Phantom started as a browser-native agent. You talk, it reacts, and the extension takes care of the visible work: reading the page, clicking, scrolling, opening tabs, and moving through the browser with almost no friction. That part was already compelling. What was missing was a trustworthy answer to a more serious question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gives it the right to act for the user outside the tab?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question changes the architecture immediately. A browser extension can hold state, but it is the wrong place to improvise long-lived authority. As soon as Phantom started reaching into Gmail, Calendar, Docs, Sheets, Tasks, and Linear, “the extension has access” stopped sounding clever and started sounding irresponsible.&lt;/p&gt;

&lt;p&gt;The project became more interesting when I stopped treating authorization as a detail. Instead of asking how to cram more power into the local agent, I asked how to split the system so the local part remained fast and intimate while the authority layer became visible, scoped, and reviewable.&lt;/p&gt;

&lt;p&gt;That is where Auth0 changed the direction of the product. Phantom did not stop being a local browser companion. It stopped pretending that local execution should also be the source of truth for identity, connected accounts, provider access, and approval.&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%2F48egr8biec1x7th63fx1.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%2F48egr8biec1x7th63fx1.png" alt="Compact Phantom Auth0 runtime architecture diagram" width="784" height="949"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runtime split. The extension stays local. The hosted companion and gateway become the authority surface. Auth0 owns connected accounts, delegated access, and approval. Providers stay downstream instead of being smuggled into browser state.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth0 turned Phantom into a delegated system instead of a clever extension
&lt;/h2&gt;

&lt;p&gt;The first concrete shift was connected accounts. Provider login should not be a side effect of whatever tab the user happens to have open. With Auth0 Token Vault, Phantom can connect Google and Linear through a proper authority layer, which means the browser extension no longer needs to own raw provider credentials or fake its way through session cookies and copied tokens.&lt;/p&gt;

&lt;p&gt;The second shift was visibility. The companion is not there just to look nice. It is the surface that tells the truth about the system: which accounts are connected, which actions are pending, which high-risk operations need approval, and what has already happened. That interface matters because it translates invisible authorization state into something a user can actually inspect.&lt;/p&gt;

&lt;p&gt;The third shift was approval. Reads can stay lightweight. Mutating actions should not. Creating calendar events, sending mail, and opening real work in external systems should step up into explicit approval instead of feeling like silent model behavior. Auth0 Guardian gives that moment a real boundary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The product got better when it became less magical and more legible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s the core of what changed:&lt;/p&gt;

&lt;h3&gt;
  
  
  Token Vault
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Provider authority moved out of the extension.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google and Linear now sit behind Auth0’s connected-account flow instead of leaking into the browser runtime as an implementation shortcut.&lt;/p&gt;

&lt;h3&gt;
  
  
  Companion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The system exposes status instead of hiding it in prompts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Connected accounts, pending approvals, and delegated action history are all visible in one place, which makes the app easier to trust and easier to demo honestly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guardian
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;High-risk actions gained a real consent boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Approval is no longer a narrative flourish. It is an actual gate in the runtime, tied to the user and reflected in the system’s action state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part is not that Phantom can act. It is how those actions travel.
&lt;/h2&gt;

&lt;p&gt;Once the agent asks to do something meaningful, the request moves through a chain that makes sense. The extension initiates the action. The gateway performs provider-specific work. Auth0 sits in the middle as the authority layer. The companion shows the resulting action record. If approval is required, that request becomes visible before the mutating step can finish.&lt;/p&gt;

&lt;p&gt;That gives Phantom a much stronger operating model than “the AI did something in my browser.” It becomes possible to say exactly what happened, where it happened, and why the system was allowed to proceed.&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%2Fhl8gvxw46jrkg2c438u8.jpeg" 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%2Fhl8gvxw46jrkg2c438u8.jpeg" alt="Delegated action lanes diagram" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Delegated action flow. The browser agent initiates work locally. The hosted gateway handles provider calls. Results return as action records rather than disappearing into conversational state.&lt;/em&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%2Fz4kliflv7k1l34x61cqs.jpeg" 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%2Fz4kliflv7k1l34x61cqs.jpeg" alt="Extension, Auth0, and provider lanes diagram" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Authority lanes. Phantom remains the local interface, but Auth0 becomes the durable authority layer between the agent and downstream providers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-up approval is what makes the product feel responsible
&lt;/h2&gt;

&lt;p&gt;A useful agent should not force the same friction on every action, but it should absolutely create friction when the user’s behalf is being asserted in a consequential way. That is the point where Auth0 stops being a backend convenience and becomes product substance.&lt;/p&gt;

&lt;p&gt;The approval loop matters because it is easy to explain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user asks for an action.&lt;/li&gt;
&lt;li&gt;Phantom prepares it.&lt;/li&gt;
&lt;li&gt;Auth0 requests approval.&lt;/li&gt;
&lt;li&gt;The user approves on their own device.&lt;/li&gt;
&lt;li&gt;The action completes.&lt;/li&gt;
&lt;li&gt;The companion records the outcome.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence is a lot less flashy than an unconstrained agent, but it is much closer to something people could actually live with.&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%2F3onskem31jlwgyb1dp3t.jpeg" 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%2F3onskem31jlwgyb1dp3t.jpeg" alt="Guardian QR approval flow diagram" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Approval loop. The system becomes more trustworthy when the approval step is visible, attributable, and tied to the final action record instead of being treated like invisible middleware.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this architecture feels right
&lt;/h2&gt;

&lt;p&gt;What I like most about the final shape is that it keeps both halves of the idea intact. Phantom still feels like a local companion. It speaks, watches, and acts in the browser without turning into a remote dashboard. But the authority behind it is no longer improvised. Identity, connected accounts, delegated access, and approval now live in the right place.&lt;/p&gt;

&lt;p&gt;That split is the real product.&lt;/p&gt;

&lt;p&gt;Local execution gives the system immediacy. Auth0 gives it discipline. One without the other would either feel weak or unsafe. Together they make a browser agent that can do real work without pretending that permission is somebody else’s problem.&lt;/p&gt;

&lt;p&gt;Phantom is still the same spirit at the edge of the browser. The difference is that now it has a visible authority model behind it. That is what turned it from a neat interaction demo into a system I can explain with a straight face.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;Live site: &lt;a href="https://phantom-auth0-server-pio3n3nsna-uc.a.run.app" rel="noopener noreferrer"&gt;https://phantom-auth0-server-pio3n3nsna-uc.a.run.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion: &lt;a href="https://phantom-auth0-server-pio3n3nsna-uc.a.run.app/companion" rel="noopener noreferrer"&gt;https://phantom-auth0-server-pio3n3nsna-uc.a.run.app/companion&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/youneslaaroussi/phantom-auth0" rel="noopener noreferrer"&gt;https://github.com/youneslaaroussi/phantom-auth0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>auth0challenge</category>
      <category>ai</category>
      <category>security</category>
      <category>extensions</category>
    </item>
    <item>
      <title>I Built a Browser AI Agent in One Session — Here's What Happened</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sat, 14 Mar 2026 18:21:02 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/i-built-a-browser-ai-agent-in-one-session-heres-what-happened-4n1l</link>
      <guid>https://dev.to/youneslaaroussi/i-built-a-browser-ai-agent-in-one-session-heres-what-happened-4n1l</guid>
      <description>&lt;p&gt;&lt;em&gt;This article was created for the purposes of entering the Gemini Live Agent Challenge hackathon. #GeminiLiveAgentChallenge&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;What if your browser had a friend? Not a chatbot. Not an assistant. A little spirit that floats next to your cursor, listens to your voice, watches your screen, and just... does things for you.&lt;/p&gt;

&lt;p&gt;That's Phantom. And the weirdest part isn't what it does — it's how it got built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise was simple
&lt;/h2&gt;

&lt;p&gt;I wanted to talk to my browser. Not type commands into a terminal. Not click through menus. Just say "open YouTube and search for lo-fi music" and have it happen.&lt;/p&gt;

&lt;p&gt;The Gemini Live API made this possible — real-time bidirectional audio over WebSockets, with function calling baked in. The model can listen, talk back, AND execute tools, all in the same stream. No polling. No turn-based nonsense. Just a live conversation where the AI can actually do things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody talks about: building speed
&lt;/h2&gt;

&lt;p&gt;Here's where it gets meta. I used Gemini as my coding agent throughout the entire build. Not just for boilerplate — for architecture decisions, debugging WebSocket frame formats, generating deployment scripts, even creating the mascot art.&lt;/p&gt;

&lt;p&gt;The whole project — Chrome extension, Cloud Run proxy, landing page, 8 persona system, sound design, animated sprites, onboarding flow, trace debugger — was built in a single extended session. One human, one AI, rapid-fire iteration.&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%2F0pmbximiv3ic0jjr2rpj.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%2F0pmbximiv3ic0jjr2rpj.png" alt="Phantom Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;Phantom is a Chrome extension (built with Plasmo) that opens a side panel. When you tap the mic button, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Opens a WebSocket to the Gemini Live API (either directly with your key, or through our Cloud Run proxy)&lt;/li&gt;
&lt;li&gt;Streams your microphone audio as PCM at 16kHz&lt;/li&gt;
&lt;li&gt;Receives spoken responses AND function calls in the same stream&lt;/li&gt;
&lt;li&gt;Executes browser tools — clicking, typing, scrolling, navigating tabs&lt;/li&gt;
&lt;li&gt;Optionally streams your screen at 1 FPS so the model can see what you see&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight: Gemini Live's &lt;code&gt;realtimeInput&lt;/code&gt; lets you send audio and video frames simultaneously. The model processes them together. So when you say "click the blue button," it can actually see the blue button in the video stream and figure out which element you mean.&lt;/p&gt;

&lt;h3&gt;
  
  
  The proxy problem
&lt;/h3&gt;

&lt;p&gt;Free API keys have rate limits. We rotate through multiple keys on the server side, and the Cloud Run proxy handles the WebSocket relay. The client never sees the API key.&lt;/p&gt;

&lt;p&gt;One painful discovery: when proxying WebSocket frames through Node.js, the &lt;code&gt;ws&lt;/code&gt; library's default &lt;code&gt;maxPayload&lt;/code&gt; silently drops large messages. Our 100KB JPEG frames were vanishing. A one-line fix (&lt;code&gt;maxPayload: 10 * 1024 * 1024&lt;/code&gt;) solved hours of "why is the model hallucinating what's on screen."&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%2Fotqb33fqu1y1ishy2u9r.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%2Fotqb33fqu1y1ishy2u9r.png" alt="Voice Interaction Loop"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The tool system
&lt;/h3&gt;

&lt;p&gt;The model has access to 14 browser tools via Gemini's function calling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Navigation&lt;/strong&gt;: openTab, getTabs, switchTab, getPageTitle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaction&lt;/strong&gt;: clickOn, typeInto, pressKey, highlightElement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspection&lt;/strong&gt;: getAccessibilitySnapshot, findElements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Movement&lt;/strong&gt;: scrollDown, scrollUp, scrollToElement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each tool plays its own sound effect (generated via ElevenLabs' SFX API) — a soft whoosh for navigation, crystal clicks for typing, gentle chimes for success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy Shield: What the AI Never Sees
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth about screen-sharing AI agents: they see everything. Your passwords. Your credit cards. Your API keys. Every token, every secret, every SSN on screen — all of it gets sent as JPEG frames to a remote model.&lt;/p&gt;

&lt;p&gt;We built &lt;strong&gt;Privacy Shield&lt;/strong&gt; to fix this.&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%2Fegh05mcyv6dcgmrarbqq.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%2Fegh05mcyv6dcgmrarbqq.png" alt="Privacy Shield Pipeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Before every single frame capture (once per second), Phantom injects a script into the active page that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scans the DOM&lt;/strong&gt; for sensitive inputs — password fields, credit card inputs, anything with &lt;code&gt;autocomplete="cc-number"&lt;/code&gt;, inputs named &lt;code&gt;ssn&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;api_key&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scans visible text&lt;/strong&gt; for PII patterns — credit card numbers, Social Security numbers, API keys (Google, OpenAI, AWS, ElevenLabs), bearer tokens, private keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applies a CSS blur&lt;/strong&gt; to every match&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Captures the screenshot&lt;/strong&gt; — the JPEG now has sensitive content blurred&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removes the blur&lt;/strong&gt; instantly — the user never sees it (~30ms round trip)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: Gemini sees your screen, but never sees your secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it catches
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passwords&lt;/td&gt;
&lt;td&gt;All &lt;code&gt;type="password"&lt;/code&gt; inputs, login forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credit cards&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;4111-1111-1111-1111&lt;/code&gt; style numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSNs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;123-45-6789&lt;/code&gt; format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API keys&lt;/td&gt;
&lt;td&gt;Google (&lt;code&gt;AIza...&lt;/code&gt;), OpenAI (&lt;code&gt;sk-...&lt;/code&gt;), AWS (&lt;code&gt;AKIA...&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;Bearer tokens, private keys, generic secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form context&lt;/td&gt;
&lt;td&gt;Any input inside a &lt;code&gt;/login&lt;/code&gt; or &lt;code&gt;/payment&lt;/code&gt; form action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Zero latency, zero dependencies
&lt;/h3&gt;

&lt;p&gt;No API calls. No cloud services. No model inference. Pure DOM analysis + regex, running in under 5ms per frame. For production, this could be augmented with Google Cloud DLP's 150+ infoType detectors — but for real-time 1 FPS streaming, the deterministic approach is faster and more reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;Every screen-sharing AI tool should have this. Most don't. We tested Google Cloud DLP — it's thorough but takes ~2 seconds per request. At 1 FPS, that's unusable. Privacy Shield runs in 5ms and catches the patterns that matter most in a browser context.&lt;/p&gt;

&lt;p&gt;This isn't a feature. It's a responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mascot changed everything
&lt;/h2&gt;

&lt;p&gt;Halfway through the build, I had a working agent. It could hear, see, and act. But it felt like a tool. Functional. Sterile. The kind of thing you demo once and forget.&lt;/p&gt;

&lt;p&gt;So I asked Gemini to generate pixel art mascots.&lt;/p&gt;

&lt;p&gt;I fed it a prompt for a "one-eyed spirit wisp, ethereal blue-purple glow, 64x64 pixel art" and got back something with genuine character. A little floating creature with a single curious eye. It looked like it belonged in a SNES game.&lt;/p&gt;

&lt;p&gt;Then I went further. I asked for variations: the same wisp wearing a detective hat, a crown, nerdy glasses, a pirate hat, headphones, a wizard hat, and tiny devil horns. Same style, same palette, all consistent. Gemini's image generation (via &lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;) kept the character recognizable across every variation.&lt;/p&gt;

&lt;p&gt;These became &lt;strong&gt;personas&lt;/strong&gt; — not just cosmetic skins, but full personality packages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Persona&lt;/th&gt;
&lt;th&gt;Voice&lt;/th&gt;
&lt;th&gt;Vibe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phantom&lt;/td&gt;
&lt;td&gt;Kore&lt;/td&gt;
&lt;td&gt;Friendly, curious spirit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleuth&lt;/td&gt;
&lt;td&gt;Charon&lt;/td&gt;
&lt;td&gt;Noir detective, dramatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regent&lt;/td&gt;
&lt;td&gt;Orus&lt;/td&gt;
&lt;td&gt;Regal, dignified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Byte&lt;/td&gt;
&lt;td&gt;Puck&lt;/td&gt;
&lt;td&gt;Nerdy, excitable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Captain&lt;/td&gt;
&lt;td&gt;Fenrir&lt;/td&gt;
&lt;td&gt;Pirate, adventurous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vibe&lt;/td&gt;
&lt;td&gt;Aoede&lt;/td&gt;
&lt;td&gt;Chill, laid back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arcane&lt;/td&gt;
&lt;td&gt;Zephyr&lt;/td&gt;
&lt;td&gt;Mystical wizard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gremlin&lt;/td&gt;
&lt;td&gt;Leda&lt;/td&gt;
&lt;td&gt;Chaotic, mischievous&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each persona has its own Gemini voice, mascot image, and system prompt that shapes how the agent talks. When you pick "Captain," the agent calls websites "islands" and says "aye aye!" When you pick "Gremlin," it's gleefully chaotic but still gets the job done.&lt;/p&gt;

&lt;p&gt;Users pick their persona during onboarding. It's the second screen they see, right after "Hey, I'm Phantom." Judges remember characters. They forget features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta layer: AI building AI
&lt;/h2&gt;

&lt;p&gt;The most honest thing I can say about this project is that it was a collaboration between a human with ideas and an AI with execution speed.&lt;/p&gt;

&lt;p&gt;Here's what Gemini specifically helped build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: The WebSocket proxy, tool system, and session management were pair-programmed with a coding agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mascot art&lt;/strong&gt;: All 9 character variations generated via &lt;code&gt;gemini-2.5-flash-image&lt;/code&gt; with img2img — I provided the base wisp and asked for costume variations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sprite animations&lt;/strong&gt;: 4 spritesheets (idle, listening, talking, thinking) generated from the same base character&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt;: When the vision proxy wasn't working, the agent wrote a direct-vs-proxy comparison test that isolated the &lt;code&gt;maxPayload&lt;/code&gt; bug&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: The Cloud Run setup, Artifact Registry config, Secret Manager integration, and service account creation were all scripted live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sound effects came from ElevenLabs' SFX API — text descriptions like "soft magical chime, fairy-like sparkle, UI connect sound" turned into actual audio files that now play when you connect, toggle vision, or execute a tool.&lt;/p&gt;

&lt;p&gt;What took days in previous projects took hours here. Not because the code was simpler, but because the iteration loop was: idea → implement → test → fix → next, with no context-switching overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Character sells&lt;/strong&gt;. A pixel art wisp with a detective hat is more memorable than any feature list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sound matters&lt;/strong&gt;. A tiny chime when you connect makes the whole experience feel 10x more polished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Live API is undersold&lt;/strong&gt;. Bidirectional audio + function calling + video input in one WebSocket is genuinely new. Most demos treat it as a voice chatbot. It's actually an agent runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-assisted development isn't cheating&lt;/strong&gt; — it's the new normal. The human still makes every creative and architectural decision. The AI just removes the friction between thinking and doing.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Phantom is open source. Install the Chrome extension, pick a persona, and start talking to your browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/youneslaaroussi/Phantom" rel="noopener noreferrer"&gt;github.com/youneslaaroussi/Phantom&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live site&lt;/strong&gt;: &lt;a href="https://phantom-server-pio3n3nsna-uc.a.run.app" rel="noopener noreferrer"&gt;phantom-server-pio3n3nsna-uc.a.run.app&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the Gemini Live Agent Challenge. #GeminiLiveAgentChallenge&lt;/em&gt;&lt;/p&gt;

</description>
      <category>geminiliveagentchallenge</category>
      <category>hackathon</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>BlockArt: Bringing GPT-4o Image Generation to Storyblok’s CMS</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sun, 29 Jun 2025 19:27:49 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/blockart-bringing-gpt-4o-image-generation-to-storybloks-cms-5860</link>
      <guid>https://dev.to/youneslaaroussi/blockart-bringing-gpt-4o-image-generation-to-storybloks-cms-5860</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/storyblok"&gt;Storyblok Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1097344530" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;BlockArt&lt;/strong&gt; — an AI-powered image editing plugin that turns Storyblok into your creative playground.&lt;/p&gt;

&lt;p&gt;No more jumping between tools or begging designers for assets. With BlockArt, your CMS becomes your studio. Generate images from text. Mask and edit like a pro. Inpaint with surgical precision. Drop in a wild prompt and watch it stream in, pixel by pixel, right inside Storyblok.&lt;/p&gt;

&lt;p&gt;It’s not just about playing with images. This thing is smart. It writes your alt-texts, enhances your prompts, and syncs directly with Storyblok’s asset system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Capabilities
&lt;/h3&gt;

&lt;p&gt;🎨 &lt;strong&gt;AI-Powered Image Editing&lt;/strong&gt;: Full image transformation and precise inpainting using OpenAI's GPT-4o Responses API&lt;br&gt;
🖼️ &lt;strong&gt;Text-to-Image Generation&lt;/strong&gt;: Create entirely new images from text descriptions&lt;br&gt;
🎭 &lt;strong&gt;Advanced Mask Editor&lt;/strong&gt;: Pixel-perfect selection tools for targeted editing&lt;br&gt;
📝 &lt;strong&gt;Smart Prompt Enhancement&lt;/strong&gt;: Leverage Storyblok's Prompt AI (Beta) for intelligent suggestion improvements&lt;br&gt;
♿ &lt;strong&gt;Automatic Accessibility&lt;/strong&gt;: AI-generated alt-text for SEO and accessibility compliance&lt;br&gt;
📚 &lt;strong&gt;Version History&lt;/strong&gt;: Track and revert between different edits&lt;br&gt;
🔄 &lt;strong&gt;Seamless Asset Management&lt;/strong&gt;: Complete integration with Storyblok's asset pipeline&lt;br&gt;
🌍 &lt;strong&gt;Multi-Region Support&lt;/strong&gt;: Works with all Storyblok regions (US, EU, CA, AP, CN)&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/youneslaaroussi" rel="noopener noreferrer"&gt;
        youneslaaroussi
      &lt;/a&gt; / &lt;a href="https://github.com/youneslaaroussi/blockart" rel="noopener noreferrer"&gt;
        blockart
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Generate and Edit Storyblok Assets with AI
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;BlockArt Storyblok Field Plugin&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://reactjs.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f889d3d75fa7551d1dc8af8f3ad48f0f04f3509f2b13a221aaba3cee1888a517/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f52656163742d31382e332d3631444146423f7374796c653d666f722d7468652d6261646765266c6f676f3d7265616374266c6f676f436f6c6f723d7768697465" alt="React"&gt;&lt;/a&gt;
&lt;a href="https://www.typescriptlang.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6af529ff1cf3b95c430772332e36e6c25a1345aea6337ea651f44d6b59df36f5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d352e372d3331373843363f7374796c653d666f722d7468652d6261646765266c6f676f3d74797065736372697074266c6f676f436f6c6f723d7768697465" alt="TypeScript"&gt;&lt;/a&gt;
&lt;a href="https://vitejs.dev/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f88dd0def2128cefaed505832bd00ecfc88dcc1b5c87a23e638a324270af2d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f566974652d352e342d3634364346463f7374796c653d666f722d7468652d6261646765266c6f676f3d76697465266c6f676f436f6c6f723d7768697465" alt="Vite"&gt;&lt;/a&gt;
&lt;a href="https://openai.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/81ec1150938f46302f3316fcc47f04051b05f3d0205bfec8f6047990cd8ae243/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f70656e41492d352e352d3431323939313f7374796c653d666f722d7468652d6261646765266c6f676f3d6f70656e6169266c6f676f436f6c6f723d7768697465" alt="OpenAI"&gt;&lt;/a&gt;
&lt;a href="https://www.storyblok.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/04b641bbc61e21128753e0df774352a10c1e442b93fa6b494525bcc262cc2291/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53746f7279626c6f6b2d4669656c645f506c7567696e2d3039423341463f7374796c653d666f722d7468652d6261646765266c6f676f3d73746f7279626c6f6b266c6f676f436f6c6f723d7768697465" alt="Storyblok"&gt;&lt;/a&gt;
&lt;a href="https://pnpm.io/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cfdf402d837ca0d862409b7c097397d4f0b04d66f628f064d8b7dbea998e8c92/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706e706d2d382e31352d4636393232303f7374796c653d666f722d7468652d6261646765266c6f676f3d706e706d266c6f676f436f6c6f723d7768697465" alt="pnpm"&gt;&lt;/a&gt;
&lt;a href="https://github.com/youneslaaroussi/blockart/./LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/153acf9dff19deb8abfc598c53bac50a4ceae0f5c83a552711060d3d78d2c057/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e3f7374796c653d666f722d7468652d6261646765" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A powerful Storyblok field plugin for AI-powered image editing and asset management.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/blockart/./media/Diagram.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fblockart%2F.%2Fmedia%2FDiagram.png" alt="Architectural Diagram"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎥 Watch the Videos&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
    &lt;tbody&gt;
&lt;tr&gt;
      &lt;td&gt;
        &lt;a href="https://vimeo.com/1097344530" rel="nofollow noopener noreferrer"&gt;
          &lt;img src="https://camo.githubusercontent.com/3c201db6976712f9225ef54a6ae25e47d243224d65fb0243aa1774b0591e0720/68747470733a2f2f76756d626e61696c2e636f6d2f313039373334343533302e6a7067" alt="BlockArt Demo Video" width="300"&gt;
          &lt;br&gt;
          &lt;strong&gt;🚀 Demo Video&lt;/strong&gt;
        &lt;/a&gt;
      &lt;/td&gt;
      &lt;td&gt;
        &lt;a href="https://vimeo.com/1097344544" rel="nofollow noopener noreferrer"&gt;
          &lt;img src="https://camo.githubusercontent.com/23f7a2fb292c858f6e519f7ac48fbce62afd24b5f51575c53c998bd157ca25bc/68747470733a2f2f76756d626e61696c2e636f6d2f313039373334343534342e6a7067" alt="BlockArt Setup Instructions" width="300"&gt;
          &lt;br&gt;
          &lt;strong&gt;⚙️ Setup Instructions&lt;/strong&gt;
        &lt;/a&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Prerequisites&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Before getting started, make sure you have the following installed and set up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; (version 16 or higher)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pnpm&lt;/strong&gt; package manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storyblok account&lt;/strong&gt; with at least one space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt; for version control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; account with billing enabled and an API key&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Setup&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Option 1: Direct Plugin Installation (Recommended)&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;The easiest way to get started is to use our pre-built plugin file:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Screenshot&lt;/th&gt;
&lt;th&gt;Instructions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/blockart/./media/plugin1.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fblockart%2F.%2Fmedia%2Fplugin1.png" alt="Plugin Step 1"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Go to &lt;strong&gt;My Plugins&lt;/strong&gt; in your Storyblok account and click &lt;strong&gt;New Field-type&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/blockart/./media/plugin2.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fblockart%2F.%2Fmedia%2Fplugin2.png" alt="Plugin Step 2"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Enter a name for your plugin (e.g., "my-blockart-plugin") and click &lt;strong&gt;Save&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/blockart/./media/plugin3.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fblockart%2F.%2Fmedia%2Fplugin3.png" alt="Plugin Step 3"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Copy the contents of &lt;a href="https://raw.githubusercontent.com/youneslaaroussi/blockart/refs/heads/main/build_plugin.js" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;./build_plugin.js&lt;/code&gt;&lt;/a&gt; from this repository and paste it into the code editor, then click &lt;strong&gt;Save&lt;/strong&gt; and &lt;strong&gt;Publish&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Option 2: Build from Source&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;If you prefer to build the plugin…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/youneslaaroussi/blockart" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Here’s how it goes: you open your Storyblok editor, drop in the BlockArt field, and boom — instant image playground. You can start fresh with a text prompt or pick an existing asset. Want to tweak an image? Choose full edit or inpainting mode. Our built-in mask editor lets you paint over the part you want to change. Then feed it a smart prompt (manually or get help from Storyblok’s Prompt AI), and let GPT-4o work its magic.&lt;/p&gt;

&lt;p&gt;As the image streams in, you watch it evolve in real-time. Once happy, you hit save and the image is stored back in your Storyblok assets, complete with metadata, alt-text, and version history. It’s buttery-smooth.&lt;/p&gt;

&lt;p&gt;See the Github README or &lt;a href="https://vimeo.com/1097344544" rel="noopener noreferrer"&gt;watch this Vimeo&lt;/a&gt; to setup BlockArt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storyblok Space:&lt;/strong&gt; &lt;a href="https://app.storyblok.com/#/me/spaces/285338869245722/" rel="noopener noreferrer"&gt;https://app.storyblok.com/#/me/spaces/285338869245722/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(note that this link is not publicly accessible)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Website that uses assets created using Storyblok:&lt;/strong&gt; &lt;a href="https://main.d1uj1niyggenq6.amplifyapp.com/landing?_storyblok_published=61822370039376" rel="noopener noreferrer"&gt;https://main.d1uj1niyggenq6.amplifyapp.com/landing?_storyblok_published=61822370039376&lt;/a&gt;&lt;br&gt;
(but please note that the actual deliverable of my submission is the plugin itself, which is INSIDE the storyblok editor, and unfortunately spaces cannot be shared publicly so please setup the plugin within your own editor using instructions above)&lt;/p&gt;

&lt;p&gt;And here is the repo with the demo space code: &lt;a href="https://github.com/youneslaaroussi/blockart-demo-space" rel="noopener noreferrer"&gt;https://github.com/youneslaaroussi/blockart-demo-space&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Used Storyblok
&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%2F2aifn22mznyv8xatrvhl.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%2F2aifn22mznyv8xatrvhl.png" alt="Architectural Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;BlockArt demonstrates deep integration with Storyblok's ecosystem through multiple API layers and features:&lt;/p&gt;
&lt;h3&gt;
  
  
  Local Testing with Storyblok CLI
&lt;/h3&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%2Ffd4u04sy5lxyt0jpvqgl.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%2Ffd4u04sy5lxyt0jpvqgl.png" alt="Storyblok banner"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To rapidly prototype and test BlockArt in a realistic environment, I used the &lt;a href="https://github.com/storyblok/storyblok-cli/tree/next/src#readme" rel="noopener noreferrer"&gt;&lt;strong&gt;Storyblok CLI v4&lt;/strong&gt;&lt;/a&gt; to spin up a full-featured local demo space. This allowed me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scaffold a clean Storyblok space structure with demo components&lt;/li&gt;
&lt;li&gt;Test plugin integration directly within simulated content blocks&lt;/li&gt;
&lt;li&gt;Validate asset upload workflows and Prompt AI interactions without polluting a production space&lt;/li&gt;
&lt;li&gt;Iterate faster by decoupling plugin development from live projects&lt;/li&gt;
&lt;li&gt;Run a TLS Proxy to directly serve localhost and use it in Storyblok editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This local CLI-powered setup was instrumental in refining the user experience and debugging complex edge cases during development.&lt;/p&gt;
&lt;h3&gt;
  
  
  🔧 Management API Mastery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete Asset Lifecycle&lt;/strong&gt;: From signed upload requests to S3 storage to asset finalization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Region Architecture&lt;/strong&gt;: Supports all Storyblok regions with dynamic API endpoint resolution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Asset Management&lt;/strong&gt;: Upload, update, delete, and retrieve operations with comprehensive error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata Management&lt;/strong&gt;: Automatic alt-text and title updates for improved SEO and accessibility&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🎨 Field Plugin SDK Integration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native UI Integration&lt;/strong&gt;: Seamlessly blends with Storyblok's interface design system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Content Sync&lt;/strong&gt;: Bidirectional data flow between plugin and Storyblok&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asset Browser Integration&lt;/strong&gt;: Direct access to existing media library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin State Management&lt;/strong&gt;: Persistent storage of edit history and user preferences&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🤖 Storyblok Prompt AI (Beta) Integration
&lt;/h3&gt;

&lt;p&gt;The plugin leverages Storyblok's cutting-edge Prompt AI for enhanced user experience:&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="c1"&gt;// Example AI enhancement implementation&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enhancePrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalPrompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;promptAIAction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'rephrase', 'extend', 'simplify', etc.&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;originalPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;160&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;basedOnCurrentStory&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="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;&lt;strong&gt;Supported Enhancement Actions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prompt&lt;/code&gt;: Generate creative suggestions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rephrase&lt;/code&gt;: Improve clarity and effectiveness
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extend&lt;/code&gt;: Add rich descriptive details&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simplify&lt;/code&gt;: Make prompts more accessible&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adjust-tone&lt;/code&gt;: Modify style and mood&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fix_spelling_and_grammar&lt;/code&gt;: Ensure quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frontend &amp;amp; Core
&lt;/h3&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%2Fyd6ky4say5e3pv3eqmun.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%2Fyd6ky4say5e3pv3eqmun.png" alt="Frontend Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 18.3&lt;/strong&gt; with TypeScript for robust type safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite 5.4&lt;/strong&gt; for lightning-fast development and optimized builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storyblok Field Plugin SDK 1.6&lt;/strong&gt; for seamless CMS integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lucide React&lt;/strong&gt; for consistent iconography&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI &amp;amp; Image Processing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI SDK 5.5&lt;/strong&gt; with GPT-4o Responses API for advanced image editing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Image Processing Pipeline&lt;/strong&gt; for format conversion and optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Streaming&lt;/strong&gt; for progressive image generation feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing &amp;amp; Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vitest&lt;/strong&gt; for comprehensive unit testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing Library&lt;/strong&gt; for component testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESLint&lt;/strong&gt; with TypeScript rules for code quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI Integration
&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%2F997htfm3xxzvjds4k7lo.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%2F997htfm3xxzvjds4k7lo.png" alt="Prompt Generation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;BlockArt pushes the boundaries of AI integration within Storyblok, creating a seamless bridge between creative vision and technical execution:&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Cutting-Edge AI Models
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI GPT-4o Responses API&lt;/strong&gt;: Leverages the latest multimodal capabilities for superior image understanding and generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Image Processing&lt;/strong&gt;: Supports both full image editing and precise inpainting with mask-based editing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming Generation&lt;/strong&gt;: Real-time progress updates with partial image previews for enhanced user experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎯 Intelligent Workflow Automation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context-Aware Processing&lt;/strong&gt;: AI understands both image content and user intent for optimal results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Alt-Text Generation&lt;/strong&gt;: Uses vision models to create descriptive, SEO-friendly alt attributes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Prompt Enhancement&lt;/strong&gt;: Integrates with Storyblok's Prompt AI to improve user prompts automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control Intelligence&lt;/strong&gt;: Tracks editing decisions and suggests improvements based on history&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔄 Seamless Content Integration
&lt;/h3&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%2Fszj0j83s5tqm0qck2629.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%2Fszj0j83s5tqm0qck2629.png" alt="Generated Image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The AI layer doesn't just generate images—it creates content that's immediately ready for production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example of the complete AI-to-Storyblok pipeline&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processAndSaveImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. AI processes the image&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editedImage&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;processImageWithStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. AI generates accessible alt-text&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;altText&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;generateAltText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;editedImage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Save to Storyblok with metadata&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asset&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;storyblokAPI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;editedImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nf"&gt;generateFilename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalFilename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;altText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`AI-edited: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prompt&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;asset&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;h3&gt;
  
  
  📈 Performance Optimizations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming Updates&lt;/strong&gt;: Users see progress in real-time, improving perceived performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Caching&lt;/strong&gt;: Reduces API calls while maintaining data freshness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive Quality&lt;/strong&gt;: Balances generation speed with output quality based on use case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Recovery&lt;/strong&gt;: Graceful handling of API failures with automatic retry mechanisms&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Learnings and Takeaways
&lt;/h2&gt;

&lt;p&gt;Building BlockArt has been an incredible journey that pushed the boundaries of what's possible when combining AI with modern CMS platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  🏆 What I'm Proud Of
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Technical Innovation&lt;/strong&gt;: Successfully integrated OpenAI's newest GPT-4o Responses API with Storyblok's Management API, creating a seamless AI-powered workflow that feels native to the platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Experience Design&lt;/strong&gt;: Created an intuitive multi-step interface that makes complex AI image editing accessible to content creators without technical backgrounds. The streaming updates and real-time previews provide immediate feedback that enhances the creative process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comprehensive Integration&lt;/strong&gt;: Went beyond basic functionality to implement a complete asset management pipeline, version history, accessibility features, and multi-region support—making it production-ready for enterprise use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility First&lt;/strong&gt;: Automatic alt-text generation ensures every AI-created image meets accessibility standards, something often overlooked in AI image tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔧 Technical Challenges Overcome
&lt;/h3&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%2Fbphp2u3af80u7rbmp3n2.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%2Fbphp2u3af80u7rbmp3n2.png" alt="AI Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex Asset Pipeline&lt;/strong&gt;: Implementing Storyblok's three-stage upload process (signed response → S3 upload → finalization) required deep understanding of cloud storage patterns and error handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Model Integration&lt;/strong&gt;: Working with OpenAI's latest Responses API meant adapting to cutting-edge technology with limited documentation, requiring extensive experimentation and optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time Streaming&lt;/strong&gt;: Building a responsive UI that handles streaming image generation while maintaining state consistency across React components was technically challenging but crucial for user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-Browser Compatibility&lt;/strong&gt;: Ensuring the plugin works consistently across different browsers, especially handling canvas operations and file uploads in various environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  💡 Key Insights
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AI + CMS Synergy&lt;/strong&gt;: The combination of AI capabilities with robust content management creates exponentially more value than either technology alone. Users can generate, edit, and immediately deploy content in a single workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive Enhancement&lt;/strong&gt;: Starting with a solid foundation (Storyblok's infrastructure) and adding AI as an enhancement layer proved more successful than building AI-first and trying to integrate CMS features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User-Centric AI&lt;/strong&gt;: The most impactful AI features aren't the most technically impressive ones—they're the ones that solve real user problems, like automatic alt-text generation and prompt enhancement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Experience Matters&lt;/strong&gt;: Storyblok's excellent documentation, SDK, and developer tools made it possible to focus on innovation rather than fighting with APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Future Vision
&lt;/h3&gt;

&lt;p&gt;This project demonstrates the incredible potential of AI-augmented content management. I envision a future where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content creators can describe their vision in natural language and see it instantly realized&lt;/li&gt;
&lt;li&gt;Accessibility becomes automatic rather than an afterthought&lt;/li&gt;
&lt;li&gt;Creative workflows are enhanced, not replaced, by AI assistance&lt;/li&gt;
&lt;li&gt;CMS platforms become intelligent creative partners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BlockArt is just the beginning of this transformation, and I'm excited to continue pushing the boundaries of what's possible when human creativity meets artificial intelligence within powerful platforms like Storyblok.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ for the Storyblok Developer Challenge&lt;/em&gt; &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>webdev</category>
      <category>api</category>
      <category>storyblokchallenge</category>
    </item>
    <item>
      <title>CC-my-Jira: Agentic JIRA Ticketing via Postmark</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sat, 07 Jun 2025 16:16:57 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/cc-my-jira-agentic-jira-ticketing-via-postmark-amf</link>
      <guid>https://dev.to/youneslaaroussi/cc-my-jira-agentic-jira-ticketing-via-postmark-amf</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/devteam/join-the-postmark-challenge-inbox-innovators-3000-in-prizes-497l"&gt;Postmark Challenge: Inbox Innovators&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&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%2F8dcivsbcphbtgxw7f1xw.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%2F8dcivsbcphbtgxw7f1xw.png" alt="Architectural Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/youneslaaroussi/ccmyjira-server/raw/main/images/FullDiagram.png" rel="noopener noreferrer"&gt;&lt;em&gt;view highres architectural diagram&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;CCMyJira&lt;/strong&gt;, an AI-powered, multi-tenant SaaS platform that intelligently transforms emails into structured, actionable JIRA tickets. It bridges the communication gap between email-centric clients/users and JIRA-focused development teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Core Problem:&lt;/strong&gt; Critical requests, bug reports, and feedback in emails often get lost, misinterpreted, or require manual, error-prone transcription into JIRA, leading to inefficiencies and delays.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CCMyJira's Solution – Key Capabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🧠 AI Agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Leverages GPT-4.1 to understand the content, intent, and technical nuances of incoming emails.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🎟️ Intelligent Ticket Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Searches existing JIRA issues to prevent duplicates; creates new tickets or updates existing ones with information from follow-up emails.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;📎 Seamless Attachment Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatically extracts and uploads all email attachments (documents, images, logs) and embedded images to the relevant JIRA ticket, preserving crucial context.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🎯 Smart Assignee Suggestions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI considers team workload, skills (from JIRA profiles/roles), and email content (like @mentions) to suggest the most appropriate assignee.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🏢 Multi-Tenant SaaS Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supports multiple organizations with Atlassian OAuth for self-serve onboarding, custom domain verification (using Postmark for verification emails), and data isolation via Supabase Row Level Security (RLS).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;⚡ Reliable &amp;amp; Scalable Email Ingestion (via Postmark)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Postmark webhooks&lt;/strong&gt; receive an immediate &lt;code&gt;200 OK&lt;/code&gt; (~50ms). Intensive AI/JIRA processing (15-60+ secs) is handled by background workers (BullMQ/Redis), preventing Postmark timeouts and ensuring every email is reliably captured and processed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;📧 Robust Email Handling (Postmark)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Inbound Parsing:&lt;/strong&gt; All emails (to &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt;) are parsed by Postmark, providing rich JSON. &lt;strong&gt;Outbound Delivery:&lt;/strong&gt; Postmark sends critical operational emails, like domain verifications.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Essentially, CCMyJira acts as an intelligent automation layer, ensuring every important email becomes an actionable JIRA item, with Postmark as the crucial email gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Watch the Video Demo:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1090954995" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Experience CCMyJira live: &lt;strong&gt;&lt;a href="https://ccmyjira.com" rel="noopener noreferrer"&gt;ccmyjira.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We offer two ways to explore the platform's capabilities:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 🚀 Instant Demo (No Atlassian Account Needed)
&lt;/h3&gt;

&lt;p&gt;This mode lets you see the core email-to-ticket functionality in action immediately using a shared demo JIRA board.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Go to &lt;strong&gt;&lt;a href="https://ccmyjira.com" rel="noopener noreferrer"&gt;ccmyjira.com&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;During the onboarding, click &lt;strong&gt;"Use Demo Account"&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Send an email from &lt;strong&gt;any email address&lt;/strong&gt; to &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt;. Try including a subject like "Urgent Bug: Website Checkout Broken," details in the body, and perhaps an inline image or an attachment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Observe the demo JIRA board UI on the CCMyJira website. Your new ticket, along with any attachments, should appear live within a minute or two as the AI processes it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What you'll see:&lt;/strong&gt;&lt;br&gt;
An email transformed into a JIRA ticket in a Kanban view:&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%2Fwtap8cq0k48hvlh76uli.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%2Fwtap8cq0k48hvlh76uli.png" alt="Example JIRA Ticket created by CCMyJira"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(note that in demo mode, you can't directly access the Jira links because they can't be public, therefore I built a custom API to fetch Jira tickets and view attachments)&lt;/p&gt;
&lt;h3&gt;
  
  
  2. 🔒 Full SaaS Experience (With Your Atlassian Account)
&lt;/h3&gt;

&lt;p&gt;This mode demonstrates the complete multi-tenant SaaS flow, connecting CCMyJira to your own Atlassian JIRA instance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Go to &lt;strong&gt;&lt;a href="https://ccmyjira.com" rel="noopener noreferrer"&gt;ccmyjira.com&lt;/a&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Click &lt;strong&gt;"Login with Atlassian"&lt;/strong&gt; and authenticate with your Atlassian account.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Follow the onboarding steps to create an organization.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Verify your company domain&lt;/strong&gt;. Note: Public email providers (like gmail.com) cannot be used for verified routing to your private JIRA for security reasons; a custom domain is required. Postmark will send a verification email to an admin address at your domain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Configure your JIRA board details (target project, etc.).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Send an email &lt;strong&gt;from your verified company domain&lt;/strong&gt; to &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt;. This address can also be CC'd or BCC'd.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Watch as the AI processes the email and creates/updates a ticket directly in &lt;em&gt;your&lt;/em&gt; JIRA project, also reflected in the CCMyJira dashboard.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Kanban View within CCMyJira:&lt;/strong&gt;&lt;br&gt;
Once tickets are processed, you can view them in JIRA or the Kanban view:&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%2Fb3s0ivqu6gkulpv6c3ra.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%2Fb3s0ivqu6gkulpv6c3ra.png" alt="JIRA Ticket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing Tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Complex Emails:&lt;/strong&gt; Try sending emails with mixed content: inline images, multiple attachments (PDFs, logs, text files), HTML formatting, and long discussion threads.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Keywords:&lt;/strong&gt; Use keywords that might imply priority (e.g., "urgent," "critical") or ticket type (e.g., "bug report," "feature request").&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Mentions:&lt;/strong&gt; If you've set up users in your JIRA project, try @mentioning a user in the email body.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Follow-ups:&lt;/strong&gt; Reply to the original email thread (ensuring &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt; is a recipient). The AI should identify it as a follow-up and update the existing JIRA ticket.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Code Repository
&lt;/h2&gt;

&lt;p&gt;The project is split into two main repositories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Server-Side Application (Backend):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  This repository contains all the NestJS backend logic, including the AI agent, JIRA integration, Postmark webhook handling, multi-tenancy features, queue management, and API endpoints. The comprehensive documentation (&lt;code&gt;/docs&lt;/code&gt; folder) detailing the architecture and features resides here.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/youneslaaroussi" rel="noopener noreferrer"&gt;
        youneslaaroussi
      &lt;/a&gt; / &lt;a href="https://github.com/youneslaaroussi/ccmyjira-server" rel="noopener noreferrer"&gt;
        ccmyjira-server
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AI-Powered Email-to-JIRA SaaS (Postmark Challenge)&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://nestjs.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b3c6b18987b608ff1a1517e5e7a309f8f70948ab0db40aacf7483e58a116e3d0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4e6573744a532d4530323334453f7374796c653d666f722d7468652d6261646765266c6f676f3d6e6573746a73266c6f676f436f6c6f723d7768697465" alt="NestJS"&gt;&lt;/a&gt;
&lt;a href="https://www.typescriptlang.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b308ff9a6de632b94c933c0f27975188080f8cf88a115ae10338540f8d9ab8ab/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d3030374143433f7374796c653d666f722d7468652d6261646765266c6f676f3d74797065736372697074266c6f676f436f6c6f723d7768697465" alt="TypeScript"&gt;&lt;/a&gt;
&lt;a href="https://openai.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/44d21edf29271fb91509a4092365524e6a3b2dbde47aa6b21f4c8198416de4b0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f70656e41492d3431323939313f7374796c653d666f722d7468652d6261646765266c6f676f3d6f70656e6169266c6f676f436f6c6f723d7768697465" alt="OpenAI"&gt;&lt;/a&gt;
&lt;a href="https://postmarkapp.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a269dfd4bc6ee8586e457d1917c166209d9186e470456678067c024e5e97edb6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f506f73746d61726b2d4646363535303f7374796c653d666f722d7468652d6261646765266c6f676f3d706f73746d61726b266c6f676f436f6c6f723d7768697465" alt="Postmark"&gt;&lt;/a&gt;
&lt;a href="https://docs.bullmq.io/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1d3d84b8cf6e8401dc53a440e03aed905bdb08797c9ff534951356b5204c28c2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f42756c6c4d512d4646364236423f7374796c653d666f722d7468652d6261646765266c6f676f3d7265646973266c6f676f436f6c6f723d7768697465" alt="BullMQ"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🏆 Built for Postmark Challenge&lt;/strong&gt; | &lt;a href="https://api.ccmyjira.com" rel="nofollow noopener noreferrer"&gt;Live Demo&lt;/a&gt; | &lt;a href="https://api.ccmyjira.com/api/docs" rel="nofollow noopener noreferrer"&gt;API Docs&lt;/a&gt; | &lt;a href="https://dev.to/devteam/join-the-postmark-challenge-inbox-innovators-3000-in-prizes-497l" rel="nofollow"&gt;Challenge Link&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🏗️ Architecture&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/ccmyjira-server/./images/FullDiagram.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fccmyjira-server%2F.%2Fimages%2FFullDiagram.png" alt="Overall Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Try It Now (Live Demo)&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;🟢 No Atlassian Account Needed (Demo Mode)&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Go to &lt;a href="https://ccmyjira.com" rel="nofollow noopener noreferrer"&gt;ccmyjira.com&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Click "Use Demo Account" during onboarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Send an email from &lt;strong&gt;any email address&lt;/strong&gt; to &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;See the ticket and attachments appear live in the demo JIRA board UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;🔒 With Your Atlassian Account (Full SaaS Flow)&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Go to &lt;a href="https://ccmyjira.com" rel="nofollow noopener noreferrer"&gt;ccmyjira.com&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Authenticate with your Atlassian account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Verify your &lt;strong&gt;company domain&lt;/strong&gt; (no public email providers like gmail)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Configure your JIRA board (project, users, sprints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Send an email from your &lt;strong&gt;verified domain&lt;/strong&gt; to &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt; (or use forwarding/CC/reply-all)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Watch tickets and attachments appear live in your JIRA board UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/ccmyjira-server/./images/ccmyjira.com_.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fccmyjira-server%2F.%2Fimages%2Fccmyjira.com_.png" alt="Kanban View"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For authenticated orgs, only emails sent from your verified domain are accepted and routed…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/youneslaaroussi/ccmyjira-server" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Website &amp;amp; Dashboard (Frontend):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  This repository contains the Next.js/React frontend application, which includes the user onboarding, dashboard for viewing processed tickets, organization management, and domain verification UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/youneslaaroussi" rel="noopener noreferrer"&gt;
        youneslaaroussi
      &lt;/a&gt; / &lt;a href="https://github.com/youneslaaroussi/ccmyjira-site" rel="noopener noreferrer"&gt;
        ccmyjira-site
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      https://ccmyjira.com
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CCMyJIRA - Professional JIRA Dashboard&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A modern, professional dashboard for real-time JIRA ticket monitoring, team workload management, and system performance tracking.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🛠 Prerequisites&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bun&lt;/strong&gt; (latest version)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JIRA instance&lt;/strong&gt; with API access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid JIRA project key&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Installation&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clone the repository:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https://github.com/youneslaaroussi/ccmyjira-site.git
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; ccmyjira-site&lt;/pre&gt;

&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;strong&gt;Install dependencies:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;bun install&lt;/pre&gt;

&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;&lt;strong&gt;Set up environment variables:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;cp env.example .env.local&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Configure your &lt;code&gt;.env.local&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-dotenv notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-v"&gt;NEXT_PUBLIC_API_BASE_URL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;http://localhost:3001&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;&lt;strong&gt;Start the development server:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;bun dev&lt;/pre&gt;

&lt;/div&gt;
&lt;ol start="5"&gt;
&lt;li&gt;&lt;strong&gt;Open &lt;a href="http://localhost:3000" rel="nofollow noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; in your browser.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Features Overview&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enhanced Kanban Board&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interactive ticket management with drag-and-drop, filtering, and real-time updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team Workload Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Visual representation of team capacity and task distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Real-time Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live dashboard with KPIs, sprint progress, and performance tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Modern UI/UX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Responsive design with dark mode, animations, and professional styling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organization Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-organization support with domain verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attachment Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;View and download JIRA ticket attachments directly from the dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advanced Filtering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Search and filter tickets by&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/youneslaaroussi/ccmyjira-site" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&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%2Fg67wb56akdaw4qnq8h0j.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%2Fg67wb56akdaw4qnq8h0j.png" alt="Smart Assignment Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/youneslaaroussi/ccmyjira-server/raw/main/images/SmartAssignmentDiagram.png" rel="noopener noreferrer"&gt;&lt;em&gt;view highres smart assignment diagram&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building CCMyJira was an exploration into creating a deeply integrated system where email, AI, and project management converge to streamline workflows. The mission was clear: make emails first-class citizens in JIRA, intelligently.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Idea: Intelligent Email-to-JIRA Automation
&lt;/h3&gt;

&lt;p&gt;The project stemmed from the universal challenge of critical information shared via email often being siloed or requiring tedious manual effort to integrate into project management tools like JIRA. I aimed to not just automate this but to infuse it with intelligence to understand context, prevent redundancy, and ensure tasks are actionable.&lt;/p&gt;

&lt;p&gt;The agent has access to some of these tools:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_current_period&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Get current date and time information&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read_jira_tickets&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="s1"&gt;Read JIRA tickets from a specific time period&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Number of days to look back&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filter by ticket status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filter by assignee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;searchText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search in summary/description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_jira_ticket&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="s1"&gt;Create a new JIRA ticket. Email attachments and embedded images are automatically included.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ticket title/summary&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="s1"&gt;Detailed description. Email context will be automatically appended.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;issueType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bug, Story, Task, Epic, etc.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Highest, High, Medium, Low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Username or email to assign&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Array of labels to add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Array of component names&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modify_jira_ticket&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="s1"&gt;Modify an existing JIRA ticket. New email attachments and embedded images are automatically added.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ticketKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JIRA ticket key (e.g., PROJ-123)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New summary (optional)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New status (optional)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New assignee (optional)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add comment. Email context will be automatically appended. (optional)&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;h3&gt;
  
  
  Leveraging Postmark for Robust Email Processing
&lt;/h3&gt;

&lt;p&gt;Postmark was instrumental, serving as the reliable and developer-friendly gateway for all email interactions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Inbound Email Parsing – The Heart of the System:&lt;/strong&gt; CCMyJira's ability to process emails begins with Postmark. Emails sent to &lt;code&gt;hello@send.ccmyjira.com&lt;/code&gt; are received and meticulously parsed by Postmark. The resulting rich JSON payload, complete with attachments, HTML/text bodies, and headers, is then dispatched via webhook to our NestJS API endpoint (&lt;code&gt;/webhooks/postmark&lt;/code&gt;). This structured data is the fuel for our AI engine.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Critical: Immediate Webhook Response &amp;amp; Asynchronous Processing:&lt;/strong&gt; A cornerstone of the architecture (detailed in &lt;code&gt;README.md&lt;/code&gt; and &lt;code&gt;docs/01-architecture.md&lt;/code&gt;) is the immediate &lt;code&gt;200 OK&lt;/code&gt; response (target &amp;lt;100ms) to Postmark webhooks. The intensive AI processing and JIRA API communications (often 15-60+ seconds) are deferred to background jobs managed by BullMQ and Redis. This design is &lt;em&gt;vital&lt;/em&gt; as it prevents Postmark webhook timeouts, ensuring no email is lost and maintaining system responsiveness and scalability.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Outbound Email for Operational Integrity:&lt;/strong&gt; Beyond inbound, Postmark handles essential outbound communications. Most significantly, it sends domain verification emails (as outlined in &lt;code&gt;docs/15-domain-verification-guide.md&lt;/code&gt;). This Postmark-powered step is crucial for the multi-tenant security model, allowing organizations to securely link their email domains to their JIRA instances.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Professional Presence with Custom Domains:&lt;/strong&gt; Postmark facilitates the use of CCMyJira's own inbound domain (&lt;code&gt;send.ccmyjira.com&lt;/code&gt;) and a verified sender domain (&lt;code&gt;noreply@ccmyjira.com&lt;/code&gt;), enhancing trust and brand consistency for users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Postmark's reliability, comprehensive API, and clear documentation for both inbound parsing and outbound sending were pivotal, allowing me to focus on the application's core intelligence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;NestJS (TypeScript)&lt;/td&gt;
&lt;td&gt;Modular, scalable, and maintainable API development.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;OpenAI GPT-4o SDK&lt;/td&gt;
&lt;td&gt;Email analysis, intelligent ticket creation, smart assignments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue System&lt;/td&gt;
&lt;td&gt;BullMQ &amp;amp; Redis&lt;/td&gt;
&lt;td&gt;Managing background jobs for AI processing &amp;amp; JIRA interactions, ensuring webhook responsiveness.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Supabase (PostgreSQL)&lt;/td&gt;
&lt;td&gt;Multi-tenant data storage (users, orgs, domain configs, JIRA settings) with Row Level Security for tenant isolation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Containerization&lt;/td&gt;
&lt;td&gt;Docker &amp;amp; Docker Compose&lt;/td&gt;
&lt;td&gt;Consistent development, testing, and production environments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js &amp;amp; React&lt;/td&gt;
&lt;td&gt;Modern, interactive UI for dashboards, onboarding, and configuration (details in &lt;code&gt;docs/16-frontend-integration-guide.md&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment &amp;amp; Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;AWS EC2 (t3.medium)&lt;/td&gt;
&lt;td&gt;Application and worker hosting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balancing&lt;/td&gt;
&lt;td&gt;AWS Application Load Balancer (ALB)&lt;/td&gt;
&lt;td&gt;SSL termination and request distribution.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;AWS Route 53&lt;/td&gt;
&lt;td&gt;Managing &lt;code&gt;api.ccmyjira.com&lt;/code&gt; and &lt;code&gt;ccmyjira.com&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Redis&lt;/td&gt;
&lt;td&gt;Upstash Redis&lt;/td&gt;
&lt;td&gt;Queue backend for BullMQ in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Implementation Details &amp;amp; Architectural Decisions
&lt;/h3&gt;

&lt;p&gt;CCMyJira's effectiveness stems from several core architectural choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Postmark-Driven Asynchronous Processing:&lt;/strong&gt; To ensure reliability with Postmark, incoming emails trigger a rapid &lt;code&gt;200 OK&lt;/code&gt; response. The actual complex processing (AI analysis, JIRA updates) is then handled by a robust background queue system (BullMQ/Redis). This prevents Postmark webhook timeouts and allows the system to gracefully manage email bursts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sophisticated AI Agent (&lt;code&gt;docs/04-ai-agent.md&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Iterative Intelligence:&lt;/strong&gt; Uses multi-step GPT-4o conversations to understand email context deeply, search JIRA for existing issues (preventing duplicates), and then decide on the best action (create, update, comment).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Comprehensive Data Handling:&lt;/strong&gt; Processes and attaches all email content, including files and embedded images, directly to JIRA tickets.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;High-Quality Ticket Creation:&lt;/strong&gt; AI is guided to create well-structured, professional JIRA tickets (see &lt;code&gt;docs/improved-ticket-example.md&lt;/code&gt;) that are immediately actionable, a significant step up from raw email dumps.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Smart Assignments:&lt;/strong&gt; Suggests appropriate JIRA users for tickets based on workload, skills, and email content (like @mentions).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**Secure &amp;amp; Scalable Multi-Tenancy (&lt;code&gt;docs/13-multi-tenant-architecture.md&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Seamless Onboarding:&lt;/strong&gt; Users connect via Atlassian OAuth, securely linking their JIRA instances.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Postmark-Secured Domain Verification:&lt;/strong&gt; Organizations verify domain ownership using a Postmark-sent confirmation email. Only emails from verified domains are routed to a tenant's JIRA, ensuring emails go to the correct organization.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Data Isolation:&lt;/strong&gt; Supabase RLS strictly separates each organization's data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Targeted Email Routing:&lt;/strong&gt; A dedicated service maps incoming email domains to the correct tenant, applying their specific JIRA and AI settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Production-Ready Design:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Comprehensive Documentation:&lt;/strong&gt; The project is backed by extensive documentation covering architecture, setup, and APIs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Dockerized:&lt;/strong&gt; For consistent environments from development to production.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Large Payload Support:&lt;/strong&gt; Engineered to handle large emails with multiple attachments (up to 10MB).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Modular &amp;amp; API-First:&lt;/strong&gt; Promotes maintainability and clear separation of concerns.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Experience with Postmark
&lt;/h3&gt;

&lt;p&gt;My experience integrating Postmark was exceptionally positive and fundamental to CCMyJira's success. Postmark effectively solved the critical first step: getting email content into the application reliably and in a structured format.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Developer-Friendly Inbound Parsing:&lt;/strong&gt; Postmark's webhook setup was straightforward, and the parsed email JSON was rich and easy to work with. This was the primary Postmark feature utilized and it formed the very foundation of the application.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reliability &amp;amp; Peace of Mind:&lt;/strong&gt; Knowing Postmark handles the complexities of email reception and parsing (with webhook retries) allowed me to focus on the core AI and JIRA integration logic.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Excellent Documentation:&lt;/strong&gt; Clear guides and API references accelerated development significantly.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Efficient Testing:&lt;/strong&gt; The ability to quickly test the inbound flow was invaluable for iteration.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Built for Scale:&lt;/strong&gt; The webhook architecture, paired with our asynchronous processing, ensures the system is ready for high email volumes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Postmark's inbound parsing was not just a feature used; it was the &lt;strong&gt;enabling technology&lt;/strong&gt; that made CCMyJira feasible. It elegantly abstracts the complexities of email handling, empowering developers to build innovative email-driven applications like this one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Developed by Younes Laaroussi.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>postmarkchallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>MailBudget — From Messy Receipts to Meaningful Money Insights</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sat, 31 May 2025 14:03:57 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/your-inbox-now-an-ai-financial-assistant-postmark-challenge-inbox-innovators-2ne5</link>
      <guid>https://dev.to/youneslaaroussi/your-inbox-now-an-ai-financial-assistant-postmark-challenge-inbox-innovators-2ne5</guid>
      <description>&lt;p&gt;&lt;em&gt;Submission for the &lt;a href="https://dev.to/challenges/postmark"&gt;Postmark Challenge: Inbox Innovators&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1089331410" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MailBudget&lt;/strong&gt; is a proactive, modular finance assistant that leverages Postmark's robust inbound email parsing capabilities to automate financial insights and inbox cleanup. &lt;/p&gt;

&lt;p&gt;At its core, MailBudget functions as a &lt;strong&gt;"hub-and-spoke" system&lt;/strong&gt;—Postmark serves as the universal inbox gateway, while lightweight plug-in adapters (such as Gmail Cleanup) can be added or removed without impacting the core application. &lt;/p&gt;

&lt;p&gt;Security (DKIM/SPF) validation happens upfront, AI-driven insights are generated downstream, and structured data is neatly stored for easy access.&lt;/p&gt;

&lt;p&gt;With MailBudget, email receipts become actionable data, spending spikes are proactively identified, and your inbox stays tidy—all without manual intervention&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 Demo
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Warning:&lt;/strong&gt; Any data you share will appear in a public database, so please don't share any receipts with confidential information. If you do accidentally send something, please reach out to me and I'll delete it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forward a Receipt&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Send any email receipt or invoice to the following address:
&lt;code&gt;3728c7c80a75490dd061ca9611e2ba2e@inbound.postmarkapp.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch Your Data Appear&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to the &lt;a href="https://airtable.com/appkfTzH5U3FypFKz/shrweFPGPkrplilT0" rel="noopener noreferrer"&gt;live Airtable view&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Refresh after 10-30 seconds to see structured receipt data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant AI Insights&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Receive immediate insights by running:
&lt;code&gt;curl -X POST "http://3.95.208.224/ai-agent/analyze?to=your-email@example.com"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Alternatively, explore via &lt;a href="http://3.95.208.224/api/docs" rel="noopener noreferrer"&gt;Swagger UI&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;📖 Full API Docs: &lt;a href="http://3.95.208.224/api/docs" rel="noopener noreferrer"&gt;http://3.95.208.224/api/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screenshots &amp;amp; video walkthrough:&lt;br&gt;&lt;br&gt;
&lt;em&gt;Insight email&lt;/em&gt;&lt;br&gt;&lt;br&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%2Fm4du4zlsbw36ha78u5kl.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%2Fm4du4zlsbw36ha78u5kl.png" alt="Insight email"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Monthly report&lt;/em&gt;&lt;br&gt;&lt;br&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%2Flw0mpbrf59g4sz6krw8a.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%2Flw0mpbrf59g4sz6krw8a.png" alt="Monthly report"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Inbox cleanup requires running the app locally with your own Gmail OAuth keys. A dry-run preview mode is included so nothing is deleted by accident.&lt;/p&gt;


&lt;h2&gt;
  
  
  💻 Code Repository
&lt;/h2&gt;

&lt;p&gt;All source code, setup instructions, and a one-click Docker deployment are on GitHub:  &lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/youneslaaroussi" rel="noopener noreferrer"&gt;
        youneslaaroussi
      &lt;/a&gt; / &lt;a href="https://github.com/youneslaaroussi/mailbudget" rel="noopener noreferrer"&gt;
        mailbudget
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;MailBudget: The Proactive Postmark-Powered Inbox-Cleaning Finance Assistant&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Transform your email receipts into intelligent financial insights — and automatically tidy up your inbox&lt;/em&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📑 Table of Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#-the-problem-we-solve" rel="noopener noreferrer"&gt;The Problem We Solve&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#-how-it-works" rel="noopener noreferrer"&gt;How It Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#-try-it-now--no-setup-needed" rel="noopener noreferrer"&gt;Try It Now — No Setup Needed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#%EF%B8%8F-local-setup" rel="noopener noreferrer"&gt;Local Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#-gmail-integration-optional" rel="noopener noreferrer"&gt;Gmail Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#-deployment-guide" rel="noopener noreferrer"&gt;Deployment Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget?tab=readme-ov-file#-competitive-landscape" rel="noopener noreferrer"&gt;Competitive Landscape&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 The Problem We Solve&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Every day, inboxes fill up with receipts, invoices, and bank notifications—valuable financial data trapped in unstructured text. &lt;strong&gt;MailBudget&lt;/strong&gt; unlocks that data.&lt;/p&gt;
&lt;p&gt;By leveraging &lt;strong&gt;Postmark&lt;/strong&gt; as our rock-solid inbound gateway, MailBudget turns inbox clutter into clarity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Parses every receipt or invoice into clean, structured data&lt;/li&gt;
&lt;li&gt;✅ Surfaces anomalies &amp;amp; trends with an AI/RAG pipeline&lt;/li&gt;
&lt;li&gt;🧹 After processing, it can delete the original email so your workspace stays squeaky-clean&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🛠 How It Works&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/youneslaaroussi/mailbudget/./media/Diagram.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fmailbudget%2F.%2Fmedia%2FDiagram.png" alt="Architectural Diagram"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;: MailBudget is a hub-and-spoke system—Postmark acts as the universal inbox gateway, while lightweight plug-in adapters (e.g., Gmail Cleanup) bolt on or…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/youneslaaroussi/mailbudget" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  ⚙️ How I Built It
&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%2Favkmmiryet251xssvz2z.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%2Favkmmiryet251xssvz2z.png" alt="Architectural Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/youneslaaroussi/mailbudget/raw/main/media/Diagram.png" rel="noopener noreferrer"&gt;&lt;em&gt;view highres diagram&lt;/em&gt;&lt;/a&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postmark Inbound &amp;amp; Send APIs&lt;/strong&gt; handle all email I/O and security (DKIM, SPF, spam filtering) so I could focus on features, not deliverability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js, NestJS &amp;amp; TypeScript&lt;/strong&gt; power the server.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-4&lt;/strong&gt; extracts structured data from email bodies and crafts the insight summaries.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airtable&lt;/strong&gt; is the single source of truth for transactions, making RAG queries trivial.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gmail Cleanup Adapter&lt;/strong&gt; speaks to Google via OAuth2; because the core logic lives behind Postmark webhooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💡 Challenges &amp;amp; Key Implementations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agentic toolkit&lt;/strong&gt; – The GPT-4 agent isn't a one-shot prompt; it wields named tools defined in &lt;code&gt;src/ai-agent/services&lt;/code&gt; such as &lt;code&gt;queryAirtable&lt;/code&gt;, &lt;code&gt;upsertTransaction&lt;/code&gt;, &lt;code&gt;listSubscriptions&lt;/code&gt;, and &lt;code&gt;sendInsightEmail&lt;/code&gt;. Armed with the caller's context it can chain calls—e.g., fetch last-30-days spend, compare against today, then craft a spike alert—before deciding to deliver or discard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-line &amp;amp; mixed-format receipts&lt;/strong&gt; – Many vendors stuff several SKUs in one email while others attach PDF invoices. We built a two-pass extractor: first an HTML/Markdown pass, then (if needed) a PDF→text pass. Parsed line-items roll up into a single transaction but stay individually searchable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False-positive guardrail&lt;/strong&gt; – Not every email that looks like a receipt is one. A lightweight classifier checks sender domain + template heuristics + AI confidence; uncertain messages get labeled "informational" so they don't pollute the ledger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency &amp;amp; duplicates&lt;/strong&gt; – Postmark's webhook ID plus an email hash ensure the same email can't be processed twice—even across redeploys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Biggest win? Keeping the architecture modular: Postmark at the center, tiny adapters at the edge, and AI doing the heavy lifting in between. It meant I spent more time polishing the user experience (and adding dry-run / WCAG formatting) instead of wrangling infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Agent Workflow Example
&lt;/h3&gt;

&lt;p&gt;Below is a real trace of MailBudget’s AI agent workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;parse_postmark_webhook&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123&lt;span class="o"&gt;)&lt;/span&gt;:
   → upsertTransaction&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;vendor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Amazon"&lt;/span&gt;, &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$39&lt;/span&gt;&lt;span class="s2"&gt;.99"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
   → queryAirtable&lt;span class="o"&gt;(&lt;/span&gt;last_30_days_spend&lt;span class="o"&gt;)&lt;/span&gt;
   → detect_spike&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;25%&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;# Detected a 32% spending spike&lt;/span&gt;
   → sendInsightEmail&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"user@example.com"&lt;/span&gt;, &lt;span class="nv"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"[Spike Alert] Coffee spending up 32%"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💭 Why It Matters
&lt;/h2&gt;

&lt;p&gt;Picture a coffee-shop founder drowning in hundreds of emailed Stripe receipts every month or a visually-impaired freelancer who relies on screen-readers to track deductible costs.&lt;br&gt;&lt;br&gt;
MailBudget turns those noisy inboxes into quiet ledgers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Saves hours of manual spreadsheet work for indie founders.&lt;/li&gt;
&lt;li&gt;Spots duplicate subscription charges before they become costly leaks.&lt;/li&gt;
&lt;li&gt;Gives accountants a single Airtable feed they can integrate with existing tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, less grunt work, fewer surprises, more informed financial decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  ♿ Accessibility
&lt;/h2&gt;

&lt;p&gt;Emails generated by MailBudget follow WCAG AA guidelines: proper semantic headings, high-contrast colour palette, and ALT text for every table cell so screen-reader users get the same insight as everyone else.&lt;br&gt;&lt;br&gt;
The public demo is zero-sign-up— just forward an email—so you can try it without technical hurdles.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom Domain&lt;/strong&gt; - configure our own custom domain that is easy to remember when forwarding emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outlook Cleanup Adapter&lt;/strong&gt; – plug-in already sketched, will extend the same OAuth flow to Microsoft 365.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV / QuickBooks exporter&lt;/strong&gt; – one-click hand-off to popular accounting suites.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The modular design (Postmark hub, adapters on the edge) makes each of these only a weekend project away.&lt;/p&gt;

&lt;p&gt;—-&lt;br&gt;
Update: &lt;/p&gt;

&lt;p&gt;Please change all API links to this: &lt;a href="http://54.164.243.239/" rel="noopener noreferrer"&gt;http://54.164.243.239/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>postmarkchallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>How Vidova.ai Can Elevate Your Technical Blog Posts on dev.to</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Thu, 08 Aug 2024 14:03:41 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/how-vidovaai-can-elevate-your-technical-blog-posts-on-devto-2502</link>
      <guid>https://dev.to/youneslaaroussi/how-vidovaai-can-elevate-your-technical-blog-posts-on-devto-2502</guid>
      <description>&lt;p&gt;Writing technical articles on &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt; is a fantastic way to share your knowledge, showcase your skills, and engage with a vibrant community of developers. But if you want to take your posts to the next level, incorporating visual content is essential. Whether you're demonstrating code, explaining software concepts, or showcasing a new tool, high-quality visuals can make your content more engaging and easier to understand. That’s where &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Visual Content Matters in Technical Writing
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/996212114" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Visuals are crucial in breaking down complex information into digestible pieces. They can illustrate concepts that might be difficult to explain with words alone, helping your readers grasp the material more quickly and retain it longer. Screenshots, GIFs, and videos are especially effective when writing about software development.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Vidova.ai Simplifies Visual Content Creation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; is designed to help you quickly and easily create high-quality visual content for your blog posts. Here’s how it can enhance your dev.to articles:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Effortless Screen Recording&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/996211786" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;With Vidova.ai, you can record your screen in just a few clicks. Whether you’re demonstrating a piece of code, showcasing a tool, or walking through a tutorial, Vidova.ai makes it easy to capture everything on your screen in stunning 4K resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggested Use:&lt;/strong&gt; Record a short video clip demonstrating how a piece of code works, and embed it in your dev.to post to give your readers a clear, visual understanding of the concept.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Built-in Video Editing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/996211330" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Vidova.ai comes with integrated editing tools that allow you to trim, cut, and enhance your videos without needing additional software. This means you can quickly polish your recordings before adding them to your article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggested Use:&lt;/strong&gt; After recording a screen session, use the built-in editor to highlight key parts of your demo, add annotations, or zoom in on important details.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;AI-Generated Captions&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/996210353" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ensure your videos are accessible to all readers with AI-generated captions. Vidova.ai automatically creates captions for your videos, saving you time and ensuring that your content reaches a wider audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggested Use:&lt;/strong&gt; Include a video with captions that explain a complicated process or concept, making it easier for non-native speakers and those with hearing impairments to follow along.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Custom Cursor Enhancements&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/996211739" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;With Vidova.ai, you can replace your system cursor with a custom SVG cursor that remains crisp at any zoom level. This is especially useful when you need to direct attention to specific areas of your screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggested Use:&lt;/strong&gt; When recording a tutorial, use the custom cursor to point out important elements in your UI, ensuring that your readers don’t miss any critical details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Vidova.ai is Perfect for dev.to Writers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time-Saving:&lt;/strong&gt; Quickly create, edit, and publish visual content without the need for multiple tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-Quality Output:&lt;/strong&gt; Produce professional-looking videos that enhance your technical content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailored for Developers:&lt;/strong&gt; Vidova.ai is designed with tech professionals in mind, making it the ideal tool for creating content that resonates with the dev.to community.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Incorporating visual content into your dev.to articles is a surefire way to increase engagement and provide more value to your readers. With &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt;, you can create stunning visual content that complements your technical writing, making your posts stand out in the crowded world of online content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Ready to enhance your dev.to articles?&lt;/strong&gt; Try &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova&lt;/a&gt; today and see the difference it can make in your content creation process.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>ai</category>
      <category>productivity</category>
      <category>news</category>
    </item>
    <item>
      <title>This one tool will Take your Landing Pages to the Next Level</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Thu, 11 Jul 2024 02:38:26 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/this-one-tool-will-take-your-landing-pages-to-the-next-level-2l88</link>
      <guid>https://dev.to/youneslaaroussi/this-one-tool-will-take-your-landing-pages-to-the-next-level-2l88</guid>
      <description>&lt;p&gt;OBS Studio has long been a staple for streamers and content creators, but for developers and technical professionals seeking streamlined functionality and ease of use, OBS often falls short. This is particularly true when trying to integrate features like AI-generated captions or displaying keyboard actions—tasks that can become tangled in a web of plugins and configurations. Here’s why &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; offers a superior alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛑 The Limitations of OBS for Simple Enhancements
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978769108" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;OBS, while powerful, complicates what should be straightforward. Adding basic functionalities such as AI captions or displaying keyboard actions usually involves navigating through multiple plugins, some of which are not free. This can quickly become a frustrating and costly endeavor.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Enter Vidova.ai: A Tailored Solution
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978769125" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; is designed to cut through the complexity, offering a seamless and intuitive screen recording experience tailored for tech professionals. It simplifies every aspect of screen recording and editing, ensuring that you can focus more on creating and less on configuring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;👌 User-Friendly Interface:&lt;/strong&gt; Quickly start recording with an intuitive setup that bypasses the steep learning curve associated with OBS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔧 Integrated Developer Features:&lt;/strong&gt; Enjoy built-in support for AI captions and displaying keyboard shortcuts during recordings—no plugins or additional purchases necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎥 Efficient Editing and Recording:&lt;/strong&gt; Capture and edit high-quality videos up to 4K at 60 FPS with integrated tools designed for productivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🖱️ Advanced Cursor Enhancement
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978769090" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A standout feature of &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; is its ability to replace your system cursor with a high-quality SVG cursor during recordings. This not only enhances the visual appeal of your videos but also offers optional smoothing of cursor motion, creating a sleek, glide-like movement that can make tutorials and demonstrations significantly more engaging and easier to follow.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🌟 Benefits of Vidova's SVG Cursor Enhancements:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔍 Enhanced Clarity:&lt;/strong&gt; The high-resolution SVG cursor remains crisp and clear at all zoom levels, making it ideal for high-definition recordings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌊 Smooth Motion:&lt;/strong&gt; The optional smooth glide feature makes cursor movements fluid and easy to track, reducing visual clutter and enhancing viewer comprehension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💼 Professional Aesthetics:&lt;/strong&gt; The sleek cursor design contributes to a more polished and professional-looking video, setting your content apart from others.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔁 Why Make the Switch to Vidova.ai
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978771055" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you’re still using OBS out of habit, consider these compelling reasons to switch to &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🚫 No More Plugin Hassles:&lt;/strong&gt; Say goodbye to the complexity of plugins for basic features. Vidova.ai offers these functionalities out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 Tailored for Creators:&lt;/strong&gt; Unlike OBS, which is designed for a broad audience, Vidova.ai is specifically crafted to support the workflows of developers and tech educators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚙️ Streamlined Design:&lt;/strong&gt; Focus on creating content with a tool that is both powerful and easy to use, designed to enhance your productivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤝 Join the Vidova.ai Community
&lt;/h2&gt;

&lt;p&gt;Choosing &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; means joining a community of like-minded tech professionals who value efficiency and quality. Your feedback and experiences help shape the software, ensuring that it continuously evolves to meet the specific needs of its users. Vidova.ai isn't just about providing a tool; it's about fostering a collaborative community that enhances everyone's screen recording experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://discord.gg/55wgwerYvy" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2F0%2AX60YJNSu9WW4NkpJ" alt="Join Discord" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎬  Final Words
&lt;/h2&gt;

&lt;p&gt;It's time to move away from the cumbersome OBS and embrace a tool that truly aligns with your needs as a developer or tech educator. Vidova.ai combines ease of use with powerful features, making it the ideal choice for those who want to produce high-quality, professional-looking videos without the hassle of complex setups and plugins.&lt;/p&gt;

&lt;p&gt;Say goodbye to the generic approach of OBS and welcome the tailored efficiency of &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt;. Enhance your productivity and elevate your content with a tool designed specifically for tech professionals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚀 For Teams:&lt;/strong&gt; Vidova.ai is also perfect for teams looking to enhance their collaborative projects and streamline their screen recording processes. For team inquiries or to discuss how Vidova.ai can benefit your organization, please reach out directly to me at &lt;a href="mailto:ceo@vidova.ai"&gt;ceo@vidova.ai&lt;/a&gt;. Let’s optimize your team's creative potential with Vidova.ai.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Don’t wait!&lt;/strong&gt; Join us at Vidova.ai and become part of a movement that’s redefining what screen recording software can do. Sign up today and start transforming the way you create and share your projects.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>news</category>
      <category>career</category>
    </item>
    <item>
      <title>Stop Using OBS: Why Vidova.ai is the Screen Recorder You Didn’t Know You Needed</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sat, 06 Jul 2024 05:04:11 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/stop-using-obs-why-vidovaai-is-the-screen-recorder-you-didnt-know-you-needed-43pg</link>
      <guid>https://dev.to/youneslaaroussi/stop-using-obs-why-vidovaai-is-the-screen-recorder-you-didnt-know-you-needed-43pg</guid>
      <description>&lt;p&gt;OBS Studio has long been a staple for streamers and content creators, but for developers and technical professionals seeking streamlined functionality and ease of use, OBS often falls short. This is particularly true when trying to integrate features like AI-generated captions or displaying keyboard actions—tasks that can become tangled in a web of plugins and configurations. Here’s why &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; offers a superior alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛑 The Limitations of OBS for Simple Enhancements
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978769108" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;OBS, while powerful, complicates what should be straightforward. Adding basic functionalities such as AI captions or displaying keyboard actions usually involves navigating through multiple plugins, some of which are not free. This can quickly become a frustrating and costly endeavor.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Enter Vidova.ai: A Tailored Solution
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978769125" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; is designed to cut through the complexity, offering a seamless and intuitive screen recording experience tailored for tech professionals. It simplifies every aspect of screen recording and editing, ensuring that you can focus more on creating and less on configuring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;👌 User-Friendly Interface:&lt;/strong&gt; Quickly start recording with an intuitive setup that bypasses the steep learning curve associated with OBS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔧 Integrated Developer Features:&lt;/strong&gt; Enjoy built-in support for AI captions and displaying keyboard shortcuts during recordings—no plugins or additional purchases necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎥 Efficient Editing and Recording:&lt;/strong&gt; Capture and edit high-quality videos up to 4K at 60 FPS with integrated tools designed for productivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🖱️ Advanced Cursor Enhancement
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978769090" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A standout feature of &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; is its ability to replace your system cursor with a high-quality SVG cursor during recordings. This not only enhances the visual appeal of your videos but also offers optional smoothing of cursor motion, creating a sleek, glide-like movement that can make tutorials and demonstrations significantly more engaging and easier to follow.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🌟 Benefits of Vidova's SVG Cursor Enhancements:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔍 Enhanced Clarity:&lt;/strong&gt; The high-resolution SVG cursor remains crisp and clear at all zoom levels, making it ideal for high-definition recordings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌊 Smooth Motion:&lt;/strong&gt; The optional smooth glide feature makes cursor movements fluid and easy to track, reducing visual clutter and enhancing viewer comprehension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💼 Professional Aesthetics:&lt;/strong&gt; The sleek cursor design contributes to a more polished and professional-looking video, setting your content apart from others.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔁 Why Make the Switch to Vidova.ai
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/978771055" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you’re still using OBS out of habit, consider these compelling reasons to switch to &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🚫 No More Plugin Hassles:&lt;/strong&gt; Say goodbye to the complexity of plugins for basic features. Vidova.ai offers these functionalities out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 Tailored for Creators:&lt;/strong&gt; Unlike OBS, which is designed for a broad audience, Vidova.ai is specifically crafted to support the workflows of developers and tech educators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚙️ Streamlined Design:&lt;/strong&gt; Focus on creating content with a tool that is both powerful and easy to use, designed to enhance your productivity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤝 Join the Vidova.ai Community
&lt;/h2&gt;

&lt;p&gt;Choosing &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt; means joining a community of like-minded tech professionals who value efficiency and quality. Your feedback and experiences help shape the software, ensuring that it continuously evolves to meet the specific needs of its users. Vidova.ai isn't just about providing a tool; it's about fostering a collaborative community that enhances everyone's screen recording experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://discord.gg/55wgwerYvy" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2F0%2AX60YJNSu9WW4NkpJ" alt="Join Discord" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎬  Final Words
&lt;/h2&gt;

&lt;p&gt;It's time to move away from the cumbersome OBS and embrace a tool that truly aligns with your needs as a developer or tech educator. Vidova.ai combines ease of use with powerful features, making it the ideal choice for those who want to produce high-quality, professional-looking videos without the hassle of complex setups and plugins.&lt;/p&gt;

&lt;p&gt;Say goodbye to the generic approach of OBS and welcome the tailored efficiency of &lt;a href="https://vidova.ai" rel="noopener noreferrer"&gt;Vidova.ai&lt;/a&gt;. Enhance your productivity and elevate your content with a tool designed specifically for tech professionals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚀 For Teams:&lt;/strong&gt; Vidova.ai is also perfect for teams looking to enhance their collaborative projects and streamline their screen recording processes. For team inquiries or to discuss how Vidova.ai can benefit your organization, please reach out directly to me at &lt;a href="mailto:ceo@vidova.ai"&gt;ceo@vidova.ai&lt;/a&gt;. Let’s optimize your team's creative potential with Vidova.ai.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Don’t wait!&lt;/strong&gt; Join us at Vidova.ai and become part of a movement that’s redefining what screen recording software can do. Sign up today and start transforming the way you create and share your projects.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>news</category>
      <category>microsoft</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Build a Telegram Bot using Typescript &amp; Node.js</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Mon, 02 Jan 2023 18:36:14 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/how-to-build-a-telegram-bot-using-typescript-nodejs-3j5e</link>
      <guid>https://dev.to/youneslaaroussi/how-to-build-a-telegram-bot-using-typescript-nodejs-3j5e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you’ll learn all the steps that go into building a dead-simple Telegram bot and hosting it on the cloud. It will greet people and apply ＦＡＮＣＹ text effects.&lt;/p&gt;

&lt;p&gt;You’ll be writing code using the Typescript language and running it on the Node.js server environment.&lt;/p&gt;

&lt;p&gt;And seeing as Telegram bots are built on an HTTP-based API, you’ll be using the &lt;a href="https://grammy.dev/" rel="noopener noreferrer"&gt;GrammY framework&lt;/a&gt; for higher-level abstractions and a better programming experience.&lt;/p&gt;

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

&lt;p&gt;Before writing any code, make sure to have the following programs installed on your computer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://telegram.org/" rel="noopener noreferrer"&gt;Telegram client&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/download/" rel="noopener noreferrer"&gt;Node.js v14+&lt;/a&gt;, NPM v6+, and cURL.&lt;/li&gt;
&lt;li&gt;VS Code, or any other IDE of choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll be using &lt;a href="https://www.cyclic.sh/" rel="noopener noreferrer"&gt;Cyclic&lt;/a&gt; to host this project on the cloud, so make sure to &lt;a href="https://app.cyclic.sh/#/join/eludadev" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; (referral link) and take advantage of the &lt;strong&gt;Free Forever&lt;/strong&gt; tier.&lt;/p&gt;

&lt;p&gt;And while you don’t have to be an expert in it, you should know a bit of Typescript. You’ll be using it to write all the code in this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup the Project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Downloading the starter files
&lt;/h3&gt;

&lt;p&gt;Start-up this project by cloning the final version into your computer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/eludadev/telegram-bot.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telegram-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And make sure to roll it back to its very first stage, so you can learn how to build the rest of it in this tutorial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; 3ea99a5e111e84da4825b0732d76c386b5c8fdda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that’s done, install the project’s dependencies:&lt;br&gt;
&lt;/p&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;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting your Telegram bot API token
&lt;/h3&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%2Fp5kkpfgtg7kzt38n91s7.gif" 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%2Fp5kkpfgtg7kzt38n91s7.gif" alt="Getting your Telegram bot API token" width="760" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the Telegram client, start a conversation with the &lt;a href="https://telegram.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;. Send it the following messages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/newbot&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Your bot name&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;_Your bot username _(can’t contain spaces and &lt;strong&gt;must&lt;/strong&gt; end in “bot”)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, create a new file in the project’s root directory called &lt;code&gt;.env&lt;/code&gt;. Paste the following line into that file, replacing &lt;code&gt;&amp;lt;YOUR-API-TOKEN&amp;gt;&lt;/code&gt; with your bot’s API token that you just got from the last message with &lt;a href="https://telegram.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELEGRAM_TOKEN=&amp;lt;YOUR-API-TOKEN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initializing the Telegram bot
&lt;/h3&gt;

&lt;p&gt;Create a new file called &lt;code&gt;bot.ts&lt;/code&gt; in the &lt;code&gt;src/&lt;/code&gt; directory. That’s where you’ll be writing code for the rest of this tutorial.&lt;/p&gt;

&lt;p&gt;You’ll be using the &lt;a href="https://grammy.dev/" rel="noopener noreferrer"&gt;GrammY framework&lt;/a&gt; to build this bot; it’s much easier this way instead of interacting directly with the &lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;API routes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Head into the bot script and import the library in question:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Bot&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;grammy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Create a bot using the Telegram token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bot&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;Bot&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELEGRAM_TOKEN&lt;/span&gt; &lt;span class="o"&gt;||&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;As you can see, the bot is created with the &lt;code&gt;TELEGRAM_TOKEN&lt;/code&gt; variable that you just set in the &lt;code&gt;.env&lt;/code&gt; file. After that, handle all message events by responding with a friendly robot introduction:&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;introductionMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Hello! I'm a Telegram bot.
I'm powered by Cyclic, the next-generation serverless computing platform.

&amp;lt;b&amp;gt;Commands&amp;lt;/b&amp;gt;
/yo - Be greeted by me
/effect [text] - Show a keyboard to apply text effects to [text]`&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;replyWithIntro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;introductionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;parse_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTML&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;bot&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;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replyWithIntro&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take note of the &lt;code&gt;parse_mode&lt;/code&gt; parameter. It’s used to allow HTML tags in the message response, such as &lt;code&gt;&amp;lt;b&amp;gt;Commands&amp;lt;/b&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And finally, start-up the bot by running one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow that by executing the bot script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F9u6tnyckeg9h9wys1zo1.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%2F9u6tnyckeg9h9wys1zo1.png" alt="Follow that by executing the bot script" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And boom! You’ve got yourself a dead-simple bot running on your computer. You can test-it out by sending it a message on Telegram; you may want to follow the link previously given to you by &lt;a href="https://telegram.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Basic Commands
&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%2Fy2np783xikyxjcynwubo.gif" 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%2Fy2np783xikyxjcynwubo.gif" alt="Handle Basic Commands" width="796" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Telegram bots can handle commands, which aren’t much different from ordinary messages. The syntax for such interactions follows the following format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replyWithIntro&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it out by sending the &lt;code&gt;/start&lt;/code&gt; message to your bot. Note that your program is automatically updated once you modify the bot script.&lt;/p&gt;

&lt;p&gt;After that’s done, modify your bot to handle the &lt;code&gt;/yo&lt;/code&gt; command. It will simply respond with the username of the sender:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Handle the /yo command to greet the user&lt;/span&gt;
&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yo&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;ctx&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Yo &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;username&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since the &lt;code&gt;bot.on(“message”)&lt;/code&gt; event handler is on top, it will catch all messages and the /start and /yo commands won’t have an effect. Please make sure to always keep it at the bottom of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Keep this at the bottom of the file&lt;/span&gt;
&lt;span class="nx"&gt;bot&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;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replyWithIntro&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build Inline Keyboards
&lt;/h2&gt;

&lt;p&gt;Telegram bots can also respond with a set of buttons underneath the message.&lt;/p&gt;

&lt;p&gt;Let’s do a simple demonstration. Change the introductory response to also contain a button that links users to the &lt;a href="https://www.cyclic.sh/" rel="noopener noreferrer"&gt;Cyclic&lt;/a&gt; website, the platform that we’ll use to deploy our bot to the cloud for free at the end of this tutorial:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InlineKeyboard&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;grammy&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;aboutUrlKeyboard&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;InlineKeyboard&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Host your own bot for free.&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;https://cyclic.sh/&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;replyWithIntro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;introductionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;reply_markup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aboutUrlKeyboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parse_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTML&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this step, you may want to re-execute the bot script after interrupting it with the &lt;code&gt;Ctrl+C&lt;/code&gt; keyboard combination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A more advanced example
&lt;/h3&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%2Fnne1pkmpvxbtiyn0vnvh.gif" 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%2Fnne1pkmpvxbtiyn0vnvh.gif" alt="A more advanced example" width="796" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s now do a more advanced example. Other inline keyboards may contain general buttons, whose actions can be handled to the furthest extent.&lt;/p&gt;

&lt;p&gt;Handle the &lt;code&gt;/effect&lt;/code&gt;command, and make it apply bold, italic, and a bunch more effects to text:&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;import&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lodash&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;applyTextEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Variant&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;./textEffects&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Variant&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TextEffectVariant&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;./textEffects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TextEffectVariant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allEffects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;[]&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Monospace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Italic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Doublestruck&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;o&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Circled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Squared&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;effectCallbackCodeAccessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effectCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TextEffectVariant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`effect-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;effectCode&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;effectsKeyboardAccessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effectCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;effectsAccessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;effectCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;effectCodes&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;code&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;allEffects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;effect&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;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;effects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;effectsAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effectCodes&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;keyboard&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;InlineKeyboard&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;chunkedEffects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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;effectsChunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunkedEffects&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;const&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;effectsChunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;effectCallbackCodeAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;row&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;keyboard&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;textEffectResponseAccessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;modifiedText&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`Original: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modifiedText&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\nModified: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;modifiedText&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="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;parseTextEffectResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;modifiedText&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;originalText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/Original: &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modifiedTextMatch&lt;/span&gt; &lt;span class="o"&gt;=&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/Modified: &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;modifiedText&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;modifiedTextMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;modifiedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;modifiedTextMatch&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="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;modifiedTextMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;originalText&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;modifiedText&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Handle the /effect command to apply text effects using an inline keyboard&lt;/span&gt;
&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;effect&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;ctx&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textEffectResponseAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;reply_markup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;effectsKeyboardAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;allEffects&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;effect&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;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Handle text effects from the effect keyboard&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;effect&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;allEffects&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;allEffectCodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allEffects&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;effect&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;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callbackQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;effectCallbackCodeAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;originalText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseTextEffectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&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;modifiedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyTextEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;la&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editMessageText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;textEffectResponseAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;modifiedText&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;reply_markup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;effectsKeyboardAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;allEffectCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handle Inline Queries
&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%2Forrdm9ywpsdm1jfhpgj6.gif" 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%2Forrdm9ywpsdm1jfhpgj6.gif" alt="Handle Inline Queries" width="796" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Telegram bots support inline queries, a feature which enables them to be invoked from any chat within Telegram by calling them with their “@” username. Let’s use this to allow users to apply text effects in any conversation using your bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling inline mode for your Telegram bot
&lt;/h3&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%2F0xy5i79t4pe9umec5org.gif" 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%2F0xy5i79t4pe9umec5org.gif" alt="Enabling inline mode for your Telegram bot" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By default, this feature comes disabled. Contact &lt;a href="https://telegram.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt; to enable it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/mybots&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Select your bot from the inline keyboard&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Bot settings&lt;/li&gt;
&lt;li&gt;Inline mode&lt;/li&gt;
&lt;li&gt;Turn on&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Handling the “effect” inline query
&lt;/h3&gt;

&lt;p&gt;Inline queries are generally handled by matching a RegEx pattern. We’ll listen for the “effect [effect] [text]” query and handle it by applying [effect] to [text]:&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;queryRegEx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/effect &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;monospace|bold|italic&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inlineQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRegEx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inlineQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullQueryMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fullQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRegEx&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;fullQueryMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;effectLabel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fullQueryMatch&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fullQueryMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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;effectCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allEffects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&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;effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;effectLabel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modifiedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyTextEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effectCode&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Variant&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answerInlineQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;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;article&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-effect&lt;/span&gt;&lt;span class="dl"&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;Text Effects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;input_message_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Original: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Modified: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;modifiedText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;parse_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTML&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;reply_markup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InlineKeyboard&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;switchInline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Share&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullQuery&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://t.me/EludaDevSmarterBot&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;Create stylish Unicode text, all within Telegram.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// one month in seconds&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;
  
  
  Polish the Telegram Bot
&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%2Fdf56csu0cdp5ejzk8i2g.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%2Fdf56csu0cdp5ejzk8i2g.png" alt="Polish the Telegram Bot" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s often useful for Telegram bots to display a list of supported commands, and while we’re already doing that in the introductory message, there’s a more formal way of doing so, and it’s as simple as one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Suggest commands in the menu&lt;/span&gt;
&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMyCommands&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yo&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;Be greeted by the bot&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;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;effect&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;Apply text effects on the text. (usage: /effect [text])&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;Note that to see the new menu, you must restart your Telegram client.&lt;/p&gt;

&lt;p&gt;Furthermore, professional bots come with a profile picture and a well-formed description. You can do all of that by contacting &lt;a href="https://telegram.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Edit bot&lt;/li&gt;
&lt;li&gt;Edit about / Edit description / Edit botpic&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deploy the Telegram Bot to the Cloud
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Long Polling vs Webhooks
&lt;/h3&gt;

&lt;p&gt;There are two fundamentally different ways of deploying your Telegram bot to the web.&lt;/p&gt;

&lt;p&gt;The first one is &lt;a href="https://grammy.dev/guide/deployment-types.html#how-does-long-polling-work" rel="noopener noreferrer"&gt;Long Polling&lt;/a&gt;, and we’ve already been using it in this tutorial by running &lt;code&gt;bot.start()&lt;/code&gt;. With it, bots constantly send requests to the Telegram servers checking for new messages, and responding to them accordingly.&lt;/p&gt;

&lt;p&gt;This approach is not compatible with the serverless architecture, as the latter expects applications to only run once, and only on-demand.&lt;/p&gt;

&lt;p&gt;“Serverless means applications are only on for the time it takes to process individual requests. They are suspended immediately after each response is sent.” — &lt;a href="https://docs.cyclic.sh/serverless/on-demand" rel="noopener noreferrer"&gt;Cyclic docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The solution to this problem is deployment by &lt;a href="https://grammy.dev/guide/deployment-types.html#how-do-webhooks-work" rel="noopener noreferrer"&gt;Webhooks&lt;/a&gt;, an alternative strategy that makes the Telegram client itself contact your bot when there’s a new message. And while this comes with &lt;a href="https://grammy.dev/guide/deployment-types.html#webhook-reply" rel="noopener noreferrer"&gt;its own drawbacks&lt;/a&gt;, it’s fully compatible with serverless architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Webhooks for deployment
&lt;/h3&gt;

&lt;p&gt;By following the &lt;code&gt;NODE_ENV&lt;/code&gt; environment variable, we can tell whether the bot instance is running in a development or a production stage. Replace the &lt;code&gt;bot.start()&lt;/code&gt; command with the following:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;webhookCallback&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;grammy&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;express&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;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Start the server&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&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="c1"&gt;// Use Webhooks for the production server&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;webhookCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&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;PORT&lt;/span&gt; &lt;span class="o"&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&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="s2"&gt;`Bot listening on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use Long Polling for development&lt;/span&gt;
  &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;h3&gt;
  
  
  Pushing all files to a Github Repository
&lt;/h3&gt;

&lt;p&gt;It’s imperative that we use a Github repository to store our bot files so we can deploy it to the cloud. After &lt;a href="https://github.com/new" rel="noopener noreferrer"&gt;creating a new repository&lt;/a&gt; (either public or private), run the following commands to link it with your local Git instance, replacing &lt;code&gt;&amp;lt;YOUR-GH-REPO-LINK&amp;gt;&lt;/code&gt; with your repo’s URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GH_REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR-GH-REPO-LINK&amp;gt;"&lt;/span&gt;
git remote remove origin
git remote add origin &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GH_REPO&lt;/span&gt;&lt;span class="s2"&gt;.git"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, stage, commit, and push your files to the new remote origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Build the Telegram bot."&lt;/span&gt;
git branch &lt;span class="nt"&gt;-M&lt;/span&gt; main
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploying for free using Cyclic
&lt;/h3&gt;

&lt;p&gt;After &lt;a href="https://app.cyclic.sh/#/join/eludadev" rel="noopener noreferrer"&gt;creating your Cyclic account &lt;/a&gt;(referral link), use it to deploy your new Telegram bot.&lt;/p&gt;

&lt;p&gt;It’s free forever, and no credit card is required.&lt;/p&gt;

&lt;p&gt;Note that you must sign up using the same Github account that you used to create your bot’s repository.&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%2Fs1clo1z99prwsgxgazo5.gif" 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%2Fs1clo1z99prwsgxgazo5.gif" alt="Click on the Deploy button and switch to the “Link your own” tab." width="600" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open your &lt;a href="https://app.cyclic.sh/#/" rel="noopener noreferrer"&gt;Cyclic dashboard&lt;/a&gt;, click on the Deploy button and switch to the “Link your own” tab. Search for your bot’s repo and click on the “Connect” button. Then sit back and watch it do all the work for you!&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%2Faew5x8u2vx1atixu4vvt.gif" 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%2Faew5x8u2vx1atixu4vvt.gif" alt="Setting the same environment variables." width="614" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final step is setting the same environment variables as you did in the &lt;code&gt;.env&lt;/code&gt; file. Open your Cyclic deployment’s dashboard page, switch to the Variables page and set the appropriate values for the environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NODE_ENV:&lt;/strong&gt; production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TELEGRAM_TOKEN:&lt;/strong&gt; your bot’s API token (same as &lt;code&gt;.env&lt;/code&gt; file)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Connecting your Telegram bot to your Cyclic server
&lt;/h3&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%2Fns4j2zltctynpnmnykli.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%2Fns4j2zltctynpnmnykli.png" alt="Connecting your Telegram bot to your Cyclic server" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You want to tell Telegram to send the Webhook requests to your Cyclic server. So conclude this project by copying your Cyclic deployment’s URL and running these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TELEGRAM_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;... &lt;span class="c"&gt;# YOUR TELEGRAM API TOKEN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TELEGRAM_WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;... &lt;span class="c"&gt;# YOUR CYCLIC DEPLOYMENT URL&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.telegram.org/bot&lt;/span&gt;&lt;span class="nv"&gt;$TELEGRAM_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;/setWebhook?url=&lt;/span&gt;&lt;span class="nv"&gt;$TELEGRAM_WEBHOOK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And boom! Your bot’s now actively running on the cloud! You may stop your local development instance by pressing &lt;code&gt;Ctrl+C&lt;/code&gt; and notice how your bot is still working.&lt;/p&gt;

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

&lt;p&gt;You’ve successfully reached the end of this tutorial! There are many more things you could add to this bot, including but not limited to &lt;a href="https://grammy.dev/guide/games.html" rel="noopener noreferrer"&gt;games&lt;/a&gt;, &lt;a href="https://grammy.dev/guide/errors.html" rel="noopener noreferrer"&gt;error handling&lt;/a&gt; and &lt;a href="https://grammy.dev/plugins/i18n.html" rel="noopener noreferrer"&gt;internationalization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You may also want to learn more about Telegram bots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://grammy.dev/guide/inline-queries.html" rel="noopener noreferrer"&gt;https://grammy.dev/guide/inline-queries.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grammy.dev/plugins/keyboard.html" rel="noopener noreferrer"&gt;https://grammy.dev/plugins/keyboard.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grammy.dev/guide/deployment-types.html" rel="noopener noreferrer"&gt;https://grammy.dev/guide/deployment-types.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here are some resources to advance your knowledge about serverless computing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cyclic.sh/vs-heroku" rel="noopener noreferrer"&gt;Cyclic vs Heroku&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cyclic.sh/posts/considerations-for-serverless-active-active-routing/" rel="noopener noreferrer"&gt;Considerations for Serverless Active-Active: Routing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cyclic.sh/posts/how-to-fail-at-serverless-serverless-is-stateless/" rel="noopener noreferrer"&gt;How to Fail at Serverless: Serverless is Stateless&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cyclic.sh/tutorials/rest-api-and-dynamodb/part-1" rel="noopener noreferrer"&gt;Creating and Deploying a RESTful API on Serverless Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>welcome</category>
    </item>
    <item>
      <title>12 Rarely Used JavaScript Web APIs that Will Boost Your Website to THE MOON 🚀</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sat, 23 Jul 2022 07:43:00 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/12-rarely-used-javascript-web-apis-that-will-take-your-website-to-the-next-level-4lf1</link>
      <guid>https://dev.to/youneslaaroussi/12-rarely-used-javascript-web-apis-that-will-take-your-website-to-the-next-level-4lf1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;There are over 97 Web APIs, and you’re only using 5% of them. Let’s unlock that other 95 percent!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After taking a long stroll down the dark realms of the spec, I realized that there were so many technologies out there being left-out.&lt;/p&gt;

&lt;p&gt;My goal with this article is to bring them to the light. And I’ll prove to you, using practical examples, that some APIs are really worth a shot!&lt;/p&gt;

&lt;p&gt;Each section will thoroughly examine each API and provide a fun example that beautifully demonstrates a practical use-case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ad: Like freebies? 🤑
&lt;/h2&gt;

&lt;p&gt;I made a pack of 100 free hover animations. &lt;a href="https://www.producthunt.com/posts/100-css-buttons?ref=dev" rel="noopener noreferrer"&gt;Get it now&lt;/a&gt;, share it, and do whatever you want with it. It's yours forever! ❤️&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.producthunt.com/posts/100-css-buttons?ref=dev" rel="noopener noreferrer"&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%2Fqvcivn0t2ae0tzwtwtc1.png" alt="Every style imaginable." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📑 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🤳 Screen Orientation API&lt;/li&gt;
&lt;li&gt;📺️ Fullscreen API&lt;/li&gt;
&lt;li&gt;📲 Intersection Observer API&lt;/li&gt;
&lt;li&gt;💤 Screen Wake Lock API&lt;/li&gt;
&lt;li&gt;💻️ Screen Capture API&lt;/li&gt;
&lt;li&gt;📔 IndexedDB API&lt;/li&gt;
&lt;li&gt;🧠 Local and Session Storage APIs&lt;/li&gt;
&lt;li&gt;🎨 Houdini API&lt;/li&gt;
&lt;li&gt;🕸️ Web Share API&lt;/li&gt;
&lt;li&gt;📋️ Clipboard API&lt;/li&gt;
&lt;li&gt;✒️ Selection API&lt;/li&gt;
&lt;li&gt;👀 Page Visibility API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Warning about portrait mode
&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%2Flh3.googleusercontent.com%2F9TGRzd_Au_EmpwScLyUw-IOWeAweNTM9sJQN5_2Dp8tl8W7Xu6ClOKL1iCr9Pob-NxpP36gj0shbazYWb1oTocZADcQ2av8ZiUzHladIrSYOH3hlIe1GAL6ft4fGUlDPbjrFuEKbLdVVuhIAwM0" 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%2Flh3.googleusercontent.com%2F9TGRzd_Au_EmpwScLyUw-IOWeAweNTM9sJQN5_2Dp8tl8W7Xu6ClOKL1iCr9Pob-NxpP36gj0shbazYWb1oTocZADcQ2av8ZiUzHladIrSYOH3hlIe1GAL6ft4fGUlDPbjrFuEKbLdVVuhIAwM0" alt="Screen is too narrow. try in landscape mode." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screen is too narrow. try in landscape mode.&lt;/p&gt;

&lt;p&gt;Some apps like non-linear video editors were not meant for vertical devices: they just don’t work well in narrow screens!&lt;/p&gt;

&lt;p&gt;Sure, the &lt;a href="https://www.smashingmagazine.com/2011/01/guidelines-for-responsive-web-design/" rel="noopener noreferrer"&gt;web is supposed to be responsive&lt;/a&gt;, but it’s not always worth it to port a whole wide layout to a narrow display.&lt;/p&gt;

&lt;p&gt;Wouldn’t it be nice if we could warn our users when their device is rotated in the wrong direction? Let me introduce to you… the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation" rel="noopener noreferrer"&gt;Screen Orientation API&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;In order to avoid errors, it’s important to check support for the Screen Orientation API. This is as simple as: &lt;code&gt;if ('orientation' in screen)&lt;/code&gt;. You’ll see this pattern again and again throughout this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Screen Orientation API
&lt;/h3&gt;

&lt;p&gt;Browsers expose a global variable named &lt;code&gt;screen&lt;/code&gt;, which we’ll use to access the information we need. The &lt;code&gt;[ScreenOrientation](https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation)&lt;/code&gt; instance can be accessed with &lt;code&gt;screen.orientation&lt;/code&gt;. We’ll be working with this object throughout this section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detecting the screen orientation
&lt;/h3&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%2Fnzaxnpvword2zs2oifz9.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%2Fnzaxnpvword2zs2oifz9.png" alt="Screen Orientation types and angles: https://w3c.github.io/screen-orientation/#dfn-screen-orientation-values-table" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screen Orientation types and angles: &lt;a href="https://w3c.github.io/screen-orientation/#dfn-screen-orientation-values-table" rel="noopener noreferrer"&gt;https://w3c.github.io/screen-orientation/#dfn-screen-orientation-values-table&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contrary to popular belief, there are &lt;em&gt;four&lt;/em&gt; ways that a screen can be oriented, as can be seen in the picture above. But we’re only interested in knowing whether the screen is in portrait or landscape mode, and it’s easy to write a function that tells us exactly that:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrientation&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;isPortrait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;portrait&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isPortrait&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;portrait&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;landscape&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;h3&gt;
  
  
  Locking the screen orientation
&lt;/h3&gt;

&lt;p&gt;Native apps like Instagram lock the screen orientation while it’s in-use. As &lt;a href="https://www.smashingmagazine.com/2018/12/pwa-native-mobile-apps/" rel="noopener noreferrer"&gt;the line between PWAs and native apps&lt;/a&gt; is getting blurrier by the day, it’s not a surprise that this feature is also on the web.&lt;/p&gt;

&lt;p&gt;While &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation#browser_compatibility" rel="noopener noreferrer"&gt;less supported&lt;/a&gt;, it’s also possible to lock and unlock the screen orientation using this code snippet:&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="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Don’t forget to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock#exceptions" rel="noopener noreferrer"&gt;handle errors&lt;/a&gt; too, because as I already stated, this feature is not well-supported.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making your website a full-screen experience
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/ludviglindblom/embed/medXwN?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Browsers cover our websites with a lot of UI elements, distracting the user from what’s important.&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%2F5rww2zhgutyijg9hl2r8.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%2F5rww2zhgutyijg9hl2r8.png" alt="Screenshot of Chrome mobile, highlighting the browser’s UI elements." width="800" height="1423"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Screenshot of Chrome mobile, highlighting the browser’s UI elements.&lt;/p&gt;

&lt;p&gt;This is especially a problem when it comes to immersive content, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;movies,&lt;/li&gt;
&lt;li&gt;games,&lt;/li&gt;
&lt;li&gt;maximizing images.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the list goes on-and-on.&lt;/p&gt;

&lt;p&gt;Thankfully, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API" rel="noopener noreferrer"&gt;Fullscreen API&lt;/a&gt; comes and saves the day!&lt;/p&gt;

&lt;p&gt;This feature is very &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#browser_compatibility" rel="noopener noreferrer"&gt;well supported in all modern browsers&lt;/a&gt;, so don’t worry about using it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Entering fullscreen mode
&lt;/h3&gt;

&lt;p&gt;Surprisingly, any DOM element can enter fullscreen mode:&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="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestFullscreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;However, most of the time, we want the entire page to enter fullscreen. The root document element —&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;— can be accessed in JavaScript with &lt;code&gt;document.documentElement&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So it’s not unusual to see this code snippet floating around the web:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestFullscreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Leaving fullscreen mode
&lt;/h3&gt;

&lt;p&gt;There’s a variety of ways for exiting. Some of them are the browser-default keyboard shortcuts: &lt;code&gt;ESC&lt;/code&gt; and &lt;code&gt;F11&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There’s also the possibility to leave by switching tabs &lt;code&gt;Ctrl+Tab&lt;/code&gt; or jumping windows &lt;code&gt;Alt+Tab&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, the most important leaving mechanism is the one that &lt;em&gt;you&lt;/em&gt; — the developer — provide. You can programmatically disembark fullscreen mode with the following:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exitFullscreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In a deployment environment, however, it’s important to avoid errors by checking if this function exists before calling it:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitFullscreen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;    
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exitFullscreen&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;h3&gt;
  
  
  Verifying if the user is in fullscreen mode
&lt;/h3&gt;

&lt;p&gt;If we wanted to implement a fullscreen toggle as seen in &lt;a href="https://codepen.io/ludviglindblom/pen/medXwN" rel="noopener noreferrer"&gt;the Codepen at the start&lt;/a&gt;, we’d need a way to determine whether or not fullscreen mode is active.&lt;/p&gt;

&lt;p&gt;That’s totally possible with the following code snippet:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullscreenElement&lt;/span&gt; &lt;span class="c1"&gt;// returns 'null' or the fullscreen-enabled DOM element&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For better browser compatibility, we’d have to check for multiple attributes:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullscreenElement&lt;/span&gt;    
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mozFullscreenElement&lt;/span&gt;    
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msFullscreenElement&lt;/span&gt;    
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitFullscreenElement&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this in hand, we can implement a fullscreen toggle:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleFullScreen&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullscreenElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestFullscreen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitFullscreen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   
            &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exitFullscreen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
        &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Animating an element as it enters the viewport
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/eYMmvxq?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Consider all the times you needed to do something when an element enters into view:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Animating it,&lt;/li&gt;
&lt;li&gt;Loading more content (&lt;a href="https://www.smashingmagazine.com/2013/05/infinite-scrolling-lets-get-to-the-bottom-of-this/" rel="noopener noreferrer"&gt;Infinite Scrolling&lt;/a&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.smashingmagazine.com/2019/05/hybrid-lazy-loading-progressive-migration-native/" rel="noopener noreferrer"&gt;Lazy-loading an image&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;Registering ad revenue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One naive solution is calling &lt;code&gt;getBoundingClientRect&lt;/code&gt; on every scroll event. And I mean… it works!&lt;/p&gt;

&lt;p&gt;However, it’s terribly inefficient. It runs on the &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Main_thread" rel="noopener noreferrer"&gt;main thread&lt;/a&gt;, so the more event listeners that we register, the slower our app becomes.&lt;/p&gt;

&lt;p&gt;Thankfully, browser engineers have blessed us with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" rel="noopener noreferrer"&gt;Intersection Observer API&lt;/a&gt;: an efficient solution that delegates all optimizations to the browser, so that we — web developers — can focus on what’s important.&lt;/p&gt;

&lt;p&gt;We’re gonna make a pretty cool effect where  text elements are highlighted only when they enter into view, creating a sleek and modern animation that our readers will appreciate. See it with your own eyes in &lt;a href="https://codepen.io/eludadev/pen/eYMmvxq" rel="noopener noreferrer"&gt;the Codepen above&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating an observer
&lt;/h3&gt;

&lt;p&gt;Before we start listening for intersection events, we must to create an &lt;strong&gt;observer&lt;/strong&gt; object that handles all the background tasks:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="na"&gt;root&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="c1"&gt;// use viewport  &lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&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;observer&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;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You may have noticed &lt;code&gt;threshold&lt;/code&gt;. It’s an option that tells the browser to only trigger intersection events when &lt;em&gt;N%&lt;/em&gt; of the element is visible.&lt;/p&gt;
&lt;h3&gt;
  
  
  Handling intersection events
&lt;/h3&gt;

&lt;p&gt;Let’s define callback, a function that will be called once an intersection event occurs.&lt;/p&gt;

&lt;p&gt;We want to handle the event only when the element is shown at least N% in the viewport:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intersectionRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="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;It’s time to decide what to do with an element when it enters into view. In this case, we’ll simply assign it the &lt;code&gt;.active&lt;/code&gt; class name, while delegating the animation responsibilities to CSS.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intersectionRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&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;We can also “undo” this effect once it leaves the screen:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intersectionRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&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;For a deeper introduction to the IntersectionObserver API, read this &lt;a href="https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/" rel="noopener noreferrer"&gt;amazing article by Denys Mishunov&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preventing the screen from going dark
&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%2Flh6.googleusercontent.com%2FYDGDh4pkxK81gN5swTPFwTdTrtJFDIw4Mg0G-Gcu_7MdsCCHnu31cMZfKPyosmhQsw0VS3ZKsMEi8jOUxIs1ZpI0YKkTDYGWasmsPXOPllebv2i-97yWmWiO3IOeyk3vDxQGbbQDVThCgDT_SA" 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%2Flh6.googleusercontent.com%2FYDGDh4pkxK81gN5swTPFwTdTrtJFDIw4Mg0G-Gcu_7MdsCCHnu31cMZfKPyosmhQsw0VS3ZKsMEi8jOUxIs1ZpI0YKkTDYGWasmsPXOPllebv2i-97yWmWiO3IOeyk3vDxQGbbQDVThCgDT_SA" alt="Paused space-launch video on Youtube, showing the video player’s controls." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paused space-launch video on Youtube, showing the video player’s controls.&lt;/p&gt;

&lt;p&gt;Long-form videos require the screen to stay on even without any interaction. This behavior is usually seen on native applications, but with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API" rel="noopener noreferrer"&gt;Screen Wake Lock API&lt;/a&gt;, it’s on the web too!&lt;/p&gt;

&lt;p&gt;There are many other use-cases for this API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Online games,&lt;/li&gt;
&lt;li&gt;Presentations,&lt;/li&gt;
&lt;li&gt;Drawing on canvas,&lt;/li&gt;
&lt;li&gt;Camera,&lt;/li&gt;
&lt;li&gt;Streaming,&lt;/li&gt;
&lt;li&gt;Timers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the list never stops.&lt;/p&gt;

&lt;p&gt;Let’s delve deeper into its inner-workings!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Firefox and Safari have yet to support this feature. So it’s always a good idea to check its availability first, in order to avoid all kinds of errors: if ('wakelock' in navigator)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Acquiring wakelock
&lt;/h3&gt;

&lt;p&gt;A video player, such as Youtube, might acquire wakelock in its &lt;code&gt;play&lt;/code&gt; function:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;wakelock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// … &lt;/span&gt;
    &lt;span class="nx"&gt;wakelock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wakeLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the user’s battery is too low, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API#requesting_a_wake_lock" rel="noopener noreferrer"&gt;expect it to fail&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Releasing wakelock
&lt;/h3&gt;

&lt;p&gt;It’s bad practice to keep the wakelock on forever, as that will hurt the user’s battery and might even degrade performance. So make sure to always release it when possible:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;wakelock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&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;Whenever the user leaves your website’s tab, the wakelock is automatically released.&lt;/p&gt;

&lt;p&gt;In this case, you should re-acquirese it by listening to the &lt;code&gt;visibilitychange&lt;/code&gt; event, which we’ll learn more about &lt;a href="https://www.notion.so/Rarely-Used-JavaScript-Web-APIs-that-Will-Take-Your-Website-to-The-Next-Level-71849681706142f3bd7681ad2e4a753f" rel="noopener noreferrer"&gt;in another section&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But in a nutshell, it’s triggered when the user leaves/enters the website’s tab.&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wadocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wakelock&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&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;wakelock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wakeLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screen&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;h2&gt;
  
  
  Recording your screen
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/MWVYoOE?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;There’s an increasing number of web-based screen recording apps. But how exactly do they do it? The answer is surprisingly simple.&lt;/p&gt;

&lt;p&gt;The secret to their success is the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API" rel="noopener noreferrer"&gt;Screen Capture API,&lt;/a&gt; an easy-to-use interface that allows users to record their screen in a wide variety of ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whole screen,&lt;/li&gt;
&lt;li&gt;Specific window,&lt;/li&gt;
&lt;li&gt;Specific tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also comes with extra nifty features, including but not limited to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;blurring/covering overlapping windows to avoid accidentally sharing sensitive information,&lt;/li&gt;
&lt;li&gt;hiding/showing the cursor,&lt;/li&gt;
&lt;li&gt;recording sound.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Browser compatibility
&lt;/h3&gt;

&lt;p&gt;I hate to be the bearer of bad news, but &lt;a href="https://caniuse.com/mdn-api_mediadevices_getdisplaymedia" rel="noopener noreferrer"&gt;no mobile browsers support this API&lt;/a&gt; as of yet.&lt;/p&gt;

&lt;p&gt;On the other hand, it’s very well-supported by modern desktop navigators! (&lt;a href="https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/" rel="noopener noreferrer"&gt;except Internet Explorer&lt;/a&gt;, of course)&lt;/p&gt;
&lt;h3&gt;
  
  
  Starting screen capture
&lt;/h3&gt;

&lt;p&gt;With this delight of an API, recording the screen is shockingly simple:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;always&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// show the cursor&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// don't record audio&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDisplayMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// returns a promise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Would you believe me if I told you that was it? Well, I never lie.&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%2F3dwbpkbvgj8mw2zgoewr.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%2F3dwbpkbvgj8mw2zgoewr.png" alt="Screen Capture Prompt showing the 3 types: entire screen, window, tab." width="800" height="589"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Screen Capture Prompt showing the 3 types: entire screen, window, tab.&lt;/p&gt;

&lt;p&gt;The above function tells the browser to show a prompt for selecting the desired recording surface, as can be seen on the image above. (try it yourself in &lt;a href="https://codepen.io/eludadev/pen/MWVYoOE" rel="noopener noreferrer"&gt;the codepen at the start&lt;/a&gt; of this section)&lt;/p&gt;
&lt;h3&gt;
  
  
  Preview the recording
&lt;/h3&gt;

&lt;p&gt;It would be nice if we could see exactly what the website is seeing. Thankfully, that’s terribly easy to do:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"preview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And that was it for the HTML. Now, let’s move into the JavaScript logic:&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="nx"&gt;previewElem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;previewElem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDisplayMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That’s it! Now you can see what’s being recorded, in real time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Stopping screen capture
&lt;/h3&gt;

&lt;p&gt;With one method, we can achieve everything! Note that it’s important to have a preview element first, as demonstrated in the last subsection.&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tracks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previewElem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;tracks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&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;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nx"&gt;previewElem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Storing tabular data in an on-device database
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/VwXYbPr?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;There’s an entire NoSQL database system hidden right in your browser, and it’s accessed with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API" rel="noopener noreferrer"&gt;IndexedDB&lt;/a&gt; API!&lt;/p&gt;

&lt;p&gt;Each operation is asynchronous, so it never slows down other operations. It’s also cleared once the user erases the browser’s cache or locally-stored data.&lt;/p&gt;

&lt;p&gt;In addition to all of that, it also supports the usual search, get, and put actions, with transactions on the side. It can store almost all kinds of data, including but not limited to &lt;code&gt;File&lt;/code&gt;, images and videos as &lt;code&gt;Blob&lt;/code&gt;, and &lt;code&gt;String&lt;/code&gt; of course.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unfortunately, not all browsers agree on which kind of data should be supported. Safari on iOS, for example, cannot store Blob data. However, it’s possible to &lt;a href="https://web.dev/indexeddb-best-practices/#not-everything-can-be-stored-in-indexeddb-on-all-platforms" rel="noopener noreferrer"&gt;convert all other formats to an &lt;code&gt;ArrayBuffer&lt;/code&gt;&lt;/a&gt;, which is very well-supported by all platforms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Storage limit is not an issue, because &lt;a href="https://web.dev/storage-for-the-web/#how-much" rel="noopener noreferrer"&gt;most browsers allocate a bunch of space that your websites can consume freely&lt;/a&gt;. Also, each database is tied not only to a domain name, but to the very specific subdomain. To add to all of that, &lt;a href="https://caniuse.com/mdn-api_indexeddb" rel="noopener noreferrer"&gt;browser compatibility&lt;/a&gt; is not an issue at all, not even on IE11.&lt;/p&gt;

&lt;p&gt;There are many things that we can do with this treat of an API, including but not limited to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storing structured data for offline use,&lt;/li&gt;
&lt;li&gt;Speeding up the loading time for repeated visits,&lt;/li&gt;
&lt;li&gt;Caching user-generated data,&lt;/li&gt;
&lt;li&gt;Temporarily saving data before uploading it to a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s see how we can store contact data in IndexedDB!&lt;/p&gt;
&lt;h3&gt;
  
  
  Using IndexedDB
&lt;/h3&gt;

&lt;p&gt;Before we can do anything, we should use a &lt;a href="https://www.npmjs.com/package/idb" rel="noopener noreferrer"&gt;wrapper library&lt;/a&gt; on top of IndexedDB because, by default, it’s too complicated; it uses events instead of promises.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openDB&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idb&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;db&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;openDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Creates a new database if it doesn't exist.&lt;/span&gt;
    &lt;span class="nf"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts&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="c1"&gt;// The 'id' property of the object will be the key.&lt;/span&gt;
            &lt;span class="na"&gt;keyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// If it isn't explicitly set, create a value by auto incrementing.&lt;/span&gt;
            &lt;span class="na"&gt;autoIncrement&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="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;// Create an index on the 'name' property of the objects.&lt;/span&gt;
        &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And with that done, we can start storing structured data like it’s no one’s business!&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John Doe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;johndoe@john.doe&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;And we can just as easily retrieve all of it:&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="c1"&gt;// Get all contacts in name order:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contacts&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllFromIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&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 all we need to know for our simple use-case. But if you’re still interested, you can delve deeper into &lt;a href="https://www.npmjs.com/package/idb#examples" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Storing text data on the device even when the user leaves
&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%2Fiooy4ha5fxbjjw1yipc8.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%2Fiooy4ha5fxbjjw1yipc8.png" alt="Newsletter form" width="474" height="262"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;While we can use IndexedDB to store large and complicated data on the browser, it’s still important to consider those other times when all we need to save is a simple key-value pair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login information,&lt;/li&gt;
&lt;li&gt;Newsletter subscription state,&lt;/li&gt;
&lt;li&gt;Cookies consent,&lt;/li&gt;
&lt;li&gt;Analytics tracking pixels.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;It’s important to note that sensitive information (passwords, credit card information, social security numbers, etc…) should NEVER be saved in the browser, because it’s &lt;a href="https://auth0.com/blog/secure-browser-storage-the-facts/" rel="noopener noreferrer"&gt;vulnerable to XSS attacks&lt;/a&gt; in addition to &lt;a href="https://fingerprint.com/blog/indexeddb-api-browser-vulnerability-safari-15/" rel="noopener noreferrer"&gt;other exploits&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There’s a special utensil for such simple cases, and it’s called the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API" rel="noopener noreferrer"&gt;Web Storage API&lt;/a&gt;. And just like IndexedDB, it’s tied to the particular subdomain. It’s also cleared if the user empties the browser’s cache or data.&lt;/p&gt;

&lt;p&gt;In this API, you’ll find two types of storage: &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt;. They offer different benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local storage &lt;em&gt;persists&lt;/em&gt; data even when the user leaves the website, unlike session storage which clears all data as soon as the tab is closed.&lt;/li&gt;
&lt;li&gt;Local storage can store more data, unlike session storage which is maxed-out at 5MB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Using local storage
&lt;/h3&gt;

&lt;p&gt;Say that we’re implementing a newsletter subscription form. We don’t want to keep showing it to the user even after they’ve subscribed, so we’ll use the &lt;code&gt;localStorage&lt;/code&gt; global variable to conditionally display it:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-newsletter-subscribed&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can now &lt;a href="https://developer.chrome.com/docs/devtools/storage/localstorage/" rel="noopener noreferrer"&gt;use DevTools to see the new item&lt;/a&gt;, saved right in your computer.&lt;/p&gt;

&lt;p&gt;And now, let’s write a function that decides whether or not to show the subscription form:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSubscribed&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-newsletter-subscribed&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-newsletter-subscribed&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="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;As you can see, we’re first checking if the &lt;code&gt;newsletter-subscribed&lt;/code&gt; item exists. If it does, we simply return its value using &lt;code&gt;getItem()&lt;/code&gt;, otherwise, we return &lt;code&gt;false&lt;/code&gt; because the user hasn’t subscribed yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a location-aware ripple effect
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/abYzWeE?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;With the advance of the web, special effects have grown too. These days, CSS properties are just not enough to achieve our wildest dreams.&lt;/p&gt;

&lt;p&gt;Our last resort used to be GIFs and images, but with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API" rel="noopener noreferrer"&gt;CSS Painting API&lt;/a&gt;, that’s not the case anymore!&lt;/p&gt;

&lt;p&gt;Now, we can use all the stylish effects that come with the HTML Canvas to draw anything over an element’s background.&lt;/p&gt;

&lt;p&gt;Browser compatibility is not that great. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API#browser_compatibility" rel="noopener noreferrer"&gt;Firefox and Safari on iOS are yet to support it.&lt;/a&gt; Therefore, it’s very important to run the following: &lt;code&gt;if ('paintWorklet' in CSS)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let’s build a ripple effect without any pseudo-elements, inspired by &lt;a href="https://github.com/GoogleChromeLabs/houdini-samples" rel="noopener noreferrer"&gt;Google’s own implementation&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  The JavaScript Logic
&lt;/h3&gt;

&lt;p&gt;For this effect to work, we need to use JavaScript events to get the cursor’s &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; positions:&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="c1"&gt;// index.js&lt;/span&gt;

&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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;e&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offsetX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offsetY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;offsetX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;offsetY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;'&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;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Since the ripple effect is an animation that evolves over time, we need to keep track of its timeline using a &lt;code&gt;tick&lt;/code&gt; variable:&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="c1"&gt;// index.js&lt;/span&gt;

&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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;e&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;// ...&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Continue animation&lt;/span&gt;
        &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The above code uses &lt;code&gt;[requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)&lt;/code&gt; to create an efficient and optimized animation. During each animation step, we calculate the “tick” and assign it to a CSS property.&lt;/p&gt;

&lt;p&gt;If we leave it like this, it will run forever. So let’s add an “end condition” to end the animation. We’re gonna stop it when it reaches 1 second (meaning 1000 milliseconds):&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="c1"&gt;// index.js&lt;/span&gt;

&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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;e&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;// ...&lt;/span&gt;

    &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Stop the animation after 1 second&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;tick&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tick&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Continue animation&lt;/span&gt;
        &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And that’s it for the logic!&lt;/p&gt;
&lt;h3&gt;
  
  
  The Paint Worklet
&lt;/h3&gt;

&lt;p&gt;Let’s make the actual ripple effect, using the Paint API.&lt;/p&gt;

&lt;p&gt;This effect should go into a separate file, which we’ll call &lt;code&gt;ripple.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start by retrieving the CSS properties that we just defined:&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="c1"&gt;// ripple.js&lt;/span&gt;

&lt;span class="nf"&gt;registerPaint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ripple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="err"&gt;{
    &lt;/span&gt;&lt;span class="nc"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;inputProperties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tick&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;Next, we’ll use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API" rel="noopener noreferrer"&gt;Canvas API&lt;/a&gt; to draw a circle into the button’s background:&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="c1"&gt;// ripple.js&lt;/span&gt;

&lt;span class="nf"&gt;registerPaint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ripple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="err"&gt;{
    //...

    &lt;/span&gt;&lt;span class="nc"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="c1"&gt;// Retrieve props&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="c1"&gt;// Clamp tick in [0, 1000]&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;tick&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;tick&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

        &lt;span class="c1"&gt;// Draw ripple&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rippleColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(255,255,255,0.54)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rippleColor&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&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="c1"&gt;// center&lt;/span&gt;
            &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// radius&lt;/span&gt;
            &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="c1"&gt;// full circle&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nx"&gt;ctx&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Registering the paint worklet
&lt;/h3&gt;

&lt;p&gt;Go back into your &lt;code&gt;index.js&lt;/code&gt; file, and add the following code:&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="c1"&gt;// index.js&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paintWorklet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paintWorklet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ripple.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It will first check if CSS Paint API is supported, and only then will it link the ripple effect.&lt;/p&gt;

&lt;p&gt;And we’re done! All that’s left is to use this effect. So add the following code to your CSS:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0d1117&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ripple&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;For a deeper introduction to the CSS Paint API, read this &lt;a href="https://www.smashingmagazine.com/2020/03/practical-overview-css-houdini/" rel="noopener noreferrer"&gt;amazing article by Adrian Bece&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Showing a native sharing menu
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/qBoEjqM?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;There is so much content on the web that we may want to share with others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;links,&lt;/li&gt;
&lt;li&gt;images,&lt;/li&gt;
&lt;li&gt;paragraphs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the list never ends.&lt;/p&gt;

&lt;p&gt;Normally, a developer would implement their own sharing system with links to Twitter, Facebook, and other social media sites.&lt;/p&gt;

&lt;p&gt;These components, however, always fall short compared to their native counterparts, which come with a gigantic quantity of options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sharing with contacts,&lt;/li&gt;
&lt;li&gt;sharing with other apps,&lt;/li&gt;
&lt;li&gt;sharing over bluetooth,&lt;/li&gt;
&lt;li&gt;copying to clipboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the list, again, never ends.&lt;/p&gt;

&lt;p&gt;These native sharing menus used to be exclusive to native applications, but with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API" rel="noopener noreferrer"&gt;Web Share API&lt;/a&gt;, that fact is no longer true.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#browser_compatibility" rel="noopener noreferrer"&gt;Browser compatibility&lt;/a&gt; is excellent in mobile browsers, but struggles a bit when it comes to Firefox on desktop.&lt;/p&gt;

&lt;p&gt;Try it yourself in the Codepen above, and if it’s not supported in your device, here’s what it &lt;em&gt;can&lt;/em&gt; to look like:&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%2Fy1bv0w4szwdvhnx43h4q.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%2Fy1bv0w4szwdvhnx43h4q.png" alt="Sharing menu with many options including Gmail, Messages, Reddit, and LinkedIn." width="800" height="1422"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Sharing menu with many options including Gmail, Messages, Reddit, and LinkedIn.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sharing URLs
&lt;/h3&gt;

&lt;p&gt;The method to look for is &lt;code&gt;navigator.share&lt;/code&gt;. It takes an object containing a title, a string of text, and a URL.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shareData&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="s1"&gt;Smashing Magazine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Smashing Magazine — For Web Designers And Developers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.smashingmagazine.com/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shareData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that this function is protected by &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation" rel="noopener noreferrer"&gt;transient activation&lt;/a&gt;, meaning it requires a UI event (like clicking) before it can be handled.&lt;/p&gt;
&lt;h2&gt;
  
  
  Copying text to the clipboard
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/ZExYyeX?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The clipboard is one of the most underrated features in today’s computers. Would we developers survive without the constant &lt;code&gt;Ctrl+C&lt;/code&gt;’ing code from Stackoverflow? Doubt it.&lt;/p&gt;

&lt;p&gt;Clipboards are all about &lt;em&gt;moving digital information&lt;/em&gt; from point A to point B. The only alternative is rewriting content by-hand, which is a huge opportunity for errors. Modern clipboards also allow the copying of images and other forms of media.&lt;/p&gt;

&lt;p&gt;With the advent of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API" rel="noopener noreferrer"&gt;Clipboard API&lt;/a&gt;, developers can shine the beauty of UX on their users by programmatically copying important information to the user’s clipboard. This feature is also seen everywhere, from code in the MDN website to Twitter. It’s only missing in Stackoverflow, and for &lt;a href="https://stackoverflow.blog/2019/11/26/copying-code-from-stack-overflow-you-might-be-spreading-security-vulnerabilities/" rel="noopener noreferrer"&gt;good reason&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#browser_compatibility" rel="noopener noreferrer"&gt;Browser compatibility is also great&lt;/a&gt;, except IE of course.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using the Clipboard API
&lt;/h3&gt;

&lt;p&gt;Copying text is extremely simple:&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="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Howdy, partner!&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;And reading it is just as easy:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Sharing the selected text
&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%2Fisn1ni1852olwl5yko84.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%2Fisn1ni1852olwl5yko84.png" alt="Selected text on a blog with a sharing tooltip on top of it." width="475" height="266"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Selected text on a blog with a sharing tooltip on top of it.&lt;/p&gt;

&lt;p&gt;Multiple blogs such as Medium allow users to effortlessly share selected text with other social platforms.&lt;/p&gt;

&lt;p&gt;Being such a useful feature, it encourages the sharing of content, and as a result, it grows the blog to enormous proportions.&lt;/p&gt;

&lt;p&gt;We already saw how to invoke a &lt;a href="https://dev.toabout:blank#showing-a-native-sharing-menu"&gt;native sharing menu in a previous section&lt;/a&gt;, so let’s just focus on text selection.&lt;/p&gt;

&lt;p&gt;Also, we won’t see &lt;a href="https://www.smashingmagazine.com/2021/02/designing-tooltips-mobile-user-interfaces/" rel="noopener noreferrer"&gt;how to add a tooltip on top of selected text&lt;/a&gt;, but we‘ll delve into using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Selection_API" rel="noopener noreferrer"&gt;Selection API&lt;/a&gt; to retrieve the selected portion of text, because this whole article is about APIs and their use-cases.&lt;/p&gt;

&lt;p&gt;And no need to worry about the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#browser_compatibility" rel="noopener noreferrer"&gt;browser compatibility&lt;/a&gt;, because it’s just perfect!&lt;/p&gt;
&lt;h3&gt;
  
  
  Getting the selected text
&lt;/h3&gt;

&lt;p&gt;This is a terribly easy thing to do:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelection&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&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! Now seek the &lt;a href="https://dev.toabout:blank#showing-a-native-sharing-menu"&gt;previous section on the Web Share API&lt;/a&gt; to pop-up an OS-defined sharing menu, and let your users go wild!&lt;/p&gt;
&lt;h2&gt;
  
  
  Changing the title when the user leaves the tab
&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%2F9d4mn66h4c1eb68jievl.gif" 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%2F9d4mn66h4c1eb68jievl.gif" alt="page-visibility.gif" width="376" height="158"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Website title changing from “Shopping website” to “Please stay” when the user leaves the tab.&lt;/p&gt;

&lt;p&gt;It’s possible for a website to tell if it’s being viewed or not with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API" rel="noopener noreferrer"&gt;Page Visibility API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While I don’t advocate using the Page Visibility API to grab the user’s attention with annoying messages, it has many positive use-cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;showing new notifications,&lt;/li&gt;
&lt;li&gt;reporting engagement analytics,&lt;/li&gt;
&lt;li&gt;pausing video and audio,&lt;/li&gt;
&lt;li&gt;stopping an image carousel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API#browser_compatibility" rel="noopener noreferrer"&gt;Browser compatibility&lt;/a&gt; is not an issue.&lt;/p&gt;
&lt;h3&gt;
  
  
  Detecting page visibility
&lt;/h3&gt;

&lt;p&gt;We can get the page’s visibility state any time with the following code:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&lt;/span&gt; &lt;span class="c1"&gt;// 'visible' or 'hidden'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But real use-cases require listening to events and respectively changing some behavior.&lt;/p&gt;

&lt;p&gt;Unfortunately, the event name varies by browser, so we have to do the following:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hidden&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;visibilityChange&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&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="c1"&gt;// Opera 12.10 and Firefox 18 and later support&lt;/span&gt;
  &lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;visibilityChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msHidden&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&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;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;msHidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;visibilityChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;msvisibilitychange&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitHidden&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&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;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webkitHidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;visibilityChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webkitvisibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We then can listen for page visibility events, like this:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visibilityChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleVisibilityChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleVisibilityChange&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// page is hidden&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// page is visible&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;For the purposes of our demonstration, we’ll just change the document title:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleVisibilityChange&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please stay!!!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shopping 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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that I don’t recommend doing this, as it’s just annoying to the user and a fundamental malpractice in ethical web design.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Web APIs these days are bridging the gap between the web applications and the native applications.&lt;/p&gt;

&lt;p&gt;The web is starting to become a real threat to the monopolies created by the App Store and the Google Play Store, and it’s showing no signs of stopping. Let’s discuss this in the comment section below!&lt;/p&gt;

&lt;p&gt;There are many more APIs that we haven’t explored yet, and some can do unbelievable things like &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API" rel="noopener noreferrer"&gt;scanning bar-codes&lt;/a&gt; and even &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API" rel="noopener noreferrer"&gt;recognizing speech&lt;/a&gt;! So stay tuned for a part 2! ❤️&lt;/p&gt;
&lt;h2&gt;
  
  
  Honorable Mentions
&lt;/h2&gt;

&lt;p&gt;It would be a shame to not mention another group of APIs that are very rarely used, and yet have so many interesting and practical use-cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API" rel="noopener noreferrer"&gt;Geolocation API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs" rel="noopener noreferrer"&gt;Sensor API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API" rel="noopener noreferrer"&gt;Vibration API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API" rel="noopener noreferrer"&gt;Performance API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API" rel="noopener noreferrer"&gt;Resize Observer API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Like freebies? 🤑
&lt;/h2&gt;

&lt;p&gt;I made a pack of 100 free hover animations. &lt;a href="https://www.producthunt.com/posts/100-css-buttons?ref=dev" rel="noopener noreferrer"&gt;Get it now&lt;/a&gt;, share it, and do whatever you want with it. It's yours forever! ❤️&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.producthunt.com/posts/100-css-buttons?ref=dev" rel="noopener noreferrer"&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%2Fqvcivn0t2ae0tzwtwtc1.png" alt="Every style imaginable." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;div class="ltag__user ltag__user__id__"&gt;
    &lt;div class="ltag__user__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F99mvlsfu5tfj9m7ku25d.png" alt="[deleted user] image"&gt;
    &lt;/div&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;[Deleted User]&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Made 100 CSS Buttons For Your Next Big Project 🚀️</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Sun, 22 May 2022 22:35:00 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/i-made-100-css-buttons-for-your-next-big-project-m55</link>
      <guid>https://dev.to/youneslaaroussi/i-made-100-css-buttons-for-your-next-big-project-m55</guid>
      <description>&lt;p&gt;If you like this article, don’t forget to click on that &lt;strong&gt;heart button&lt;/strong&gt; to show your appreciation.&lt;/p&gt;

&lt;p&gt;You’re probably thinking: &lt;em&gt;That’s an odd way to start an article…&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But I just wanted to show you that buttons &lt;em&gt;really&lt;/em&gt; are everywhere!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/youneslaaroussi" rel="noopener noreferrer"&gt;
        youneslaaroussi
      &lt;/a&gt; / &lt;a href="https://github.com/youneslaaroussi/ui-buttons" rel="noopener noreferrer"&gt;
        ui-buttons
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      100 Modern CSS Buttons. Every style that you can imagine.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a href="https://ui-buttons.web.app" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fassets%2Fbanner.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀️ We're on Product Hunt!&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If you want me to keep making &lt;strong&gt;amazing free resources&lt;/strong&gt; for you, I would &lt;em&gt;really appreaciate&lt;/em&gt; your feedback and support on my &lt;strong&gt;Product Hunt&lt;/strong&gt; launch! 🤗️&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.producthunt.com/widgets/cards/r/UG9zdDozNTM0MDI=" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttonsassets%2Fproduct-hunt.png" alt="100 CSS Buttons"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🤖️ To See Code, Click on One of The Links&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preview&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/basic" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F1-basic%2Fpreview.webp" alt="CSS Button that changes color on click or hover."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/basic" rel="nofollow noopener noreferrer"&gt;Basic&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button that changes color on click or hover.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/inverted-triangles" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F10-inverted-triangles%2Fpreview.webp" alt="CSS Button slides its two inverted triangles to the middle on click or hover."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/inverted-triangles" rel="nofollow noopener noreferrer"&gt;Inverted Triangles&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button slides its two inverted triangles to the middle on click or hover.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/line-slide" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F100-line-slide%2Fpreview.webp" alt="CSS Button that slides its pseudo-element underline on hover or click."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/line-slide" rel="nofollow noopener noreferrer"&gt;Line Slide&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button that slides its pseudo-element underline on hover or click.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/don't-cross-the-line" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F101-don%27t-cross-the-line%2Fpreview.webp" alt="CSS Button that crosses over itself and expands on hover or click."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/don't-cross-the-line" rel="nofollow noopener noreferrer"&gt;Don't Cross The Line&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button that crosses over itself and expands on hover or click.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/slicer-and-marquee" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F11-slicer-and-marquee%2Fpreview.webp" alt="CSS Button that slices its background and cycles its content vertically on click or hover."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/slicer-and-marquee" rel="nofollow noopener noreferrer"&gt;Slicer And Marquee&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button that slices its background and cycles its content vertically on click or hover.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/zoom-in-and-text-rotate" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F12-zoom-in-and-text-rotate%2Fpreview.webp" alt="CSS Button that slides two inward-pointing pseudo-element triangles to the center on hover or click."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/zoom-in-and-text-rotate" rel="nofollow noopener noreferrer"&gt;Zoom In And Text Rotate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button that slides two inward-pointing pseudo-element triangles to the center on hover or click.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/alternate-blocks-and-text-flip" rel="nofollow noopener noreferrer"&gt; &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fyouneslaaroussi%2Fui-buttons.%2Fbuttons%2F13-alternate-blocks-and-text-flip%2Fpreview.webp" alt="CSS Button that slides its four alternate blocks and flips its text vertically on click or hover."&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ui-buttons.web.app/alternate-blocks-and-text-flip" rel="nofollow noopener noreferrer"&gt;Alternate Blocks And Text Flip&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CSS Button that slides its four alternate blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/youneslaaroussi/ui-buttons" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You’re bored. So you take your phone out, and you click buttons to —&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;turn it on (a &lt;em&gt;physical button&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;open the &lt;a href="https://twitter.com/eludadev" rel="noopener noreferrer"&gt;twitter app&lt;/a&gt; (an &lt;em&gt;icon button&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;go to the home page (a &lt;em&gt;navigation button&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;like and reply to a tweet (an &lt;em&gt;action button&lt;/em&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;and the list goes on and on….&lt;/p&gt;

&lt;p&gt;I think that you get it by now, we are obsessed with pressing buttons! So let me propose an idea…&lt;/p&gt;

&lt;p&gt;Buttons should be more fun! And by “fun”, I really mean —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;practical,&lt;/li&gt;
&lt;li&gt;precise,&lt;/li&gt;
&lt;li&gt;and modern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But as web developers, we already have to worry about so many other things —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accessibility (a11y),&lt;/li&gt;
&lt;li&gt;web vitals,&lt;/li&gt;
&lt;li&gt;seo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So imagine if someone could just take one little hurdle away, so that we can focus on the more important stuff…&lt;/p&gt;

&lt;p&gt;I decided that I should be that person. I took on the responsibility of building buttons that are —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accessible (uses ARIA attributes),&lt;/li&gt;
&lt;li&gt;modern in style,&lt;/li&gt;
&lt;li&gt;responsive (works on desktop and mobile).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And not just that, I also decided to make &lt;strong&gt;ONE HUNDRED&lt;/strong&gt; different styles for these buttons. You are &lt;em&gt;guaranteed&lt;/em&gt; to find that one style that &lt;em&gt;just works&lt;/em&gt; on your website!&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/zYRdddK?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Now let’s break-apart one of my favorite button styles: &lt;a href="https://github.com/eludadev/ui-buttons/tree/main/buttons/31-overfold" rel="noopener noreferrer"&gt;&lt;strong&gt;The Overfold&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It first starts by animating a &lt;code&gt;clip-path&lt;/code&gt; from one corner to another.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/wvyqPNj?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The second part of the effect involves scrolling text vertically, while clipping it using &lt;code&gt;overflow: hidden&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/eludadev/embed/RwQZxZM?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Did you know?&lt;/strong&gt; I’m working on &lt;strong&gt;150 CSS Buttons&lt;/strong&gt;. Follow me to be the first to know when it drops! 🤗&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__"&gt;
    &lt;div class="ltag__user__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F99mvlsfu5tfj9m7ku25d.png" alt="[deleted user] image"&gt;
    &lt;/div&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;[Deleted User]&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1526989502199234567-139" src="https://platform.twitter.com/embed/Tweet.html?id=1526989502199234567"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1526989502199234567-139');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1526989502199234567&amp;amp;theme=dark"
  }



&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>Those HTML Elements You Never Use 🌚🕵🏿</title>
      <dc:creator>Younes Laaroussi</dc:creator>
      <pubDate>Mon, 11 Apr 2022 20:24:00 +0000</pubDate>
      <link>https://dev.to/youneslaaroussi/those-html-elements-you-never-use-16bi</link>
      <guid>https://dev.to/youneslaaroussi/those-html-elements-you-never-use-16bi</guid>
      <description>&lt;p&gt;There are over &lt;strong&gt;a hundred&lt;/strong&gt; elements in HTML, all of which can be applied to pieces of text to give them special meaning in a document. Most of us only know a few, like the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;&lt;/a&gt; elements…&lt;/p&gt;

&lt;p&gt;But there are actually a bunch more hidden in the dark realms of the &lt;a href="https://dev.w3.org/html5/html-author/" rel="noopener noreferrer"&gt;W3C reference&lt;/a&gt;. That’s why, in this article, I took the liberty of diving deep into the HTML documentation, to come out with a handy bag of elements that will improve your website in not one, but &lt;strong&gt;two&lt;/strong&gt; very important ways: accessibility and SEO.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;D&lt;/code&gt; to bookmark this article and easily come back to it when you need it. And with that said, let’s begin!&lt;/p&gt;

&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;abbr&amp;gt;&lt;/code&gt;&lt;/a&gt; — Abbreviation
&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%2Fwa955z63369nucw8xrgx.gif" 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%2Fwa955z63369nucw8xrgx.gif" alt="abbr" width="780" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This elements represents both &lt;strong&gt;abbreviations&lt;/strong&gt; (like Corporation ➟ Corp.) and &lt;strong&gt;acronyms&lt;/strong&gt; (like Cascading Style Sheets ➟ CSS). Additionally, you can use its &lt;code&gt;title&lt;/code&gt; attribute to write the full form of the word so that screen readers can read it and users can hover over it to read it.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/a&gt; — Insert and Delete
&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%2Fmcgyhckvdocsm1hawh2c.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%2Fmcgyhckvdocsm1hawh2c.png" alt="ins" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/a&gt; elements represent a range of text that has been added or deleted to a document. You may have already seen these elements in the &lt;a href="https://github.com/vercel/next.js/pull/36067/files" rel="noopener noreferrer"&gt;Github Pull Requests&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dfn" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;dfn&amp;gt;&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;var&amp;gt;&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;kbd&amp;gt;&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/samp" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;samp&amp;gt;&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;output&amp;gt;&lt;/code&gt;&lt;/a&gt; — Technical Elements
&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%2Fm5re9db9y6aupb95scm8.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%2Fm5re9db9y6aupb95scm8.png" alt="dfn" width="748" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are elements that represent special technically-oriented parts in a document, like definitions, variables, keystrokes, etc…&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;bdo&amp;gt;&lt;/code&gt;&lt;/a&gt; — Text Directionality
&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%2F2y7pjd4a33odpbw0onnl.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%2F2y7pjd4a33odpbw0onnl.png" alt="bdo" width="736" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This element changes the directionality of text to make it render backwards. You can control its behavior using the &lt;code&gt;dir&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;Although not its intended use, but it can reverse text using nothing but HTML!&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt;&lt;/a&gt; — Highlighting Text
&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%2Flcgbki4wsap0qsen4tyk.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%2Flcgbki4wsap0qsen4tyk.png" alt="mark" width="739" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The purpose of this element is to highlight text like you would with a marker.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;area&amp;gt;&lt;/code&gt;&lt;/a&gt; — Clickable Image Areas
&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%2F70134tynvhr0kzzxl1hl.gif" 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%2F70134tynvhr0kzzxl1hl.gif" alt="area" width="780" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use this element to make certain areas of your image behave like links!&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;dl&amp;gt;&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;dd&amp;gt;&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;dt&amp;gt;&lt;/code&gt;&lt;/a&gt; — Description Lists
&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%2F7sp6u46opu6a1lbsffa5.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%2F7sp6u46opu6a1lbsffa5.png" alt="dl" width="741" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use these elements to create a semantically-accurate description list where you define multiple terms in one block of text.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;sup&amp;gt;&lt;/code&gt;&lt;/a&gt;and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;sub&amp;gt;&lt;/code&gt;&lt;/a&gt;— Superscripts and Subscripts
&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%2F9qy03k2s7bxla52rp6e3.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%2F9qy03k2s7bxla52rp6e3.png" alt="The Pythagorean theorem is often expressed as the following equation: a2 + b2 = c2" width="728" height="211"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;With these two elements, you can add &lt;strong&gt;superscripts&lt;/strong&gt; (like x²) and &lt;strong&gt;subscripts&lt;/strong&gt; (like x₀) to your document.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;figcaption&amp;gt;&lt;/code&gt;&lt;/a&gt; — Labeled Images
&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%2F6gpjjur0gp27hy5qbltt.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%2F6gpjjur0gp27hy5qbltt.png" alt="figure" width="738" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt;&lt;/a&gt; to contain any element that you want, like an image for example. And then, you add &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;figcaption&amp;gt;&lt;/code&gt;&lt;/a&gt; as its last child, where you can add a block of text that describes what’s above it.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;progress&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;meter&amp;gt;&lt;/code&gt;&lt;/a&gt;— Marking Progress
&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%2F393avkx9ul4wpse5wrd6.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%2F393avkx9ul4wpse5wrd6.png" alt="progress" width="759" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one allows you to create semantically-right progress-bar elements where you show how far an action is from being finished.  &lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;&lt;/a&gt; — Expandable Menus
&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%2F7brbrc0rpzb876hanhoi.gif" 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%2F7brbrc0rpzb876hanhoi.gif" alt="details" width="780" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use this element to create a native menu that has a title and can expand using a button. No JavaScript needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;&lt;/a&gt; — Pop-up Dialogs
&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%2Ftt72qoiv39zlcpkh5jsx.gif" 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%2Ftt72qoiv39zlcpkh5jsx.gif" alt="dialog" width="780" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s possible to create semantically-accurate dialogs using this element. It doesn’t do much by itself, so you have to use CSS and JavaScript to add more functionality.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;datalist&amp;gt;&lt;/code&gt;&lt;/a&gt;— Text Input Suggestions
&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%2F4vhz7uslle4f8ob4cas9.gif" 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%2F4vhz7uslle4f8ob4cas9.gif" alt="Choose a flavor" width="780" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This element lets you manually add text-input suggestions. You can add anything you want!&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt;&lt;/a&gt;— Grouping Form Elements
&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%2F0if254cpvwuop576ptff.gif" 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%2F0if254cpvwuop576ptff.gif" alt="Choose your favorite monster: Kraken - Sasquatch - Mothman" width="780" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep your forms tidy and more user-friendly by using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt;&lt;/a&gt;element.&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt;&lt;/a&gt;— Embedding External Objects
&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%2Faavm59i9olvricrdpdjc.gif" 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%2Faavm59i9olvricrdpdjc.gif" alt="object" width="780" height="424"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;With this amazing element, you can embed almost any file you want to your website! The most commonly supported files are PDFs, Youtube videos, etc…&lt;/p&gt;




&lt;h2&gt;
  
  
  👉 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt;&lt;/a&gt;— If JavaScript Is Disabled
&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%2Ftg2ukhlh1m0yaofuciq0.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%2Ftg2ukhlh1m0yaofuciq0.png" alt="CodePen doesn't work very well without JavaScript. We're all for progressive enhancement, but CodePen is a bit unique in that it's all about writing and showing front end code, including JavaScript. It's required to use most of the features of CodePen. Need to know how to enable it? Go here." width="734" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This element can be used to show some content when JavaScript is disabled by the browser. It’s very commonly used by websites that heavily depend on JavaScript, like Single Page Applications (SPAs).&lt;/p&gt;




&lt;p&gt;If you found this guide useful, please don’t forget to &lt;strong&gt;bookmark it&lt;/strong&gt; for future reference.&lt;/p&gt;

&lt;p&gt;I make posts like this everyday, so please &lt;strong&gt;follow me&lt;/strong&gt; to stay informed. ❤️&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__"&gt;
    &lt;div class="ltag__user__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F99mvlsfu5tfj9m7ku25d.png" alt="[deleted user] image"&gt;
    &lt;/div&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;[Deleted User]&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1526989502199234567-199" src="https://platform.twitter.com/embed/Tweet.html?id=1526989502199234567"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1526989502199234567-199');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1526989502199234567&amp;amp;theme=dark"
  }



&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>webdev</category>
      <category>html</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
