<?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: LION ZHANL</title>
    <description>The latest articles on DEV Community by LION ZHANL (@lion_zhanl_74eb3870e22a92).</description>
    <link>https://dev.to/lion_zhanl_74eb3870e22a92</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3676001%2F59c90be6-d303-4c3b-a854-b694162dd989.png</url>
      <title>DEV Community: LION ZHANL</title>
      <link>https://dev.to/lion_zhanl_74eb3870e22a92</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lion_zhanl_74eb3870e22a92"/>
    <language>en</language>
    <item>
      <title>Designing an AI Sticker Removal Workflow With Real User Control</title>
      <dc:creator>LION ZHANL</dc:creator>
      <pubDate>Thu, 30 Jul 2026 05:37:57 +0000</pubDate>
      <link>https://dev.to/lion_zhanl_74eb3870e22a92/designing-an-ai-sticker-removal-workflow-with-real-user-control-cdk</link>
      <guid>https://dev.to/lion_zhanl_74eb3870e22a92/designing-an-ai-sticker-removal-workflow-with-real-user-control-cdk</guid>
      <description>&lt;p&gt;The model call is only one part of an AI image editor. The surrounding workflow has to make uncertainty visible and give the user a way to correct it. That is the product idea behind &lt;a href="https://www.removestickerfromphoto.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Remove Sticker From Photo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The editor begins in &lt;strong&gt;Auto Detect&lt;/strong&gt; mode. This is useful for visually distinct emojis, labels, and digital stickers. Detection will never be perfect, so the second mode—&lt;strong&gt;Brush Area&lt;/strong&gt;—lets the user draw a mask over the exact overlay. The model then reconstructs the selected pixels using nearby image context rather than regenerating the entire photo.&lt;/p&gt;

&lt;p&gt;An edit costs &lt;strong&gt;4 credits&lt;/strong&gt;. After processing, the output is rendered inside the editing frame and can be checked through a &lt;strong&gt;before-and-after comparison&lt;/strong&gt;. The job is also recorded in &lt;strong&gt;task history&lt;/strong&gt;, including work that is still processing and completed images that can be reopened later. These details matter because an asynchronous result should not disappear when a user navigates away.&lt;/p&gt;

&lt;p&gt;The interface deliberately avoids claiming that AI “uncovers” hidden content. Once a sticker is flattened into an image, the original pixels are unavailable; the output is a generated approximation. Users should edit only material they own or have permission to modify. The service should not be used to remove ownership marks, manipulate documentary evidence, impersonate someone, or create deceptive media.&lt;/p&gt;

&lt;p&gt;Good AI UX pairs convenient automation with precise manual control, inspectable results, persistent state, and honest limits.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Face Swap Group Photos: Target Selection and Cropping</title>
      <dc:creator>LION ZHANL</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:39:33 +0000</pubDate>
      <link>https://dev.to/lion_zhanl_74eb3870e22a92/face-swap-group-photos-target-selection-and-cropping-233i</link>
      <guid>https://dev.to/lion_zhanl_74eb3870e22a92/face-swap-group-photos-target-selection-and-cropping-233i</guid>
      <description>&lt;p&gt;Group photos are among the hardest inputs for an AI face swap. The problem is not simply that there are more faces. Each face can have a different size, angle, expression, lighting condition, and level of obstruction. The model also has to decide which detected face or faces should receive the reference identity.&lt;/p&gt;

&lt;p&gt;In the current faceswap workflow, you upload a target image and use either the built-in face reference or a permitted custom reference. There is no precise face-number selector. If control over one person matters, &lt;strong&gt;cropping is the most reliable selection tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This guide explains how to prepare group photos without throwing away more of the scene than necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Decide What You Want the Result to Contain
&lt;/h2&gt;

&lt;p&gt;Before editing the file, describe the intended result in one sentence.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Keep the entire group, but transform the person standing in the center.”&lt;/li&gt;
&lt;li&gt;“Create a close meme crop of the person on the left.”&lt;/li&gt;
&lt;li&gt;“Transform every clearly visible face with the same reference.”&lt;/li&gt;
&lt;li&gt;“Keep two people, but remove the distant background crowd from the working image.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are different tasks. A single upload cannot always solve all of them reliably.&lt;/p&gt;

&lt;p&gt;If your goal is one person, isolate one person. If the group context is essential, accept that face selection and consistency may be less predictable. Do not assume that the largest face, the first person from the left, or the central face will always be selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Face Detection Changes in a Group
&lt;/h2&gt;

&lt;p&gt;A human instantly separates foreground people, background faces, reflections, posters, and partially visible heads. A model sees candidate facial patterns.&lt;/p&gt;

&lt;p&gt;Detection can be influenced by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;face size in pixels;&lt;/li&gt;
&lt;li&gt;sharpness and contrast;&lt;/li&gt;
&lt;li&gt;how much of the face is visible;&lt;/li&gt;
&lt;li&gt;head angle;&lt;/li&gt;
&lt;li&gt;distance from the image edge;&lt;/li&gt;
&lt;li&gt;overlap with another person;&lt;/li&gt;
&lt;li&gt;whether a face appears in a screen, mirror, or poster; and&lt;/li&gt;
&lt;li&gt;preprocessing performed by the model provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small but front-facing background face can sometimes be easier to detect than a larger foreground profile. That is why “the obvious subject” to a person may not be the obvious subject to the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Group-Photo Scenarios
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario 1: One intended person, group context not required
&lt;/h3&gt;

&lt;p&gt;Crop the photo to a head-and-shoulders or upper-body composition around that person. This gives the most control and usually the highest output quality.&lt;/p&gt;

&lt;p&gt;Leave enough space around the hair, ears, and jaw. Do not crop exactly along the face boundary. The model needs surrounding pixels to blend edges and preserve the scene’s light.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: One intended person, group context must remain
&lt;/h3&gt;

&lt;p&gt;This is more difficult because faces you do not intend to transform remain detectable.&lt;/p&gt;

&lt;p&gt;Use a two-stage workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a working crop that includes the intended person and enough nearby context.&lt;/li&gt;
&lt;li&gt;Generate and evaluate the face result in that crop.&lt;/li&gt;
&lt;li&gt;If you have the rights and editing skills, place the reviewed result back into a copy of the original using a normal image editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you selection control without asking the face-swap model to understand the entire group. The final composite still needs careful edge, lighting, and disclosure review. Do not use this method to create false evidence or deceive viewers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: Several intended faces
&lt;/h3&gt;

&lt;p&gt;Expect more variation. The same reference can look different across people because each target face has different geometry, pose, and light.&lt;/p&gt;

&lt;p&gt;For a stylized meme, variation may be acceptable. For a polished image, process people in separate crops and review each result. A single group-level pass is faster, but it gives you less control over which faces change and how consistently they blend.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reliable Cropping Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Start from the original file
&lt;/h3&gt;

&lt;p&gt;Do not begin with a screenshot if the original photo is available. Screenshots often reduce resolution, add interface borders, and introduce an extra compression cycle.&lt;/p&gt;

&lt;p&gt;Duplicate the original so you can return to it. Never repeatedly save over the only copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Identify every detectable face
&lt;/h3&gt;

&lt;p&gt;Look beyond the intended subject. Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;background guests;&lt;/li&gt;
&lt;li&gt;people partly outside the frame;&lt;/li&gt;
&lt;li&gt;faces on phones or monitors;&lt;/li&gt;
&lt;li&gt;mirrors and windows;&lt;/li&gt;
&lt;li&gt;posters, printed shirts, or signs; and&lt;/li&gt;
&lt;li&gt;faces hidden behind text or stickers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an unwanted face remains recognizable, assume it may be detected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Draw the first crop around the intended subject
&lt;/h3&gt;

&lt;p&gt;For a close result, include the full head, neck, shoulders, and a small amount of background. For a medium crop, keep the upper body and relevant objects.&lt;/p&gt;

&lt;p&gt;Avoid cuts through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hairline or top of head;&lt;/li&gt;
&lt;li&gt;chin or jaw;&lt;/li&gt;
&lt;li&gt;ears;&lt;/li&gt;
&lt;li&gt;hands touching the face; and&lt;/li&gt;
&lt;li&gt;another person’s overlapping shoulder if that overlap defines the jaw edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A crop that is too tight can improve face size but damage boundary information. The goal is not the smallest possible rectangle; it is the cleanest useful context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Check pixel detail after cropping
&lt;/h3&gt;

&lt;p&gt;Cropping makes the face larger on screen, but it does not add real detail. View the crop at 100%. If the eyes and mouth are still blocky, return to the original and choose another image.&lt;/p&gt;

&lt;p&gt;A useful crop should make the target unambiguous while preserving enough pixels for feature edges. Use the &lt;a href="https://charliekirkface.net/blog/face-swap-photo-quality-scorecard-before-upload" rel="noopener noreferrer"&gt;photo quality scorecard&lt;/a&gt; to compare the original and crop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Match the reference
&lt;/h3&gt;

&lt;p&gt;The built-in reference is designed to provide a stable default identity. If you upload a custom reference, choose one person only and use a clear portrait.&lt;/p&gt;

&lt;p&gt;Do not use a group photo as the reference image. The reference should answer “which identity?” while the target answers “which scene and pose?” Multiple faces in both images create two layers of ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Export once
&lt;/h3&gt;

&lt;p&gt;Use JPG for a normal photograph when file size matters, or PNG/WebP when your workflow requires those formats. Export at high but not excessive quality and avoid several save-and-reopen cycles.&lt;/p&gt;

&lt;p&gt;The current upload interface displays the accepted formats and file-size limit. Follow that displayed limit rather than assuming a large camera original will upload unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Inspect the generated result at two scales
&lt;/h3&gt;

&lt;p&gt;At normal viewing size, ask whether the intended person changed and whether the group still reads naturally.&lt;/p&gt;

&lt;p&gt;At 100% zoom, inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;eyes and eyewear;&lt;/li&gt;
&lt;li&gt;hairline and ears;&lt;/li&gt;
&lt;li&gt;jaw overlap with nearby people;&lt;/li&gt;
&lt;li&gt;skin color under mixed light;&lt;/li&gt;
&lt;li&gt;teeth and open mouths; and&lt;/li&gt;
&lt;li&gt;any untouched faces that may have changed unexpectedly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not share a result merely because the intended face looks acceptable. A background person may contain a more serious artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uneven Face Sizes Create Uneven Quality
&lt;/h2&gt;

&lt;p&gt;Consider a photo with three people:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;person A is close to the camera and fills 400 pixels from chin to forehead;&lt;/li&gt;
&lt;li&gt;person B is farther away and fills 160 pixels;&lt;/li&gt;
&lt;li&gt;person C is a blurred background guest with a 60-pixel face.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if all three are detected, the model does not have equal information for them. Person A may look convincing, person B may be soft, and person C may become a distorted patch.&lt;/p&gt;

&lt;p&gt;This is not a single “quality setting” problem. It is an information problem inside the source image. Increasing the output resolution does not make person C’s original face more detailed.&lt;/p&gt;

&lt;p&gt;If every person matters, use an image where the faces are similar in scale and focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overlap Is Harder Than Simple Occlusion
&lt;/h2&gt;

&lt;p&gt;A pair of glasses covers part of one face but belongs to that person. In a group, another person’s hair, hand, shoulder, or face may cross the target boundary. The model must decide which pixels belong to whom.&lt;/p&gt;

&lt;p&gt;Common failures include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the target jaw blending into the neighboring cheek;&lt;/li&gt;
&lt;li&gt;hair from one person crossing the transformed forehead;&lt;/li&gt;
&lt;li&gt;duplicated eye or mouth shapes near an overlap;&lt;/li&gt;
&lt;li&gt;a skin-colored seam around a hand; and&lt;/li&gt;
&lt;li&gt;the reference identity appearing partly on the wrong face.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cropping cannot repair a severe overlap. Choose another frame when the target face is physically blocked by another person.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do When the Wrong Face Changes
&lt;/h2&gt;

&lt;p&gt;Do not immediately submit the same file again. A repeat may produce a slightly different image, but it does not remove the ambiguity.&lt;/p&gt;

&lt;p&gt;Use this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crop out every other visible face.&lt;/li&gt;
&lt;li&gt;Keep the full intended head and some surrounding context.&lt;/li&gt;
&lt;li&gt;Confirm the target face is the largest clear face in the crop.&lt;/li&gt;
&lt;li&gt;Remove screenshot borders or irrelevant reflections by cropping, not by painting over the face.&lt;/li&gt;
&lt;li&gt;Try a reference with a more compatible angle.&lt;/li&gt;
&lt;li&gt;Generate once and compare.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the intended scene requires several unmodified people around the target, use the two-stage workflow described earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pre-Upload Group Checklist
&lt;/h2&gt;

&lt;p&gt;Before generating, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I can name the exact person or people I intend to transform.&lt;/li&gt;
&lt;li&gt;I have permission or another lawful basis to use the photo and depicted likenesses.&lt;/li&gt;
&lt;li&gt;The target face is not severely covered or cropped.&lt;/li&gt;
&lt;li&gt;The crop excludes irrelevant faces, screens, mirrors, and posters.&lt;/li&gt;
&lt;li&gt;The target remains detailed at 100% zoom.&lt;/li&gt;
&lt;li&gt;The reference contains one clear face.&lt;/li&gt;
&lt;li&gt;The final image will not be presented as evidence of a real event.&lt;/li&gt;
&lt;li&gt;I am prepared to label the result as AI-edited parody where context could be misunderstood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot answer the first question clearly, the model will probably face the same ambiguity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and Consent Matter More in Groups
&lt;/h2&gt;

&lt;p&gt;A group photo can contain people who never expected their image to be processed by an AI service. Public availability does not automatically grant every privacy, publicity, biometric, copyright, or contractual permission.&lt;/p&gt;

&lt;p&gt;Use photos you are allowed to process. Avoid images involving minors, private settings, sensitive events, or people who have objected. For public sharing, consider whether every identifiable person could reasonably understand the result as a joke rather than a factual record.&lt;/p&gt;

&lt;p&gt;Review the &lt;a href="https://charliekirkface.net/content-policy" rel="noopener noreferrer"&gt;Content Policy&lt;/a&gt;, &lt;a href="https://charliekirkface.net/ai-disclosure" rel="noopener noreferrer"&gt;AI Disclosure&lt;/a&gt;, and &lt;a href="https://charliekirkface.net/privacy-policy" rel="noopener noreferrer"&gt;Privacy Policy&lt;/a&gt; before uploading sensitive material.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendation
&lt;/h2&gt;

&lt;p&gt;For reliable control, treat cropping as target selection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One person wanted:&lt;/strong&gt; isolate one person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group context wanted:&lt;/strong&gt; test a controlled crop, then use a lawful, clearly disclosed editing workflow if you need to restore context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several faces wanted:&lt;/strong&gt; expect variation and review every face separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best group input is not necessarily the widest or highest-megapixel image. It is the image where the intended faces are clear, similarly lit, minimally overlapped, and easy to distinguish from every other face-like object.&lt;/p&gt;

&lt;p&gt;When your crop is ready, open the &lt;a href="https://charliekirkface.net/kirkify-face" rel="noopener noreferrer"&gt;faceswap tool&lt;/a&gt;. If the result contains eye, hairline, jaw, or skin artifacts, continue with our &lt;a href="https://charliekirkface.net/blog/face-swap-artifact-troubleshooting-eyes-hairline-jaw-and-skin" rel="noopener noreferrer"&gt;face-swap artifact troubleshooting guide&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://charliekirkface.net/blog/face-swap-group-photos-target-selection-and-cropping" rel="noopener noreferrer"&gt;https://charliekirkface.net/blog/face-swap-group-photos-target-selection-and-cropping&lt;/a&gt;. This cross-post points search engines to the original canonical article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>machinelearning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I Built a Privacy-First Browser Game Portal with Click-to-Load Iframes</title>
      <dc:creator>LION ZHANL</dc:creator>
      <pubDate>Thu, 30 Jul 2026 03:24:32 +0000</pubDate>
      <link>https://dev.to/lion_zhanl_74eb3870e22a92/how-i-built-a-privacy-first-browser-game-portal-with-click-to-load-iframes-5f2n</link>
      <guid>https://dev.to/lion_zhanl_74eb3870e22a92/how-i-built-a-privacy-first-browser-game-portal-with-click-to-load-iframes-5f2n</guid>
      <description>&lt;p&gt;Embedding a browser game looks simple:&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;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://games.example.net/my-game"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line lets a third party join the page lifecycle immediately. It can download a large bundle, establish connections, run scripts, request storage, display advertising, or fail before the visitor decides to play.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI-assistance disclosure:&lt;/strong&gt; I used AI to help draft and edit this article, then reviewed its architecture, code, claims, and limitations before publication.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a game directory, that default is both expensive and surprising. A visitor may have opened the page to read the controls, compare games, or check whether the game works on a phone. Loading the player before that intent is known wastes bandwidth and collapses two separate decisions—visiting the guide and opening the third-party game—into one.&lt;/p&gt;

&lt;p&gt;While working on a browser-game portal, I treated the site and the embedded player as two different trust and performance boundaries. The page renders first-party information immediately. The third-party frame is created only after an explicit Play action.&lt;/p&gt;

&lt;p&gt;This article explains that pattern and the engineering details that made it useful rather than merely decorative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a two-layer model
&lt;/h2&gt;

&lt;p&gt;The outer page should be a complete page without the game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A descriptive heading and summary&lt;/li&gt;
&lt;li&gt;Controls and gameplay tips&lt;/li&gt;
&lt;li&gt;Developer and platform information&lt;/li&gt;
&lt;li&gt;Related games and category navigation&lt;/li&gt;
&lt;li&gt;A poster or cover image&lt;/li&gt;
&lt;li&gt;A real button that starts the player&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inner layer is a small launcher responsible for the game lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the requested game.&lt;/li&gt;
&lt;li&gt;Wait for an intentional Play action.&lt;/li&gt;
&lt;li&gt;Create the provider iframe.&lt;/li&gt;
&lt;li&gt;Report loading state.&lt;/li&gt;
&lt;li&gt;Offer recovery when loading is slow or blocked.&lt;/li&gt;
&lt;li&gt;Remove the frame when the player resets it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Do not put the remote URL in the initial markup
&lt;/h2&gt;

&lt;p&gt;Native iframe lazy loading is helpful below the fold, but it is not an intent gate. Browsers decide when a &lt;code&gt;loading="lazy"&lt;/code&gt; frame is close enough to fetch. If the goal is “no third-party game request before Play,” the iframe must not exist yet, or it must not have a remote &lt;code&gt;src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The initial markup can be ordinary, semantic HTML:&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;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"game-launcher"&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"launcher-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/covers/sky-hoops.webp"&lt;/span&gt;
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"960"&lt;/span&gt;
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"540"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"launcher-copy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"launcher-status"&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Ready to play&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"launcher-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sky Hoops&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"play-game"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Play now&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"game-mount"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty mount is intentional. It prevents speculative loading, makes the initial DOM inexpensive, and gives JavaScript one controlled place to attach and remove the player.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolve games through an allowlist
&lt;/h2&gt;

&lt;p&gt;Never copy a URL from a query parameter directly into &lt;code&gt;iframe.src&lt;/code&gt;. A route such as &lt;code&gt;/play?url=...&lt;/code&gt; becomes an open embed proxy and can make a trusted-looking domain display an attacker-controlled page.&lt;/p&gt;

&lt;p&gt;Resolve a stable slug against server-owned configuration instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;slug&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;title&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;embedUrl&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;Game&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sky-hoops&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;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sky-hoops&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;Sky Hoops&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;embedUrl&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://games.example.net/embed/sky-hoops&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;function&lt;/span&gt; &lt;span class="nf"&gt;getAllowedGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&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;Game&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&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;game&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedUrl&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;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;games.example.net&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;`/embed/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&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;allowed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate protocol, hostname, and path—not just whether a string starts with an expected domain. A prefix test can be fooled by values such as &lt;code&gt;trusted.example.attacker.test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also keep the source page URL separate from the embed URL. The former is useful for attribution and a fallback link; the latter is an implementation detail and should be narrowly allowlisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the iframe only in the click handler
&lt;/h2&gt;

&lt;p&gt;The launcher can now transition from &lt;code&gt;ready&lt;/code&gt; to &lt;code&gt;loading&lt;/code&gt; to &lt;code&gt;playing&lt;/code&gt;:&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;mount&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;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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;#game-mount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&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;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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;#play-game&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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;#launcher-status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createGameFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HTMLIFrameElement&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;frame&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;iframe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;frame&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; game`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autoplay; fullscreen; gamepad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowFullscreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referrerPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strict-origin-when-cross-origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow-scripts allow-same-origin allow-forms allow-pointer-lock allow-fullscreen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;frame&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="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="s2"&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="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;game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAllowedGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sky-hoops&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This game is currently unavailable.&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="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;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Loading &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createGameFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;frame&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="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is ready.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&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;once&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="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&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;In production, keep launcher state in one place rather than inferring it from scattered CSS classes and flags. A small reducer makes repeated starts, resets, timeouts, and fullscreen transitions easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat &lt;code&gt;load&lt;/code&gt; as a transport signal, not proof of playability
&lt;/h2&gt;

&lt;p&gt;An iframe &lt;code&gt;load&lt;/code&gt; event means the browser completed a navigation. It does not prove that the game rendered successfully. A provider may return an error page, a regional block, an authentication prompt, or a page that refuses to work inside a frame. Cross-origin isolation prevents the parent from reading the frame body to distinguish those cases.&lt;/p&gt;

&lt;p&gt;Build the recovery UI around that limitation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show progress immediately after Play.&lt;/li&gt;
&lt;li&gt;If no &lt;code&gt;load&lt;/code&gt; event arrives after a reasonable timeout, reveal help.&lt;/li&gt;
&lt;li&gt;Keep a link to open the provider’s source page in a new tab.&lt;/li&gt;
&lt;li&gt;Offer Retry, which removes the old frame before creating a new one.&lt;/li&gt;
&lt;li&gt;Let the user return to the launcher without reloading the entire page.&lt;/li&gt;
&lt;li&gt;Record coarse start, load, timeout, and reset events only when analytics consent permits it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A timeout should not claim that the provider is down. “Still connecting” is accurate; “server offline” is usually not.&lt;/p&gt;

&lt;p&gt;If a provider documents a &lt;code&gt;postMessage&lt;/code&gt; ready event, verify &lt;code&gt;event.origin&lt;/code&gt; and the message schema. Never accept a generic &lt;code&gt;{ type: "ready" }&lt;/code&gt; from any origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use sandboxing as one layer, not a magic shield
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sandbox&lt;/code&gt; attribute is powerful, but game compatibility often requires a considered set of capabilities. Canvas games may need scripts, pointer lock, fullscreen, forms, audio, or a same-origin context inside the provider’s own origin.&lt;/p&gt;

&lt;p&gt;Grant only what a tested game needs. Be especially deliberate about combining &lt;code&gt;allow-scripts&lt;/code&gt; and &lt;code&gt;allow-same-origin&lt;/code&gt;. The risk depends on frame origin and control: a cross-origin provider is different from untrusted content served on your own origin.&lt;/p&gt;

&lt;p&gt;Then add surrounding controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Content Security Policy with a narrow &lt;code&gt;frame-src&lt;/code&gt; allowlist&lt;/li&gt;
&lt;li&gt;A restrictive Permissions Policy&lt;/li&gt;
&lt;li&gt;HTTPS-only embed URLs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;referrerPolicy="strict-origin-when-cross-origin"&lt;/code&gt; or a stricter policy when compatible&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rel="noopener noreferrer"&lt;/code&gt; on new-tab fallback links&lt;/li&gt;
&lt;li&gt;No secrets, user tokens, or sensitive data in frame URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sandboxing does not make an unreviewed provider trustworthy. Confirm that you are allowed to embed the game, test the provider’s behavior, and keep a kill switch in the catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility belongs in the launcher state machine
&lt;/h2&gt;

&lt;p&gt;The iframe boundary makes keyboard and screen-reader behavior easy to overlook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, not a clickable &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give every iframe a specific &lt;code&gt;title&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Announce loading and failure status through an &lt;code&gt;aria-live&lt;/code&gt; region.&lt;/li&gt;
&lt;li&gt;Move focus into the frame only after the user initiated the action.&lt;/li&gt;
&lt;li&gt;Return focus to the Play button after Reset.&lt;/li&gt;
&lt;li&gt;Keep focus indicators visible and do not make fullscreen the only playable mode.&lt;/li&gt;
&lt;li&gt;Respect reduced-motion preferences in loading animation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cross-origin games may have accessibility limitations the host cannot repair. The host can still provide clear controls, honest status, keyboard-reachable recovery actions, and text instructions outside the frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: optimize the page you control
&lt;/h2&gt;

&lt;p&gt;Click-to-load removes the largest third-party cost from the initial path, but the outer page still needs discipline.&lt;/p&gt;

&lt;p&gt;Reserve poster dimensions to prevent layout shift. Serve responsive, compressed covers. Avoid preconnecting to every provider in a large catalog; a preconnect is itself a network and privacy-relevant action. If you use one, add it only when the user is likely to start that specific game—or after pointer intent—after evaluating the tradeoff.&lt;/p&gt;

&lt;p&gt;Code-split the launcher if it is not needed above the fold. Keep the game catalog small or load only the selected entry. Do not run a carousel, several animated covers, and a video background while claiming the frame is the performance problem.&lt;/p&gt;

&lt;p&gt;Measure page readiness separately from game readiness (the time from Play to the best available ready signal). One combined “load time” hides whether the host or provider needs work.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO: index the guide, not the empty shell
&lt;/h2&gt;

&lt;p&gt;A remote game frame is not a substitute for first-party content. Give each game a stable, canonical URL with a unique title, description, heading, and useful guide. Add breadcrumbs and relevant internal links. Where accurate, structured data can describe the page and game, but it should match visible content.&lt;/p&gt;

&lt;p&gt;The launcher document itself is plumbing. If it has its own route, keep it out of the sitemap and prevent it from competing with the game detail page. The public detail page should be the canonical result.&lt;/p&gt;

&lt;p&gt;This also improves resilience: if an embed is temporarily unavailable, the page remains a useful guide rather than a blank rectangle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test privacy and failure behavior, not only the happy path
&lt;/h2&gt;

&lt;p&gt;My minimum test matrix includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load a game page and verify that no provider frame exists before Play.&lt;/li&gt;
&lt;li&gt;Inspect the network log and confirm that no game host request occurs before Play.&lt;/li&gt;
&lt;li&gt;Start the game using keyboard only.&lt;/li&gt;
&lt;li&gt;Simulate a slow connection and check that recovery help appears.&lt;/li&gt;
&lt;li&gt;Block the provider with CSP and verify the fallback remains usable.&lt;/li&gt;
&lt;li&gt;Click Play, Reset, and Play again; confirm only one iframe exists.&lt;/li&gt;
&lt;li&gt;Test a modified slug, narrow viewport, rejected fullscreen request, and denied analytics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important architectural choice is not the button color or the loading animation. It is preserving a meaningful boundary between “I opened this page” and “I chose to run this third-party game.”&lt;/p&gt;

&lt;p&gt;That boundary gives visitors more control and gives developers cleaner performance metrics, smaller failure domains, and a page that remains useful even when the embedded service does not.&lt;/p&gt;




&lt;p&gt;Disclosure: I help maintain Play Basketball Bros, the independent browser-game guide used as the implementation case study in this article. Third-party games remain controlled by their respective providers; this article does not claim ownership of them.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>performance</category>
      <category>a11y</category>
    </item>
  </channel>
</rss>
