@webround/core 0.3.9 → 0.3.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.
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // packages/core/src/context/WrAppContext.tsx
3
3
  import { createContext, useContext, useState, useCallback, useEffect, useMemo, useSyncExternalStore, lazy, Suspense, } from 'react';
4
- import { createBrowserRouter, RouterProvider, useLocation, useParams } from 'react-router-dom';
4
+ import { createBrowserRouter, matchRoutes, RouterProvider, useLocation, useParams } from 'react-router-dom';
5
5
  import { WrScroll } from '../scroll';
6
- import { wrResolveRoutes, wrBuildRegistry, wrBuildLazyRegistry, wrBuildPageConfigMap, wrExpandRoutesForLangs, wrExtractLang, wrBuildLangPath, } from '../routing';
6
+ import { wrResolveRoutes, wrBuildRegistry, wrBuildLazyRegistry, wrBuildPageConfigMap, wrExpandRoutesForLangs, wrExtractLang, wrBuildLangPath, wrHomeOf, } from '../routing';
7
7
  import { WrPageProvider } from './WrPageContext';
8
8
  import { WrLayout } from './WrLayout';
9
9
  import { WrSharedContext } from '../components/WrShared';
@@ -50,6 +50,24 @@ function WrRouteElement({ route, pageConfigMap, lazyFallback: LazyFallback, Page
50
50
  })();
51
51
  return (_jsxs(PageProvider, { config: config, pageName: pageName, children: [_jsx(WrScroll, {}), _jsxs(WrSharedContext.Provider, { value: shared, children: [_jsx(WrSeoHead, { site: seo, config: config, pathname: pathname, lang: lang, langs: langs }), _jsx(WrLayout, { Header: Header, Footer: Footer, showHeader: route.header, showFooter: route.footer, pageConfigMap: pageConfigMap, ScopeProvider: PageProvider, children: body })] })] }, pageName));
52
52
  }
53
+ /**
54
+ * An address that leads to no page is the home page. The address is replaced BEFORE the router reads it, so the first render is the home page's: what was served for that address is the home page's
55
+ * snapshot, and the app takes it over (hydrates it) only if it draws the same page. A redirect after the first render would draw nothing at first, and React neither takes over nor removes what it did not claim.
56
+ */
57
+ function replaceUnknownAddress(routes, notFound, langs) {
58
+ if (typeof window === 'undefined')
59
+ return;
60
+ const { pathname, search, hash } = window.location;
61
+ const matches = matchRoutes(routes, pathname);
62
+ const match = matches?.[matches.length - 1];
63
+ // `/:lang/…` answers for any word in that place: only the languages of the site are one (see WrRouteElement)
64
+ const unknownLang = !!langs && match?.params.lang !== undefined && !langs.available.includes(match.params.lang);
65
+ if (match?.route !== notFound && !unknownLang)
66
+ return;
67
+ const home = wrHomeOf(pathname, langs);
68
+ if (home !== pathname)
69
+ window.history.replaceState(window.history.state, '', `${home}${search}${hash}`);
70
+ }
53
71
  export function WrAppProvider({ routesFile, eagerModules, lazyModules, configModules, langs, initialLang = 'default', lazyFallback = WrDefaultPageFallback, onRouterReady, PageProvider = WrPageProvider, Header, Footer, seo, children, }) {
54
72
  const router = useMemo(() => {
55
73
  const registry = wrBuildRegistry(eagerModules);
@@ -60,14 +78,17 @@ export function WrAppProvider({ routesFile, eagerModules, lazyModules, configMod
60
78
  console.error('[wr] unresolved route -> component mappings:', errors.missingComponents);
61
79
  }
62
80
  const expandedRoutes = wrExpandRoutesForLangs(routes, langs);
63
- const builtRouter = createBrowserRouter([
81
+ // An address that leads to no page takes the visitor to the home page
82
+ const notFound = { path: '*', element: _jsx(WrNotFound, { langs: langs }) };
83
+ const routeObjects = [
64
84
  ...expandedRoutes.map((route) => ({
65
85
  path: route.pattern,
66
86
  element: _jsx(WrRouteElement, { route: route, pageConfigMap: pageConfigMap, lazyFallback: lazyFallback, PageProvider: PageProvider, Header: Header, Footer: Footer, seo: seo, langs: langs })
67
87
  })),
68
- // An address that leads to no page takes the visitor to the home page
69
- { path: '*', element: _jsx(WrNotFound, { langs: langs }) },
70
- ]);
88
+ notFound,
89
+ ];
90
+ replaceUnknownAddress(routeObjects, notFound, langs);
91
+ const builtRouter = createBrowserRouter(routeObjects);
71
92
  onRouterReady?.(builtRouter.navigate);
72
93
  return builtRouter;
73
94
  }, [routesFile, eagerModules, lazyModules, configModules, langs, lazyFallback, onRouterReady, PageProvider, Header, Footer, seo]);
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();
@@ -33,10 +48,24 @@ export function WrSeoHead({ site, config, pathname, lang, langs }) {
33
48
  if (title)
34
49
  document.head.querySelectorAll('title:not([data-wr-seo])').forEach((other) => other.remove());
35
50
  }, [title]);
36
- return (_jsxs(_Fragment, { children: [title && _jsx("title", { "data-wr-seo": "", children: title }), tags.map(({ tag, attrs }) => {
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 }) => {
37
66
  const props = Object.fromEntries(Object.entries(attrs).map(([key, value]) => [PROP[key] ?? key, value]));
38
67
  const key = `${tag}:${attrs.name ?? attrs.property ?? `${attrs.rel}:${attrs.hreflang ?? ''}`}`;
39
- return tag === 'meta' ? _jsx("meta", { ...props }, key) : _jsx("link", { ...props }, key);
40
- }), 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}`))] }));
41
70
  }
42
71
  //# sourceMappingURL=seo-react.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webround/core",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",