@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/dist/components/SEOTags/SEOTags.d.ts +10 -0
- package/dist/strapi-slices.cjs.development.js +104 -1
- package/dist/strapi-slices.cjs.development.js.map +1 -1
- package/dist/strapi-slices.cjs.production.min.js +1 -1
- package/dist/strapi-slices.cjs.production.min.js.map +1 -1
- package/dist/strapi-slices.esm.js +104 -1
- package/dist/strapi-slices.esm.js.map +1 -1
- package/package.json +2 -1
- package/src/components/SEOTags/SEOTags.tsx +175 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treely/strapi-slices",
|
|
3
|
-
"version": "7.
|
|
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
|
};
|