@lexingtonthemes/seo 0.1.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.
@@ -0,0 +1,457 @@
1
+ import { escape } from "html-escaper";
2
+ import type { AstroSeoProps, OpenGraphMedia } from "../types";
3
+
4
+ const createMetaTag = (attributes: Record<string, string>): string => {
5
+ const attrs = Object.entries(attributes)
6
+ .map(([key, value]) => `${key}="${escape(value)}"`)
7
+ .join(" ");
8
+ return `<meta ${attrs}>`;
9
+ };
10
+
11
+ const createLinkTag = (attributes: Record<string, string>): string => {
12
+ const attrs = Object.entries(attributes)
13
+ .map(([key, value]) => `${key}="${escape(value)}"`)
14
+ .join(" ");
15
+ return `<link ${attrs}>`;
16
+ };
17
+
18
+ const createOpenGraphTag = (property: string, content: string): string => {
19
+ return createMetaTag({ property: `og:${property}`, content });
20
+ };
21
+
22
+ const buildOpenGraphMediaTags = (
23
+ mediaType: "image" | "video",
24
+ media: ReadonlyArray<OpenGraphMedia>
25
+ ): string => {
26
+ let tags = "";
27
+
28
+ const addTag = (tag: string) => {
29
+ tags += tag + "\n";
30
+ };
31
+
32
+ media.forEach((medium) => {
33
+ addTag(createOpenGraphTag(mediaType, medium.url));
34
+
35
+ if (medium.alt) {
36
+ addTag(createOpenGraphTag(`${mediaType}:alt`, medium.alt));
37
+ }
38
+
39
+ if (medium.secureUrl) {
40
+ addTag(createOpenGraphTag(`${mediaType}:secure_url`, medium.secureUrl));
41
+ }
42
+
43
+ if (medium.type) {
44
+ addTag(createOpenGraphTag(`${mediaType}:type`, medium.type));
45
+ }
46
+
47
+ if (medium.width) {
48
+ addTag(createOpenGraphTag(`${mediaType}:width`, medium.width.toString()));
49
+ }
50
+
51
+ if (medium.height) {
52
+ addTag(
53
+ createOpenGraphTag(`${mediaType}:height`, medium.height.toString())
54
+ );
55
+ }
56
+ });
57
+ return tags;
58
+ };
59
+
60
+ export const buildTags = (config: AstroSeoProps): string => {
61
+ let tagsToRender = "";
62
+
63
+ const addTag = (tag: string) => {
64
+ tagsToRender += tag + "\n";
65
+ };
66
+
67
+ const addMetaTag = (attributes: Record<string, string>) => {
68
+ addTag(
69
+ `<meta ${Object.entries(attributes)
70
+ .map(([key, value]) => `${key}="${escape(value)}"`)
71
+ .join(" ")} />`
72
+ );
73
+ };
74
+
75
+ const addLinkTag = (attributes: Record<string, string>) => {
76
+ addTag(
77
+ `<link ${Object.entries(attributes)
78
+ .map(([key, value]) => `${key}="${escape(value)}"`)
79
+ .join(" ")} />`
80
+ );
81
+ };
82
+
83
+ const addOpenGraphTag = (property: string, content: string) => {
84
+ addMetaTag({ property: `og:${property}`, content });
85
+ };
86
+
87
+ // Title
88
+ if (config.title) {
89
+ const formattedTitle = config.titleTemplate
90
+ ? config.titleTemplate.replace("%s", config.title)
91
+ : config.title;
92
+ addTag(`<title>${escape(formattedTitle)}</title>`);
93
+ }
94
+
95
+ // Description
96
+ if (config.description) {
97
+ addTag(createMetaTag({ name: "description", content: config.description }));
98
+ }
99
+
100
+ // Robots: noindex, nofollow, and other robotsProps
101
+ let robotsContent: string[] = [];
102
+ if (typeof config.noindex !== "undefined") {
103
+ robotsContent.push(config.noindex ? "noindex" : "index");
104
+ }
105
+
106
+ if (typeof config.nofollow !== "undefined") {
107
+ robotsContent.push(config.nofollow ? "nofollow" : "follow");
108
+ }
109
+
110
+ if (config.robotsProps) {
111
+ const {
112
+ nosnippet,
113
+ maxSnippet,
114
+ maxImagePreview,
115
+ noarchive,
116
+ unavailableAfter,
117
+ noimageindex,
118
+ notranslate,
119
+ } = config.robotsProps;
120
+
121
+ if (nosnippet) robotsContent.push("nosnippet");
122
+ if (typeof maxSnippet === 'number') robotsContent.push(`max-snippet:${maxSnippet}`);
123
+ if (maxImagePreview)
124
+ robotsContent.push(`max-image-preview:${maxImagePreview}`);
125
+ if (noarchive) robotsContent.push("noarchive");
126
+ if (unavailableAfter)
127
+ robotsContent.push(`unavailable_after:${unavailableAfter}`);
128
+ if (noimageindex) robotsContent.push("noimageindex");
129
+ if (notranslate) robotsContent.push("notranslate");
130
+ }
131
+
132
+ if (robotsContent.length > 0) {
133
+ addTag(createMetaTag({ name: "robots", content: robotsContent.join(",") }));
134
+ }
135
+
136
+ // Canonical
137
+ if (config.canonical) {
138
+ addTag(createLinkTag({ rel: "canonical", href: config.canonical }));
139
+ }
140
+
141
+ // Mobile Alternate
142
+ if (config.mobileAlternate) {
143
+ addTag(
144
+ createLinkTag({
145
+ rel: "alternate",
146
+ media: config.mobileAlternate.media,
147
+ href: config.mobileAlternate.href,
148
+ })
149
+ );
150
+ }
151
+
152
+ // Language Alternates
153
+ if (config.languageAlternates && config.languageAlternates.length > 0) {
154
+ config.languageAlternates.forEach((languageAlternate) => {
155
+ addTag(
156
+ createLinkTag({
157
+ rel: "alternate",
158
+ hreflang: languageAlternate.hreflang,
159
+ href: languageAlternate.href,
160
+ })
161
+ );
162
+ });
163
+ }
164
+
165
+ // OpenGraph
166
+ if (config.openGraph) {
167
+ const title = config.openGraph?.title || config.title;
168
+ if (title) {
169
+ addTag(createOpenGraphTag("title", title));
170
+ }
171
+
172
+ const description = config.openGraph?.description || config.description;
173
+ if (description) {
174
+ addTag(createOpenGraphTag("description", description));
175
+ }
176
+
177
+ if (config.openGraph.url) {
178
+ addTag(createOpenGraphTag("url", config.openGraph.url));
179
+ }
180
+
181
+ if (config.openGraph.type) {
182
+ addTag(createOpenGraphTag("type", config.openGraph.type));
183
+ }
184
+
185
+ if (config.openGraph.images && config.openGraph.images.length) {
186
+ addTag(buildOpenGraphMediaTags("image", config.openGraph.images));
187
+ }
188
+
189
+ if (config.openGraph.videos && config.openGraph.videos.length) {
190
+ addTag(buildOpenGraphMediaTags("video", config.openGraph.videos));
191
+ }
192
+
193
+ if (config.openGraph.locale) {
194
+ addTag(createOpenGraphTag("locale", config.openGraph.locale));
195
+ }
196
+
197
+ if (config.openGraph.site_name) {
198
+ addTag(createOpenGraphTag("site_name", config.openGraph.site_name));
199
+ }
200
+
201
+ // Open Graph Profile
202
+ if (config.openGraph.profile) {
203
+ if (config.openGraph.profile.firstName) {
204
+ addTag(
205
+ createOpenGraphTag(
206
+ "profile:first_name",
207
+ config.openGraph.profile.firstName
208
+ )
209
+ );
210
+ }
211
+ if (config.openGraph.profile.lastName) {
212
+ addTag(
213
+ createOpenGraphTag(
214
+ "profile:last_name",
215
+ config.openGraph.profile.lastName
216
+ )
217
+ );
218
+ }
219
+ if (config.openGraph.profile.username) {
220
+ addTag(
221
+ createOpenGraphTag(
222
+ "profile:username",
223
+ config.openGraph.profile.username
224
+ )
225
+ );
226
+ }
227
+ if (config.openGraph.profile.gender) {
228
+ addTag(
229
+ createOpenGraphTag("profile:gender", config.openGraph.profile.gender)
230
+ );
231
+ }
232
+ }
233
+
234
+ // Open Graph Book
235
+ if (config.openGraph.book) {
236
+ if (
237
+ config.openGraph.book.authors &&
238
+ config.openGraph.book.authors.length
239
+ ) {
240
+ config.openGraph.book.authors.forEach((author) => {
241
+ addTag(createOpenGraphTag("book:author", author));
242
+ });
243
+ }
244
+ if (config.openGraph.book.isbn) {
245
+ addTag(createOpenGraphTag("book:isbn", config.openGraph.book.isbn));
246
+ }
247
+ if (config.openGraph.book.releaseDate) {
248
+ addTag(
249
+ createOpenGraphTag(
250
+ "book:release_date",
251
+ config.openGraph.book.releaseDate
252
+ )
253
+ );
254
+ }
255
+ if (config.openGraph.book.tags && config.openGraph.book.tags.length) {
256
+ config.openGraph.book.tags.forEach((tag) => {
257
+ addTag(createOpenGraphTag("book:tag", tag));
258
+ });
259
+ }
260
+ }
261
+
262
+ // Open Graph Article
263
+ if (config.openGraph.article) {
264
+ if (config.openGraph.article.publishedTime) {
265
+ addTag(
266
+ createOpenGraphTag(
267
+ "article:published_time",
268
+ config.openGraph.article.publishedTime
269
+ )
270
+ );
271
+ }
272
+ if (config.openGraph.article.modifiedTime) {
273
+ addTag(
274
+ createOpenGraphTag(
275
+ "article:modified_time",
276
+ config.openGraph.article.modifiedTime
277
+ )
278
+ );
279
+ }
280
+ if (config.openGraph.article.expirationTime) {
281
+ addTag(
282
+ createOpenGraphTag(
283
+ "article:expiration_time",
284
+ config.openGraph.article.expirationTime
285
+ )
286
+ );
287
+ }
288
+ if (
289
+ config.openGraph.article.authors &&
290
+ config.openGraph.article.authors.length
291
+ ) {
292
+ config.openGraph.article.authors.forEach((author) => {
293
+ addTag(createOpenGraphTag("article:author", author));
294
+ });
295
+ }
296
+ if (config.openGraph.article.section) {
297
+ addTag(
298
+ createOpenGraphTag(
299
+ "article:section",
300
+ config.openGraph.article.section
301
+ )
302
+ );
303
+ }
304
+ if (
305
+ config.openGraph.article.tags &&
306
+ config.openGraph.article.tags.length
307
+ ) {
308
+ config.openGraph.article.tags.forEach((tag) => {
309
+ addTag(createOpenGraphTag("article:tag", tag));
310
+ });
311
+ }
312
+ }
313
+
314
+ // Open Graph Video
315
+ if (config.openGraph.video) {
316
+ if (
317
+ config.openGraph.video.actors &&
318
+ config.openGraph.video.actors.length
319
+ ) {
320
+ config.openGraph.video.actors.forEach((actor) => {
321
+ addTag(createOpenGraphTag("video:actor", actor.profile));
322
+ if (actor.role) {
323
+ addTag(createOpenGraphTag("video:actor:role", actor.role));
324
+ }
325
+ });
326
+ }
327
+ if (
328
+ config.openGraph.video.directors &&
329
+ config.openGraph.video.directors.length
330
+ ) {
331
+ config.openGraph.video.directors.forEach((director) => {
332
+ addTag(createOpenGraphTag("video:director", director));
333
+ });
334
+ }
335
+ if (
336
+ config.openGraph.video.writers &&
337
+ config.openGraph.video.writers.length
338
+ ) {
339
+ config.openGraph.video.writers.forEach((writer) => {
340
+ addTag(createOpenGraphTag("video:writer", writer));
341
+ });
342
+ }
343
+ if (config.openGraph.video.duration) {
344
+ addTag(
345
+ createOpenGraphTag(
346
+ "video:duration",
347
+ config.openGraph.video.duration.toString()
348
+ )
349
+ );
350
+ }
351
+ if (config.openGraph.video.releaseDate) {
352
+ addTag(
353
+ createOpenGraphTag(
354
+ "video:release_date",
355
+ config.openGraph.video.releaseDate
356
+ )
357
+ );
358
+ }
359
+ if (config.openGraph.video.tags && config.openGraph.video.tags.length) {
360
+ config.openGraph.video.tags.forEach((tag) => {
361
+ addTag(createOpenGraphTag("video:tag", tag));
362
+ });
363
+ }
364
+ if (config.openGraph.video.series) {
365
+ addTag(
366
+ createOpenGraphTag("video:series", config.openGraph.video.series)
367
+ );
368
+ }
369
+ }
370
+ }
371
+
372
+ // Facebook
373
+ if (config.facebook && config.facebook.appId) {
374
+ addTag(
375
+ createMetaTag({ property: "fb:app_id", content: config.facebook.appId })
376
+ );
377
+ }
378
+
379
+ // Twitter
380
+ if (config.twitter) {
381
+ if (config.twitter.cardType) {
382
+ addTag(
383
+ createMetaTag({
384
+ name: "twitter:card",
385
+ content: config.twitter.cardType,
386
+ })
387
+ );
388
+ }
389
+
390
+ if (config.twitter.site) {
391
+ addTag(
392
+ createMetaTag({ name: "twitter:site", content: config.twitter.site })
393
+ );
394
+ }
395
+
396
+ if (config.twitter.handle) {
397
+ addTag(
398
+ createMetaTag({
399
+ name: "twitter:creator",
400
+ content: config.twitter.handle,
401
+ })
402
+ );
403
+ }
404
+ }
405
+
406
+ // Additional Meta Tags
407
+ if (config.additionalMetaTags && config.additionalMetaTags.length > 0) {
408
+ config.additionalMetaTags.forEach((metaTag) => {
409
+ const attributes: Record<string, string> = {
410
+ content: metaTag.content,
411
+ };
412
+
413
+ if ("name" in metaTag && metaTag.name) {
414
+ attributes.name = metaTag.name;
415
+ } else if ("property" in metaTag && metaTag.property) {
416
+ attributes.property = metaTag.property;
417
+ } else if ("httpEquiv" in metaTag && metaTag.httpEquiv) {
418
+ attributes["http-equiv"] = metaTag.httpEquiv;
419
+ }
420
+
421
+ addTag(createMetaTag(attributes));
422
+ });
423
+ }
424
+
425
+ // Additional Link Tags
426
+ if (config.additionalLinkTags && config.additionalLinkTags.length > 0) {
427
+ config.additionalLinkTags.forEach((linkTag) => {
428
+ const attributes: Record<string, string> = {
429
+ rel: linkTag.rel,
430
+ href: linkTag.href,
431
+ };
432
+
433
+ if (linkTag.sizes) {
434
+ attributes.sizes = linkTag.sizes;
435
+ }
436
+ if (linkTag.media) {
437
+ attributes.media = linkTag.media;
438
+ }
439
+ if (linkTag.type) {
440
+ attributes.type = linkTag.type;
441
+ }
442
+ if (linkTag.color) {
443
+ attributes.color = linkTag.color;
444
+ }
445
+ if (linkTag.as) {
446
+ attributes.as = linkTag.as;
447
+ }
448
+ if (linkTag.crossOrigin) {
449
+ attributes.crossorigin = linkTag.crossOrigin;
450
+ }
451
+
452
+ addTag(createLinkTag(attributes));
453
+ });
454
+ }
455
+
456
+ return tagsToRender.trim();
457
+ };