Skip to content

Phones/tablets: social links open a new tab that closes itself if an app takes over - #230

Merged
darkmastermindz merged 3 commits into
mainfrom
ios-same-tab
Oct 25, 2026
Merged

darkmastermindz merged 3 commits into
mainfrom
ios-same-tab

Conversation

@darkmastermindz

@darkmastermindz darkmastermindz commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

On iPhone Safari, tapping the Mastodon pill (https://hansel.run/mastodon) left a blank tab behind. Here's why:

  • The pill opened the link in a new tab.
  • hansel.run redirected to the Mastodon profile, and iOS handed that link to the Mastodon app.
  • The new tab was left empty.

The same happens with any link whose site has an app (Instagram, Messenger, LinkedIn, Strava, GitHub), and on Android too.

Links still open in a new tab. On phones and tablets, that tab first loads a small hanselwei.dev hand-off page, /out/<id>, which goes straight on to the link:

  • If the link loads in the browser, nothing changes.
  • If the phone hands it to an app, the tab stays on the hand-off page. Once you come back from the app, the tab closes itself, so no blank tab is left.
  • Desktops open the links directly, as before.

This replaces this PR's first version, which opened the links in the same tab.

Related Issue

None. The user reported it directly.

Changes Made

  • New utils/outbound.js with the social link addresses by id (github, mastodon, instagram, instagram-codes...), plus outboundFor(id) (Object.hasOwn) and outPath(id).
  • New pages/out/[id].vue, the hand-off page. It shows "Opening Mastodon…" with a link back to the site, and sends the tab on to the link.
    • App takes over: if the page is hidden and then shown again while still on it, it calls window.close(). Where it can't close the tab, it goes back (or to the home page).
    • Back from the profile: it doesn't bounce into the link again. A history.state marker and the bfcache pageshow handle this.
    • Unknown id: the tab closes, or goes back.
    • It's noindex, and /out is disallowed in robots.txt.
    • Its tab title is Opening… · Hansel Wei, matching the site's title format from Tab titles end with "Hansel Wei" #239.
  • nuxt.config.js prerenders one /out/<id> page per id. I used the path rather than ?to= because Nuxt briefly drops the query string while starting up.
  • utils/mobileDevice.js:
    • isAppleMobile counts iPhone, iPad, iPod, and iPadOS in desktop mode.
    • handsOffToApps counts those plus Android.
  • components/SocialFlipLink.vue: once the page has loaded on a phone or tablet, the pill and the Instagram menu links point at /out/<id>. The prerendered HTML (what crawlers see) keeps the real addresses.
  • components/Socials.vue takes its addresses from utils/outbound.js.
  • Tests:
    • tests/app/mobile-device.test.mjs: which devices count.
    • tests/security/outbound.test.mjs, with an OUTBOUND_IDS payload list: only known ids lead anywhere. URLs, javascript:, path tricks, prototype keys and XSS all give nothing, and every address is https.
  • README.md: the social links paragraph.
  • The Nuxt 4 fix that was ported here from Fix main on Nuxt 4: CSS minify error and CSP-blocked import map #241 is now on main, so it no longer shows in this diff.
  • Ported from Accessibility: readable count on the selected Projects chip #249 (pages/projects.vue): the selected Projects chip's count is readable on the purple (#e9d5ff, 6.2:1).

Testing

Device Pill href Link loads normally App takes over (simulated: navigation cancelled, tab hidden then shown) Back from the profile
Desktop https://hansel.run/mastodon new tab at hansel.run n/a n/a
Pixel 7 /out/mastodon new tab ends at hansel.run tab showed "Opening Mastodon…", then closed lands back on the site
iPhone 14 /out/mastodon new tab ends at hansel.run tab showed "Opening Mastodon…", then closed lands back on the site

On phones, the Instagram menu links are /out/instagram and /out/instagram-codes.

Additional Information

  • The real hand-off from iOS or Android to an app can't be tested here, and the sandbox can't reach hansel.run. Please tap Mastodon on an iPhone with the deploy preview.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS

@darkmastermindz darkmastermindz changed the title iPhone/iPad: open social links in the same tab (no blank tab after app hand-off) Phones/tablets: social links open a new tab that closes itself if an app takes over Oct 20, 2026
@github-actions
github-actions Bot force-pushed the ios-same-tab branch 2 times, most recently from 09ad62b to 8e719f7 Compare October 21, 2026 04:35
@github-actions
github-actions Bot force-pushed the ios-same-tab branch 2 times, most recently from 083b56e to cd007e7 Compare October 23, 2026 19:18
@github-actions
github-actions Bot force-pushed the ios-same-tab branch 2 times, most recently from 1a6802e to 9b648ed Compare October 24, 2026 21:11
@darkmastermindz

Copy link
Copy Markdown
Owner Author

@claude rebase

Copy link
Copy Markdown
Owner Author

Android touch (1/4) and (3/4) failed on ad43e66. The cause isn't this PR.


Generated by Claude Code

@github-actions
github-actions Bot force-pushed the ios-same-tab branch 2 times, most recently from 2c4ad67 to 82a4240 Compare October 25, 2026 02:02
…e no blank tab

On iPhone Safari the Mastodon pill (hansel.run/mastodon) opened a new tab,
iOS handed the redirect to the Mastodon app, and the tab was left blank.
On iPhone and iPad the social links (and the Instagram menu) now open in
the same tab: the app takes over and Safari stays on the site, or the
page loads there and Back returns. Other platforms keep new tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
… takes over

Replaces the same-tab approach. On phones and tablets each social link
opens /out/<id> in a new tab, a small hand-off page that goes on to the
link. When the phone hands the link to an app instead (Mastodon via
hansel.run, Instagram, Messenger...), the tab stays on the hand-off page,
and once the visitor comes back it closes itself rather than staying
blank. Back from a profile doesn't bounce into the link again.

The addresses move to utils/outbound.js; the id in the path only ever
names one of them (Object.hasOwn), so the page can't redirect anywhere
else. One page per id is prerendered, and /out is kept out of robots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
@darkmastermindz
darkmastermindz merged commit 076d670 into main Oct 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants