proteum 2.5.20 → 2.5.22

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.5.20",
4
+ "version": "2.5.22",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -3,26 +3,23 @@
3
3
  ----------------------------------*/
4
4
 
5
5
  /*----------------------------------
6
- - TYPES
6
+ - DEPENDENCIES
7
7
  ----------------------------------*/
8
8
 
9
- type TJsonLdNode = { '@type'?: string | string[]; '@id'?: string; [key: string]: unknown };
9
+ import type { Thing } from 'schema-dts';
10
+ import type { TApplicationIdentityConfig } from '@common/applicationConfig';
10
11
 
11
- type TIdentity = {
12
- name: string;
13
- description: string;
14
- locale: string;
15
- author: { name: string; url: string };
16
- web: { jsonld?: Record<string, unknown> };
17
- };
12
+ /*----------------------------------
13
+ - TYPES
14
+ ----------------------------------*/
18
15
 
19
16
  export type TDefaultJsonLdInput = {
20
17
  /** The nodes the page pushed itself, before the defaults are added. */
21
- pageJsonLd: readonly TJsonLdNode[];
22
- url: string;
23
- title: string;
24
- description: string;
25
- identity: TIdentity;
18
+ pageJsonLd: readonly Thing[];
19
+ url: string | undefined;
20
+ title: string | undefined;
21
+ description: string | undefined;
22
+ identity: TApplicationIdentityConfig;
26
23
  resolveUrl: (path: string) => string;
27
24
  };
28
25
 
@@ -30,14 +27,40 @@ export type TDefaultJsonLdInput = {
30
27
  - HELPERS
31
28
  ----------------------------------*/
32
29
 
33
- const hasType = (node: TJsonLdNode, type: string): boolean =>
34
- Array.isArray(node['@type']) ? node['@type'].includes(type) : node['@type'] === type;
30
+ /**
31
+ * `WebPage` and the schema.org types that specialise it. A page that describes itself as an
32
+ * `AboutPage` or a `CollectionPage` has described its web page: the generic node beside it
33
+ * would be a second page for the same URL, exactly as beside a plain `WebPage`.
34
+ */
35
+ const WEB_PAGE_TYPES: ReadonlySet<string> = new Set([
36
+ 'WebPage',
37
+ 'AboutPage',
38
+ 'CheckoutPage',
39
+ 'CollectionPage',
40
+ 'ContactPage',
41
+ 'FAQPage',
42
+ 'ItemPage',
43
+ 'MedicalWebPage',
44
+ 'ProfilePage',
45
+ 'QAPage',
46
+ 'RealEstateListing',
47
+ 'SearchResultsPage',
48
+ ]);
49
+
50
+ const describesWebPage = (node: Thing): boolean => {
51
+ if (typeof node !== 'object' || node === null || !('@type' in node)) return false;
52
+
53
+ const declared: unknown = node['@type'];
54
+ return (Array.isArray(declared) ? declared : [declared]).some(
55
+ (type) => typeof type === 'string' && WEB_PAGE_TYPES.has(type),
56
+ );
57
+ };
35
58
 
36
59
  /**
37
60
  * The publisher identity every page carries (`#organization`, `#website`) plus a generic
38
61
  * `WebPage` for pages that describe none themselves.
39
62
  *
40
- * A page that already pushed a `WebPage` keeps its own: a second node for the same URL
63
+ * A page that already pushed a `WebPage`, or one of its subtypes, keeps its own: a second node for the same URL
41
64
  * with another name and description reads as two contradictory pages to a consumer.
42
65
  * Empty `sameAs` and `potentialAction` arrays are left out for the same reason: they say
43
66
  * "no profiles" and "no actions" where the app said nothing. `identity.web.jsonld` can
@@ -50,8 +73,8 @@ export const buildDefaultJsonLd = ({
50
73
  description,
51
74
  identity,
52
75
  resolveUrl,
53
- }: TDefaultJsonLdInput): TJsonLdNode[] => {
54
- const nodes: TJsonLdNode[] = [
76
+ }: TDefaultJsonLdInput): Thing[] => {
77
+ const nodes: Thing[] = [
55
78
  {
56
79
  '@type': 'Organization',
57
80
  '@id': resolveUrl('/#organization'),
@@ -78,7 +101,7 @@ export const buildDefaultJsonLd = ({
78
101
  },
79
102
  ];
80
103
 
81
- if (!pageJsonLd.some((node) => hasType(node, 'WebPage'))) {
104
+ if (!pageJsonLd.some(describesWebPage)) {
82
105
  nodes.push({
83
106
  '@type': 'WebPage',
84
107
  '@id': url,
@@ -19,7 +19,7 @@ const input = {
19
19
  };
20
20
 
21
21
  test('default JSON-LD adds a generic WebPage only when the page has none', () => {
22
- const withoutPage = buildDefaultJsonLd({ ...input, pageJsonLd: [{ '@type': 'FAQPage' }] });
22
+ const withoutPage = buildDefaultJsonLd({ ...input, pageJsonLd: [{ '@type': 'BreadcrumbList' }] });
23
23
  assert.deepEqual(withoutPage.map((node) => node['@type']), ['Organization', 'WebSite', 'WebPage']);
24
24
  assert.equal(withoutPage[2]['@id'], 'https://example.test/pricing');
25
25
  assert.equal(withoutPage[2].name, 'Pricing');
@@ -32,6 +32,14 @@ test('default JSON-LD adds a generic WebPage only when the page has none', () =>
32
32
 
33
33
  const withTypedArray = buildDefaultJsonLd({ ...input, pageJsonLd: [{ '@type': ['WebPage', 'FAQPage'] }] });
34
34
  assert.deepEqual(withTypedArray.map((node) => node['@type']), ['Organization', 'WebSite']);
35
+
36
+ // Subtypes of WebPage count: an AboutPage or a CollectionPage has described the page.
37
+ for (const subtype of ['AboutPage', 'CollectionPage', 'FAQPage', 'ProfilePage']) {
38
+ const nodes = buildDefaultJsonLd({ ...input, pageJsonLd: [{ '@type': subtype }] });
39
+ assert.deepEqual(nodes.map((node) => node['@type']), ['Organization', 'WebSite'], subtype);
40
+ }
41
+ const unrelated = buildDefaultJsonLd({ ...input, pageJsonLd: [{ '@type': 'SoftwareApplication' }] });
42
+ assert.equal(unrelated.length, 3);
35
43
  });
36
44
 
37
45
  test('default JSON-LD emits no empty sameAs or potentialAction, and keeps identity additions', () => {