@uniflowed/router 0.0.0-alpha.10 → 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.
- package/index.js +1 -0
- package/internal/runtime.js +162 -5
- package/package.json +2 -2
package/index.js
CHANGED
package/internal/runtime.js
CHANGED
|
@@ -131,14 +131,87 @@ export type LoadingModule = {
|
|
|
131
131
|
...
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* How a Twitter card is laid out, which is the whole of what `card` may be.
|
|
136
|
+
*
|
|
137
|
+
* A union rather than a string: every one of the four is spelled exactly this
|
|
138
|
+
* way and a fifth value is silently ignored by the crawler, so a typo in it
|
|
139
|
+
* costs a card and produces no error anywhere.
|
|
140
|
+
*/
|
|
141
|
+
export type TwitterCard = "summary" | "summary_large_image" | "app" | "player";
|
|
142
|
+
|
|
134
143
|
/** Document metadata a page or layout declares. */
|
|
135
144
|
export type Metadata = {
|
|
136
145
|
readonly title?: string,
|
|
137
146
|
readonly description?: string,
|
|
147
|
+
/**
|
|
148
|
+
* The absolute URL every other URL here is resolved against.
|
|
149
|
+
*
|
|
150
|
+
* Open Graph and Twitter both require absolute image URLs, and a route
|
|
151
|
+
* module has no way to know the host it will be served from — so without
|
|
152
|
+
* this, `openGraph.images: ["/og.png"]` ships exactly as written and is not
|
|
153
|
+
* a valid `og:image`. Declare it once on the root layout and every
|
|
154
|
+
* descendant inherits it through the same merge as everything else.
|
|
155
|
+
*
|
|
156
|
+
* Resolution is the URL standard's, so `"/og.png"` is resolved against the
|
|
157
|
+
* *origin* and `"og.png"` against the base's own path — not against the
|
|
158
|
+
* page's URL, which `Head` does not know.
|
|
159
|
+
*/
|
|
160
|
+
readonly metadataBase?: string,
|
|
161
|
+
/**
|
|
162
|
+
* This page's canonical URL, for `<link rel="canonical">` and `og:url`.
|
|
163
|
+
*
|
|
164
|
+
* Relative to `metadataBase` when it is not absolute. A page
|
|
165
|
+
* reachable at more than one path — a query a filter added, a duplicate
|
|
166
|
+
* under a second section — is one page, and this is how it says so.
|
|
167
|
+
*/
|
|
168
|
+
readonly canonical?: string,
|
|
138
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
|
+
*/
|
|
177
|
+
readonly title?: string,
|
|
178
|
+
/** The description a share card shows. Falls back to `description`. */
|
|
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,
|
|
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,
|
|
203
|
+
},
|
|
204
|
+
readonly twitter?: {
|
|
205
|
+
readonly card?: TwitterCard,
|
|
206
|
+
readonly site?: string,
|
|
207
|
+
readonly creator?: string,
|
|
208
|
+
/** Falls back to `openGraph.title`, and then to `title`. */
|
|
139
209
|
readonly title?: string,
|
|
210
|
+
/** Falls back to `openGraph.description`, and then to `description`. */
|
|
140
211
|
readonly description?: string,
|
|
141
212
|
readonly images?: $ReadOnlyArray<string>,
|
|
213
|
+
/** Falls back to `openGraph.imageAlt`. */
|
|
214
|
+
readonly imageAlt?: string,
|
|
142
215
|
},
|
|
143
216
|
};
|
|
144
217
|
|
|
@@ -1543,19 +1616,103 @@ function renderable<TProps extends { ... }>(
|
|
|
1543
1616
|
return component as any;
|
|
1544
1617
|
}
|
|
1545
1618
|
|
|
1619
|
+
/**
|
|
1620
|
+
* One URL from a route's metadata, made absolute if it can be.
|
|
1621
|
+
*
|
|
1622
|
+
* Open Graph, Twitter and `rel="canonical"` all want an absolute URL, and a
|
|
1623
|
+
* route module cannot know the host it is served from — so `metadataBase` is
|
|
1624
|
+
* how a site says it once, and this is where it is applied.
|
|
1625
|
+
*
|
|
1626
|
+
* Three things it deliberately does not do. It does not resolve against the
|
|
1627
|
+
* *page's* URL: `Head` renders inside the route and does not know it, and a
|
|
1628
|
+
* `metadataBase` is a site-wide fact rather than a per-page one. It does not
|
|
1629
|
+
* invent a base: with none declared the value is emitted exactly as written,
|
|
1630
|
+
* which is what every page that predates this field already gets. And it does
|
|
1631
|
+
* not throw — a `metadataBase` that is not a URL is a mistake in one field,
|
|
1632
|
+
* and turning it into a blank page would be a worse answer than an unresolved
|
|
1633
|
+
* `og:image`.
|
|
1634
|
+
*/
|
|
1635
|
+
function absoluteUrl(value: string, base: void | string): string {
|
|
1636
|
+
if (base == null) return value;
|
|
1637
|
+
try {
|
|
1638
|
+
return new URL(value, base).href;
|
|
1639
|
+
} catch {
|
|
1640
|
+
return value;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1546
1644
|
component Head(metadata: Metadata) {
|
|
1547
|
-
const { title, description, openGraph } = metadata;
|
|
1645
|
+
const { title, description, metadataBase, canonical, openGraph, twitter } = metadata;
|
|
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;
|
|
1548
1665
|
return (
|
|
1549
1666
|
<>
|
|
1550
1667
|
{title != null ? <title>{title}</title> : null}
|
|
1551
1668
|
{description != null ? <meta name="description" content={description} /> : null}
|
|
1552
|
-
{
|
|
1553
|
-
{
|
|
1554
|
-
|
|
1669
|
+
{href != null ? <link rel="canonical" href={href} /> : null}
|
|
1670
|
+
{/* `og:url` *is* the canonical URL of the page, in Open Graph's own
|
|
1671
|
+
words, so one declaration answers both rather than asking a project
|
|
1672
|
+
to write the same URL twice and keep them in step. */}
|
|
1673
|
+
{href != null ? <meta property="og:url" content={href} /> : null}
|
|
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} />
|
|
1555
1685
|
) : null}
|
|
1556
1686
|
{openGraph?.images != null
|
|
1557
|
-
? openGraph.images.map((image) =>
|
|
1687
|
+
? openGraph.images.map((image) => (
|
|
1688
|
+
<meta key={image} property="og:image" content={absoluteUrl(image, metadataBase)} />
|
|
1689
|
+
))
|
|
1558
1690
|
: null}
|
|
1691
|
+
{openGraph?.imageAlt != null && openGraph?.images != null ? (
|
|
1692
|
+
<meta property="og:image:alt" content={openGraph.imageAlt} />
|
|
1693
|
+
) : null}
|
|
1694
|
+
{/* `name`, not `property`: Open Graph is RDFa and Twitter's cards are
|
|
1695
|
+
not, and a `property="twitter:card"` is ignored by the crawler that
|
|
1696
|
+
reads it. */}
|
|
1697
|
+
{twitter?.card != null ? <meta name="twitter:card" content={twitter.card} /> : null}
|
|
1698
|
+
{twitter?.site != null ? <meta name="twitter:site" content={twitter.site} /> : null}
|
|
1699
|
+
{twitter?.creator != null ? <meta name="twitter:creator" content={twitter.creator} /> : null}
|
|
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} />
|
|
1707
|
+
) : null}
|
|
1708
|
+
{twitter?.images != null
|
|
1709
|
+
? twitter.images.map((image) => (
|
|
1710
|
+
<meta key={image} name="twitter:image" content={absoluteUrl(image, metadataBase)} />
|
|
1711
|
+
))
|
|
1712
|
+
: null}
|
|
1713
|
+
{twitterImageAlt != null && twitter?.images != null ? (
|
|
1714
|
+
<meta name="twitter:image:alt" content={twitterImageAlt} />
|
|
1715
|
+
) : null}
|
|
1559
1716
|
</>
|
|
1560
1717
|
);
|
|
1561
1718
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/router",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
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.
|
|
34
|
+
"@uniflowed/server": "0.0.0-alpha.12"
|
|
35
35
|
}
|
|
36
36
|
}
|