@devsantara/head 0.2.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/index.js ADDED
@@ -0,0 +1,484 @@
1
+ //#region src/builder.ts
2
+ var HeadBuilder = class {
3
+ metadataBase;
4
+ elements = [];
5
+ adapter;
6
+ /**
7
+ * Resolves a value that can be either static or a callback function receiving helper utilities.
8
+ *
9
+ * @param valueOrFn - Static value or function that returns the value
10
+ * @returns The resolved value
11
+ */
12
+ parseValueOrFn(valueOrFn) {
13
+ if (typeof valueOrFn === "function") return valueOrFn({ resolveUrl: this.resolveUrl.bind(this) });
14
+ return valueOrFn;
15
+ }
16
+ /**
17
+ * Creates a new HeadBuilder instance for constructing HTML head elements with optional base URL resolution and output transformation.
18
+ *
19
+ * @param options - Configuration options
20
+ * @param options.metadataBase - Base URL for resolving relative URLs in metadata (Open Graph, canonical, etc.)
21
+ * @param options.adapter - Adapter to transform output into framework-specific format
22
+ *
23
+ * @example
24
+ * const head = new HeadBuilder({
25
+ * metadataBase: new URL('https://devsantara.com'),
26
+ * adapter: new ReactAdapter()
27
+ * })
28
+ * .addTitle('My Site')
29
+ * .build();
30
+ */
31
+ constructor(options) {
32
+ this.metadataBase = options?.metadataBase;
33
+ this.adapter = options?.adapter;
34
+ }
35
+ /**
36
+ * Resolves a relative or absolute URL into an absolute URL using the configured metadataBase.
37
+ *
38
+ * @param url - The URL to resolve
39
+ * @returns The resolved absolute URL as a string
40
+ */
41
+ resolveUrl(url) {
42
+ if (url instanceof URL) return url.href;
43
+ if (!this.metadataBase) return url;
44
+ try {
45
+ return new URL(url, this.metadataBase).href;
46
+ } catch {
47
+ return url;
48
+ }
49
+ }
50
+ /**
51
+ * Adds a head element to the internal collection for later transformation.
52
+ *
53
+ * @param type - The HTML element type
54
+ * @param attributes - The element's attributes
55
+ * @returns The builder instance for method chaining
56
+ */
57
+ addElement(type, attributes) {
58
+ this.elements.push({
59
+ type,
60
+ attributes
61
+ });
62
+ return this;
63
+ }
64
+ /**
65
+ * Adds a custom meta element with any valid attributes. Use this for meta tags without dedicated helper methods.
66
+ *
67
+ * @param attributes - The meta element attributes
68
+ * @returns The builder instance for method chaining
69
+ *
70
+ * @example
71
+ * new HeadBuilder()
72
+ * .addMeta({ name: 'theme-color', content: '#ffffff' })
73
+ * .build();
74
+ */
75
+ addMeta(attributes) {
76
+ return this.addElement("meta", attributes);
77
+ }
78
+ /**
79
+ * Adds a custom link element with any valid attributes. Use this for link tags without dedicated helper methods.
80
+ *
81
+ * @param attributes - The link element attributes
82
+ * @returns The builder instance for method chaining
83
+ *
84
+ * @example
85
+ * new HeadBuilder()
86
+ * .addLink({ rel: 'preconnect', href: 'https://fonts.googleapis.com' })
87
+ * .build();
88
+ */
89
+ addLink(attributes) {
90
+ return this.addElement("link", attributes);
91
+ }
92
+ /**
93
+ * Adds a custom script element with any valid attributes for external scripts or inline code.
94
+ *
95
+ * @param attributes - The script element attributes
96
+ * @returns The builder instance for method chaining
97
+ *
98
+ * @example
99
+ * new HeadBuilder()
100
+ * .addScript({ src: '/analytics.js', async: true })
101
+ * .build();
102
+ */
103
+ addScript(attributes) {
104
+ return this.addElement("script", attributes);
105
+ }
106
+ /**
107
+ * Adds a custom style element with inline CSS.
108
+ *
109
+ * @param attributes - The style element attributes
110
+ * @returns The builder instance for method chaining
111
+ *
112
+ * @example
113
+ * new HeadBuilder()
114
+ * .addStyle({ children: 'body { margin: 0; padding: 0; }' })
115
+ * .build();
116
+ */
117
+ addStyle(attributes) {
118
+ return this.addElement("style", attributes);
119
+ }
120
+ /**
121
+ * Adds a character encoding declaration to specify how the document should be interpreted.
122
+ *
123
+ * @param charSet - The character encoding (e.g., 'utf-8', 'iso-8859-1')
124
+ * @returns The builder instance for method chaining
125
+ *
126
+ * @example
127
+ * new HeadBuilder()
128
+ * .addCharSet('utf-8')
129
+ * .build();
130
+ */
131
+ addCharSet(charSet) {
132
+ return this.addElement("meta", { charSet });
133
+ }
134
+ /**
135
+ * Adds a color scheme preference indicating which color schemes the page supports for proper rendering.
136
+ *
137
+ * @param colorScheme - The supported color schemes (e.g., 'light', 'dark', 'light dark')
138
+ * @returns The builder instance for method chaining
139
+ *
140
+ * @example
141
+ * new HeadBuilder()
142
+ * .addColorScheme('light dark')
143
+ * .build();
144
+ */
145
+ addColorScheme(colorScheme) {
146
+ return this.addElement("meta", {
147
+ name: "color-scheme",
148
+ content: colorScheme
149
+ });
150
+ }
151
+ /**
152
+ * Adds a title element that appears in browser tabs, search results, and bookmarks.
153
+ *
154
+ * @param title - The document title text
155
+ * @returns The builder instance for method chaining
156
+ *
157
+ * @example
158
+ * new HeadBuilder()
159
+ * .addTitle('My Awesome Website')
160
+ * .build();
161
+ */
162
+ addTitle(title) {
163
+ return this.addElement("title", { children: title });
164
+ }
165
+ /**
166
+ * Adds viewport configuration for responsive web design and mobile optimization.
167
+ *
168
+ * @param options - Viewport settings (width, initial scale, zoom controls, etc.)
169
+ * @returns The builder instance for method chaining
170
+ *
171
+ * @example
172
+ * new HeadBuilder()
173
+ * .addViewport({ width: 'device-width', initialScale: 1 })
174
+ * .build();
175
+ */
176
+ addViewport(options) {
177
+ const contentParts = [];
178
+ if (options.width !== void 0) contentParts.push(`width=${options.width}`);
179
+ if (options.height !== void 0) contentParts.push(`height=${options.height}`);
180
+ if (options.initialScale !== void 0) contentParts.push(`initial-scale=${options.initialScale}`);
181
+ if (options.minimumScale !== void 0) contentParts.push(`minimum-scale=${options.minimumScale}`);
182
+ if (options.maximumScale !== void 0) contentParts.push(`maximum-scale=${options.maximumScale}`);
183
+ if (options.userScalable !== void 0) contentParts.push(`user-scalable=${options.userScalable ? "yes" : "no"}`);
184
+ if (options.viewportFit !== void 0) contentParts.push(`viewport-fit=${options.viewportFit}`);
185
+ if (options.interactiveWidget !== void 0) contentParts.push(`interactive-widget=${options.interactiveWidget}`);
186
+ return this.addElement("meta", {
187
+ name: "viewport",
188
+ content: contentParts.join(", ")
189
+ });
190
+ }
191
+ /**
192
+ * Adds a description that appears in search engine results and social media previews.
193
+ *
194
+ * @param description - The page description text
195
+ * @returns The builder instance for method chaining
196
+ *
197
+ * @example
198
+ * new HeadBuilder()
199
+ * .addDescription('A comprehensive guide to web development')
200
+ * .build();
201
+ */
202
+ addDescription(description) {
203
+ return this.addElement("meta", {
204
+ name: "description",
205
+ content: description
206
+ });
207
+ }
208
+ /**
209
+ * Adds a canonical URL to help search engines identify the preferred version of a page and prevent duplicate content issues.
210
+ *
211
+ * @param valueOrFn - The canonical URL or a callback function receiving helper utilities
212
+ * @returns The builder instance for method chaining
213
+ *
214
+ * @example
215
+ * new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
216
+ * .addCanonical((helper) => helper.resolveUrl('/page'))
217
+ * .build();
218
+ */
219
+ addCanonical(valueOrFn) {
220
+ const value = this.parseValueOrFn(valueOrFn);
221
+ return this.addElement("link", {
222
+ rel: "canonical",
223
+ href: value.toString()
224
+ });
225
+ }
226
+ /**
227
+ * Adds robots directives to control search engine crawling and indexing behavior.
228
+ *
229
+ * @param options - Robots configuration with index/follow booleans and custom directives
230
+ * @returns The builder instance for method chaining
231
+ *
232
+ * @example
233
+ * new HeadBuilder()
234
+ * .addRobots({ index: true, follow: true, 'max-snippet': 160 })
235
+ * .build();
236
+ */
237
+ addRobots(options) {
238
+ const directiveParts = [];
239
+ for (const [key, value] of Object.entries(options)) {
240
+ if (value === void 0) continue;
241
+ if (key === "index") directiveParts.push(value ? "index" : "noindex");
242
+ else if (key === "follow") directiveParts.push(value ? "follow" : "nofollow");
243
+ else if (typeof value === "string" || typeof value === "number") directiveParts.push(`${key}:${value}`);
244
+ else if (value) directiveParts.push(key);
245
+ }
246
+ return this.addElement("meta", {
247
+ name: "robots",
248
+ content: directiveParts.join(", ")
249
+ });
250
+ }
251
+ /**
252
+ * Adds Open Graph metadata for rich social media previews on platforms like Facebook, LinkedIn, and Slack.
253
+ *
254
+ * @param valueOrFn - Open Graph configuration or a callback function receiving helper utilities
255
+ * @returns The builder instance for method chaining
256
+ *
257
+ * @example
258
+ * new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
259
+ * .addOpenGraph((helper) => ({
260
+ * title: 'My Page',
261
+ * url: helper.resolveUrl('/page'),
262
+ * image: { url: helper.resolveUrl('/og-image.jpg') }
263
+ * }))
264
+ * .build();
265
+ */
266
+ addOpenGraph(valueOrFn) {
267
+ const options = this.parseValueOrFn(valueOrFn);
268
+ if (options.title) this.addElement("meta", {
269
+ property: "og:title",
270
+ content: options.title
271
+ });
272
+ if (options.description) this.addElement("meta", {
273
+ property: "og:description",
274
+ content: options.description
275
+ });
276
+ if (options.url) this.addElement("meta", {
277
+ property: "og:url",
278
+ content: options.url.toString()
279
+ });
280
+ if (options.locale) this.addElement("meta", {
281
+ property: "og:locale",
282
+ content: options.locale
283
+ });
284
+ if (options.image) {
285
+ this.addElement("meta", {
286
+ property: "og:image",
287
+ content: options.image.url.toString()
288
+ });
289
+ if (options.image.alt) this.addElement("meta", {
290
+ property: "og:image:alt",
291
+ content: options.image.alt
292
+ });
293
+ if (options.image.type) this.addElement("meta", {
294
+ property: "og:image:type",
295
+ content: options.image.type
296
+ });
297
+ if (options.image.width) this.addElement("meta", {
298
+ property: "og:image:width",
299
+ content: options.image.width.toString()
300
+ });
301
+ if (options.image.height) this.addElement("meta", {
302
+ property: "og:image:height",
303
+ content: options.image.height.toString()
304
+ });
305
+ }
306
+ if (options.type) {
307
+ this.addElement("meta", {
308
+ property: "og:type",
309
+ content: options.type.name
310
+ });
311
+ if ("properties" in options.type) for (const typeProperty of options.type.properties) this.addElement("meta", {
312
+ property: typeProperty.name,
313
+ content: typeProperty.content
314
+ });
315
+ }
316
+ return this;
317
+ }
318
+ /**
319
+ * Adds Twitter Card metadata for rich previews when links are shared on Twitter/X.
320
+ *
321
+ * @param valueOrFn - Twitter Card configuration or a callback function receiving helper utilities
322
+ * @returns The builder instance for method chaining
323
+ *
324
+ * @example
325
+ * new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
326
+ * .addTwitter((helper) => ({
327
+ * card: { name: 'summary_large_image' },
328
+ * image: { url: helper.resolveUrl('/twitter-card.jpg') }
329
+ * }))
330
+ * .build();
331
+ */
332
+ addTwitter(valueOrFn) {
333
+ const options = this.parseValueOrFn(valueOrFn);
334
+ if (options.title) this.addElement("meta", {
335
+ name: "twitter:title",
336
+ content: options.title
337
+ });
338
+ if (options.description) this.addElement("meta", {
339
+ name: "twitter:description",
340
+ content: options.description
341
+ });
342
+ if (options.site) this.addElement("meta", {
343
+ name: "twitter:site",
344
+ content: options.site
345
+ });
346
+ if (options.siteId) this.addElement("meta", {
347
+ name: "twitter:site:id",
348
+ content: options.siteId
349
+ });
350
+ if (options.creator) this.addElement("meta", {
351
+ name: "twitter:creator",
352
+ content: options.creator
353
+ });
354
+ if (options.creatorId) this.addElement("meta", {
355
+ name: "twitter:creator:id",
356
+ content: options.creatorId
357
+ });
358
+ if (options.image) {
359
+ this.addElement("meta", {
360
+ name: "twitter:image",
361
+ content: options.image.url.toString()
362
+ });
363
+ if (options.image.alt) this.addElement("meta", {
364
+ name: "twitter:image:alt",
365
+ content: options.image.alt
366
+ });
367
+ }
368
+ if (options.card) {
369
+ this.addElement("meta", {
370
+ name: "twitter:card",
371
+ content: options.card.name
372
+ });
373
+ if ("properties" in options.card) for (const cardProperty of options.card.properties) this.addElement("meta", {
374
+ name: cardProperty.name,
375
+ content: cardProperty.content.toString()
376
+ });
377
+ }
378
+ return this;
379
+ }
380
+ /**
381
+ * Adds alternate language/locale versions of the page to help search engines serve the correct localized content to users.
382
+ *
383
+ * @param valueOrFn - Locale-to-URL mapping or a callback function receiving helper utilities
384
+ * @returns The builder instance for method chaining
385
+ *
386
+ * @example
387
+ * new HeadBuilder({ metadataBase: new URL('https://example.com') })
388
+ * .addAlternateLocale((helper) => ({
389
+ * 'en-US': helper.resolveUrl('/en'),
390
+ * 'fr-FR': helper.resolveUrl('/fr'),
391
+ * 'x-default': helper.resolveUrl('/')
392
+ * }))
393
+ * .build();
394
+ */
395
+ addAlternateLocale(valueOrFn) {
396
+ const options = this.parseValueOrFn(valueOrFn);
397
+ for (const [lang, href] of Object.entries(options)) this.addElement("link", {
398
+ rel: "alternate",
399
+ hrefLang: lang,
400
+ href: String(href)
401
+ });
402
+ return this;
403
+ }
404
+ /**
405
+ * Adds a web app manifest link that defines how your application appears when installed on devices.
406
+ *
407
+ * @param valueOrFn - The manifest URL or a callback function receiving helper utilities
408
+ * @returns The builder instance for method chaining
409
+ *
410
+ * @example
411
+ * new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
412
+ * .addManifest((helper) => helper.resolveUrl('/manifest.json'))
413
+ * .build();
414
+ */
415
+ addManifest(valueOrFn) {
416
+ const href = this.parseValueOrFn(valueOrFn);
417
+ return this.addElement("link", {
418
+ rel: "manifest",
419
+ href: href.toString()
420
+ });
421
+ }
422
+ /**
423
+ * Adds an external CSS stylesheet link to the page.
424
+ *
425
+ * @param href - The stylesheet URL
426
+ * @param options - Additional link attributes (media queries, integrity, etc.)
427
+ * @returns The builder instance for method chaining
428
+ *
429
+ * @example
430
+ * new HeadBuilder()
431
+ * .addStylesheet('/styles.css', { media: 'print' })
432
+ * .build();
433
+ */
434
+ addStylesheet(href, options) {
435
+ return this.addElement("link", {
436
+ rel: "stylesheet",
437
+ href: href.toString(),
438
+ ...options
439
+ });
440
+ }
441
+ /**
442
+ * Adds a favicon or app icon using preset types or custom rel values.
443
+ *
444
+ * @param preset - Icon type ('icon', 'apple', 'shortcut', or custom string)
445
+ * @param valueOrFn - Icon configuration or a callback function receiving helper utilities
446
+ * @returns The builder instance for method chaining
447
+ *
448
+ * @example
449
+ * new HeadBuilder({ metadataBase: new URL('https://devsantara.com') })
450
+ * .addIcon('apple', (helper) => ({
451
+ * href: helper.resolveUrl('/apple-icon.png'),
452
+ * sizes: '180x180'
453
+ * }))
454
+ * .build();
455
+ */
456
+ addIcon(preset, valueOrFn) {
457
+ const options = this.parseValueOrFn(valueOrFn);
458
+ const rel = {
459
+ apple: "apple-touch-icon",
460
+ icon: "icon",
461
+ shortcut: "shortcut icon"
462
+ }[preset] || preset;
463
+ return this.addElement("link", {
464
+ rel,
465
+ href: options.href.toString(),
466
+ type: options.type,
467
+ sizes: options.sizes,
468
+ media: options.media,
469
+ fetchPriority: options.fetchPriority
470
+ });
471
+ }
472
+ /**
473
+ * Builds and returns the final head configuration. Returns adapted output if an adapter was provided, otherwise returns `HeadElement[]`.
474
+ *
475
+ * @returns The head configuration in the target format
476
+ */
477
+ build() {
478
+ if (this.adapter) return this.adapter.transform(this.elements);
479
+ return this.elements;
480
+ }
481
+ };
482
+
483
+ //#endregion
484
+ export { HeadBuilder };