diff --git a/src/lib/seo/index.ts b/src/lib/seo/index.ts new file mode 100644 index 00000000..13c1936e --- /dev/null +++ b/src/lib/seo/index.ts @@ -0,0 +1,6 @@ +/** + * SEO utilities - Schema markup and JSON-LD generators + */ + +export * from './schema-generators'; +export * from './json-ld'; diff --git a/src/lib/seo/json-ld.ts b/src/lib/seo/json-ld.ts new file mode 100644 index 00000000..8fc6deba --- /dev/null +++ b/src/lib/seo/json-ld.ts @@ -0,0 +1,25 @@ +/** + * JSON-LD utility functions for safely rendering structured data + */ + +/** + * Safely convert schema object to JSON-LD script tag + * Escapes special characters to prevent XSS + */ +export function schemaToJsonLd(schema: unknown, nonce?: string): string { + if (!schema) return ''; + + try { + const json = JSON.stringify(schema) + .replace(//g, '\\u003e') + .replace(/-->/g, '--\\u003e') + .replace(/\//g, '\\/'); + + const nonceAttr = nonce ? ` nonce="${nonce}"` : ''; + return ``; + } catch (error) { + console.error('Error generating JSON-LD tag:', error); + return ''; + } +} diff --git a/src/lib/seo/schema-generators.ts b/src/lib/seo/schema-generators.ts new file mode 100644 index 00000000..db499989 --- /dev/null +++ b/src/lib/seo/schema-generators.ts @@ -0,0 +1,294 @@ +import { site, author } from '$lib/constants/site'; + +/** + * Schema markup generators for SEO optimization + * All functions return JSON-LD structured data objects + */ + +/** + * Generate WebSite schema for homepage + */ +export function generateWebSiteSchema() { + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: site.name, + description: site.description, + url: site.url, + inLanguage: 'en-US', + copyrightYear: '2025', + copyrightHolder: { + '@type': 'Person', + name: author.name, + url: author.url, + }, + creator: { + '@type': 'Person', + name: author.name, + url: author.url, + }, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${site.url}/?search={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, + }; +} + +/** + * Generate SoftwareApplication schema for homepage + */ +export function generateHomepageSoftwareSchema() { + return { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: site.name, + description: site.longDescription, + url: site.url, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Any', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + }, + author: { + '@type': 'Person', + name: author.name, + url: author.url, + }, + softwareVersion: '3.0', + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '5', + ratingCount: '1', + }, + featureList: [ + 'IPv4 and IPv6 subnet calculator', + 'CIDR notation converter', + 'IP address format conversion', + 'Network diagnostics tools', + 'DNS record generators', + 'DHCP configuration builder', + 'Offline-first PWA', + ], + }; +} + +/** + * Generate Organization schema for homepage + */ +export function generateOrganizationSchema() { + return { + '@context': 'https://schema.org', + '@type': 'Organization', + name: site.name, + url: site.url, + logo: { + '@type': 'ImageObject', + url: `${site.url}/icon.png`, + width: '1024', + height: '1024', + }, + description: site.longDescription, + founder: { + '@type': 'Person', + name: author.name, + url: author.url, + }, + sameAs: [site.repo, site.mirror, site.docker, author.githubUrl, author.portfolio], + contactPoint: { + '@type': 'ContactPoint', + contactType: 'Developer', + url: site.repo, + }, + }; +} + +/** + * Generate SoftwareApplication schema for individual tool pages + */ +export function generateToolSchema(options: { + url: string; + title: string; + description: string; + keywords?: string[]; + category?: string; +}) { + const { url, title, description, keywords = [], category = 'DeveloperApplication' } = options; + + return { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: title, + description, + url, + applicationCategory: category, + operatingSystem: 'Any', + browserRequirements: 'Requires JavaScript. Modern browser required.', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + }, + author: { + '@type': 'Person', + name: author.name, + url: author.url, + }, + provider: { + '@type': 'Organization', + name: site.name, + url: site.url, + }, + isAccessibleForFree: true, + inLanguage: 'en-US', + keywords: keywords.join(', '), + applicationSubCategory: 'Network Tool', + }; +} + +/** + * Generate HowTo schema for tools with step-by-step usage + */ +export function generateHowToSchema(options: { + url: string; + name: string; + description: string; + steps: Array<{ name: string; text: string; image?: string }>; + toolName?: string; +}) { + const { url, name, description, steps, toolName } = options; + + return { + '@context': 'https://schema.org', + '@type': 'HowTo', + name, + description, + url, + inLanguage: 'en-US', + step: steps.map((step, index) => ({ + '@type': 'HowToStep', + position: index + 1, + name: step.name, + text: step.text, + ...(step.image && { image: step.image }), + })), + ...(toolName && { + tool: { + '@type': 'HowToTool', + name: toolName, + }, + }), + totalTime: 'PT2M', + }; +} + +/** + * Generate WebPage schema for tool pages + */ +export function generateWebPageSchema(options: { + url: string; + title: string; + description: string; + datePublished?: string; + dateModified?: string; +}) { + const { url, title, description, datePublished, dateModified } = options; + + return { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: title, + description, + url, + inLanguage: 'en-US', + isPartOf: { + '@type': 'WebSite', + name: site.name, + url: site.url, + }, + author: { + '@type': 'Person', + name: author.name, + url: author.url, + }, + publisher: { + '@type': 'Organization', + name: site.name, + url: site.url, + }, + ...(datePublished && { datePublished }), + ...(dateModified && { dateModified }), + }; +} + +interface PageDetails { + title: string; + description: string; + keywords: string[]; +} + +/** + * Smart schema generator for tool pages + * Automatically generates appropriate schemas based on page type + */ +export function generateToolPageSchemas(pageDetails: PageDetails, currentPath: string): object[] { + const url = `${site.url}${currentPath}`; + const schemas: object[] = []; + + // Add SoftwareApplication schema + schemas.push( + generateToolSchema({ + url, + title: pageDetails.title, + description: pageDetails.description || '', + keywords: pageDetails.keywords, + }), + ); + + // Add WebPage schema + schemas.push( + generateWebPageSchema({ + url, + title: pageDetails.title, + description: pageDetails.description || '', + dateModified: new Date().toISOString(), + }), + ); + + // Add HowTo schema for calculator-type tools + if ( + pageDetails.title.toLowerCase().includes('calculator') || + pageDetails.title.toLowerCase().includes('converter') || + pageDetails.title.toLowerCase().includes('generator') + ) { + schemas.push( + generateHowToSchema({ + url, + name: `How to use ${pageDetails.title}`, + description: `Step-by-step guide for using the ${pageDetails.title} tool`, + toolName: pageDetails.title, + steps: [ + { + name: 'Enter your input', + text: 'Enter the required information into the input fields', + }, + { + name: 'Review the results', + text: 'The tool automatically calculates and displays the results', + }, + { + name: 'Copy or export results', + text: 'Copy the results to your clipboard or export as needed', + }, + ], + }), + ); + } + + return schemas; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1ebff0fa..5065f1d2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -31,6 +31,15 @@ import Footer from '$lib/components/furniture/Footer.svelte'; import OfflineIndicator from '$lib/components/common/OfflineIndicator.svelte'; + // SEO Schema imports + import { schemaToJsonLd } from '$lib/seo/json-ld'; + import { + generateWebSiteSchema, + generateHomepageSoftwareSchema, + generateOrganizationSchema, + generateToolPageSchemas, + } from '$lib/seo/schema-generators'; + let { data, children } = $props(); // Gets data from the server load function let faviconTrigger = $state(0); // Trigger to force favicon updates let accessibilitySettings = $state(accessibility); // Accessibility settings store @@ -226,22 +235,16 @@ } }); - /* Uses the server-generated breadcrumb data, to build a JSON-LD breadcrumb object */ - function jsonLdTag(data: unknown, type = 'application/ld+json', nonce?: string) { - if (!data) return ''; - try { - const json = JSON.stringify(data) - .replace(//g, '\\u003e') - .replace(/-->/g, '--\\u003e') - .replace(/\//g, '\\/'); - const nonceAttr = nonce ? ` nonce="${nonce}"` : ''; - return ` @@ -302,103 +305,29 @@ + - {@html jsonLdTag(data.breadcrumbJsonLd)} + {@html schemaToJsonLd(data.breadcrumbJsonLd)} - + {#if $page.url.pathname === '/'} + - {@html jsonLdTag({ - '@context': 'https://schema.org', - '@type': 'WebSite', - name: site.name, - description: site.description, - url: site.url, - inLanguage: 'en-US', - copyrightYear: '2025', - copyrightHolder: { - '@type': 'Person', - name: author.name, - url: author.url, - }, - creator: { - '@type': 'Person', - name: author.name, - url: author.url, - }, - potentialAction: { - '@type': 'SearchAction', - target: { - '@type': 'EntryPoint', - urlTemplate: `${site.url}/?search={search_term_string}`, - }, - 'query-input': 'required name=search_term_string', - }, - })} - - + {@html schemaToJsonLd(generateWebSiteSchema())} + + - {@html jsonLdTag({ - '@context': 'https://schema.org', - '@type': 'SoftwareApplication', - name: site.name, - description: site.longDescription, - url: site.url, - applicationCategory: 'DeveloperApplication', - operatingSystem: 'Any', - offers: { - '@type': 'Offer', - price: '0', - priceCurrency: 'USD', - }, - author: { - '@type': 'Person', - name: author.name, - url: author.url, - }, - softwareVersion: '3.0', - aggregateRating: { - '@type': 'AggregateRating', - ratingValue: '5', - ratingCount: '1', - }, - featureList: [ - 'IPv4 and IPv6 subnet calculator', - 'CIDR notation converter', - 'IP address format conversion', - 'Network diagnostics tools', - 'DNS record generators', - 'DHCP configuration builder', - 'Offline-first PWA', - ], - })} - - + {@html schemaToJsonLd(generateHomepageSoftwareSchema())} + + - {@html jsonLdTag({ - '@context': 'https://schema.org', - '@type': 'Organization', - name: site.name, - url: site.url, - logo: { - '@type': 'ImageObject', - url: `${site.url}/icon.png`, - width: '1024', - height: '1024', - }, - description: site.longDescription, - founder: { - '@type': 'Person', - name: author.name, - url: author.url, - }, - sameAs: [site.repo, site.mirror, site.docker, author.githubUrl, author.portfolio], - contactPoint: { - '@type': 'ContactPoint', - contactType: 'Developer', - url: site.repo, - }, - })} + {@html schemaToJsonLd(generateOrganizationSchema())} + {:else} + + {#each toolSchemas as schema, i (i)} + + {@html schemaToJsonLd(schema)} + {/each} {/if}