Skip to content

Commit 0e7c21e

Browse files
committed
Adds JSON-LD
1 parent 4b89876 commit 0e7c21e

File tree

4 files changed

+361
-107
lines changed

4 files changed

+361
-107
lines changed

src/lib/seo/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* SEO utilities - Schema markup and JSON-LD generators
3+
*/
4+
5+
export * from './schema-generators';
6+
export * from './json-ld';

src/lib/seo/json-ld.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* JSON-LD utility functions for safely rendering structured data
3+
*/
4+
5+
/**
6+
* Safely convert schema object to JSON-LD script tag
7+
* Escapes special characters to prevent XSS
8+
*/
9+
export function schemaToJsonLd(schema: unknown, nonce?: string): string {
10+
if (!schema) return '';
11+
12+
try {
13+
const json = JSON.stringify(schema)
14+
.replace(/</g, '\\u003c')
15+
.replace(/>/g, '\\u003e')
16+
.replace(/-->/g, '--\\u003e')
17+
.replace(/\//g, '\\/');
18+
19+
const nonceAttr = nonce ? ` nonce="${nonce}"` : '';
20+
return `<script type="application/ld+json"${nonceAttr}>${json}</script>`;
21+
} catch (error) {
22+
console.error('Error generating JSON-LD tag:', error);
23+
return '';
24+
}
25+
}

src/lib/seo/schema-generators.ts

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { site, author } from '$lib/constants/site';
2+
3+
/**
4+
* Schema markup generators for SEO optimization
5+
* All functions return JSON-LD structured data objects
6+
*/
7+
8+
/**
9+
* Generate WebSite schema for homepage
10+
*/
11+
export function generateWebSiteSchema() {
12+
return {
13+
'@context': 'https://schema.org',
14+
'@type': 'WebSite',
15+
name: site.name,
16+
description: site.description,
17+
url: site.url,
18+
inLanguage: 'en-US',
19+
copyrightYear: '2025',
20+
copyrightHolder: {
21+
'@type': 'Person',
22+
name: author.name,
23+
url: author.url,
24+
},
25+
creator: {
26+
'@type': 'Person',
27+
name: author.name,
28+
url: author.url,
29+
},
30+
potentialAction: {
31+
'@type': 'SearchAction',
32+
target: {
33+
'@type': 'EntryPoint',
34+
urlTemplate: `${site.url}/?search={search_term_string}`,
35+
},
36+
'query-input': 'required name=search_term_string',
37+
},
38+
};
39+
}
40+
41+
/**
42+
* Generate SoftwareApplication schema for homepage
43+
*/
44+
export function generateHomepageSoftwareSchema() {
45+
return {
46+
'@context': 'https://schema.org',
47+
'@type': 'SoftwareApplication',
48+
name: site.name,
49+
description: site.longDescription,
50+
url: site.url,
51+
applicationCategory: 'DeveloperApplication',
52+
operatingSystem: 'Any',
53+
offers: {
54+
'@type': 'Offer',
55+
price: '0',
56+
priceCurrency: 'USD',
57+
},
58+
author: {
59+
'@type': 'Person',
60+
name: author.name,
61+
url: author.url,
62+
},
63+
softwareVersion: '3.0',
64+
aggregateRating: {
65+
'@type': 'AggregateRating',
66+
ratingValue: '5',
67+
ratingCount: '1',
68+
},
69+
featureList: [
70+
'IPv4 and IPv6 subnet calculator',
71+
'CIDR notation converter',
72+
'IP address format conversion',
73+
'Network diagnostics tools',
74+
'DNS record generators',
75+
'DHCP configuration builder',
76+
'Offline-first PWA',
77+
],
78+
};
79+
}
80+
81+
/**
82+
* Generate Organization schema for homepage
83+
*/
84+
export function generateOrganizationSchema() {
85+
return {
86+
'@context': 'https://schema.org',
87+
'@type': 'Organization',
88+
name: site.name,
89+
url: site.url,
90+
logo: {
91+
'@type': 'ImageObject',
92+
url: `${site.url}/icon.png`,
93+
width: '1024',
94+
height: '1024',
95+
},
96+
description: site.longDescription,
97+
founder: {
98+
'@type': 'Person',
99+
name: author.name,
100+
url: author.url,
101+
},
102+
sameAs: [site.repo, site.mirror, site.docker, author.githubUrl, author.portfolio],
103+
contactPoint: {
104+
'@type': 'ContactPoint',
105+
contactType: 'Developer',
106+
url: site.repo,
107+
},
108+
};
109+
}
110+
111+
/**
112+
* Generate SoftwareApplication schema for individual tool pages
113+
*/
114+
export function generateToolSchema(options: {
115+
url: string;
116+
title: string;
117+
description: string;
118+
keywords?: string[];
119+
category?: string;
120+
}) {
121+
const { url, title, description, keywords = [], category = 'DeveloperApplication' } = options;
122+
123+
return {
124+
'@context': 'https://schema.org',
125+
'@type': 'SoftwareApplication',
126+
name: title,
127+
description,
128+
url,
129+
applicationCategory: category,
130+
operatingSystem: 'Any',
131+
browserRequirements: 'Requires JavaScript. Modern browser required.',
132+
offers: {
133+
'@type': 'Offer',
134+
price: '0',
135+
priceCurrency: 'USD',
136+
},
137+
author: {
138+
'@type': 'Person',
139+
name: author.name,
140+
url: author.url,
141+
},
142+
provider: {
143+
'@type': 'Organization',
144+
name: site.name,
145+
url: site.url,
146+
},
147+
isAccessibleForFree: true,
148+
inLanguage: 'en-US',
149+
keywords: keywords.join(', '),
150+
applicationSubCategory: 'Network Tool',
151+
};
152+
}
153+
154+
/**
155+
* Generate HowTo schema for tools with step-by-step usage
156+
*/
157+
export function generateHowToSchema(options: {
158+
url: string;
159+
name: string;
160+
description: string;
161+
steps: Array<{ name: string; text: string; image?: string }>;
162+
toolName?: string;
163+
}) {
164+
const { url, name, description, steps, toolName } = options;
165+
166+
return {
167+
'@context': 'https://schema.org',
168+
'@type': 'HowTo',
169+
name,
170+
description,
171+
url,
172+
inLanguage: 'en-US',
173+
step: steps.map((step, index) => ({
174+
'@type': 'HowToStep',
175+
position: index + 1,
176+
name: step.name,
177+
text: step.text,
178+
...(step.image && { image: step.image }),
179+
})),
180+
...(toolName && {
181+
tool: {
182+
'@type': 'HowToTool',
183+
name: toolName,
184+
},
185+
}),
186+
totalTime: 'PT2M',
187+
};
188+
}
189+
190+
/**
191+
* Generate WebPage schema for tool pages
192+
*/
193+
export function generateWebPageSchema(options: {
194+
url: string;
195+
title: string;
196+
description: string;
197+
datePublished?: string;
198+
dateModified?: string;
199+
}) {
200+
const { url, title, description, datePublished, dateModified } = options;
201+
202+
return {
203+
'@context': 'https://schema.org',
204+
'@type': 'WebPage',
205+
name: title,
206+
description,
207+
url,
208+
inLanguage: 'en-US',
209+
isPartOf: {
210+
'@type': 'WebSite',
211+
name: site.name,
212+
url: site.url,
213+
},
214+
author: {
215+
'@type': 'Person',
216+
name: author.name,
217+
url: author.url,
218+
},
219+
publisher: {
220+
'@type': 'Organization',
221+
name: site.name,
222+
url: site.url,
223+
},
224+
...(datePublished && { datePublished }),
225+
...(dateModified && { dateModified }),
226+
};
227+
}
228+
229+
interface PageDetails {
230+
title: string;
231+
description: string;
232+
keywords: string[];
233+
}
234+
235+
/**
236+
* Smart schema generator for tool pages
237+
* Automatically generates appropriate schemas based on page type
238+
*/
239+
export function generateToolPageSchemas(pageDetails: PageDetails, currentPath: string): object[] {
240+
const url = `${site.url}${currentPath}`;
241+
const schemas: object[] = [];
242+
243+
// Add SoftwareApplication schema
244+
schemas.push(
245+
generateToolSchema({
246+
url,
247+
title: pageDetails.title,
248+
description: pageDetails.description || '',
249+
keywords: pageDetails.keywords,
250+
}),
251+
);
252+
253+
// Add WebPage schema
254+
schemas.push(
255+
generateWebPageSchema({
256+
url,
257+
title: pageDetails.title,
258+
description: pageDetails.description || '',
259+
dateModified: new Date().toISOString(),
260+
}),
261+
);
262+
263+
// Add HowTo schema for calculator-type tools
264+
if (
265+
pageDetails.title.toLowerCase().includes('calculator') ||
266+
pageDetails.title.toLowerCase().includes('converter') ||
267+
pageDetails.title.toLowerCase().includes('generator')
268+
) {
269+
schemas.push(
270+
generateHowToSchema({
271+
url,
272+
name: `How to use ${pageDetails.title}`,
273+
description: `Step-by-step guide for using the ${pageDetails.title} tool`,
274+
toolName: pageDetails.title,
275+
steps: [
276+
{
277+
name: 'Enter your input',
278+
text: 'Enter the required information into the input fields',
279+
},
280+
{
281+
name: 'Review the results',
282+
text: 'The tool automatically calculates and displays the results',
283+
},
284+
{
285+
name: 'Copy or export results',
286+
text: 'Copy the results to your clipboard or export as needed',
287+
},
288+
],
289+
}),
290+
);
291+
}
292+
293+
return schemas;
294+
}

0 commit comments

Comments
 (0)