@uniflowed/router 0.0.0-alpha.10 → 0.0.0-alpha.11
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 +89 -2
- package/package.json +2 -2
package/index.js
CHANGED
package/internal/runtime.js
CHANGED
|
@@ -131,15 +131,54 @@ 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?: {
|
|
139
170
|
readonly title?: string,
|
|
140
171
|
readonly description?: string,
|
|
141
172
|
readonly images?: $ReadOnlyArray<string>,
|
|
142
173
|
},
|
|
174
|
+
readonly twitter?: {
|
|
175
|
+
readonly card?: TwitterCard,
|
|
176
|
+
readonly site?: string,
|
|
177
|
+
readonly creator?: string,
|
|
178
|
+
readonly title?: string,
|
|
179
|
+
readonly description?: string,
|
|
180
|
+
readonly images?: $ReadOnlyArray<string>,
|
|
181
|
+
},
|
|
143
182
|
};
|
|
144
183
|
|
|
145
184
|
/** Arguments a loader receives. */
|
|
@@ -1543,18 +1582,66 @@ function renderable<TProps extends { ... }>(
|
|
|
1543
1582
|
return component as any;
|
|
1544
1583
|
}
|
|
1545
1584
|
|
|
1585
|
+
/**
|
|
1586
|
+
* One URL from a route's metadata, made absolute if it can be.
|
|
1587
|
+
*
|
|
1588
|
+
* Open Graph, Twitter and `rel="canonical"` all want an absolute URL, and a
|
|
1589
|
+
* route module cannot know the host it is served from — so `metadataBase` is
|
|
1590
|
+
* how a site says it once, and this is where it is applied.
|
|
1591
|
+
*
|
|
1592
|
+
* Three things it deliberately does not do. It does not resolve against the
|
|
1593
|
+
* *page's* URL: `Head` renders inside the route and does not know it, and a
|
|
1594
|
+
* `metadataBase` is a site-wide fact rather than a per-page one. It does not
|
|
1595
|
+
* invent a base: with none declared the value is emitted exactly as written,
|
|
1596
|
+
* which is what every page that predates this field already gets. And it does
|
|
1597
|
+
* not throw — a `metadataBase` that is not a URL is a mistake in one field,
|
|
1598
|
+
* and turning it into a blank page would be a worse answer than an unresolved
|
|
1599
|
+
* `og:image`.
|
|
1600
|
+
*/
|
|
1601
|
+
function absoluteUrl(value: string, base: void | string): string {
|
|
1602
|
+
if (base == null) return value;
|
|
1603
|
+
try {
|
|
1604
|
+
return new URL(value, base).href;
|
|
1605
|
+
} catch {
|
|
1606
|
+
return value;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1546
1610
|
component Head(metadata: Metadata) {
|
|
1547
|
-
const { title, description, openGraph } = metadata;
|
|
1611
|
+
const { title, description, metadataBase, canonical, openGraph, twitter } = metadata;
|
|
1612
|
+
const href = canonical != null ? absoluteUrl(canonical, metadataBase) : null;
|
|
1548
1613
|
return (
|
|
1549
1614
|
<>
|
|
1550
1615
|
{title != null ? <title>{title}</title> : null}
|
|
1551
1616
|
{description != null ? <meta name="description" content={description} /> : null}
|
|
1617
|
+
{href != null ? <link rel="canonical" href={href} /> : null}
|
|
1618
|
+
{/* `og:url` *is* the canonical URL of the page, in Open Graph's own
|
|
1619
|
+
words, so one declaration answers both rather than asking a project
|
|
1620
|
+
to write the same URL twice and keep them in step. */}
|
|
1621
|
+
{href != null ? <meta property="og:url" content={href} /> : null}
|
|
1552
1622
|
{openGraph?.title != null ? <meta property="og:title" content={openGraph.title} /> : null}
|
|
1553
1623
|
{openGraph?.description != null ? (
|
|
1554
1624
|
<meta property="og:description" content={openGraph.description} />
|
|
1555
1625
|
) : null}
|
|
1556
1626
|
{openGraph?.images != null
|
|
1557
|
-
? openGraph.images.map((image) =>
|
|
1627
|
+
? openGraph.images.map((image) => (
|
|
1628
|
+
<meta key={image} property="og:image" content={absoluteUrl(image, metadataBase)} />
|
|
1629
|
+
))
|
|
1630
|
+
: null}
|
|
1631
|
+
{/* `name`, not `property`: Open Graph is RDFa and Twitter's cards are
|
|
1632
|
+
not, and a `property="twitter:card"` is ignored by the crawler that
|
|
1633
|
+
reads it. */}
|
|
1634
|
+
{twitter?.card != null ? <meta name="twitter:card" content={twitter.card} /> : null}
|
|
1635
|
+
{twitter?.site != null ? <meta name="twitter:site" content={twitter.site} /> : null}
|
|
1636
|
+
{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} />
|
|
1640
|
+
) : null}
|
|
1641
|
+
{twitter?.images != null
|
|
1642
|
+
? twitter.images.map((image) => (
|
|
1643
|
+
<meta key={image} name="twitter:image" content={absoluteUrl(image, metadataBase)} />
|
|
1644
|
+
))
|
|
1558
1645
|
: null}
|
|
1559
1646
|
</>
|
|
1560
1647
|
);
|
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.11",
|
|
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.11"
|
|
35
35
|
}
|
|
36
36
|
}
|