@octanejs/seo 0.0.37 → 0.0.39
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/package.json +2 -2
- package/src/descriptors.ts +12 -4
- package/src/expand.ts +1 -1
- package/src/registry.ts +2 -2
- package/src/useStrayOwnerDiagnostic.ts +33 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/seo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"vitest": "^4.1.10",
|
|
37
|
-
"octane": "0.2.
|
|
37
|
+
"octane": "0.2.8"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"test": "vitest run"
|
package/src/descriptors.ts
CHANGED
|
@@ -30,20 +30,28 @@ export interface SeoDescriptor {
|
|
|
30
30
|
* Settings that belong to the app rather than to one declaration. They are
|
|
31
31
|
* registered like any other metadata and merged last-wins, so setting them once
|
|
32
32
|
* near the root applies them to every page's title and URLs.
|
|
33
|
+
*
|
|
34
|
+
* Every field admits `undefined` as well as being optional. A caller assembles
|
|
35
|
+
* this from props that are themselves optional, and the merge in `applyConfig`
|
|
36
|
+
* reads each field with an `!== undefined` test, so an absent key and a present
|
|
37
|
+
* `undefined` have always meant the same thing here. Spelling that out is what
|
|
38
|
+
* lets the object be built in one expression under `exactOptionalPropertyTypes`,
|
|
39
|
+
* which matters because this package publishes source and the consumer's
|
|
40
|
+
* compiler options are the ones that compile it.
|
|
33
41
|
*/
|
|
34
42
|
export interface SeoConfig {
|
|
35
43
|
/** Origin used to absolute-ise canonical, og:url, and image URLs. */
|
|
36
|
-
site?: string;
|
|
44
|
+
site?: string | undefined;
|
|
37
45
|
/** `%s` is replaced by the page title, e.g. `'%s · Acme'`. */
|
|
38
|
-
titleTemplate?: string;
|
|
46
|
+
titleTemplate?: string | undefined;
|
|
39
47
|
/**
|
|
40
48
|
* Whether an `openGraph`/`twitter` block was declared. Opting in is about
|
|
41
49
|
* having asked for the family, not about which tags that produced:
|
|
42
50
|
* `openGraph: { publishedTime }` emits only `article:published_time`, which no
|
|
43
51
|
* `og:` prefix scan would recognise.
|
|
44
52
|
*/
|
|
45
|
-
declaredOpenGraph?: boolean;
|
|
46
|
-
declaredTwitter?: boolean;
|
|
53
|
+
declaredOpenGraph?: boolean | undefined;
|
|
54
|
+
declaredTwitter?: boolean | undefined;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
package/src/expand.ts
CHANGED
|
@@ -148,7 +148,7 @@ export function expandSeo(input: SeoInput): SeoDescriptor[] {
|
|
|
148
148
|
// merge, so one place decides which origin applies.
|
|
149
149
|
const images = normalizeImages(og.images);
|
|
150
150
|
for (let i = 0; i < images.length; i++) {
|
|
151
|
-
const image = images[i]
|
|
151
|
+
const image = images[i]!;
|
|
152
152
|
const url = image.url;
|
|
153
153
|
// Repeated og:image tags are legitimate, so each gets its own identity.
|
|
154
154
|
out.push({
|
package/src/registry.ts
CHANGED
|
@@ -38,8 +38,8 @@ export interface SeoRegistry {
|
|
|
38
38
|
function sameDescriptors(a: readonly SeoDescriptor[], b: readonly SeoDescriptor[]): boolean {
|
|
39
39
|
if (a.length !== b.length) return false;
|
|
40
40
|
for (let i = 0; i < a.length; i++) {
|
|
41
|
-
const x = a[i]
|
|
42
|
-
const y = b[i]
|
|
41
|
+
const x = a[i]!;
|
|
42
|
+
const y = b[i]!;
|
|
43
43
|
if (x.key !== y.key || x.tag !== y.tag || x.text !== y.text) return false;
|
|
44
44
|
const xa = x.attrs;
|
|
45
45
|
const ya = y.attrs;
|
|
@@ -16,27 +16,42 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { useEffect } from 'octane';
|
|
18
18
|
|
|
19
|
+
// This package publishes source, so consumers need no Node types. Bundlers
|
|
20
|
+
// replace the direct `process.env.NODE_ENV` read in both development and
|
|
21
|
+
// production browser builds. An unbundled browser host without `process`
|
|
22
|
+
// cannot identify its mode, so the catch below skips the diagnostic safely.
|
|
23
|
+
declare const process: { readonly env: { readonly NODE_ENV?: string } };
|
|
24
|
+
|
|
19
25
|
let liveOwners = 0;
|
|
20
26
|
let reported = false;
|
|
21
27
|
|
|
22
28
|
export function useStrayOwnerDiagnostic(owns: boolean): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
let checkedEnvironment = false;
|
|
30
|
+
try {
|
|
31
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
32
|
+
checkedEnvironment = true;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!owns) return;
|
|
35
|
+
liveOwners++;
|
|
36
|
+
if (liveOwners > 1 && !reported) {
|
|
37
|
+
reported = true;
|
|
38
|
+
console.error(
|
|
39
|
+
'[@octanejs/seo] Two <Head> elements are mounted with neither containing the ' +
|
|
40
|
+
'other, so each emits its own merged set: the document will carry duplicate ' +
|
|
41
|
+
'tags and the FIRST in document order will win, which makes the other ' +
|
|
42
|
+
"component's metadata silently ineffective. Wrap the app in a single " +
|
|
43
|
+
'<Head> so every other <Head> block merges into it.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return () => {
|
|
47
|
+
liveOwners--;
|
|
48
|
+
if (liveOwners <= 1) reported = false;
|
|
49
|
+
};
|
|
50
|
+
});
|
|
36
51
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Only an unresolved environment probe may fail closed. A hook error
|
|
54
|
+
// must still reach its caller, including in a development browser.
|
|
55
|
+
if (checkedEnvironment || typeof process !== 'undefined') throw error;
|
|
56
|
+
}
|
|
42
57
|
}
|