@treely/strapi-slices 7.10.0 → 7.11.0

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@treely/strapi-slices",
3
- "version": "7.10.0",
3
+ "version": "7.11.0",
4
4
  "license": "MIT",
5
5
  "author": "Tree.ly FlexCo",
6
6
  "description": "@treely/strapi-slices is a open source library maintained by Tree.ly.",
@@ -153,6 +153,7 @@
153
153
  "formik": "^2.4.5",
154
154
  "framer-motion": "^10.16.5",
155
155
  "mapbox-gl": "^2.15.0",
156
+ "schema-dts": "^1.1.5",
156
157
  "swr": "^2.3.2",
157
158
  "yocto-queue": "^1.2.0"
158
159
  },
@@ -4,6 +4,96 @@ import {
4
4
  DEFAULT_SHARE_ALT,
5
5
  DEFAULT_SHARE_IMAGE,
6
6
  } from '../../constants/metadata';
7
+ import {
8
+ Article,
9
+ BlogPosting,
10
+ Brand,
11
+ BreadcrumbList,
12
+ Event,
13
+ FAQPage,
14
+ HowTo,
15
+ LocalBusiness,
16
+ Offer,
17
+ Organization,
18
+ Person,
19
+ Product,
20
+ Service,
21
+ WebPage,
22
+ WithContext,
23
+ } from 'schema-dts';
24
+
25
+ type SupportedSchemaType =
26
+ | Article
27
+ | BlogPosting
28
+ | Brand
29
+ | BreadcrumbList
30
+ | Event
31
+ | FAQPage
32
+ | HowTo
33
+ | LocalBusiness
34
+ | Offer
35
+ | Organization
36
+ | Person
37
+ | Product
38
+ | Service
39
+ | WebPage;
40
+
41
+ // Helper function to convert SchemaValue to string
42
+ const getTextValue = (value: unknown): string => {
43
+ if (typeof value === 'string') return value;
44
+ if (typeof value === 'object' && value !== null && 'text' in value) {
45
+ return (value as { text: string }).text;
46
+ }
47
+ return '';
48
+ };
49
+
50
+ // Helper function to safely access properties from a schema
51
+ const getSchemaProperty = (
52
+ schema: SupportedSchemaType,
53
+ property: string
54
+ ): string => {
55
+ return (
56
+ getTextValue((schema as unknown as Record<string, unknown>)[property]) || ''
57
+ );
58
+ };
59
+
60
+ // Helper function to get a unique identifier from a schema
61
+ const getSchemaIdentifier = (schema: SupportedSchemaType): string => {
62
+ const type = (schema as { '@type': string })['@type'];
63
+
64
+ switch (type) {
65
+ case 'Organization':
66
+ return getSchemaProperty(schema, 'name') || 'default';
67
+ case 'Article':
68
+ case 'BlogPosting':
69
+ return getSchemaProperty(schema, 'headline') || 'untitled-article';
70
+ case 'Product':
71
+ return getSchemaProperty(schema, 'name') || 'untitled-product';
72
+ case 'Person':
73
+ return getSchemaProperty(schema, 'name') || 'unnamed-person';
74
+ case 'Event':
75
+ return getSchemaProperty(schema, 'name') || 'untitled-event';
76
+ case 'LocalBusiness':
77
+ return getSchemaProperty(schema, 'name') || 'unnamed-business';
78
+ case 'Service':
79
+ return getSchemaProperty(schema, 'name') || 'unnamed-service';
80
+ case 'Brand':
81
+ return getSchemaProperty(schema, 'name') || 'unnamed-brand';
82
+ case 'FAQPage':
83
+ return 'faq-page';
84
+ case 'HowTo':
85
+ return getSchemaProperty(schema, 'name') || 'untitled-howto';
86
+ case 'BreadcrumbList':
87
+ return 'breadcrumbs';
88
+ case 'Offer':
89
+ const offer = schema as Offer;
90
+ return `offer-${offer.price ?? 'unknown-price'}`;
91
+ case 'WebPage':
92
+ return getSchemaProperty(schema, 'name') || 'untitled-page';
93
+ default:
94
+ return 'unknown-schema';
95
+ }
96
+ };
7
97
 
8
98
  interface SEOTagsProps {
9
99
  title: string;
@@ -15,8 +105,57 @@ interface SEOTagsProps {
15
105
  metaTitleSuffix?: string;
16
106
  favicon?: string;
17
107
  domain?: string;
108
+ /**
109
+ * Structured data for SEO purposes, following the schema.org standard.
110
+ * This can be a single schema object or an array of schema objects.
111
+ * Each object must include an `@context` property set to "https://schema.org"
112
+ * and an `@type` property indicating the type of schema (e.g., Article, Product).
113
+ * The schema falls back to the default schema if it is invalid.
114
+ */
115
+ schemaMarkup?:
116
+ | WithContext<SupportedSchemaType>
117
+ | WithContext<SupportedSchemaType>[];
18
118
  }
19
119
 
120
+ const validateSchema = (
121
+ schema: WithContext<SupportedSchemaType> | WithContext<SupportedSchemaType>[]
122
+ ): boolean => {
123
+ if (Array.isArray(schema)) {
124
+ return schema.every(
125
+ (item) => item['@context'] === 'https://schema.org' && '@type' in item
126
+ );
127
+ }
128
+ return schema['@context'] === 'https://schema.org' && '@type' in schema;
129
+ };
130
+
131
+ const DEFAULT_SCHEMA: WithContext<Organization> = {
132
+ '@context': 'https://schema.org',
133
+ '@type': 'Organization',
134
+ name: 'Tree.ly',
135
+ url: 'https://tree.ly',
136
+ logo: 'https://cdn.tree.ly/logo.png',
137
+ address: {
138
+ '@type': 'PostalAddress',
139
+ streetAddress: 'Littengasse 2b/c',
140
+ addressLocality: 'Dornbirn',
141
+ postalCode: '6850',
142
+ addressRegion: 'Vorarlberg',
143
+ addressCountry: 'AT',
144
+ },
145
+ contactPoint: {
146
+ '@type': 'ContactPoint',
147
+ telephone: '+43-5572-432015',
148
+ contactType: 'Customer Service',
149
+ areaServed: 'AT',
150
+ availableLanguage: ['English', 'German'],
151
+ },
152
+ sameAs: [
153
+ 'https://www.linkedin.com/company/tree-ly',
154
+ 'https://www.facebook.com/treely',
155
+ 'https://www.instagram.com/treely',
156
+ ],
157
+ };
158
+
20
159
  export const SEOTags: React.FC<SEOTagsProps> = ({
21
160
  title,
22
161
  description,
@@ -24,10 +163,35 @@ export const SEOTags: React.FC<SEOTagsProps> = ({
24
163
  metaTitleSuffix = 'Tree.ly',
25
164
  favicon = 'https://cdn.tree.ly/favicon.ico',
26
165
  domain = 'tree.ly',
166
+ schemaMarkup,
27
167
  }: SEOTagsProps) => {
28
168
  const shareImageUrl = shareImage?.url ?? DEFAULT_SHARE_IMAGE;
29
169
  const shareImageAlt = shareImage?.alt ?? DEFAULT_SHARE_ALT;
30
170
 
171
+ let schemas = schemaMarkup || DEFAULT_SCHEMA;
172
+ let isValidSchema = validateSchema(schemas);
173
+
174
+ if (schemaMarkup && !isValidSchema) {
175
+ console.warn(
176
+ 'Invalid schema markup provided to SEOTags component. Falling back to default schema.',
177
+ schemaMarkup
178
+ );
179
+ schemas = DEFAULT_SCHEMA;
180
+ isValidSchema = true;
181
+ }
182
+
183
+ const schemaArray = Array.isArray(schemas) ? schemas : [schemas];
184
+
185
+ const getSchemaKey = (
186
+ schema: WithContext<SupportedSchemaType>,
187
+ index: number
188
+ ): string => {
189
+ const type = (schema as { '@type': string })['@type'];
190
+ const identifier = getSchemaIdentifier(schema as SupportedSchemaType);
191
+ // Add index to ensure uniqueness, especially for fallback identifiers
192
+ return `${type}-${identifier}-${index}`;
193
+ };
194
+
31
195
  return (
32
196
  <Head>
33
197
  <title>{`${title} - ${metaTitleSuffix}`}</title>
@@ -48,6 +212,17 @@ export const SEOTags: React.FC<SEOTagsProps> = ({
48
212
  <meta name="twitter:description" content={description} />
49
213
  <meta name="twitter:image" content={shareImageUrl} />
50
214
  <meta name="twitter:image:alt" content={shareImageAlt} />
215
+
216
+ {isValidSchema &&
217
+ schemaArray.map((schema, index) => (
218
+ <script
219
+ key={getSchemaKey(schema, index)}
220
+ type="application/ld+json"
221
+ dangerouslySetInnerHTML={{
222
+ __html: JSON.stringify(schema),
223
+ }}
224
+ />
225
+ ))}
51
226
  </Head>
52
227
  );
53
228
  };