diff --git a/.gitignore b/.gitignore index 42d5d2a..083db29 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,8 @@ pnpm-debug.log* # macOS-specific files .DS_Store -package-lock.json \ No newline at end of file +package-lock.json +pnpm-lock.yaml + +# tarballs +*.tgz \ No newline at end of file diff --git a/package.json b/package.json index 9f539ea..d5b6512 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,6 @@ "workspaces": [ "packages/*" ], - "scripts": { - }, "author": { "name": "onWidget", "email": "contact@onwidget.com", diff --git a/packages/analytics/package.json b/packages/analytics/package.json index aae518e..abf2ef8 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -40,9 +40,9 @@ ], "scripts": {}, "devDependencies": { - "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0" + "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" }, "peerDependencies": { - "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0" + "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" } } diff --git a/packages/seo/package.json b/packages/seo/package.json index 45c210e..2ead97e 100644 --- a/packages/seo/package.json +++ b/packages/seo/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@types/html-escaper": "^3.0.0", "@types/jest": "^29.5.4", - "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0", + "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "html-escaper": "^3.0.3", "html-validate": "^8.18.2", "jest": "^29.6.3", @@ -55,6 +55,6 @@ "typescript": "^5.1.6" }, "peerDependencies": { - "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0" + "astro": "^1.2.1 || ^2.0.0 || ^3.0.0-beta.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" } } diff --git a/packages/seo/src/__test__/buildTags.test.ts b/packages/seo/src/__test__/buildTags.test.ts index 3321e24..d5c256c 100644 --- a/packages/seo/src/__test__/buildTags.test.ts +++ b/packages/seo/src/__test__/buildTags.test.ts @@ -4,438 +4,555 @@ import { HtmlValidate as _HtmlValidate } from "html-validate/node"; const validate = new _HtmlValidate(); describe("buildTags function", () => { - it("should return an empty string if no config is provided", () => { - const result = buildTags({}); - expect(result).toBe(""); - }); - - it("should handle null or undefined values gracefully", () => { - const config = { - title: null, - description: undefined, - }; - // @ts-ignore - const result = buildTags(config); - expect(result).not.toContain("null"); - expect(result).not.toContain("undefined"); - }); - - it("should escape special characters correctly", async () => { - const config = { - title: "Title & Description", - }; - const result = buildTags(config); - expect(result).toContain("Title & Description"); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct title tag", async () => { - const config = { - title: '', - }; - const result = buildTags(config); - expect(result).toContain( - "<script>alert("hacked")</script>" - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct description tag", async () => { - const config = { - description: '', - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should escape URLs", async () => { - const config = { - openGraph: { - url: '', - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct robots tag for noindex and nofollow", async () => { - const config = { - noindex: true, - nofollow: true, - }; - const result = buildTags(config); - expect(result).toContain(''); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct twitter card tag", async () => { - const config = { - twitter: { - cardType: "summary_large_image", - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct facebook app id tag", async () => { - const config = { - facebook: { - appId: "1234567890", - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct openGraph title tag", async () => { - const config = { - openGraph: { - title: "Test Title", - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct canonical link tag", async () => { - const config = { - canonical: "https://example.com/page", - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct alternate link tag for mobile", async () => { - const config = { - mobileAlternate: { - media: "only screen and (max-width: 640px)", - href: "https://m.example.com/page", - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct openGraph description tag", async () => { - const config = { - openGraph: { - description: "Test Description", - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct openGraph type tag", async () => { - const config = { - openGraph: { - type: "article", - }, - }; - const result = buildTags(config); - expect(result).toContain(''); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct openGraph locale tag", async () => { - const config = { - openGraph: { - locale: "en_US", - }, - }; - const result = buildTags(config); - expect(result).toContain(''); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct openGraph site_name tag", async () => { - const config = { - openGraph: { - site_name: "Test Site", - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should handle multiple OpenGraph media tags correctly", async () => { - const config = { - openGraph: { - images: [ - { url: "https://example.com/image1.jpg" }, - { url: "https://example.com/image2.jpg" }, - ], - videos: [ - { url: "https://example.com/video1.mp4" }, - { url: "https://example.com/video2.mp4" }, - ], - }, - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct languageAlternates link tags", async () => { - const config = { - languageAlternates: [ - { hreflang: "es", href: "https://example.com/es" }, - { hreflang: "fr", href: "https://example.com/fr" }, - ], - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct twitter site and creator tags", async () => { - const config = { - twitter: { - site: "@testsite", - handle: "@testhandle", - }, - }; - const result = buildTags(config); - expect(result).toContain(''); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct additionalMetaTags", async () => { - const config = { - additionalMetaTags: [ - { name: "viewport", content: "width=device-width, initial-scale=1" }, - ], - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate correct additionalLinkTags", async () => { - const config = { - additionalLinkTags: [ - { rel: "stylesheet", href: "https://example.com/styles.css" }, - ], - }; - const result = buildTags(config); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate og:title and og:description from title and description if not explicitly set", async () => { - const config = { - title: "Test Title", - description: "Test Description", - openGraph: {}, - }; - const result = buildTags(config); - expect(result).toContain(''); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should not generate og:title and og:description if openGraph is not defined", async () => { - const config = { - title: "Test Title", - description: "Test Description", - }; - const result = buildTags(config); - expect(result).not.toContain( - '' - ); - expect(result).not.toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - // Casos de prueba realistas - it("should generate a complete set of tags for a blog post", async () => { - const config = { - title: "My Blog Post", - description: "A detailed description of my blog post.", - canonical: "https://example.com/blog/my-blog-post", - openGraph: { - type: "article", - url: "https://example.com/blog/my-blog-post", - title: "My Blog Post", - description: "A detailed description of my blog post.", - images: [{ url: "https://example.com/images/blog-image.jpg" }], - }, - twitter: { - cardType: "summary_large_image", - site: "@example", - }, - }; - const result = buildTags(config); - expect(result).toContain("My Blog Post"); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain(''); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain(''); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); - - it("should generate a complete set of tags for a product page", async () => { - const config = { - title: "Product Name", - description: "Description of the product.", - canonical: "https://example.com/products/product-name", - openGraph: { - type: "product", - url: "https://example.com/products/product-name", - title: "Product Name", - description: "Description of the product.", - images: [{ url: "https://example.com/images/product-image.jpg" }], - }, - twitter: { - cardType: "summary", - site: "@examplestore", - }, - }; - const result = buildTags(config); - expect(result).toContain("Product Name"); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain(''); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain( - '' - ); - expect(result).toContain(''); - expect(result).toContain( - '' - ); - - const htmlResult = await validate.validateString(result); - expect(htmlResult.valid).toBe(true); - }); + it("should return an empty string if no config is provided", () => { + const result = buildTags({}); + expect(result).toBe(""); + }); + + it("should handle null or undefined values gracefully", () => { + const config = { + title: null, + description: undefined, + }; + // @ts-ignore + const result = buildTags(config); + expect(result).not.toContain("null"); + expect(result).not.toContain("undefined"); + }); + + it("should escape special characters correctly", async () => { + const config = { + title: "Title & Description", + }; + const result = buildTags(config); + expect(result).toContain("Title & Description"); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct title tag", async () => { + const config = { + title: '', + }; + const result = buildTags(config); + expect(result).toContain( + "<script>alert("hacked")</script>" + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct description tag", async () => { + const config = { + description: '', + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should escape URLs", async () => { + const config = { + openGraph: { + url: '', + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct robots tag for noindex and nofollow", async () => { + const config = { + noindex: true, + nofollow: true, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct twitter card tag", async () => { + const config = { + twitter: { + cardType: "summary_large_image", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct facebook app id tag", async () => { + const config = { + facebook: { + appId: "1234567890", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct openGraph title tag", async () => { + const config = { + openGraph: { + title: "Test Title", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct canonical link tag", async () => { + const config = { + canonical: "https://example.com/page", + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct alternate link tag for mobile", async () => { + const config = { + mobileAlternate: { + media: "only screen and (max-width: 640px)", + href: "https://m.example.com/page", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct openGraph description tag", async () => { + const config = { + openGraph: { + description: "Test Description", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct openGraph type tag", async () => { + const config = { + openGraph: { + type: "article", + }, + }; + const result = buildTags(config); + expect(result).toContain(''); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct openGraph locale tag", async () => { + const config = { + openGraph: { + locale: "en_US", + }, + }; + const result = buildTags(config); + expect(result).toContain(''); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct openGraph site_name tag", async () => { + const config = { + openGraph: { + site_name: "Test Site", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should handle multiple OpenGraph media tags correctly", async () => { + const config = { + openGraph: { + images: [ + { url: "https://example.com/image1.jpg" }, + { url: "https://example.com/image2.jpg" }, + ], + videos: [ + { url: "https://example.com/video1.mp4" }, + { url: "https://example.com/video2.mp4" }, + ], + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct OpenGraph profile tags", async () => { + const config = { + openGraph: { + profile: { + firstName: "John", + lastName: "Doe", + gender: "male", + username: "johndoe", + }, + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct OpenGraph book tags", async () => { + const config = { + openGraph: { + book: { + authors: [ + "http://walterisaacson.com", + "http://author2.com", + ], + isbn: "978-1451648539", + releaseDate: "2011-10-24", + tags: ["Steve Jobs", "tag2"], + }, + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain(''); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct languageAlternates link tags", async () => { + const config = { + languageAlternates: [ + { hreflang: "es", href: "https://example.com/es" }, + { hreflang: "fr", href: "https://example.com/fr" }, + ], + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct twitter site and creator tags", async () => { + const config = { + twitter: { + site: "@testsite", + handle: "@testhandle", + }, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct additionalMetaTags", async () => { + const config = { + additionalMetaTags: [ + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + ], + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate correct additionalLinkTags", async () => { + const config = { + additionalLinkTags: [ + { rel: "stylesheet", href: "https://example.com/styles.css" }, + ], + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate og:title and og:description from title and description if not explicitly set", async () => { + const config = { + title: "Test Title", + description: "Test Description", + openGraph: {}, + }; + const result = buildTags(config); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should not generate og:title and og:description if openGraph is not defined", async () => { + const config = { + title: "Test Title", + description: "Test Description", + }; + const result = buildTags(config); + expect(result).not.toContain( + '' + ); + expect(result).not.toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + // Casos de prueba realistas + it("should generate a complete set of tags for a blog post", async () => { + const config = { + title: "My Blog Post", + description: "A detailed description of my blog post.", + canonical: "https://example.com/blog/my-blog-post", + openGraph: { + type: "article", + url: "https://example.com/blog/my-blog-post", + title: "My Blog Post", + description: "A detailed description of my blog post.", + images: [{ url: "https://example.com/images/blog-image.jpg" }], + article: { + publishedTime: "2022-01-01T12:00:00Z", + modifiedTime: "2022-01-02T12:00:00Z", + expirationTime: "2022-01-03T12:00:00Z", + section: "Technology", + tags: ["tag1", "tag2"], + authors: [ + "https://www.facebook.com/john-doe", + "https://www.facebook.com/jane-doe", + ], + }, + }, + twitter: { + cardType: "summary_large_image", + site: "@example", + }, + }; + const result = buildTags(config); + expect(result).toContain("My Blog Post"); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain(''); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + // OpenGraph article meta tags + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); + + it("should generate a complete set of tags for a product page", async () => { + const config = { + title: "Product Name", + description: "Description of the product.", + canonical: "https://example.com/products/product-name", + openGraph: { + type: "product", + url: "https://example.com/products/product-name", + title: "Product Name", + description: "Description of the product.", + images: [ + { url: "https://example.com/images/product-image.jpg" }, + ], + }, + twitter: { + cardType: "summary", + site: "@examplestore", + }, + }; + const result = buildTags(config); + expect(result).toContain("Product Name"); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain(''); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + expect(result).toContain( + '' + ); + + const htmlResult = await validate.validateString(result); + expect(htmlResult.valid).toBe(true); + }); }); diff --git a/packages/seo/src/utils/buildTags.ts b/packages/seo/src/utils/buildTags.ts index b4256bc..e3cd46e 100644 --- a/packages/seo/src/utils/buildTags.ts +++ b/packages/seo/src/utils/buildTags.ts @@ -2,456 +2,513 @@ import { escape } from "html-escaper"; import type { AstroSeoProps, OpenGraphMedia } from "../types"; const createMetaTag = (attributes: Record): string => { - const attrs = Object.entries(attributes) - .map(([key, value]) => `${key}="${escape(value)}"`) - .join(" "); - return ``; + const attrs = Object.entries(attributes) + .map(([key, value]) => `${key}="${escape(value)}"`) + .join(" "); + return ``; }; const createLinkTag = (attributes: Record): string => { - const attrs = Object.entries(attributes) - .map(([key, value]) => `${key}="${escape(value)}"`) - .join(" "); - return ``; + const attrs = Object.entries(attributes) + .map(([key, value]) => `${key}="${escape(value)}"`) + .join(" "); + return ``; }; -const createOpenGraphTag = (property: string, content: string): string => { - return createMetaTag({ property: `og:${property}`, content }); +const createOpenGraphTag = ( + property: string, + content: string, + ogPrefix: boolean = true +): string => { + return createMetaTag({ + property: ogPrefix ? `og:${property}` : `${property}`, + content, + }); }; const buildOpenGraphMediaTags = ( - mediaType: "image" | "video", - media: ReadonlyArray + mediaType: "image" | "video", + media: ReadonlyArray ): string => { - let tags = ""; - - const addTag = (tag: string) => { - tags += tag + "\n"; - }; - - media.forEach((medium) => { - addTag(createOpenGraphTag(mediaType, medium.url)); - - if (medium.alt) { - addTag(createOpenGraphTag(`${mediaType}:alt`, medium.alt)); - } - - if (medium.secureUrl) { - addTag(createOpenGraphTag(`${mediaType}:secure_url`, medium.secureUrl)); - } - - if (medium.type) { - addTag(createOpenGraphTag(`${mediaType}:type`, medium.type)); - } - - if (medium.width) { - addTag(createOpenGraphTag(`${mediaType}:width`, medium.width.toString())); - } - - if (medium.height) { - addTag( - createOpenGraphTag(`${mediaType}:height`, medium.height.toString()) - ); - } - }); - return tags; + let tags = ""; + + const addTag = (tag: string) => { + tags += tag + "\n"; + }; + + media.forEach((medium) => { + addTag(createOpenGraphTag(mediaType, medium.url)); + + if (medium.alt) { + addTag(createOpenGraphTag(`${mediaType}:alt`, medium.alt)); + } + + if (medium.secureUrl) { + addTag( + createOpenGraphTag(`${mediaType}:secure_url`, medium.secureUrl) + ); + } + + if (medium.type) { + addTag(createOpenGraphTag(`${mediaType}:type`, medium.type)); + } + + if (medium.width) { + addTag( + createOpenGraphTag( + `${mediaType}:width`, + medium.width.toString() + ) + ); + } + + if (medium.height) { + addTag( + createOpenGraphTag( + `${mediaType}:height`, + medium.height.toString() + ) + ); + } + }); + return tags; }; export const buildTags = (config: AstroSeoProps): string => { - let tagsToRender = ""; - - const addTag = (tag: string) => { - tagsToRender += tag + "\n"; - }; - - const addMetaTag = (attributes: Record) => { - addTag( - ` `${key}="${escape(value)}"`) - .join(" ")} />` - ); - }; + let tagsToRender = ""; - const addLinkTag = (attributes: Record) => { - addTag( - ` `${key}="${escape(value)}"`) - .join(" ")} />` - ); - }; - - const addOpenGraphTag = (property: string, content: string) => { - addMetaTag({ property: `og:${property}`, content }); - }; - - // Title - if (config.title) { - const formattedTitle = config.titleTemplate - ? config.titleTemplate.replace("%s", config.title) - : config.title; - addTag(`${escape(formattedTitle)}`); - } - - // Description - if (config.description) { - addTag(createMetaTag({ name: "description", content: config.description })); - } - - // Robots: noindex, nofollow, and other robotsProps - let robotsContent: string[] = []; - if (typeof config.noindex !== "undefined") { - robotsContent.push(config.noindex ? "noindex" : "index"); - } - - if (typeof config.nofollow !== "undefined") { - robotsContent.push(config.nofollow ? "nofollow" : "follow"); - } - - if (config.robotsProps) { - const { - nosnippet, - maxSnippet, - maxImagePreview, - noarchive, - unavailableAfter, - noimageindex, - notranslate, - } = config.robotsProps; - - if (nosnippet) robotsContent.push("nosnippet"); - if (typeof maxSnippet === 'number') robotsContent.push(`max-snippet:${maxSnippet}`); - if (maxImagePreview) - robotsContent.push(`max-image-preview:${maxImagePreview}`); - if (noarchive) robotsContent.push("noarchive"); - if (unavailableAfter) - robotsContent.push(`unavailable_after:${unavailableAfter}`); - if (noimageindex) robotsContent.push("noimageindex"); - if (notranslate) robotsContent.push("notranslate"); - } - - if (robotsContent.length > 0) { - addTag(createMetaTag({ name: "robots", content: robotsContent.join(",") })); - } - - // Canonical - if (config.canonical) { - addTag(createLinkTag({ rel: "canonical", href: config.canonical })); - } - - // Mobile Alternate - if (config.mobileAlternate) { - addTag( - createLinkTag({ - rel: "alternate", - media: config.mobileAlternate.media, - href: config.mobileAlternate.href, - }) - ); - } - - // Language Alternates - if (config.languageAlternates && config.languageAlternates.length > 0) { - config.languageAlternates.forEach((languageAlternate) => { - addTag( - createLinkTag({ - rel: "alternate", - hreflang: languageAlternate.hreflang, - href: languageAlternate.href, - }) - ); - }); - } + const addTag = (tag: string) => { + tagsToRender += tag + "\n"; + }; - // OpenGraph - if (config.openGraph) { - const title = config.openGraph?.title || config.title; - if (title) { - addTag(createOpenGraphTag("title", title)); - } + const addMetaTag = (attributes: Record) => { + addTag( + ` `${key}="${escape(value)}"`) + .join(" ")} />` + ); + }; - const description = config.openGraph?.description || config.description; - if (description) { - addTag(createOpenGraphTag("description", description)); + const addLinkTag = (attributes: Record) => { + addTag( + ` `${key}="${escape(value)}"`) + .join(" ")} />` + ); + }; + + const addOpenGraphTag = (property: string, content: string) => { + addMetaTag({ property: `og:${property}`, content }); + }; + + // Title + if (config.title) { + const formattedTitle = config.titleTemplate + ? config.titleTemplate.replace("%s", config.title) + : config.title; + addTag(`${escape(formattedTitle)}`); } - if (config.openGraph.url) { - addTag(createOpenGraphTag("url", config.openGraph.url)); + // Description + if (config.description) { + addTag( + createMetaTag({ name: "description", content: config.description }) + ); } - if (config.openGraph.type) { - addTag(createOpenGraphTag("type", config.openGraph.type)); + // Robots: noindex, nofollow, and other robotsProps + let robotsContent: string[] = []; + if (typeof config.noindex !== "undefined") { + robotsContent.push(config.noindex ? "noindex" : "index"); } - if (config.openGraph.images && config.openGraph.images.length) { - addTag(buildOpenGraphMediaTags("image", config.openGraph.images)); + if (typeof config.nofollow !== "undefined") { + robotsContent.push(config.nofollow ? "nofollow" : "follow"); } - if (config.openGraph.videos && config.openGraph.videos.length) { - addTag(buildOpenGraphMediaTags("video", config.openGraph.videos)); + if (config.robotsProps) { + const { + nosnippet, + maxSnippet, + maxImagePreview, + noarchive, + unavailableAfter, + noimageindex, + notranslate, + } = config.robotsProps; + + if (nosnippet) robotsContent.push("nosnippet"); + if (typeof maxSnippet === "number") + robotsContent.push(`max-snippet:${maxSnippet}`); + if (maxImagePreview) + robotsContent.push(`max-image-preview:${maxImagePreview}`); + if (noarchive) robotsContent.push("noarchive"); + if (unavailableAfter) + robotsContent.push(`unavailable_after:${unavailableAfter}`); + if (noimageindex) robotsContent.push("noimageindex"); + if (notranslate) robotsContent.push("notranslate"); } - if (config.openGraph.locale) { - addTag(createOpenGraphTag("locale", config.openGraph.locale)); + if (robotsContent.length > 0) { + addTag( + createMetaTag({ name: "robots", content: robotsContent.join(",") }) + ); } - if (config.openGraph.site_name) { - addTag(createOpenGraphTag("site_name", config.openGraph.site_name)); + // Canonical + if (config.canonical) { + addTag(createLinkTag({ rel: "canonical", href: config.canonical })); } - // Open Graph Profile - if (config.openGraph.profile) { - if (config.openGraph.profile.firstName) { + // Mobile Alternate + if (config.mobileAlternate) { addTag( - createOpenGraphTag( - "profile:first_name", - config.openGraph.profile.firstName - ) + createLinkTag({ + rel: "alternate", + media: config.mobileAlternate.media, + href: config.mobileAlternate.href, + }) ); - } - if (config.openGraph.profile.lastName) { - addTag( - createOpenGraphTag( - "profile:last_name", - config.openGraph.profile.lastName - ) - ); - } - if (config.openGraph.profile.username) { - addTag( - createOpenGraphTag( - "profile:username", - config.openGraph.profile.username - ) - ); - } - if (config.openGraph.profile.gender) { - addTag( - createOpenGraphTag("profile:gender", config.openGraph.profile.gender) - ); - } } - // Open Graph Book - if (config.openGraph.book) { - if ( - config.openGraph.book.authors && - config.openGraph.book.authors.length - ) { - config.openGraph.book.authors.forEach((author) => { - addTag(createOpenGraphTag("book:author", author)); - }); - } - if (config.openGraph.book.isbn) { - addTag(createOpenGraphTag("book:isbn", config.openGraph.book.isbn)); - } - if (config.openGraph.book.releaseDate) { - addTag( - createOpenGraphTag( - "book:release_date", - config.openGraph.book.releaseDate - ) - ); - } - if (config.openGraph.book.tags && config.openGraph.book.tags.length) { - config.openGraph.book.tags.forEach((tag) => { - addTag(createOpenGraphTag("book:tag", tag)); + // Language Alternates + if (config.languageAlternates && config.languageAlternates.length > 0) { + config.languageAlternates.forEach((languageAlternate) => { + addTag( + createLinkTag({ + rel: "alternate", + hreflang: languageAlternate.hreflang, + href: languageAlternate.href, + }) + ); }); - } } - // Open Graph Article - if (config.openGraph.article) { - if (config.openGraph.article.publishedTime) { - addTag( - createOpenGraphTag( - "article:published_time", - config.openGraph.article.publishedTime - ) - ); - } - if (config.openGraph.article.modifiedTime) { - addTag( - createOpenGraphTag( - "article:modified_time", - config.openGraph.article.modifiedTime - ) - ); - } - if (config.openGraph.article.expirationTime) { - addTag( - createOpenGraphTag( - "article:expiration_time", - config.openGraph.article.expirationTime - ) - ); - } - if ( - config.openGraph.article.authors && - config.openGraph.article.authors.length - ) { - config.openGraph.article.authors.forEach((author) => { - addTag(createOpenGraphTag("article:author", author)); - }); - } - if (config.openGraph.article.section) { - addTag( - createOpenGraphTag( - "article:section", - config.openGraph.article.section - ) - ); - } - if ( - config.openGraph.article.tags && - config.openGraph.article.tags.length - ) { - config.openGraph.article.tags.forEach((tag) => { - addTag(createOpenGraphTag("article:tag", tag)); - }); - } + // OpenGraph + if (config.openGraph) { + const title = config.openGraph?.title || config.title; + if (title) { + addTag(createOpenGraphTag("title", title)); + } + + const description = config.openGraph?.description || config.description; + if (description) { + addTag(createOpenGraphTag("description", description)); + } + + if (config.openGraph.url) { + addTag(createOpenGraphTag("url", config.openGraph.url)); + } + + if (config.openGraph.type) { + addTag(createOpenGraphTag("type", config.openGraph.type)); + } + + if (config.openGraph.images && config.openGraph.images.length) { + addTag(buildOpenGraphMediaTags("image", config.openGraph.images)); + } + + if (config.openGraph.videos && config.openGraph.videos.length) { + addTag(buildOpenGraphMediaTags("video", config.openGraph.videos)); + } + + if (config.openGraph.locale) { + addTag(createOpenGraphTag("locale", config.openGraph.locale)); + } + + if (config.openGraph.site_name) { + addTag(createOpenGraphTag("site_name", config.openGraph.site_name)); + } + + // Open Graph Profile + if (config.openGraph.profile) { + if (config.openGraph.profile.firstName) { + addTag( + createOpenGraphTag( + "profile:first_name", + config.openGraph.profile.firstName, + false + ) + ); + } + if (config.openGraph.profile.lastName) { + addTag( + createOpenGraphTag( + "profile:last_name", + config.openGraph.profile.lastName, + false + ) + ); + } + if (config.openGraph.profile.username) { + addTag( + createOpenGraphTag( + "profile:username", + config.openGraph.profile.username, + false + ) + ); + } + if (config.openGraph.profile.gender) { + addTag( + createOpenGraphTag( + "profile:gender", + config.openGraph.profile.gender, + false + ) + ); + } + } + + // Open Graph Book + if (config.openGraph.book) { + if ( + config.openGraph.book.authors && + config.openGraph.book.authors.length + ) { + config.openGraph.book.authors.forEach((author) => { + addTag(createOpenGraphTag("book:author", author, false)); + }); + } + if (config.openGraph.book.isbn) { + addTag( + createOpenGraphTag( + "book:isbn", + config.openGraph.book.isbn, + false + ) + ); + } + if (config.openGraph.book.releaseDate) { + addTag( + createOpenGraphTag( + "book:release_date", + config.openGraph.book.releaseDate, + false + ) + ); + } + if ( + config.openGraph.book.tags && + config.openGraph.book.tags.length + ) { + config.openGraph.book.tags.forEach((tag) => { + addTag(createOpenGraphTag("book:tag", tag, false)); + }); + } + } + + // Open Graph Article + if (config.openGraph.article) { + if (config.openGraph.article.publishedTime) { + addTag( + createOpenGraphTag( + "article:published_time", + config.openGraph.article.publishedTime, + false + ) + ); + } + if (config.openGraph.article.modifiedTime) { + addTag( + createOpenGraphTag( + "article:modified_time", + config.openGraph.article.modifiedTime, + false + ) + ); + } + if (config.openGraph.article.expirationTime) { + addTag( + createOpenGraphTag( + "article:expiration_time", + config.openGraph.article.expirationTime, + false + ) + ); + } + if ( + config.openGraph.article.authors && + config.openGraph.article.authors.length + ) { + config.openGraph.article.authors.forEach((author) => { + addTag(createOpenGraphTag("article:author", author, false)); + }); + } + if (config.openGraph.article.section) { + addTag( + createOpenGraphTag( + "article:section", + config.openGraph.article.section, + false + ) + ); + } + if ( + config.openGraph.article.tags && + config.openGraph.article.tags.length + ) { + config.openGraph.article.tags.forEach((tag) => { + addTag(createOpenGraphTag("article:tag", tag, false)); + }); + } + } + + // Open Graph Video + if (config.openGraph.video) { + if ( + config.openGraph.video.actors && + config.openGraph.video.actors.length + ) { + config.openGraph.video.actors.forEach((actor) => { + addTag(createOpenGraphTag("video:actor", actor.profile)); + if (actor.role) { + addTag( + createOpenGraphTag("video:actor:role", actor.role) + ); + } + }); + } + if ( + config.openGraph.video.directors && + config.openGraph.video.directors.length + ) { + config.openGraph.video.directors.forEach((director) => { + addTag(createOpenGraphTag("video:director", director)); + }); + } + if ( + config.openGraph.video.writers && + config.openGraph.video.writers.length + ) { + config.openGraph.video.writers.forEach((writer) => { + addTag(createOpenGraphTag("video:writer", writer)); + }); + } + if (config.openGraph.video.duration) { + addTag( + createOpenGraphTag( + "video:duration", + config.openGraph.video.duration.toString() + ) + ); + } + if (config.openGraph.video.releaseDate) { + addTag( + createOpenGraphTag( + "video:release_date", + config.openGraph.video.releaseDate + ) + ); + } + if ( + config.openGraph.video.tags && + config.openGraph.video.tags.length + ) { + config.openGraph.video.tags.forEach((tag) => { + addTag(createOpenGraphTag("video:tag", tag)); + }); + } + if (config.openGraph.video.series) { + addTag( + createOpenGraphTag( + "video:series", + config.openGraph.video.series + ) + ); + } + } } - // Open Graph Video - if (config.openGraph.video) { - if ( - config.openGraph.video.actors && - config.openGraph.video.actors.length - ) { - config.openGraph.video.actors.forEach((actor) => { - addTag(createOpenGraphTag("video:actor", actor.profile)); - if (actor.role) { - addTag(createOpenGraphTag("video:actor:role", actor.role)); - } - }); - } - if ( - config.openGraph.video.directors && - config.openGraph.video.directors.length - ) { - config.openGraph.video.directors.forEach((director) => { - addTag(createOpenGraphTag("video:director", director)); - }); - } - if ( - config.openGraph.video.writers && - config.openGraph.video.writers.length - ) { - config.openGraph.video.writers.forEach((writer) => { - addTag(createOpenGraphTag("video:writer", writer)); - }); - } - if (config.openGraph.video.duration) { - addTag( - createOpenGraphTag( - "video:duration", - config.openGraph.video.duration.toString() - ) - ); - } - if (config.openGraph.video.releaseDate) { + // Facebook + if (config.facebook && config.facebook.appId) { addTag( - createOpenGraphTag( - "video:release_date", - config.openGraph.video.releaseDate - ) + createMetaTag({ + property: "fb:app_id", + content: config.facebook.appId, + }) ); - } - if (config.openGraph.video.tags && config.openGraph.video.tags.length) { - config.openGraph.video.tags.forEach((tag) => { - addTag(createOpenGraphTag("video:tag", tag)); - }); - } - if (config.openGraph.video.series) { - addTag( - createOpenGraphTag("video:series", config.openGraph.video.series) - ); - } } - } - - // Facebook - if (config.facebook && config.facebook.appId) { - addTag( - createMetaTag({ property: "fb:app_id", content: config.facebook.appId }) - ); - } - - // Twitter - if (config.twitter) { - if (config.twitter.cardType) { - addTag( - createMetaTag({ - name: "twitter:card", - content: config.twitter.cardType, - }) - ); + + // Twitter + if (config.twitter) { + if (config.twitter.cardType) { + addTag( + createMetaTag({ + name: "twitter:card", + content: config.twitter.cardType, + }) + ); + } + + if (config.twitter.site) { + addTag( + createMetaTag({ + name: "twitter:site", + content: config.twitter.site, + }) + ); + } + + if (config.twitter.handle) { + addTag( + createMetaTag({ + name: "twitter:creator", + content: config.twitter.handle, + }) + ); + } } - if (config.twitter.site) { - addTag( - createMetaTag({ name: "twitter:site", content: config.twitter.site }) - ); + // Additional Meta Tags + if (config.additionalMetaTags && config.additionalMetaTags.length > 0) { + config.additionalMetaTags.forEach((metaTag) => { + const attributes: Record = { + content: metaTag.content, + }; + + if ("name" in metaTag && metaTag.name) { + attributes.name = metaTag.name; + } else if ("property" in metaTag && metaTag.property) { + attributes.property = metaTag.property; + } else if ("httpEquiv" in metaTag && metaTag.httpEquiv) { + attributes["http-equiv"] = metaTag.httpEquiv; + } + + addTag(createMetaTag(attributes)); + }); } - if (config.twitter.handle) { - addTag( - createMetaTag({ - name: "twitter:creator", - content: config.twitter.handle, - }) - ); + // Additional Link Tags + if (config.additionalLinkTags && config.additionalLinkTags.length > 0) { + config.additionalLinkTags.forEach((linkTag) => { + const attributes: Record = { + rel: linkTag.rel, + href: linkTag.href, + }; + + if (linkTag.sizes) { + attributes.sizes = linkTag.sizes; + } + if (linkTag.media) { + attributes.media = linkTag.media; + } + if (linkTag.type) { + attributes.type = linkTag.type; + } + if (linkTag.color) { + attributes.color = linkTag.color; + } + if (linkTag.as) { + attributes.as = linkTag.as; + } + if (linkTag.crossOrigin) { + attributes.crossorigin = linkTag.crossOrigin; + } + + addTag(createLinkTag(attributes)); + }); } - } - - // Additional Meta Tags - if (config.additionalMetaTags && config.additionalMetaTags.length > 0) { - config.additionalMetaTags.forEach((metaTag) => { - const attributes: Record = { - content: metaTag.content, - }; - - if ("name" in metaTag && metaTag.name) { - attributes.name = metaTag.name; - } else if ("property" in metaTag && metaTag.property) { - attributes.property = metaTag.property; - } else if ("httpEquiv" in metaTag && metaTag.httpEquiv) { - attributes["http-equiv"] = metaTag.httpEquiv; - } - - addTag(createMetaTag(attributes)); - }); - } - - // Additional Link Tags - if (config.additionalLinkTags && config.additionalLinkTags.length > 0) { - config.additionalLinkTags.forEach((linkTag) => { - const attributes: Record = { - rel: linkTag.rel, - href: linkTag.href, - }; - - if (linkTag.sizes) { - attributes.sizes = linkTag.sizes; - } - if (linkTag.media) { - attributes.media = linkTag.media; - } - if (linkTag.type) { - attributes.type = linkTag.type; - } - if (linkTag.color) { - attributes.color = linkTag.color; - } - if (linkTag.as) { - attributes.as = linkTag.as; - } - if (linkTag.crossOrigin) { - attributes.crossorigin = linkTag.crossOrigin; - } - - addTag(createLinkTag(attributes)); - }); - } - return tagsToRender.trim(); + return tagsToRender.trim(); };