Skip to content

Commit 5e2bdaf

Browse files
committed
feat: seo
1 parent 9802d53 commit 5e2bdaf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+672
-95
lines changed

src/components/Seo.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import useSeo from '../hooks/useSeo';
2+
3+
const Seo = ({ title, description, keywords, ogTitle, ogDescription, ogImage, twitterCard, twitterTitle, twitterDescription, twitterImage }) => {
4+
useSeo({
5+
title,
6+
description,
7+
keywords,
8+
ogTitle,
9+
ogDescription,
10+
ogImage,
11+
twitterCard,
12+
twitterTitle,
13+
twitterDescription,
14+
twitterImage,
15+
});
16+
17+
return null;
18+
};
19+
20+
export default Seo;

src/components/Toast.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ const Toast = ({ id, title, message, duration = 3000, type, removeToast }) => {
2626
>
2727
<div className="flex flex-col text-sm group w-max flex-grow">
2828
<span className="text-base text-red-100">{title}</span>
29-
<hr className="mt-1 mb-1 min-w-max mr-5 border-red-200" />
29+
<motion.hr
30+
initial={{ width: 0 }}
31+
animate={{ width: "100%" }}
32+
transition={{ duration: duration / 1000 }}
33+
className="mt-1 mb-1 min-w-max mr-5 border-red-200"
34+
/>
3035
<span className="text-sm text-stone-200">{message}</span>
3136
</div>
3237
<button onClick={() => removeToast(id)} className="p-2 border rounded-sm shadow-2xl border-dashed rounded-xs hover:bg-toast-background/100 ">

src/hooks/useSeo.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { useEffect } from 'react';
2+
3+
function useSeo({ title, description, keywords, ogTitle, ogDescription, ogImage, twitterCard, twitterTitle, twitterDescription, twitterImage }) {
4+
useEffect(() => {
5+
// Set document title
6+
if (title) {
7+
document.title = title;
8+
}
9+
10+
// Set meta description
11+
if (description) {
12+
const metaDescription = document.querySelector('meta[name="description"]');
13+
if (metaDescription) {
14+
metaDescription.setAttribute('content', description);
15+
} else {
16+
const newMeta = document.createElement('meta');
17+
newMeta.setAttribute('name', 'description');
18+
newMeta.setAttribute('content', description);
19+
document.head.appendChild(newMeta);
20+
}
21+
}
22+
23+
// Set meta keywords
24+
if (keywords) {
25+
const metaKeywords = document.querySelector('meta[name="keywords"]');
26+
if (metaKeywords) {
27+
metaKeywords.setAttribute('content', keywords);
28+
} else {
29+
const newMeta = document.createElement('meta');
30+
newMeta.setAttribute('name', 'keywords');
31+
newMeta.setAttribute('content', keywords);
32+
document.head.appendChild(newMeta);
33+
}
34+
}
35+
36+
// Set Open Graph meta tags
37+
if (ogTitle) {
38+
const metaOgTitle = document.querySelector('meta[property="og:title"]');
39+
if (metaOgTitle) {
40+
metaOgTitle.setAttribute('content', ogTitle);
41+
} else {
42+
const newMeta = document.createElement('meta');
43+
newMeta.setAttribute('property', 'og:title');
44+
newMeta.setAttribute('content', ogTitle);
45+
document.head.appendChild(newMeta);
46+
}
47+
}
48+
49+
if (ogDescription) {
50+
const metaOgDescription = document.querySelector('meta[property="og:description"]');
51+
if (metaOgDescription) {
52+
metaOgDescription.setAttribute('content', ogDescription);
53+
} else {
54+
const newMeta = document.createElement('meta');
55+
newMeta.setAttribute('property', 'og:description');
56+
newMeta.setAttribute('content', ogDescription);
57+
document.head.appendChild(newMeta);
58+
}
59+
}
60+
61+
if (ogImage) {
62+
const metaOgImage = document.querySelector('meta[property="og:image"]');
63+
if (metaOgImage) {
64+
metaOgImage.setAttribute('content', ogImage);
65+
} else {
66+
const newMeta = document.createElement('meta');
67+
newMeta.setAttribute('property', 'og:image');
68+
newMeta.setAttribute('content', ogImage);
69+
document.head.appendChild(newMeta);
70+
}
71+
}
72+
73+
// Set Twitter card meta tags
74+
if (twitterCard) {
75+
const metaTwitterCard = document.querySelector('meta[name="twitter:card"]');
76+
if (metaTwitterCard) {
77+
metaTwitterCard.setAttribute('content', twitterCard);
78+
} else {
79+
const newMeta = document.createElement('meta');
80+
newMeta.setAttribute('name', 'twitter:card');
81+
newMeta.setAttribute('content', twitterCard);
82+
document.head.appendChild(newMeta);
83+
}
84+
}
85+
86+
if (twitterTitle) {
87+
const metaTwitterTitle = document.querySelector('meta[name="twitter:title"]');
88+
if (metaTwitterTitle) {
89+
metaTwitterTitle.setAttribute('content', twitterTitle);
90+
} else {
91+
const newMeta = document.createElement('meta');
92+
newMeta.setAttribute('name', 'twitter:title');
93+
newMeta.setAttribute('content', twitterTitle);
94+
document.head.appendChild(newMeta);
95+
}
96+
}
97+
98+
if (twitterDescription) {
99+
const metaTwitterDescription = document.querySelector('meta[name="twitter:description"]');
100+
if (metaTwitterDescription) {
101+
metaTwitterDescription.setAttribute('content', twitterDescription);
102+
} else {
103+
const newMeta = document.createElement('meta');
104+
newMeta.setAttribute('name', 'twitter:description');
105+
newMeta.setAttribute('content', twitterDescription);
106+
document.head.appendChild(newMeta);
107+
}
108+
}
109+
110+
if (twitterImage) {
111+
const metaTwitterImage = document.querySelector('meta[name="twitter:image"]');
112+
if (metaTwitterImage) {
113+
metaTwitterImage.setAttribute('content', twitterImage);
114+
} else {
115+
const newMeta = document.createElement('meta');
116+
newMeta.setAttribute('name', 'twitter:image');
117+
newMeta.setAttribute('content', twitterImage);
118+
document.head.appendChild(newMeta);
119+
}
120+
}
121+
122+
}, [title, description, keywords, ogTitle, ogDescription, ogImage, twitterCard, twitterTitle, twitterDescription, twitterImage]);
123+
}
124+
125+
export default useSeo;

src/pages/AboutPage.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ArrowSquareOutIcon,
77
EnvelopeIcon,
88
} from '@phosphor-icons/react';
9-
import usePageTitle from '../utils/usePageTitle';
9+
import useSeo from '../hooks/useSeo';
1010

1111
const LinkRenderer = ({ href, children }) => {
1212
const isExternal = href.startsWith('http') || href.startsWith('https');
@@ -23,12 +23,24 @@ const LinkRenderer = ({ href, children }) => {
2323
};
2424

2525
const AboutPage = () => {
26-
usePageTitle('About Me');
2726
const [content, setContent] = useState('');
2827
const [email, setEmail] = useState('');
2928
const [title, setTitle] = useState('About Me');
3029
const [loading, setLoading] = useState(true);
3130

31+
useSeo({
32+
title: `${title} | Fezcodex`,
33+
description: 'Learn more about Fezcodex, the developer behind this website.',
34+
keywords: ['Fezcodex', 'about', 'portfolio', 'developer', 'software engineer'],
35+
ogTitle: `${title} | Fezcodex`,
36+
ogDescription: 'Learn more about Fezcodex, the developer behind this website.',
37+
ogImage: 'https://fezcode.github.io/logo512.png',
38+
twitterCard: 'summary_large_image',
39+
twitterTitle: `${title} | Fezcodex`,
40+
twitterDescription: 'Learn more about Fezcodex, the developer behind this website.',
41+
twitterImage: 'https://fezcode.github.io/logo512.png'
42+
});
43+
3244
useEffect(() => {
3345
const fetchAboutContent = async () => {
3446
try {

src/pages/BlogPage.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import React, { useState, useEffect } from 'react';
22
import { Link } from 'react-router-dom';
33
import PostItem from '../components/PostItem';
4-
import usePageTitle from '../utils/usePageTitle';
4+
import useSeo from '../hooks/useSeo';
55
import { ArrowLeftIcon } from '@phosphor-icons/react';
66

77
const BlogPage = () => {
8-
usePageTitle('Blog');
8+
useSeo({
9+
title: 'Blog | Fezcodex',
10+
description: 'Catch up on the latest news and insights from the Fezcodex blog.',
11+
keywords: ['Fezcodex', 'blog', 'dev', 'rant', 'series', 'd&d'],
12+
ogTitle: 'Blog | Fezcodex',
13+
ogDescription: 'Catch up on the latest news and insights from the Fezcodex blog.',
14+
ogImage: 'https://fezcode.github.io/logo512.png',
15+
twitterCard: 'summary_large_image',
16+
twitterTitle: 'Blog | Fezcodex',
17+
twitterDescription: 'Catch up on the latest news and insights from the Fezcodex blog.',
18+
twitterImage: 'https://fezcode.github.io/logo512.png'
19+
});
920
const [displayItems, setDisplayItems] = useState([]);
1021
const [loading, setLoading] = useState(true);
1122
const [activeFilter, setActiveFilter] = useState('all'); // New state for active filter

src/pages/BlogPostPage.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import PostMetadata from '../components/metadata-cards/PostMetadata';
1313
import CodeModal from '../components/CodeModal';
1414
import { useToast } from '../hooks/useToast';
1515
import ImageModal from '../components/ImageModal';
16+
import Seo from '../components/Seo';
1617
import remarkGfm from 'remark-gfm';
1718
import rehypeRaw from 'rehype-raw';
1819

@@ -312,6 +313,18 @@ const BlogPostPage = () => {
312313

313314
return (
314315
<div className="bg-gray-900 py-16 sm:py-24">
316+
<Seo
317+
title={`${post.attributes.title} | Fezcodex`}
318+
description={post.body.substring(0, 150)}
319+
keywords={post.attributes.tags ? post.attributes.tags.join(', ') : ''}
320+
ogTitle={`${post.attributes.title} | Fezcodex`}
321+
ogDescription={post.body.substring(0, 150)}
322+
ogImage={post.attributes.image || 'https://fezcode.github.io/logo512.png'}
323+
twitterCard="summary_large_image"
324+
twitterTitle={`${post.attributes.title} | Fezcodex`}
325+
twitterDescription={post.body.substring(0, 150)}
326+
twitterImage={post.attributes.image || 'https://fezcode.github.io/logo512.png'}
327+
/>
315328
<div className="mx-auto max-w-7xl px-6 lg:px-8">
316329
<div className="lg:grid lg:grid-cols-4 lg:gap-8">
317330
<div className="lg:col-span-3">

src/pages/DndBookPage.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, { useState, useEffect, useContext } from 'react';
22
import { useParams, Link } from 'react-router-dom';
33
import { motion } from 'framer-motion';
4-
import usePageTitle from '../utils/usePageTitle';
54
import '../styles/dnd.css';
65
import { DndContext } from '../context/DndContext'; // Import DndContext
76
import { parseWallpaperName } from '../utils/dndUtils'; // Import parseWallpaperName
87
import dndWallpapers from '../utils/dndWallpapers';
8+
import useSeo from "../hooks/useSeo";
99

1010
const pageVariants = {
1111
initial: {
@@ -32,6 +32,19 @@ function DndBookPage() {
3232
const [bgImage, setBgImage] = useState(''); // State for background image
3333
const { setBgImageName, setBreadcrumbs } = useContext(DndContext); // Get setBgImageName and setBreadcrumbs from context
3434

35+
useSeo({
36+
title: `${pageTitle} | From Serfs and Frauds`,
37+
description: `Explore the episodes of ${pageTitle}, a book in the From Serfs and Frauds D&D campaign.`,
38+
keywords: ['Fezcodex', 'd&d', 'dnd', 'from serfs and frauds', 'book', pageTitle],
39+
ogTitle: `${pageTitle} | From Serfs and Frauds`,
40+
ogDescription: `Explore the episodes of ${pageTitle}, a book in the From Serfs and Frauds D&D campaign.`,
41+
ogImage: 'https://fezcode.github.io/logo512.png',
42+
twitterCard: 'summary_large_image',
43+
twitterTitle: `${pageTitle} | From Serfs and Frauds`,
44+
twitterDescription: `Explore the episodes of ${pageTitle}, a book in the From Serfs and Frauds D&D campaign.`,
45+
twitterImage: 'https://fezcode.github.io/logo512.png'
46+
});
47+
3548
useEffect(() => {
3649
const fetchBookData = async () => {
3750
try {
@@ -84,8 +97,6 @@ function DndBookPage() {
8497
setBgImageName(parseWallpaperName(randomImage.split('/').pop()));
8598
}, [setBgImageName]);
8699

87-
usePageTitle(pageTitle);
88-
89100
if (!book) {
90101
return (
91102
<motion.div

src/pages/DndEpisodePage.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import React, { useState, useEffect, useContext } from 'react';
2-
import { useParams } from 'react-router-dom';
2+
import {Link, useParams} from 'react-router-dom';
33
import { motion } from 'framer-motion';
4-
import usePageTitle from '../utils/usePageTitle';
54
import '../styles/dnd.css';
6-
// import dndEpisodes from '../utils/dndEpisodes'; // Removed import
7-
import { Link } from 'react-router-dom'; // Import Link for navigation
85
import { DndContext } from '../context/DndContext'; // Import DndContext
96
import { parseWallpaperName } from '../utils/dndUtils'; // Import parseWallpaperName
107
import dndWallpapers from '../utils/dndWallpapers';
8+
import useSeo from "../hooks/useSeo";
119

1210
const pageVariants = {
1311
initial: {
@@ -35,15 +33,26 @@ function DndEpisodePage() {
3533
const [book, setBook] = useState(null); // State to store the current book
3634
const [bgImage, setBgImage] = useState(''); // State for background image
3735

36+
useSeo({
37+
title: `${episodeTitle} | From Serfs and Frauds`,
38+
description: `Read the episode "${episodeTitle}" from the Dungeons & Dragons campaign, From Serfs and Frauds.`,
39+
keywords: ['Fezcodex', 'd&d', 'dnd', 'from serfs and frauds', 'episode', episodeTitle],
40+
ogTitle: `${episodeTitle} | From Serfs and Frauds`,
41+
ogDescription: `Read the episode "${episodeTitle}" from the Dungeons & Dragons campaign, From Serfs and Frauds.`,
42+
ogImage: 'https://fezcode.github.io/logo512.png',
43+
twitterCard: 'summary_large_image',
44+
twitterTitle: `${episodeTitle} | From Serfs and Frauds`,
45+
twitterDescription: `Read the episode "${episodeTitle}" from the Dungeons & Dragons campaign, From Serfs and Frauds.`,
46+
twitterImage: 'https://fezcode.github.io/logo512.png'
47+
});
48+
3849
useEffect(() => {
3950
const images = dndWallpapers;
4051
const randomImage = images[Math.floor(Math.random() * images.length)];
4152
setBgImage(randomImage);
4253
setBgImageName(parseWallpaperName(randomImage.split('/').pop()));
4354
}, [setBgImageName]);
4455

45-
usePageTitle(episodeTitle);
46-
4756
const [allBooks, setAllBooks] = useState([]); // Renamed from allEpisodes to allBooks
4857

4958
useEffect(() => {
@@ -142,9 +151,9 @@ function DndEpisodePage() {
142151
<div className="flex flex-wrap justify-between w-[90%] max-w-[800px] mx-auto my-8 z-10 gap-4">
143152
<div className="flex-1 text-left min-w-[200px]">
144153
{prevEpisode && (
145-
<Link to={`/dnd/books/${bookId}/pages/${prevEpisode.id}`} className="dnd-episode-nav-button">
146-
&larr; Previous Episode
147-
</Link> )}
154+
<Link to={`/dnd/books/${bookId}/pages/${prevEpisode.id}`} className="dnd-episode-nav-button">
155+
&larr; Previous Episode
156+
</Link>)}
148157
</div>
149158
<div className="flex-1 text-center min-w-[200px]">
150159
<Link to="/dnd/lore" className="dnd-episode-nav-button">
@@ -153,9 +162,9 @@ function DndEpisodePage() {
153162
</div>
154163
<div className="flex-1 text-right min-w-[200px]">
155164
{nextEpisode && (
156-
<Link to={`/dnd/books/${bookId}/pages/${nextEpisode.id}`} className="dnd-episode-nav-button">
157-
Next Episode &rarr;
158-
</Link> )}
165+
<Link to={`/dnd/books/${bookId}/pages/${nextEpisode.id}`} className="dnd-episode-nav-button">
166+
Next Episode &rarr;
167+
</Link>)}
159168
</div>
160169
</div>
161170
</div>

src/pages/DndLorePage.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useState, useEffect, useContext } from 'react'; // Added useState, useEffect, and useContext
22
import { Link } from 'react-router-dom';
33
import { motion } from 'framer-motion';
4-
import usePageTitle from '../utils/usePageTitle';
54
import '../styles/dnd.css';
65
import { DndContext } from '../context/DndContext'; // Import DndContext
76
import { parseWallpaperName } from '../utils/dndUtils'; // Import parseWallpaperName
@@ -10,6 +9,7 @@ import DndCard from '../components/DndCard'; // Import DndCard
109
import Slider from 'react-slick'; // Import Slider
1110
import 'slick-carousel/slick/slick.css'; // Import slick-carousel CSS
1211
import 'slick-carousel/slick/slick-theme.css'; // Import slick-carousel theme CSS
12+
import useSeo from "../hooks/useSeo";
1313

1414
const pageVariants = {
1515
initial: {
@@ -30,7 +30,18 @@ const pageTransition = {
3030
};
3131

3232
function DndLorePage() {
33-
usePageTitle('The Lore');
33+
useSeo({
34+
title: 'The Lore | From Serfs and Frauds',
35+
description: 'Explore the world\'s history and tales from the Dungeons & Dragons campaign, From Serfs and Frauds.',
36+
keywords: ['Fezcodex', 'd&d', 'dnd', 'from serfs and frauds', 'lore', 'history', 'tales'],
37+
ogTitle: 'The Lore | From Serfs and Frauds',
38+
ogDescription: 'Explore the world\'s history and tales from the Dungeons & Dragons campaign, From Serfs and Frauds.',
39+
ogImage: 'https://fezcode.github.io/logo512.png',
40+
twitterCard: 'summary_large_image',
41+
twitterTitle: 'The Lore | From Serfs and Frauds',
42+
twitterDescription: 'Explore the world\'s history and tales from the Dungeons & Dragons campaign, From Serfs and Frauds.',
43+
twitterImage: 'https://fezcode.github.io/logo512.png'
44+
});
3445
const { setBgImageName, setBreadcrumbs } = useContext(DndContext); // Get setBgImageName and setBreadcrumbs from context
3546
const [bgImage, setBgImage] = useState(''); // State for background image
3647

0 commit comments

Comments
 (0)