Skip to content

Commit 4b92202

Browse files
committed
feat: Implement SEO enhancements with JSON-LD structured data for entities, add dynamic metadata generation, and improve sitemap configuration for better search engine visibility
1 parent 42716aa commit 4b92202

File tree

14 files changed

+1755
-810
lines changed

14 files changed

+1755
-810
lines changed

next-sitemap.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ const sitemapConfig = {
44
generateIndexSitemap: false,
55
changefreq: "monthly",
66
exclude: ["/api*"],
7+
transform: async (config, path) => {
8+
// Prioritize slug-based URLs over numeric IDs for SEO
9+
// Numeric IDs get lower priority since they're not canonical
10+
const isNumericEntityPage = /\/[a-z]+\/\d+$/.test(path)
11+
12+
return {
13+
loc: path,
14+
changefreq: config.changefreq,
15+
priority: isNumericEntityPage ? 0.5 : path === "/" ? 1.0 : 0.7,
16+
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
17+
}
18+
},
719
}
820

921
module.exports = sitemapConfig

next.config.mjs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3-
trailingSlash: undefined,
4-
async headers() {
5-
return [
6-
{
7-
source: "/api/:path*",
8-
headers: [
9-
{ key: "Access-Control-Allow-Origin", value: "*" },
10-
{ key: "Access-Control-Allow-Methods", value: "GET, OPTIONS" },
11-
{ key: "Access-Control-Allow-Headers", value: "Content-Type" },
12-
],
13-
},
14-
]
3+
output: "export",
4+
trailingSlash: false,
5+
images: {
6+
unoptimized: true,
157
},
168
}
179

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { getEntityName } from "@/lib/category-data"
2+
3+
interface EntityJsonLdProps {
4+
category: string
5+
data: {
6+
name?: string
7+
title?: string
8+
url: string
9+
[key: string]: unknown
10+
}
11+
slug: string
12+
}
13+
14+
/**
15+
* Generates JSON-LD structured data for Star Wars entities
16+
* Uses schema.org types appropriate for each category
17+
*/
18+
export function EntityJsonLd({ category, data, slug }: EntityJsonLdProps) {
19+
const name = getEntityName(data)
20+
const pageUrl = `https://swapi.info/${category}/${slug}`
21+
const apiUrl = data.url
22+
23+
// Base structured data that applies to all entities
24+
const baseData = {
25+
"@context": "https://schema.org",
26+
name,
27+
url: pageUrl,
28+
mainEntityOfPage: {
29+
"@type": "WebPage",
30+
"@id": pageUrl,
31+
},
32+
isPartOf: {
33+
"@type": "WebAPI",
34+
name: "SWAPI - Star Wars API",
35+
url: "https://swapi.info/api",
36+
},
37+
}
38+
39+
let jsonLd: Record<string, unknown>
40+
41+
switch (category) {
42+
case "people":
43+
jsonLd = {
44+
...baseData,
45+
"@type": "Person",
46+
description: `${name} is a character from the Star Wars universe.`,
47+
gender: data.gender !== "n/a" ? data.gender : undefined,
48+
birthDate: data.birth_year !== "unknown" ? data.birth_year : undefined,
49+
height:
50+
data.height !== "unknown"
51+
? {
52+
"@type": "QuantitativeValue",
53+
value: data.height,
54+
unitCode: "CMT",
55+
}
56+
: undefined,
57+
sameAs: apiUrl,
58+
}
59+
break
60+
61+
case "films":
62+
jsonLd = {
63+
...baseData,
64+
"@type": "Movie",
65+
description:
66+
data.opening_crawl?.toString().slice(0, 200) ??
67+
`${name} is a Star Wars film.`,
68+
director: data.director,
69+
producer: data.producer,
70+
datePublished: data.release_date,
71+
genre: "Science Fiction",
72+
inLanguage: "en",
73+
sameAs: apiUrl,
74+
}
75+
break
76+
77+
case "planets":
78+
jsonLd = {
79+
...baseData,
80+
"@type": "Place",
81+
description: `${name} is a planet in the Star Wars universe. Climate: ${data.climate}, Terrain: ${data.terrain}.`,
82+
additionalProperty: [
83+
{
84+
"@type": "PropertyValue",
85+
name: "Climate",
86+
value: data.climate,
87+
},
88+
{
89+
"@type": "PropertyValue",
90+
name: "Terrain",
91+
value: data.terrain,
92+
},
93+
{
94+
"@type": "PropertyValue",
95+
name: "Population",
96+
value: data.population,
97+
},
98+
],
99+
sameAs: apiUrl,
100+
}
101+
break
102+
103+
case "species":
104+
jsonLd = {
105+
...baseData,
106+
"@type": "Thing",
107+
description: `${name} is a species in the Star Wars universe. Classification: ${data.classification}.`,
108+
additionalProperty: [
109+
{
110+
"@type": "PropertyValue",
111+
name: "Classification",
112+
value: data.classification,
113+
},
114+
{
115+
"@type": "PropertyValue",
116+
name: "Language",
117+
value: data.language,
118+
},
119+
{
120+
"@type": "PropertyValue",
121+
name: "Average Height",
122+
value: data.average_height,
123+
},
124+
{
125+
"@type": "PropertyValue",
126+
name: "Average Lifespan",
127+
value: data.average_lifespan,
128+
},
129+
],
130+
sameAs: apiUrl,
131+
}
132+
break
133+
134+
case "starships":
135+
jsonLd = {
136+
...baseData,
137+
"@type": "Vehicle",
138+
description: `${name} is a starship in the Star Wars universe. Model: ${data.model}.`,
139+
model: data.model,
140+
manufacturer: {
141+
"@type": "Organization",
142+
name: data.manufacturer,
143+
},
144+
vehicleConfiguration: data.starship_class,
145+
additionalProperty: [
146+
{
147+
"@type": "PropertyValue",
148+
name: "Hyperdrive Rating",
149+
value: data.hyperdrive_rating,
150+
},
151+
{
152+
"@type": "PropertyValue",
153+
name: "MGLT",
154+
value: data.MGLT,
155+
},
156+
{
157+
"@type": "PropertyValue",
158+
name: "Crew",
159+
value: data.crew,
160+
},
161+
{
162+
"@type": "PropertyValue",
163+
name: "Passengers",
164+
value: data.passengers,
165+
},
166+
],
167+
sameAs: apiUrl,
168+
}
169+
break
170+
171+
case "vehicles":
172+
jsonLd = {
173+
...baseData,
174+
"@type": "Vehicle",
175+
description: `${name} is a vehicle in the Star Wars universe. Model: ${data.model}.`,
176+
model: data.model,
177+
manufacturer: {
178+
"@type": "Organization",
179+
name: data.manufacturer,
180+
},
181+
vehicleConfiguration: data.vehicle_class,
182+
additionalProperty: [
183+
{
184+
"@type": "PropertyValue",
185+
name: "Crew",
186+
value: data.crew,
187+
},
188+
{
189+
"@type": "PropertyValue",
190+
name: "Passengers",
191+
value: data.passengers,
192+
},
193+
{
194+
"@type": "PropertyValue",
195+
name: "Max Speed",
196+
value: data.max_atmosphering_speed,
197+
},
198+
],
199+
sameAs: apiUrl,
200+
}
201+
break
202+
203+
default:
204+
jsonLd = {
205+
...baseData,
206+
"@type": "Thing",
207+
description: `${name} from the Star Wars universe.`,
208+
sameAs: apiUrl,
209+
}
210+
}
211+
212+
return (
213+
<script
214+
type="application/ld+json"
215+
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD requires innerHTML
216+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
217+
/>
218+
)
219+
}

0 commit comments

Comments
 (0)