Skip to content

Commit 33ebf8c

Browse files
Merge pull request #42 from mechdeveloper/feature/site-improvements
Add SEO/OG tags, blog search, and Vercel Analytics
2 parents d36631a + 8e66fe1 commit 33ebf8c

6 files changed

Lines changed: 147 additions & 38 deletions

File tree

components/layout/layout.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,51 @@ import Footer from '../sections/Footer'
44
import { useRouter } from 'next/router'
55
import config from '../../lib/config';
66

7+
const twitterHandle = '@' + config.social.twitter.split('/').pop();
8+
const defaultOgImage = `${config.siteUrl}/profile.jpeg`;
9+
710
const Layout = ({ children, pageMeta }) => {
811

912
const router = useRouter();
1013

1114
const meta = {
1215
title: config.siteName,
1316
description: config.siteDescription,
17+
image: defaultOgImage,
1418
type: 'website',
1519
...pageMeta,
1620
};
1721

22+
const canonicalUrl = `${config.siteUrl}${router.asPath}`;
1823

1924
return (
2025
<>
2126
<Head>
2227
<title>{meta.title}</title>
23-
<meta name="description" content={meta.description}></meta>
24-
<link rel="icon" href='/favicon.ico'></link>
28+
<meta name="description" content={meta.description} />
29+
<link rel="canonical" href={canonicalUrl} />
30+
<link rel="icon" href='/favicon.ico' />
31+
2532
{/* Open Graph */}
26-
<meta property='og:url' content={`${config.siteUrl}${router.asPath}`}/>
27-
<meta property='og:type' content={meta.type}/>
28-
<meta property='og:site_name' content={config.siteName}/>
29-
<meta property='og:description' content={meta.description}/>
30-
<meta property='og:title' content={meta.title}/>
31-
{meta.date && (
32-
<meta property='article:published_time' content={meta.date} />
33+
<meta property="og:url" content={canonicalUrl} />
34+
<meta property="og:type" content={meta.type} />
35+
<meta property="og:site_name" content={config.siteName} />
36+
<meta property="og:title" content={meta.title} />
37+
<meta property="og:description" content={meta.description} />
38+
<meta property="og:image" content={meta.image} />
39+
{meta.type === 'article' && meta.date && (
40+
<meta property="article:published_time" content={meta.date} />
3341
)}
42+
43+
{/* Twitter Card */}
44+
<meta name="twitter:card" content="summary_large_image" />
45+
<meta name="twitter:site" content={twitterHandle} />
46+
<meta name="twitter:creator" content={twitterHandle} />
47+
<meta name="twitter:title" content={meta.title} />
48+
<meta name="twitter:description" content={meta.description} />
49+
<meta name="twitter:image" content={meta.image} />
3450
</Head>
35-
51+
3652
<div className='min-h-screen flex flex-col'>
3753
<Header />
3854
<main className='flex-grow container mx-auto px-4 sm:px-6'>{children}</main>

package-lock.json

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@mdx-js/react": "^2.3.0",
1313
"@next/mdx": "^13.2.4",
1414
"@tailwindcss/typography": "^0.5.9",
15+
"@vercel/analytics": "^2.0.1",
1516
"date-fns": "^2.29.3",
1617
"gray-matter": "^4.0.3",
1718
"highlight.js": "^11.11.1",

pages/_app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import '../styles/globals.css';
22
import { ThemeProvider } from 'next-themes';
3+
import { Analytics } from '@vercel/analytics/react';
34

45
export default function App({ Component, pageProps }) {
56
return (
67
<ThemeProvider enableSystem={true} attribute='class'>
78
<Component {...pageProps} />
9+
<Analytics />
810
</ThemeProvider>
911
);
1012
}

pages/blog/[id].js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export default function Blog({ postData }) {
3030
const pageMeta = {
3131
type: 'article',
3232
title: postData.title,
33+
description: postData.excerpt || postData.title,
34+
date: postData.date,
3335
}
3436

3537
return (

pages/blog/index.js

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,85 @@
1-
import Card from '../../components/card/card'
1+
import { useState } from 'react';
2+
import Card from '../../components/card/card';
23
import Layout from '../../components/layout/layout';
34
import { getSortedPostsData } from '../../lib/posts';
5+
import config from '../../lib/config';
46

57
export async function getStaticProps() {
6-
78
try {
89
const allPostsData = await getSortedPostsData();
9-
10-
return {
11-
props: {
12-
allPostsData,
13-
},
14-
};
15-
} catch(error) {
16-
return {
17-
props: {
18-
allPostsData: []
19-
}
20-
}
10+
return { props: { allPostsData } };
11+
} catch (error) {
12+
return { props: { allPostsData: [] } };
2113
}
22-
2314
}
2415

25-
export default function Blog ({ allPostsData }) {
16+
export default function Blog({ allPostsData }) {
17+
const [query, setQuery] = useState('');
18+
19+
const filtered = query.trim()
20+
? allPostsData.filter(
21+
(post) =>
22+
post.title?.toLowerCase().includes(query.toLowerCase()) ||
23+
post.excerpt?.toLowerCase().includes(query.toLowerCase())
24+
)
25+
: allPostsData;
26+
2627
return (
27-
<Layout>
28-
<section className='text-center pt-12 sm:pt-24 pb-16'>
29-
<h1 className='text-4xl sm:text-7xl font-bold capitalize'>
30-
Blog Posts
31-
</h1>
32-
</section>
28+
<Layout
29+
pageMeta={{
30+
title: `Blog – ${config.siteName}`,
31+
description: 'Articles on Azure, DevOps, Kubernetes, Docker, and cloud-native engineering.',
32+
}}
33+
>
34+
<section className="text-center pt-12 sm:pt-24 pb-10">
35+
<h1 className="text-4xl sm:text-7xl font-bold capitalize mb-8">Blog Posts</h1>
3336

34-
<div className='grid grid-cols-1 gap-6 sm:gap-8 max-w-screen-lg mx-auto pb-8'>
35-
{allPostsData.map(post => <Card key={post.id} {...post} />)}
36-
</div>
37-
</Layout>
38-
);
39-
};
37+
{/* Search */}
38+
<div className="relative max-w-lg mx-auto">
39+
<svg
40+
className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"
41+
fill="none"
42+
stroke="currentColor"
43+
viewBox="0 0 24 24"
44+
>
45+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z" />
46+
</svg>
47+
<input
48+
type="text"
49+
placeholder="Search posts…"
50+
value={query}
51+
onChange={(e) => setQuery(e.target.value)}
52+
className="w-full pl-10 pr-10 py-2.5 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
53+
/>
54+
{query && (
55+
<button
56+
onClick={() => setQuery('')}
57+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
58+
aria-label="Clear search"
59+
>
60+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
62+
</svg>
63+
</button>
64+
)}
65+
</div>
66+
67+
{query && (
68+
<p className="mt-3 text-sm text-gray-500 dark:text-gray-400">
69+
{filtered.length} result{filtered.length !== 1 ? 's' : ''} for &ldquo;{query}&rdquo;
70+
</p>
71+
)}
72+
</section>
4073

74+
<div className="grid grid-cols-1 gap-6 sm:gap-8 max-w-screen-lg mx-auto pb-16">
75+
{filtered.length > 0 ? (
76+
filtered.map((post) => <Card key={post.id} {...post} />)
77+
) : (
78+
<p className="text-center text-gray-500 dark:text-gray-400 py-12">
79+
No posts match &ldquo;{query}&rdquo;
80+
</p>
81+
)}
82+
</div>
83+
</Layout>
84+
);
85+
}

0 commit comments

Comments
 (0)