Skip to content

feat: metadata parity for appLinks, iTunes, Twitter player/app cards#452

Open
NathanDrake2406 wants to merge 3 commits intocloudflare:mainfrom
NathanDrake2406:feat/metadata-applinks-itunes-twitter-cards
Open

feat: metadata parity for appLinks, iTunes, Twitter player/app cards#452
NathanDrake2406 wants to merge 3 commits intocloudflare:mainfrom
NathanDrake2406:feat/metadata-applinks-itunes-twitter-cards

Conversation

@NathanDrake2406
Copy link
Contributor

@NathanDrake2406 NathanDrake2406 commented Mar 11, 2026

Summary

  • Add appLinks metadata support — renders al:* property tags for iOS, Android, Windows, and web platforms
  • Add itunes metadata support — renders apple-itunes-app meta tag with app-id and app-argument
  • Add Twitter player card support — renders twitter:player, twitter:player:stream, twitter:player:width, twitter:player:height
  • Add Twitter app card support — renders twitter:app:name/id/url per platform (iphone, ipad, googleplay)

All four were documented as N/A gaps in metadata.test.ts (lines 345, 354, 363). Types match Next.js extra-types.d.ts and twitter-types.d.ts definitions. Rendering matches Next.js generate/opengraph.js and generate/basic.js output.

Test plan

  • 4 new fixture pages (metadata-itunes, metadata-applinks, metadata-twitter-player, metadata-twitter-app)
  • 4 new integration tests asserting exact meta tag output
  • All 45 metadata tests pass
  • All 652 shims tests pass
  • Typecheck clean
  • CI (Format, Lint, Typecheck, Vitest, Playwright E2E)

…pp cards

Add four missing Next.js metadata features to the Metadata type and
MetadataHead renderer:

- appLinks: renders al:* property tags (ios, iphone, ipad, android,
  windows_phone, windows, windows_universal, web)
- itunes: renders apple-itunes-app name tag with app-id and app-argument
- twitter player cards: renders twitter:player, twitter:player:stream,
  twitter:player:width, twitter:player:height
- twitter app cards: renders twitter:app:name/id/url per platform
  (iphone, ipad, googleplay)

Includes fixture pages and tests ported from Next.js test suite.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@452

commit: e76ab01

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd7dd95a8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +609 to +612
<meta key={key++} name="twitter:player" content={player.playerUrl.toString()} />,
);
elements.push(
<meta key={key++} name="twitter:player:stream" content={player.streamUrl.toString()} />,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve Twitter card URLs against metadataBase

The new Twitter player/app rendering path emits URL values via .toString() instead of the existing resolveUrl helper, so relative values (for example playerUrl: "/player" with metadataBase) are output as relative strings instead of absolute URLs. This is inconsistent with how other metadata URLs are handled in this file (e.g. OG/Twitter images) and causes malformed Twitter card metadata unless every caller manually provides absolute URLs.

Useful? React with 👍 / 👎.

Comment on lines +831 to +833
property={`al:${platform}:${k}`}
content={String(v)}
/>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve App Links URL fields against metadataBase

App Links tags currently stringify descriptor values directly, so url fields under appLinks (such as al:ios:url or al:web:url) remain relative when metadata uses metadataBase. Because these tags are expected to carry canonical URLs and the shim already resolves other URL-bearing metadata fields, this introduces incorrect output for relative-path configurations.

Useful? React with 👍 / 👎.

@james-elicx
Copy link
Collaborator

/bigbonk review

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 11, 2026

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: metadata parity for appLinks, iTunes, Twitter player/app cards

Good work filling in the four documented metadata gaps. The types match Next.js definitions, the fixture pages and tests are well-structured, and the rendering logic is correct for the common cases. I have one behavioral difference from Next.js and a few minor nits.

Behavioral difference

Twitter app card name emission is gated on id presence — Next.js doesn't do this. In Next.js, the name, id, and url checks inside the platform loop are independent. If app.name is set, Next.js emits twitter:app:name:{platform} for all three platforms regardless of whether app.id[platform] exists. The current implementation nests name inside if (app.id[platform] !== undefined), so a platform without an id won't get a name tag either. See inline comment below.

This probably doesn't matter in practice (you'd rarely set name without any id), but since the goal is Next.js parity, it's worth aligning.

Test coverage note

The twitter-app test fixture happens to have all three platforms with both id and name set, so it doesn't exercise the divergence above. A test with name set but only one platform's id defined would catch it.

Summary

Overall this is a clean, well-tested PR. The one structural issue (twitter app name gating) is a minor parity gap. The rest looks solid — types are accurate, resolveUrl is used correctly for URL fields, and the N/A comments are properly cleaned up.

Comment on lines +631 to +655
for (const platform of ["iphone", "ipad", "googleplay"] as const) {
if (app.id[platform] !== undefined) {
if (app.name) {
elements.push(
<meta key={key++} name={`twitter:app:name:${platform}`} content={app.name} />,
);
}
elements.push(
<meta
key={key++}
name={`twitter:app:id:${platform}`}
content={String(app.id[platform])}
/>,
);
if (app.url?.[platform] !== undefined) {
const appUrl = app.url[platform]!.toString();
elements.push(
<meta
key={key++}
name={`twitter:app:url:${platform}`}
content={resolveUrl(appUrl) ?? appUrl}
/>,
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavioral difference from Next.js: In Next.js, the name, id, and url checks inside the platform loop are independent — not nested. If app.name is set, Next.js emits twitter:app:name:{platform} for all three platforms regardless of whether app.id[platform] exists:

// Next.js (packages/next/src/lib/metadata/metadata.tsx)
for (const platform of ['iphone', 'ipad', 'googleplay'] as const) {
  if (app.name) { /* emit name */ }
  if (app.id[platform]) { /* emit id */ }
  if (app.url?.[platform]) { /* emit url */ }
}

Here, name and url are nested inside if (app.id[platform] !== undefined), which means a platform without an id won't get name or url tags. This diverges from Next.js behavior.

Suggested change
for (const platform of ["iphone", "ipad", "googleplay"] as const) {
if (app.id[platform] !== undefined) {
if (app.name) {
elements.push(
<meta key={key++} name={`twitter:app:name:${platform}`} content={app.name} />,
);
}
elements.push(
<meta
key={key++}
name={`twitter:app:id:${platform}`}
content={String(app.id[platform])}
/>,
);
if (app.url?.[platform] !== undefined) {
const appUrl = app.url[platform]!.toString();
elements.push(
<meta
key={key++}
name={`twitter:app:url:${platform}`}
content={resolveUrl(appUrl) ?? appUrl}
/>,
);
}
}
if (tw.app) {
const { app } = tw;
for (const platform of ["iphone", "ipad", "googleplay"] as const) {
if (app.name) {
elements.push(
<meta key={key++} name={`twitter:app:name:${platform}`} content={app.name} />,
);
}
if (app.id[platform] !== undefined) {
elements.push(
<meta
key={key++}
name={`twitter:app:id:${platform}`}
content={String(app.id[platform])}
/>,
);
}
if (app.url?.[platform] !== undefined) {
const appUrl = app.url[platform]!.toString();
elements.push(
<meta
key={key++}
name={`twitter:app:url:${platform}`}
content={resolveUrl(appUrl) ?? appUrl}
/>,
);
}
}
}

if (!entries) continue;
const list = Array.isArray(entries) ? entries : [entries];
for (const entry of list) {
for (const [k, v] of Object.entries(entry)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Object.entries(entry) iterates properties in insertion order, which means the tag order depends on how the user wrote their object literal. This is fine functionally (and how Next.js does it too for resolved arrays), but worth noting that for AppLinksAndroid the package field will render as al:android:package — which is correct. Just double-checking you considered that Object.entries will include all enumerable own properties, so if someone passes unexpected extra fields they'd also be rendered. Low risk but worth being aware of.

if (tw.players) {
const players = Array.isArray(tw.players) ? tw.players : [tw.players];
for (const player of players) {
const playerUrl = player.playerUrl.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: player.playerUrl.toString() is called before passing to resolveUrl, which is good. But note that if playerUrl is already a URL object, .toString() produces an absolute URL that resolveUrl will pass through unchanged (the startsWith('http') check). This means metadataBase resolution only kicks in for string values that are relative paths — which is the correct behavior and matches how it should work. Just confirming this is intentional.

expect(html).toContain('name="twitter:app:name:iphone" content="My App"');
expect(html).toContain('name="twitter:app:id:iphone" content="id123456789"');
expect(html).toContain('name="twitter:app:url:iphone" content="https://example.com/iphone"');
expect(html).toContain('name="twitter:app:id:ipad" content="id123456789"');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test doesn't assert twitter:app:name:ipad. Since the fixture sets name: "My App" and all three platforms have IDs, the tag should be present. Consider adding:

Suggested change
expect(html).toContain('name="twitter:app:id:ipad" content="id123456789"');
expect(html).toContain('name="twitter:app:name:ipad" content="My App"');
expect(html).toContain('name="twitter:app:id:ipad" content="id123456789"');

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 11, 2026

Review posted successfully. Here's a summary of what I found:

One behavioral difference from Next.js:

  • The Twitter app card rendering nests name and url tag emission inside if (app.id[platform] !== undefined), but Next.js treats these as independent checks. In Next.js, if app.name is set, it emits twitter:app:name:{platform} for all three platforms regardless of whether that platform has an id. I provided a suggestion that restructures the loop to match Next.js's flat/independent check pattern.

Minor items:

  • The test for twitter-app cards is missing an assertion for twitter:app:name:ipad — suggested adding it.
  • Noted that Object.entries on the appLinks descriptors will pass through any unexpected extra properties, which is low risk but worth awareness.
  • Confirmed that resolveUrl is correctly applied to URL fields in the new code paths.

Overall, the PR is well-structured with clean types, good fixture pages, and solid test coverage. The main actionable item is the twitter app name gating fix for Next.js parity.

github run

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