This commit is contained in:
31
app/Main.tsx
31
app/Main.tsx
@@ -1,10 +1,10 @@
|
||||
import Link from '@/components/Link'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { formatDate } from 'pliny/utils/formatDate'
|
||||
import NewsletterForm from 'pliny/ui/NewsletterForm'
|
||||
import Link from '@/components/Link';
|
||||
import Tag from '@/components/Tag';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { formatDate } from 'pliny/utils/formatDate';
|
||||
import NewsletterForm from 'pliny/ui/NewsletterForm';
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
const MAX_DISPLAY = 5;
|
||||
|
||||
export default function Home({ posts }) {
|
||||
return (
|
||||
@@ -21,7 +21,7 @@ export default function Home({ posts }) {
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{!posts.length && 'No posts found.'}
|
||||
{posts.slice(0, MAX_DISPLAY).map((post) => {
|
||||
const { slug, date, title, summary, tags } = post
|
||||
const { slug, date, title, summary, tags } = post;
|
||||
return (
|
||||
<li key={slug} className="py-12">
|
||||
<article>
|
||||
@@ -29,7 +29,9 @@ export default function Home({ posts }) {
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time>
|
||||
<time dateTime={date}>
|
||||
{formatDate(date, siteMetadata.locale)}
|
||||
</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-5 xl:col-span-3">
|
||||
@@ -38,8 +40,7 @@ export default function Home({ posts }) {
|
||||
<h2 className="text-2xl font-bold leading-8 tracking-tight">
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
className="text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</Link>
|
||||
</h2>
|
||||
@@ -57,8 +58,7 @@ export default function Home({ posts }) {
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Read "${title}"`}
|
||||
>
|
||||
aria-label={`Read "${title}"`}>
|
||||
Read more →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@ export default function Home({ posts }) {
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -75,8 +75,7 @@ export default function Home({ posts }) {
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="All posts"
|
||||
>
|
||||
aria-label="All posts">
|
||||
All Posts →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -87,5 +86,5 @@ export default function Home({ posts }) {
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { Authors, allAuthors } from 'contentlayer/generated'
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
||||
import AuthorLayout from '@/layouts/AuthorLayout'
|
||||
import { coreContent } from 'pliny/utils/contentlayer'
|
||||
import { genPageMetadata } from 'app/seo'
|
||||
import { Authors, allAuthors } from 'contentlayer/generated';
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components';
|
||||
import AuthorLayout from '@/layouts/AuthorLayout';
|
||||
import { coreContent } from 'pliny/utils/contentlayer';
|
||||
import { genPageMetadata } from 'app/seo';
|
||||
|
||||
export const metadata = genPageMetadata({ title: 'About' })
|
||||
export const metadata = genPageMetadata({ title: 'About' });
|
||||
|
||||
export default function Page() {
|
||||
const author = allAuthors.find((p) => p.slug === 'default') as Authors
|
||||
const mainContent = coreContent(author)
|
||||
const author = allAuthors.find((p) => p.slug === 'default') as Authors;
|
||||
const mainContent = coreContent(author);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -16,5 +16,5 @@ export default function Page() {
|
||||
<MDXLayoutRenderer code={author.body.code} />
|
||||
</AuthorLayout>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { NewsletterAPI } from 'pliny/newsletter'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { NewsletterAPI } from 'pliny/newsletter';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
const handler = NewsletterAPI({
|
||||
// @ts-ignore
|
||||
provider: siteMetadata.newsletter.provider,
|
||||
})
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST }
|
||||
export { handler as GET, handler as POST };
|
||||
|
@@ -1,54 +1,54 @@
|
||||
import 'css/prism.css'
|
||||
import 'katex/dist/katex.css'
|
||||
import 'css/prism.css';
|
||||
import 'katex/dist/katex.css';
|
||||
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import { components } from '@/components/MDXComponents'
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
||||
import { sortPosts, coreContent } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs, allAuthors } from 'contentlayer/generated'
|
||||
import type { Authors, Blog } from 'contentlayer/generated'
|
||||
import PostSimple from '@/layouts/PostSimple'
|
||||
import PostLayout from '@/layouts/PostLayout'
|
||||
import PostBanner from '@/layouts/PostBanner'
|
||||
import { Metadata } from 'next'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import PageTitle from '@/components/PageTitle';
|
||||
import { components } from '@/components/MDXComponents';
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components';
|
||||
import { sortPosts, coreContent } from 'pliny/utils/contentlayer';
|
||||
import { allBlogs, allAuthors } from 'contentlayer/generated';
|
||||
import type { Authors, Blog } from 'contentlayer/generated';
|
||||
import PostSimple from '@/layouts/PostSimple';
|
||||
import PostLayout from '@/layouts/PostLayout';
|
||||
import PostBanner from '@/layouts/PostBanner';
|
||||
import { Metadata } from 'next';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const defaultLayout = 'PostLayout'
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const defaultLayout = 'PostLayout';
|
||||
const layouts = {
|
||||
PostSimple,
|
||||
PostLayout,
|
||||
PostBanner,
|
||||
}
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string[] }
|
||||
params: { slug: string[] };
|
||||
}): Promise<Metadata | undefined> {
|
||||
const slug = decodeURI(params.slug.join('/'))
|
||||
const post = allBlogs.find((p) => p.slug === slug)
|
||||
const authorList = post?.authors || ['default']
|
||||
const slug = decodeURI(params.slug.join('/'));
|
||||
const post = allBlogs.find((p) => p.slug === slug);
|
||||
const authorList = post?.authors || ['default'];
|
||||
const authorDetails = authorList.map((author) => {
|
||||
const authorResults = allAuthors.find((p) => p.slug === author)
|
||||
return coreContent(authorResults as Authors)
|
||||
})
|
||||
const authorResults = allAuthors.find((p) => p.slug === author);
|
||||
return coreContent(authorResults as Authors);
|
||||
});
|
||||
if (!post) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const publishedAt = new Date(post.date).toISOString()
|
||||
const modifiedAt = new Date(post.lastmod || post.date).toISOString()
|
||||
const authors = authorDetails.map((author) => author.name)
|
||||
let imageList = [siteMetadata.socialBanner]
|
||||
const publishedAt = new Date(post.date).toISOString();
|
||||
const modifiedAt = new Date(post.lastmod || post.date).toISOString();
|
||||
const authors = authorDetails.map((author) => author.name);
|
||||
let imageList = [siteMetadata.socialBanner];
|
||||
if (post.images) {
|
||||
imageList = typeof post.images === 'string' ? [post.images] : post.images
|
||||
imageList = typeof post.images === 'string' ? [post.images] : post.images;
|
||||
}
|
||||
const ogImages = imageList.map((img) => {
|
||||
return {
|
||||
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
@@ -71,37 +71,37 @@ export async function generateMetadata({
|
||||
description: post.summary,
|
||||
images: imageList,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = allBlogs.map((p) => ({ slug: p.slug.split('/') }))
|
||||
const paths = allBlogs.map((p) => ({ slug: p.slug.split('/') }));
|
||||
|
||||
return paths
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: { slug: string[] } }) {
|
||||
const slug = decodeURI(params.slug.join('/'))
|
||||
const sortedPosts = sortPosts(allBlogs) as Blog[]
|
||||
const postIndex = sortedPosts.findIndex((p) => p.slug === slug)
|
||||
const prev = coreContent(sortedPosts[postIndex + 1])
|
||||
const next = coreContent(sortedPosts[postIndex - 1])
|
||||
const post = sortedPosts.find((p) => p.slug === slug) as Blog
|
||||
const authorList = post?.authors || ['default']
|
||||
const slug = decodeURI(params.slug.join('/'));
|
||||
const sortedPosts = sortPosts(allBlogs) as Blog[];
|
||||
const postIndex = sortedPosts.findIndex((p) => p.slug === slug);
|
||||
const prev = coreContent(sortedPosts[postIndex + 1]);
|
||||
const next = coreContent(sortedPosts[postIndex - 1]);
|
||||
const post = sortedPosts.find((p) => p.slug === slug) as Blog;
|
||||
const authorList = post?.authors || ['default'];
|
||||
const authorDetails = authorList.map((author) => {
|
||||
const authorResults = allAuthors.find((p) => p.slug === author)
|
||||
return coreContent(authorResults as Authors)
|
||||
})
|
||||
const mainContent = coreContent(post)
|
||||
const jsonLd = post.structuredData
|
||||
const authorResults = allAuthors.find((p) => p.slug === author);
|
||||
return coreContent(authorResults as Authors);
|
||||
});
|
||||
const mainContent = coreContent(post);
|
||||
const jsonLd = post.structuredData;
|
||||
jsonLd['author'] = authorDetails.map((author) => {
|
||||
return {
|
||||
'@type': 'Person',
|
||||
name: author.name,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const Layout = layouts[post.layout || defaultLayout]
|
||||
const Layout = layouts[post.layout || defaultLayout];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -120,11 +120,19 @@ export default async function Page({ params }: { params: { slug: string[] } }) {
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<Layout content={mainContent} authorDetails={authorDetails} next={next} prev={prev}>
|
||||
<MDXLayoutRenderer code={post.body.code} components={components} toc={post.toc} />
|
||||
<Layout
|
||||
content={mainContent}
|
||||
authorDetails={authorDetails}
|
||||
next={next}
|
||||
prev={prev}>
|
||||
<MDXLayoutRenderer
|
||||
code={post.body.code}
|
||||
components={components}
|
||||
toc={post.toc}
|
||||
/>
|
||||
</Layout>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,23 +1,23 @@
|
||||
import ListLayout from '@/layouts/ListLayoutWithTags'
|
||||
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import { genPageMetadata } from 'app/seo'
|
||||
import ListLayout from '@/layouts/ListLayoutWithTags';
|
||||
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer';
|
||||
import { allBlogs } from 'contentlayer/generated';
|
||||
import { genPageMetadata } from 'app/seo';
|
||||
|
||||
const POSTS_PER_PAGE = 5
|
||||
const POSTS_PER_PAGE = 5;
|
||||
|
||||
export const metadata = genPageMetadata({ title: 'Blog' })
|
||||
export const metadata = genPageMetadata({ title: 'Blog' });
|
||||
|
||||
export default function BlogPage() {
|
||||
const posts = allCoreContent(sortPosts(allBlogs))
|
||||
const pageNumber = 1
|
||||
const posts = allCoreContent(sortPosts(allBlogs));
|
||||
const pageNumber = 1;
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
POSTS_PER_PAGE * pageNumber
|
||||
)
|
||||
POSTS_PER_PAGE * pageNumber,
|
||||
);
|
||||
const pagination = {
|
||||
currentPage: pageNumber,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListLayout
|
||||
@@ -26,5 +26,5 @@ export default function BlogPage() {
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,27 +1,29 @@
|
||||
import ListLayout from '@/layouts/ListLayoutWithTags'
|
||||
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import ListLayout from '@/layouts/ListLayoutWithTags';
|
||||
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer';
|
||||
import { allBlogs } from 'contentlayer/generated';
|
||||
|
||||
const POSTS_PER_PAGE = 5
|
||||
const POSTS_PER_PAGE = 5;
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const totalPages = Math.ceil(allBlogs.length / POSTS_PER_PAGE)
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({ page: (i + 1).toString() }))
|
||||
const totalPages = Math.ceil(allBlogs.length / POSTS_PER_PAGE);
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
||||
page: (i + 1).toString(),
|
||||
}));
|
||||
|
||||
return paths
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
export default function Page({ params }: { params: { page: string } }) {
|
||||
const posts = allCoreContent(sortPosts(allBlogs))
|
||||
const pageNumber = parseInt(params.page as string)
|
||||
const posts = allCoreContent(sortPosts(allBlogs));
|
||||
const pageNumber = parseInt(params.page as string);
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
POSTS_PER_PAGE * pageNumber
|
||||
)
|
||||
POSTS_PER_PAGE * pageNumber,
|
||||
);
|
||||
const pagination = {
|
||||
currentPage: pageNumber,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListLayout
|
||||
@@ -30,5 +32,5 @@ export default function Page({ params }: { params: { page: string } }) {
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
40
app/head.tsx
40
app/head.tsx
@@ -1,16 +1,42 @@
|
||||
export default function Head() {
|
||||
return (
|
||||
<>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/static/favicons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/static/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/static/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/static/favicons/safari-pinned-tab.svg"
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fff"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#000"
|
||||
/>
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,21 +1,21 @@
|
||||
import 'css/tailwind.css'
|
||||
import 'pliny/search/algolia.css'
|
||||
import 'css/tailwind.css';
|
||||
import 'pliny/search/algolia.css';
|
||||
|
||||
import { Space_Grotesk } from 'next/font/google'
|
||||
import { Analytics, AnalyticsConfig } from 'pliny/analytics'
|
||||
import { SearchProvider, SearchConfig } from 'pliny/search'
|
||||
import Header from '@/components/Header'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import Footer from '@/components/Footer'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { ThemeProviders } from './theme-providers'
|
||||
import { Metadata } from 'next'
|
||||
import { Space_Grotesk } from 'next/font/google';
|
||||
import { Analytics, AnalyticsConfig } from 'pliny/analytics';
|
||||
import { SearchProvider, SearchConfig } from 'pliny/search';
|
||||
import Header from '@/components/Header';
|
||||
import SectionContainer from '@/components/SectionContainer';
|
||||
import Footer from '@/components/Footer';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { ThemeProviders } from './theme-providers';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
const space_grotesk = Space_Grotesk({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-space-grotesk',
|
||||
})
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteMetadata.siteUrl),
|
||||
@@ -55,30 +55,62 @@ export const metadata: Metadata = {
|
||||
card: 'summary_large_image',
|
||||
images: [siteMetadata.socialBanner],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html
|
||||
lang={siteMetadata.language}
|
||||
className={`${space_grotesk.variable} scroll-smooth`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png" />
|
||||
suppressHydrationWarning>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/static/favicons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/static/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/static/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/static/favicons/safari-pinned-tab.svg"
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fff"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#000"
|
||||
/>
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
<body className="bg-white text-black antialiased dark:bg-gray-950 dark:text-white">
|
||||
<ThemeProviders>
|
||||
<Analytics analyticsConfig={siteMetadata.analytics as AnalyticsConfig} />
|
||||
<Analytics
|
||||
analyticsConfig={siteMetadata.analytics as AnalyticsConfig}
|
||||
/>
|
||||
<SectionContainer>
|
||||
<div className="flex h-screen flex-col justify-between font-sans">
|
||||
<SearchProvider searchConfig={siteMetadata.search as SearchConfig}>
|
||||
<SearchProvider
|
||||
searchConfig={siteMetadata.search as SearchConfig}>
|
||||
<Header />
|
||||
<main className="mb-auto">{children}</main>
|
||||
</SearchProvider>
|
||||
@@ -88,5 +120,5 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
</ThemeProviders>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Link from '@/components/Link'
|
||||
import Link from '@/components/Link';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
@@ -12,14 +12,15 @@ export default function NotFound() {
|
||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
||||
Sorry we couldn't find this page.
|
||||
</p>
|
||||
<p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
|
||||
<p className="mb-8">
|
||||
But dont worry, you can find plenty of other things on our homepage.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500"
|
||||
>
|
||||
className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
|
||||
Back to homepage
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
12
app/page.tsx
12
app/page.tsx
@@ -1,9 +1,9 @@
|
||||
import { sortPosts, allCoreContent } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import Main from './Main'
|
||||
import { sortPosts, allCoreContent } from 'pliny/utils/contentlayer';
|
||||
import { allBlogs } from 'contentlayer/generated';
|
||||
import Main from './Main';
|
||||
|
||||
export default async function Page() {
|
||||
const sortedPosts = sortPosts(allBlogs)
|
||||
const posts = allCoreContent(sortedPosts)
|
||||
return <Main posts={posts} />
|
||||
const sortedPosts = sortPosts(allBlogs);
|
||||
const posts = allCoreContent(sortedPosts);
|
||||
return <Main posts={posts} />;
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import projectsData from '@/data/projectsData'
|
||||
import Card from '@/components/Card'
|
||||
import { genPageMetadata } from 'app/seo'
|
||||
import projectsData from '@/data/projectsData';
|
||||
import Card from '@/components/Card';
|
||||
import { genPageMetadata } from 'app/seo';
|
||||
|
||||
export const metadata = genPageMetadata({ title: 'Projects' })
|
||||
export const metadata = genPageMetadata({ title: 'Projects' });
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
@@ -31,5 +31,5 @@ export default function Projects() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { MetadataRoute } from 'next';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
@@ -9,5 +9,5 @@ export default function robots(): MetadataRoute.Robots {
|
||||
},
|
||||
sitemap: `${siteMetadata.siteUrl}/sitemap.xml`,
|
||||
host: siteMetadata.siteUrl,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
21
app/seo.tsx
21
app/seo.tsx
@@ -1,15 +1,20 @@
|
||||
import { Metadata } from 'next'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { Metadata } from 'next';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
interface PageSEOProps {
|
||||
title: string
|
||||
description?: string
|
||||
image?: string
|
||||
title: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function genPageMetadata({ title, description, image, ...rest }: PageSEOProps): Metadata {
|
||||
export function genPageMetadata({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
...rest
|
||||
}: PageSEOProps): Metadata {
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
@@ -27,5 +32,5 @@ export function genPageMetadata({ title, description, image, ...rest }: PageSEOP
|
||||
images: image ? [image] : [siteMetadata.socialBanner],
|
||||
},
|
||||
...rest,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { MetadataRoute } from 'next';
|
||||
import { allBlogs } from 'contentlayer/generated';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const siteUrl = siteMetadata.siteUrl
|
||||
const siteUrl = siteMetadata.siteUrl;
|
||||
const blogRoutes = allBlogs.map((post) => ({
|
||||
url: `${siteUrl}/${post.path}`,
|
||||
lastModified: post.lastmod || post.date,
|
||||
}))
|
||||
}));
|
||||
|
||||
const routes = ['', 'blog', 'projects', 'tags'].map((route) => ({
|
||||
url: `${siteUrl}/${route}`,
|
||||
lastModified: new Date().toISOString().split('T')[0],
|
||||
}))
|
||||
}));
|
||||
|
||||
return [...routes, ...blogRoutes]
|
||||
return [...routes, ...blogRoutes];
|
||||
}
|
||||
|
@@ -1,14 +1,18 @@
|
||||
import { slug } from 'github-slugger'
|
||||
import { allCoreContent } from 'pliny/utils/contentlayer'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayoutWithTags'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import tagData from 'app/tag-data.json'
|
||||
import { genPageMetadata } from 'app/seo'
|
||||
import { Metadata } from 'next'
|
||||
import { slug } from 'github-slugger';
|
||||
import { allCoreContent } from 'pliny/utils/contentlayer';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import ListLayout from '@/layouts/ListLayoutWithTags';
|
||||
import { allBlogs } from 'contentlayer/generated';
|
||||
import tagData from 'app/tag-data.json';
|
||||
import { genPageMetadata } from 'app/seo';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export async function generateMetadata({ params }: { params: { tag: string } }): Promise<Metadata> {
|
||||
const tag = params.tag
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { tag: string };
|
||||
}): Promise<Metadata> {
|
||||
const tag = params.tag;
|
||||
return genPageMetadata({
|
||||
title: tag,
|
||||
description: `${siteMetadata.title} ${tag} tagged content`,
|
||||
@@ -18,26 +22,29 @@ export async function generateMetadata({ params }: { params: { tag: string } }):
|
||||
'application/rss+xml': `${siteMetadata.siteUrl}/tags/${tag}/feed.xml`,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const tagCounts = tagData as Record<string, number>
|
||||
const tagKeys = Object.keys(tagCounts)
|
||||
const tagCounts = tagData as Record<string, number>;
|
||||
const tagKeys = Object.keys(tagCounts);
|
||||
const paths = tagKeys.map((tag) => ({
|
||||
tag: tag,
|
||||
}))
|
||||
return paths
|
||||
}
|
||||
}));
|
||||
return paths;
|
||||
};
|
||||
|
||||
export default function TagPage({ params }: { params: { tag: string } }) {
|
||||
const { tag } = params
|
||||
const { tag } = params;
|
||||
// Capitalize first letter and convert space to dash
|
||||
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
|
||||
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1);
|
||||
const filteredPosts = allCoreContent(
|
||||
allBlogs.filter(
|
||||
(post) => post.draft !== true && post.tags && post.tags.map((t) => slug(t)).includes(tag)
|
||||
)
|
||||
)
|
||||
return <ListLayout posts={filteredPosts} title={title} />
|
||||
(post) =>
|
||||
post.draft !== true &&
|
||||
post.tags &&
|
||||
post.tags.map((t) => slug(t)).includes(tag),
|
||||
),
|
||||
);
|
||||
return <ListLayout posts={filteredPosts} title={title} />;
|
||||
}
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import Link from '@/components/Link'
|
||||
import Tag from '@/components/Tag'
|
||||
import { slug } from 'github-slugger'
|
||||
import tagData from 'app/tag-data.json'
|
||||
import { genPageMetadata } from 'app/seo'
|
||||
import Link from '@/components/Link';
|
||||
import Tag from '@/components/Tag';
|
||||
import { slug } from 'github-slugger';
|
||||
import tagData from 'app/tag-data.json';
|
||||
import { genPageMetadata } from 'app/seo';
|
||||
|
||||
export const metadata = genPageMetadata({ title: 'Tags', description: 'Things I blog about' })
|
||||
export const metadata = genPageMetadata({
|
||||
title: 'Tags',
|
||||
description: 'Things I blog about',
|
||||
});
|
||||
|
||||
export default async function Page() {
|
||||
const tagCounts = tagData as Record<string, number>
|
||||
const tagKeys = Object.keys(tagCounts)
|
||||
const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
|
||||
const tagCounts = tagData as Record<string, number>;
|
||||
const tagKeys = Object.keys(tagCounts);
|
||||
const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a]);
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-start divide-y divide-gray-200 dark:divide-gray-700 md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6 md:divide-y-0">
|
||||
@@ -27,15 +30,14 @@ export default async function Page() {
|
||||
<Link
|
||||
href={`/tags/${slug(t)}`}
|
||||
className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300"
|
||||
aria-label={`View posts tagged ${t}`}
|
||||
>
|
||||
aria-label={`View posts tagged ${t}`}>
|
||||
{` (${tagCounts[t]})`}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
export function ThemeProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme} enableSystem>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={siteMetadata.theme}
|
||||
enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user