Mã QR tự động chuyển hướng đến đúng cửa hàng ứng dụng
Một lập trình viên đã xây dựng dịch vụ shortlink riêng để mã QR trên danh thiếp in giấy có thể dẫn người dùng đến đúng cửa hàng ứng dụng tùy theo thiết bị. Bài viết phân tích cách hệ thống nhận diện thiết bị qua User-Agent, xử lý bot tìm kiếm và những cạm bẫy khi chuyển hướng vĩnh viễn.

Khi giới thiệu PokerNexus vào đầu tuần này, tác giả có nhắc qua rằng dự án triển khai "một máy chủ shortlink riêng cho các liên kết in ấn". Bài viết này giải thích cụ thể máy chủ đó làm gì và vì sao nó được thiết kế theo cách hiện tại.
Mã QR dẫn đến đúng cửa hàng ứng dụng
Mục tiêu ban đầu rất đơn giản: làm những tấm danh thiếp có mã QR ở mặt sau với dòng chữ "Tải về". Vấn đề nằm ở chỗ một mã QR chỉ mã hóa được một URL duy nhất, trong khi "tải ứng dụng" lại có nghĩa là ba đích đến khác nhau tùy vào người quét mã.
Thêm nữa, danh thiếp đã in ra thì không thể sửa được nữa một khi đã nằm trong ví của khách hàng, nên URL trên đó phải tiếp tục hoạt động kể cả khi đích đến thay đổi về sau. Cả hai vấn đề đều dẫn đến cùng một giải pháp: in một URL do mình kiểm soát, rồi để máy chủ quyết định mỗi yêu cầu sẽ đi đâu. Các tấm thiếp trỏ tới https://go.pokernexus.com/app.
Vì sao phải là một miền riêng
Có thể dùng một đường dẫn trên miền chính để có địa chỉ ngắn hơn, nhưng cách đó không hoạt động. Ứng dụng iOS đăng ký pokernexus.com làm miền universal link, và tệp apple-app-site-association của nó khai báo mọi đường dẫn ("/": "*"), bởi ứng dụng và website dùng chung một cây định tuyến. Android với verified app link cũng làm điều tương tự cho miền gốc.
Điều đó có nghĩa là ứng dụng đã cài sẽ chặn mọi liên kết tới miền chính. Một đường dẫn /app ở đó sẽ mở ứng dụng trên đúng những chiếc điện thoại vốn đã có ứng dụng, còn phần chuyển hướng thì chỉ chạy trên những máy chưa cài. Tệ hơn, nó đặt logic dò thiết bị lên trước toàn bộ website, nơi một lỗi nhỏ có thể đẩy mọi khách truy cập vào App Store.
Miền go.pokernexus.com được cố ý để không đăng ký. Danh sách quyền của iOS chỉ khai báo applinks:pokernexus.com, còn Android tải assetlinks.json theo từng host nên host shortlink đơn giản là không phục vụ tệp nào. Cả hai nền tảng đều không chặn nó. Chạy nó như một dịch vụ độc lập cũng biến "không bao giờ phục vụ ứng dụng trên host đó" từ một quy tắc phải ghi nhớ thành một thuộc tính của hệ thống triển khai. Dịch vụ này không import gói nào từ workspace, nên image Docker nhỏ gọn và có thể triển khai theo lịch riêng. Việc đổi đích đến chỉ là sửa một dòng, không cần build lại website.
Thiết kế mã QR cho danh thiếp
Mã QR được tạo bằng công cụ chạy hoàn toàn trong trình duyệt, xuất ra SVG hoặc PNG thuần — đúng định dạng mà xưởng in cần.
Vài chi tiết quan trọng hơn trên danh thiếp so với trên màn hình. URL ngắn tạo ra mã ít dày đặc hơn, và mã ít dày đặc hơn thì các ô vuông lớn hơn, quét ổn định hơn ở kích thước danh thiếp. Chuỗi go.pokernexus.com/app đủ ngắn để giữ mã QR ở phiên bản thấp. Việc đặt logo vào giữa sẽ che mất một số ô, nên mã phải dựa vào khả năng sửa lỗi để bù lại. Công cụ tạo mã tự động nâng mức lên H (khoảng 30% khả năng phục hồi) ngay khi thêm biểu tượng ở giữa, nhờ đó logo câu lạc bộ PokerNexus nằm chính giữa mà không làm hỏng khả năng quét.
Bảng định tuyến và logic chọn đích đến
Máy chủ là một ứng dụng Hono nhỏ, độc lập. Mọi nơi nó có thể gửi người dùng tới đều được mô tả bằng một đối tượng Destination: một URL, kèm cờ cho biết có nên sao chép chuỗi truy vấn đến hay không.
Đường dẫn / đưa khách truy cập vào website, còn /app hỏi hàm storeFor xem đích đến nào phù hợp với thiết bị. Quyết định chỉ dựa trên header User-Agent:
Mọi nhánh không khớp chắc chắn đều rơi về website. Sự bất đối xứng này là có chủ đích: một câu trả lời sai dẫn tới website vẫn là một trang hoạt động bình thường, trong khi một câu trả lời sai dẫn tới cửa hàng ứng dụng lại là lời mời cài đặt cho một thiết bị không chạy được ứng dụng.
Thứ tự kiểm tra quan trọng hơn tưởng tượng
Thứ tự các bước kiểm tra rất dễ bị bỏ qua. Bot thu thập dữ liệu của Google tự nhận là điện thoại và thêm tên riêng vào cuối, nên user agent của bot Android chứa chuỗi Android, còn bot iOS chứa chuỗi iPhone.
Nếu kiểm tra thiết bị chạy trước, Googlebot sẽ bị đẩy tới trang cửa hàng ứng dụng trong khi người dùng laptop lại được đưa tới website. Phục vụ bot khác với người dùng thật chính là dấu hiệu của cloaking, và đó không phải điều mà tác giả muốn công cụ tìm kiếm suy luận ra. Kiểm tra bot trước sẽ đưa mọi bot tới cùng nơi mà khách truy cập máy tính để bàn đi.
Mẫu nhận diện bot được thiết kế khớp chuỗi con một cách lỏng lẻo có chủ đích. Một kết quả dương tính giả chỉ khiến một chiếc điện thoại mất liên kết cửa hàng và rơi về website. Một kết quả âm tính giả lại đẩy bot vào cửa hàng ứng dụng. Hai loại sai sót này không cùng mức độ nghiêm trọng, nên mẫu nhận diện nghiêng về phía website.
Trường hợp iPad: một sự đánh đổi được chấp nhận
Chrome trên iPad ghi rõ tên thiết bị trong user agent, nên mẫu nhận diện iOS bắt được nó. Safari trên iPad thì không. Kể từ iPadOS 13, trình duyệt này mặc định yêu cầu phiên bản website dành cho máy tính để bàn, và user agent nó gửi giống hệt từng byte so với Safari trên máy Mac.
Các trình duyệt nhân Chromium cung cấp một giải pháp có cấu trúc hơn user agent, gọi là client hints — những header yêu cầu có tiền tố Sec-CH-UA-, ví dụ Sec-CH-UA-Platform: "Android" và Sec-CH-UA-Mobile: ?1, mô tả trình duyệt, nền tảng và loại thiết bị thành các trường riêng biệt thay vì một chuỗi dài. Tuy nhiên Safari không gửi chúng, nên không có header nào phân biệt được iPad với máy Mac.
Thử nghiệm đầu tiên là một trang trung gian nhỏ cho mọi user agent Macintosh, kiểm tra navigator.maxTouchPoints bằng JavaScript (iPad báo có điểm chạm, máy Mac thì không) rồi gọi location.replace với đích đến phù hợp. Cách này hoạt động, nhưng đã bị gỡ bỏ ngay trong cùng pull request. Nó khiến mọi khách dùng Mac tốn thêm một vòng yêu cầu và thoáng thấy màn hình trắng. Nó cũng thay thế một phản hồi 302 có thể lưu cache bằng phản hồi 200 buộc phải yêu cầu không lưu cache, đồng thời đặt một đoạn script lên một host mà toàn bộ thiết kế chỉ là một bảng tra cứu và một nhánh điều kiện.
Vì vậy Safari trên iPad chấp nhận rơi về website — một sự đánh đổi được thừa nhận. Website có sẵn huy hiệu App Store và Google Play trên trang Giới thiệu, nên phương án dự phòng này không phải là ngõ cụt.
Vì sao phải dùng chuyển hướng tạm thời
Rất dễ muốn dùng mã 301 vì URL in trên danh thiếp là vĩnh viễn. Nhưng chuyển hướng vĩnh viễn sẽ bị trình duyệt và mọi thứ đứng trước nó lưu cache. Một chiếc điện thoại đổi chủ sẽ giữ mãi câu trả lời của chủ cũ, và việc trỏ lại một liên kết đã in sẽ không còn tác dụng với bất kỳ ai đã từng quét mã. Trỏ lại liên kết in chính là một trong những lý do chính để dùng máy chủ chuyển hướng, nên ở đây phải dùng chuyển hướng tạm thời.
Header Vary giải quyết mối lo tương tự ở tầng cache. Mặc định, một cache (của trình duyệt, CDN, hay proxy trong mạng doanh nghiệp) lưu phản hồi chỉ theo URL và trả lại phản hồi đó cho yêu cầu tiếp theo với cùng URL. Vary cho cache biết những header yêu cầu nào cũng ảnh hưởng đến phản hồi, để nó chỉ tái sử dụng bản đã lưu khi các header đó cũng khớp. Đường dẫn /app trả về ba đích đến khác nhau cho cùng một URL, nên nếu thiếu Vary: User-Agent, một cache chia sẻ có thể lưu chuyển hướng dành cho điện thoại Android và trao liên kết Play Store đó cho chiếc iPhone tiếp theo.
Phương thức HEAD được xử lý song song với GET vì các công cụ kiểm tra liên kết thường dùng nó, và một công cụ kiểm tra trỏ vào địa chỉ in trên danh thiếp nên thấy cùng phản hồi 302 như điện thoại.
Xử lý chuỗi truy vấn
Gắn thẻ chiến dịch vào liên kết in (?utm_source=cards&utm_medium=qr) là việc hữu ích, nên phương án dự phòng về website sẽ chuyển tiếp chuỗi truy vấn đến. Còn các đích đến cửa hàng ứng dụng thì bỏ nó đi.
Việc bỏ chuỗi truy vấn với Google Play không phải là tùy chọn. ID gói của trang Play nằm trong chuỗi truy vấn (?id=com.pokernexus.app), và hàm locationFor thay thế chuỗi truy vấn chứ không gộp vào. Chuyển tiếp thẻ chiến dịch tới đó sẽ ghi đè ID và mở ra một trang không tồn tại. Một bài kiểm tra ghim đích đến Play ở trạng thái "bỏ" để điều này không thể thay đổi một cách âm thầm.
Kiểm thử và những chi tiết nhỏ còn lại
Nếu ứng dụng đã được cài, /app vẫn dẫn tới trang cửa hàng, nơi hệ điều hành hiển thị nút "Mở" thay vì "Tải". Việc đưa người dùng vào cửa hàng rồi sau khi cài đặt mở thẳng một màn hình cụ thể (deferred deep linking) là một tính năng khác, và không phải thứ mà máy chủ này cố gắng cung cấp.
Các đường dẫn không xác định trả về 404 thay vì rơi về website. Một lỗi đánh máy trong URL in trên danh thiếp mà âm thầm dẫn về trang chủ trông giống như đang hoạt động, và thời điểm phát hiện lỗi đánh máy đó là lúc kiểm tra mã QR, chứ không phải sau khi cả hộp danh thiếp đã in xong và giao đến. Cùng lý do đó, /App, /apps và /app/extra đều trả về 404, trong khi dấu gạch chéo ở cuối được bỏ qua.
Các bài kiểm thử dùng chuỗi user agent thật thay vì chuỗi rút gọn. Ngoài điện thoại và máy tính để bàn thông thường, bộ dữ liệu mẫu còn có trình duyệt trong ứng dụng của Instagram và Facebook, máy tính bảng Android, Chrome và Safari trên iPad, công cụ mở rộng liên kết của Slack, curl, và cả hai bot thu thập dữ liệu dành cho điện thoại của Google. Một bài kiểm thử khác đọc ID App Store và ID gói Android từ mã nguồn của ứng dụng khách rồi xác nhận chúng khớp với các hằng số trong dịch vụ shortlink, để hai bên không thể lệch nhau.
Kết quả là một dịch vụ nhỏ đến mức có thể đọc hết trong một lần ngồi, và một URL duy nhất hoạt động trên mặt sau tấm danh thiếp bất kể thứ gì quét vào nó. Với những ai muốn tự tạo mã QR, công cụ tạo mã là miễn phí và không có dữ liệu nào bạn nhập rời khỏi trình duyệt.
Bài viết liên quan

Công nghệ
Mô hình AI hàng đầu giỏi Vật lý đến đâu? Nghiên cứu mới chỉ ra các bài kiểm tra hiện hành đang đánh giá sai
16 tháng 9, 2026

Công nghệ
Nộp đơn xin việc lẽ ra nên khó hơn. Thật đấy
25 tháng 8, 2026

Công nghệ
Bastardica: Công cụ tạo font "lai" độc đáo chạy hoàn toàn trên trình duyệt
23 tháng 9, 2026