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}