This commit is contained in:
parent
de1da22508
commit
3932a2b612
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
singleQuote: true,
|
|
||||||
trailingCommas: 'all',
|
|
||||||
bracketSpacing: true,
|
|
||||||
bracketSameLine: true,
|
|
||||||
};
|
|
31
app/Main.tsx
31
app/Main.tsx
@ -1,10 +1,10 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import { formatDate } from 'pliny/utils/formatDate'
|
import { formatDate } from 'pliny/utils/formatDate';
|
||||||
import NewsletterForm from 'pliny/ui/NewsletterForm'
|
import NewsletterForm from 'pliny/ui/NewsletterForm';
|
||||||
|
|
||||||
const MAX_DISPLAY = 5
|
const MAX_DISPLAY = 5;
|
||||||
|
|
||||||
export default function Home({ posts }) {
|
export default function Home({ posts }) {
|
||||||
return (
|
return (
|
||||||
@ -21,7 +21,7 @@ export default function Home({ posts }) {
|
|||||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{!posts.length && 'No posts found.'}
|
{!posts.length && 'No posts found.'}
|
||||||
{posts.slice(0, MAX_DISPLAY).map((post) => {
|
{posts.slice(0, MAX_DISPLAY).map((post) => {
|
||||||
const { slug, date, title, summary, tags } = post
|
const { slug, date, title, summary, tags } = post;
|
||||||
return (
|
return (
|
||||||
<li key={slug} className="py-12">
|
<li key={slug} className="py-12">
|
||||||
<article>
|
<article>
|
||||||
@ -29,7 +29,9 @@ export default function Home({ posts }) {
|
|||||||
<dl>
|
<dl>
|
||||||
<dt className="sr-only">Published on</dt>
|
<dt className="sr-only">Published on</dt>
|
||||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="space-y-5 xl:col-span-3">
|
<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">
|
<h2 className="text-2xl font-bold leading-8 tracking-tight">
|
||||||
<Link
|
<Link
|
||||||
href={`/blog/${slug}`}
|
href={`/blog/${slug}`}
|
||||||
className="text-gray-900 dark:text-gray-100"
|
className="text-gray-900 dark:text-gray-100">
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
@ -57,8 +58,7 @@ export default function Home({ posts }) {
|
|||||||
<Link
|
<Link
|
||||||
href={`/blog/${slug}`}
|
href={`/blog/${slug}`}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label={`Read "${title}"`}
|
aria-label={`Read "${title}"`}>
|
||||||
>
|
|
||||||
Read more →
|
Read more →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +66,7 @@ export default function Home({ posts }) {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -75,8 +75,7 @@ export default function Home({ posts }) {
|
|||||||
<Link
|
<Link
|
||||||
href="/blog"
|
href="/blog"
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label="All posts"
|
aria-label="All posts">
|
||||||
>
|
|
||||||
All Posts →
|
All Posts →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -87,5 +86,5 @@ export default function Home({ posts }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Authors, allAuthors } from 'contentlayer/generated'
|
import { Authors, allAuthors } from 'contentlayer/generated';
|
||||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
import { MDXLayoutRenderer } from 'pliny/mdx-components';
|
||||||
import AuthorLayout from '@/layouts/AuthorLayout'
|
import AuthorLayout from '@/layouts/AuthorLayout';
|
||||||
import { coreContent } from 'pliny/utils/contentlayer'
|
import { coreContent } from 'pliny/utils/contentlayer';
|
||||||
import { genPageMetadata } from 'app/seo'
|
import { genPageMetadata } from 'app/seo';
|
||||||
|
|
||||||
export const metadata = genPageMetadata({ title: 'About' })
|
export const metadata = genPageMetadata({ title: 'About' });
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const author = allAuthors.find((p) => p.slug === 'default') as Authors
|
const author = allAuthors.find((p) => p.slug === 'default') as Authors;
|
||||||
const mainContent = coreContent(author)
|
const mainContent = coreContent(author);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -16,5 +16,5 @@ export default function Page() {
|
|||||||
<MDXLayoutRenderer code={author.body.code} />
|
<MDXLayoutRenderer code={author.body.code} />
|
||||||
</AuthorLayout>
|
</AuthorLayout>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { NewsletterAPI } from 'pliny/newsletter'
|
import { NewsletterAPI } from 'pliny/newsletter';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
const handler = NewsletterAPI({
|
const handler = NewsletterAPI({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
provider: siteMetadata.newsletter.provider,
|
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 'css/prism.css';
|
||||||
import 'katex/dist/katex.css'
|
import 'katex/dist/katex.css';
|
||||||
|
|
||||||
import PageTitle from '@/components/PageTitle'
|
import PageTitle from '@/components/PageTitle';
|
||||||
import { components } from '@/components/MDXComponents'
|
import { components } from '@/components/MDXComponents';
|
||||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
import { MDXLayoutRenderer } from 'pliny/mdx-components';
|
||||||
import { sortPosts, coreContent } from 'pliny/utils/contentlayer'
|
import { sortPosts, coreContent } from 'pliny/utils/contentlayer';
|
||||||
import { allBlogs, allAuthors } from 'contentlayer/generated'
|
import { allBlogs, allAuthors } from 'contentlayer/generated';
|
||||||
import type { Authors, Blog } from 'contentlayer/generated'
|
import type { Authors, Blog } from 'contentlayer/generated';
|
||||||
import PostSimple from '@/layouts/PostSimple'
|
import PostSimple from '@/layouts/PostSimple';
|
||||||
import PostLayout from '@/layouts/PostLayout'
|
import PostLayout from '@/layouts/PostLayout';
|
||||||
import PostBanner from '@/layouts/PostBanner'
|
import PostBanner from '@/layouts/PostBanner';
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production'
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
const defaultLayout = 'PostLayout'
|
const defaultLayout = 'PostLayout';
|
||||||
const layouts = {
|
const layouts = {
|
||||||
PostSimple,
|
PostSimple,
|
||||||
PostLayout,
|
PostLayout,
|
||||||
PostBanner,
|
PostBanner,
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { slug: string[] }
|
params: { slug: string[] };
|
||||||
}): Promise<Metadata | undefined> {
|
}): Promise<Metadata | undefined> {
|
||||||
const slug = decodeURI(params.slug.join('/'))
|
const slug = decodeURI(params.slug.join('/'));
|
||||||
const post = allBlogs.find((p) => p.slug === slug)
|
const post = allBlogs.find((p) => p.slug === slug);
|
||||||
const authorList = post?.authors || ['default']
|
const authorList = post?.authors || ['default'];
|
||||||
const authorDetails = authorList.map((author) => {
|
const authorDetails = authorList.map((author) => {
|
||||||
const authorResults = allAuthors.find((p) => p.slug === author)
|
const authorResults = allAuthors.find((p) => p.slug === author);
|
||||||
return coreContent(authorResults as Authors)
|
return coreContent(authorResults as Authors);
|
||||||
})
|
});
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishedAt = new Date(post.date).toISOString()
|
const publishedAt = new Date(post.date).toISOString();
|
||||||
const modifiedAt = new Date(post.lastmod || post.date).toISOString()
|
const modifiedAt = new Date(post.lastmod || post.date).toISOString();
|
||||||
const authors = authorDetails.map((author) => author.name)
|
const authors = authorDetails.map((author) => author.name);
|
||||||
let imageList = [siteMetadata.socialBanner]
|
let imageList = [siteMetadata.socialBanner];
|
||||||
if (post.images) {
|
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) => {
|
const ogImages = imageList.map((img) => {
|
||||||
return {
|
return {
|
||||||
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
|
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
@ -71,37 +71,37 @@ export async function generateMetadata({
|
|||||||
description: post.summary,
|
description: post.summary,
|
||||||
images: imageList,
|
images: imageList,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateStaticParams = async () => {
|
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[] } }) {
|
export default async function Page({ params }: { params: { slug: string[] } }) {
|
||||||
const slug = decodeURI(params.slug.join('/'))
|
const slug = decodeURI(params.slug.join('/'));
|
||||||
const sortedPosts = sortPosts(allBlogs) as Blog[]
|
const sortedPosts = sortPosts(allBlogs) as Blog[];
|
||||||
const postIndex = sortedPosts.findIndex((p) => p.slug === slug)
|
const postIndex = sortedPosts.findIndex((p) => p.slug === slug);
|
||||||
const prev = coreContent(sortedPosts[postIndex + 1])
|
const prev = coreContent(sortedPosts[postIndex + 1]);
|
||||||
const next = coreContent(sortedPosts[postIndex - 1])
|
const next = coreContent(sortedPosts[postIndex - 1]);
|
||||||
const post = sortedPosts.find((p) => p.slug === slug) as Blog
|
const post = sortedPosts.find((p) => p.slug === slug) as Blog;
|
||||||
const authorList = post?.authors || ['default']
|
const authorList = post?.authors || ['default'];
|
||||||
const authorDetails = authorList.map((author) => {
|
const authorDetails = authorList.map((author) => {
|
||||||
const authorResults = allAuthors.find((p) => p.slug === author)
|
const authorResults = allAuthors.find((p) => p.slug === author);
|
||||||
return coreContent(authorResults as Authors)
|
return coreContent(authorResults as Authors);
|
||||||
})
|
});
|
||||||
const mainContent = coreContent(post)
|
const mainContent = coreContent(post);
|
||||||
const jsonLd = post.structuredData
|
const jsonLd = post.structuredData;
|
||||||
jsonLd['author'] = authorDetails.map((author) => {
|
jsonLd['author'] = authorDetails.map((author) => {
|
||||||
return {
|
return {
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
name: author.name,
|
name: author.name,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
const Layout = layouts[post.layout || defaultLayout]
|
const Layout = layouts[post.layout || defaultLayout];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -120,11 +120,19 @@ export default async function Page({ params }: { params: { slug: string[] } }) {
|
|||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
/>
|
/>
|
||||||
<Layout content={mainContent} authorDetails={authorDetails} next={next} prev={prev}>
|
<Layout
|
||||||
<MDXLayoutRenderer code={post.body.code} components={components} toc={post.toc} />
|
content={mainContent}
|
||||||
|
authorDetails={authorDetails}
|
||||||
|
next={next}
|
||||||
|
prev={prev}>
|
||||||
|
<MDXLayoutRenderer
|
||||||
|
code={post.body.code}
|
||||||
|
components={components}
|
||||||
|
toc={post.toc}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import ListLayout from '@/layouts/ListLayoutWithTags'
|
import ListLayout from '@/layouts/ListLayoutWithTags';
|
||||||
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer'
|
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer';
|
||||||
import { allBlogs } from 'contentlayer/generated'
|
import { allBlogs } from 'contentlayer/generated';
|
||||||
import { genPageMetadata } from 'app/seo'
|
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() {
|
export default function BlogPage() {
|
||||||
const posts = allCoreContent(sortPosts(allBlogs))
|
const posts = allCoreContent(sortPosts(allBlogs));
|
||||||
const pageNumber = 1
|
const pageNumber = 1;
|
||||||
const initialDisplayPosts = posts.slice(
|
const initialDisplayPosts = posts.slice(
|
||||||
POSTS_PER_PAGE * (pageNumber - 1),
|
POSTS_PER_PAGE * (pageNumber - 1),
|
||||||
POSTS_PER_PAGE * pageNumber
|
POSTS_PER_PAGE * pageNumber,
|
||||||
)
|
);
|
||||||
const pagination = {
|
const pagination = {
|
||||||
currentPage: pageNumber,
|
currentPage: pageNumber,
|
||||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListLayout
|
<ListLayout
|
||||||
@ -26,5 +26,5 @@ export default function BlogPage() {
|
|||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
title="All Posts"
|
title="All Posts"
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,29 @@
|
|||||||
import ListLayout from '@/layouts/ListLayoutWithTags'
|
import ListLayout from '@/layouts/ListLayoutWithTags';
|
||||||
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer'
|
import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer';
|
||||||
import { allBlogs } from 'contentlayer/generated'
|
import { allBlogs } from 'contentlayer/generated';
|
||||||
|
|
||||||
const POSTS_PER_PAGE = 5
|
const POSTS_PER_PAGE = 5;
|
||||||
|
|
||||||
export const generateStaticParams = async () => {
|
export const generateStaticParams = async () => {
|
||||||
const totalPages = Math.ceil(allBlogs.length / POSTS_PER_PAGE)
|
const totalPages = Math.ceil(allBlogs.length / POSTS_PER_PAGE);
|
||||||
const paths = Array.from({ length: totalPages }, (_, i) => ({ page: (i + 1).toString() }))
|
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
||||||
|
page: (i + 1).toString(),
|
||||||
|
}));
|
||||||
|
|
||||||
return paths
|
return paths;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Page({ params }: { params: { page: string } }) {
|
export default function Page({ params }: { params: { page: string } }) {
|
||||||
const posts = allCoreContent(sortPosts(allBlogs))
|
const posts = allCoreContent(sortPosts(allBlogs));
|
||||||
const pageNumber = parseInt(params.page as string)
|
const pageNumber = parseInt(params.page as string);
|
||||||
const initialDisplayPosts = posts.slice(
|
const initialDisplayPosts = posts.slice(
|
||||||
POSTS_PER_PAGE * (pageNumber - 1),
|
POSTS_PER_PAGE * (pageNumber - 1),
|
||||||
POSTS_PER_PAGE * pageNumber
|
POSTS_PER_PAGE * pageNumber,
|
||||||
)
|
);
|
||||||
const pagination = {
|
const pagination = {
|
||||||
currentPage: pageNumber,
|
currentPage: pageNumber,
|
||||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListLayout
|
<ListLayout
|
||||||
@ -30,5 +32,5 @@ export default function Page({ params }: { params: { page: string } }) {
|
|||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
title="All Posts"
|
title="All Posts"
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
40
app/head.tsx
40
app/head.tsx
@ -1,16 +1,42 @@
|
|||||||
export default function Head() {
|
export default function Head() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
<link
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png" />
|
rel="apple-touch-icon"
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png" />
|
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="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="msapplication-TileColor" content="#000000" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
<meta
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
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" />
|
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import 'css/tailwind.css'
|
import 'css/tailwind.css';
|
||||||
import 'pliny/search/algolia.css'
|
import 'pliny/search/algolia.css';
|
||||||
|
|
||||||
import { Space_Grotesk } from 'next/font/google'
|
import { Space_Grotesk } from 'next/font/google';
|
||||||
import { Analytics, AnalyticsConfig } from 'pliny/analytics'
|
import { Analytics, AnalyticsConfig } from 'pliny/analytics';
|
||||||
import { SearchProvider, SearchConfig } from 'pliny/search'
|
import { SearchProvider, SearchConfig } from 'pliny/search';
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header';
|
||||||
import SectionContainer from '@/components/SectionContainer'
|
import SectionContainer from '@/components/SectionContainer';
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import { ThemeProviders } from './theme-providers'
|
import { ThemeProviders } from './theme-providers';
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
const space_grotesk = Space_Grotesk({
|
const space_grotesk = Space_Grotesk({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
variable: '--font-space-grotesk',
|
variable: '--font-space-grotesk',
|
||||||
})
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL(siteMetadata.siteUrl),
|
metadataBase: new URL(siteMetadata.siteUrl),
|
||||||
@ -55,30 +55,62 @@ export const metadata: Metadata = {
|
|||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
images: [siteMetadata.socialBanner],
|
images: [siteMetadata.socialBanner],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang={siteMetadata.language}
|
lang={siteMetadata.language}
|
||||||
className={`${space_grotesk.variable} scroll-smooth`}
|
className={`${space_grotesk.variable} scroll-smooth`}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning>
|
||||||
>
|
<link
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
rel="apple-touch-icon"
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png" />
|
sizes="76x76"
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png" />
|
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="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="msapplication-TileColor" content="#000000" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
<meta
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
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" />
|
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||||
<body className="bg-white text-black antialiased dark:bg-gray-950 dark:text-white">
|
<body className="bg-white text-black antialiased dark:bg-gray-950 dark:text-white">
|
||||||
<ThemeProviders>
|
<ThemeProviders>
|
||||||
<Analytics analyticsConfig={siteMetadata.analytics as AnalyticsConfig} />
|
<Analytics
|
||||||
|
analyticsConfig={siteMetadata.analytics as AnalyticsConfig}
|
||||||
|
/>
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<div className="flex h-screen flex-col justify-between font-sans">
|
<div className="flex h-screen flex-col justify-between font-sans">
|
||||||
<SearchProvider searchConfig={siteMetadata.search as SearchConfig}>
|
<SearchProvider
|
||||||
|
searchConfig={siteMetadata.search as SearchConfig}>
|
||||||
<Header />
|
<Header />
|
||||||
<main className="mb-auto">{children}</main>
|
<main className="mb-auto">{children}</main>
|
||||||
</SearchProvider>
|
</SearchProvider>
|
||||||
@ -88,5 +120,5 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
</ThemeProviders>
|
</ThemeProviders>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
@ -12,14 +12,15 @@ export default function NotFound() {
|
|||||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
||||||
Sorry we couldn't find this page.
|
Sorry we couldn't find this page.
|
||||||
</p>
|
</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
|
<Link
|
||||||
href="/"
|
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
|
Back to homepage
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
12
app/page.tsx
12
app/page.tsx
@ -1,9 +1,9 @@
|
|||||||
import { sortPosts, allCoreContent } from 'pliny/utils/contentlayer'
|
import { sortPosts, allCoreContent } from 'pliny/utils/contentlayer';
|
||||||
import { allBlogs } from 'contentlayer/generated'
|
import { allBlogs } from 'contentlayer/generated';
|
||||||
import Main from './Main'
|
import Main from './Main';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const sortedPosts = sortPosts(allBlogs)
|
const sortedPosts = sortPosts(allBlogs);
|
||||||
const posts = allCoreContent(sortedPosts)
|
const posts = allCoreContent(sortedPosts);
|
||||||
return <Main posts={posts} />
|
return <Main posts={posts} />;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import projectsData from '@/data/projectsData'
|
import projectsData from '@/data/projectsData';
|
||||||
import Card from '@/components/Card'
|
import Card from '@/components/Card';
|
||||||
import { genPageMetadata } from 'app/seo'
|
import { genPageMetadata } from 'app/seo';
|
||||||
|
|
||||||
export const metadata = genPageMetadata({ title: 'Projects' })
|
export const metadata = genPageMetadata({ title: 'Projects' });
|
||||||
|
|
||||||
export default function Projects() {
|
export default function Projects() {
|
||||||
return (
|
return (
|
||||||
@ -31,5 +31,5 @@ export default function Projects() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MetadataRoute } from 'next'
|
import { MetadataRoute } from 'next';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
return {
|
return {
|
||||||
@ -9,5 +9,5 @@ export default function robots(): MetadataRoute.Robots {
|
|||||||
},
|
},
|
||||||
sitemap: `${siteMetadata.siteUrl}/sitemap.xml`,
|
sitemap: `${siteMetadata.siteUrl}/sitemap.xml`,
|
||||||
host: siteMetadata.siteUrl,
|
host: siteMetadata.siteUrl,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
21
app/seo.tsx
21
app/seo.tsx
@ -1,15 +1,20 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
interface PageSEOProps {
|
interface PageSEOProps {
|
||||||
title: string
|
title: string;
|
||||||
description?: string
|
description?: string;
|
||||||
image?: string
|
image?: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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 {
|
return {
|
||||||
title,
|
title,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@ -27,5 +32,5 @@ export function genPageMetadata({ title, description, image, ...rest }: PageSEOP
|
|||||||
images: image ? [image] : [siteMetadata.socialBanner],
|
images: image ? [image] : [siteMetadata.socialBanner],
|
||||||
},
|
},
|
||||||
...rest,
|
...rest,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { MetadataRoute } from 'next'
|
import { MetadataRoute } from 'next';
|
||||||
import { allBlogs } from 'contentlayer/generated'
|
import { allBlogs } from 'contentlayer/generated';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
const siteUrl = siteMetadata.siteUrl
|
const siteUrl = siteMetadata.siteUrl;
|
||||||
const blogRoutes = allBlogs.map((post) => ({
|
const blogRoutes = allBlogs.map((post) => ({
|
||||||
url: `${siteUrl}/${post.path}`,
|
url: `${siteUrl}/${post.path}`,
|
||||||
lastModified: post.lastmod || post.date,
|
lastModified: post.lastmod || post.date,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
const routes = ['', 'blog', 'projects', 'tags'].map((route) => ({
|
const routes = ['', 'blog', 'projects', 'tags'].map((route) => ({
|
||||||
url: `${siteUrl}/${route}`,
|
url: `${siteUrl}/${route}`,
|
||||||
lastModified: new Date().toISOString().split('T')[0],
|
lastModified: new Date().toISOString().split('T')[0],
|
||||||
}))
|
}));
|
||||||
|
|
||||||
return [...routes, ...blogRoutes]
|
return [...routes, ...blogRoutes];
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { slug } from 'github-slugger'
|
import { slug } from 'github-slugger';
|
||||||
import { allCoreContent } from 'pliny/utils/contentlayer'
|
import { allCoreContent } from 'pliny/utils/contentlayer';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import ListLayout from '@/layouts/ListLayoutWithTags'
|
import ListLayout from '@/layouts/ListLayoutWithTags';
|
||||||
import { allBlogs } from 'contentlayer/generated'
|
import { allBlogs } from 'contentlayer/generated';
|
||||||
import tagData from 'app/tag-data.json'
|
import tagData from 'app/tag-data.json';
|
||||||
import { genPageMetadata } from 'app/seo'
|
import { genPageMetadata } from 'app/seo';
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: { tag: string } }): Promise<Metadata> {
|
export async function generateMetadata({
|
||||||
const tag = params.tag
|
params,
|
||||||
|
}: {
|
||||||
|
params: { tag: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const tag = params.tag;
|
||||||
return genPageMetadata({
|
return genPageMetadata({
|
||||||
title: tag,
|
title: tag,
|
||||||
description: `${siteMetadata.title} ${tag} tagged content`,
|
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`,
|
'application/rss+xml': `${siteMetadata.siteUrl}/tags/${tag}/feed.xml`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateStaticParams = async () => {
|
export const generateStaticParams = async () => {
|
||||||
const tagCounts = tagData as Record<string, number>
|
const tagCounts = tagData as Record<string, number>;
|
||||||
const tagKeys = Object.keys(tagCounts)
|
const tagKeys = Object.keys(tagCounts);
|
||||||
const paths = tagKeys.map((tag) => ({
|
const paths = tagKeys.map((tag) => ({
|
||||||
tag: tag,
|
tag: tag,
|
||||||
}))
|
}));
|
||||||
return paths
|
return paths;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function TagPage({ params }: { params: { tag: string } }) {
|
export default function TagPage({ params }: { params: { tag: string } }) {
|
||||||
const { tag } = params
|
const { tag } = params;
|
||||||
// Capitalize first letter and convert space to dash
|
// 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(
|
const filteredPosts = allCoreContent(
|
||||||
allBlogs.filter(
|
allBlogs.filter(
|
||||||
(post) => post.draft !== true && post.tags && post.tags.map((t) => slug(t)).includes(tag)
|
(post) =>
|
||||||
)
|
post.draft !== true &&
|
||||||
)
|
post.tags &&
|
||||||
return <ListLayout posts={filteredPosts} title={title} />
|
post.tags.map((t) => slug(t)).includes(tag),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return <ListLayout posts={filteredPosts} title={title} />;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag';
|
||||||
import { slug } from 'github-slugger'
|
import { slug } from 'github-slugger';
|
||||||
import tagData from 'app/tag-data.json'
|
import tagData from 'app/tag-data.json';
|
||||||
import { genPageMetadata } from 'app/seo'
|
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() {
|
export default async function Page() {
|
||||||
const tagCounts = tagData as Record<string, number>
|
const tagCounts = tagData as Record<string, number>;
|
||||||
const tagKeys = Object.keys(tagCounts)
|
const tagKeys = Object.keys(tagCounts);
|
||||||
const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
|
const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a]);
|
||||||
return (
|
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">
|
<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
|
<Link
|
||||||
href={`/tags/${slug(t)}`}
|
href={`/tags/${slug(t)}`}
|
||||||
className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300"
|
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]})`}
|
{` (${tagCounts[t]})`}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
export function ThemeProviders({ children }: { children: React.ReactNode }) {
|
export function ThemeProviders({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme} enableSystem>
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme={siteMetadata.theme}
|
||||||
|
enableSystem>
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import Image from './Image'
|
import Image from './Image';
|
||||||
import Link from './Link'
|
import Link from './Link';
|
||||||
|
|
||||||
const Card = ({ title, description, imgSrc, href }) => (
|
const Card = ({ title, description, imgSrc, href }) => (
|
||||||
<div className="md max-w-[544px] p-4 md:w-1/2">
|
<div className="md max-w-[544px] p-4 md:w-1/2">
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
imgSrc && 'h-full'
|
imgSrc && 'h-full'
|
||||||
} overflow-hidden rounded-md border-2 border-gray-200 border-opacity-60 dark:border-gray-700`}
|
} overflow-hidden rounded-md border-2 border-gray-200 border-opacity-60 dark:border-gray-700`}>
|
||||||
>
|
|
||||||
{imgSrc &&
|
{imgSrc &&
|
||||||
(href ? (
|
(href ? (
|
||||||
<Link href={href} aria-label={`Link to ${title}`}>
|
<Link href={href} aria-label={`Link to ${title}`}>
|
||||||
@ -38,19 +37,20 @@ const Card = ({ title, description, imgSrc, href }) => (
|
|||||||
title
|
title
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">{description}</p>
|
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
{href && (
|
{href && (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="text-base font-medium leading-6 text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-base font-medium leading-6 text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label={`Link to ${title}`}
|
aria-label={`Link to ${title}`}>
|
||||||
>
|
|
||||||
Learn more →
|
Learn more →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default Card
|
export default Card;
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { Comments as CommentsComponent } from 'pliny/comments'
|
import { Comments as CommentsComponent } from 'pliny/comments';
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
export default function Comments({ slug }: { slug: string }) {
|
export default function Comments({ slug }: { slug: string }) {
|
||||||
const [loadComments, setLoadComments] = useState(false)
|
const [loadComments, setLoadComments] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!loadComments && <button onClick={() => setLoadComments(true)}>Load Comments</button>}
|
{!loadComments && (
|
||||||
|
<button onClick={() => setLoadComments(true)}>Load Comments</button>
|
||||||
|
)}
|
||||||
{siteMetadata.comments && loadComments && (
|
{siteMetadata.comments && loadComments && (
|
||||||
<CommentsComponent commentsConfig={siteMetadata.comments} slug={slug} />
|
<CommentsComponent commentsConfig={siteMetadata.comments} slug={slug} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import Link from './Link'
|
import Link from './Link';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import SocialIcon from '@/components/social-icons'
|
import SocialIcon from '@/components/social-icons';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer>
|
||||||
<div className="mt-16 flex flex-col items-center">
|
<div className="mt-16 flex flex-col items-center">
|
||||||
<div className="mb-3 flex space-x-4">
|
<div className="mb-3 flex space-x-4">
|
||||||
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
|
<SocialIcon
|
||||||
|
kind="mail"
|
||||||
|
href={`mailto:${siteMetadata.email}`}
|
||||||
|
size={6}
|
||||||
|
/>
|
||||||
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
|
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
|
||||||
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
|
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
|
||||||
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
|
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
|
||||||
@ -33,5 +37,5 @@ export default function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import headerNavLinks from '@/data/headerNavLinks'
|
import headerNavLinks from '@/data/headerNavLinks';
|
||||||
import Logo from '@/data/logo.svg'
|
import Logo from '@/data/logo.svg';
|
||||||
import Link from './Link'
|
import Link from './Link';
|
||||||
import MobileNav from './MobileNav'
|
import MobileNav from './MobileNav';
|
||||||
import ThemeSwitch from './ThemeSwitch'
|
import ThemeSwitch from './ThemeSwitch';
|
||||||
import SearchButton from './SearchButton'
|
import SearchButton from './SearchButton';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
return (
|
return (
|
||||||
@ -32,8 +32,7 @@ const Header = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={link.title}
|
key={link.title}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="hidden sm:block font-medium text-gray-900 dark:text-gray-100"
|
className="hidden sm:block font-medium text-gray-900 dark:text-gray-100">
|
||||||
>
|
|
||||||
{link.title}
|
{link.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@ -42,7 +41,7 @@ const Header = () => {
|
|||||||
<MobileNav />
|
<MobileNav />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Header
|
export default Header;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import NextImage, { ImageProps } from 'next/image'
|
import NextImage, { ImageProps } from 'next/image';
|
||||||
|
|
||||||
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
|
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />;
|
||||||
|
|
||||||
export default Image
|
export default Image;
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google';
|
||||||
import SectionContainer from './SectionContainer'
|
import SectionContainer from './SectionContainer';
|
||||||
import Footer from './Footer'
|
import Footer from './Footer';
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
import Header from './Header'
|
import Header from './Header';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
})
|
});
|
||||||
|
|
||||||
const LayoutWrapper = ({ children }: Props) => {
|
const LayoutWrapper = ({ children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<div className={`${inter.className} flex h-screen flex-col justify-between font-sans`}>
|
<div
|
||||||
|
className={`${inter.className} flex h-screen flex-col justify-between font-sans`}>
|
||||||
<Header />
|
<Header />
|
||||||
<main className="mb-auto">{children}</main>
|
<main className="mb-auto">{children}</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LayoutWrapper
|
export default LayoutWrapper;
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||||
import Link from 'next/link'
|
import Link from 'next/link';
|
||||||
import type { LinkProps } from 'next/link'
|
import type { LinkProps } from 'next/link';
|
||||||
import { AnchorHTMLAttributes } from 'react'
|
import { AnchorHTMLAttributes } from 'react';
|
||||||
|
|
||||||
const CustomLink = ({ href, ...rest }: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
const CustomLink = ({
|
||||||
const isInternalLink = href && href.startsWith('/')
|
href,
|
||||||
const isAnchorLink = href && href.startsWith('#')
|
...rest
|
||||||
|
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
|
const isInternalLink = href && href.startsWith('/');
|
||||||
|
const isAnchorLink = href && href.startsWith('#');
|
||||||
|
|
||||||
if (isInternalLink) {
|
if (isInternalLink) {
|
||||||
return <Link href={href} {...rest} />
|
return <Link href={href} {...rest} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnchorLink) {
|
if (isAnchorLink) {
|
||||||
return <a href={href} {...rest} />
|
return <a href={href} {...rest} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
|
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CustomLink
|
export default CustomLink;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import TOCInline from 'pliny/ui/TOCInline'
|
import TOCInline from 'pliny/ui/TOCInline';
|
||||||
import Pre from 'pliny/ui/Pre'
|
import Pre from 'pliny/ui/Pre';
|
||||||
import BlogNewsletterForm from 'pliny/ui/BlogNewsletterForm'
|
import BlogNewsletterForm from 'pliny/ui/BlogNewsletterForm';
|
||||||
import type { MDXComponents } from 'mdx/types'
|
import type { MDXComponents } from 'mdx/types';
|
||||||
import Image from './Image'
|
import Image from './Image';
|
||||||
import CustomLink from './Link'
|
import CustomLink from './Link';
|
||||||
|
|
||||||
export const components: MDXComponents = {
|
export const components: MDXComponents = {
|
||||||
Image,
|
Image,
|
||||||
@ -11,4 +11,4 @@ export const components: MDXComponents = {
|
|||||||
a: CustomLink,
|
a: CustomLink,
|
||||||
pre: Pre,
|
pre: Pre,
|
||||||
BlogNewsletterForm,
|
BlogNewsletterForm,
|
||||||
}
|
};
|
||||||
|
@ -1,33 +1,35 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import Link from './Link'
|
import Link from './Link';
|
||||||
import headerNavLinks from '@/data/headerNavLinks'
|
import headerNavLinks from '@/data/headerNavLinks';
|
||||||
|
|
||||||
const MobileNav = () => {
|
const MobileNav = () => {
|
||||||
const [navShow, setNavShow] = useState(false)
|
const [navShow, setNavShow] = useState(false);
|
||||||
|
|
||||||
const onToggleNav = () => {
|
const onToggleNav = () => {
|
||||||
setNavShow((status) => {
|
setNavShow((status) => {
|
||||||
if (status) {
|
if (status) {
|
||||||
document.body.style.overflow = 'auto'
|
document.body.style.overflow = 'auto';
|
||||||
} else {
|
} else {
|
||||||
// Prevent scrolling
|
// Prevent scrolling
|
||||||
document.body.style.overflow = 'hidden'
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
|
||||||
return !status
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return !status;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button aria-label="Toggle Menu" onClick={onToggleNav} className="sm:hidden">
|
<button
|
||||||
|
aria-label="Toggle Menu"
|
||||||
|
onClick={onToggleNav}
|
||||||
|
className="sm:hidden">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-gray-900 dark:text-gray-100 h-8 w-8"
|
className="text-gray-900 dark:text-gray-100 h-8 w-8">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||||
@ -38,16 +40,17 @@ const MobileNav = () => {
|
|||||||
<div
|
<div
|
||||||
className={`fixed left-0 top-0 z-10 h-full w-full transform opacity-95 dark:opacity-[0.98] bg-white duration-300 ease-in-out dark:bg-gray-950 ${
|
className={`fixed left-0 top-0 z-10 h-full w-full transform opacity-95 dark:opacity-[0.98] bg-white duration-300 ease-in-out dark:bg-gray-950 ${
|
||||||
navShow ? 'translate-x-0' : 'translate-x-full'
|
navShow ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}>
|
||||||
>
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button className="mr-8 mt-11 h-8 w-8" aria-label="Toggle Menu" onClick={onToggleNav}>
|
<button
|
||||||
|
className="mr-8 mt-11 h-8 w-8"
|
||||||
|
aria-label="Toggle Menu"
|
||||||
|
onClick={onToggleNav}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-gray-900 dark:text-gray-100"
|
className="text-gray-900 dark:text-gray-100">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
@ -62,8 +65,7 @@ const MobileNav = () => {
|
|||||||
<Link
|
<Link
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
|
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
|
||||||
onClick={onToggleNav}
|
onClick={onToggleNav}>
|
||||||
>
|
|
||||||
{link.title}
|
{link.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -71,7 +73,7 @@ const MobileNav = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MobileNav
|
export default MobileNav;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PageTitle({ children }: Props) {
|
export default function PageTitle({ children }: Props) {
|
||||||
@ -9,5 +9,5 @@ export default function PageTitle({ children }: Props) {
|
|||||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
|
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</h1>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const ScrollTopAndComment = () => {
|
const ScrollTopAndComment = () => {
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWindowScroll = () => {
|
const handleWindowScroll = () => {
|
||||||
if (window.scrollY > 50) setShow(true)
|
if (window.scrollY > 50) setShow(true);
|
||||||
else setShow(false)
|
else setShow(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleWindowScroll)
|
window.addEventListener('scroll', handleWindowScroll);
|
||||||
return () => window.removeEventListener('scroll', handleWindowScroll)
|
return () => window.removeEventListener('scroll', handleWindowScroll);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleScrollTop = () => {
|
const handleScrollTop = () => {
|
||||||
window.scrollTo({ top: 0 })
|
window.scrollTo({ top: 0 });
|
||||||
}
|
};
|
||||||
const handleScrollToComment = () => {
|
const handleScrollToComment = () => {
|
||||||
document.getElementById('comment')?.scrollIntoView()
|
document.getElementById('comment')?.scrollIntoView();
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed bottom-8 right-8 hidden flex-col gap-3 ${show ? 'md:flex' : 'md:hidden'}`}
|
className={`fixed bottom-8 right-8 hidden flex-col gap-3 ${
|
||||||
>
|
show ? 'md:flex' : 'md:hidden'
|
||||||
|
}`}>
|
||||||
{siteMetadata.comments?.provider && (
|
{siteMetadata.comments?.provider && (
|
||||||
<button
|
<button
|
||||||
aria-label="Scroll To Comment"
|
aria-label="Scroll To Comment"
|
||||||
onClick={handleScrollToComment}
|
onClick={handleScrollToComment}
|
||||||
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
|
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600">
|
||||||
>
|
|
||||||
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@ -44,8 +44,7 @@ const ScrollTopAndComment = () => {
|
|||||||
<button
|
<button
|
||||||
aria-label="Scroll To Top"
|
aria-label="Scroll To Top"
|
||||||
onClick={handleScrollTop}
|
onClick={handleScrollTop}
|
||||||
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
|
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600">
|
||||||
>
|
|
||||||
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@ -55,7 +54,7 @@ const ScrollTopAndComment = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ScrollTopAndComment
|
export default ScrollTopAndComment;
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { AlgoliaButton } from 'pliny/search/AlgoliaButton'
|
import { AlgoliaButton } from 'pliny/search/AlgoliaButton';
|
||||||
import { KBarButton } from 'pliny/search/KBarButton'
|
import { KBarButton } from 'pliny/search/KBarButton';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
const SearchButton = () => {
|
const SearchButton = () => {
|
||||||
if (
|
if (
|
||||||
siteMetadata.search &&
|
siteMetadata.search &&
|
||||||
(siteMetadata.search.provider === 'algolia' || siteMetadata.search.provider === 'kbar')
|
(siteMetadata.search.provider === 'algolia' ||
|
||||||
|
siteMetadata.search.provider === 'kbar')
|
||||||
) {
|
) {
|
||||||
const SearchButtonWrapper =
|
const SearchButtonWrapper =
|
||||||
siteMetadata.search.provider === 'algolia' ? AlgoliaButton : KBarButton
|
siteMetadata.search.provider === 'algolia' ? AlgoliaButton : KBarButton;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchButtonWrapper aria-label="Search">
|
<SearchButtonWrapper aria-label="Search">
|
||||||
@ -18,8 +19,7 @@ const SearchButton = () => {
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="text-gray-900 dark:text-gray-100 h-6 w-6"
|
className="text-gray-900 dark:text-gray-100 h-6 w-6">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@ -27,8 +27,8 @@ const SearchButton = () => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</SearchButtonWrapper>
|
</SearchButtonWrapper>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SearchButton
|
export default SearchButton;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SectionContainer({ children }: Props) {
|
export default function SectionContainer({ children }: Props) {
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</section>
|
<section className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">
|
||||||
)
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link';
|
||||||
import { slug } from 'github-slugger'
|
import { slug } from 'github-slugger';
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tag = ({ text }: Props) => {
|
const Tag = ({ text }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/tags/${slug(text)}`}
|
href={`/tags/${slug(text)}`}
|
||||||
className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
>
|
|
||||||
{text.split(' ').join('-')}
|
{text.split(' ').join('-')}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Tag
|
export default Tag;
|
||||||
|
@ -1,30 +1,28 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react';
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
const ThemeSwitch = () => {
|
const ThemeSwitch = () => {
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false);
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
// When mounted on client, now we can show the UI
|
// When mounted on client, now we can show the UI
|
||||||
useEffect(() => setMounted(true), [])
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label="Toggle Dark Mode"
|
aria-label="Toggle Dark Mode"
|
||||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-gray-900 dark:text-gray-100 h-6 w-6"
|
className="text-gray-900 dark:text-gray-100 h-6 w-6">
|
||||||
>
|
|
||||||
{mounted && theme === 'dark' ? (
|
{mounted && theme === 'dark' ? (
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@ -36,7 +34,7 @@ const ThemeSwitch = () => {
|
|||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ThemeSwitch
|
export default ThemeSwitch;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SVGProps } from 'react'
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
// Icons taken from: https://simpleicons.org/
|
// Icons taken from: https://simpleicons.org/
|
||||||
// To add a new icon, add a new function here and add it to components in social-icons/index.tsx
|
// To add a new icon, add a new function here and add it to components in social-icons/index.tsx
|
||||||
@ -8,7 +8,7 @@ export function Facebook(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"></path>
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Github(svgProps: SVGProps<SVGSVGElement>) {
|
export function Github(svgProps: SVGProps<SVGSVGElement>) {
|
||||||
@ -16,7 +16,7 @@ export function Github(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Linkedin(svgProps: SVGProps<SVGSVGElement>) {
|
export function Linkedin(svgProps: SVGProps<SVGSVGElement>) {
|
||||||
@ -24,7 +24,7 @@ export function Linkedin(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path>
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Mail(svgProps: SVGProps<SVGSVGElement>) {
|
export function Mail(svgProps: SVGProps<SVGSVGElement>) {
|
||||||
@ -33,7 +33,7 @@ export function Mail(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
|
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
|
||||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
|
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Twitter(svgProps: SVGProps<SVGSVGElement>) {
|
export function Twitter(svgProps: SVGProps<SVGSVGElement>) {
|
||||||
@ -41,7 +41,7 @@ export function Twitter(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"></path>
|
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Youtube(svgProps: SVGProps<SVGSVGElement>) {
|
export function Youtube(svgProps: SVGProps<SVGSVGElement>) {
|
||||||
@ -49,7 +49,7 @@ export function Youtube(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||||
<path d="M23.499 6.203a3.008 3.008 0 00-2.089-2.089c-1.87-.501-9.4-.501-9.4-.501s-7.509-.01-9.399.501a3.008 3.008 0 00-2.088 2.09A31.258 31.26 0 000 12.01a31.258 31.26 0 00.523 5.785 3.008 3.008 0 002.088 2.089c1.869.502 9.4.502 9.4.502s7.508 0 9.399-.502a3.008 3.008 0 002.089-2.09 31.258 31.26 0 00.5-5.784 31.258 31.26 0 00-.5-5.808zm-13.891 9.4V8.407l6.266 3.604z"></path>
|
<path d="M23.499 6.203a3.008 3.008 0 00-2.089-2.089c-1.87-.501-9.4-.501-9.4-.501s-7.509-.01-9.399.501a3.008 3.008 0 00-2.088 2.09A31.258 31.26 0 000 12.01a31.258 31.26 0 00.523 5.785 3.008 3.008 0 002.088 2.089c1.869.502 9.4.502 9.4.502s7.508 0 9.399-.502a3.008 3.008 0 002.089-2.09 31.258 31.26 0 00.5-5.784 31.258 31.26 0 00-.5-5.808zm-13.891 9.4V8.407l6.266 3.604z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Mastodon(svgProps: SVGProps<SVGSVGElement>) {
|
export function Mastodon(svgProps: SVGProps<SVGSVGElement>) {
|
||||||
@ -57,5 +57,5 @@ export function Mastodon(svgProps: SVGProps<SVGSVGElement>) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||||
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z" />
|
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Mail, Github, Facebook, Youtube, Linkedin, Twitter, Mastodon } from './icons'
|
import {
|
||||||
|
Mail,
|
||||||
|
Github,
|
||||||
|
Facebook,
|
||||||
|
Youtube,
|
||||||
|
Linkedin,
|
||||||
|
Twitter,
|
||||||
|
Mastodon,
|
||||||
|
} from './icons';
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
mail: Mail,
|
mail: Mail,
|
||||||
@ -8,33 +16,36 @@ const components = {
|
|||||||
linkedin: Linkedin,
|
linkedin: Linkedin,
|
||||||
twitter: Twitter,
|
twitter: Twitter,
|
||||||
mastodon: Mastodon,
|
mastodon: Mastodon,
|
||||||
}
|
};
|
||||||
|
|
||||||
type SocialIconProps = {
|
type SocialIconProps = {
|
||||||
kind: keyof typeof components
|
kind: keyof typeof components;
|
||||||
href: string | undefined
|
href: string | undefined;
|
||||||
size?: number
|
size?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
|
const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
|
||||||
if (!href || (kind === 'mail' && !/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href)))
|
if (
|
||||||
return null
|
!href ||
|
||||||
|
(kind === 'mail' &&
|
||||||
|
!/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
const SocialSvg = components[kind]
|
const SocialSvg = components[kind];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="text-sm text-gray-500 transition hover:text-gray-600"
|
className="text-sm text-gray-500 transition hover:text-gray-600"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={href}
|
href={href}>
|
||||||
>
|
|
||||||
<span className="sr-only">{kind}</span>
|
<span className="sr-only">{kind}</span>
|
||||||
<SocialSvg
|
<SocialSvg
|
||||||
className={`fill-current text-gray-700 hover:text-primary-500 dark:text-gray-200 dark:hover:text-primary-400 h-${size} w-${size}`}
|
className={`fill-current text-gray-700 hover:text-primary-500 dark:text-gray-200 dark:hover:text-primary-400 h-${size} w-${size}`}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SocialIcon
|
export default SocialIcon;
|
||||||
|
@ -21,12 +21,21 @@ const siteMetadata = {
|
|||||||
analytics: {
|
analytics: {
|
||||||
// If you want to use an analytics provider you have to add it to the
|
// If you want to use an analytics provider you have to add it to the
|
||||||
// content security policy in the `next.config.js` file.
|
// content security policy in the `next.config.js` file.
|
||||||
// supports plausible, simpleAnalytics, umami or googleAnalytics
|
// supports Plausible, Simple Analytics, Umami, Posthog or Google Analytics.
|
||||||
plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
|
umamiAnalytics: {
|
||||||
simpleAnalytics: false, // true or false
|
// We use an env variable for this site to avoid other users cloning our analytics ID
|
||||||
umamiWebsiteId: '', // e.g. 123e4567-e89b-12d3-a456-426614174000
|
umamiWebsiteId: process.env.NEXT_UMAMI_ID, // e.g. 123e4567-e89b-12d3-a456-426614174000
|
||||||
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
|
},
|
||||||
posthogAnalyticsId: '', // posthog.init e.g. phc_5yXvArzvRdqtZIsHkEm3Fkkhm3d0bEYUXCaFISzqPSQ
|
// plausibleAnalytics: {
|
||||||
|
// plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
|
||||||
|
// },
|
||||||
|
// simpleAnalytics: {},
|
||||||
|
// posthogAnalytics: {
|
||||||
|
// posthogProjectApiKey: '', // e.g. 123e4567-e89b-12d3-a456-426614174000
|
||||||
|
// },
|
||||||
|
// googleAnalytics: {
|
||||||
|
// googleAnalyticsId: '', // e.g. G-XXXXXXX
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
newsletter: {
|
newsletter: {
|
||||||
// supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus
|
// supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus
|
||||||
@ -80,6 +89,20 @@ const siteMetadata = {
|
|||||||
url: process.env.NEXT_PUBLIC_COMMENTO_URL,
|
url: process.env.NEXT_PUBLIC_COMMENTO_URL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
search: {
|
||||||
|
provider: 'kbar', // kbar or algolia
|
||||||
|
kbarConfig: {
|
||||||
|
searchDocumentsPath: 'search.json', // path to load documents to search
|
||||||
|
},
|
||||||
|
// provider: 'algolia',
|
||||||
|
// algoliaConfig: {
|
||||||
|
// // The application ID provided by Algolia
|
||||||
|
// appId: 'R2IYF7ETH7',
|
||||||
|
// // Public API key: it is safe to commit it
|
||||||
|
// apiKey: '599cec31baffa4868cae4e79f180729b',
|
||||||
|
// indexName: 'docsearch',
|
||||||
|
// },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = siteMetadata;
|
module.exports = siteMetadata;
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
import type { Authors } from 'contentlayer/generated'
|
import type { Authors } from 'contentlayer/generated';
|
||||||
import SocialIcon from '@/components/social-icons'
|
import SocialIcon from '@/components/social-icons';
|
||||||
import Image from '@/components/Image'
|
import Image from '@/components/Image';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
content: Omit<Authors, '_id' | '_raw' | 'body'>
|
content: Omit<Authors, '_id' | '_raw' | 'body'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthorLayout({ children, content }: Props) {
|
export default function AuthorLayout({ children, content }: Props) {
|
||||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = content
|
const {
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
occupation,
|
||||||
|
company,
|
||||||
|
email,
|
||||||
|
twitter,
|
||||||
|
linkedin,
|
||||||
|
github,
|
||||||
|
} = content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -30,7 +39,9 @@ export default function AuthorLayout({ children, content }: Props) {
|
|||||||
className="h-48 w-48 rounded-full"
|
className="h-48 w-48 rounded-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<h3 className="pb-2 pt-4 text-2xl font-bold leading-8 tracking-tight">{name}</h3>
|
<h3 className="pb-2 pt-4 text-2xl font-bold leading-8 tracking-tight">
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
<div className="text-gray-500 dark:text-gray-400">{occupation}</div>
|
<div className="text-gray-500 dark:text-gray-400">{occupation}</div>
|
||||||
<div className="text-gray-500 dark:text-gray-400">{company}</div>
|
<div className="text-gray-500 dark:text-gray-400">{company}</div>
|
||||||
<div className="flex space-x-3 pt-6">
|
<div className="flex space-x-3 pt-6">
|
||||||
@ -46,5 +57,5 @@ export default function AuthorLayout({ children, content }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,49 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation';
|
||||||
import { formatDate } from 'pliny/utils/formatDate'
|
import { formatDate } from 'pliny/utils/formatDate';
|
||||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
import { CoreContent } from 'pliny/utils/contentlayer';
|
||||||
import type { Blog } from 'contentlayer/generated'
|
import type { Blog } from 'contentlayer/generated';
|
||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
totalPages: number
|
totalPages: number;
|
||||||
currentPage: number
|
currentPage: number;
|
||||||
}
|
}
|
||||||
interface ListLayoutProps {
|
interface ListLayoutProps {
|
||||||
posts: CoreContent<Blog>[]
|
posts: CoreContent<Blog>[];
|
||||||
title: string
|
title: string;
|
||||||
initialDisplayPosts?: CoreContent<Blog>[]
|
initialDisplayPosts?: CoreContent<Blog>[];
|
||||||
pagination?: PaginationProps
|
pagination?: PaginationProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pagination({ totalPages, currentPage }: PaginationProps) {
|
function Pagination({ totalPages, currentPage }: PaginationProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
const basePath = pathname.split('/')[1]
|
const basePath = pathname.split('/')[1];
|
||||||
const prevPage = currentPage - 1 > 0
|
const prevPage = currentPage - 1 > 0;
|
||||||
const nextPage = currentPage + 1 <= totalPages
|
const nextPage = currentPage + 1 <= totalPages;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||||
<nav className="flex justify-between">
|
<nav className="flex justify-between">
|
||||||
{!prevPage && (
|
{!prevPage && (
|
||||||
<button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
<button
|
||||||
|
className="cursor-auto disabled:opacity-50"
|
||||||
|
disabled={!prevPage}>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{prevPage && (
|
{prevPage && (
|
||||||
<Link
|
<Link
|
||||||
href={currentPage - 1 === 1 ? `/${basePath}/` : `/${basePath}/page/${currentPage - 1}`}
|
href={
|
||||||
rel="prev"
|
currentPage - 1 === 1
|
||||||
>
|
? `/${basePath}/`
|
||||||
|
: `/${basePath}/page/${currentPage - 1}`
|
||||||
|
}
|
||||||
|
rel="prev">
|
||||||
Previous
|
Previous
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@ -46,7 +51,9 @@ function Pagination({ totalPages, currentPage }: PaginationProps) {
|
|||||||
{currentPage} of {totalPages}
|
{currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
{!nextPage && (
|
{!nextPage && (
|
||||||
<button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
<button
|
||||||
|
className="cursor-auto disabled:opacity-50"
|
||||||
|
disabled={!nextPage}>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -57,7 +64,7 @@ function Pagination({ totalPages, currentPage }: PaginationProps) {
|
|||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListLayout({
|
export default function ListLayout({
|
||||||
@ -66,15 +73,17 @@ export default function ListLayout({
|
|||||||
initialDisplayPosts = [],
|
initialDisplayPosts = [],
|
||||||
pagination,
|
pagination,
|
||||||
}: ListLayoutProps) {
|
}: ListLayoutProps) {
|
||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const filteredBlogPosts = posts.filter((post) => {
|
const filteredBlogPosts = posts.filter((post) => {
|
||||||
const searchContent = post.title + post.summary + post.tags?.join(' ')
|
const searchContent = post.title + post.summary + post.tags?.join(' ');
|
||||||
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
|
return searchContent.toLowerCase().includes(searchValue.toLowerCase());
|
||||||
})
|
});
|
||||||
|
|
||||||
// If initialDisplayPosts exist, display it if no searchValue is specified
|
// If initialDisplayPosts exist, display it if no searchValue is specified
|
||||||
const displayPosts =
|
const displayPosts =
|
||||||
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
|
initialDisplayPosts.length > 0 && !searchValue
|
||||||
|
? initialDisplayPosts
|
||||||
|
: filteredBlogPosts;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -99,8 +108,7 @@ export default function ListLayout({
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@ -113,20 +121,24 @@ export default function ListLayout({
|
|||||||
<ul>
|
<ul>
|
||||||
{!filteredBlogPosts.length && 'No posts found.'}
|
{!filteredBlogPosts.length && 'No posts found.'}
|
||||||
{displayPosts.map((post) => {
|
{displayPosts.map((post) => {
|
||||||
const { path, date, title, summary, tags } = post
|
const { path, date, title, summary, tags } = post;
|
||||||
return (
|
return (
|
||||||
<li key={path} className="py-4">
|
<li key={path} className="py-4">
|
||||||
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
|
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
|
||||||
<dl>
|
<dl>
|
||||||
<dt className="sr-only">Published on</dt>
|
<dt className="sr-only">Published on</dt>
|
||||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="space-y-3 xl:col-span-3">
|
<div className="space-y-3 xl:col-span-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-bold leading-8 tracking-tight">
|
<h3 className="text-2xl font-bold leading-8 tracking-tight">
|
||||||
<Link href={`/${path}`} className="text-gray-900 dark:text-gray-100">
|
<Link
|
||||||
|
href={`/${path}`}
|
||||||
|
className="text-gray-900 dark:text-gray-100">
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
@ -140,13 +152,16 @@ export default function ListLayout({
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{pagination && pagination.totalPages > 1 && !searchValue && (
|
{pagination && pagination.totalPages > 1 && !searchValue && (
|
||||||
<Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
|
<Pagination
|
||||||
|
currentPage={pagination.currentPage}
|
||||||
|
totalPages={pagination.totalPages}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,51 @@
|
|||||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation';
|
||||||
import { slug } from 'github-slugger'
|
import { slug } from 'github-slugger';
|
||||||
import { formatDate } from 'pliny/utils/formatDate'
|
import { formatDate } from 'pliny/utils/formatDate';
|
||||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
import { CoreContent } from 'pliny/utils/contentlayer';
|
||||||
import type { Blog } from 'contentlayer/generated'
|
import type { Blog } from 'contentlayer/generated';
|
||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import tagData from 'app/tag-data.json'
|
import tagData from 'app/tag-data.json';
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
totalPages: number
|
totalPages: number;
|
||||||
currentPage: number
|
currentPage: number;
|
||||||
}
|
}
|
||||||
interface ListLayoutProps {
|
interface ListLayoutProps {
|
||||||
posts: CoreContent<Blog>[]
|
posts: CoreContent<Blog>[];
|
||||||
title: string
|
title: string;
|
||||||
initialDisplayPosts?: CoreContent<Blog>[]
|
initialDisplayPosts?: CoreContent<Blog>[];
|
||||||
pagination?: PaginationProps
|
pagination?: PaginationProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pagination({ totalPages, currentPage }: PaginationProps) {
|
function Pagination({ totalPages, currentPage }: PaginationProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
const basePath = pathname.split('/')[1]
|
const basePath = pathname.split('/')[1];
|
||||||
const prevPage = currentPage - 1 > 0
|
const prevPage = currentPage - 1 > 0;
|
||||||
const nextPage = currentPage + 1 <= totalPages
|
const nextPage = currentPage + 1 <= totalPages;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||||
<nav className="flex justify-between">
|
<nav className="flex justify-between">
|
||||||
{!prevPage && (
|
{!prevPage && (
|
||||||
<button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
<button
|
||||||
|
className="cursor-auto disabled:opacity-50"
|
||||||
|
disabled={!prevPage}>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{prevPage && (
|
{prevPage && (
|
||||||
<Link
|
<Link
|
||||||
href={currentPage - 1 === 1 ? `/${basePath}/` : `/${basePath}/page/${currentPage - 1}`}
|
href={
|
||||||
rel="prev"
|
currentPage - 1 === 1
|
||||||
>
|
? `/${basePath}/`
|
||||||
|
: `/${basePath}/page/${currentPage - 1}`
|
||||||
|
}
|
||||||
|
rel="prev">
|
||||||
Previous
|
Previous
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@ -48,7 +53,9 @@ function Pagination({ totalPages, currentPage }: PaginationProps) {
|
|||||||
{currentPage} of {totalPages}
|
{currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
{!nextPage && (
|
{!nextPage && (
|
||||||
<button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
<button
|
||||||
|
className="cursor-auto disabled:opacity-50"
|
||||||
|
disabled={!nextPage}>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -59,7 +66,7 @@ function Pagination({ totalPages, currentPage }: PaginationProps) {
|
|||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListLayoutWithTags({
|
export default function ListLayoutWithTags({
|
||||||
@ -68,12 +75,13 @@ export default function ListLayoutWithTags({
|
|||||||
initialDisplayPosts = [],
|
initialDisplayPosts = [],
|
||||||
pagination,
|
pagination,
|
||||||
}: ListLayoutProps) {
|
}: ListLayoutProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
const tagCounts = tagData as Record<string, number>
|
const tagCounts = tagData as Record<string, number>;
|
||||||
const tagKeys = Object.keys(tagCounts)
|
const tagKeys = Object.keys(tagCounts);
|
||||||
const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
|
const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a]);
|
||||||
|
|
||||||
const displayPosts = initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
|
const displayPosts =
|
||||||
|
initialDisplayPosts.length > 0 ? initialDisplayPosts : posts;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -87,12 +95,13 @@ export default function ListLayoutWithTags({
|
|||||||
<div className="hidden max-h-screen h-full sm:flex flex-wrap bg-gray-50 dark:bg-gray-900/70 shadow-md pt-5 dark:shadow-gray-800/40 rounded min-w-[280px] max-w-[280px]">
|
<div className="hidden max-h-screen h-full sm:flex flex-wrap bg-gray-50 dark:bg-gray-900/70 shadow-md pt-5 dark:shadow-gray-800/40 rounded min-w-[280px] max-w-[280px]">
|
||||||
<div className="py-4 px-6">
|
<div className="py-4 px-6">
|
||||||
{pathname.startsWith('/blog') ? (
|
{pathname.startsWith('/blog') ? (
|
||||||
<h3 className="text-primary-500 font-bold uppercase">All Posts</h3>
|
<h3 className="text-primary-500 font-bold uppercase">
|
||||||
|
All Posts
|
||||||
|
</h3>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/blog`}
|
href={`/blog`}
|
||||||
className="font-bold uppercase text-gray-700 dark:text-gray-300 hover:text-primary-500 dark:hover:text-primary-500"
|
className="font-bold uppercase text-gray-700 dark:text-gray-300 hover:text-primary-500 dark:hover:text-primary-500">
|
||||||
>
|
|
||||||
All Posts
|
All Posts
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@ -108,13 +117,12 @@ export default function ListLayoutWithTags({
|
|||||||
<Link
|
<Link
|
||||||
href={`/tags/${slug(t)}`}
|
href={`/tags/${slug(t)}`}
|
||||||
className="py-2 px-3 uppercase text-sm font-medium text-gray-500 dark:text-gray-300 hover:text-primary-500 dark:hover:text-primary-500"
|
className="py-2 px-3 uppercase text-sm font-medium text-gray-500 dark:text-gray-300 hover:text-primary-500 dark:hover:text-primary-500"
|
||||||
aria-label={`View posts tagged ${t}`}
|
aria-label={`View posts tagged ${t}`}>
|
||||||
>
|
|
||||||
{`${t} (${tagCounts[t]})`}
|
{`${t} (${tagCounts[t]})`}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -122,20 +130,24 @@ export default function ListLayoutWithTags({
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{displayPosts.map((post) => {
|
{displayPosts.map((post) => {
|
||||||
const { path, date, title, summary, tags } = post
|
const { path, date, title, summary, tags } = post;
|
||||||
return (
|
return (
|
||||||
<li key={path} className="py-5">
|
<li key={path} className="py-5">
|
||||||
<article className="space-y-2 flex flex-col xl:space-y-0">
|
<article className="space-y-2 flex flex-col xl:space-y-0">
|
||||||
<dl>
|
<dl>
|
||||||
<dt className="sr-only">Published on</dt>
|
<dt className="sr-only">Published on</dt>
|
||||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold leading-8 tracking-tight">
|
<h2 className="text-2xl font-bold leading-8 tracking-tight">
|
||||||
<Link href={`/${path}`} className="text-gray-900 dark:text-gray-100">
|
<Link
|
||||||
|
href={`/${path}`}
|
||||||
|
className="text-gray-900 dark:text-gray-100">
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
@ -149,15 +161,18 @@ export default function ListLayoutWithTags({
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
{pagination && pagination.totalPages > 1 && (
|
{pagination && pagination.totalPages > 1 && (
|
||||||
<Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
|
<Pagination
|
||||||
|
currentPage={pagination.currentPage}
|
||||||
|
totalPages={pagination.totalPages}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,33 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
import Image from '@/components/Image'
|
import Image from '@/components/Image';
|
||||||
import Bleed from 'pliny/ui/Bleed'
|
import Bleed from 'pliny/ui/Bleed';
|
||||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
import { CoreContent } from 'pliny/utils/contentlayer';
|
||||||
import type { Blog } from 'contentlayer/generated'
|
import type { Blog } from 'contentlayer/generated';
|
||||||
import Comments from '@/components/Comments'
|
import Comments from '@/components/Comments';
|
||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import PageTitle from '@/components/PageTitle'
|
import PageTitle from '@/components/PageTitle';
|
||||||
import SectionContainer from '@/components/SectionContainer'
|
import SectionContainer from '@/components/SectionContainer';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
content: CoreContent<Blog>
|
content: CoreContent<Blog>;
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
next?: { path: string; title: string }
|
next?: { path: string; title: string };
|
||||||
prev?: { path: string; title: string }
|
prev?: { path: string; title: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostMinimal({ content, next, prev, children }: LayoutProps) {
|
export default function PostMinimal({
|
||||||
const { slug, title, images } = content
|
content,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
children,
|
||||||
|
}: LayoutProps) {
|
||||||
|
const { slug, title, images } = content;
|
||||||
const displayImage =
|
const displayImage =
|
||||||
images && images.length > 0 ? images[0] : 'https://picsum.photos/seed/picsum/800/400'
|
images && images.length > 0
|
||||||
|
? images[0]
|
||||||
|
: 'https://picsum.photos/seed/picsum/800/400';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
@ -31,7 +38,12 @@ export default function PostMinimal({ content, next, prev, children }: LayoutPro
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Bleed>
|
<Bleed>
|
||||||
<div className="aspect-[2/1] w-full relative">
|
<div className="aspect-[2/1] w-full relative">
|
||||||
<Image src={displayImage} alt={title} fill className="object-cover" />
|
<Image
|
||||||
|
src={displayImage}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Bleed>
|
</Bleed>
|
||||||
</div>
|
</div>
|
||||||
@ -39,9 +51,13 @@ export default function PostMinimal({ content, next, prev, children }: LayoutPro
|
|||||||
<PageTitle>{title}</PageTitle>
|
<PageTitle>{title}</PageTitle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="prose max-w-none py-4 dark:prose-invert">{children}</div>
|
<div className="prose max-w-none py-4 dark:prose-invert">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
{siteMetadata.comments && (
|
{siteMetadata.comments && (
|
||||||
<div className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300" id="comment">
|
<div
|
||||||
|
className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300"
|
||||||
|
id="comment">
|
||||||
<Comments slug={slug} />
|
<Comments slug={slug} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -52,8 +68,7 @@ export default function PostMinimal({ content, next, prev, children }: LayoutPro
|
|||||||
<Link
|
<Link
|
||||||
href={`/${prev.path}`}
|
href={`/${prev.path}`}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label={`Previous post: ${prev.title}`}
|
aria-label={`Previous post: ${prev.title}`}>
|
||||||
>
|
|
||||||
← {prev.title}
|
← {prev.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -63,8 +78,7 @@ export default function PostMinimal({ content, next, prev, children }: LayoutPro
|
|||||||
<Link
|
<Link
|
||||||
href={`/${next.path}`}
|
href={`/${next.path}`}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label={`Next post: ${next.title}`}
|
aria-label={`Next post: ${next.title}`}>
|
||||||
>
|
|
||||||
{next.title} →
|
{next.title} →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -74,5 +88,5 @@ export default function PostMinimal({ content, next, prev, children }: LayoutPro
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
import { CoreContent } from 'pliny/utils/contentlayer';
|
||||||
import type { Blog, Authors } from 'contentlayer/generated'
|
import type { Blog, Authors } from 'contentlayer/generated';
|
||||||
import Comments from '@/components/Comments'
|
import Comments from '@/components/Comments';
|
||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import PageTitle from '@/components/PageTitle'
|
import PageTitle from '@/components/PageTitle';
|
||||||
import SectionContainer from '@/components/SectionContainer'
|
import SectionContainer from '@/components/SectionContainer';
|
||||||
import Image from '@/components/Image'
|
import Image from '@/components/Image';
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
|
||||||
|
|
||||||
const editUrl = (path) => `${siteMetadata.siteRepo}/raw/branch/master/data/${path}`
|
const editUrl = (path) =>
|
||||||
|
`${siteMetadata.siteRepo}/raw/branch/master/data/${path}`;
|
||||||
|
|
||||||
const Copyright = () => (
|
const Copyright = () => (
|
||||||
<a
|
<a
|
||||||
rel="license"
|
rel="license"
|
||||||
href="http://creativecommons.org/licenses/by-sa/4.0/"
|
href="http://creativecommons.org/licenses/by-sa/4.0/"
|
||||||
className="inline-flex self-center"
|
className="inline-flex self-center">
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
className="border-0"
|
className="border-0"
|
||||||
alt="知识共享许可协议"
|
alt="知识共享许可协议"
|
||||||
@ -26,26 +26,32 @@ const Copyright = () => (
|
|||||||
src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png"
|
src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
|
|
||||||
const postDateTemplate: Intl.DateTimeFormatOptions = {
|
const postDateTemplate: Intl.DateTimeFormatOptions = {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
}
|
};
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
content: CoreContent<Blog>
|
content: CoreContent<Blog>;
|
||||||
authorDetails: CoreContent<Authors>[]
|
authorDetails: CoreContent<Authors>[];
|
||||||
next?: { path: string; title: string }
|
next?: { path: string; title: string };
|
||||||
prev?: { path: string; title: string }
|
prev?: { path: string; title: string };
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostLayout({ content, authorDetails, next, prev, children }: LayoutProps) {
|
export default function PostLayout({
|
||||||
const { filePath, path, slug, date, title, tags } = content
|
content,
|
||||||
const basePath = path.split('/')[0]
|
authorDetails,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
children,
|
||||||
|
}: LayoutProps) {
|
||||||
|
const { filePath, path, slug, date, title, tags } = content;
|
||||||
|
const basePath = path.split('/')[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
@ -59,7 +65,10 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
<dt className="sr-only">Published on</dt>
|
<dt className="sr-only">Published on</dt>
|
||||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||||
<time dateTime={date}>
|
<time dateTime={date}>
|
||||||
{new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
|
{new Date(date).toLocaleDateString(
|
||||||
|
siteMetadata.locale,
|
||||||
|
postDateTemplate,
|
||||||
|
)}
|
||||||
</time>
|
</time>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +84,9 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
<dd>
|
<dd>
|
||||||
<ul className="flex flex-wrap justify-center gap-4 sm:space-x-12 xl:block xl:space-x-0 xl:space-y-8">
|
<ul className="flex flex-wrap justify-center gap-4 sm:space-x-12 xl:block xl:space-x-0 xl:space-y-8">
|
||||||
{authorDetails.map((author) => (
|
{authorDetails.map((author) => (
|
||||||
<li className="flex items-center space-x-2" key={author.name}>
|
<li
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
key={author.name}>
|
||||||
{author.avatar && (
|
{author.avatar && (
|
||||||
<Image
|
<Image
|
||||||
src={author.avatar}
|
src={author.avatar}
|
||||||
@ -87,15 +98,19 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
)}
|
)}
|
||||||
<dl className="whitespace-nowrap text-sm font-medium leading-5">
|
<dl className="whitespace-nowrap text-sm font-medium leading-5">
|
||||||
<dt className="sr-only">Name</dt>
|
<dt className="sr-only">Name</dt>
|
||||||
<dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
|
<dd className="text-gray-900 dark:text-gray-100">
|
||||||
|
{author.name}
|
||||||
|
</dd>
|
||||||
<dt className="sr-only">Twitter</dt>
|
<dt className="sr-only">Twitter</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{author.twitter && (
|
{author.twitter && (
|
||||||
<Link
|
<Link
|
||||||
href={author.twitter}
|
href={author.twitter}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||||
>
|
{author.twitter.replace(
|
||||||
{author.twitter.replace('https://twitter.com/', '@')}
|
'https://twitter.com/',
|
||||||
|
'@',
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
@ -106,7 +121,9 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
||||||
<div className="prose max-w-none pb-8 pt-10 dark:prose-invert">{children}</div>
|
<div className="prose max-w-none pb-8 pt-10 dark:prose-invert">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
<div className="pb-6 pt-6 text-sm text-gray-700 dark:text-gray-300 flex items-center gap-4 ">
|
<div className="pb-6 pt-6 text-sm text-gray-700 dark:text-gray-300 flex items-center gap-4 ">
|
||||||
<Copyright />
|
<Copyright />
|
||||||
<Link href={editUrl(filePath)}>{'View source'}</Link>
|
<Link href={editUrl(filePath)}>{'View source'}</Link>
|
||||||
@ -114,8 +131,7 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
{siteMetadata.comments && (
|
{siteMetadata.comments && (
|
||||||
<div
|
<div
|
||||||
className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300"
|
className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300"
|
||||||
id="comment"
|
id="comment">
|
||||||
>
|
|
||||||
<Comments slug={slug} />
|
<Comments slug={slug} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -163,8 +179,7 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
<Link
|
<Link
|
||||||
href={`/${basePath}`}
|
href={`/${basePath}`}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label="Back to the blog"
|
aria-label="Back to the blog">
|
||||||
>
|
|
||||||
← Back to the blog
|
← Back to the blog
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -173,5 +188,5 @@ export default function PostLayout({ content, authorDetails, next, prev, childre
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,28 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react';
|
||||||
import { formatDate } from 'pliny/utils/formatDate'
|
import { formatDate } from 'pliny/utils/formatDate';
|
||||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
import { CoreContent } from 'pliny/utils/contentlayer';
|
||||||
import type { Blog } from 'contentlayer/generated'
|
import type { Blog } from 'contentlayer/generated';
|
||||||
import Comments from '@/components/Comments'
|
import Comments from '@/components/Comments';
|
||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link';
|
||||||
import PageTitle from '@/components/PageTitle'
|
import PageTitle from '@/components/PageTitle';
|
||||||
import SectionContainer from '@/components/SectionContainer'
|
import SectionContainer from '@/components/SectionContainer';
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata';
|
||||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
content: CoreContent<Blog>
|
content: CoreContent<Blog>;
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
next?: { path: string; title: string }
|
next?: { path: string; title: string };
|
||||||
prev?: { path: string; title: string }
|
prev?: { path: string; title: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostLayout({ content, next, prev, children }: LayoutProps) {
|
export default function PostLayout({
|
||||||
const { path, slug, date, title } = content
|
content,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
children,
|
||||||
|
}: LayoutProps) {
|
||||||
|
const { path, slug, date, title } = content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
@ -30,7 +35,9 @@ export default function PostLayout({ content, next, prev, children }: LayoutProp
|
|||||||
<div>
|
<div>
|
||||||
<dt className="sr-only">Published on</dt>
|
<dt className="sr-only">Published on</dt>
|
||||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
<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>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
@ -41,10 +48,14 @@ export default function PostLayout({ content, next, prev, children }: LayoutProp
|
|||||||
</header>
|
</header>
|
||||||
<div className="grid-rows-[auto_1fr] divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:divide-y-0">
|
<div className="grid-rows-[auto_1fr] divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:divide-y-0">
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
||||||
<div className="prose max-w-none pb-8 pt-10 dark:prose-invert">{children}</div>
|
<div className="prose max-w-none pb-8 pt-10 dark:prose-invert">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{siteMetadata.comments && (
|
{siteMetadata.comments && (
|
||||||
<div className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300" id="comment">
|
<div
|
||||||
|
className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300"
|
||||||
|
id="comment">
|
||||||
<Comments slug={slug} />
|
<Comments slug={slug} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -55,8 +66,7 @@ export default function PostLayout({ content, next, prev, children }: LayoutProp
|
|||||||
<Link
|
<Link
|
||||||
href={`/${prev.path}`}
|
href={`/${prev.path}`}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label={`Previous post: ${prev.title}`}
|
aria-label={`Previous post: ${prev.title}`}>
|
||||||
>
|
|
||||||
← {prev.title}
|
← {prev.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -66,8 +76,7 @@ export default function PostLayout({ content, next, prev, children }: LayoutProp
|
|||||||
<Link
|
<Link
|
||||||
href={`/${next.path}`}
|
href={`/${next.path}`}
|
||||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
aria-label={`Next post: ${next.title}`}
|
aria-label={`Next post: ${next.title}`}>
|
||||||
>
|
|
||||||
{next.title} →
|
{next.title} →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -78,5 +87,5 @@ export default function PostLayout({ content, next, prev, children }: LayoutProp
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false,
|
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
printWidth: 100,
|
trailingCommas: 'all',
|
||||||
tabWidth: 2,
|
|
||||||
useTabs: false,
|
|
||||||
trailingComma: 'es5',
|
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
}
|
bracketSameLine: true,
|
||||||
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import rss from './rss.mjs'
|
import rss from './rss.mjs';
|
||||||
|
|
||||||
async function postbuild() {
|
async function postbuild() {
|
||||||
await rss()
|
await rss();
|
||||||
}
|
}
|
||||||
|
|
||||||
postbuild()
|
postbuild();
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { writeFileSync, mkdirSync } from 'fs'
|
import { writeFileSync, mkdirSync } from 'fs';
|
||||||
import path from 'path'
|
import path from 'path';
|
||||||
import GithubSlugger from 'github-slugger'
|
import GithubSlugger from 'github-slugger';
|
||||||
import { escape } from 'pliny/utils/htmlEscaper.js'
|
import { escape } from 'pliny/utils/htmlEscaper.js';
|
||||||
import siteMetadata from '../data/siteMetadata.js'
|
import siteMetadata from '../data/siteMetadata.js';
|
||||||
import tagData from '../app/tag-data.json' assert { type: 'json' }
|
import tagData from '../app/tag-data.json' assert { type: 'json' };
|
||||||
import { allBlogs } from '../.contentlayer/generated/index.mjs'
|
import { allBlogs } from '../.contentlayer/generated/index.mjs';
|
||||||
|
|
||||||
const generateRssItem = (config, post) => `
|
const generateRssItem = (config, post) => `
|
||||||
<item>
|
<item>
|
||||||
@ -16,7 +16,7 @@ const generateRssItem = (config, post) => `
|
|||||||
<author>${config.email} (${config.author})</author>
|
<author>${config.email} (${config.author})</author>
|
||||||
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
|
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
|
||||||
</item>
|
</item>
|
||||||
`
|
`;
|
||||||
|
|
||||||
const generateRss = (config, posts, page = 'feed.xml') => `
|
const generateRss = (config, posts, page = 'feed.xml') => `
|
||||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
@ -28,35 +28,37 @@ const generateRss = (config, posts, page = 'feed.xml') => `
|
|||||||
<managingEditor>${config.email} (${config.author})</managingEditor>
|
<managingEditor>${config.email} (${config.author})</managingEditor>
|
||||||
<webMaster>${config.email} (${config.author})</webMaster>
|
<webMaster>${config.email} (${config.author})</webMaster>
|
||||||
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
|
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
|
||||||
<atom:link href="${config.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
|
<atom:link href="${
|
||||||
|
config.siteUrl
|
||||||
|
}/${page}" rel="self" type="application/rss+xml"/>
|
||||||
${posts.map((post) => generateRssItem(config, post)).join('')}
|
${posts.map((post) => generateRssItem(config, post)).join('')}
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`
|
`;
|
||||||
|
|
||||||
async function generateRSS(config, allBlogs, page = 'feed.xml') {
|
async function generateRSS(config, allBlogs, page = 'feed.xml') {
|
||||||
const publishPosts = allBlogs.filter((post) => post.draft !== true)
|
const publishPosts = allBlogs.filter((post) => post.draft !== true);
|
||||||
// RSS for blog post
|
// RSS for blog post
|
||||||
if (publishPosts.length > 0) {
|
if (publishPosts.length > 0) {
|
||||||
const rss = generateRss(config, publishPosts)
|
const rss = generateRss(config, publishPosts);
|
||||||
writeFileSync(`./public/${page}`, rss)
|
writeFileSync(`./public/${page}`, rss);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publishPosts.length > 0) {
|
if (publishPosts.length > 0) {
|
||||||
for (const tag of Object.keys(tagData)) {
|
for (const tag of Object.keys(tagData)) {
|
||||||
const filteredPosts = allBlogs.filter((post) =>
|
const filteredPosts = allBlogs.filter((post) =>
|
||||||
post.tags.map((t) => GithubSlugger.slug(t)).includes(tag)
|
post.tags.map((t) => GithubSlugger.slug(t)).includes(tag),
|
||||||
)
|
);
|
||||||
const rss = generateRss(config, filteredPosts, `tags/${tag}/${page}`)
|
const rss = generateRss(config, filteredPosts, `tags/${tag}/${page}`);
|
||||||
const rssPath = path.join('public', 'tags', tag)
|
const rssPath = path.join('public', 'tags', tag);
|
||||||
mkdirSync(rssPath, { recursive: true })
|
mkdirSync(rssPath, { recursive: true });
|
||||||
writeFileSync(path.join(rssPath, page), rss)
|
writeFileSync(path.join(rssPath, page), rss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rss = () => {
|
const rss = () => {
|
||||||
generateRSS(siteMetadata, allBlogs)
|
generateRSS(siteMetadata, allBlogs);
|
||||||
console.log('RSS feed generated...')
|
console.log('RSS feed generated...');
|
||||||
}
|
};
|
||||||
export default rss
|
export default rss;
|
||||||
|
Loading…
Reference in New Issue
Block a user