A complete summary and step-by-step guide to implement universal links (Android/iOS) and dynamic previews for your vocabulary app, starting from setting up assetlinks.json
and apple-app-site-association
to implementing the backend and Flutter app functionality.
assetlinks.json
Create a file named assetlinks.json
:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.t9chnih.app",
"sha256_cert_fingerprints": [
"YOUR_APP_SHA256_CERT_FINGERPRINT"
]
}
}
]
com.t9chnih.app
with your app's package name.YOUR_APP_SHA256_CERT_FINGERPRINT
with your app's SHA256 fingerprint, obtained using:
keytool -list -v -keystore <keystore-path> -alias <key-alias>
Host assetlinks.json
at:
https://www.t9chnih.com/.well-known/assetlinks.json
Update AndroidManifest.xml
:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="www.t9chnih.com" android:pathPrefix="/share/vocabulary" />
</intent-filter>
apple-app-site-association
Create a file named apple-app-site-association
:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.t9chnih.app",
"paths": ["/share/vocabulary/*"]
}
]
}
}
TEAMID
with your Apple Developer Team ID.com.t9chnih.vlug
with your app’s bundle ID.Host apple-app-site-association
at:
https://www.t9chnih.com/.well-known/apple-app-site-association
Add Associated Domains in Xcode:
applinks:www.t9chnih.com
Modify the /share/vocabulary/:id
endpoint to serve Open Graph and Twitter meta tags dynamically.
router.get('/share/vocabulary/:vocabularyId', async (req, res) => {
const vocabularyId = req.params.vocabularyId;
const vocabularyEntry = await pool.query(
'SELECT * FROM vocabulary WHERE vocabulary_id = $1 AND is_deleted = FALSE',
[vocabularyId]
);
if (vocabularyEntry.rowCount === 0) {
return res.status(404).send('Vocabulary not found');
}
const { word, translation, example } = vocabularyEntry.rows[0];
const imageUrl = `https://www.t9chnih.com/share/vocabulary/${vocabularyId}/image`;
res.set('Content-Type', 'text/html');
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>${word} - Vocabulary</title>
<meta property="og:title" content="${word}" />
<meta property="og:description" content="Translation: ${translation}. Example: ${example}" />
<meta property="og:image" content="${imageUrl}" />
<meta property="og:url" content="https://www.t9chnih.com/share/vocabulary/${vocabularyId}" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${word}" />
<meta name="twitter:description" content="Translation: ${translation}. Example: ${example}" />
<meta name="twitter:image" content="${imageUrl}" />
</head>
<body>
<h1>${word}</h1>
<p><strong>Translation:</strong> ${translation}</p>
<p><strong>Example:</strong> ${example}</p>
</body>
</html>
`);
});
Use node-canvas
to create images dynamically with vocabulary details.
router.get('/share/vocabulary/:vocabularyId/image', async (req, res) => {
const vocabularyId = req.params.vocabularyId;
const vocabularyEntry = await pool.query(
'SELECT * FROM vocabulary WHERE vocabulary_id = $1 AND is_deleted = FALSE',
[vocabularyId]
);
if (vocabularyEntry.rowCount === 0) {
return res.status(404).send('Vocabulary not found');
}
const { word, translation, example } = vocabularyEntry.rows[0];
const canvas = createCanvas(1200, 628);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000000';
ctx.font = 'bold 50px Arial';
ctx.textAlign = 'center';
ctx.fillText(word, canvas.width / 2, 200);
ctx.font = '30px Arial';
ctx.fillText(`Translation: ${translation}`, canvas.width / 2, 300);
ctx.font = 'italic 25px Arial';
ctx.fillText(`Example: ${example}`, canvas.width / 2, 400);
res.setHeader('Content-Type', 'image/png');
res.send(canvas.toBuffer());
});
Use the app_links
package to handle universal links.
class DeepLinkController extends GetxController {
final AppLinks _appLinks = AppLinks();
@override
void onInit() {
super.onInit();
_initDeepLinks();
}
void _initDeepLinks() async {
_appLinks.uriLinkStream.listen((Uri? uri) {
if (uri != null) _handleDeepLink(uri);
});
final Uri? initialUri = await _appLinks.getInitialAppLink();
if (initialUri != null) _handleDeepLink(initialUri);
}
void _handleDeepLink(Uri uri) {
if (uri.host == 'www.t9chnih.com' && uri.pathSegments.length == 3 && uri.pathSegments[0] == 'share' && uri.pathSegments[1] == 'vocabulary') {
final vocabId = uri.pathSegments[2];
Get.toNamed('/vocabularyDetail', arguments: {'vocabId': vocabId});
}
}
}
Test universal links:
adb shell am start -W -a android.intent.action.VIEW -d "https://www.t9chnih.com/share/vocabulary/587" com.t9chnih.vlug
Open the link in Safari:
https://www.t9chnih.com/share/vocabulary/587
Use tools to test previews:
This complete guide sets up universal links, dynamic meta tags, and Flutter app handling for your vocabulary app. Let me know if you need further assistance! 🚀