[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-urlpattern-native-route-matching":44,"search-suggestions":260,"related-urlpattern-native-route-matching":299,"code:js:true:1bavd9i":427,"code:js:true:urjh0g":428,"code:js:true:1hc7dhl":429,"code:js:true:1w4todp":430,"code:js:true:i995p4":431,"code:js:true:1i3eexn":432,"comments-01a00952-9d58-7601-89ac-42fc7bf00fc9":433},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":243,"seo":244,"translationGroupId":247,"thread":248,"assessments":250,"translations":256,"quiz":258},"01a00952-9d58-7601-89ac-42fc7bf00fc9","urlpattern-native-route-matching","Stop Writing Regex to Match URLs — The Browser Already Can",null,"A service worker's route regex was missing one anchor and started caching live edit forms as if they were read-only pages. URLPattern, native since 2021, doesn't let you make that mistake.","\u002Fmedia\u002Fcovers\u002Furlpattern-native-route-matching.png",6,"2026-08-20T06:53:07.359Z",56,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"browser","Browser",{"slug":73,"name":74,"color":48},"frontend","Frontend",{"assessments":76},1,{"slug":46,"title":78},"URLPattern route matcher — interactive playground",{"blocks":80,"version":76},[81,85,88,91,97,100,105,108,111,115,118,121,124,127,131,134,137,140,143,146,150,153,156,159,163,166,169,172,175,178,181,184,187,191,194,198,201,204,207,210,213,216,219,222,225,228,231,234],{"id":82,"html":83,"type":84},"b1","\u003Cp>Priya was three paragraphs into rewriting a support ticket when the page flashed and her draft reverted to what it had looked like an hour earlier. She hadn&#39;t refreshed. Nobody had.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>The service worker had.\u003C\u002Fp>",{"id":89,"html":90,"type":84},"b3","\u003Cp>It was running a cache-first strategy for ticket pages — fetch once, serve from cache after that, so the dashboard felt instant on a flaky connection. The intent was to cache \u003Ccode>\u002Ftickets\u002F482\u003C\u002Fcode>, the read-only view, and leave \u003Ccode>\u002Ftickets\u002F482\u002Fedit\u003C\u002Fcode> alone, since an edit form is exactly the page you never want served stale. Here&#39;s the line that decided which was which:\u003C\u002Fp>",{"id":92,"code":93,"type":94,"language":95,"highlight":96},"b4","const isTicketView = \u002F^\\\u002Ftickets\\\u002F\\d+\u002F.test(pathname);","code","js",[],{"id":98,"html":99,"type":84},"b5","\u003Cp>Spot it yet? Read it once more before you scroll.\u003C\u002Fp>",{"id":101,"html":102,"text":103,"type":104,"level":31},"b6","The missing character was \u003Ccode>$\u003C\u002Fcode>","The missing character was $","heading",{"id":106,"html":107,"type":84},"b7","\u003Cp>\u003Ccode>\u002F^\\\u002Ftickets\\\u002F\\d+\u002F\u003C\u002Fcode> anchors the \u003Cem>start\u003C\u002Fem> of the string — \u003Ccode>^\u003C\u002Fcode> — but never anchors the end. So it matches \u003Ccode>\u002Ftickets\u002F482\u003C\u002Fcode>. It also matches \u003Ccode>\u002Ftickets\u002F482\u002Fedit\u003C\u002Fcode>, \u003Ccode>\u002Ftickets\u002F482\u002Fhistory\u003C\u002Fcode>, and \u003Ccode>\u002Ftickets\u002F482-anything-at-all\u003C\u002Fcode>, because &quot;one or more digits after \u003Ccode>\u002Ftickets\u002F\u003C\u002Fcode>&quot; is true of all of them. The regex was never wrong about what it checked. It just never checked enough.\u003C\u002Fp>",{"id":109,"html":110,"type":84},"b8","\u003Cp>The one-character fix is obvious once you see it:\u003C\u002Fp>",{"id":112,"code":113,"type":94,"language":95,"highlight":114},"b9","const isTicketView = \u002F^\\\u002Ftickets\\\u002F\\d+$\u002F.test(pathname);",[],{"id":116,"html":117,"type":84},"b10","\u003Cp>Ship that and you&#39;ll hit the next edge case within a week: a trailing slash (\u003Ccode>\u002Ftickets\u002F482\u002F\u003C\u002Fcode>) now fails to match, because \u003Ccode>$\u003C\u002Fcode> demands nothing comes after the digits — not even a slash. Add \u003Ccode>\\\u002F?\u003C\u002Fcode> before the \u003Ccode>$\u003C\u002Fcode> and you&#39;ve fixed that one. Then someone deep-links to \u003Ccode>\u002Ftickets\u002F482?tab=history\u003C\u002Fcode> and the query string breaks the anchor again, because \u003Ccode>pathname\u003C\u002Fcode> on some code paths actually holds the full URL. Each fix is a patch on the last, and every patch is a chance to reintroduce the first bug in a new shape.\u003C\u002Fp>",{"id":119,"html":120,"type":84},"b11","\u003Cp>This is the part nobody tells you about hand-rolled URL matching: it isn&#39;t hard because regex is hard. It&#39;s hard because &quot;does this path match this shape&quot; has a dozen boundary conditions, and a hand-written pattern only encodes the ones you happened to think of on the day you wrote it.\u003C\u002Fp>",{"id":122,"html":123,"text":123,"type":104,"level":31},"b12","The API built for exactly this job",{"id":125,"html":126,"type":84},"b13","\u003Cp>The browser has had a purpose-built answer since 2021, and it isn&#39;t a regex — it&#39;s \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FURLPattern\">\u003Ccode>URLPattern\u003C\u002Fcode>\u003C\u002Fa>, a global constructor that matches and parses URLs the way a router does, natively:\u003C\u002Fp>",{"id":128,"code":129,"type":94,"language":95,"highlight":130},"b14","const ticketView = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\" });\n\nticketView.test({ pathname: \"\u002Ftickets\u002F482\" });        \u002F\u002F true\nticketView.test({ pathname: \"\u002Ftickets\u002F482\u002Fedit\" });   \u002F\u002F false",[],{"id":132,"html":133,"type":84},"b15","\u003Cp>No \u003Ccode>^\u003C\u002Fcode>, no \u003Ccode>$\u003C\u002Fcode>, no \u003Ccode>\\d+\u003C\u002Fcode>. \u003Ccode>\u002Ftickets\u002F:id\u003C\u002Fcode> matches the \u003Cem>whole\u003C\u002Fem> pathname component by default — that&#39;s not a lucky accident of this example, it&#39;s the core design decision. A \u003Ccode>URLPattern\u003C\u002Fcode> pattern is exact-match unless you explicitly tell it otherwise. There&#39;s no anchor to forget, because there&#39;s nothing unanchored to begin with.\u003C\u002Fp>",{"id":135,"html":136,"type":84},"b16","\u003Cp>That was invented for this exact scenario, by the way — it originally shipped to give service workers a real routing syntax for \u003Ccode>fetch\u003C\u002Fcode> event handlers instead of everyone writing their own regex. You&#39;re not reaching for an obscure API here; you&#39;re reaching for the one built by people who&#39;d already hit Priya&#39;s bug.\u003C\u002Fp>",{"id":138,"html":139,"text":139,"type":104,"level":31},"b17","Reading the pattern syntax",{"id":141,"html":142,"type":84},"b18","\u003Cp>\u003Ccode>:id\u003C\u002Fcode> is a \u003Cem>named group\u003C\u002Fem> — it captures a path segment and gives it a name you can read back later. That&#39;s the syntax doing double duty: matching and extracting, in one string.\u003C\u002Fp>",{"id":144,"html":145,"type":84},"b19","\u003Cp>Three more pieces cover almost everything else you&#39;ll need:\u003C\u002Fp>",{"id":147,"code":148,"type":94,"language":95,"highlight":149},"b20","\u002F\u002F Wildcard — matches anything, including further slashes\nnew URLPattern({ pathname: \"\u002Ffiles\u002F*\" }).test({ pathname: \"\u002Ffiles\u002F2026\u002Fq3\u002Freport.pdf\" }); \u002F\u002F true\n\n\u002F\u002F Optional group — the {…} makes a whole segment, slash included, optional\nnew URLPattern({ pathname: \"\u002Fblog{\u002F:year}?\" }).test({ pathname: \"\u002Fblog\" });       \u002F\u002F true\nnew URLPattern({ pathname: \"\u002Fblog{\u002F:year}?\" }).test({ pathname: \"\u002Fblog\u002F2026\" });  \u002F\u002F true\n\n\u002F\u002F Custom regex inside a named group — for when a plain segment isn't specific enough\nnew URLPattern({ pathname: \"\u002F:kind(ticket|comment)\u002F:id\" }).test({ pathname: \"\u002Fcomment\u002F482\" }); \u002F\u002F true",[],{"id":151,"html":152,"type":84},"b21","\u003Cp>Four building blocks — a literal, \u003Ccode>:name\u003C\u002Fcode>, \u003Ccode>*\u003C\u002Fcode>, and \u003Ccode>{…}?\u003C\u002Fcode> — cover the shapes that used to take a paragraph of regex to express, and read back close to plain English.\u003C\u002Fp>",{"id":154,"html":155,"text":155,"type":104,"level":31},"b22","Pulling the params back out",{"id":157,"html":158,"type":84},"b23","\u003Cp>\u003Ccode>test()\u003C\u002Fcode> only answers yes or no. When you need the actual \u003Ccode>id\u003C\u002Fcode>, call \u003Ccode>exec()\u003C\u002Fcode> instead:\u003C\u002Fp>",{"id":160,"code":161,"type":94,"language":95,"highlight":162},"b24","const pattern = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\" });\nconst match = pattern.exec({ pathname: \"\u002Ftickets\u002F482\" });\n\nmatch.pathname.groups.id; \u002F\u002F \"482\"",[],{"id":164,"html":165,"type":84},"b25","\u003Cp>\u003Ccode>exec()\u003C\u002Fcode> returns \u003Ccode>null\u003C\u002Fcode> on no match, and otherwise an object with one entry per URL component you matched against (\u003Ccode>pathname\u003C\u002Fcode>, \u003Ccode>search\u003C\u002Fcode>, \u003Ccode>hash\u003C\u002Fcode>, and so on), each carrying a \u003Ccode>groups\u003C\u002Fcode> object keyed by name. No manual \u003Ccode>.split(&quot;\u002F&quot;)\u003C\u002Fcode>, no \u003Ccode>match[1]\u003C\u002Fcode> where you have to remember what group 1 was.\u003C\u002Fp>",{"id":167,"html":168,"type":84},"b26","\u003C!-- playground:start -->",{"id":170,"html":171,"text":171,"type":104,"level":31},"b27","🎮 Try it yourself",{"id":173,"html":174,"type":84},"b28","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Furlpattern-native-route-matching\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":176,"html":177,"type":84},"b29","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":179,"html":180,"type":84},"b30","\u003C!-- playground:end -->",{"id":182,"html":183,"text":183,"type":104,"level":31},"b31","The fix, with two patterns instead of one regex",{"id":185,"html":186,"type":84},"b32","\u003Cp>Back in the service worker, the honest fix isn&#39;t a smarter regex — it&#39;s naming the two things that were always different:\u003C\u002Fp>",{"id":188,"code":189,"type":94,"language":95,"highlight":190},"b33","const ticketView = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\" });\nconst ticketEdit = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\u002Fedit\" });\n\nself.addEventListener(\"fetch\", (event) => {\n  const url = new URL(event.request.url);\n\n  if (ticketView.test(url) && !ticketEdit.test(url)) {\n    event.respondWith(cacheFirst(event.request));\n  }\n  \u002F\u002F ticketEdit, and everything else, falls through to the network.\n});",[],{"id":192,"html":193,"type":84},"b34","\u003Cp>\u003Ccode>ticketView\u003C\u002Fcode> never matches \u003Ccode>\u002Ftickets\u002F482\u002Fedit\u003C\u002Fcode> — not because of a character you remembered to add, but because the pattern simply describes a shorter path than the one it&#39;s being compared against. There&#39;s no anchor to audit in a future code review, because exactness was never optional.\u003C\u002Fp>",{"id":195,"html":196,"text":197,"type":104,"level":31},"b35","Where it doesn&#39;t replace anything","Where it doesn't replace anything",{"id":199,"html":200,"type":84},"b36","\u003Cp>\u003Ccode>URLPattern\u003C\u002Fcode> matches and extracts. It doesn&#39;t dispatch, and it doesn&#39;t rank overlapping routes by specificity the way a full router does — if you hand it a list of ten patterns, you&#39;re still the one deciding which to check first. For a page with dozens of nested, code-split routes, you&#39;ll still likely reach for a router library that happens to use something like this under the hood. But for the far more common case — a service worker&#39;s allowlist, an analytics filter deciding which URLs to sample, a tiny client-side router that only ever needed four routes — you don&#39;t need the library. You need the four lines above.\u003C\u002Fp>",{"id":202,"html":203,"type":84},"b37","\u003Cp>It&#39;s also worth checking your target audience before you lean on it fully: it&#39;s been in Chrome and Edge since 2021, and Firefox and Safari caught up through 2025, so by now it&#39;s safe to reach for directly rather than through a polyfill in almost any modern app.\u003C\u002Fp>",{"id":205,"html":206,"text":206,"type":104,"level":31},"b38","The one-line version",{"id":208,"html":209,"type":84},"b39","\u003Cp>The regex wasn&#39;t wrong about what it checked for — it just never had to promise it checked \u003Cem>everything\u003C\u002Fem>, and one missing \u003Ccode>$\u003C\u002Fcode> let an edit form get served like a snapshot. \u003Ccode>URLPattern\u003C\u002Fcode> doesn&#39;t make that promise implicit. Exactness is the default, not a character you have to remember.\u003C\u002Fp>",{"id":211,"html":212,"type":84},"b40","\u003Cp>Next time you catch yourself writing \u003Ccode>\u002F^\\\u002Fsomething\\\u002F\\d+\u002F\u003C\u002Fcode> and mentally listing the edge cases you&#39;ll patch in later — trailing slash, query string, that one route with two segments — stop and reach for \u003Ccode>new URLPattern({ pathname: &quot;...&quot; })\u003C\u002Fcode> instead. What&#39;s the last regex you wrote to match a URL? I&#39;d bet it&#39;s missing an anchor somewhere too.\u003C\u002Fp>",{"id":214,"html":215,"type":84},"b41","\u003C!-- quiz:start -->",{"id":217,"html":218,"text":218,"type":104,"level":31},"b42","🧠 Test yourself",{"id":220,"html":221,"type":84},"b43","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Furlpattern-native-route-matching\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":223,"html":224,"type":84},"b44","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":226,"html":227,"type":84},"b45","\u003C!-- quiz:end -->",{"id":229,"type":230},"b46","divider",{"id":232,"html":233,"type":84},"b47","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":235,"type":236,"items":237,"ordered":18},"b48","list",[238,239,240,241,242],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","Priya was three paragraphs into rewriting a support ticket when the page flashed and her draft reverted to what it had looked like an hour earlier. She hadn't refreshed. Nobody had.\n\nThe service worker had.\n\nIt was running a cache-first strategy for ticket pages — fetch once, serve from cache after that, so the dashboard felt instant on a flaky connection. The intent was to cache `\u002Ftickets\u002F482`, the read-only view, and leave `\u002Ftickets\u002F482\u002Fedit` alone, since an edit form is exactly the page you never want served stale. Here's the line that decided which was which:\n\n```js\nconst isTicketView = \u002F^\\\u002Ftickets\\\u002F\\d+\u002F.test(pathname);\n```\n\nSpot it yet? Read it once more before you scroll.\n\n## The missing character was `$`\n\n`\u002F^\\\u002Ftickets\\\u002F\\d+\u002F` anchors the *start* of the string — `^` — but never anchors the end. So it matches `\u002Ftickets\u002F482`. It also matches `\u002Ftickets\u002F482\u002Fedit`, `\u002Ftickets\u002F482\u002Fhistory`, and `\u002Ftickets\u002F482-anything-at-all`, because \"one or more digits after `\u002Ftickets\u002F`\" is true of all of them. The regex was never wrong about what it checked. It just never checked enough.\n\nThe one-character fix is obvious once you see it:\n\n```js\nconst isTicketView = \u002F^\\\u002Ftickets\\\u002F\\d+$\u002F.test(pathname);\n```\n\nShip that and you'll hit the next edge case within a week: a trailing slash (`\u002Ftickets\u002F482\u002F`) now fails to match, because `$` demands nothing comes after the digits — not even a slash. Add `\\\u002F?` before the `$` and you've fixed that one. Then someone deep-links to `\u002Ftickets\u002F482?tab=history` and the query string breaks the anchor again, because `pathname` on some code paths actually holds the full URL. Each fix is a patch on the last, and every patch is a chance to reintroduce the first bug in a new shape.\n\nThis is the part nobody tells you about hand-rolled URL matching: it isn't hard because regex is hard. It's hard because \"does this path match this shape\" has a dozen boundary conditions, and a hand-written pattern only encodes the ones you happened to think of on the day you wrote it.\n\n## The API built for exactly this job\n\nThe browser has had a purpose-built answer since 2021, and it isn't a regex — it's [`URLPattern`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FURLPattern), a global constructor that matches and parses URLs the way a router does, natively:\n\n```js\nconst ticketView = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\" });\n\nticketView.test({ pathname: \"\u002Ftickets\u002F482\" });        \u002F\u002F true\nticketView.test({ pathname: \"\u002Ftickets\u002F482\u002Fedit\" });   \u002F\u002F false\n```\n\nNo `^`, no `$`, no `\\d+`. `\u002Ftickets\u002F:id` matches the *whole* pathname component by default — that's not a lucky accident of this example, it's the core design decision. A `URLPattern` pattern is exact-match unless you explicitly tell it otherwise. There's no anchor to forget, because there's nothing unanchored to begin with.\n\nThat was invented for this exact scenario, by the way — it originally shipped to give service workers a real routing syntax for `fetch` event handlers instead of everyone writing their own regex. You're not reaching for an obscure API here; you're reaching for the one built by people who'd already hit Priya's bug.\n\n## Reading the pattern syntax\n\n`:id` is a *named group* — it captures a path segment and gives it a name you can read back later. That's the syntax doing double duty: matching and extracting, in one string.\n\nThree more pieces cover almost everything else you'll need:\n\n```js\n\u002F\u002F Wildcard — matches anything, including further slashes\nnew URLPattern({ pathname: \"\u002Ffiles\u002F*\" }).test({ pathname: \"\u002Ffiles\u002F2026\u002Fq3\u002Freport.pdf\" }); \u002F\u002F true\n\n\u002F\u002F Optional group — the {…} makes a whole segment, slash included, optional\nnew URLPattern({ pathname: \"\u002Fblog{\u002F:year}?\" }).test({ pathname: \"\u002Fblog\" });       \u002F\u002F true\nnew URLPattern({ pathname: \"\u002Fblog{\u002F:year}?\" }).test({ pathname: \"\u002Fblog\u002F2026\" });  \u002F\u002F true\n\n\u002F\u002F Custom regex inside a named group — for when a plain segment isn't specific enough\nnew URLPattern({ pathname: \"\u002F:kind(ticket|comment)\u002F:id\" }).test({ pathname: \"\u002Fcomment\u002F482\" }); \u002F\u002F true\n```\n\nFour building blocks — a literal, `:name`, `*`, and `{…}?` — cover the shapes that used to take a paragraph of regex to express, and read back close to plain English.\n\n## Pulling the params back out\n\n`test()` only answers yes or no. When you need the actual `id`, call `exec()` instead:\n\n```js\nconst pattern = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\" });\nconst match = pattern.exec({ pathname: \"\u002Ftickets\u002F482\" });\n\nmatch.pathname.groups.id; \u002F\u002F \"482\"\n```\n\n`exec()` returns `null` on no match, and otherwise an object with one entry per URL component you matched against (`pathname`, `search`, `hash`, and so on), each carrying a `groups` object keyed by name. No manual `.split(\"\u002F\")`, no `match[1]` where you have to remember what group 1 was.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Furlpattern-native-route-matching\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The fix, with two patterns instead of one regex\n\nBack in the service worker, the honest fix isn't a smarter regex — it's naming the two things that were always different:\n\n```js\nconst ticketView = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\" });\nconst ticketEdit = new URLPattern({ pathname: \"\u002Ftickets\u002F:id\u002Fedit\" });\n\nself.addEventListener(\"fetch\", (event) => {\n  const url = new URL(event.request.url);\n\n  if (ticketView.test(url) && !ticketEdit.test(url)) {\n    event.respondWith(cacheFirst(event.request));\n  }\n  \u002F\u002F ticketEdit, and everything else, falls through to the network.\n});\n```\n\n`ticketView` never matches `\u002Ftickets\u002F482\u002Fedit` — not because of a character you remembered to add, but because the pattern simply describes a shorter path than the one it's being compared against. There's no anchor to audit in a future code review, because exactness was never optional.\n\n## Where it doesn't replace anything\n\n`URLPattern` matches and extracts. It doesn't dispatch, and it doesn't rank overlapping routes by specificity the way a full router does — if you hand it a list of ten patterns, you're still the one deciding which to check first. For a page with dozens of nested, code-split routes, you'll still likely reach for a router library that happens to use something like this under the hood. But for the far more common case — a service worker's allowlist, an analytics filter deciding which URLs to sample, a tiny client-side router that only ever needed four routes — you don't need the library. You need the four lines above.\n\nIt's also worth checking your target audience before you lean on it fully: it's been in Chrome and Edge since 2021, and Firefox and Safari caught up through 2025, so by now it's safe to reach for directly rather than through a polyfill in almost any modern app.\n\n## The one-line version\n\nThe regex wasn't wrong about what it checked for — it just never had to promise it checked *everything*, and one missing `$` let an edit form get served like a snapshot. `URLPattern` doesn't make that promise implicit. Exactness is the default, not a character you have to remember.\n\nNext time you catch yourself writing `\u002F^\\\u002Fsomething\\\u002F\\d+\u002F` and mentally listing the edge cases you'll patch in later — trailing slash, query string, that one route with two segments — stop and reach for `new URLPattern({ pathname: \"...\" })` instead. What's the last regex you wrote to match a URL? I'd bet it's missing an anchor somewhere too.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Furlpattern-native-route-matching\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":47,"canonical":245,"description":246},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Furlpattern-native-route-matching","A service worker's route regex was missing one anchor and started caching live edit forms as if they were read-only pages. URLPattern, native since 2021, doesn't let you make that ","01a00952-9d58-7601-89ac-47f055ebe7d9",{"id":249,"locked":18},"01a00952-9d96-7186-8378-02ee0479aefc",[251],{"id":252,"slug":46,"title":253,"_count":254},"01a00952-9dcb-7138-b8b7-b818b267ea1d","URLPattern: native route matching",{"questions":255},7,[257],{"locale":13,"slug":46},{"id":252,"slug":46,"title":253,"_count":259,"questionCount":255},{"questions":255},[261,263,265,267,271,275,279,282,286,290,293,296],{"slug":67,"name":68,"articles":262},59,{"slug":73,"name":74,"articles":264},53,{"slug":61,"name":65,"articles":266},51,{"slug":268,"name":269,"articles":270},"css","Css",23,{"slug":272,"name":273,"articles":274},"typescript","Typescript",10,{"slug":276,"name":277,"articles":278},"performance","Performance",8,{"slug":280,"name":281,"articles":51},"grammar","Grammar",{"slug":283,"name":284,"articles":285},"react","React",5,{"slug":287,"name":288,"articles":289},"tutorial","Tutorial",4,{"slug":291,"name":292,"articles":289},"node","Node",{"slug":294,"name":295,"articles":43},"ai","Ai",{"slug":297,"name":298,"articles":43},"html","Html",{"items":300,"meta":426},[301,313,332,352,370,388,406],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":302,"likeCount":19,"commentCount":19,"author":303,"vertical":304,"topic":305,"tags":306,"_count":311,"playground":312,"hasQuiz":17,"hasPlayground":17},57,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[307,308,309,310],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":314,"slug":315,"title":316,"subtitle":48,"excerpt":317,"coverUrl":318,"locale":13,"readingMinutes":289,"publishedAt":319,"viewCount":320,"likeCount":19,"commentCount":19,"author":321,"vertical":322,"topic":323,"tags":325,"_count":330,"playground":331,"hasQuiz":17,"hasPlayground":17},"019ffa10-eeed-707d-9b50-7d83664c7a9a","css-trigonometric-functions-radial-layout","Stop Writing Math.cos() in a Resize Listener","CSS learned trigonometry. Here's the radial menu that used to need a resize listener — and doesn't anymore.","\u002Fmedia\u002Fcovers\u002Fcss-trigonometric-functions-radial-layout.png","2026-08-17T07:03:57.776Z",336,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":268,"name":324},"CSS",[326,327,328,329],{"slug":268,"name":269,"color":48},{"slug":67,"name":68,"color":48},{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":315},{"id":333,"slug":334,"title":335,"subtitle":48,"excerpt":336,"coverUrl":337,"locale":13,"readingMinutes":285,"publishedAt":338,"viewCount":339,"likeCount":19,"commentCount":19,"author":340,"vertical":341,"topic":342,"tags":343,"_count":350,"playground":351,"hasQuiz":17,"hasPlayground":17},"019ff19b-f9ca-772e-8f09-cd3d3c640d2b","web-locks-api-tab-coordination","Five tabs open, one refresh token — the race nobody noticed","Users with multiple tabs open triggered a silent race: every tab refreshed the same expiring auth token at once, and the server started invalidating its own sessions. navigator.locks fixes it with a real mutex, no polling or localStorage hacks required.","\u002Fmedia\u002Fcovers\u002Fweb-locks-api-tab-coordination.png","2026-08-15T06:44:54.917Z",457,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[344,345,346,347],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":348,"name":349,"color":48},"async","Async",{"assessments":76},{"slug":334},{"id":353,"slug":354,"title":355,"subtitle":48,"excerpt":356,"coverUrl":357,"locale":13,"readingMinutes":43,"publishedAt":358,"viewCount":359,"likeCount":19,"commentCount":19,"author":360,"vertical":361,"topic":362,"tags":363,"_count":368,"playground":369,"hasQuiz":17,"hasPlayground":17},"019fe660-8bc3-7444-8c96-86953902f5e0","findlast-search-from-end","You copy and reverse the array to find the last match. `findLast()` searches from the end directly.","The usual workaround — `[...arr].reverse().find()` — allocates a full copy just to walk it backward. ES2023 added `findLast()` and `findLastIndex()` to search from the end without touching the original.","\u002Fmedia\u002Fcovers\u002Ffindlast-search-from-end.png","2026-08-14T07:38:34.486Z",334,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[364,365,366,367],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":272,"name":273,"color":48},{"assessments":76},{"slug":354},{"id":371,"slug":372,"title":373,"subtitle":48,"excerpt":374,"coverUrl":375,"locale":13,"readingMinutes":289,"publishedAt":376,"viewCount":377,"likeCount":19,"commentCount":19,"author":378,"vertical":379,"topic":380,"tags":381,"_count":386,"playground":387,"hasQuiz":17,"hasPlayground":17},"019fe661-0450-720a-ad6e-5eb995429cd7","sendbeacon-reliable-unload-data","Your `fetch()` in `beforeunload` is being silently dropped. Use `navigator.sendBeacon()`.","Browsers cancel in-flight network requests when a page unloads. The common fix — synchronous XHR — is deprecated. `navigator.sendBeacon()` is the correct, fire-and-forget API designed exactly for this case.","\u002Fmedia\u002Fcovers\u002Fsendbeacon-reliable-unload-data.png","2026-08-13T07:40:48.196Z",373,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[382,383,384,385],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":276,"name":277,"color":48},{"assessments":76},{"slug":372},{"id":389,"slug":390,"title":391,"subtitle":48,"excerpt":392,"coverUrl":393,"locale":13,"readingMinutes":289,"publishedAt":394,"viewCount":395,"likeCount":19,"commentCount":19,"author":396,"vertical":397,"topic":398,"tags":399,"_count":404,"playground":405,"hasQuiz":17,"hasPlayground":17},"019ff19b-f763-70e0-abdf-eded59c8e69a","overscroll-behavior-scroll-containment","You're blocking touchmove events to contain scroll. `overscroll-behavior` does it natively.","When a scrollable container hits its edge, the page behind it starts scrolling too. Developers block this with JavaScript event handlers or body overflow tricks. overscroll-behavior is the CSS property that turns off scroll chaining in one line.","\u002Fmedia\u002Fcovers\u002Foverscroll-behavior-scroll-containment.png","2026-08-12T07:38:53.786Z",378,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":268,"name":324},[400,401,402,403],{"slug":268,"name":269,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":61,"name":65,"color":48},{"assessments":76},{"slug":390},{"id":407,"slug":408,"title":409,"subtitle":48,"excerpt":410,"coverUrl":411,"locale":13,"readingMinutes":289,"publishedAt":412,"viewCount":413,"likeCount":19,"commentCount":19,"author":414,"vertical":415,"topic":416,"tags":417,"_count":424,"playground":425,"hasQuiz":17,"hasPlayground":17},"019ff19b-f68d-77ea-9a2c-70e14c5c7a05","mutation-observer-dom-change-detection","You're polling setInterval to detect DOM changes. `MutationObserver` fires when they happen.","Watching for DOM mutations with setInterval or custom event dispatch is fragile and imprecise. The MutationObserver API delivers a callback exactly when attributes, text content, or child elements change — with full control over which types of mutations you watch.","\u002Fmedia\u002Fcovers\u002Fmutation-observer-dom-change-detection.png","2026-08-11T15:48:01.781Z",532,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[418,419,420,421],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":422,"name":423,"color":48},"dom","Dom",{"assessments":76},{"slug":408},{"page":76,"perPage":255,"total":266,"totalPages":278},"\u003Cdiv class=\"shj shj-lang-js shj-oneline\" data-lang=\"js\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> isTicketView \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">^\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">\\\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">tickets\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">\\\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">\\d\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-var\">+\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>pathname\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-oneline\" data-lang=\"js\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> isTicketView \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">^\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">\\\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">tickets\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">\\\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">\\d\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-var\">+\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">$\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>pathname\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> ticketView \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F:id\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nticketView\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F482\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;        \u003Cspan class=\"shj-cmnt\">\u002F\u002F true\n\u003C\u002Fspan>ticketView\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F482\u002Fedit\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;   \u003Cspan class=\"shj-cmnt\">\u002F\u002F false\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Wildcard — matches anything, including further slashes\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ffiles\u002F*\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ffiles\u002F2026\u002Fq3\u002Freport.pdf\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F true\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F Optional group — the {…} makes a whole segment, slash included, optional\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Fblog{\u002F:year}?\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Fblog\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;       \u003Cspan class=\"shj-cmnt\">\u002F\u002F true\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Fblog{\u002F:year}?\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Fblog\u002F2026\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;  \u003Cspan class=\"shj-cmnt\">\u002F\u002F true\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F Custom regex inside a named group — for when a plain segment isn't specific enough\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002F:kind(ticket|comment)\u002F:id\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Fcomment\u002F482\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F true\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> pattern \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F:id\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> match \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> pattern\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">exec\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F482\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nmatch\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>pathname\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>groups\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id; \u003Cspan class=\"shj-cmnt\">\u002F\u002F \"482\"\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> ticketView \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F:id\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> ticketEdit \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">URLPattern\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pathname\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\u002Ftickets\u002F:id\u002Fedit\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nself\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"fetch\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> url \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-num\">URL\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>request\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>url\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>ticketView\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>url\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>ticketEdit\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">test\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>url\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">respondWith\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-func\">cacheFirst\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>request\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F ticketEdit, and everything else, falls through to the network.\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":434},[]]