All files / src/lib/seo article-schema.ts

100% Statements 10/10
100% Branches 15/15
100% Functions 4/4
100% Lines 9/9

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75                        16x 16x 16x 16x     16x       25x   16x                                                               4x 3x                                  
// Builds the schema.org Article JSON-LD object for blog posts.
// Extracted into a plain function so it can be unit-tested without Astro.
 
import type { BlogPost } from "../api/types";
 
export interface ArticleSchemaInput {
	blog: BlogPost;
	canonicalUrl: string;
	siteUrl?: string;
}
 
export function buildArticleSchema(input: ArticleSchemaInput): Record<string, unknown> {
	const { blog, canonicalUrl } = input;
	const SITE_URL = input.siteUrl ?? "https://interioring.com";
	const SITE_NAME = "Interioring";
	const LOGO_URL = `${SITE_URL}/logo.png`;
 
	// Build deduplicated keywords array: primary first, then secondaries, no empties, no dupes.
	const rawKeywords: (string | null | undefined)[] = [
		blog.primaryKeyword,
		...(blog.secondaryKeywords ?? []),
	];
	const keywords = [...new Set(rawKeywords.filter((k): k is string => Boolean(k)))];
 
	return {
		"@context": "https://schema.org",
		"@type": "Article",
		headline: blog.title,
		description: blog.metaDescription,
		image: blog.featuredImageUrl ?? `${SITE_URL}/og-image.jpg`,
		datePublished: blog.publishedAt,
		dateModified: blog.dateUpdated,
		author: {
			"@type": "Organization",
			name: SITE_NAME,
			url: SITE_URL,
			logo: {
				"@type": "ImageObject",
				url: LOGO_URL,
			},
		},
		publisher: {
			"@type": "Organization",
			name: SITE_NAME,
			url: SITE_URL,
			logo: {
				"@type": "ImageObject",
				url: LOGO_URL,
			},
		},
		mainEntityOfPage: {
			"@type": "WebPage",
			"@id": canonicalUrl,
		},
		...(blog.pros && blog.pros.length > 0 && {
			mentions: blog.pros
				.filter((v) => v.pro)
				.map((v) => ({
					"@type": "LocalBusiness",
					name: v.pro?.businessName,
					url: `${SITE_URL}/pros/${v.pro?.slug ?? v.pro?.id}`,
				})),
		}),
		...(keywords.length > 0 && {
			keywords: keywords.join(", "),
		}),
		...(blog.primaryKeyword && {
			about: {
				"@type": "Thing",
				name: blog.primaryKeyword,
			},
		}),
	};
}