refactor: migrate to rsc and app dir
This commit is contained in:
parent
a03d358ef9
commit
09ba0550ca
11
.env.example
11
.env.example
@ -1,3 +1,4 @@
|
||||
# visit https://giscus.app to get your Giscus ids
|
||||
NEXT_PUBLIC_GISCUS_REPO=
|
||||
NEXT_PUBLIC_GISCUS_REPOSITORY_ID=
|
||||
NEXT_PUBLIC_GISCUS_CATEGORY=
|
||||
@ -10,20 +11,16 @@ MAILCHIMP_API_KEY=
|
||||
MAILCHIMP_API_SERVER=
|
||||
MAILCHIMP_AUDIENCE_ID=
|
||||
|
||||
BUTTONDOWN_API_URL=https://api.buttondown.email/v1/
|
||||
BUTTONDOWN_API_KEY=
|
||||
|
||||
CONVERTKIT_API_URL=https://api.convertkit.com/v3/
|
||||
CONVERTKIT_API_KEY=
|
||||
// curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID
|
||||
CONVERTKIT_FORM_ID=
|
||||
# curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID
|
||||
CONVERTKIT_FORM_ID=
|
||||
|
||||
KLAVIYO_API_KEY=
|
||||
KLAVIYO_LIST_ID=
|
||||
|
||||
REVUE_API_URL=https://www.getrevue.co/api/v2/
|
||||
REVUE_API_KEY=
|
||||
|
||||
EMAILOCTOPUS_API_URL=https://emailoctopus.com/api/1.6/
|
||||
EMAILOCTOPUS_API_KEY=
|
||||
EMAILOCTOPUS_LIST_ID=
|
||||
EMAILOCTOPUS_LIST_ID=
|
||||
|
23
.eslintrc.js
23
.eslintrc.js
@ -1,17 +1,38 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
env: {
|
||||
browser: true,
|
||||
amd: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:prettier/recommended', 'next', 'next/core-web-vitals'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'next',
|
||||
'next/core-web-vitals',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'error',
|
||||
{
|
||||
components: ['Link'],
|
||||
specialLink: ['hrefLeft', 'hrefRight'],
|
||||
aspects: ['invalidHref', 'preferButton'],
|
||||
},
|
||||
],
|
||||
'react/prop-types': 0,
|
||||
'no-unused-vars': 0,
|
||||
'react/no-unescaped-entities': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
}
|
||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,5 +1,5 @@
|
||||
## Source: https://github.com/alexkaratarakis/gitattributes
|
||||
## Modified * text=auto to * text=auto eol=lf to force LF endings.
|
||||
## Modified * text=auto to * text=auto eol=lf eol=lf to force LF endings.
|
||||
|
||||
## GITATTRIBUTES FOR WEB PROJECTS
|
||||
#
|
||||
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -4,6 +4,10 @@
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/.yarn/*
|
||||
!/.yarn/releases
|
||||
!/.yarn/plugins
|
||||
!/.yarn/sdks
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@ -17,8 +21,12 @@ public/sitemap.xml
|
||||
# production
|
||||
/build
|
||||
*.xml
|
||||
|
||||
# rss feed
|
||||
/public/feed.xml
|
||||
/public/feed.xml
|
||||
|
||||
# search
|
||||
/public/search.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@ -33,4 +41,7 @@ yarn-error.log*
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.production.local
|
||||
|
||||
# Contentlayer
|
||||
.contentlayer
|
||||
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
@ -1,26 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import Link from '@/components/Link'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
|
||||
import NewsletterForm from '@/components/NewsletterForm'
|
||||
import { formatDate } from 'pliny/utils/formatDate'
|
||||
import { NewsletterForm } from 'pliny/ui/NewsletterForm'
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
|
||||
export async function getStaticProps() {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
|
||||
return { props: { posts } }
|
||||
}
|
||||
|
||||
export default function Home({ posts }) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
||||
{/* <PageSEO title={siteMetadata.title} description={siteMetadata.description} /> */}
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||
<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-6xl md:leading-14">
|
||||
Latest
|
||||
</h1>
|
||||
@ -30,8 +24,8 @@ export default function Home({ posts }) {
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{!posts.length && 'No posts found.'}
|
||||
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
|
||||
const { slug, date, title, summary, tags } = frontMatter
|
||||
{posts.slice(0, MAX_DISPLAY).map((post) => {
|
||||
const { slug, date, title, summary, tags } = post
|
||||
return (
|
||||
<li key={slug} className="py-12">
|
||||
<article>
|
||||
@ -39,7 +33,7 @@ export default function Home({ posts }) {
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
<time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-5 xl:col-span-3">
|
||||
@ -85,13 +79,13 @@ export default function Home({ posts }) {
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="all posts"
|
||||
aria-label="All posts"
|
||||
>
|
||||
All Posts →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{siteMetadata.newsletter.provider !== '' && (
|
||||
{siteMetadata.newsletter?.provider && (
|
||||
<div className="flex items-center justify-center pt-4">
|
||||
<NewsletterForm />
|
||||
</div>
|
16
app/about/About.tsx
Normal file
16
app/about/About.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
|
||||
export default function About({ author }) {
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={author.layout || DEFAULT_LAYOUT}
|
||||
content={author}
|
||||
MDXComponents={MDXComponents}
|
||||
/>
|
||||
)
|
||||
}
|
7
app/about/page.tsx
Normal file
7
app/about/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { allAuthors } from 'contentlayer/generated'
|
||||
import About from './About'
|
||||
|
||||
export default function Page() {
|
||||
const author = allAuthors.find((p) => p.slug === 'default')
|
||||
return <About author={author} />
|
||||
}
|
14
app/api/newsletter2/route.ts
Normal file
14
app/api/newsletter2/route.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { NewsletterAPI } from 'pliny/newsletter'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const res = await request.json()
|
||||
console.log(res) // { email: 'test@example.com' }
|
||||
|
||||
return NextResponse.json({ res })
|
||||
// return NewsletterAPI({
|
||||
// // @ts-ignore
|
||||
// provider: siteMetadata.newsletter.provider,
|
||||
// })
|
||||
}
|
21
app/blog/[...slug]/Blog.tsx
Normal file
21
app/blog/[...slug]/Blog.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
|
||||
const DEFAULT_LAYOUT = 'PostLayout'
|
||||
|
||||
export default function Blog({ post, authorDetails, prev, next }) {
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={post.layout || DEFAULT_LAYOUT}
|
||||
content={post}
|
||||
MDXComponents={MDXComponents}
|
||||
toc={post.toc}
|
||||
authorDetails={authorDetails}
|
||||
prev={prev}
|
||||
next={next}
|
||||
/>
|
||||
)
|
||||
}
|
44
app/blog/[...slug]/page.tsx
Normal file
44
app/blog/[...slug]/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import { sortedBlogPost, coreContent } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs, allAuthors } from 'contentlayer/generated'
|
||||
import type { Authors, Blog } from 'contentlayer/generated'
|
||||
import BlogPage from './Blog'
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = allBlogs.map((p) => ({ slug: p.slug.split('/') }))
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { slug: string[] } }) {
|
||||
const slug = params.slug.join('/')
|
||||
const sortedPosts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const postIndex = sortedPosts.findIndex((p) => p.slug === slug)
|
||||
const prevContent = sortedPosts[postIndex + 1] || null
|
||||
const prev = prevContent ? coreContent(prevContent) : null
|
||||
const nextContent = sortedPosts[postIndex - 1] || null
|
||||
const next = nextContent ? coreContent(nextContent) : null
|
||||
const post = sortedPosts.find((p) => p.slug === slug)
|
||||
const authorList = post?.authors || ['default']
|
||||
const authorDetails = authorList.map((author) => {
|
||||
const authorResults = allAuthors.find((p) => p.slug === author)
|
||||
return coreContent(authorResults as Authors)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{post && 'draft' in post && post.draft === true ? (
|
||||
<div className="mt-24 text-center">
|
||||
<PageTitle>
|
||||
Under Construction{' '}
|
||||
<span role="img" aria-label="roadwork sign">
|
||||
🚧
|
||||
</span>
|
||||
</PageTitle>
|
||||
</div>
|
||||
) : (
|
||||
<BlogPage post={post} authorDetails={authorDetails} prev={prev} next={next} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
31
app/blog/page.tsx
Normal file
31
app/blog/page.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { sortedBlogPost } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
|
||||
const POSTS_PER_PAGE = 5
|
||||
|
||||
export default function BlogPage() {
|
||||
const posts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const pageNumber = 1
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
POSTS_PER_PAGE * pageNumber
|
||||
)
|
||||
const pagination = {
|
||||
currentPage: pageNumber,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} /> */}
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
41
app/blog/page/[page]/page.tsx
Normal file
41
app/blog/page/[page]/page.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { allCoreContent, sortedBlogPost } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
|
||||
const POSTS_PER_PAGE = 5
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const totalPosts = allBlogs
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({ page: (i + 1).toString() }))
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { page: string } }) {
|
||||
const posts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const pageNumber = parseInt(params.page as string)
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
POSTS_PER_PAGE * pageNumber
|
||||
)
|
||||
const pagination = {
|
||||
currentPage: pageNumber,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <PageSEO title={siteMetadata.title} description={siteMetadata.description} /> */}
|
||||
<ListLayout
|
||||
posts={allCoreContent(posts)}
|
||||
initialDisplayPosts={allCoreContent(initialDisplayPosts)}
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
29
app/globals.css
Normal file
29
app/globals.css
Normal file
@ -0,0 +1,29 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.task-list-item::before {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.task-list-item {
|
||||
@apply list-none;
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
@apply mt-12 border-t border-gray-200 pt-8 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.data-footnote-backref {
|
||||
@apply no-underline;
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
@apply my-5;
|
||||
}
|
||||
|
||||
/* https://stackoverflow.com/questions/61083813/how-to-avoid-internal-autofill-selected-style-to-be-applied */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus {
|
||||
transition: background-color 600000s 0s, color 600000s 0s;
|
||||
}
|
16
app/head.tsx
Normal file
16
app/head.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
export default function Head() {
|
||||
return (
|
||||
<>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
</>
|
||||
)
|
||||
}
|
35
app/layout.tsx
Normal file
35
app/layout.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import './globals.css'
|
||||
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Analytics } from 'pliny/analytics'
|
||||
import { SearchProvider } from 'pliny/search'
|
||||
import Header from '@/components/Header'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import Footer from '@/components/Footer'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { ThemeProviders } from './theme-providers'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang={siteMetadata.language} className={`${inter.className} scroll-smooth`}>
|
||||
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
||||
<ThemeProviders>
|
||||
{/* <Analytics analyticsConfig={siteMetadata.analytics} /> */}
|
||||
<SectionContainer>
|
||||
<div className="flex h-screen flex-col justify-between font-sans">
|
||||
{/* <SearchProvider searchConfig={siteMetadata.search}> */}
|
||||
<Header />
|
||||
<main className="mb-auto">{children}</main>
|
||||
{/* </SearchProvider> */}
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</ThemeProviders>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
import Link from '@/components/Link'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
|
||||
export default function FourZeroFour() {
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Page Not Found - ${siteMetadata.title}`} />
|
||||
{/* <PageSEO title="Page Not Found" description="Sorry we couldn't find this page :(" /> */}
|
||||
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
|
||||
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
|
||||
<div className="space-x-2 pb-8 pt-6 md:space-y-5">
|
||||
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
|
||||
404
|
||||
</h1>
|
||||
@ -19,10 +18,11 @@ export default function FourZeroFour() {
|
||||
<p className="mb-8">
|
||||
But dont worry, you can find plenty of other things on our homepage.
|
||||
</p>
|
||||
<Link href="/">
|
||||
<button 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
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500"
|
||||
>
|
||||
Back to homepage
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
10
app/page.tsx
Normal file
10
app/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { sortedBlogPost, allCoreContent } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
import Main from './Main'
|
||||
|
||||
export default async function Page() {
|
||||
const sortedPosts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const posts = allCoreContent(sortedPosts)
|
||||
return <Main posts={posts} />
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import projectsData from '@/data/projectsData'
|
||||
import Card from '@/components/Card'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||
<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-6xl md:leading-14">
|
||||
Projects
|
||||
</h1>
|
@ -1,23 +1,19 @@
|
||||
import { getAllTags } from 'pliny/utils/contentlayer'
|
||||
import Link from '@/components/Link'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllTags } from '@/lib/tags'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
import { kebabCase } from 'pliny/utils/kebabCase'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const tags = await getAllTags('blog')
|
||||
|
||||
return { props: { tags } }
|
||||
}
|
||||
|
||||
export default function Tags({ tags }) {
|
||||
export default async function Page() {
|
||||
const tags = await getAllTags(allBlogs)
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
|
||||
{/* <PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" /> */}
|
||||
<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="space-x-2 pt-6 pb-8 md:space-y-5">
|
||||
<div className="space-x-2 pb-8 pt-6 md:space-y-5">
|
||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:border-r-2 md:px-6 md:text-6xl md:leading-14">
|
||||
Tags
|
||||
</h1>
|
||||
@ -26,11 +22,12 @@ export default function Tags({ tags }) {
|
||||
{Object.keys(tags).length === 0 && 'No tags found.'}
|
||||
{sortedTags.map((t) => {
|
||||
return (
|
||||
<div key={t} className="mt-2 mb-2 mr-5">
|
||||
<div key={t} className="mb-2 mr-5 mt-2">
|
||||
<Tag text={t} />
|
||||
<Link
|
||||
href={`/tags/${kebabCase(t)}`}
|
||||
className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300"
|
||||
aria-label={`View posts tagged ${t}`}
|
||||
>
|
||||
{` (${tags[t]})`}
|
||||
</Link>
|
12
app/theme-providers.tsx
Normal file
12
app/theme-providers.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export function ThemeProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme} enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
@ -2,7 +2,7 @@ import Image from './Image'
|
||||
import Link from './Link'
|
||||
|
||||
const Card = ({ title, description, imgSrc, href }) => (
|
||||
<div className="md p-4 md:w-1/2" style={{ maxWidth: '544px' }}>
|
||||
<div className="md max-w-[544px] p-4 md:w-1/2">
|
||||
<div
|
||||
className={`${
|
||||
imgSrc && 'h-full'
|
@ -1,23 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
import Router from 'next/router'
|
||||
|
||||
/**
|
||||
* Client-side complement to next-remote-watch
|
||||
* Re-triggers getStaticProps when watched mdx files change
|
||||
*
|
||||
*/
|
||||
export const ClientReload = () => {
|
||||
// Exclude socket.io from prod bundle
|
||||
useEffect(() => {
|
||||
import('socket.io-client').then((module) => {
|
||||
const socket = module.io()
|
||||
socket.on('reload', (data) => {
|
||||
Router.replace(Router.asPath, undefined, {
|
||||
scroll: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
@ -7,12 +7,12 @@ export default function Footer() {
|
||||
<footer>
|
||||
<div className="mt-16 flex flex-col items-center">
|
||||
<div className="mb-3 flex space-x-4">
|
||||
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size="6" />
|
||||
<SocialIcon kind="github" href={siteMetadata.github} size="6" />
|
||||
<SocialIcon kind="facebook" href={siteMetadata.facebook} size="6" />
|
||||
<SocialIcon kind="youtube" href={siteMetadata.youtube} size="6" />
|
||||
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size="6" />
|
||||
<SocialIcon kind="twitter" href={siteMetadata.twitter} size="6" />
|
||||
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
|
||||
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
|
||||
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
|
||||
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
|
||||
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
|
||||
<SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
|
||||
</div>
|
||||
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div>{siteMetadata.author}</div>
|
46
components/Header.tsx
Normal file
46
components/Header.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import headerNavLinks from '@/data/headerNavLinks'
|
||||
import Logo from '@/data/logo.svg'
|
||||
import Link from './Link'
|
||||
import MobileNav from './MobileNav'
|
||||
import ThemeSwitch from './ThemeSwitch'
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<header className="flex items-center justify-between py-10">
|
||||
<div>
|
||||
<Link href="/" aria-label={siteMetadata.headerTitle}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-3">
|
||||
<Logo />
|
||||
</div>
|
||||
{typeof siteMetadata.headerTitle === 'string' ? (
|
||||
<div className="hidden h-6 text-2xl font-semibold sm:block">
|
||||
{siteMetadata.headerTitle}
|
||||
</div>
|
||||
) : (
|
||||
siteMetadata.headerTitle
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center text-base leading-5">
|
||||
<div className="hidden sm:block">
|
||||
{headerNavLinks.map((link) => (
|
||||
<Link
|
||||
key={link.title}
|
||||
href={link.href}
|
||||
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ThemeSwitch />
|
||||
<MobileNav />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
@ -1,6 +0,0 @@
|
||||
import NextImage from 'next/image'
|
||||
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
const Image = ({ ...rest }) => <NextImage {...rest} />
|
||||
|
||||
export default Image
|
5
components/Image.tsx
Normal file
5
components/Image.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
|
||||
|
||||
export default Image
|
@ -1,54 +0,0 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import headerNavLinks from '@/data/headerNavLinks'
|
||||
import Logo from '@/data/logo.svg'
|
||||
import Link from './Link'
|
||||
import SectionContainer from './SectionContainer'
|
||||
import Footer from './Footer'
|
||||
import MobileNav from './MobileNav'
|
||||
import ThemeSwitch from './ThemeSwitch'
|
||||
|
||||
const LayoutWrapper = ({ children }) => {
|
||||
return (
|
||||
<SectionContainer>
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<header className="flex items-center justify-between py-10">
|
||||
<div>
|
||||
<Link href="/" aria-label={siteMetadata.headerTitle}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-3">
|
||||
<Logo />
|
||||
</div>
|
||||
{typeof siteMetadata.headerTitle === 'string' ? (
|
||||
<div className="hidden h-6 text-2xl font-semibold sm:block">
|
||||
{siteMetadata.headerTitle}
|
||||
</div>
|
||||
) : (
|
||||
siteMetadata.headerTitle
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center text-base leading-5">
|
||||
<div className="hidden sm:block">
|
||||
{headerNavLinks.map((link) => (
|
||||
<Link
|
||||
key={link.title}
|
||||
href={link.href}
|
||||
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<ThemeSwitch />
|
||||
<MobileNav />
|
||||
</div>
|
||||
</header>
|
||||
<main className="mb-auto">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutWrapper
|
27
components/LayoutWrapper.tsx
Normal file
27
components/LayoutWrapper.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Inter } from 'next/font/google'
|
||||
import SectionContainer from './SectionContainer'
|
||||
import Footer from './Footer'
|
||||
import { ReactNode } from 'react'
|
||||
import Header from './Header'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
const LayoutWrapper = ({ children }: Props) => {
|
||||
return (
|
||||
<SectionContainer>
|
||||
<div className={`${inter.className} flex h-screen flex-col justify-between font-sans`}>
|
||||
<Header />
|
||||
<main className="mb-auto">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutWrapper
|
@ -1,16 +1,17 @@
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import Link from 'next/link'
|
||||
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
|
||||
|
||||
const CustomLink = ({ href, ...rest }) => {
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
|
||||
if (isInternalLink) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a {...rest} />
|
||||
</Link>
|
||||
)
|
||||
// @ts-ignore
|
||||
return <Link href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
@ -1,26 +0,0 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { useMemo } from 'react'
|
||||
import { getMDXComponent } from 'mdx-bundler/client'
|
||||
import Image from './Image'
|
||||
import CustomLink from './Link'
|
||||
import TOCInline from './TOCInline'
|
||||
import Pre from './Pre'
|
||||
import { BlogNewsletterForm } from './NewsletterForm'
|
||||
|
||||
export const MDXComponents = {
|
||||
Image,
|
||||
TOCInline,
|
||||
a: CustomLink,
|
||||
pre: Pre,
|
||||
BlogNewsletterForm: BlogNewsletterForm,
|
||||
wrapper: ({ components, layout, ...rest }) => {
|
||||
const Layout = require(`../layouts/${layout}`).default
|
||||
return <Layout {...rest} />
|
||||
},
|
||||
}
|
||||
|
||||
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
|
||||
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
|
||||
|
||||
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
|
||||
}
|
23
components/MDXComponents.tsx
Normal file
23
components/MDXComponents.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React from 'react'
|
||||
import { MDXLayout, ComponentMap } from 'pliny/mdx-components'
|
||||
import { TOCInline } from 'pliny/ui/TOCInline'
|
||||
import { Pre } from 'pliny/ui/Pre'
|
||||
import { BlogNewsletterForm } from 'pliny/ui/NewsletterForm'
|
||||
|
||||
import Image from './Image'
|
||||
import CustomLink from './Link'
|
||||
|
||||
export const Wrapper = ({ layout, content, ...rest }: MDXLayout) => {
|
||||
const Layout = require(`../layouts/${layout}`).default
|
||||
return <Layout content={content} {...rest} />
|
||||
}
|
||||
|
||||
export const MDXComponents: ComponentMap = {
|
||||
Image,
|
||||
TOCInline,
|
||||
a: CustomLink,
|
||||
pre: Pre,
|
||||
wrapper: Wrapper,
|
||||
BlogNewsletterForm,
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from './Link'
|
||||
import headerNavLinks from '@/data/headerNavLinks'
|
||||
@ -20,7 +22,6 @@ const MobileNav = () => {
|
||||
return (
|
||||
<div className="sm:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 mr-1 h-8 w-8 rounded py-1"
|
||||
aria-label="Toggle Menu"
|
||||
onClick={onToggleNav}
|
||||
@ -39,13 +40,12 @@ const MobileNav = () => {
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
className={`fixed top-0 left-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
|
||||
className={`fixed left-0 top-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
|
||||
navShow ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="mr-5 mt-11 h-8 w-8 rounded"
|
||||
aria-label="Toggle Menu"
|
||||
onClick={onToggleNav}
|
@ -1,84 +0,0 @@
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
||||
const inputEl = useRef(null)
|
||||
const [error, setError] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [subscribed, setSubscribed] = useState(false)
|
||||
|
||||
const subscribe = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
|
||||
body: JSON.stringify({
|
||||
email: inputEl.current.value,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const { error } = await res.json()
|
||||
if (error) {
|
||||
setError(true)
|
||||
setMessage('Your e-mail address is invalid or you are already subscribed!')
|
||||
return
|
||||
}
|
||||
|
||||
inputEl.current.value = ''
|
||||
setError(false)
|
||||
setSubscribed(true)
|
||||
setMessage('Successfully! 🎉 You are now subscribed.')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</div>
|
||||
<form className="flex flex-col sm:flex-row" onSubmit={subscribe}>
|
||||
<div>
|
||||
<label className="sr-only" htmlFor="email-input">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
autoComplete="email"
|
||||
className="w-72 rounded-md px-4 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600 dark:bg-black"
|
||||
id="email-input"
|
||||
name="email"
|
||||
placeholder={subscribed ? "You're subscribed ! 🎉" : 'Enter your email'}
|
||||
ref={inputEl}
|
||||
required
|
||||
type="email"
|
||||
disabled={subscribed}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full rounded-md shadow-sm sm:mt-0 sm:ml-3">
|
||||
<button
|
||||
className={`w-full rounded-md bg-primary-500 py-2 px-4 font-medium text-white sm:py-0 ${
|
||||
subscribed ? 'cursor-default' : 'hover:bg-primary-700 dark:hover:bg-primary-400'
|
||||
} focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2 dark:ring-offset-black`}
|
||||
type="submit"
|
||||
disabled={subscribed}
|
||||
>
|
||||
{subscribed ? 'Thank you!' : 'Sign up'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{error && (
|
||||
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">{message}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewsletterForm
|
||||
|
||||
export const BlogNewsletterForm = ({ title }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="bg-gray-100 p-6 dark:bg-gray-800 sm:px-14 sm:py-8">
|
||||
<NewsletterForm title={title} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
@ -1,4 +1,10 @@
|
||||
export default function PageTitle({ children }) {
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function PageTitle({ children }: Props) {
|
||||
return (
|
||||
<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}
|
@ -1,36 +0,0 @@
|
||||
import Link from '@/components/Link'
|
||||
|
||||
export default function Pagination({ totalPages, currentPage }) {
|
||||
const prevPage = parseInt(currentPage) - 1 > 0
|
||||
const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages)
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<nav className="flex justify-between">
|
||||
{!prevPage && (
|
||||
<button rel="previous" className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
||||
Previous
|
||||
</button>
|
||||
)}
|
||||
{prevPage && (
|
||||
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
|
||||
<button rel="previous">Previous</button>
|
||||
</Link>
|
||||
)}
|
||||
<span>
|
||||
{currentPage} of {totalPages}
|
||||
</span>
|
||||
{!nextPage && (
|
||||
<button rel="next" className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
||||
Next
|
||||
</button>
|
||||
)}
|
||||
{nextPage && (
|
||||
<Link href={`/blog/page/${currentPage + 1}`}>
|
||||
<button rel="next">Next</button>
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import { useState, useRef } from 'react'
|
||||
|
||||
const Pre = (props) => {
|
||||
const textInput = useRef(null)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const onEnter = () => {
|
||||
setHovered(true)
|
||||
}
|
||||
const onExit = () => {
|
||||
setHovered(false)
|
||||
setCopied(false)
|
||||
}
|
||||
const onCopy = () => {
|
||||
setCopied(true)
|
||||
navigator.clipboard.writeText(textInput.current.textContent)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
|
||||
{hovered && (
|
||||
<button
|
||||
aria-label="Copy code"
|
||||
type="button"
|
||||
className={`absolute right-2 top-2 h-8 w-8 rounded border-2 bg-gray-700 p-1 dark:bg-gray-800 ${
|
||||
copied
|
||||
? 'border-green-400 focus:border-green-400 focus:outline-none'
|
||||
: 'border-gray-300'
|
||||
}`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
className={copied ? 'text-green-400' : 'text-gray-300'}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<pre>{props.children}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pre
|
@ -1,8 +1,30 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
||||
import type { Blog, Authors } from 'contentlayer/generated'
|
||||
interface CommonSEOProps {
|
||||
title: string
|
||||
description: string
|
||||
ogType: string
|
||||
ogImage:
|
||||
| string
|
||||
| {
|
||||
'@type': string
|
||||
url: string
|
||||
}[]
|
||||
twImage: string
|
||||
canonicalUrl?: string
|
||||
}
|
||||
|
||||
const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl }) => {
|
||||
const CommonSEO = ({
|
||||
title,
|
||||
description,
|
||||
ogType,
|
||||
ogImage,
|
||||
twImage,
|
||||
canonicalUrl,
|
||||
}: CommonSEOProps) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Head>
|
||||
@ -14,7 +36,7 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
|
||||
<meta property="og:site_name" content={siteMetadata.title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
{ogImage.constructor.name === 'Array' ? (
|
||||
{Array.isArray(ogImage) ? (
|
||||
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
|
||||
) : (
|
||||
<meta property="og:image" content={ogImage} key={ogImage} />
|
||||
@ -32,7 +54,12 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
|
||||
)
|
||||
}
|
||||
|
||||
export const PageSEO = ({ title, description }) => {
|
||||
interface PageSEOProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const PageSEO = ({ title, description }: PageSEOProps) => {
|
||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
return (
|
||||
@ -46,7 +73,7 @@ export const PageSEO = ({ title, description }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const TagSEO = ({ title, description }) => {
|
||||
export const TagSEO = ({ title, description }: PageSEOProps) => {
|
||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const router = useRouter()
|
||||
@ -71,6 +98,11 @@ export const TagSEO = ({ title, description }) => {
|
||||
)
|
||||
}
|
||||
|
||||
interface BlogSeoProps extends CoreContent<Blog> {
|
||||
authorDetails?: CoreContent<Authors>[]
|
||||
url: string
|
||||
}
|
||||
|
||||
export const BlogSEO = ({
|
||||
authorDetails,
|
||||
title,
|
||||
@ -80,11 +112,10 @@ export const BlogSEO = ({
|
||||
url,
|
||||
images = [],
|
||||
canonicalUrl,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
}: BlogSeoProps) => {
|
||||
const publishedAt = new Date(date).toISOString()
|
||||
const modifiedAt = new Date(lastmod || date).toISOString()
|
||||
let imagesArr =
|
||||
const imagesArr =
|
||||
images.length === 0
|
||||
? [siteMetadata.socialBanner]
|
||||
: typeof images === 'string'
|
||||
@ -142,7 +173,7 @@ export const BlogSEO = ({
|
||||
<>
|
||||
<CommonSEO
|
||||
title={title}
|
||||
description={summary}
|
||||
description={summary || ''}
|
||||
ogType="article"
|
||||
ogImage={featuredImages}
|
||||
twImage={twImageUrl}
|
@ -18,16 +18,15 @@ const ScrollTopAndComment = () => {
|
||||
window.scrollTo({ top: 0 })
|
||||
}
|
||||
const handleScrollToComment = () => {
|
||||
document.getElementById('comment').scrollIntoView()
|
||||
document.getElementById('comment')?.scrollIntoView()
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`fixed right-8 bottom-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.comment.provider && (
|
||||
{siteMetadata.comments?.provider && (
|
||||
<button
|
||||
aria-label="Scroll To Comment"
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
@ -42,7 +41,6 @@ const ScrollTopAndComment = () => {
|
||||
)}
|
||||
<button
|
||||
aria-label="Scroll To Top"
|
||||
type="button"
|
||||
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"
|
||||
>
|
@ -1,3 +0,0 @@
|
||||
export default function SectionContainer({ children }) {
|
||||
return <div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
|
||||
}
|
11
components/SectionContainer.tsx
Normal file
11
components/SectionContainer.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function SectionContainer({ children }: Props) {
|
||||
return (
|
||||
<section className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</section>
|
||||
)
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/**
|
||||
* @typedef TocHeading
|
||||
* @prop {string} value
|
||||
* @prop {number} depth
|
||||
* @prop {string} url
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates an inline table of contents
|
||||
* Exclude titles matching this string (new RegExp('^(' + string + ')$', 'i')).
|
||||
* If an array is passed the array gets joined with a pipe (new RegExp('^(' + array.join('|') + ')$', 'i')).
|
||||
*
|
||||
* @param {{
|
||||
* toc: TocHeading[],
|
||||
* indentDepth?: number,
|
||||
* fromHeading?: number,
|
||||
* toHeading?: number,
|
||||
* asDisclosure?: boolean,
|
||||
* exclude?: string|string[]
|
||||
* }} props
|
||||
*
|
||||
*/
|
||||
const TOCInline = ({
|
||||
toc,
|
||||
indentDepth = 3,
|
||||
fromHeading = 1,
|
||||
toHeading = 6,
|
||||
asDisclosure = false,
|
||||
exclude = '',
|
||||
}) => {
|
||||
const re = Array.isArray(exclude)
|
||||
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
|
||||
: new RegExp('^(' + exclude + ')$', 'i')
|
||||
|
||||
const filteredToc = toc.filter(
|
||||
(heading) =>
|
||||
heading.depth >= fromHeading && heading.depth <= toHeading && !re.test(heading.value)
|
||||
)
|
||||
|
||||
const tocList = (
|
||||
<ul>
|
||||
{filteredToc.map((heading) => (
|
||||
<li key={heading.value} className={`${heading.depth >= indentDepth && 'ml-6'}`}>
|
||||
<a href={heading.url}>{heading.value}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{asDisclosure ? (
|
||||
<details open>
|
||||
<summary className="ml-6 pt-2 pb-2 text-xl font-bold">Table of Contents</summary>
|
||||
<div className="ml-6">{tocList}</div>
|
||||
</details>
|
||||
) : (
|
||||
tocList
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TOCInline
|
@ -1,14 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
|
||||
const Tag = ({ text }) => {
|
||||
return (
|
||||
<Link href={`/tags/${kebabCase(text)}`}>
|
||||
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
{text.split(' ').join('-')}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tag
|
19
components/Tag.tsx
Normal file
19
components/Tag.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
import { kebabCase } from 'pliny/utils/kebabCase'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
}
|
||||
|
||||
const Tag = ({ text }: Props) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/tags/${kebabCase(text)}`}
|
||||
className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
{text.split(' ').join('-')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tag
|
@ -1,19 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
const ThemeSwitch = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme, resolvedTheme } = useTheme()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
// When mounted on client, now we can show the UI
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label="Toggle Dark Mode"
|
||||
type="button"
|
||||
className="ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4"
|
||||
onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -21,7 +26,7 @@ const ThemeSwitch = () => {
|
||||
fill="currentColor"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
|
||||
{mounted && theme === 'dark' ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
@ -1,36 +0,0 @@
|
||||
import Script from 'next/script'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const GAScript = () => {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
strategy="lazyOnload"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${siteMetadata.analytics.googleAnalyticsId}`}
|
||||
/>
|
||||
|
||||
<Script strategy="lazyOnload" id="ga-script">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${siteMetadata.analytics.googleAnalyticsId}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GAScript
|
||||
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
|
||||
export const logEvent = (action, category, label, value) => {
|
||||
window.gtag?.('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
})
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import Script from 'next/script'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const PlausibleScript = () => {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
strategy="lazyOnload"
|
||||
data-domain={siteMetadata.analytics.plausibleDataDomain}
|
||||
src="https://plausible.io/js/plausible.js"
|
||||
/>
|
||||
<Script strategy="lazyOnload" id="plausible-script">
|
||||
{`
|
||||
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlausibleScript
|
||||
|
||||
// https://plausible.io/docs/custom-event-goals
|
||||
export const logEvent = (eventName, ...rest) => {
|
||||
return window.plausible?.(eventName, ...rest)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import Script from 'next/script'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const PosthogScript = () => {
|
||||
return (
|
||||
<>
|
||||
<Script strategy="lazyOnload" id="posthog-script">
|
||||
{`
|
||||
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||
posthog.init('${siteMetadata.analytics.posthogAnalyticsId}',{api_host:'https://app.posthog.com'})
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PosthogScript
|
@ -1,25 +0,0 @@
|
||||
import Script from 'next/script'
|
||||
|
||||
const SimpleAnalyticsScript = () => {
|
||||
return (
|
||||
<>
|
||||
<Script strategy="lazyOnload" id="sa-script">
|
||||
{`
|
||||
window.sa_event=window.sa_event||function(){var a=[].slice.call(arguments);window.sa_event.q?window.sa_event.q.push(a):window.sa_event.q=[a]};
|
||||
`}
|
||||
</Script>
|
||||
<Script strategy="lazyOnload" src="https://scripts.simpleanalyticscdn.com/latest.js" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// https://docs.simpleanalytics.com/events
|
||||
export const logEvent = (eventName, callback) => {
|
||||
if (callback) {
|
||||
return window.sa_event?.(eventName, callback)
|
||||
} else {
|
||||
return window.sa_event?.(eventName)
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleAnalyticsScript
|
@ -1,18 +0,0 @@
|
||||
import Script from 'next/script'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const UmamiScript = () => {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
async
|
||||
defer
|
||||
data-website-id={siteMetadata.analytics.umamiWebsiteId}
|
||||
src="https://umami.example.com/umami.js" // Replace with your umami instance
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UmamiScript
|
@ -1,22 +0,0 @@
|
||||
import GA from './GoogleAnalytics'
|
||||
import Plausible from './Plausible'
|
||||
import SimpleAnalytics from './SimpleAnalytics'
|
||||
import Umami from './Umami'
|
||||
import Posthog from './Posthog'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
const Analytics = () => {
|
||||
return (
|
||||
<>
|
||||
{isProduction && siteMetadata.analytics.plausibleDataDomain && <Plausible />}
|
||||
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
|
||||
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
|
||||
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
|
||||
{isProduction && siteMetadata.analytics.posthogAnalyticsId && <Posthog />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Analytics
|
@ -1,37 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const Disqus = ({ frontMatter }) => {
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
|
||||
const COMMENTS_ID = 'disqus_thread'
|
||||
|
||||
function LoadComments() {
|
||||
setEnabledLoadComments(false)
|
||||
|
||||
window.disqus_config = function () {
|
||||
this.page.url = window.location.href
|
||||
this.page.identifier = frontMatter.slug
|
||||
}
|
||||
if (window.DISQUS === undefined) {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
|
||||
script.setAttribute('data-timestamp', +new Date())
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.async = true
|
||||
document.body.appendChild(script)
|
||||
} else {
|
||||
window.DISQUS.reset({ reload: true })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
<div className="disqus-frame" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Disqus
|
@ -1,72 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const Giscus = () => {
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
const { theme, resolvedTheme } = useTheme()
|
||||
const commentsTheme =
|
||||
siteMetadata.comment.giscusConfig.themeURL === ''
|
||||
? theme === 'dark' || resolvedTheme === 'dark'
|
||||
? siteMetadata.comment.giscusConfig.darkTheme
|
||||
: siteMetadata.comment.giscusConfig.theme
|
||||
: siteMetadata.comment.giscusConfig.themeURL
|
||||
|
||||
const COMMENTS_ID = 'comments-container'
|
||||
|
||||
const LoadComments = useCallback(() => {
|
||||
setEnabledLoadComments(false)
|
||||
|
||||
const {
|
||||
repo,
|
||||
repositoryId,
|
||||
category,
|
||||
categoryId,
|
||||
mapping,
|
||||
reactions,
|
||||
metadata,
|
||||
inputPosition,
|
||||
lang,
|
||||
} = siteMetadata?.comment?.giscusConfig
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://giscus.app/client.js'
|
||||
script.setAttribute('data-repo', repo)
|
||||
script.setAttribute('data-repo-id', repositoryId)
|
||||
script.setAttribute('data-category', category)
|
||||
script.setAttribute('data-category-id', categoryId)
|
||||
script.setAttribute('data-mapping', mapping)
|
||||
script.setAttribute('data-reactions-enabled', reactions)
|
||||
script.setAttribute('data-emit-metadata', metadata)
|
||||
script.setAttribute('data-input-position', inputPosition)
|
||||
script.setAttribute('data-lang', lang)
|
||||
script.setAttribute('data-theme', commentsTheme)
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.async = true
|
||||
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.appendChild(script)
|
||||
|
||||
return () => {
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.innerHTML = ''
|
||||
}
|
||||
}, [commentsTheme])
|
||||
|
||||
// Reload on theme change
|
||||
useEffect(() => {
|
||||
const iframe = document.querySelector('iframe.giscus-frame')
|
||||
if (!iframe) return
|
||||
LoadComments()
|
||||
}, [LoadComments])
|
||||
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
<div className="giscus" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Giscus
|
@ -1,52 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const Utterances = () => {
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
const { theme, resolvedTheme } = useTheme()
|
||||
const commentsTheme =
|
||||
theme === 'dark' || resolvedTheme === 'dark'
|
||||
? siteMetadata.comment.utterancesConfig.darkTheme
|
||||
: siteMetadata.comment.utterancesConfig.theme
|
||||
|
||||
const COMMENTS_ID = 'comments-container'
|
||||
|
||||
const LoadComments = useCallback(() => {
|
||||
setEnabledLoadComments(false)
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://utteranc.es/client.js'
|
||||
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
|
||||
script.setAttribute('issue-term', siteMetadata.comment.utterancesConfig.issueTerm)
|
||||
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
|
||||
script.setAttribute('theme', commentsTheme)
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.async = true
|
||||
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.appendChild(script)
|
||||
|
||||
return () => {
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.innerHTML = ''
|
||||
}
|
||||
}, [commentsTheme])
|
||||
|
||||
// Reload on theme change
|
||||
useEffect(() => {
|
||||
const iframe = document.querySelector('iframe.utterances-frame')
|
||||
if (!iframe) return
|
||||
LoadComments()
|
||||
}, [LoadComments])
|
||||
|
||||
// Added `relative` to fix a weird bug with `utterances-frame` position
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
<div className="utterances-frame relative" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Utterances
|
@ -1,39 +0,0 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const UtterancesComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Utterances')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
const GiscusComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Giscus')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
const DisqusComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Disqus')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const Comments = ({ frontMatter }) => {
|
||||
const comment = siteMetadata?.comment
|
||||
if (!comment || Object.keys(comment).length === 0) return <></>
|
||||
return (
|
||||
<div id="comment">
|
||||
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && <GiscusComponent />}
|
||||
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
|
||||
<UtterancesComponent />
|
||||
)}
|
||||
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
|
||||
<DisqusComponent frontMatter={frontMatter} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Comments
|
99
contentlayer.config.ts
Normal file
99
contentlayer.config.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { defineDocumentType, ComputedFields, makeSource } from 'contentlayer/source-files'
|
||||
import readingTime from 'reading-time'
|
||||
import path from 'path'
|
||||
// Remark packages
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import {
|
||||
remarkExtractFrontmatter,
|
||||
remarkCodeTitles,
|
||||
remarkImgToJsx,
|
||||
extractTocHeadings,
|
||||
} from 'pliny/mdx-plugins.js'
|
||||
// Rehype packages
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeCitation from 'rehype-citation'
|
||||
import rehypePrismPlus from 'rehype-prism-plus'
|
||||
import rehypePresetMinify from 'rehype-preset-minify'
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
const computedFields: ComputedFields = {
|
||||
readingTime: { type: 'json', resolve: (doc) => readingTime(doc.body.raw) },
|
||||
slug: {
|
||||
type: 'string',
|
||||
resolve: (doc) => doc._raw.flattenedPath.replace(/^.+?(\/)/, ''),
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
resolve: (doc) => doc._raw.flattenedPath,
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
resolve: (doc) => doc._raw.sourceFilePath,
|
||||
},
|
||||
toc: { type: 'string', resolve: (doc) => extractTocHeadings(doc.body.raw) },
|
||||
}
|
||||
|
||||
export const Blog = defineDocumentType(() => ({
|
||||
name: 'Blog',
|
||||
filePathPattern: 'blog/**/*.mdx',
|
||||
contentType: 'mdx',
|
||||
fields: {
|
||||
title: { type: 'string', required: true },
|
||||
date: { type: 'date', required: true },
|
||||
tags: { type: 'list', of: { type: 'string' } },
|
||||
lastmod: { type: 'date' },
|
||||
draft: { type: 'boolean' },
|
||||
summary: { type: 'string' },
|
||||
images: { type: 'list', of: { type: 'string' } },
|
||||
authors: { type: 'list', of: { type: 'string' } },
|
||||
layout: { type: 'string' },
|
||||
bibliography: { type: 'string' },
|
||||
canonicalUrl: { type: 'string' },
|
||||
},
|
||||
computedFields,
|
||||
}))
|
||||
|
||||
export const Authors = defineDocumentType(() => ({
|
||||
name: 'Authors',
|
||||
filePathPattern: 'authors/**/*.mdx',
|
||||
contentType: 'mdx',
|
||||
fields: {
|
||||
name: { type: 'string', required: true },
|
||||
avatar: { type: 'string' },
|
||||
occupation: { type: 'string' },
|
||||
company: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
twitter: { type: 'string' },
|
||||
linkedin: { type: 'string' },
|
||||
github: { type: 'string' },
|
||||
layout: { type: 'string' },
|
||||
},
|
||||
computedFields,
|
||||
}))
|
||||
|
||||
export default makeSource({
|
||||
contentDirPath: 'data',
|
||||
documentTypes: [Blog, Authors],
|
||||
mdx: {
|
||||
cwd: process.cwd(),
|
||||
remarkPlugins: [
|
||||
remarkExtractFrontmatter,
|
||||
remarkGfm,
|
||||
remarkCodeTitles,
|
||||
remarkMath,
|
||||
remarkImgToJsx,
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
rehypeKatex,
|
||||
[rehypeCitation, { path: path.join(root, 'data') }],
|
||||
[rehypePrismPlus, { defaultLanguage: 'js' }],
|
||||
rehypePresetMinify,
|
||||
],
|
||||
},
|
||||
})
|
63
css/docsearch.css
Normal file
63
css/docsearch.css
Normal file
@ -0,0 +1,63 @@
|
||||
|
||||
.light .DocSearch {
|
||||
--docsearch-primary-color: theme(colors.primary.600);
|
||||
--docsearch-highlight-color: theme(colors.primary.600);
|
||||
--docsearch-searchbox-shadow: inset 0 0 0 2px theme(colors.primary.600);
|
||||
--docsearch-muted-color: theme(colors.gray.500);
|
||||
--docsearch-container-background: theme(colors.gray.400 / 80%);
|
||||
/* Modal */
|
||||
--docsearch-modal-background: theme(colors.gray.200);
|
||||
/* Search box */
|
||||
--docsearch-searchbox-background: theme(colors.gray.100);
|
||||
--docsearch-searchbox-focus-background: theme(colors.gray.100);
|
||||
/* Hit */
|
||||
--docsearch-hit-color: theme(colors.gray.700);
|
||||
--docsearch-hit-shadow: none;
|
||||
--docsearch-hit-active-color: theme(colors.gray.800);
|
||||
--docsearch-hit-background: theme(colors.gray.100);
|
||||
/* Footer */
|
||||
--docsearch-footer-background: theme(colors.gray.100);
|
||||
}
|
||||
|
||||
.dark .DocSearch {
|
||||
--docsearch-primary-color: theme(colors.primary.600);
|
||||
--docsearch-highlight-color: theme(colors.primary.600);
|
||||
--docsearch-searchbox-shadow: inset 0 0 0 2px theme(colors.primary.600);
|
||||
--docsearch-text-color: theme(colors.gray.200);
|
||||
--docsearch-muted-color: theme(colors.gray.400);
|
||||
--docsearch-container-background: theme(colors.gray.900 / 80%);
|
||||
/* Modal */
|
||||
--docsearch-modal-background: theme(colors.gray.900);
|
||||
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
|
||||
0 3px 8px 0 rgb(0, 3, 9);
|
||||
/* Search box */
|
||||
--docsearch-searchbox-background: theme(colors.gray.800);
|
||||
--docsearch-searchbox-focus-background: theme(colors.gray.800);
|
||||
/* Hit */
|
||||
--docsearch-hit-color: theme(colors.gray.200);
|
||||
--docsearch-hit-shadow: none;
|
||||
--docsearch-hit-active-color: theme(colors.gray.100);
|
||||
--docsearch-hit-background: theme(colors.gray.800);
|
||||
/* Footer */
|
||||
--docsearch-footer-background: theme(colors.gray.900);
|
||||
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
|
||||
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
--docsearch-key-gradient: linear-gradient(-26.5deg,
|
||||
theme(colors.gray.800) 0%,
|
||||
theme(colors.gray.900) 100%);
|
||||
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85),
|
||||
inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3);
|
||||
--docsearch-logo-color: theme(colors.gray.300);
|
||||
}
|
||||
|
||||
.light .DocSearch-Input {
|
||||
@apply hover:ring-0 ring-0;
|
||||
}
|
||||
|
||||
.dark .DocSearch-Input {
|
||||
@apply hover:ring-0 ring-0;
|
||||
}
|
||||
|
||||
.DocSearch-Container {
|
||||
@apply backdrop-blur;
|
||||
}
|
@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.highlight-line {
|
||||
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
|
||||
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
|
||||
}
|
||||
|
||||
.line-number::before {
|
||||
@ -138,3 +138,7 @@
|
||||
.token.table {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.token.table {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -11,7 +11,11 @@
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
@apply pt-8 mt-12 border-t border-gray-200 dark:border-gray-700;
|
||||
@apply mt-12 border-t border-gray-200 pt-8 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.data-footnote-backref {
|
||||
@apply no-underline;
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
|
@ -10,7 +10,9 @@ summary: 'How to derive the OLS Estimator with matrix notation and a tour of mat
|
||||
|
||||
Parsing and display of math equations is included in this blog template. Parsing of math is enabled by `remark-math` and `rehype-katex`.
|
||||
KaTeX and its associated font is included in `_document.js` so feel free to use it on any page.
|
||||
^[For the full list of supported TeX functions, check out the [KaTeX documentation](https://katex.org/docs/supported.html)]
|
||||
[^footnote]
|
||||
|
||||
[^footnote]: For the full list of supported TeX functions, check out the [KaTeX documentation](https://katex.org/docs/supported.html)
|
||||
|
||||
Inline math symbols can be included by enclosing the term between the `$` symbol.
|
||||
|
||||
|
@ -49,12 +49,8 @@ _Note_: If you try to save the image, it is in webp format, if your browser supp
|
||||
|
||||
![ocean](/static/images/ocean.jpeg)
|
||||
|
||||
<p>
|
||||
Photo by [YUCAR
|
||||
FotoGrafik](https://unsplash.com/@yucar?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/s/photos/sea?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
</p>
|
||||
Photo by [YUCAR FotoGrafik](https://unsplash.com/@yucar?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on [Unsplash](https://unsplash.com/s/photos/sea?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
|
||||
# Benefits
|
||||
|
||||
|
@ -60,7 +60,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
||||
- Blog templates
|
||||
- TOC component
|
||||
- Support for nested routing of blog posts
|
||||
- Newsletter component with support for mailchimp, buttondown, convertkit, klaviyo, revue, and emailoctopus
|
||||
- Newsletter component with support for mailchimp, buttondown and convertkit
|
||||
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
||||
- Projects page
|
||||
- Preconfigured security headers
|
||||
@ -77,27 +77,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
1. Try installing the starter using the new [Pliny project CLI](https://github.com/timlrx/pliny):
|
||||
|
||||
```bash
|
||||
npm i -g @pliny/cli
|
||||
pliny new --template=starter-blog my-blog
|
||||
```
|
||||
|
||||
It supports the updated version of the blog with Contentlayer, optional choice of TS/JS and different package managers as well as more modularized components which will be the basis of the template going forward.
|
||||
|
||||
Alternatively to stick with the current version, TypeScript and Contentlayer:
|
||||
|
||||
```bash
|
||||
npx degit 'timlrx/tailwind-nextjs-starter-blog#contentlayer'
|
||||
```
|
||||
|
||||
or JS (official support)
|
||||
|
||||
```bash
|
||||
npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git
|
||||
```
|
||||
|
||||
1. JS (official support) - `npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git` or TS (community support) - `npx degit timlrx/tailwind-nextjs-starter-blog#typescript`
|
||||
2. Personalize `siteMetadata.js` (site related information)
|
||||
3. Modify the content security policy in `next.config.js` if you want to use
|
||||
any analytics provider or a commenting solution other than giscus.
|
||||
@ -157,7 +137,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
|
||||
|
||||
Frontmatter follows [Hugo's standards](https://gohugo.io/content-management/front-matter/).
|
||||
|
||||
Currently 7 fields are supported.
|
||||
Currently 10 fields are supported.
|
||||
|
||||
```
|
||||
title (required)
|
||||
|
@ -3,7 +3,7 @@ title: My fancy title
|
||||
date: '2021-01-31'
|
||||
tags: ['hello']
|
||||
draft: true
|
||||
summary:
|
||||
summary: draft post
|
||||
images: []
|
||||
---
|
||||
|
@ -7,7 +7,6 @@ draft: false
|
||||
summary: 'An overview of the new features released in v1 - code block copy, multiple authors, frontmatter layout and more'
|
||||
layout: PostSimple
|
||||
bibliography: references-data.bib
|
||||
canonicalUrl: https://tailwind-nextjs-starter-blog.vercel.app/blog/new-features-in-v1/
|
||||
---
|
||||
|
||||
## Overview
|
||||
@ -16,7 +15,9 @@ A post on the new features introduced in v1.0. New features:
|
||||
|
||||
<TOCInline toc={props.toc} exclude="Overview" toHeading={2} />
|
||||
|
||||
First load JS decreased from 43kB to 39kB despite all the new features added!^[With the new changes in Nextjs 12, first load JS increase to 45kB.]
|
||||
First load JS decreased from 43kB to 39kB despite all the new features added! [^1]
|
||||
|
||||
[^1]: With the new changes in Nextjs 12, first load JS increase to 45kB.
|
||||
|
||||
See [upgrade guide](#upgrade-guide) below if you are migrating from v0 version of the template.
|
||||
|
||||
@ -59,14 +60,11 @@ Some new possibilities include loading components directly in the mdx file using
|
||||
For example, the following jsx snippet can be used directly in an MDX file to render the page title component:
|
||||
|
||||
```jsx
|
||||
import PageTitle from './PageTitle.js'
|
||||
// Or import PageTitle from './components/PageTitle.js' if you are using js
|
||||
import PageTitle from './components/PageTitle.tsx'
|
||||
;<PageTitle> Using JSX components in MDX </PageTitle>
|
||||
```
|
||||
|
||||
import PageTitle from './PageTitle.js'
|
||||
|
||||
<PageTitle> Using JSX components in MDX </PageTitle>
|
||||
|
||||
The default configuration resolves all components relative to the `components` directory.
|
||||
|
||||
**Note**:
|
||||
@ -338,7 +336,7 @@ To modify the styles, change the following class selectors in the `prism.css` fi
|
||||
}
|
||||
|
||||
.line-number::before {
|
||||
@apply mr-4 -ml-2 inline-block w-4 text-right text-gray-400;
|
||||
@apply -ml-2 mr-4 inline-block w-4 text-right text-gray-400;
|
||||
content: attr(line);
|
||||
}
|
||||
```
|
||||
|
@ -18,17 +18,17 @@ Since we are using mdx, we can create a simple responsive flexbox grid to displa
|
||||
|
||||
# Gallery
|
||||
|
||||
<div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2">
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="-mx-2 flex flex-wrap overflow-hidden xl:-mx-2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Maple](/static/images/canada/maple.jpg)
|
||||
</div>
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Lake](/static/images/canada/lake.jpg)
|
||||
</div>
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Mountains](/static/images/canada/mountains.jpg)
|
||||
</div>
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Toronto](/static/images/canada/toronto.jpg)
|
||||
</div>
|
||||
</div>
|
||||
@ -36,17 +36,17 @@ Since we are using mdx, we can create a simple responsive flexbox grid to displa
|
||||
# Implementation
|
||||
|
||||
```js
|
||||
<div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2">
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="-mx-2 flex flex-wrap overflow-hidden xl:-mx-2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Maple](/static/images/canada/maple.jpg)
|
||||
</div>
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Lake](/static/images/canada/lake.jpg)
|
||||
</div>
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Mountains](/static/images/canada/mountains.jpg)
|
||||
</div>
|
||||
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||
![Toronto](/static/images/canada/toronto.jpg)
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,9 +3,9 @@ title: 'The Time Machine'
|
||||
date: '2018-08-15'
|
||||
tags: ['writings', 'book', 'reflection']
|
||||
draft: false
|
||||
summary: 'The Time Traveller (for so it will be convenient to speak of him) was
|
||||
expounding a recondite matter to us. His pale grey eyes shone and
|
||||
twinkled, and his usually pale face was flushed and animated...'
|
||||
summary: The Time Traveller (for so it will be convenient to speak of him) was
|
||||
expounding a recondite matter to us. His pale grey eyes shone and
|
||||
twinkled, and his usually pale face was flushed and animated...
|
||||
---
|
||||
|
||||
# The Time Machine by H. G. Wells
|
||||
|
@ -30,4 +30,4 @@
|
||||
author={Xie, Yihui},
|
||||
year={2016},
|
||||
publisher={CRC Press}
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("pliny/config").PlinyConfig } */
|
||||
const siteMetadata = {
|
||||
title: 'Next.js Starter Blog',
|
||||
author: 'Tails Azimuth',
|
||||
@ -24,16 +27,16 @@ const siteMetadata = {
|
||||
plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
|
||||
simpleAnalytics: false, // true or false
|
||||
umamiWebsiteId: '', // e.g. 123e4567-e89b-12d3-a456-426614174000
|
||||
posthogProjectApiKey: '', // e.g. AhnJK8392ndPOav87as450xd
|
||||
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
|
||||
posthogAnalyticsId: '', // posthog.init e.g. phc_5yXvArzvRdqtZIsHkEm3Fkkhm3d0bEYUXCaFISzqPSQ
|
||||
},
|
||||
newsletter: {
|
||||
// supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus
|
||||
// Please add your .env file and modify it according to your selection
|
||||
provider: 'buttondown',
|
||||
},
|
||||
comment: {
|
||||
// If you want to use a commenting system other than giscus you have to add it to the
|
||||
comments: {
|
||||
// If you want to use an analytics provider you have to add it to the
|
||||
// content security policy in the `next.config.js` file.
|
||||
// Select a provider and use the environment variables associated to it
|
||||
// https://vercel.com/docs/environment-variables
|
||||
@ -52,34 +55,30 @@ const siteMetadata = {
|
||||
// theme example: light, dark, dark_dimmed, dark_high_contrast
|
||||
// transparent_dark, preferred_color_scheme, custom
|
||||
theme: 'light',
|
||||
// Place the comment box above the comments. options: bottom, top
|
||||
inputPosition: 'bottom',
|
||||
// Choose the language giscus will be displayed in. options: en, es, zh-CN, zh-TW, ko, ja etc
|
||||
lang: 'en',
|
||||
// theme when dark mode
|
||||
darkTheme: 'transparent_dark',
|
||||
// If the theme option above is set to 'custom`
|
||||
// please provide a link below to your custom theme css file.
|
||||
// example: https://giscus.app/themes/custom_example.css
|
||||
themeURL: '',
|
||||
},
|
||||
utterancesConfig: {
|
||||
// Visit the link below, and follow the steps in the 'configuration' section
|
||||
// https://utteranc.es/
|
||||
repo: process.env.NEXT_PUBLIC_UTTERANCES_REPO,
|
||||
issueTerm: '', // supported options: pathname, url, title
|
||||
label: '', // label (optional): Comment 💬
|
||||
// theme example: github-light, github-dark, preferred-color-scheme
|
||||
// github-dark-orange, icy-dark, dark-blue, photon-dark, boxy-light
|
||||
theme: '',
|
||||
// theme when dark mode
|
||||
darkTheme: '',
|
||||
},
|
||||
disqusConfig: {
|
||||
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
|
||||
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
|
||||
// This corresponds to the `data-lang="en"` in giscus's configurations
|
||||
lang: 'en',
|
||||
},
|
||||
},
|
||||
// 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
|
||||
|
@ -6,7 +6,8 @@
|
||||
"@/data/*": ["data/*"],
|
||||
"@/layouts/*": ["layouts/*"],
|
||||
"@/lib/*": ["lib/*"],
|
||||
"@/css/*": ["css/*"]
|
||||
"@/css/*": ["css/*"],
|
||||
"contentlayer/generated": ["./.contentlayer/generated"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,38 @@
|
||||
import { ReactNode } from 'react'
|
||||
import type { Authors } from 'contentlayer/generated'
|
||||
import SocialIcon from '@/components/social-icons'
|
||||
import Image from '@/components/Image'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
|
||||
export default function AuthorLayout({ children, frontMatter }) {
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
content: Omit<Authors, '_id' | '_raw' | 'body'>
|
||||
}
|
||||
|
||||
export default function AuthorLayout({ children, content }: Props) {
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = content
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`About - ${name}`} description={`About me - ${name}`} />
|
||||
{/* <PageSEO title={`About - ${name}`} description={`About me - ${name}`} /> */}
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||
<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-6xl md:leading-14">
|
||||
About
|
||||
</h1>
|
||||
</div>
|
||||
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
|
||||
<div className="flex flex-col items-center pt-8">
|
||||
<Image
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
width="192px"
|
||||
height="192px"
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">{name}</h3>
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{avatar && (
|
||||
<Image
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<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">{company}</div>
|
||||
<div className="flex space-x-3 pt-6">
|
||||
@ -33,7 +42,7 @@ export default function AuthorLayout({ children, frontMatter }) {
|
||||
<SocialIcon kind="twitter" href={twitter} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose max-w-none pt-8 pb-8 dark:prose-dark xl:col-span-2">{children}</div>
|
||||
<div className="prose max-w-none pb-8 pt-8 dark:prose-dark xl:col-span-2">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
@ -1,91 +0,0 @@
|
||||
import Link from '@/components/Link'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { useState } from 'react'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
|
||||
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const filteredBlogPosts = posts.filter((frontMatter) => {
|
||||
const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')
|
||||
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
|
||||
})
|
||||
|
||||
// If initialDisplayPosts exist, display it if no searchValue is specified
|
||||
const displayPosts =
|
||||
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<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-6xl md:leading-14">
|
||||
{title}
|
||||
</h1>
|
||||
<div className="relative max-w-lg">
|
||||
<input
|
||||
aria-label="Search articles"
|
||||
type="text"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder="Search articles"
|
||||
className="block w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-900 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<svg
|
||||
className="absolute right-3 top-3 h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{!filteredBlogPosts.length && 'No posts found.'}
|
||||
{displayPosts.map((frontMatter) => {
|
||||
const { slug, date, title, summary, tags } = frontMatter
|
||||
return (
|
||||
<li key={slug} className="py-4">
|
||||
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-3 xl:col-span-3">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-8 tracking-tight">
|
||||
<Link href={`/blog/${slug}`} className="text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="flex flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag} text={tag} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose max-w-none text-gray-500 dark:text-gray-400">
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{pagination && pagination.totalPages > 1 && !searchValue && (
|
||||
<Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
154
layouts/ListLayout.tsx
Normal file
154
layouts/ListLayout.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { formatDate } from 'pliny/utils/formatDate'
|
||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
import Link from '@/components/Link'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
interface PaginationProps {
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
}
|
||||
interface ListLayoutProps {
|
||||
posts: CoreContent<Blog>[]
|
||||
title: string
|
||||
initialDisplayPosts?: CoreContent<Blog>[]
|
||||
pagination?: PaginationProps
|
||||
}
|
||||
|
||||
function Pagination({ totalPages, currentPage }: PaginationProps) {
|
||||
const pathname = usePathname() as string
|
||||
const basePath = pathname.split('/')[1]
|
||||
const prevPage = currentPage - 1 > 0
|
||||
const nextPage = currentPage + 1 <= totalPages
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||
<nav className="flex justify-between">
|
||||
{!prevPage && (
|
||||
<button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
||||
Previous
|
||||
</button>
|
||||
)}
|
||||
{prevPage && (
|
||||
<Link
|
||||
href={currentPage - 1 === 1 ? `/${basePath}/` : `/${basePath}/page/${currentPage - 1}`}
|
||||
rel="prev"
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span>
|
||||
{currentPage} of {totalPages}
|
||||
</span>
|
||||
{!nextPage && (
|
||||
<button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
||||
Next
|
||||
</button>
|
||||
)}
|
||||
{nextPage && (
|
||||
<Link href={`/${basePath}/page/${currentPage + 1}`} rel="next">
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ListLayout({
|
||||
posts,
|
||||
title,
|
||||
initialDisplayPosts = [],
|
||||
pagination,
|
||||
}: ListLayoutProps) {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const filteredBlogPosts = posts.filter((post) => {
|
||||
const searchContent = post.title + post.summary + post.tags?.join(' ')
|
||||
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
|
||||
})
|
||||
|
||||
// If initialDisplayPosts exist, display it if no searchValue is specified
|
||||
const displayPosts =
|
||||
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pb-8 pt-6 md:space-y-5">
|
||||
<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-6xl md:leading-14">
|
||||
{title}
|
||||
</h1>
|
||||
<div className="relative max-w-lg">
|
||||
<label>
|
||||
<span className="sr-only">Search articles</span>
|
||||
<input
|
||||
aria-label="Search articles"
|
||||
type="text"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder="Search articles"
|
||||
className="block w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-900 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</label>
|
||||
<svg
|
||||
className="absolute right-3 top-3 h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{!filteredBlogPosts.length && 'No posts found.'}
|
||||
{displayPosts.map((post) => {
|
||||
const { path, date, title, summary, tags } = post
|
||||
return (
|
||||
<li key={path} className="py-4">
|
||||
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-3 xl:col-span-3">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-8 tracking-tight">
|
||||
<Link href={`/${path}`} className="text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="flex flex-wrap">
|
||||
{tags?.map((tag) => (
|
||||
<Tag key={tag} text={tag} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose max-w-none text-gray-500 dark:text-gray-400">
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{pagination && pagination.totalPages > 1 && !searchValue && (
|
||||
<Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,31 +1,43 @@
|
||||
import { useState, ReactNode } from 'react'
|
||||
import { Comments } from 'pliny/comments'
|
||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
||||
import type { Blog, Authors } from 'contentlayer/generated'
|
||||
import Link from '@/components/Link'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import { BlogSEO } from '@/components/SEO'
|
||||
// import { BlogSEO } from '@/components/SEO'
|
||||
import Image from '@/components/Image'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import Comments from '@/components/comments'
|
||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||
|
||||
const editUrl = (fileName) => `${siteMetadata.siteRepo}/blob/master/data/blog/${fileName}`
|
||||
const discussUrl = (slug) =>
|
||||
`https://mobile.twitter.com/search?q=${encodeURIComponent(
|
||||
`${siteMetadata.siteUrl}/blog/${slug}`
|
||||
)}`
|
||||
const editUrl = (path) => `${siteMetadata.siteRepo}/blob/master/data/${path}`
|
||||
const discussUrl = (path) =>
|
||||
`https://mobile.twitter.com/search?q=${encodeURIComponent(`${siteMetadata.siteUrl}/${path}`)}`
|
||||
|
||||
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
|
||||
const postDateTemplate: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}
|
||||
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||
const { slug, fileName, date, title, images, tags } = frontMatter
|
||||
interface LayoutProps {
|
||||
content: CoreContent<Blog>
|
||||
authorDetails: CoreContent<Authors>[]
|
||||
next?: { path: string; title: string }
|
||||
prev?: { path: string; title: string }
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function PostLayout({ content, authorDetails, next, prev, children }: LayoutProps) {
|
||||
const { filePath, path, slug, date, title, tags } = content
|
||||
const basePath = path.split('/')[0]
|
||||
const [loadComments, setLoadComments] = useState(false)
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<BlogSEO
|
||||
url={`${siteMetadata.siteUrl}/blog/${slug}`}
|
||||
authorDetails={authorDetails}
|
||||
{...frontMatter}
|
||||
/>
|
||||
{/* <BlogSEO url={`${siteMetadata.siteUrl}/${path}`} authorDetails={authorDetails} {...content} /> */}
|
||||
<ScrollTopAndComment />
|
||||
<article>
|
||||
<div className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700">
|
||||
@ -46,21 +58,18 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0"
|
||||
style={{ gridTemplateRows: 'auto 1fr' }}
|
||||
>
|
||||
<dl className="pt-6 pb-10 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700">
|
||||
<div className="grid-rows-[auto_1fr] divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0">
|
||||
<dl className="pb-10 pt-6 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700">
|
||||
<dt className="sr-only">Authors</dt>
|
||||
<dd>
|
||||
<ul className="flex justify-center space-x-8 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) => (
|
||||
<li className="flex items-center space-x-2" key={author.name}>
|
||||
{author.avatar && (
|
||||
<Image
|
||||
src={author.avatar}
|
||||
width="38px"
|
||||
height="38px"
|
||||
width={38}
|
||||
height={38}
|
||||
alt="avatar"
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
@ -86,15 +95,25 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</dd>
|
||||
</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="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
|
||||
<div className="pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
<Link href={discussUrl(slug)} rel="nofollow">
|
||||
{'Discuss on Twitter'}
|
||||
<div className="prose max-w-none pb-8 pt-10 dark:prose-dark">{children}</div>
|
||||
<div className="pb-6 pt-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
<Link href={discussUrl(path)} rel="nofollow">
|
||||
Discuss on Twitter
|
||||
</Link>
|
||||
{` • `}
|
||||
<Link href={editUrl(fileName)}>{'View on GitHub'}</Link>
|
||||
<Link href={editUrl(filePath)}>View on GitHub</Link>
|
||||
</div>
|
||||
<Comments frontMatter={frontMatter} />
|
||||
{siteMetadata.comments && (
|
||||
<div
|
||||
className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300"
|
||||
id="comment"
|
||||
>
|
||||
{!loadComments && (
|
||||
<button onClick={() => setLoadComments(true)}>Load Comments</button>
|
||||
)}
|
||||
{loadComments && <Comments commentsConfig={siteMetadata.comments} slug={slug} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<footer>
|
||||
<div className="divide-gray-200 text-sm font-medium leading-5 dark:divide-gray-700 xl:col-start-1 xl:row-start-2 xl:divide-y">
|
||||
@ -118,7 +137,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
Previous Article
|
||||
</h2>
|
||||
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<Link href={`/blog/${prev.slug}`}>{prev.title}</Link>
|
||||
<Link href={`/${prev.path}`}>{prev.title}</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -128,7 +147,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
Next Article
|
||||
</h2>
|
||||
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<Link href={`/blog/${next.slug}`}>{next.title}</Link>
|
||||
<Link href={`/${next.path}`}>{next.title}</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -137,8 +156,9 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</div>
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href="/blog"
|
||||
href={`/${basePath}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="Back to the blog"
|
||||
>
|
||||
← Back to the blog
|
||||
</Link>
|
@ -1,18 +1,30 @@
|
||||
import { useState, ReactNode } from 'react'
|
||||
import { Comments } from 'pliny/comments'
|
||||
import { formatDate } from 'pliny/utils/formatDate'
|
||||
import { CoreContent } from 'pliny/utils/contentlayer'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
import Link from '@/components/Link'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import { BlogSEO } from '@/components/SEO'
|
||||
// import { BlogSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import Comments from '@/components/comments'
|
||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||
const { date, title } = frontMatter
|
||||
interface LayoutProps {
|
||||
content: CoreContent<Blog>
|
||||
children: ReactNode
|
||||
next?: { path: string; title: string }
|
||||
prev?: { path: string; title: string }
|
||||
}
|
||||
|
||||
export default function PostLayout({ content, next, prev, children }: LayoutProps) {
|
||||
const [loadComments, setLoadComments] = useState(false)
|
||||
|
||||
const { path, slug, date, title } = content
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
|
||||
{/* <BlogSEO url={`${siteMetadata.siteUrl}/${path}`} {...content} /> */}
|
||||
<ScrollTopAndComment />
|
||||
<article>
|
||||
<div>
|
||||
@ -22,7 +34,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
<div>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
<time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@ -31,21 +43,26 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:divide-y-0 "
|
||||
style={{ gridTemplateRows: 'auto 1fr' }}
|
||||
>
|
||||
<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="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
|
||||
<div className="prose max-w-none pb-8 pt-10 dark:prose-dark">{children}</div>
|
||||
</div>
|
||||
<Comments frontMatter={frontMatter} />
|
||||
{siteMetadata.comments && (
|
||||
<div className="pb-6 pt-6 text-center text-gray-700 dark:text-gray-300" id="comment">
|
||||
{!loadComments && (
|
||||
<button onClick={() => setLoadComments(true)}>Load Comments</button>
|
||||
)}
|
||||
{loadComments && <Comments commentsConfig={siteMetadata.comments} slug={slug} />}
|
||||
</div>
|
||||
)}
|
||||
<footer>
|
||||
<div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base">
|
||||
{prev && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/blog/${prev.slug}`}
|
||||
href={`/${prev.path}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Previous post: ${prev.title}`}
|
||||
>
|
||||
← {prev.title}
|
||||
</Link>
|
||||
@ -54,8 +71,9 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
{next && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/blog/${next.slug}`}
|
||||
href={`/${next.path}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Next post: ${next.title}`}
|
||||
>
|
||||
{next.title} →
|
||||
</Link>
|
@ -1,32 +0,0 @@
|
||||
import { escape } from '@/lib/utils/htmlEscaper'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const generateRssItem = (post) => `
|
||||
<item>
|
||||
<guid>${siteMetadata.siteUrl}/blog/${post.slug}</guid>
|
||||
<title>${escape(post.title)}</title>
|
||||
<link>${siteMetadata.siteUrl}/blog/${post.slug}</link>
|
||||
${post.summary && `<description>${escape(post.summary)}</description>`}
|
||||
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
|
||||
<author>${siteMetadata.email} (${siteMetadata.author})</author>
|
||||
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
|
||||
</item>
|
||||
`
|
||||
|
||||
const generateRss = (posts, page = 'feed.xml') => `
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>${escape(siteMetadata.title)}</title>
|
||||
<link>${siteMetadata.siteUrl}/blog</link>
|
||||
<description>${escape(siteMetadata.description)}</description>
|
||||
<language>${siteMetadata.language}</language>
|
||||
<managingEditor>${siteMetadata.email} (${siteMetadata.author})</managingEditor>
|
||||
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
|
||||
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
|
||||
<atom:link href="${siteMetadata.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
|
||||
${posts.map(generateRssItem).join('')}
|
||||
</channel>
|
||||
</rss>
|
||||
`
|
||||
export default generateRss
|
136
lib/mdx.js
136
lib/mdx.js
@ -1,136 +0,0 @@
|
||||
import { bundleMDX } from 'mdx-bundler'
|
||||
import fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import path from 'path'
|
||||
import readingTime from 'reading-time'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import getAllFilesRecursively from './utils/files'
|
||||
// Remark packages
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkFootnotes from 'remark-footnotes'
|
||||
import remarkMath from 'remark-math'
|
||||
import remarkExtractFrontmatter from './remark-extract-frontmatter'
|
||||
import remarkCodeTitles from './remark-code-title'
|
||||
import remarkTocHeadings from './remark-toc-headings'
|
||||
import remarkImgToJsx from './remark-img-to-jsx'
|
||||
// Rehype packages
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeCitation from 'rehype-citation'
|
||||
import rehypePrismPlus from 'rehype-prism-plus'
|
||||
import rehypePresetMinify from 'rehype-preset-minify'
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
export function getFiles(type) {
|
||||
const prefixPaths = path.join(root, 'data', type)
|
||||
const files = getAllFilesRecursively(prefixPaths)
|
||||
// Only want to return blog/path and ignore root, replace is needed to work on Windows
|
||||
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
|
||||
}
|
||||
|
||||
export function formatSlug(slug) {
|
||||
return slug.replace(/\.(mdx|md)/, '')
|
||||
}
|
||||
|
||||
export function dateSortDesc(a, b) {
|
||||
if (a > b) return -1
|
||||
if (a < b) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
export async function getFileBySlug(type, slug) {
|
||||
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
|
||||
const mdPath = path.join(root, 'data', type, `${slug}.md`)
|
||||
const source = fs.existsSync(mdxPath)
|
||||
? fs.readFileSync(mdxPath, 'utf8')
|
||||
: fs.readFileSync(mdPath, 'utf8')
|
||||
|
||||
// https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent
|
||||
if (process.platform === 'win32') {
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'esbuild.exe')
|
||||
} else {
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'bin', 'esbuild')
|
||||
}
|
||||
|
||||
let toc = []
|
||||
|
||||
const { code, frontmatter } = await bundleMDX({
|
||||
source,
|
||||
// mdx imports can be automatically source from the components directory
|
||||
cwd: path.join(root, 'components'),
|
||||
xdmOptions(options, frontmatter) {
|
||||
// this is the recommended way to add custom remark/rehype plugins:
|
||||
// The syntax might look weird, but it protects you in case we add/remove
|
||||
// plugins in the future.
|
||||
options.remarkPlugins = [
|
||||
...(options.remarkPlugins ?? []),
|
||||
remarkExtractFrontmatter,
|
||||
[remarkTocHeadings, { exportRef: toc }],
|
||||
remarkGfm,
|
||||
remarkCodeTitles,
|
||||
[remarkFootnotes, { inlineNotes: true }],
|
||||
remarkMath,
|
||||
remarkImgToJsx,
|
||||
]
|
||||
options.rehypePlugins = [
|
||||
...(options.rehypePlugins ?? []),
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
rehypeKatex,
|
||||
[rehypeCitation, { path: path.join(root, 'data') }],
|
||||
[rehypePrismPlus, { ignoreMissing: true }],
|
||||
rehypePresetMinify,
|
||||
]
|
||||
return options
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.loader = {
|
||||
...options.loader,
|
||||
'.js': 'jsx',
|
||||
}
|
||||
return options
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
mdxSource: code,
|
||||
toc,
|
||||
frontMatter: {
|
||||
readingTime: readingTime(code),
|
||||
slug: slug || null,
|
||||
fileName: fs.existsSync(mdxPath) ? `${slug}.mdx` : `${slug}.md`,
|
||||
...frontmatter,
|
||||
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllFilesFrontMatter(folder) {
|
||||
const prefixPaths = path.join(root, 'data', folder)
|
||||
|
||||
const files = getAllFilesRecursively(prefixPaths)
|
||||
|
||||
const allFrontMatter = []
|
||||
|
||||
files.forEach((file) => {
|
||||
// Replace is needed to work on Windows
|
||||
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
|
||||
// Remove Unexpected File
|
||||
if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') {
|
||||
return
|
||||
}
|
||||
const source = fs.readFileSync(file, 'utf8')
|
||||
const { data: frontmatter } = matter(source)
|
||||
if (frontmatter.draft !== true) {
|
||||
allFrontMatter.push({
|
||||
...frontmatter,
|
||||
slug: formatSlug(fileName),
|
||||
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
export default function remarkCodeTitles() {
|
||||
return (tree) =>
|
||||
visit(tree, 'code', (node, index, parent) => {
|
||||
const nodeLang = node.lang || ''
|
||||
let language = ''
|
||||
let title = ''
|
||||
|
||||
if (nodeLang.includes(':')) {
|
||||
language = nodeLang.slice(0, nodeLang.search(':'))
|
||||
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length)
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
return
|
||||
}
|
||||
|
||||
const className = 'remark-code-title'
|
||||
|
||||
const titleNode = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'div',
|
||||
attributes: [{ type: 'mdxJsxAttribute', name: 'className', value: className }],
|
||||
children: [{ type: 'text', value: title }],
|
||||
data: { _xdmExplicitJsx: true },
|
||||
}
|
||||
|
||||
parent.children.splice(index, 0, titleNode)
|
||||
node.lang = language
|
||||
})
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { load } from 'js-yaml'
|
||||
|
||||
export default function extractFrontmatter() {
|
||||
return (tree, file) => {
|
||||
visit(tree, 'yaml', (node, index, parent) => {
|
||||
file.data.frontmatter = load(node.value)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { visit } from 'unist-util-visit'
|
||||
import sizeOf from 'image-size'
|
||||
import fs from 'fs'
|
||||
|
||||
export default function remarkImgToJsx() {
|
||||
return (tree) => {
|
||||
visit(
|
||||
tree,
|
||||
// only visit p tags that contain an img element
|
||||
(node) => node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
|
||||
(node) => {
|
||||
const imageNode = node.children.find((n) => n.type === 'image')
|
||||
|
||||
// only local files
|
||||
if (fs.existsSync(`${process.cwd()}/public${imageNode.url}`)) {
|
||||
const dimensions = sizeOf(`${process.cwd()}/public${imageNode.url}`)
|
||||
|
||||
// Convert original node to next/image
|
||||
;(imageNode.type = 'mdxJsxFlowElement'),
|
||||
(imageNode.name = 'Image'),
|
||||
(imageNode.attributes = [
|
||||
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
|
||||
{ type: 'mdxJsxAttribute', name: 'src', value: imageNode.url },
|
||||
{ type: 'mdxJsxAttribute', name: 'width', value: dimensions.width },
|
||||
{ type: 'mdxJsxAttribute', name: 'height', value: dimensions.height },
|
||||
])
|
||||
|
||||
// Change node type from p to div to avoid nesting error
|
||||
node.type = 'div'
|
||||
node.children = [imageNode]
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { slug } from 'github-slugger'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
|
||||
export default function remarkTocHeadings(options) {
|
||||
return (tree) =>
|
||||
visit(tree, 'heading', (node, index, parent) => {
|
||||
const textContent = toString(node)
|
||||
options.exportRef.push({
|
||||
value: textContent,
|
||||
url: '#' + slug(textContent),
|
||||
depth: node.depth,
|
||||
})
|
||||
})
|
||||
}
|
30
lib/tags.js
30
lib/tags.js
@ -1,30 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import path from 'path'
|
||||
import { getFiles } from './mdx'
|
||||
import kebabCase from './utils/kebabCase'
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
export async function getAllTags(type) {
|
||||
const files = await getFiles(type)
|
||||
|
||||
let tagCount = {}
|
||||
// Iterate through each post, putting all found tags into `tags`
|
||||
files.forEach((file) => {
|
||||
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
|
||||
const { data } = matter(source)
|
||||
if (data.tags && data.draft !== true) {
|
||||
data.tags.forEach((tag) => {
|
||||
const formattedTag = kebabCase(tag)
|
||||
if (formattedTag in tagCount) {
|
||||
tagCount[formattedTag] += 1
|
||||
} else {
|
||||
tagCount[formattedTag] = 1
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return tagCount
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const pipe =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduce((v, f) => f(v), x)
|
||||
|
||||
const flattenArray = (input) =>
|
||||
input.reduce((acc, item) => [...acc, ...(Array.isArray(item) ? item : [item])], [])
|
||||
|
||||
const map = (fn) => (input) => input.map(fn)
|
||||
|
||||
const walkDir = (fullPath) => {
|
||||
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
|
||||
}
|
||||
|
||||
const pathJoinPrefix = (prefix) => (extraPath) => path.join(prefix, extraPath)
|
||||
|
||||
const getAllFilesRecursively = (folder) =>
|
||||
pipe(fs.readdirSync, map(pipe(pathJoinPrefix(folder), walkDir)), flattenArray)(folder)
|
||||
|
||||
export default getAllFilesRecursively
|
@ -1,14 +0,0 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const formatDate = (date) => {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}
|
||||
const now = new Date(date).toLocaleDateString(siteMetadata.locale, options)
|
||||
|
||||
return now
|
||||
}
|
||||
|
||||
export default formatDate
|
@ -1,23 +0,0 @@
|
||||
const { replace } = ''
|
||||
|
||||
// escape
|
||||
const es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g
|
||||
const ca = /[&<>'"]/g
|
||||
|
||||
const esca = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"',
|
||||
}
|
||||
const pe = (m) => esca[m]
|
||||
|
||||
/**
|
||||
* Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
|
||||
* @param {string} es the input to safely escape
|
||||
* @returns {string} the escaped input, and it **throws** an error if
|
||||
* the input type is unexpected, except for boolean and numbers,
|
||||
* converted as string.
|
||||
*/
|
||||
export const escape = (es) => replace.call(es, ca, pe)
|
@ -1,5 +0,0 @@
|
||||
import { slug } from 'github-slugger'
|
||||
|
||||
const kebabCase = (str) => slug(str)
|
||||
|
||||
export default kebabCase
|
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -1,3 +1,5 @@
|
||||
const { withContentlayer } = require('next-contentlayer')
|
||||
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
})
|
||||
@ -52,36 +54,35 @@ const securityHeaders = [
|
||||
},
|
||||
]
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
|
||||
eslint: {
|
||||
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: securityHeaders,
|
||||
},
|
||||
]
|
||||
},
|
||||
webpack: (config, { dev, isServer }) => {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
})
|
||||
|
||||
if (!dev && !isServer) {
|
||||
// Replace React with Preact only in client production build
|
||||
Object.assign(config.resolve.alias, {
|
||||
'react/jsx-runtime.js': 'preact/compat/jsx-runtime',
|
||||
react: 'preact/compat',
|
||||
'react-dom/test-utils': 'preact/test-utils',
|
||||
'react-dom': 'preact/compat',
|
||||
/**
|
||||
* @type {import('next/dist/next-server/server/config').NextConfig}
|
||||
**/
|
||||
module.exports = () => {
|
||||
const plugins = [withContentlayer, withBundleAnalyzer]
|
||||
return plugins.reduce((acc, next) => next(acc), {
|
||||
reactStrictMode: true,
|
||||
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
|
||||
eslint: {
|
||||
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
||||
},
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: securityHeaders,
|
||||
},
|
||||
]
|
||||
},
|
||||
webpack: (config, options) => {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
})
|
||||
return config
|
||||
},
|
||||
})
|
||||
}
|
||||
|
10657
package-lock.json
generated
10657
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
86
package.json
86
package.json
@ -3,64 +3,64 @@
|
||||
"version": "1.5.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "cross-env SOCKET=true node ./scripts/next-remote-watch.js ./data",
|
||||
"dev": "next dev",
|
||||
"build": "next build && node ./scripts/generate-sitemap",
|
||||
"start": "next dev",
|
||||
"dev": "cross-env INIT_CWD=$PWD next dev",
|
||||
"build": "cross-env INIT_CWD=$PWD next build && cross-env NODE_OPTIONS='--experimental-json-modules' node -r esbuild-register ./scripts/postbuild.mjs",
|
||||
"serve": "next start",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"lint": "next lint --fix --dir pages --dir components --dir lib --dir layouts --dir scripts",
|
||||
"prepare": "husky install"
|
||||
"lint": "next lint --fix --dir pages --dir components --dir lib --dir layouts --dir scripts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "4.5.2",
|
||||
"@mailchimp/mailchimp_marketing": "^3.0.58",
|
||||
"@next/bundle-analyzer": "^12.1.4",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"esbuild": "^0.13.13",
|
||||
"github-slugger": "^1.3.0",
|
||||
"@next/bundle-analyzer": "13.4.8",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"contentlayer": "0.3.4",
|
||||
"esbuild": "0.18.11",
|
||||
"github-slugger": "^1.4.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"image-size": "1.0.0",
|
||||
"mdx-bundler": "^8.0.0",
|
||||
"next": "12.1.4",
|
||||
"next-themes": "^0.0.14",
|
||||
"postcss": "^8.4.5",
|
||||
"preact": "^10.6.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"reading-time": "1.3.0",
|
||||
"mdx-bundler": "^9.2.1",
|
||||
"next": "13.4.8",
|
||||
"next-contentlayer": "0.3.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"pliny": "0.0.10",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"reading-time": "1.5.0",
|
||||
"rehype-autolink-headings": "^6.1.0",
|
||||
"rehype-citation": "^0.4.0",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"rehype-citation": "^1.0.2",
|
||||
"rehype-katex": "^6.0.3",
|
||||
"rehype-preset-minify": "6.0.0",
|
||||
"rehype-prism-plus": "^1.1.3",
|
||||
"rehype-slug": "^5.0.0",
|
||||
"remark-footnotes": "^4.0.1",
|
||||
"rehype-prism-plus": "^1.6.0",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark": "^14.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sharp": "^0.28.3",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"unist-util-visit": "^4.0.0"
|
||||
"tailwindcss": "^3.3.2",
|
||||
"unist-util-visit": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^6.1.2",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@types/react": "^18.2.14",
|
||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||
"@typescript-eslint/parser": "^5.61.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dedent": "^0.7.0",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-next": "12.1.4",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"file-loader": "^6.0.0",
|
||||
"esbuild-register": "3.4.2",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-next": "13.4.7",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"globby": "11.0.3",
|
||||
"husky": "^6.0.0",
|
||||
"inquirer": "^8.1.1",
|
||||
"lint-staged": "^11.0.0",
|
||||
"next-remote-watch": "^1.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.4",
|
||||
"socket.io": "^4.4.0",
|
||||
"socket.io-client": "^4.4.0"
|
||||
"husky": "^8.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.+(js|jsx|ts|tsx)": [
|
||||
|
@ -1,31 +0,0 @@
|
||||
import '@/css/tailwind.css'
|
||||
import '@/css/prism.css'
|
||||
import 'katex/dist/katex.css'
|
||||
|
||||
import '@fontsource/inter/variable-full.css'
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import Head from 'next/head'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import Analytics from '@/components/analytics'
|
||||
import LayoutWrapper from '@/components/LayoutWrapper'
|
||||
import { ClientReload } from '@/components/ClientReload'
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isSocket = process.env.SOCKET
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
|
||||
<Head>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
</Head>
|
||||
{isDevelopment && isSocket && <ClientReload />}
|
||||
<Analytics />
|
||||
<LayoutWrapper>
|
||||
<Component {...pageProps} />
|
||||
</LayoutWrapper>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en" className="scroll-smooth">
|
||||
<Head>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/static/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/static/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
</Head>
|
||||
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
@ -1,21 +0,0 @@
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { getFileBySlug } from '@/lib/mdx'
|
||||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const authorDetails = await getFileBySlug('authors', ['default'])
|
||||
return { props: { authorDetails } }
|
||||
}
|
||||
|
||||
export default function About({ authorDetails }) {
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
/>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user