@webround/core 0.3.8 → 0.3.10

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/dist/seo-react.js CHANGED
@@ -1,8 +1,23 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useSyncExternalStore } from 'react';
2
+ import { useEffect, useRef, useSyncExternalStore } from 'react';
3
3
  import { wrSeoPageOf, wrSeoTags } from './seo.js';
4
4
  import { wrBuildLangPath } from './routing.js';
5
5
  const PROP = { hreflang: 'hrefLang' };
6
+ // What marks a tag as this component's: the `<title>` the placeholder of `index.html` is told from, and the tags a snapshot was saved with
7
+ const SEO_MARK = 'data-wr-seo';
8
+ // What a tag is, apart from what it says: the title, a `<meta>` by its name or property, a `<link>` by its `rel` (and language). Structured data has no name: it is told by what it says (null)
9
+ function tagKey(tag) {
10
+ const name = tag.tagName.toLowerCase();
11
+ if (name === 'title')
12
+ return 'title';
13
+ if (name === 'meta') {
14
+ const key = tag.getAttribute('name') ?? tag.getAttribute('property');
15
+ return key ? `meta:${key}` : null;
16
+ }
17
+ if (name === 'link')
18
+ return `link:${tag.getAttribute('rel')}:${tag.getAttribute('hreflang') ?? ''}`;
19
+ return null;
20
+ }
6
21
  // What a page whose texts come from data (a product) says about itself, on top of what its JSON says. One page is shown at a time, so it is one value: the page sets it while it is shown, `WrSeoHead` reads it.
7
22
  let dynamicSeo;
8
23
  const listeners = new Set();
@@ -28,10 +43,29 @@ export function WrSeoHead({ site, config, pathname, lang, langs }) {
28
43
  const path = langs ? wrBuildLangPath('default', pathname, langs) : pathname;
29
44
  const dynamic = useSyncExternalStore(subscribe, () => dynamicSeo, () => undefined);
30
45
  const { title, tags, jsonLd } = wrSeoTags({ site, page: { ...wrSeoPageOf(config), ...dynamic }, path, lang, langs });
31
- return (_jsxs(_Fragment, { children: [title && _jsx("title", { children: title }), tags.map(({ tag, attrs }) => {
46
+ // The `<title>` that `index.html` has (a placeholder, the page app's own) is not the page's: when the page has one of its own, the head must have only that, as the HTML a crawler gets says it
47
+ useEffect(() => {
48
+ if (title)
49
+ document.head.querySelectorAll('title:not([data-wr-seo])').forEach((other) => other.remove());
50
+ }, [title]);
51
+ // The page came with these tags in its head (they carry `data-wr-seo`): a snapshot saved them, and the Worker adds a product's. The app draws its own when it starts, and the ones it draws are the ones it
52
+ // keeps up to date, so a tag that came with the page goes as soon as the app draws the one with the same name. Until then (a product's data has not come) it stays: it is the best the page has said.
53
+ const saved = useRef(null);
54
+ saved.current ?? (saved.current = typeof document === 'undefined' ? [] : [...document.head.querySelectorAll(`[${SEO_MARK}]`)]);
55
+ const drawn = JSON.stringify({ title, tags, jsonLd });
56
+ useEffect(() => {
57
+ const mine = [...document.head.querySelectorAll(`[${SEO_MARK}]`)].filter((tag) => !saved.current?.includes(tag));
58
+ for (const tag of saved.current ?? []) {
59
+ const key = tagKey(tag);
60
+ if (mine.some((other) => (key ? tagKey(other) === key : other.isEqualNode(tag))))
61
+ tag.remove();
62
+ }
63
+ saved.current = saved.current?.filter((tag) => tag.isConnected) ?? [];
64
+ }, [drawn]);
65
+ return (_jsxs(_Fragment, { children: [title && _jsx("title", { [SEO_MARK]: '', children: title }), tags.map(({ tag, attrs }) => {
32
66
  const props = Object.fromEntries(Object.entries(attrs).map(([key, value]) => [PROP[key] ?? key, value]));
33
67
  const key = `${tag}:${attrs.name ?? attrs.property ?? `${attrs.rel}:${attrs.hreflang ?? ''}`}`;
34
- return tag === 'meta' ? _jsx("meta", { ...props }, key) : _jsx("link", { ...props }, key);
35
- }), jsonLd.map((body, index) => _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: body } }, `ld:${index}`))] }));
68
+ return tag === 'meta' ? _jsx("meta", { [SEO_MARK]: '', ...props }, key) : _jsx("link", { [SEO_MARK]: '', ...props }, key);
69
+ }), jsonLd.map((body, index) => _jsx("script", { type: "application/ld+json", [SEO_MARK]: '', dangerouslySetInnerHTML: { __html: body } }, `ld:${index}`))] }));
36
70
  }
37
71
  //# sourceMappingURL=seo-react.js.map
package/dist/seo.d.ts CHANGED
@@ -139,8 +139,10 @@ export interface WrSnapshotTarget {
139
139
  file: string;
140
140
  }
141
141
  /**
142
- * Where the prerendered HTML of an address goes in `dist/`: the home of a language is `<lang>/index.html` (the default language has no prefix:
143
- * its home is `index.html`), any other address is `<address>.html` (`/en/about` → `en/about.html`, `/about` → `about.html`).
142
+ * Where the prerendered HTML of an address goes in `dist/`, as the worker that serves the sites looks for it (the same layout the v1 sites have): a folder for the language and, in it, the home as
143
+ * `index.html` and any other page as `<page>.html`: `/en` → `en/index.html`, `/en/about` → `en/about.html`. The default language has no prefix in the address and its folder is `default`:
144
+ * `/` → `default/index.html`, `/about` → `default/about.html`. The build's own `index.html` (the page app, with no page in it) is never one of these: it is what any other address is served.
145
+ * A page with a longer address keeps it whole (`/en/about/team` → `en/about/team.html`).
144
146
  */
145
147
  export declare function wrSnapshotFile(path: string, langs?: WrLangConfig): string;
146
148
  /**
package/dist/seo.js CHANGED
@@ -159,16 +159,17 @@ export function wrSeoEntries(input) {
159
159
  });
160
160
  }
161
161
  /**
162
- * Where the prerendered HTML of an address goes in `dist/`: the home of a language is `<lang>/index.html` (the default language has no prefix:
163
- * its home is `index.html`), any other address is `<address>.html` (`/en/about` → `en/about.html`, `/about` → `about.html`).
162
+ * Where the prerendered HTML of an address goes in `dist/`, as the worker that serves the sites looks for it (the same layout the v1 sites have): a folder for the language and, in it, the home as
163
+ * `index.html` and any other page as `<page>.html`: `/en` → `en/index.html`, `/en/about` → `en/about.html`. The default language has no prefix in the address and its folder is `default`:
164
+ * `/` → `default/index.html`, `/about` → `default/about.html`. The build's own `index.html` (the page app, with no page in it) is never one of these: it is what any other address is served.
165
+ * A page with a longer address keeps it whole (`/en/about/team` → `en/about/team.html`).
164
166
  */
165
167
  export function wrSnapshotFile(path, langs) {
166
168
  const segments = path.split('/').filter(Boolean);
167
- if (segments.length === 0)
168
- return 'index.html';
169
- if (segments.length === 1 && langs?.available.includes(segments[0]))
170
- return `${segments[0]}/index.html`;
171
- return `${segments.join('/')}.html`;
169
+ const isLanguage = segments.length > 0 && !!langs?.available.includes(segments[0]);
170
+ const folder = isLanguage && segments[0] !== langs?.default ? segments[0] : 'default';
171
+ const page = isLanguage ? segments.slice(1) : segments;
172
+ return page.length === 0 ? `${folder}/index.html` : `${folder}/${page.join('/')}.html`;
172
173
  }
173
174
  /**
174
175
  * The pages a build prerenders: every page with a fixed address, in every language, but those marked noindex (a crawler is told to leave them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webround/core",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",