refactor: migrate to rsc and app dir
This commit is contained in:
		
							
								
								
									
										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_REPO= | ||||||
| NEXT_PUBLIC_GISCUS_REPOSITORY_ID= | NEXT_PUBLIC_GISCUS_REPOSITORY_ID= | ||||||
| NEXT_PUBLIC_GISCUS_CATEGORY= | NEXT_PUBLIC_GISCUS_CATEGORY= | ||||||
| @@ -10,20 +11,16 @@ MAILCHIMP_API_KEY= | |||||||
| MAILCHIMP_API_SERVER= | MAILCHIMP_API_SERVER= | ||||||
| MAILCHIMP_AUDIENCE_ID= | MAILCHIMP_AUDIENCE_ID= | ||||||
|  |  | ||||||
| BUTTONDOWN_API_URL=https://api.buttondown.email/v1/ |  | ||||||
| BUTTONDOWN_API_KEY= | BUTTONDOWN_API_KEY= | ||||||
|  |  | ||||||
| CONVERTKIT_API_URL=https://api.convertkit.com/v3/ |  | ||||||
| CONVERTKIT_API_KEY= | CONVERTKIT_API_KEY= | ||||||
| // curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID  | # curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID | ||||||
| CONVERTKIT_FORM_ID=  | CONVERTKIT_FORM_ID= | ||||||
|  |  | ||||||
| KLAVIYO_API_KEY= | KLAVIYO_API_KEY= | ||||||
| KLAVIYO_LIST_ID= | KLAVIYO_LIST_ID= | ||||||
|  |  | ||||||
| REVUE_API_URL=https://www.getrevue.co/api/v2/ |  | ||||||
| REVUE_API_KEY= | REVUE_API_KEY= | ||||||
|  |  | ||||||
| EMAILOCTOPUS_API_URL=https://emailoctopus.com/api/1.6/ |  | ||||||
| EMAILOCTOPUS_API_KEY= | EMAILOCTOPUS_API_KEY= | ||||||
| EMAILOCTOPUS_LIST_ID= | EMAILOCTOPUS_LIST_ID= | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								.eslintrc.js
									
									
									
									
									
								
							| @@ -1,17 +1,38 @@ | |||||||
| module.exports = { | module.exports = { | ||||||
|   root: true, |   root: true, | ||||||
|  |   parser: '@typescript-eslint/parser', | ||||||
|   env: { |   env: { | ||||||
|     browser: true, |     browser: true, | ||||||
|     amd: true, |     amd: true, | ||||||
|     node: true, |     node: true, | ||||||
|     es6: 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: { |   rules: { | ||||||
|     'prettier/prettier': 'error', |     'prettier/prettier': 'error', | ||||||
|     'react/react-in-jsx-scope': 'off', |     'react/react-in-jsx-scope': 'off', | ||||||
|  |     'jsx-a11y/anchor-is-valid': [ | ||||||
|  |       'error', | ||||||
|  |       { | ||||||
|  |         components: ['Link'], | ||||||
|  |         specialLink: ['hrefLeft', 'hrefRight'], | ||||||
|  |         aspects: ['invalidHref', 'preferButton'], | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|     'react/prop-types': 0, |     'react/prop-types': 0, | ||||||
|     'no-unused-vars': 0, |     'no-unused-vars': 0, | ||||||
|     'react/no-unescaped-entities': 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 | ## 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 | ## GITATTRIBUTES FOR WEB PROJECTS | ||||||
| # | # | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,10 @@ | |||||||
| /node_modules | /node_modules | ||||||
| /.pnp | /.pnp | ||||||
| .pnp.js | .pnp.js | ||||||
|  | /.yarn/* | ||||||
|  | !/.yarn/releases | ||||||
|  | !/.yarn/plugins | ||||||
|  | !/.yarn/sdks | ||||||
|  |  | ||||||
| # testing | # testing | ||||||
| /coverage | /coverage | ||||||
| @@ -17,8 +21,12 @@ public/sitemap.xml | |||||||
| # production | # production | ||||||
| /build | /build | ||||||
| *.xml | *.xml | ||||||
|  |  | ||||||
| # rss feed | # rss feed | ||||||
| /public/feed.xml  | /public/feed.xml | ||||||
|  |  | ||||||
|  | # search | ||||||
|  | /public/search.json | ||||||
|  |  | ||||||
| # misc | # misc | ||||||
| .DS_Store | .DS_Store | ||||||
| @@ -33,4 +41,7 @@ yarn-error.log* | |||||||
| .env.local | .env.local | ||||||
| .env.development.local | .env.development.local | ||||||
| .env.test.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 Link from '@/components/Link' | ||||||
| import { PageSEO } from '@/components/SEO' | // import { PageSEO } from '@/components/SEO'
 | ||||||
| import Tag from '@/components/Tag' | import Tag from '@/components/Tag' | ||||||
| import siteMetadata from '@/data/siteMetadata' | import siteMetadata from '@/data/siteMetadata' | ||||||
| import { getAllFilesFrontMatter } from '@/lib/mdx' | import { formatDate } from 'pliny/utils/formatDate' | ||||||
| import formatDate from '@/lib/utils/formatDate' | import { NewsletterForm } from 'pliny/ui/NewsletterForm' | ||||||
| 
 |  | ||||||
| import NewsletterForm from '@/components/NewsletterForm' |  | ||||||
| 
 | 
 | ||||||
| const MAX_DISPLAY = 5 | const MAX_DISPLAY = 5 | ||||||
| 
 | 
 | ||||||
| export async function getStaticProps() { |  | ||||||
|   const posts = await getAllFilesFrontMatter('blog') |  | ||||||
| 
 |  | ||||||
|   return { props: { posts } } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default function Home({ posts }) { | export default function Home({ posts }) { | ||||||
|   return ( |   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="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"> |           <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 |             Latest | ||||||
|           </h1> |           </h1> | ||||||
| @@ -30,8 +24,8 @@ export default function Home({ posts }) { | |||||||
|         </div> |         </div> | ||||||
|         <ul className="divide-y divide-gray-200 dark:divide-gray-700"> |         <ul className="divide-y divide-gray-200 dark:divide-gray-700"> | ||||||
|           {!posts.length && 'No posts found.'} |           {!posts.length && 'No posts found.'} | ||||||
|           {posts.slice(0, MAX_DISPLAY).map((frontMatter) => { |           {posts.slice(0, MAX_DISPLAY).map((post) => { | ||||||
|             const { slug, date, title, summary, tags } = frontMatter |             const { slug, date, title, summary, tags } = post | ||||||
|             return ( |             return ( | ||||||
|               <li key={slug} className="py-12"> |               <li key={slug} className="py-12"> | ||||||
|                 <article> |                 <article> | ||||||
| @@ -39,7 +33,7 @@ export default function Home({ posts }) { | |||||||
|                     <dl> |                     <dl> | ||||||
|                       <dt className="sr-only">Published on</dt> |                       <dt className="sr-only">Published on</dt> | ||||||
|                       <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400"> |                       <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400"> | ||||||
|                         <time dateTime={date}>{formatDate(date)}</time> |                         <time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time> | ||||||
|                       </dd> |                       </dd> | ||||||
|                     </dl> |                     </dl> | ||||||
|                     <div className="space-y-5 xl:col-span-3"> |                     <div className="space-y-5 xl:col-span-3"> | ||||||
| @@ -85,13 +79,13 @@ export default function Home({ posts }) { | |||||||
|           <Link |           <Link | ||||||
|             href="/blog" |             href="/blog" | ||||||
|             className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" |             className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" | ||||||
|             aria-label="all posts" |             aria-label="All posts" | ||||||
|           > |           > | ||||||
|             All Posts → |             All Posts → | ||||||
|           </Link> |           </Link> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
|       {siteMetadata.newsletter.provider !== '' && ( |       {siteMetadata.newsletter?.provider && ( | ||||||
|         <div className="flex items-center justify-center pt-4"> |         <div className="flex items-center justify-center pt-4"> | ||||||
|           <NewsletterForm /> |           <NewsletterForm /> | ||||||
|         </div> |         </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 Link from '@/components/Link' | ||||||
| import { PageSEO } from '@/components/SEO' | // import { PageSEO } from '@/components/SEO'
 | ||||||
| import siteMetadata from '@/data/siteMetadata' |  | ||||||
| 
 | 
 | ||||||
| export default function FourZeroFour() { | export default function NotFound() { | ||||||
|   return ( |   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="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"> |           <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 |             404 | ||||||
|           </h1> |           </h1> | ||||||
| @@ -19,10 +18,11 @@ export default function FourZeroFour() { | |||||||
|           <p className="mb-8"> |           <p className="mb-8"> | ||||||
|             But dont worry, you can find plenty of other things on our homepage. |             But dont worry, you can find plenty of other things on our homepage. | ||||||
|           </p> |           </p> | ||||||
|           <Link href="/"> |           <Link | ||||||
|             <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"> |             href="/" | ||||||
|               Back to homepage |             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" | ||||||
|             </button> |           > | ||||||
|  |             Back to homepage | ||||||
|           </Link> |           </Link> | ||||||
|         </div> |         </div> | ||||||
|       </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 siteMetadata from '@/data/siteMetadata' | ||||||
| import projectsData from '@/data/projectsData' | import projectsData from '@/data/projectsData' | ||||||
| import Card from '@/components/Card' | import Card from '@/components/Card' | ||||||
| import { PageSEO } from '@/components/SEO' |  | ||||||
| 
 | 
 | ||||||
| export default function Projects() { | export default function Projects() { | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} /> |  | ||||||
|       <div className="divide-y divide-gray-200 dark:divide-gray-700"> |       <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"> |           <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 |             Projects | ||||||
|           </h1> |           </h1> | ||||||
| @@ -1,23 +1,19 @@ | |||||||
|  | import { getAllTags } from 'pliny/utils/contentlayer' | ||||||
| import Link from '@/components/Link' | import Link from '@/components/Link' | ||||||
| import { PageSEO } from '@/components/SEO' | // import { PageSEO } from '@/components/SEO'
 | ||||||
| import Tag from '@/components/Tag' | import Tag from '@/components/Tag' | ||||||
| import siteMetadata from '@/data/siteMetadata' | import siteMetadata from '@/data/siteMetadata' | ||||||
| import { getAllTags } from '@/lib/tags' | import { kebabCase } from 'pliny/utils/kebabCase' | ||||||
| import kebabCase from '@/lib/utils/kebabCase' | import { allBlogs } from 'contentlayer/generated' | ||||||
| 
 | 
 | ||||||
| export async function getStaticProps() { | export default async function Page() { | ||||||
|   const tags = await getAllTags('blog') |   const tags = await getAllTags(allBlogs) | ||||||
| 
 |  | ||||||
|   return { props: { tags } } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default function Tags({ tags }) { |  | ||||||
|   const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a]) |   const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a]) | ||||||
|   return ( |   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="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"> |           <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 |             Tags | ||||||
|           </h1> |           </h1> | ||||||
| @@ -26,11 +22,12 @@ export default function Tags({ tags }) { | |||||||
|           {Object.keys(tags).length === 0 && 'No tags found.'} |           {Object.keys(tags).length === 0 && 'No tags found.'} | ||||||
|           {sortedTags.map((t) => { |           {sortedTags.map((t) => { | ||||||
|             return ( |             return ( | ||||||
|               <div key={t} className="mt-2 mb-2 mr-5"> |               <div key={t} className="mb-2 mr-5 mt-2"> | ||||||
|                 <Tag text={t} /> |                 <Tag text={t} /> | ||||||
|                 <Link |                 <Link | ||||||
|                   href={`/tags/${kebabCase(t)}`} |                   href={`/tags/${kebabCase(t)}`} | ||||||
|                   className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300" |                   className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300" | ||||||
|  |                   aria-label={`View posts tagged ${t}`} | ||||||
|                 > |                 > | ||||||
|                   {` (${tags[t]})`} |                   {` (${tags[t]})`} | ||||||
|                 </Link> |                 </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' | import Link from './Link' | ||||||
| 
 | 
 | ||||||
| const Card = ({ title, description, imgSrc, href }) => ( | 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 |     <div | ||||||
|       className={`${ |       className={`${ | ||||||
|         imgSrc && 'h-full' |         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> |     <footer> | ||||||
|       <div className="mt-16 flex flex-col items-center"> |       <div className="mt-16 flex flex-col items-center"> | ||||||
|         <div className="mb-3 flex space-x-4"> |         <div className="mb-3 flex space-x-4"> | ||||||
|           <SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size="6" /> |           <SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} /> | ||||||
|           <SocialIcon kind="github" href={siteMetadata.github} size="6" /> |           <SocialIcon kind="github" href={siteMetadata.github} size={6} /> | ||||||
|           <SocialIcon kind="facebook" href={siteMetadata.facebook} size="6" /> |           <SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} /> | ||||||
|           <SocialIcon kind="youtube" href={siteMetadata.youtube} size="6" /> |           <SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} /> | ||||||
|           <SocialIcon kind="linkedin" href={siteMetadata.linkedin} size="6" /> |           <SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} /> | ||||||
|           <SocialIcon kind="twitter" href={siteMetadata.twitter} size="6" /> |           <SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} /> | ||||||
|         </div> |         </div> | ||||||
|         <div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400"> |         <div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400"> | ||||||
|           <div>{siteMetadata.author}</div> |           <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 */ | /* eslint-disable jsx-a11y/anchor-has-content */ | ||||||
| import Link from 'next/link' | 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 isInternalLink = href && href.startsWith('/') | ||||||
|   const isAnchorLink = href && href.startsWith('#') |   const isAnchorLink = href && href.startsWith('#') | ||||||
| 
 | 
 | ||||||
|   if (isInternalLink) { |   if (isInternalLink) { | ||||||
|     return ( |     // @ts-ignore
 | ||||||
|       <Link href={href}> |     return <Link href={href} {...rest} /> | ||||||
|         <a {...rest} /> |  | ||||||
|       </Link> |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (isAnchorLink) { |   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 { useState } from 'react' | ||||||
| import Link from './Link' | import Link from './Link' | ||||||
| import headerNavLinks from '@/data/headerNavLinks' | import headerNavLinks from '@/data/headerNavLinks' | ||||||
| @@ -20,7 +22,6 @@ const MobileNav = () => { | |||||||
|   return ( |   return ( | ||||||
|     <div className="sm:hidden"> |     <div className="sm:hidden"> | ||||||
|       <button |       <button | ||||||
|         type="button" |  | ||||||
|         className="ml-1 mr-1 h-8 w-8 rounded py-1" |         className="ml-1 mr-1 h-8 w-8 rounded py-1" | ||||||
|         aria-label="Toggle Menu" |         aria-label="Toggle Menu" | ||||||
|         onClick={onToggleNav} |         onClick={onToggleNav} | ||||||
| @@ -39,13 +40,12 @@ const MobileNav = () => { | |||||||
|         </svg> |         </svg> | ||||||
|       </button> |       </button> | ||||||
|       <div |       <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' |           navShow ? 'translate-x-0' : 'translate-x-full' | ||||||
|         }`}
 |         }`}
 | ||||||
|       > |       > | ||||||
|         <div className="flex justify-end"> |         <div className="flex justify-end"> | ||||||
|           <button |           <button | ||||||
|             type="button" |  | ||||||
|             className="mr-5 mt-11 h-8 w-8 rounded" |             className="mr-5 mt-11 h-8 w-8 rounded" | ||||||
|             aria-label="Toggle Menu" |             aria-label="Toggle Menu" | ||||||
|             onClick={onToggleNav} |             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 ( |   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"> |     <h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14"> | ||||||
|       {children} |       {children} | ||||||
| @@ -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 Head from 'next/head' | ||||||
| import { useRouter } from 'next/router' | import { useRouter } from 'next/router' | ||||||
| import siteMetadata from '@/data/siteMetadata' | 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() |   const router = useRouter() | ||||||
|   return ( |   return ( | ||||||
|     <Head> |     <Head> | ||||||
| @@ -14,7 +36,7 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl | |||||||
|       <meta property="og:site_name" content={siteMetadata.title} /> |       <meta property="og:site_name" content={siteMetadata.title} /> | ||||||
|       <meta property="og:description" content={description} /> |       <meta property="og:description" content={description} /> | ||||||
|       <meta property="og:title" content={title} /> |       <meta property="og:title" content={title} /> | ||||||
|       {ogImage.constructor.name === 'Array' ? ( |       {Array.isArray(ogImage) ? ( | ||||||
|         ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />) |         ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />) | ||||||
|       ) : ( |       ) : ( | ||||||
|         <meta property="og:image" content={ogImage} key={ogImage} /> |         <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 ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner | ||||||
|   const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner |   const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner | ||||||
|   return ( |   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 ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner | ||||||
|   const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner |   const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner | ||||||
|   const router = useRouter() |   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 = ({ | export const BlogSEO = ({ | ||||||
|   authorDetails, |   authorDetails, | ||||||
|   title, |   title, | ||||||
| @@ -80,11 +112,10 @@ export const BlogSEO = ({ | |||||||
|   url, |   url, | ||||||
|   images = [], |   images = [], | ||||||
|   canonicalUrl, |   canonicalUrl, | ||||||
| }) => { | }: BlogSeoProps) => { | ||||||
|   const router = useRouter() |  | ||||||
|   const publishedAt = new Date(date).toISOString() |   const publishedAt = new Date(date).toISOString() | ||||||
|   const modifiedAt = new Date(lastmod || date).toISOString() |   const modifiedAt = new Date(lastmod || date).toISOString() | ||||||
|   let imagesArr = |   const imagesArr = | ||||||
|     images.length === 0 |     images.length === 0 | ||||||
|       ? [siteMetadata.socialBanner] |       ? [siteMetadata.socialBanner] | ||||||
|       : typeof images === 'string' |       : typeof images === 'string' | ||||||
| @@ -142,7 +173,7 @@ export const BlogSEO = ({ | |||||||
|     <> |     <> | ||||||
|       <CommonSEO |       <CommonSEO | ||||||
|         title={title} |         title={title} | ||||||
|         description={summary} |         description={summary || ''} | ||||||
|         ogType="article" |         ogType="article" | ||||||
|         ogImage={featuredImages} |         ogImage={featuredImages} | ||||||
|         twImage={twImageUrl} |         twImage={twImageUrl} | ||||||
| @@ -18,16 +18,15 @@ const ScrollTopAndComment = () => { | |||||||
|     window.scrollTo({ top: 0 }) |     window.scrollTo({ top: 0 }) | ||||||
|   } |   } | ||||||
|   const handleScrollToComment = () => { |   const handleScrollToComment = () => { | ||||||
|     document.getElementById('comment').scrollIntoView() |     document.getElementById('comment')?.scrollIntoView() | ||||||
|   } |   } | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|       className={`fixed 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 |         <button | ||||||
|           aria-label="Scroll To Comment" |           aria-label="Scroll To Comment" | ||||||
|           type="button" |  | ||||||
|           onClick={handleScrollToComment} |           onClick={handleScrollToComment} | ||||||
|           className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600" |           className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600" | ||||||
|         > |         > | ||||||
| @@ -42,7 +41,6 @@ const ScrollTopAndComment = () => { | |||||||
|       )} |       )} | ||||||
|       <button |       <button | ||||||
|         aria-label="Scroll To Top" |         aria-label="Scroll To Top" | ||||||
|         type="button" |  | ||||||
|         onClick={handleScrollTop} |         onClick={handleScrollTop} | ||||||
|         className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600" |         className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600" | ||||||
|       > |       > | ||||||
| @@ -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 { useEffect, useState } from 'react' | ||||||
| import { useTheme } from 'next-themes' | import { useTheme } from 'next-themes' | ||||||
| 
 | 
 | ||||||
| const ThemeSwitch = () => { | const ThemeSwitch = () => { | ||||||
|   const [mounted, setMounted] = useState(false) |   const [mounted, setMounted] = useState(false) | ||||||
|   const { theme, setTheme, resolvedTheme } = useTheme() |   const { theme, setTheme } = useTheme() | ||||||
| 
 | 
 | ||||||
|   // When mounted on client, now we can show the UI
 |   // When mounted on client, now we can show the UI
 | ||||||
|   useEffect(() => setMounted(true), []) |   useEffect(() => setMounted(true), []) | ||||||
| 
 | 
 | ||||||
|  |   if (!mounted) { | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <button |     <button | ||||||
|       aria-label="Toggle Dark Mode" |       aria-label="Toggle Dark Mode" | ||||||
|       type="button" |  | ||||||
|       className="ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4" |       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 |       <svg | ||||||
|         xmlns="http://www.w3.org/2000/svg" |         xmlns="http://www.w3.org/2000/svg" | ||||||
| @@ -21,7 +26,7 @@ const ThemeSwitch = () => { | |||||||
|         fill="currentColor" |         fill="currentColor" | ||||||
|         className="text-gray-900 dark:text-gray-100" |         className="text-gray-900 dark:text-gray-100" | ||||||
|       > |       > | ||||||
|         {mounted && (theme === 'dark' || resolvedTheme === 'dark') ? ( |         {mounted && theme === 'dark' ? ( | ||||||
|           <path |           <path | ||||||
|             fillRule="evenodd" |             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" |             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 { | .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 { | .line-number::before { | ||||||
| @@ -138,3 +138,7 @@ | |||||||
| .token.table { | .token.table { | ||||||
|   display: inline; |   display: inline; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .token.table { | ||||||
|  |   display: inline; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -11,7 +11,11 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .footnotes { | .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 { | .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`. | 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. | 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. | 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 | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| <p> | Photo by [YUCAR FotoGrafik](https://unsplash.com/@yucar?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) | ||||||
|   Photo by [YUCAR | on [Unsplash](https://unsplash.com/s/photos/sea?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) | ||||||
|   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> |  | ||||||
|  |  | ||||||
| # Benefits | # Benefits | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea | |||||||
| - Blog templates | - Blog templates | ||||||
| - TOC component | - TOC component | ||||||
| - Support for nested routing of blog posts | - 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 | - Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus | ||||||
| - Projects page | - Projects page | ||||||
| - Preconfigured security headers | - 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 | ## Quick Start Guide | ||||||
|  |  | ||||||
| 1. Try installing the starter using the new [Pliny project CLI](https://github.com/timlrx/pliny): | 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` | ||||||
|  |  | ||||||
| ```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 |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| 2. Personalize `siteMetadata.js` (site related information) | 2. Personalize `siteMetadata.js` (site related information) | ||||||
| 3. Modify the content security policy in `next.config.js` if you want to use | 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. |    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/). | Frontmatter follows [Hugo's standards](https://gohugo.io/content-management/front-matter/). | ||||||
|  |  | ||||||
| Currently 7 fields are supported. | Currently 10 fields are supported. | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| title (required) | title (required) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ title: My fancy title | |||||||
| date: '2021-01-31' | date: '2021-01-31' | ||||||
| tags: ['hello'] | tags: ['hello'] | ||||||
| draft: true | draft: true | ||||||
| summary: | summary: draft post | ||||||
| images: [] | 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' | summary: 'An overview of the new features released in v1 - code block copy, multiple authors, frontmatter layout and more' | ||||||
| layout: PostSimple | layout: PostSimple | ||||||
| bibliography: references-data.bib | bibliography: references-data.bib | ||||||
| canonicalUrl: https://tailwind-nextjs-starter-blog.vercel.app/blog/new-features-in-v1/ |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| ## Overview | ## 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} /> | <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. | 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: | For example, the following jsx snippet can be used directly in an MDX file to render the page title component: | ||||||
|  |  | ||||||
| ```jsx | ```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> | ;<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. | The default configuration resolves all components relative to the `components` directory. | ||||||
|  |  | ||||||
| **Note**: | **Note**: | ||||||
| @@ -338,7 +336,7 @@ To modify the styles, change the following class selectors in the `prism.css` fi | |||||||
| } | } | ||||||
|  |  | ||||||
| .line-number::before { | .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); |   content: attr(line); | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -18,17 +18,17 @@ Since we are using mdx, we can create a simple responsive flexbox grid to displa | |||||||
|  |  | ||||||
| # Gallery | # Gallery | ||||||
|  |  | ||||||
| <div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2"> | <div className="-mx-2 flex flex-wrap 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="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2"> | ||||||
|      |      | ||||||
|   </div> |   </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"> | ||||||
|      |      | ||||||
|   </div> |   </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"> | ||||||
|      |      | ||||||
|   </div> |   </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"> | ||||||
|      |      | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
| @@ -36,17 +36,17 @@ Since we are using mdx, we can create a simple responsive flexbox grid to displa | |||||||
| # Implementation | # Implementation | ||||||
|  |  | ||||||
| ```js | ```js | ||||||
| <div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2"> | <div className="-mx-2 flex flex-wrap 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="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2"> | ||||||
|      |      | ||||||
|   </div> |   </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"> | ||||||
|      |      | ||||||
|   </div> |   </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"> | ||||||
|      |      | ||||||
|   </div> |   </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"> | ||||||
|      |      | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ title: 'The Time Machine' | |||||||
| date: '2018-08-15' | date: '2018-08-15' | ||||||
| tags: ['writings', 'book', 'reflection'] | tags: ['writings', 'book', 'reflection'] | ||||||
| draft: false | draft: false | ||||||
| summary: 'The Time Traveller (for so it will be convenient to speak of him) was | 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 |   expounding a recondite matter to us. His pale grey eyes shone and | ||||||
| twinkled, and his usually pale face was flushed and animated...' |   twinkled, and his usually pale face was flushed and animated... | ||||||
| --- | --- | ||||||
|  |  | ||||||
| # The Time Machine by H. G. Wells | # The Time Machine by H. G. Wells | ||||||
|   | |||||||
| @@ -30,4 +30,4 @@ | |||||||
|   author={Xie, Yihui}, |   author={Xie, Yihui}, | ||||||
|   year={2016}, |   year={2016}, | ||||||
|   publisher={CRC Press} |   publisher={CRC Press} | ||||||
| } | } | ||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | // @ts-check | ||||||
|  |  | ||||||
|  | /** @type {import("pliny/config").PlinyConfig } */ | ||||||
| const siteMetadata = { | const siteMetadata = { | ||||||
|   title: 'Next.js Starter Blog', |   title: 'Next.js Starter Blog', | ||||||
|   author: 'Tails Azimuth', |   author: 'Tails Azimuth', | ||||||
| @@ -24,16 +27,16 @@ const siteMetadata = { | |||||||
|     plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app |     plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app | ||||||
|     simpleAnalytics: false, // true or false |     simpleAnalytics: false, // true or false | ||||||
|     umamiWebsiteId: '', // e.g. 123e4567-e89b-12d3-a456-426614174000 |     umamiWebsiteId: '', // e.g. 123e4567-e89b-12d3-a456-426614174000 | ||||||
|  |     posthogProjectApiKey: '', // e.g. AhnJK8392ndPOav87as450xd | ||||||
|     googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX |     googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX | ||||||
|     posthogAnalyticsId: '', // posthog.init e.g. phc_5yXvArzvRdqtZIsHkEm3Fkkhm3d0bEYUXCaFISzqPSQ |  | ||||||
|   }, |   }, | ||||||
|   newsletter: { |   newsletter: { | ||||||
|     // supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus |     // supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus | ||||||
|     // Please add your .env file and modify it according to your selection |     // Please add your .env file and modify it according to your selection | ||||||
|     provider: 'buttondown', |     provider: 'buttondown', | ||||||
|   }, |   }, | ||||||
|   comment: { |   comments: { | ||||||
|     // If you want to use a commenting system other than giscus you have to add it to the |     // If you want to use an analytics provider you have to add it to the | ||||||
|     // content security policy in the `next.config.js` file. |     // content security policy in the `next.config.js` file. | ||||||
|     // Select a provider and use the environment variables associated to it |     // Select a provider and use the environment variables associated to it | ||||||
|     // https://vercel.com/docs/environment-variables |     // https://vercel.com/docs/environment-variables | ||||||
| @@ -52,34 +55,30 @@ const siteMetadata = { | |||||||
|       // theme example: light, dark, dark_dimmed, dark_high_contrast |       // theme example: light, dark, dark_dimmed, dark_high_contrast | ||||||
|       // transparent_dark, preferred_color_scheme, custom |       // transparent_dark, preferred_color_scheme, custom | ||||||
|       theme: 'light', |       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 |       // theme when dark mode | ||||||
|       darkTheme: 'transparent_dark', |       darkTheme: 'transparent_dark', | ||||||
|       // If the theme option above is set to 'custom` |       // If the theme option above is set to 'custom` | ||||||
|       // please provide a link below to your custom theme css file. |       // please provide a link below to your custom theme css file. | ||||||
|       // example: https://giscus.app/themes/custom_example.css |       // example: https://giscus.app/themes/custom_example.css | ||||||
|       themeURL: '', |       themeURL: '', | ||||||
|     }, |       // This corresponds to the `data-lang="en"` in giscus's configurations | ||||||
|     utterancesConfig: { |       lang: 'en', | ||||||
|       // 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, |  | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   // search: { | ||||||
|  |   //   provider: 'kbar', // kbar or algolia | ||||||
|  |   //   kbarConfig: { | ||||||
|  |   //     searchDocumentsPath: 'search.json', // path to load documents to search | ||||||
|  |   //   }, | ||||||
|  |   //   provider: 'algolia', | ||||||
|  |   //   algoliaConfig: { | ||||||
|  |   //     // The application ID provided by Algolia | ||||||
|  |   //     appId: 'R2IYF7ETH7', | ||||||
|  |   //     // Public API key: it is safe to commit it | ||||||
|  |   //     apiKey: '599cec31baffa4868cae4e79f180729b', | ||||||
|  |   //     indexName: 'docsearch', | ||||||
|  |   //   }, | ||||||
|  |   // }, | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = siteMetadata | module.exports = siteMetadata | ||||||
|   | |||||||
| @@ -6,7 +6,8 @@ | |||||||
|       "@/data/*": ["data/*"], |       "@/data/*": ["data/*"], | ||||||
|       "@/layouts/*": ["layouts/*"], |       "@/layouts/*": ["layouts/*"], | ||||||
|       "@/lib/*": ["lib/*"], |       "@/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 SocialIcon from '@/components/social-icons' | ||||||
| import Image from '@/components/Image' | import Image from '@/components/Image' | ||||||
| import { PageSEO } from '@/components/SEO' | // import { PageSEO } from '@/components/SEO'
 | ||||||
| 
 | 
 | ||||||
| export default function AuthorLayout({ children, frontMatter }) { | interface Props { | ||||||
|   const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter |   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 ( |   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="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"> |           <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 |             About | ||||||
|           </h1> |           </h1> | ||||||
|         </div> |         </div> | ||||||
|         <div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0"> |         <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"> |           <div className="flex flex-col items-center space-x-2 pt-8"> | ||||||
|             <Image |             {avatar && ( | ||||||
|               src={avatar} |               <Image | ||||||
|               alt="avatar" |                 src={avatar} | ||||||
|               width="192px" |                 alt="avatar" | ||||||
|               height="192px" |                 width={192} | ||||||
|               className="h-48 w-48 rounded-full" |                 height={192} | ||||||
|             /> |                 className="h-48 w-48 rounded-full" | ||||||
|             <h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">{name}</h3> |               /> | ||||||
|  |             )} | ||||||
|  |             <h3 className="pb-2 pt-4 text-2xl font-bold leading-8 tracking-tight">{name}</h3> | ||||||
|             <div className="text-gray-500 dark:text-gray-400">{occupation}</div> |             <div className="text-gray-500 dark:text-gray-400">{occupation}</div> | ||||||
|             <div className="text-gray-500 dark:text-gray-400">{company}</div> |             <div className="text-gray-500 dark:text-gray-400">{company}</div> | ||||||
|             <div className="flex space-x-3 pt-6"> |             <div className="flex space-x-3 pt-6"> | ||||||
| @@ -33,7 +42,7 @@ export default function AuthorLayout({ children, frontMatter }) { | |||||||
|               <SocialIcon kind="twitter" href={twitter} /> |               <SocialIcon kind="twitter" href={twitter} /> | ||||||
|             </div> |             </div> | ||||||
|           </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> | ||||||
|       </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 Link from '@/components/Link' | ||||||
| import PageTitle from '@/components/PageTitle' | import PageTitle from '@/components/PageTitle' | ||||||
| import SectionContainer from '@/components/SectionContainer' | import SectionContainer from '@/components/SectionContainer' | ||||||
| import { BlogSEO } from '@/components/SEO' | // import { BlogSEO } from '@/components/SEO'
 | ||||||
| import Image from '@/components/Image' | import Image from '@/components/Image' | ||||||
| import Tag from '@/components/Tag' | import Tag from '@/components/Tag' | ||||||
| import siteMetadata from '@/data/siteMetadata' | import siteMetadata from '@/data/siteMetadata' | ||||||
| import Comments from '@/components/comments' |  | ||||||
| import ScrollTopAndComment from '@/components/ScrollTopAndComment' | import ScrollTopAndComment from '@/components/ScrollTopAndComment' | ||||||
| 
 | 
 | ||||||
| const editUrl = (fileName) => `${siteMetadata.siteRepo}/blob/master/data/blog/${fileName}` | const editUrl = (path) => `${siteMetadata.siteRepo}/blob/master/data/${path}` | ||||||
| const discussUrl = (slug) => | const discussUrl = (path) => | ||||||
|   `https://mobile.twitter.com/search?q=${encodeURIComponent( |   `https://mobile.twitter.com/search?q=${encodeURIComponent(`${siteMetadata.siteUrl}/${path}`)}` | ||||||
|     `${siteMetadata.siteUrl}/blog/${slug}` |  | ||||||
|   )}` |  | ||||||
| 
 | 
 | ||||||
| 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 }) { | interface LayoutProps { | ||||||
|   const { slug, fileName, date, title, images, tags } = frontMatter |   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 ( |   return ( | ||||||
|     <SectionContainer> |     <SectionContainer> | ||||||
|       <BlogSEO |       {/* <BlogSEO url={`${siteMetadata.siteUrl}/${path}`} authorDetails={authorDetails} {...content} /> */} | ||||||
|         url={`${siteMetadata.siteUrl}/blog/${slug}`} |  | ||||||
|         authorDetails={authorDetails} |  | ||||||
|         {...frontMatter} |  | ||||||
|       /> |  | ||||||
|       <ScrollTopAndComment /> |       <ScrollTopAndComment /> | ||||||
|       <article> |       <article> | ||||||
|         <div className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700"> |         <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> | ||||||
|             </div> |             </div> | ||||||
|           </header> |           </header> | ||||||
|           <div |           <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"> | ||||||
|             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" |             <dl className="pb-10 pt-6 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700"> | ||||||
|             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"> |  | ||||||
|               <dt className="sr-only">Authors</dt> |               <dt className="sr-only">Authors</dt> | ||||||
|               <dd> |               <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) => ( |                   {authorDetails.map((author) => ( | ||||||
|                     <li className="flex items-center space-x-2" key={author.name}> |                     <li className="flex items-center space-x-2" key={author.name}> | ||||||
|                       {author.avatar && ( |                       {author.avatar && ( | ||||||
|                         <Image |                         <Image | ||||||
|                           src={author.avatar} |                           src={author.avatar} | ||||||
|                           width="38px" |                           width={38} | ||||||
|                           height="38px" |                           height={38} | ||||||
|                           alt="avatar" |                           alt="avatar" | ||||||
|                           className="h-10 w-10 rounded-full" |                           className="h-10 w-10 rounded-full" | ||||||
|                         /> |                         /> | ||||||
| @@ -86,15 +95,25 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi | |||||||
|               </dd> |               </dd> | ||||||
|             </dl> |             </dl> | ||||||
|             <div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0"> |             <div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0"> | ||||||
|               <div className="prose max-w-none 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 className="pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300"> |               <div className="pb-6 pt-6 text-sm text-gray-700 dark:text-gray-300"> | ||||||
|                 <Link href={discussUrl(slug)} rel="nofollow"> |                 <Link href={discussUrl(path)} rel="nofollow"> | ||||||
|                   {'Discuss on Twitter'} |                   Discuss on Twitter | ||||||
|                 </Link> |                 </Link> | ||||||
|                 {` • `} |                 {` • `} | ||||||
|                 <Link href={editUrl(fileName)}>{'View on GitHub'}</Link> |                 <Link href={editUrl(filePath)}>View on GitHub</Link> | ||||||
|               </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> | ||||||
|  |               )} | ||||||
|             </div> |             </div> | ||||||
|             <footer> |             <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"> |               <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 |                           Previous Article | ||||||
|                         </h2> |                         </h2> | ||||||
|                         <div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"> |                         <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> | ||||||
|                       </div> |                       </div> | ||||||
|                     )} |                     )} | ||||||
| @@ -128,7 +147,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi | |||||||
|                           Next Article |                           Next Article | ||||||
|                         </h2> |                         </h2> | ||||||
|                         <div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"> |                         <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> | ||||||
|                       </div> |                       </div> | ||||||
|                     )} |                     )} | ||||||
| @@ -137,8 +156,9 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi | |||||||
|               </div> |               </div> | ||||||
|               <div className="pt-4 xl:pt-8"> |               <div className="pt-4 xl:pt-8"> | ||||||
|                 <Link |                 <Link | ||||||
|                   href="/blog" |                   href={`/${basePath}`} | ||||||
|                   className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" |                   className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" | ||||||
|  |                   aria-label="Back to the blog" | ||||||
|                 > |                 > | ||||||
|                   ← Back to the blog |                   ← Back to the blog | ||||||
|                 </Link> |                 </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 Link from '@/components/Link' | ||||||
| import PageTitle from '@/components/PageTitle' | import PageTitle from '@/components/PageTitle' | ||||||
| import SectionContainer from '@/components/SectionContainer' | import SectionContainer from '@/components/SectionContainer' | ||||||
| import { BlogSEO } from '@/components/SEO' | // import { BlogSEO } from '@/components/SEO'
 | ||||||
| import siteMetadata from '@/data/siteMetadata' | import siteMetadata from '@/data/siteMetadata' | ||||||
| import formatDate from '@/lib/utils/formatDate' |  | ||||||
| import Comments from '@/components/comments' |  | ||||||
| import ScrollTopAndComment from '@/components/ScrollTopAndComment' | import ScrollTopAndComment from '@/components/ScrollTopAndComment' | ||||||
| 
 | 
 | ||||||
| export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) { | interface LayoutProps { | ||||||
|   const { date, title } = frontMatter |   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 ( |   return ( | ||||||
|     <SectionContainer> |     <SectionContainer> | ||||||
|       <BlogSEO url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} /> |       {/* <BlogSEO url={`${siteMetadata.siteUrl}/${path}`} {...content} /> */} | ||||||
|       <ScrollTopAndComment /> |       <ScrollTopAndComment /> | ||||||
|       <article> |       <article> | ||||||
|         <div> |         <div> | ||||||
| @@ -22,7 +34,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi | |||||||
|                 <div> |                 <div> | ||||||
|                   <dt className="sr-only">Published on</dt> |                   <dt className="sr-only">Published on</dt> | ||||||
|                   <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400"> |                   <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400"> | ||||||
|                     <time dateTime={date}>{formatDate(date)}</time> |                     <time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time> | ||||||
|                   </dd> |                   </dd> | ||||||
|                 </div> |                 </div> | ||||||
|               </dl> |               </dl> | ||||||
| @@ -31,21 +43,26 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </header> |           </header> | ||||||
|           <div |           <div className="grid-rows-[auto_1fr] divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:divide-y-0"> | ||||||
|             className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:divide-y-0 " |  | ||||||
|             style={{ gridTemplateRows: 'auto 1fr' }} |  | ||||||
|           > |  | ||||||
|             <div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0"> |             <div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0"> | ||||||
|               <div className="prose max-w-none 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> |             </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> |             <footer> | ||||||
|               <div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base"> |               <div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base"> | ||||||
|                 {prev && ( |                 {prev && ( | ||||||
|                   <div className="pt-4 xl:pt-8"> |                   <div className="pt-4 xl:pt-8"> | ||||||
|                     <Link |                     <Link | ||||||
|                       href={`/blog/${prev.slug}`} |                       href={`/${prev.path}`} | ||||||
|                       className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" |                       className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" | ||||||
|  |                       aria-label={`Previous post: ${prev.title}`} | ||||||
|                     > |                     > | ||||||
|                       ← {prev.title} |                       ← {prev.title} | ||||||
|                     </Link> |                     </Link> | ||||||
| @@ -54,8 +71,9 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi | |||||||
|                 {next && ( |                 {next && ( | ||||||
|                   <div className="pt-4 xl:pt-8"> |                   <div className="pt-4 xl:pt-8"> | ||||||
|                     <Link |                     <Link | ||||||
|                       href={`/blog/${next.slug}`} |                       href={`/${next.path}`} | ||||||
|                       className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" |                       className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" | ||||||
|  |                       aria-label={`Next post: ${next.title}`} | ||||||
|                     > |                     > | ||||||
|                       {next.title} → |                       {next.title} → | ||||||
|                     </Link> |                     </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')({ | const withBundleAnalyzer = require('@next/bundle-analyzer')({ | ||||||
|   enabled: process.env.ANALYZE === 'true', |   enabled: process.env.ANALYZE === 'true', | ||||||
| }) | }) | ||||||
| @@ -52,36 +54,35 @@ const securityHeaders = [ | |||||||
|   }, |   }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| module.exports = withBundleAnalyzer({ | /** | ||||||
|   reactStrictMode: true, |  * @type {import('next/dist/next-server/server/config').NextConfig} | ||||||
|   pageExtensions: ['js', 'jsx', 'md', 'mdx'], |  **/ | ||||||
|   eslint: { | module.exports = () => { | ||||||
|     dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'], |   const plugins = [withContentlayer, withBundleAnalyzer] | ||||||
|   }, |   return plugins.reduce((acc, next) => next(acc), { | ||||||
|   async headers() { |     reactStrictMode: true, | ||||||
|     return [ |     pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], | ||||||
|       { |     eslint: { | ||||||
|         source: '/(.*)', |       dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'], | ||||||
|         headers: securityHeaders, |     }, | ||||||
|       }, |     experimental: { | ||||||
|     ] |       appDir: true, | ||||||
|   }, |     }, | ||||||
|   webpack: (config, { dev, isServer }) => { |     async headers() { | ||||||
|     config.module.rules.push({ |       return [ | ||||||
|       test: /\.svg$/, |         { | ||||||
|       use: ['@svgr/webpack'], |           source: '/(.*)', | ||||||
|     }) |           headers: securityHeaders, | ||||||
|  |         }, | ||||||
|     if (!dev && !isServer) { |       ] | ||||||
|       // Replace React with Preact only in client production build |     }, | ||||||
|       Object.assign(config.resolve.alias, { |     webpack: (config, options) => { | ||||||
|         'react/jsx-runtime.js': 'preact/compat/jsx-runtime', |       config.module.rules.push({ | ||||||
|         react: 'preact/compat', |         test: /\.svg$/, | ||||||
|         'react-dom/test-utils': 'preact/test-utils', |         use: ['@svgr/webpack'], | ||||||
|         'react-dom': 'preact/compat', |  | ||||||
|       }) |       }) | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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", |   "version": "1.5.6", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "cross-env SOCKET=true node ./scripts/next-remote-watch.js ./data", |     "start": "next dev", | ||||||
|     "dev": "next dev", |     "dev": "cross-env INIT_CWD=$PWD next dev", | ||||||
|     "build": "next build && node ./scripts/generate-sitemap", |     "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", |     "serve": "next start", | ||||||
|     "analyze": "cross-env ANALYZE=true next build", |     "analyze": "cross-env ANALYZE=true next build", | ||||||
|     "lint": "next lint --fix --dir pages --dir components --dir lib --dir layouts --dir scripts", |     "lint": "next lint --fix --dir pages --dir components --dir lib --dir layouts --dir scripts" | ||||||
|     "prepare": "husky install" |  | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@fontsource/inter": "4.5.2", |     "@next/bundle-analyzer": "13.4.8", | ||||||
|     "@mailchimp/mailchimp_marketing": "^3.0.58", |     "@tailwindcss/forms": "^0.5.3", | ||||||
|     "@next/bundle-analyzer": "^12.1.4", |     "@tailwindcss/typography": "^0.5.9", | ||||||
|     "@tailwindcss/forms": "^0.4.0", |     "autoprefixer": "^10.4.13", | ||||||
|     "@tailwindcss/typography": "^0.5.0", |     "contentlayer": "0.3.4", | ||||||
|     "autoprefixer": "^10.4.0", |     "esbuild": "0.18.11", | ||||||
|     "esbuild": "^0.13.13", |     "github-slugger": "^1.4.0", | ||||||
|     "github-slugger": "^1.3.0", |  | ||||||
|     "gray-matter": "^4.0.2", |     "gray-matter": "^4.0.2", | ||||||
|     "image-size": "1.0.0", |     "image-size": "1.0.0", | ||||||
|     "mdx-bundler": "^8.0.0", |     "mdx-bundler": "^9.2.1", | ||||||
|     "next": "12.1.4", |     "next": "13.4.8", | ||||||
|     "next-themes": "^0.0.14", |     "next-contentlayer": "0.3.4", | ||||||
|     "postcss": "^8.4.5", |     "next-themes": "^0.2.1", | ||||||
|     "preact": "^10.6.2", |     "pliny": "0.0.10", | ||||||
|     "react": "17.0.2", |     "postcss": "^8.4.24", | ||||||
|     "react-dom": "17.0.2", |     "react": "18.2.0", | ||||||
|     "reading-time": "1.3.0", |     "react-dom": "18.2.0", | ||||||
|  |     "reading-time": "1.5.0", | ||||||
|     "rehype-autolink-headings": "^6.1.0", |     "rehype-autolink-headings": "^6.1.0", | ||||||
|     "rehype-citation": "^0.4.0", |     "rehype-citation": "^1.0.2", | ||||||
|     "rehype-katex": "^6.0.2", |     "rehype-katex": "^6.0.3", | ||||||
|     "rehype-preset-minify": "6.0.0", |     "rehype-preset-minify": "6.0.0", | ||||||
|     "rehype-prism-plus": "^1.1.3", |     "rehype-prism-plus": "^1.6.0", | ||||||
|     "rehype-slug": "^5.0.0", |     "rehype-slug": "^5.1.0", | ||||||
|     "remark-footnotes": "^4.0.1", |     "remark": "^14.0.2", | ||||||
|     "remark-gfm": "^3.0.1", |     "remark-gfm": "^3.0.1", | ||||||
|     "remark-math": "^5.1.1", |     "remark-math": "^5.1.1", | ||||||
|     "sharp": "^0.28.3", |     "tailwindcss": "^3.3.2", | ||||||
|     "tailwindcss": "^3.0.23", |     "unist-util-visit": "^4.1.0" | ||||||
|     "unist-util-visit": "^4.0.0" |  | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "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", |     "cross-env": "^7.0.3", | ||||||
|     "dedent": "^0.7.0", |     "dedent": "^0.7.0", | ||||||
|     "eslint": "^7.29.0", |     "esbuild-register": "3.4.2", | ||||||
|     "eslint-config-next": "12.1.4", |     "eslint": "^8.43.0", | ||||||
|     "eslint-config-prettier": "^8.3.0", |     "eslint-config-next": "13.4.7", | ||||||
|     "eslint-plugin-prettier": "^3.3.1", |     "eslint-config-prettier": "^8.8.0", | ||||||
|     "file-loader": "^6.0.0", |     "eslint-plugin-prettier": "^4.2.1", | ||||||
|  |     "file-loader": "^6.2.0", | ||||||
|     "globby": "11.0.3", |     "globby": "11.0.3", | ||||||
|     "husky": "^6.0.0", |     "husky": "^8.0.0", | ||||||
|     "inquirer": "^8.1.1", |     "js-yaml": "4.1.0", | ||||||
|     "lint-staged": "^11.0.0", |     "lint-staged": "^13.0.0", | ||||||
|     "next-remote-watch": "^1.0.0", |     "prettier": "^2.8.8", | ||||||
|     "prettier": "^2.5.1", |     "prettier-plugin-tailwindcss": "^0.3.0", | ||||||
|     "prettier-plugin-tailwindcss": "^0.1.4", |     "typescript": "^5.1.3" | ||||||
|     "socket.io": "^4.4.0", |  | ||||||
|     "socket.io-client": "^4.4.0" |  | ||||||
|   }, |   }, | ||||||
|   "lint-staged": { |   "lint-staged": { | ||||||
|     "*.+(js|jsx|ts|tsx)": [ |     "*.+(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
		Reference in New Issue
	
	Block a user