@uniflowed/router 0.0.0-alpha.11 → 0.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -167,17 +167,51 @@ export type Metadata = {
167
167
  */
168
168
  readonly canonical?: string,
169
169
  readonly openGraph?: {
170
+ /**
171
+ * The title a share card shows.
172
+ *
173
+ * Falls back to `title`, because a page that has said what it is called
174
+ * has said what its card is called — and a site made to write it twice
175
+ * writes it twice once and then lets them drift.
176
+ */
170
177
  readonly title?: string,
178
+ /** The description a share card shows. Falls back to `description`. */
171
179
  readonly description?: string,
180
+ /**
181
+ * The Open Graph object type. `website` unless a page says otherwise.
182
+ *
183
+ * Defaulted rather than omitted because `og:type` is one of the four
184
+ * properties Open Graph requires, and a document without it is not an
185
+ * Open Graph document at all — so leaving it to every project to remember
186
+ * is leaving most of them without one.
187
+ */
188
+ readonly type?: string,
189
+ /**
190
+ * The name of the site the page belongs to, which a card prints above the
191
+ * title. Declared once on the root layout.
192
+ */
193
+ readonly siteName?: string,
172
194
  readonly images?: $ReadOnlyArray<string>,
195
+ /**
196
+ * What the card's image shows, for a reader who cannot see it.
197
+ *
198
+ * One description rather than one per image: a card shows one image, and
199
+ * the array exists so a site can offer a crawler a choice of sizes rather
200
+ * than so it can show several.
201
+ */
202
+ readonly imageAlt?: string,
173
203
  },
174
204
  readonly twitter?: {
175
205
  readonly card?: TwitterCard,
176
206
  readonly site?: string,
177
207
  readonly creator?: string,
208
+ /** Falls back to `openGraph.title`, and then to `title`. */
178
209
  readonly title?: string,
210
+ /** Falls back to `openGraph.description`, and then to `description`. */
179
211
  readonly description?: string,
180
212
  readonly images?: $ReadOnlyArray<string>,
213
+ /** Falls back to `openGraph.imageAlt`. */
214
+ readonly imageAlt?: string,
181
215
  },
182
216
  };
183
217
 
@@ -1610,6 +1644,24 @@ function absoluteUrl(value: string, base: void | string): string {
1610
1644
  component Head(metadata: Metadata) {
1611
1645
  const { title, description, metadataBase, canonical, openGraph, twitter } = metadata;
1612
1646
  const href = canonical != null ? absoluteUrl(canonical, metadataBase) : null;
1647
+ // A page that said what it is called has said what its card is called. Every
1648
+ // site that had to write both wrote the same string twice, and the second
1649
+ // one is the one that goes stale — the docs site shipped thirty pages whose
1650
+ // share cards carried an image and no title at all.
1651
+ //
1652
+ // `??`, not `||`: an empty string is a decision, and a page that deliberately
1653
+ // has no card title should get none rather than the document's.
1654
+ const cardTitle = openGraph?.title ?? title;
1655
+ const cardDescription = openGraph?.description ?? description;
1656
+ // `og:type` is one of the four properties Open Graph requires. A default is
1657
+ // the difference between a document with a card and a document without one,
1658
+ // and `website` is right for everything that is not an article or a video.
1659
+ const cardType = openGraph?.type ?? "website";
1660
+ // Only when the card was asked for. A page with no `twitter.card` gets no
1661
+ // Twitter tags at all, which is what a site that never wanted one meant.
1662
+ const twitterTitle = twitter != null ? (twitter.title ?? cardTitle) : null;
1663
+ const twitterDescription = twitter != null ? (twitter.description ?? cardDescription) : null;
1664
+ const twitterImageAlt = twitter != null ? (twitter.imageAlt ?? openGraph?.imageAlt) : null;
1613
1665
  return (
1614
1666
  <>
1615
1667
  {title != null ? <title>{title}</title> : null}
@@ -1619,30 +1671,48 @@ component Head(metadata: Metadata) {
1619
1671
  words, so one declaration answers both rather than asking a project
1620
1672
  to write the same URL twice and keep them in step. */}
1621
1673
  {href != null ? <meta property="og:url" content={href} /> : null}
1622
- {openGraph?.title != null ? <meta property="og:title" content={openGraph.title} /> : null}
1623
- {openGraph?.description != null ? (
1624
- <meta property="og:description" content={openGraph.description} />
1674
+ {cardTitle != null ? <meta property="og:title" content={cardTitle} /> : null}
1675
+ {cardDescription != null ? (
1676
+ <meta property="og:description" content={cardDescription} />
1677
+ ) : null}
1678
+ {/* Only alongside something else. A document with `og:type` and nothing
1679
+ more is not a card; it is one meta tag saying the page is a page. */}
1680
+ {cardTitle != null || cardDescription != null || openGraph?.images != null ? (
1681
+ <meta property="og:type" content={cardType} />
1682
+ ) : null}
1683
+ {openGraph?.siteName != null ? (
1684
+ <meta property="og:site_name" content={openGraph.siteName} />
1625
1685
  ) : null}
1626
1686
  {openGraph?.images != null
1627
1687
  ? openGraph.images.map((image) => (
1628
1688
  <meta key={image} property="og:image" content={absoluteUrl(image, metadataBase)} />
1629
1689
  ))
1630
1690
  : null}
1691
+ {openGraph?.imageAlt != null && openGraph?.images != null ? (
1692
+ <meta property="og:image:alt" content={openGraph.imageAlt} />
1693
+ ) : null}
1631
1694
  {/* `name`, not `property`: Open Graph is RDFa and Twitter's cards are
1632
1695
  not, and a `property="twitter:card"` is ignored by the crawler that
1633
1696
  reads it. */}
1634
1697
  {twitter?.card != null ? <meta name="twitter:card" content={twitter.card} /> : null}
1635
1698
  {twitter?.site != null ? <meta name="twitter:site" content={twitter.site} /> : null}
1636
1699
  {twitter?.creator != null ? <meta name="twitter:creator" content={twitter.creator} /> : null}
1637
- {twitter?.title != null ? <meta name="twitter:title" content={twitter.title} /> : null}
1638
- {twitter?.description != null ? (
1639
- <meta name="twitter:description" content={twitter.description} />
1700
+ {/* X reads the `og:` tags when these are absent, so these are not
1701
+ required — and every validator asks for them anyway, which is a good
1702
+ enough reason when the value is one the page has already given. They
1703
+ fall back through the card's title to the document's. */}
1704
+ {twitterTitle != null ? <meta name="twitter:title" content={twitterTitle} /> : null}
1705
+ {twitterDescription != null ? (
1706
+ <meta name="twitter:description" content={twitterDescription} />
1640
1707
  ) : null}
1641
1708
  {twitter?.images != null
1642
1709
  ? twitter.images.map((image) => (
1643
1710
  <meta key={image} name="twitter:image" content={absoluteUrl(image, metadataBase)} />
1644
1711
  ))
1645
1712
  : null}
1713
+ {twitterImageAlt != null && twitter?.images != null ? (
1714
+ <meta name="twitter:image:alt" content={twitterImageAlt} />
1715
+ ) : null}
1646
1716
  </>
1647
1717
  );
1648
1718
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/router",
3
- "version": "0.0.0-alpha.11",
3
+ "version": "0.0.0-alpha.12",
4
4
  "description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,6 +31,6 @@
31
31
  "react-dom": ">=19"
32
32
  },
33
33
  "dependencies": {
34
- "@uniflowed/server": "0.0.0-alpha.11"
34
+ "@uniflowed/server": "0.0.0-alpha.12"
35
35
  }
36
36
  }