Compare commits

..

No commits in common. "fd187a13709e275d6cb2bbcfd3ad16c65e6a5fc5" and "10f64a9ba45a3eff33f954a8b56edd2bce350dc1" have entirely different histories.

69 changed files with 1118 additions and 1410 deletions

View File

@ -35,6 +35,4 @@ yarn-error.log*
.env.test.local .env.test.local
.env.production.local .env.production.local
secrets.txt secrets.txt
.pnpm-store

View File

@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

2
.gitignore vendored
View File

@ -36,5 +36,3 @@ yarn-error.log*
.env.production.local .env.production.local
secrets.txt secrets.txt
.pnpm-store

View File

@ -1,6 +1,2 @@
module.exports = { {
singleQuote: true, }
trailingCommas: 'all',
bracketSpacing: true,
bracketSameLine: true,
};

View File

@ -1,12 +1,13 @@
import Image from './Image'; import Image from './Image'
import Link from './Link'; import Link from './Link'
const Card = ({ title, description, imgSrc, href }) => ( const Card = ({ title, description, imgSrc, href }) => (
<div className="md p-4 md:w-1/2" style={{ maxWidth: '544px' }}> <div className="md p-4 md:w-1/2" style={{ maxWidth: '544px' }}>
<div <div
className={`${ className={`${
imgSrc && 'h-full' imgSrc && 'h-full'
} overflow-hidden rounded-md border-2 border-gray-200 border-opacity-60 dark:border-gray-700`}> } overflow-hidden rounded-md border-2 border-gray-200 border-opacity-60 dark:border-gray-700`}
>
{imgSrc && {imgSrc &&
(href ? ( (href ? (
<Link href={href} aria-label={`Link to ${title}`}> <Link href={href} aria-label={`Link to ${title}`}>
@ -37,20 +38,19 @@ const Card = ({ title, description, imgSrc, href }) => (
title title
)} )}
</h2> </h2>
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400"> <p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">{description}</p>
{description}
</p>
{href && ( {href && (
<Link <Link
href={href} href={href}
className="text-base font-medium leading-6 text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" className="text-base font-medium leading-6 text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label={`Link to ${title}`}> aria-label={`Link to ${title}`}
>
Learn more &rarr; Learn more &rarr;
</Link> </Link>
)} )}
</div> </div>
</div> </div>
</div> </div>
); )
export default Card; export default Card

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react'
import Router from 'next/router'; import Router from 'next/router'
/** /**
* Client-side complement to next-remote-watch * Client-side complement to next-remote-watch
@ -10,14 +10,14 @@ export const ClientReload = () => {
// Exclude socket.io from prod bundle // Exclude socket.io from prod bundle
useEffect(() => { useEffect(() => {
import('socket.io-client').then((module) => { import('socket.io-client').then((module) => {
const socket = module.io(); const socket = module.io()
socket.on('reload', () => { socket.on('reload', () => {
Router.replace(Router.asPath, undefined, { Router.replace(Router.asPath, undefined, {
scroll: false, scroll: false,
}); })
}); })
}); })
}, []); }, [])
return null; return null
}; }

View File

@ -1,17 +1,13 @@
import Link from './Link'; import Link from './Link'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import SocialIcon from '@/components/social-icons'; import SocialIcon from '@/components/social-icons'
export default function Footer() { export default function Footer() {
return ( return (
<footer> <footer>
<div className="mt-16 flex flex-col items-center"> <div className="mt-16 flex flex-col items-center">
<div className="mb-3 flex space-x-4"> <div className="mb-3 flex space-x-4">
<SocialIcon <SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
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} />
@ -26,13 +22,11 @@ export default function Footer() {
<Link href="/">{siteMetadata.title}</Link> <Link href="/">{siteMetadata.title}</Link>
</div> </div>
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400"> <div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
<Link <Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog" rel="nofollow">
href="https://github.com/timlrx/tailwind-nextjs-starter-blog"
rel="nofollow">
Tailwind Nextjs Theme Tailwind Nextjs Theme
</Link> </Link>
</div> </div>
</div> </div>
</footer> </footer>
); )
} }

View File

@ -1,5 +1,5 @@
import NextImage, { ImageProps } from 'next/image'; import NextImage, { ImageProps } from 'next/image'
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />; const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
export default Image; export default Image

View File

@ -1,15 +1,15 @@
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import headerNavLinks from '@/data/headerNavLinks'; import headerNavLinks from '@/data/headerNavLinks'
import Logo from '@/data/logo.svg'; import Logo from '@/data/logo.svg'
import Link from './Link'; import Link from './Link'
import SectionContainer from './SectionContainer'; import SectionContainer from './SectionContainer'
import Footer from './Footer'; import Footer from './Footer'
import MobileNav from './MobileNav'; import MobileNav from './MobileNav'
import ThemeSwitch from './ThemeSwitch'; import ThemeSwitch from './ThemeSwitch'
import { ReactNode } from 'react'; import { ReactNode } from 'react'
interface Props { interface Props {
children: ReactNode; children: ReactNode
} }
const LayoutWrapper = ({ children }: Props) => { const LayoutWrapper = ({ children }: Props) => {
@ -39,7 +39,8 @@ const LayoutWrapper = ({ children }: Props) => {
<Link <Link
key={link.title} key={link.title}
href={link.href} href={link.href}
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"> className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"
>
{link.title} {link.title}
</Link> </Link>
))} ))}
@ -52,7 +53,7 @@ const LayoutWrapper = ({ children }: Props) => {
<Footer /> <Footer />
</div> </div>
</SectionContainer> </SectionContainer>
); )
}; }
export default LayoutWrapper; export default LayoutWrapper

View File

@ -1,30 +1,27 @@
/* 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'; import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
const CustomLink = ({ const CustomLink = ({
href, href,
...rest ...rest
}: DetailedHTMLProps< }: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
AnchorHTMLAttributes<HTMLAnchorElement>, const isInternalLink = href && href.startsWith('/')
HTMLAnchorElement const isAnchorLink = href && href.startsWith('#')
>) => {
const isInternalLink = href && href.startsWith('/');
const isAnchorLink = href && href.startsWith('#');
if (isInternalLink) { if (isInternalLink) {
return ( return (
<Link href={href}> <Link href={href}>
<a {...rest} /> <a {...rest} />
</Link> </Link>
); )
} }
if (isAnchorLink) { if (isAnchorLink) {
return <a href={href} {...rest} />; return <a href={href} {...rest} />
} }
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />; return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
}; }
export default CustomLink; export default CustomLink

View File

@ -1,19 +1,16 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React, { useMemo } from 'react'; import React, { useMemo } from 'react'
import { ComponentMap, getMDXComponent } from 'mdx-bundler/client'; import { ComponentMap, getMDXComponent } from 'mdx-bundler/client'
import Image from './Image'; import Image from './Image'
import CustomLink from './Link'; import CustomLink from './Link'
import TOCInline from './TOCInline'; import TOCInline from './TOCInline'
import Pre from './Pre'; import Pre from './Pre'
import { BlogNewsletterForm } from './NewsletterForm'; import { BlogNewsletterForm } from './NewsletterForm'
const Wrapper: React.ComponentType<{ layout: string }> = ({ const Wrapper: React.ComponentType<{ layout: string }> = ({ layout, ...rest }) => {
layout, const Layout = require(`../layouts/${layout}`).default
...rest return <Layout {...rest} />
}) => { }
const Layout = require(`../layouts/${layout}`).default;
return <Layout {...rest} />;
};
export const MDXComponents: ComponentMap = { export const MDXComponents: ComponentMap = {
Image, Image,
@ -24,16 +21,16 @@ export const MDXComponents: ComponentMap = {
wrapper: Wrapper, wrapper: Wrapper,
//@ts-ignore //@ts-ignore
BlogNewsletterForm, BlogNewsletterForm,
}; }
interface Props { interface Props {
layout: string; layout: string
mdxSource: string; mdxSource: string
[key: string]: unknown; [key: string]: unknown
} }
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }: Props) => { export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }: Props) => {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]); const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />; return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
}; }

View File

@ -1,21 +1,21 @@
import { useState } from 'react'; import { useState } from 'react'
import Link from './Link'; import Link from './Link'
import headerNavLinks from '@/data/headerNavLinks'; import headerNavLinks from '@/data/headerNavLinks'
const MobileNav = () => { const MobileNav = () => {
const [navShow, setNavShow] = useState(false); const [navShow, setNavShow] = useState(false)
const onToggleNav = () => { const onToggleNav = () => {
setNavShow((status) => { setNavShow((status) => {
if (status) { if (status) {
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto'
} else { } else {
// Prevent scrolling // Prevent scrolling
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'
} }
return !status; return !status
}); })
}; }
return ( return (
<div className="sm:hidden"> <div className="sm:hidden">
@ -23,12 +23,14 @@ const MobileNav = () => {
type="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}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
className="text-gray-900 dark:text-gray-100"> className="text-gray-900 dark:text-gray-100"
>
{navShow ? ( {navShow ? (
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -47,19 +49,22 @@ const MobileNav = () => {
<div <div
className={`fixed top-24 right-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${ className={`fixed top-24 right-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'
}`}> }`}
>
<button <button
type="button" type="button"
aria-label="toggle modal" aria-label="toggle modal"
className="fixed h-full w-full cursor-auto focus:outline-none" className="fixed h-full w-full cursor-auto focus:outline-none"
onClick={onToggleNav}></button> onClick={onToggleNav}
></button>
<nav className="fixed mt-8 h-full"> <nav className="fixed mt-8 h-full">
{headerNavLinks.map((link) => ( {headerNavLinks.map((link) => (
<div key={link.title} className="px-12 py-4"> <div key={link.title} className="px-12 py-4">
<Link <Link
href={link.href} href={link.href}
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100" className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
onClick={onToggleNav}> onClick={onToggleNav}
>
{link.title} {link.title}
</Link> </Link>
</div> </div>
@ -67,7 +72,7 @@ const MobileNav = () => {
</nav> </nav>
</div> </div>
</div> </div>
); )
}; }
export default MobileNav; export default MobileNav

View File

@ -1,15 +1,15 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => { const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
const inputEl = useRef<HTMLInputElement>(null); const inputEl = useRef<HTMLInputElement>(null)
const [error, setError] = useState(false); const [error, setError] = useState(false)
const [message, setMessage] = useState(''); const [message, setMessage] = useState('')
const [subscribed, setSubscribed] = useState(false); const [subscribed, setSubscribed] = useState(false)
const subscribe = async (e: React.FormEvent<HTMLFormElement>) => { const subscribe = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault()
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, { const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
body: JSON.stringify({ body: JSON.stringify({
@ -19,28 +19,24 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
method: 'POST', method: 'POST',
}); })
const { error } = await res.json(); const { error } = await res.json()
if (error) { if (error) {
setError(true); setError(true)
setMessage( setMessage('Your e-mail address is invalid or you are already subscribed!')
'Your e-mail address is invalid or you are already subscribed!' return
);
return;
} }
inputEl.current.value = ''; inputEl.current.value = ''
setError(false); setError(false)
setSubscribed(true); setSubscribed(true)
setMessage('Successfully! 🎉 You are now subscribed.'); setMessage('Successfully! 🎉 You are now subscribed.')
}; }
return ( return (
<div> <div>
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100"> <div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</div>
{title}
</div>
<form className="flex flex-col sm:flex-row" onSubmit={subscribe}> <form className="flex flex-col sm:flex-row" onSubmit={subscribe}>
<div> <div>
<label className="sr-only" htmlFor="email-input"> <label className="sr-only" htmlFor="email-input">
@ -51,9 +47,7 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
className="w-72 rounded-md px-4 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600 dark:bg-black" 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" id="email-input"
name="email" name="email"
placeholder={ placeholder={subscribed ? "You're subscribed ! 🎉" : 'Enter your email'}
subscribed ? "You're subscribed ! 🎉" : 'Enter your email'
}
ref={inputEl} ref={inputEl}
required required
type="email" type="email"
@ -63,26 +57,23 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
<div className="mt-2 flex w-full rounded-md shadow-sm sm:mt-0 sm:ml-3"> <div className="mt-2 flex w-full rounded-md shadow-sm sm:mt-0 sm:ml-3">
<button <button
className={`w-full rounded-md bg-primary-500 py-2 px-4 font-medium text-white sm:py-0 ${ className={`w-full rounded-md bg-primary-500 py-2 px-4 font-medium text-white sm:py-0 ${
subscribed subscribed ? 'cursor-default' : 'hover:bg-primary-700 dark:hover:bg-primary-400'
? '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`} } focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2 dark:ring-offset-black`}
type="submit" type="submit"
disabled={subscribed}> disabled={subscribed}
>
{subscribed ? 'Thank you!' : 'Sign up'} {subscribed ? 'Thank you!' : 'Sign up'}
</button> </button>
</div> </div>
</form> </form>
{error && ( {error && (
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96"> <div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">{message}</div>
{message}
</div>
)} )}
</div> </div>
); )
}; }
export default NewsletterForm; export default NewsletterForm
export const BlogNewsletterForm = ({ title }) => ( export const BlogNewsletterForm = ({ title }) => (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
@ -90,4 +81,4 @@ export const BlogNewsletterForm = ({ title }) => (
<NewsletterForm title={title} /> <NewsletterForm title={title} />
</div> </div>
</div> </div>
); )

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react'
interface Props { interface Props {
children: ReactNode; children: ReactNode
} }
export default function PageTitle({ children }: Props) { export default function PageTitle({ children }: Props) {
@ -9,5 +9,5 @@ export default function PageTitle({ children }: Props) {
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14"> <h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
{children} {children}
</h1> </h1>
); )
} }

View File

@ -1,29 +1,24 @@
import Link from '@/components/Link'; import Link from '@/components/Link'
interface Props { interface Props {
totalPages: number; totalPages: number
currentPage: number; currentPage: number
} }
export default function Pagination({ totalPages, currentPage }: Props) { export default function Pagination({ totalPages, currentPage }: Props) {
const prevPage = currentPage - 1 > 0; const prevPage = currentPage - 1 > 0
const nextPage = currentPage + 1 <= totalPages; const nextPage = currentPage + 1 <= totalPages
return ( return (
<div className="space-y-2 pt-6 pb-8 md:space-y-5"> <div className="space-y-2 pt-6 pb-8 md:space-y-5">
<nav className="flex justify-between"> <nav className="flex justify-between">
{!prevPage && ( {!prevPage && (
<button <button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
className="cursor-auto disabled:opacity-50"
disabled={!prevPage}>
Previous Previous
</button> </button>
)} )}
{prevPage && ( {prevPage && (
<Link <Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
href={
currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`
}>
<button>Previous</button> <button>Previous</button>
</Link> </Link>
)} )}
@ -31,9 +26,7 @@ export default function Pagination({ totalPages, currentPage }: Props) {
{currentPage} of {totalPages} {currentPage} of {totalPages}
</span> </span>
{!nextPage && ( {!nextPage && (
<button <button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
className="cursor-auto disabled:opacity-50"
disabled={!nextPage}>
Next Next
</button> </button>
)} )}
@ -44,5 +37,5 @@ export default function Pagination({ totalPages, currentPage }: Props) {
)} )}
</nav> </nav>
</div> </div>
); )
} }

View File

@ -1,35 +1,31 @@
import { useState, useRef, ReactNode } from 'react'; import { useState, useRef, ReactNode } from 'react'
interface Props { interface Props {
children: ReactNode; children: ReactNode
} }
const Pre = ({ children }: Props) => { const Pre = ({ children }: Props) => {
const textInput = useRef(null); const textInput = useRef(null)
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false)
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false)
const onEnter = () => { const onEnter = () => {
setHovered(true); setHovered(true)
}; }
const onExit = () => { const onExit = () => {
setHovered(false); setHovered(false)
setCopied(false); setCopied(false)
}; }
const onCopy = () => { const onCopy = () => {
setCopied(true); setCopied(true)
navigator.clipboard.writeText(textInput.current.textContent); navigator.clipboard.writeText(textInput.current.textContent)
setTimeout(() => { setTimeout(() => {
setCopied(false); setCopied(false)
}, 2000); }, 2000)
}; }
return ( return (
<div <div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
ref={textInput}
onMouseEnter={onEnter}
onMouseLeave={onExit}
className="relative">
{hovered && ( {hovered && (
<button <button
aria-label="Copy code" aria-label="Copy code"
@ -39,13 +35,15 @@ const Pre = ({ children }: Props) => {
? 'border-green-400 focus:border-green-400 focus:outline-none' ? 'border-green-400 focus:border-green-400 focus:outline-none'
: 'border-gray-300' : 'border-gray-300'
}`} }`}
onClick={onCopy}> onClick={onCopy}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
className={copied ? 'text-green-400' : 'text-gray-300'}> className={copied ? 'text-green-400' : 'text-gray-300'}
>
{copied ? ( {copied ? (
<> <>
<path <path
@ -71,7 +69,7 @@ const Pre = ({ children }: Props) => {
<pre>{children}</pre> <pre>{children}</pre>
</div> </div>
); )
}; }
export default Pre; export default Pre

View File

@ -1,21 +1,21 @@
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 { AuthorFrontMatter } from 'types/AuthorFrontMatter'; import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
interface CommonSEOProps { interface CommonSEOProps {
title: string; title: string
description: string; description: string
ogType: string; ogType: string
ogImage: ogImage:
| string | string
| { | {
'@type': string; '@type': string
url: string; url: string
}[]; }[]
twImage: string; twImage: string
canonicalUrl?: string; canonicalUrl?: string
} }
const CommonSEO = ({ const CommonSEO = ({
@ -26,24 +26,19 @@ const CommonSEO = ({
twImage, twImage,
canonicalUrl, canonicalUrl,
}: CommonSEOProps) => { }: CommonSEOProps) => {
const router = useRouter(); const router = useRouter()
return ( return (
<Head> <Head>
<title>{title}</title> <title>{title}</title>
<meta name="robots" content="follow, index" /> <meta name="robots" content="follow, index" />
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta <meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
property="og:url"
content={`${siteMetadata.siteUrl}${router.asPath}`}
/>
<meta property="og:type" content={ogType} /> <meta property="og:type" content={ogType} />
<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} />
{Array.isArray(ogImage) ? ( {Array.isArray(ogImage) ? (
ogImage.map(({ url }) => ( ogImage.map(({ url }) => <meta property="og:image" content={url} key={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} />
)} )}
@ -54,24 +49,20 @@ const CommonSEO = ({
<meta name="twitter:image" content={twImage} /> <meta name="twitter:image" content={twImage} />
<link <link
rel="canonical" rel="canonical"
href={ href={canonicalUrl ? canonicalUrl : `${siteMetadata.siteUrl}${router.asPath}`}
canonicalUrl
? canonicalUrl
: `${siteMetadata.siteUrl}${router.asPath}`
}
/> />
</Head> </Head>
); )
}; }
interface PageSEOProps { interface PageSEOProps {
title: string; title: string
description: string; description: string
} }
export const PageSEO = ({ title, description }: PageSEOProps) => { 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 (
<CommonSEO <CommonSEO
title={title} title={title}
@ -80,13 +71,13 @@ export const PageSEO = ({ title, description }: PageSEOProps) => {
ogImage={ogImageUrl} ogImage={ogImageUrl}
twImage={twImageUrl} twImage={twImageUrl}
/> />
); )
}; }
export const TagSEO = ({ title, description }: PageSEOProps) => { 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()
return ( return (
<> <>
<CommonSEO <CommonSEO
@ -105,12 +96,12 @@ export const TagSEO = ({ title, description }: PageSEOProps) => {
/> />
</Head> </Head>
</> </>
); )
}; }
interface BlogSeoProps extends PostFrontMatter { interface BlogSeoProps extends PostFrontMatter {
authorDetails?: AuthorFrontMatter[]; authorDetails?: AuthorFrontMatter[]
url: string; url: string
} }
export const BlogSEO = ({ export const BlogSEO = ({
@ -123,35 +114,35 @@ export const BlogSEO = ({
images = [], images = [],
canonicalUrl, canonicalUrl,
}: BlogSeoProps) => { }: BlogSeoProps) => {
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()
const imagesArr = const imagesArr =
images.length === 0 images.length === 0
? [siteMetadata.socialBanner] ? [siteMetadata.socialBanner]
: typeof images === 'string' : typeof images === 'string'
? [images] ? [images]
: images; : images
const featuredImages = imagesArr.map((img) => { const featuredImages = imagesArr.map((img) => {
return { return {
'@type': 'ImageObject', '@type': 'ImageObject',
url: `${siteMetadata.siteUrl}${img}`, url: `${siteMetadata.siteUrl}${img}`,
}; }
}); })
let authorList; let authorList
if (authorDetails) { if (authorDetails) {
authorList = authorDetails.map((author) => { authorList = authorDetails.map((author) => {
return { return {
'@type': 'Person', '@type': 'Person',
name: author.name, name: author.name,
}; }
}); })
} else { } else {
authorList = { authorList = {
'@type': 'Person', '@type': 'Person',
name: siteMetadata.author, name: siteMetadata.author,
}; }
} }
const structuredData = { const structuredData = {
@ -175,9 +166,9 @@ export const BlogSEO = ({
}, },
}, },
description: summary, description: summary,
}; }
const twImageUrl = featuredImages[0].url; const twImageUrl = featuredImages[0].url
return ( return (
<> <>
@ -190,12 +181,8 @@ export const BlogSEO = ({
canonicalUrl={canonicalUrl} canonicalUrl={canonicalUrl}
/> />
<Head> <Head>
{date && ( {date && <meta property="article:published_time" content={publishedAt} />}
<meta property="article:published_time" content={publishedAt} /> {lastmod && <meta property="article:modified_time" content={modifiedAt} />}
)}
{lastmod && (
<meta property="article:modified_time" content={modifiedAt} />
)}
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -204,5 +191,5 @@ export const BlogSEO = ({
/> />
</Head> </Head>
</> </>
); )
}; }

View File

@ -1,34 +1,34 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react'
const ScrollTopAndComment = () => { const ScrollTopAndComment = () => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false)
useEffect(() => { useEffect(() => {
const handleWindowScroll = () => { const handleWindowScroll = () => {
if (window.scrollY > 50) setShow(true); if (window.scrollY > 50) setShow(true)
else setShow(false); else setShow(false)
}; }
window.addEventListener('scroll', handleWindowScroll); window.addEventListener('scroll', handleWindowScroll)
return () => window.removeEventListener('scroll', handleWindowScroll); return () => window.removeEventListener('scroll', handleWindowScroll)
}, []); }, [])
const handleScrollTop = () => { const handleScrollTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' })
}; }
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 ${ className={`fixed right-8 bottom-8 hidden flex-col gap-3 ${show ? 'md:flex' : 'md:hidden'}`}
show ? 'md:flex' : 'md:hidden' >
}`}>
<button <button
aria-label="Scroll To Comment" aria-label="Scroll To Comment"
type="button" 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"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -41,7 +41,8 @@ const ScrollTopAndComment = () => {
aria-label="Scroll To Top" aria-label="Scroll To Top"
type="button" 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"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -51,7 +52,7 @@ const ScrollTopAndComment = () => {
</svg> </svg>
</button> </button>
</div> </div>
); )
}; }
export default ScrollTopAndComment; export default ScrollTopAndComment

View File

@ -1,13 +1,9 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react'
interface Props { interface Props {
children: ReactNode; children: ReactNode
} }
export default function SectionContainer({ children }: Props) { export default function SectionContainer({ children }: Props) {
return ( return <div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
<div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">
{children}
</div>
);
} }

View File

@ -1,12 +1,12 @@
import { Toc } from 'types/Toc'; import { Toc } from 'types/Toc'
interface TOCInlineProps { interface TOCInlineProps {
toc: Toc; toc: Toc
indentDepth?: number; indentDepth?: number
fromHeading?: number; fromHeading?: number
toHeading?: number; toHeading?: number
asDisclosure?: boolean; asDisclosure?: boolean
exclude?: string | string[]; exclude?: string | string[]
} }
/** /**
@ -34,41 +34,35 @@ const TOCInline = ({
}: TOCInlineProps) => { }: TOCInlineProps) => {
const re = Array.isArray(exclude) const re = Array.isArray(exclude)
? new RegExp('^(' + exclude.join('|') + ')$', 'i') ? new RegExp('^(' + exclude.join('|') + ')$', 'i')
: new RegExp('^(' + exclude + ')$', 'i'); : new RegExp('^(' + exclude + ')$', 'i')
const filteredToc = toc.filter( const filteredToc = toc.filter(
(heading) => (heading) =>
heading.depth >= fromHeading && heading.depth >= fromHeading && heading.depth <= toHeading && !re.test(heading.value)
heading.depth <= toHeading && )
!re.test(heading.value)
);
const tocList = ( const tocList = (
<ul> <ul>
{filteredToc.map((heading) => ( {filteredToc.map((heading) => (
<li <li key={heading.value} className={`${heading.depth >= indentDepth && 'ml-6'}`}>
key={heading.value}
className={`${heading.depth >= indentDepth && 'ml-6'}`}>
<a href={heading.url}>{heading.value}</a> <a href={heading.url}>{heading.value}</a>
</li> </li>
))} ))}
</ul> </ul>
); )
return ( return (
<> <>
{asDisclosure ? ( {asDisclosure ? (
<details open> <details open>
<summary className="ml-6 pt-2 pb-2 text-xl font-bold"> <summary className="ml-6 pt-2 pb-2 text-xl font-bold">Table of Contents</summary>
Table of Contents
</summary>
<div className="ml-6">{tocList}</div> <div className="ml-6">{tocList}</div>
</details> </details>
) : ( ) : (
tocList tocList
)} )}
</> </>
); )
}; }
export default TOCInline; export default TOCInline

View File

@ -1,8 +1,8 @@
import Link from 'next/link'; import Link from 'next/link'
import kebabCase from '@/lib/utils/kebabCase'; import kebabCase from '@/lib/utils/kebabCase'
interface Props { interface Props {
text: string; text: string
} }
const Tag = ({ text }: Props) => { const Tag = ({ text }: Props) => {
@ -12,7 +12,7 @@ const Tag = ({ text }: Props) => {
{text.split(' ').join('-')} {text.split(' ').join('-')}
</a> </a>
</Link> </Link>
); )
}; }
export default Tag; export default Tag

View File

@ -1,28 +1,26 @@
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, resolvedTheme } = 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), [])
return ( return (
<button <button
aria-label="Toggle Dark Mode" aria-label="Toggle Dark Mode"
type="button" 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={() => onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
setTheme( >
theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark'
)
}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
className="text-gray-900 dark:text-gray-100"> className="text-gray-900 dark:text-gray-100"
>
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? ( {mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -34,7 +32,7 @@ const ThemeSwitch = () => {
)} )}
</svg> </svg>
</button> </button>
); )
}; }
export default ThemeSwitch; export default ThemeSwitch

View File

@ -1,6 +1,6 @@
import Script from 'next/script'; import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
const GAScript = () => { const GAScript = () => {
return ( return (
@ -21,10 +21,10 @@ const GAScript = () => {
`} `}
</Script> </Script>
</> </>
); )
}; }
export default GAScript; export default GAScript
// https://developers.google.com/analytics/devguides/collection/gtagjs/events // https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const logEvent = (action, category, label, value) => { export const logEvent = (action, category, label, value) => {
@ -32,5 +32,5 @@ export const logEvent = (action, category, label, value) => {
event_category: category, event_category: category,
event_label: label, event_label: label,
value: value, value: value,
}); })
}; }

View File

@ -1,6 +1,6 @@
import Script from 'next/script'; import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
const PlausibleScript = () => { const PlausibleScript = () => {
return ( return (
@ -16,12 +16,12 @@ const PlausibleScript = () => {
`} `}
</Script> </Script>
</> </>
); )
}; }
export default PlausibleScript; export default PlausibleScript
// https://plausible.io/docs/custom-event-goals // https://plausible.io/docs/custom-event-goals
export const logEvent = (eventName, ...rest) => { export const logEvent = (eventName, ...rest) => {
return window.plausible?.(eventName, ...rest); return window.plausible?.(eventName, ...rest)
}; }

View File

@ -1,4 +1,4 @@
import Script from 'next/script'; import Script from 'next/script'
const SimpleAnalyticsScript = () => { const SimpleAnalyticsScript = () => {
return ( return (
@ -8,21 +8,18 @@ const SimpleAnalyticsScript = () => {
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]}; 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>
<Script <Script strategy="lazyOnload" src="https://scripts.simpleanalyticscdn.com/latest.js" />
strategy="lazyOnload"
src="https://scripts.simpleanalyticscdn.com/latest.js"
/>
</> </>
); )
}; }
// https://docs.simpleanalytics.com/events // https://docs.simpleanalytics.com/events
export const logEvent = (eventName, callback) => { export const logEvent = (eventName, callback) => {
if (callback) { if (callback) {
return window.sa_event?.(eventName, callback); return window.sa_event?.(eventName, callback)
} else { } else {
return window.sa_event?.(eventName); return window.sa_event?.(eventName)
} }
}; }
export default SimpleAnalyticsScript; export default SimpleAnalyticsScript

View File

@ -1,6 +1,6 @@
import Script from 'next/script'; import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
const UmamiScript = () => { const UmamiScript = () => {
return ( return (
@ -12,7 +12,7 @@ const UmamiScript = () => {
src="https://umami.example.com/umami.js" // Replace with your umami instance src="https://umami.example.com/umami.js" // Replace with your umami instance
/> />
</> </>
); )
}; }
export default UmamiScript; export default UmamiScript

View File

@ -1,32 +1,28 @@
import GA from './GoogleAnalytics'; import GA from './GoogleAnalytics'
import Plausible from './Plausible'; import Plausible from './Plausible'
import SimpleAnalytics from './SimpleAnalytics'; import SimpleAnalytics from './SimpleAnalytics'
import Umami from './Umami'; import Umami from './Umami'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
declare global { declare global {
interface Window { interface Window {
gtag?: (...args: any[]) => void; gtag?: (...args: any[]) => void
plausible?: (...args: any[]) => void; plausible?: (...args: any[]) => void
sa_event?: (...args: any[]) => void; sa_event?: (...args: any[]) => void
} }
} }
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production'
const Analytics = () => { const Analytics = () => {
return ( return (
<> <>
{isProduction && siteMetadata.analytics.plausibleDataDomain && ( {isProduction && siteMetadata.analytics.plausibleDataDomain && <Plausible />}
<Plausible /> {isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
)}
{isProduction && siteMetadata.analytics.simpleAnalytics && (
<SimpleAnalytics />
)}
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />} {isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />} {isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
</> </>
); )
}; }
export default Analytics; export default Analytics

View File

@ -1,33 +1,30 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes'
import ReactCommento from './commento/ReactCommento'; import ReactCommento from './commento/ReactCommento'
interface Props { interface Props {
frontMatter: PostFrontMatter; frontMatter: PostFrontMatter
} }
const Commento = ({ frontMatter }: Props) => { const Commento = ({ frontMatter }: Props) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme()
const commentsTheme = useMemo(() => { const commentsTheme = useMemo(() => {
switch (resolvedTheme) { switch (resolvedTheme) {
case 'light': case 'light':
case 'dark': case 'dark':
return resolvedTheme; return resolvedTheme
default: default:
return 'auto'; return 'auto'
} }
}, [resolvedTheme]); }, [resolvedTheme])
return ( return (
<div className="my-2"> <div className="my-2">
<ReactCommento <ReactCommento url={siteMetadata.comment.commentoConfig.url} pageId={frontMatter.slug} />
url={siteMetadata.comment.commentoConfig.url}
pageId={frontMatter.slug}
/>
</div> </div>
); )
}; }
export default Commento; export default Commento

View File

@ -1,25 +1,25 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
import { ReactCusdis } from 'react-cusdis'; import { ReactCusdis } from 'react-cusdis'
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes'
interface Props { interface Props {
frontMatter: PostFrontMatter; frontMatter: PostFrontMatter
} }
const Cusdis = ({ frontMatter }: Props) => { const Cusdis = ({ frontMatter }: Props) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme()
const commentsTheme = useMemo(() => { const commentsTheme = useMemo(() => {
switch (resolvedTheme) { switch (resolvedTheme) {
case 'light': case 'light':
case 'dark': case 'dark':
return resolvedTheme; return resolvedTheme
default: default:
return 'auto'; return 'auto'
} }
}, [resolvedTheme]); }, [resolvedTheme])
return ( return (
<div className="my-2"> <div className="my-2">
<ReactCusdis <ReactCusdis
@ -35,7 +35,7 @@ const Cusdis = ({ frontMatter }: Props) => {
}} }}
/> />
</div> </div>
); )
}; }
export default Cusdis; export default Cusdis

View File

@ -1,51 +1,46 @@
import React, { useState } from 'react'; import React, { useState } from 'react'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
interface Props { interface Props {
frontMatter: PostFrontMatter; frontMatter: PostFrontMatter
} }
const Disqus = ({ frontMatter }: Props) => { const Disqus = ({ frontMatter }: Props) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true); const [enableLoadComments, setEnabledLoadComments] = useState(true)
const COMMENTS_ID = 'disqus_thread'; const COMMENTS_ID = 'disqus_thread'
function LoadComments() { function LoadComments() {
setEnabledLoadComments(false); setEnabledLoadComments(false)
// @ts-ignore // @ts-ignore
window.disqus_config = function () { window.disqus_config = function () {
this.page.url = window.location.href; this.page.url = window.location.href
this.page.identifier = frontMatter.slug; this.page.identifier = frontMatter.slug
}; }
// @ts-ignore // @ts-ignore
if (window.DISQUS === undefined) { if (window.DISQUS === undefined) {
const script = document.createElement('script'); const script = document.createElement('script')
script.src = script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
'https://' +
siteMetadata.comment.disqusConfig.shortname +
'.disqus.com/embed.js';
// @ts-ignore // @ts-ignore
script.setAttribute('data-timestamp', +new Date()); script.setAttribute('data-timestamp', +new Date())
script.setAttribute('crossorigin', 'anonymous'); script.setAttribute('crossorigin', 'anonymous')
script.async = true; script.async = true
document.body.appendChild(script); document.body.appendChild(script)
} else { } else {
// @ts-ignore // @ts-ignore
window.DISQUS.reset({ reload: true }); window.DISQUS.reset({ reload: true })
} }
} }
return ( return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300"> <div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && ( {enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<button onClick={LoadComments}>Load Comments</button>
)}
<div className="disqus-frame" id={COMMENTS_ID} /> <div className="disqus-frame" id={COMMENTS_ID} />
</div> </div>
); )
}; }
export default Disqus; export default Disqus

View File

@ -1,78 +1,61 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react'
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
interface Props { interface Props {
mapping: string; mapping: string
} }
const Giscus = ({ mapping }: Props) => { const Giscus = ({ mapping }: Props) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true); const [enableLoadComments, setEnabledLoadComments] = useState(true)
const { theme, resolvedTheme } = useTheme(); const { theme, resolvedTheme } = useTheme()
const commentsTheme = const commentsTheme =
siteMetadata.comment.giscusConfig.themeURL === '' siteMetadata.comment.giscusConfig.themeURL === ''
? theme === 'dark' || resolvedTheme === 'dark' ? theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.giscusConfig.darkTheme ? siteMetadata.comment.giscusConfig.darkTheme
: siteMetadata.comment.giscusConfig.theme : siteMetadata.comment.giscusConfig.theme
: siteMetadata.comment.giscusConfig.themeURL; : siteMetadata.comment.giscusConfig.themeURL
const COMMENTS_ID = 'comments-container'; const COMMENTS_ID = 'comments-container'
const LoadComments = useCallback(() => { const LoadComments = useCallback(() => {
setEnabledLoadComments(false); setEnabledLoadComments(false)
const script = document.createElement('script'); const script = document.createElement('script')
script.src = 'https://giscus.app/client.js'; script.src = 'https://giscus.app/client.js'
script.setAttribute('data-repo', siteMetadata.comment.giscusConfig.repo); script.setAttribute('data-repo', siteMetadata.comment.giscusConfig.repo)
script.setAttribute( script.setAttribute('data-repo-id', siteMetadata.comment.giscusConfig.repositoryId)
'data-repo-id', script.setAttribute('data-category', siteMetadata.comment.giscusConfig.category)
siteMetadata.comment.giscusConfig.repositoryId script.setAttribute('data-category-id', siteMetadata.comment.giscusConfig.categoryId)
); script.setAttribute('data-mapping', mapping)
script.setAttribute( script.setAttribute('data-reactions-enabled', siteMetadata.comment.giscusConfig.reactions)
'data-category', script.setAttribute('data-emit-metadata', siteMetadata.comment.giscusConfig.metadata)
siteMetadata.comment.giscusConfig.category script.setAttribute('data-theme', commentsTheme)
); script.setAttribute('crossorigin', 'anonymous')
script.setAttribute( script.async = true
'data-category-id',
siteMetadata.comment.giscusConfig.categoryId
);
script.setAttribute('data-mapping', mapping);
script.setAttribute(
'data-reactions-enabled',
siteMetadata.comment.giscusConfig.reactions
);
script.setAttribute(
'data-emit-metadata',
siteMetadata.comment.giscusConfig.metadata
);
script.setAttribute('data-theme', commentsTheme);
script.setAttribute('crossorigin', 'anonymous');
script.async = true;
const comments = document.getElementById(COMMENTS_ID); const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.appendChild(script); if (comments) comments.appendChild(script)
return () => { return () => {
const comments = document.getElementById(COMMENTS_ID); const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.innerHTML = ''; if (comments) comments.innerHTML = ''
}; }
}, [commentsTheme, mapping]); }, [commentsTheme, mapping])
// Reload on theme change // Reload on theme change
useEffect(() => { useEffect(() => {
const iframe = document.querySelector('iframe.giscus-frame'); const iframe = document.querySelector('iframe.giscus-frame')
if (!iframe) return; if (!iframe) return
LoadComments(); LoadComments()
}, [LoadComments]); }, [LoadComments])
return ( return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300"> <div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && ( {enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<button onClick={LoadComments}>Load Comments</button>
)}
<div className="giscus" id={COMMENTS_ID} /> <div className="giscus" id={COMMENTS_ID} />
</div> </div>
); )
}; }
export default Giscus; export default Giscus

View File

@ -1,58 +1,56 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react'
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
interface Props { interface Props {
issueTerm: string; issueTerm: string
} }
const Utterances = ({ issueTerm }: Props) => { const Utterances = ({ issueTerm }: Props) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true); const [enableLoadComments, setEnabledLoadComments] = useState(true)
const { theme, resolvedTheme } = useTheme(); const { theme, resolvedTheme } = useTheme()
const commentsTheme = const commentsTheme =
theme === 'dark' || resolvedTheme === 'dark' theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.utterancesConfig.darkTheme ? siteMetadata.comment.utterancesConfig.darkTheme
: siteMetadata.comment.utterancesConfig.theme; : siteMetadata.comment.utterancesConfig.theme
const COMMENTS_ID = 'comments-container'; const COMMENTS_ID = 'comments-container'
const LoadComments = useCallback(() => { const LoadComments = useCallback(() => {
setEnabledLoadComments(false); setEnabledLoadComments(false)
const script = document.createElement('script'); const script = document.createElement('script')
script.src = 'https://utteranc.es/client.js'; script.src = 'https://utteranc.es/client.js'
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo); script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
script.setAttribute('issue-term', issueTerm); script.setAttribute('issue-term', issueTerm)
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label); script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
script.setAttribute('theme', commentsTheme); script.setAttribute('theme', commentsTheme)
script.setAttribute('crossorigin', 'anonymous'); script.setAttribute('crossorigin', 'anonymous')
script.async = true; script.async = true
const comments = document.getElementById(COMMENTS_ID); const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.appendChild(script); if (comments) comments.appendChild(script)
return () => { return () => {
const comments = document.getElementById(COMMENTS_ID); const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.innerHTML = ''; if (comments) comments.innerHTML = ''
}; }
}, [commentsTheme, issueTerm]); }, [commentsTheme, issueTerm])
// Reload on theme change // Reload on theme change
useEffect(() => { useEffect(() => {
const iframe = document.querySelector('iframe.utterances-frame'); const iframe = document.querySelector('iframe.utterances-frame')
if (!iframe) return; if (!iframe) return
LoadComments(); LoadComments()
}, [LoadComments]); }, [LoadComments])
// Added `relative` to fix a weird bug with `utterances-frame` position // Added `relative` to fix a weird bug with `utterances-frame` position
return ( return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300"> <div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && ( {enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<button onClick={LoadComments}>Load Comments</button>
)}
<div className="utterances-frame relative" id={COMMENTS_ID} /> <div className="utterances-frame relative" id={COMMENTS_ID} />
</div> </div>
); )
}; }
export default Utterances; export default Utterances

View File

@ -1,8 +1,8 @@
import { createRef } from 'preact'; import { createRef } from 'preact'
import React, { useLayoutEffect, useMemo, useRef } from 'react'; import React, { useLayoutEffect, useMemo, useRef } from 'react'
interface DataAttributes { interface DataAttributes {
[key: string]: string | boolean | undefined; [key: string]: string | boolean | undefined
} }
const insertScript = ( const insertScript = (
@ -11,28 +11,28 @@ const insertScript = (
dataAttributes: DataAttributes, dataAttributes: DataAttributes,
onload = () => {} onload = () => {}
) => { ) => {
const script = window.document.createElement('script'); const script = window.document.createElement('script')
script.async = true; script.async = true
script.src = src; script.src = src
script.id = id; script.id = id
if (document.getElementById(id)) { if (document.getElementById(id)) {
return; return
} }
script.addEventListener('load', onload, { capture: true, once: true }); script.addEventListener('load', onload, { capture: true, once: true })
Object.entries(dataAttributes).forEach(([key, value]) => { Object.entries(dataAttributes).forEach(([key, value]) => {
if (value === undefined) { if (value === undefined) {
return; return
} }
script.setAttribute(`data-${key}`, value.toString()); script.setAttribute(`data-${key}`, value.toString())
}); })
document.body.appendChild(script); document.body.appendChild(script)
return () => { return () => {
script.remove(); script.remove()
}; }
}; }
const ReactCommento = ({ const ReactCommento = ({
url, url,
@ -42,25 +42,22 @@ const ReactCommento = ({
hideDeleted, hideDeleted,
pageId, pageId,
}: { }: {
url: string; url: string
cssOverride?: string; cssOverride?: string
autoInit?: boolean; autoInit?: boolean
noFonts?: boolean; noFonts?: boolean
hideDeleted?: boolean; hideDeleted?: boolean
pageId?: string; pageId?: string
}) => { }) => {
const containerId = useMemo( const containerId = useMemo(() => `commento-${Math.random().toString().slice(2, 8)}`, [])
() => `commento-${Math.random().toString().slice(2, 8)}`, const container = createRef<HTMLDivElement>()
[]
);
const container = createRef<HTMLDivElement>();
useLayoutEffect(() => { useLayoutEffect(() => {
if (!window) { if (!window) {
return; return
} }
window['commento'] = container.current; window['commento'] = container.current
const removeScript = insertScript( const removeScript = insertScript(
url, url,
@ -74,20 +71,11 @@ const ReactCommento = ({
'id-root': containerId, 'id-root': containerId,
}, },
() => { () => {
removeScript(); removeScript()
} }
); )
}, [ }, [autoInit, cssOverride, hideDeleted, noFonts, pageId, url, containerId, container])
autoInit,
cssOverride,
hideDeleted,
noFonts,
pageId,
url,
containerId,
container,
]);
return <div ref={container} id={containerId} />; return <div ref={container} id={containerId} />
}; }
export default ReactCommento; export default ReactCommento

View File

@ -1,69 +1,68 @@
import React from 'react'; import React from 'react'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
interface Props { interface Props {
frontMatter: PostFrontMatter; frontMatter: PostFrontMatter
} }
const UtterancesComponent = dynamic( const UtterancesComponent = dynamic(
() => { () => {
return import('@/components/comments/Utterances'); return import('@/components/comments/Utterances')
}, },
{ ssr: false } { ssr: false }
); )
const GiscusComponent = dynamic( const GiscusComponent = dynamic(
() => { () => {
return import('@/components/comments/Giscus'); return import('@/components/comments/Giscus')
}, },
{ ssr: false } { ssr: false }
); )
const DisqusComponent = dynamic( const DisqusComponent = dynamic(
() => { () => {
return import('@/components/comments/Disqus'); return import('@/components/comments/Disqus')
}, },
{ ssr: false } { ssr: false }
); )
const CusdisComponent = dynamic( const CusdisComponent = dynamic(
() => { () => {
return import('@/components/comments/Cusdis'); return import('@/components/comments/Cusdis')
}, },
{ ssr: false } { ssr: false }
); )
const CommentoComponent = dynamic( const CommentoComponent = dynamic(
() => { () => {
return import('@/components/comments/Commento'); return import('@/components/comments/Commento')
}, },
{ ssr: false } { ssr: false }
); )
const Comments = ({ frontMatter }: Props) => { const Comments = ({ frontMatter }: Props) => {
let term; let term
switch ( switch (
siteMetadata.comment.giscusConfig.mapping || siteMetadata.comment.giscusConfig.mapping ||
siteMetadata.comment.utterancesConfig.issueTerm siteMetadata.comment.utterancesConfig.issueTerm
) { ) {
case 'pathname': case 'pathname':
term = frontMatter.slug; term = frontMatter.slug
break; break
case 'url': case 'url':
term = window.location.href; term = window.location.href
break; break
case 'title': case 'title':
term = frontMatter.title; term = frontMatter.title
break; break
} }
return ( return (
<div id="comment"> <div id="comment">
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && ( {siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && (
<GiscusComponent mapping={term} /> <GiscusComponent mapping={term} />
)} )}
{siteMetadata.comment && {siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
siteMetadata.comment.provider === 'utterances' && ( <UtterancesComponent issueTerm={term} />
<UtterancesComponent issueTerm={term} /> )}
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && ( {siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
<DisqusComponent frontMatter={frontMatter} /> <DisqusComponent frontMatter={frontMatter} />
)} )}
@ -74,7 +73,7 @@ const Comments = ({ frontMatter }: Props) => {
<CommentoComponent frontMatter={frontMatter} /> <CommentoComponent frontMatter={frontMatter} />
)} )}
</div> </div>
); )
}; }
export default Comments; export default Comments

View File

@ -1,9 +1,9 @@
import Mail from './mail.svg'; import Mail from './mail.svg'
import Github from './github.svg'; import Github from './github.svg'
import Facebook from './facebook.svg'; import Facebook from './facebook.svg'
import Youtube from './youtube.svg'; import Youtube from './youtube.svg'
import Linkedin from './linkedin.svg'; import Linkedin from './linkedin.svg'
import Twitter from './twitter.svg'; import Twitter from './twitter.svg'
// Icons taken from: https://simpleicons.org/ // Icons taken from: https://simpleicons.org/
@ -14,30 +14,27 @@ const components = {
youtube: Youtube, youtube: Youtube,
linkedin: Linkedin, linkedin: Linkedin,
twitter: Twitter, twitter: Twitter,
}; }
const SocialIcon = ({ kind, href, size = 8 }) => { const SocialIcon = ({ kind, href, size = 8 }) => {
if ( if (!href || (kind === 'mail' && !/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href)))
!href || return null
(kind === 'mail' &&
!/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
)
return null;
const SocialSvg = components[kind]; const SocialSvg = components[kind]
return ( return (
<a <a
className="text-sm text-gray-500 transition hover:text-gray-600" className="text-sm text-gray-500 transition hover:text-gray-600"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href={href}> href={href}
>
<span className="sr-only">{kind}</span> <span className="sr-only">{kind}</span>
<SocialSvg <SocialSvg
className={`fill-current text-gray-700 hover:text-blue-500 dark:text-gray-200 dark:hover:text-blue-400 h-${size} w-${size}`} className={`fill-current text-gray-700 hover:text-blue-500 dark:text-gray-200 dark:hover:text-blue-400 h-${size} w-${size}`}
/> />
</a> </a>
); )
}; }
export default SocialIcon; export default SocialIcon

View File

@ -1,13 +1,10 @@
--- ---
title: 安装并配置 Arch Linux title: 安装并配置 Arch Linux
date: '2022-10-17' date: '2022-10-17'
tags: ['Arch Linux', '环境搭建', 'VPS'] tags: ['Arch Linux', '环境搭建, 'VPS']
draft: false draft: false
summary: 又到了新装 Arch Linux 的日子了。这次又是温故而知新的机会,把之前写的笔记稍微整理了一下,在这里记录下教徒搭窝的备忘录。 summary: 又到了新装 Arch Linux 的日子了。这次又是温故而知新的机会,把之前写的笔记稍微整理了一下,在这里记录下教徒搭窝的备忘录。
images: images: ['https://pan.ivanli.cc/api/v3/file/source/2238/archlinux-logo-light.png?sign=bWxqFFy3RUDT5UsWb4UD5byt-_L4h79wede3runRKFc%3D%3A0']
[
'https://pan.ivanli.cc/api/v3/file/source/2238/archlinux-logo-light.png?sign=bWxqFFy3RUDT5UsWb4UD5byt-_L4h79wede3runRKFc%3D%3A0',
]
--- ---
## 起势 ## 起势

View File

@ -1,25 +1,16 @@
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'
import { ReactNode } from 'react'; import { ReactNode } from 'react'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'; import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
interface Props { interface Props {
children: ReactNode; children: ReactNode
frontMatter: AuthorFrontMatter; frontMatter: AuthorFrontMatter
} }
export default function AuthorLayout({ children, frontMatter }: Props) { export default function AuthorLayout({ children, frontMatter }: Props) {
const { const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
name,
avatar,
occupation,
company,
email,
twitter,
linkedin,
github,
} = frontMatter;
return ( return (
<> <>
@ -39,9 +30,7 @@ export default function AuthorLayout({ children, frontMatter }: Props) {
height="192px" height="192px"
className="h-48 w-48 rounded-full" className="h-48 w-48 rounded-full"
/> />
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight"> <h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">{name}</h3>
{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">
@ -51,11 +40,9 @@ export default function AuthorLayout({ children, frontMatter }: Props) {
<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"> <div className="prose max-w-none pt-8 pb-8 dark:prose-dark xl:col-span-2">{children}</div>
{children}
</div>
</div> </div>
</div> </div>
</> </>
); )
} }

View File

@ -1,34 +1,26 @@
import Link from '@/components/Link'; import Link from '@/components/Link'
import Tag from '@/components/Tag'; import Tag from '@/components/Tag'
import { ComponentProps, useState } from 'react'; import { ComponentProps, useState } from 'react'
import Pagination from '@/components/Pagination'; import Pagination from '@/components/Pagination'
import formatDate from '@/lib/utils/formatDate'; import formatDate from '@/lib/utils/formatDate'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
interface Props { interface Props {
posts: PostFrontMatter[]; posts: PostFrontMatter[]
title: string; title: string
initialDisplayPosts?: PostFrontMatter[]; initialDisplayPosts?: PostFrontMatter[]
pagination?: ComponentProps<typeof Pagination>; pagination?: ComponentProps<typeof Pagination>
} }
export default function ListLayout({ export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }: Props) {
posts, const [searchValue, setSearchValue] = useState('')
title,
initialDisplayPosts = [],
pagination,
}: Props) {
const [searchValue, setSearchValue] = useState('');
const filteredBlogPosts = posts.filter((frontMatter) => { const filteredBlogPosts = posts.filter((frontMatter) => {
const searchContent = const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')
frontMatter.title + frontMatter.summary + frontMatter.tags.join(' '); return searchContent.toLowerCase().includes(searchValue.toLowerCase())
return searchContent.toLowerCase().includes(searchValue.toLowerCase()); })
});
// If initialDisplayPosts exist, display it if no searchValue is specified // If initialDisplayPosts exist, display it if no searchValue is specified
const displayPosts = const displayPosts =
initialDisplayPosts.length > 0 && !searchValue initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
? initialDisplayPosts
: filteredBlogPosts;
return ( return (
<> <>
@ -50,7 +42,8 @@ export default function ListLayout({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@ -63,7 +56,7 @@ export default function ListLayout({
<ul> <ul>
{!filteredBlogPosts.length && 'No posts found.'} {!filteredBlogPosts.length && 'No posts found.'}
{displayPosts.map((frontMatter) => { {displayPosts.map((frontMatter) => {
const { slug, date, title, summary, tags } = frontMatter; const { slug, date, title, summary, tags } = frontMatter
return ( return (
<li key={slug} className="py-4"> <li key={slug} className="py-4">
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0"> <article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
@ -76,9 +69,7 @@ export default function ListLayout({
<div className="space-y-3 xl:col-span-3"> <div className="space-y-3 xl:col-span-3">
<div> <div>
<h3 className="text-2xl font-bold leading-8 tracking-tight"> <h3 className="text-2xl font-bold leading-8 tracking-tight">
<Link <Link href={`/blog/${slug}`} className="text-gray-900 dark:text-gray-100">
href={`/blog/${slug}`}
className="text-gray-900 dark:text-gray-100">
{title} {title}
</Link> </Link>
</h3> </h3>
@ -94,16 +85,13 @@ export default function ListLayout({
</div> </div>
</article> </article>
</li> </li>
); )
})} })}
</ul> </ul>
</div> </div>
{pagination && pagination.totalPages > 1 && !searchValue && ( {pagination && pagination.totalPages > 1 && !searchValue && (
<Pagination <Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
/>
)} )}
</> </>
); )
} }

View File

@ -1,27 +1,27 @@
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 Comments from '@/components/comments'
import ScrollTopAndComment from '@/components/ScrollTopAndComment'; import ScrollTopAndComment from '@/components/ScrollTopAndComment'
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'; import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
const editUrl = (fileName) => const editUrl = (fileName) => `${siteMetadata.siteRepo}/raw/master/data/blog/${fileName}`
`${siteMetadata.siteRepo}/raw/master/data/blog/${fileName}`;
const discussUrl = (slug) => const discussUrl = (slug) =>
`https://mobile.twitter.com/search?q=${encodeURIComponent( `https://mobile.twitter.com/search?q=${encodeURIComponent(
`${siteMetadata.siteUrl}/blog/${slug}` `${siteMetadata.siteUrl}/blog/${slug}`
)}`; )}`
const Copyright = () => ( const Copyright = () => (
<a <a
rel="license" rel="license"
href="http://creativecommons.org/licenses/by-sa/4.0/" href="http://creativecommons.org/licenses/by-sa/4.0/"
className="inline-flex self-center"> className="inline-flex self-center"
>
<Image <Image
className="border-0" className="border-0"
alt="知识共享许可协议" alt="知识共享许可协议"
@ -30,31 +30,25 @@ const Copyright = () => (
src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png"
/> />
</a> </a>
); )
const postDateTemplate: Intl.DateTimeFormatOptions = { const postDateTemplate: Intl.DateTimeFormatOptions = {
weekday: 'long', weekday: 'long',
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
};
interface Props {
frontMatter: PostFrontMatter;
authorDetails: AuthorFrontMatter[];
next?: { slug: string; title: string };
prev?: { slug: string; title: string };
children: ReactNode;
} }
export default function PostLayout({ interface Props {
frontMatter, frontMatter: PostFrontMatter
authorDetails, authorDetails: AuthorFrontMatter[]
next, next?: { slug: string; title: string }
prev, prev?: { slug: string; title: string }
children, children: ReactNode
}: Props) { }
const { slug, fileName, date, title, images, tags } = frontMatter;
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }: Props) {
const { slug, fileName, date, title, images, tags } = frontMatter
const headerStyles = useMemo( const headerStyles = useMemo(
() => () =>
@ -64,7 +58,7 @@ export default function PostLayout({
} }
: {}, : {},
[images] [images]
); )
return ( return (
<SectionContainer> <SectionContainer>
@ -93,10 +87,7 @@ export default function PostLayout({
<dt className="sr-only">Published on</dt> <dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400"> <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
<time dateTime={date}> <time dateTime={date}>
{new Date(date).toLocaleDateString( {new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
siteMetadata.locale,
postDateTemplate
)}
</time> </time>
</dd> </dd>
</div> </div>
@ -108,15 +99,14 @@ export default function PostLayout({
</header> </header>
<div <div
className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0" className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0"
style={{ gridTemplateRows: 'auto 1fr' }}> 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"> <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 justify-center space-x-8 sm:space-x-12 xl:block xl:space-x-0 xl:space-y-8">
{authorDetails.map((author) => ( {authorDetails.map((author) => (
<li <li className="flex items-center space-x-2" key={author.name}>
className="flex items-center space-x-2"
key={author.name}>
{author.avatar && ( {author.avatar && (
<Image <Image
src={author.avatar} src={author.avatar}
@ -128,19 +118,15 @@ export default function PostLayout({
)} )}
<dl className="whitespace-nowrap text-sm font-medium leading-5"> <dl className="whitespace-nowrap text-sm font-medium leading-5">
<dt className="sr-only">Name</dt> <dt className="sr-only">Name</dt>
<dd className="text-gray-900 dark:text-gray-100"> <dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
{author.name}
</dd>
<dt className="sr-only">Twitter</dt> <dt className="sr-only">Twitter</dt>
<dd> <dd>
{author.twitter && ( {author.twitter && (
<Link <Link
href={author.twitter} href={author.twitter}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"> className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
{author.twitter.replace( >
'https://twitter.com/', {author.twitter.replace('https://twitter.com/', '@')}
'@'
)}
</Link> </Link>
)} )}
</dd> </dd>
@ -151,9 +137,7 @@ export default function PostLayout({
</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"> <div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
{children}
</div>
<div className="flex items-center gap-4 pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300"> <div className="flex items-center gap-4 pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
<Copyright /> <Copyright />
<Link href={editUrl(fileName)}>{'View source'}</Link> <Link href={editUrl(fileName)}>{'View source'}</Link>
@ -202,7 +186,8 @@ export default function PostLayout({
<div className="pt-4 xl:pt-8"> <div className="pt-4 xl:pt-8">
<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"
>
&larr; Back to the blog &larr; Back to the blog
</Link> </Link>
</div> </div>
@ -211,5 +196,5 @@ export default function PostLayout({
</div> </div>
</article> </article>
</SectionContainer> </SectionContainer>
); )
} }

View File

@ -1,28 +1,23 @@
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 formatDate from '@/lib/utils/formatDate'
import Comments from '@/components/comments'; import Comments from '@/components/comments'
import ScrollTopAndComment from '@/components/ScrollTopAndComment'; import ScrollTopAndComment from '@/components/ScrollTopAndComment'
import { ReactNode } from 'react'; import { ReactNode } from 'react'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
interface Props { interface Props {
frontMatter: PostFrontMatter; frontMatter: PostFrontMatter
children: ReactNode; children: ReactNode
next?: { slug: string; title: string }; next?: { slug: string; title: string }
prev?: { slug: string; title: string }; prev?: { slug: string; title: string }
} }
export default function PostLayout({ export default function PostLayout({ frontMatter, next, prev, children }: Props) {
frontMatter, const { slug, date, title } = frontMatter
next,
prev,
children,
}: Props) {
const { slug, date, title } = frontMatter;
return ( return (
<SectionContainer> <SectionContainer>
@ -47,11 +42,10 @@ export default function PostLayout({
</header> </header>
<div <div
className="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' }}> 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"> <div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
{children}
</div>
</div> </div>
<Comments frontMatter={frontMatter} /> <Comments frontMatter={frontMatter} />
<footer> <footer>
@ -60,7 +54,8 @@ export default function PostLayout({
<div className="pt-4 xl:pt-8"> <div className="pt-4 xl:pt-8">
<Link <Link
href={`/blog/${prev.slug}`} href={`/blog/${prev.slug}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"> className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
&larr; {prev.title} &larr; {prev.title}
</Link> </Link>
</div> </div>
@ -69,7 +64,8 @@ export default function PostLayout({
<div className="pt-4 xl:pt-8"> <div className="pt-4 xl:pt-8">
<Link <Link
href={`/blog/${next.slug}`} href={`/blog/${next.slug}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"> className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
{next.title} &rarr; {next.title} &rarr;
</Link> </Link>
</div> </div>
@ -80,5 +76,5 @@ export default function PostLayout({
</div> </div>
</article> </article>
</SectionContainer> </SectionContainer>
); )
} }

View File

@ -1,7 +1,7 @@
import { escape } from '@/lib/utils/htmlEscaper'; import { escape } from '@/lib/utils/htmlEscaper'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
const generateRssItem = (post: PostFrontMatter) => ` const generateRssItem = (post: PostFrontMatter) => `
<item> <item>
@ -13,7 +13,7 @@ const generateRssItem = (post: PostFrontMatter) => `
<author>${siteMetadata.email} (${siteMetadata.author})</author> <author>${siteMetadata.email} (${siteMetadata.author})</author>
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')} ${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
</item> </item>
`; `
const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => ` const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
@ -22,16 +22,12 @@ const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
<link>${siteMetadata.siteUrl}/blog</link> <link>${siteMetadata.siteUrl}/blog</link>
<description>${escape(siteMetadata.description)}</description> <description>${escape(siteMetadata.description)}</description>
<language>${siteMetadata.language}</language> <language>${siteMetadata.language}</language>
<managingEditor>${siteMetadata.email} (${ <managingEditor>${siteMetadata.email} (${siteMetadata.author})</managingEditor>
siteMetadata.author
})</managingEditor>
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster> <webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate> <lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
<atom:link href="${ <atom:link href="${siteMetadata.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
siteMetadata.siteUrl
}/${page}" rel="self" type="application/rss+xml"/>
${posts.map(generateRssItem).join('')} ${posts.map(generateRssItem).join('')}
</channel> </channel>
</rss> </rss>
`; `
export default generateRss; export default generateRss

View File

@ -1,78 +1,62 @@
import { bundleMDX } from 'mdx-bundler'; import { bundleMDX } from 'mdx-bundler'
import fs from 'fs'; import fs from 'fs'
import matter from 'gray-matter'; import matter from 'gray-matter'
import path from 'path'; import path from 'path'
import readingTime from 'reading-time'; import readingTime from 'reading-time'
import getAllFilesRecursively from './utils/files'; import getAllFilesRecursively from './utils/files'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'; import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { Toc } from 'types/Toc'; import { Toc } from 'types/Toc'
// Remark packages // Remark packages
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm'
import remarkFootnotes from 'remark-footnotes'; import remarkFootnotes from 'remark-footnotes'
import remarkMath from 'remark-math'; import remarkMath from 'remark-math'
import remarkExtractFrontmatter from './remark-extract-frontmatter'; import remarkExtractFrontmatter from './remark-extract-frontmatter'
import remarkCodeTitles from './remark-code-title'; import remarkCodeTitles from './remark-code-title'
import remarkTocHeadings from './remark-toc-headings'; import remarkTocHeadings from './remark-toc-headings'
import remarkImgToJsx from './remark-img-to-jsx'; import remarkImgToJsx from './remark-img-to-jsx'
// Rehype packages // Rehype packages
import rehypeSlug from 'rehype-slug'; import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex'
import rehypeCitation from 'rehype-citation'; import rehypeCitation from 'rehype-citation'
import rehypePrismPlus from 'rehype-prism-plus'; import rehypePrismPlus from 'rehype-prism-plus'
import rehypePresetMinify from 'rehype-preset-minify'; import rehypePresetMinify from 'rehype-preset-minify'
const root = process.cwd(); const root = process.cwd()
export function getFiles(type: 'blog' | 'authors') { export function getFiles(type: 'blog' | 'authors') {
const prefixPaths = path.join(root, 'data', type); const prefixPaths = path.join(root, 'data', type)
const files = getAllFilesRecursively(prefixPaths); const files = getAllFilesRecursively(prefixPaths)
// Only want to return blog/path and ignore root, replace is needed to work on Windows // Only want to return blog/path and ignore root, replace is needed to work on Windows
return files.map((file) => return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
);
} }
export function formatSlug(slug: string) { export function formatSlug(slug: string) {
return slug.replace(/\.(mdx|md)/, ''); return slug.replace(/\.(mdx|md)/, '')
} }
export function dateSortDesc(a: string, b: string) { export function dateSortDesc(a: string, b: string) {
if (a > b) return -1; if (a > b) return -1
if (a < b) return 1; if (a < b) return 1
return 0; return 0
} }
export async function getFileBySlug<T>( export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string | string[]) {
type: 'authors' | 'blog', const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
slug: string | string[] const mdPath = path.join(root, 'data', type, `${slug}.md`)
) {
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`);
const mdPath = path.join(root, 'data', type, `${slug}.md`);
const source = fs.existsSync(mdxPath) const source = fs.existsSync(mdxPath)
? fs.readFileSync(mdxPath, 'utf8') ? fs.readFileSync(mdxPath, 'utf8')
: fs.readFileSync(mdPath, 'utf8'); : fs.readFileSync(mdPath, 'utf8')
// https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent // https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent
if (process.platform === 'win32') { if (process.platform === 'win32') {
process.env.ESBUILD_BINARY_PATH = path.join( process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'esbuild.exe')
root,
'node_modules',
'esbuild',
'esbuild.exe'
);
} else { } else {
process.env.ESBUILD_BINARY_PATH = path.join( process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'bin', 'esbuild')
root,
'node_modules',
'esbuild',
'bin',
'esbuild'
);
} }
const toc: Toc = []; const toc: Toc = []
const { code, frontmatter } = await bundleMDX({ const { code, frontmatter } = await bundleMDX({
source, source,
@ -91,7 +75,7 @@ export async function getFileBySlug<T>(
[remarkFootnotes, { inlineNotes: true }], [remarkFootnotes, { inlineNotes: true }],
remarkMath, remarkMath,
remarkImgToJsx, remarkImgToJsx,
]; ]
options.rehypePlugins = [ options.rehypePlugins = [
...(options.rehypePlugins ?? []), ...(options.rehypePlugins ?? []),
rehypeSlug, rehypeSlug,
@ -100,17 +84,17 @@ export async function getFileBySlug<T>(
[rehypeCitation, { path: path.join(root, 'data') }], [rehypeCitation, { path: path.join(root, 'data') }],
[rehypePrismPlus, { ignoreMissing: true }], [rehypePrismPlus, { ignoreMissing: true }],
rehypePresetMinify, rehypePresetMinify,
]; ]
return options; return options
}, },
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.loader = { options.loader = {
...options.loader, ...options.loader,
'.js': 'jsx', '.js': 'jsx',
}; }
return options; return options
}, },
}); })
return { return {
mdxSource: code, mdxSource: code,
@ -122,36 +106,34 @@ export async function getFileBySlug<T>(
...frontmatter, ...frontmatter,
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null, date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
}, },
}; }
} }
export async function getAllFilesFrontMatter(folder: 'blog') { export async function getAllFilesFrontMatter(folder: 'blog') {
const prefixPaths = path.join(root, 'data', folder); const prefixPaths = path.join(root, 'data', folder)
const files = getAllFilesRecursively(prefixPaths); const files = getAllFilesRecursively(prefixPaths)
const allFrontMatter: PostFrontMatter[] = []; const allFrontMatter: PostFrontMatter[] = []
files.forEach((file: string) => { files.forEach((file: string) => {
// Replace is needed to work on Windows // Replace is needed to work on Windows
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/'); const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
// Remove Unexpected File // Remove Unexpected File
if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') {
return; return
} }
const source = fs.readFileSync(file, 'utf8'); const source = fs.readFileSync(file, 'utf8')
const matterFile = matter(source); const matterFile = matter(source)
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter; const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter
if ('draft' in frontmatter && frontmatter.draft !== true) { if ('draft' in frontmatter && frontmatter.draft !== true) {
allFrontMatter.push({ allFrontMatter.push({
...frontmatter, ...frontmatter,
slug: formatSlug(fileName), slug: formatSlug(fileName),
date: frontmatter.date date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
? new Date(frontmatter.date).toISOString() })
: null,
});
} }
}); })
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date)); return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
} }

View File

@ -1,38 +1,32 @@
import { visit, Parent } from 'unist-util-visit'; import { visit, Parent } from 'unist-util-visit'
export default function remarkCodeTitles() { export default function remarkCodeTitles() {
return (tree: Parent & { lang?: string }) => return (tree: Parent & { lang?: string }) =>
visit( visit(tree, 'code', (node: Parent & { lang?: string }, index, parent: Parent) => {
tree, const nodeLang = node.lang || ''
'code', let language = ''
(node: Parent & { lang?: string }, index, parent: Parent) => { let title = ''
const nodeLang = node.lang || '';
let language = '';
let title = '';
if (nodeLang.includes(':')) { if (nodeLang.includes(':')) {
language = nodeLang.slice(0, nodeLang.search(':')); language = nodeLang.slice(0, nodeLang.search(':'))
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length); 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;
} }
);
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
})
} }

View File

@ -1,12 +1,12 @@
import { VFile } from 'vfile'; import { VFile } from 'vfile'
import { visit, Parent } from 'unist-util-visit'; import { visit, Parent } from 'unist-util-visit'
import { load } from 'js-yaml'; import { load } from 'js-yaml'
export default function extractFrontmatter() { export default function extractFrontmatter() {
return (tree: Parent, file: VFile) => { return (tree: Parent, file: VFile) => {
visit(tree, 'yaml', (node: Parent) => { visit(tree, 'yaml', (node: Parent) => {
//@ts-ignore //@ts-ignore
file.data.frontmatter = load(node.value); file.data.frontmatter = load(node.value)
}); })
}; }
} }

View File

@ -1,14 +1,14 @@
import { Literal } from 'unist'; import { Literal } from 'unist'
import { visit, Parent, Node } from 'unist-util-visit'; import { visit, Parent, Node } from 'unist-util-visit'
import sizeOf from 'image-size'; import sizeOf from 'image-size'
import fs from 'fs'; import fs from 'fs'
type ImageNode = Parent & { type ImageNode = Parent & {
url: string; url: string
alt: string; alt: string
name: string; name: string
attributes: (Literal & { name: string })[]; attributes: (Literal & { name: string })[]
}; }
export default function remarkImgToJsx() { export default function remarkImgToJsx() {
return (tree: Node) => { return (tree: Node) => {
@ -16,40 +16,29 @@ export default function remarkImgToJsx() {
tree, tree,
// only visit p tags that contain an img element // only visit p tags that contain an img element
(node: Parent): node is Parent => (node: Parent): node is Parent =>
node.type === 'paragraph' && node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
node.children.some((n) => n.type === 'image'),
(node: Parent) => { (node: Parent) => {
const imageNode = node.children.find( const imageNode = node.children.find((n) => n.type === 'image') as ImageNode
(n) => n.type === 'image'
) as ImageNode;
// only local files // only local files
if (fs.existsSync(`${process.cwd()}/public${imageNode.url}`)) { if (fs.existsSync(`${process.cwd()}/public${imageNode.url}`)) {
const dimensions = sizeOf(`${process.cwd()}/public${imageNode.url}`); const dimensions = sizeOf(`${process.cwd()}/public${imageNode.url}`)
// Convert original node to next/image // Convert original node to next/image
(imageNode.type = 'mdxJsxFlowElement'), ;(imageNode.type = 'mdxJsxFlowElement'),
(imageNode.name = 'Image'), (imageNode.name = 'Image'),
(imageNode.attributes = [ (imageNode.attributes = [
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt }, { type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
{ type: 'mdxJsxAttribute', name: 'src', value: imageNode.url }, { type: 'mdxJsxAttribute', name: 'src', value: imageNode.url },
{ { type: 'mdxJsxAttribute', name: 'width', value: dimensions.width },
type: 'mdxJsxAttribute', { type: 'mdxJsxAttribute', name: 'height', value: dimensions.height },
name: 'width', ])
value: dimensions.width,
},
{
type: 'mdxJsxAttribute',
name: 'height',
value: dimensions.height,
},
]);
// Change node type from p to div to avoid nesting error // Change node type from p to div to avoid nesting error
node.type = 'div'; node.type = 'div'
node.children = [imageNode]; node.children = [imageNode]
} }
} }
); )
}; }
} }

View File

@ -1,17 +1,17 @@
//@ts-nocheck //@ts-nocheck
import { Parent } from 'unist'; import { Parent } from 'unist'
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit'
import { slug } from 'github-slugger'; import { slug } from 'github-slugger'
import { toString } from 'mdast-util-to-string'; import { toString } from 'mdast-util-to-string'
export default function remarkTocHeadings(options) { export default function remarkTocHeadings(options) {
return (tree: Parent) => return (tree: Parent) =>
visit(tree, 'heading', (node) => { visit(tree, 'heading', (node) => {
const textContent = toString(node); const textContent = toString(node)
options.exportRef.push({ options.exportRef.push({
value: textContent, value: textContent,
url: '#' + slug(textContent), url: '#' + slug(textContent),
depth: node.depth, depth: node.depth,
}); })
}); })
} }

View File

@ -1,32 +1,32 @@
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
import fs from 'fs'; import fs from 'fs'
import matter from 'gray-matter'; import matter from 'gray-matter'
import path from 'path'; import path from 'path'
import { getFiles } from './mdx'; import { getFiles } from './mdx'
import kebabCase from './utils/kebabCase'; import kebabCase from './utils/kebabCase'
const root = process.cwd(); const root = process.cwd()
export async function getAllTags(type: 'blog' | 'authors') { export async function getAllTags(type: 'blog' | 'authors') {
const files = getFiles(type); const files = getFiles(type)
const tagCount: Record<string, number> = {}; const tagCount: Record<string, number> = {}
// Iterate through each post, putting all found tags into `tags` // Iterate through each post, putting all found tags into `tags`
files.forEach((file) => { files.forEach((file) => {
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8'); const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
const matterFile = matter(source); const matterFile = matter(source)
const data = matterFile.data as PostFrontMatter; const data = matterFile.data as PostFrontMatter
if (data.tags && data.draft !== true) { if (data.tags && data.draft !== true) {
data.tags.forEach((tag) => { data.tags.forEach((tag) => {
const formattedTag = kebabCase(tag); const formattedTag = kebabCase(tag)
if (formattedTag in tagCount) { if (formattedTag in tagCount) {
tagCount[formattedTag] += 1; tagCount[formattedTag] += 1
} else { } else {
tagCount[formattedTag] = 1; tagCount[formattedTag] = 1
} }
}); })
} }
}); })
return tagCount; return tagCount
} }

View File

@ -1,33 +1,23 @@
import fs from 'fs'; import fs from 'fs'
import path from 'path'; import path from 'path'
const pipe = const pipe =
(...fns) => (...fns) =>
(x) => (x) =>
fns.reduce((v, f) => f(v), x); fns.reduce((v, f) => f(v), x)
const flattenArray = (input) => const flattenArray = (input) =>
input.reduce( input.reduce((acc, item) => [...acc, ...(Array.isArray(item) ? item : [item])], [])
(acc, item) => [...acc, ...(Array.isArray(item) ? item : [item])],
[]
);
const map = (fn) => (input) => input.map(fn); const map = (fn) => (input) => input.map(fn)
const walkDir = (fullPath: string) => { const walkDir = (fullPath: string) => {
return fs.statSync(fullPath).isFile() return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
? fullPath }
: getAllFilesRecursively(fullPath);
};
const pathJoinPrefix = (prefix: string) => (extraPath: string) => const pathJoinPrefix = (prefix: string) => (extraPath: string) => path.join(prefix, extraPath)
path.join(prefix, extraPath);
const getAllFilesRecursively = (folder: string): string[] => const getAllFilesRecursively = (folder: string): string[] =>
pipe( pipe(fs.readdirSync, map(pipe(pathJoinPrefix(folder), walkDir)), flattenArray)(folder)
fs.readdirSync,
map(pipe(pathJoinPrefix(folder), walkDir)),
flattenArray
)(folder);
export default getAllFilesRecursively; export default getAllFilesRecursively

View File

@ -1,14 +1,14 @@
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
const formatDate = (date: string) => { const formatDate = (date: string) => {
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
}; }
const now = new Date(date).toLocaleDateString(siteMetadata.locale, options); const now = new Date(date).toLocaleDateString(siteMetadata.locale, options)
return now; return now
}; }
export default formatDate; export default formatDate

View File

@ -1,7 +1,7 @@
const { replace } = ''; const { replace } = ''
// escape // escape
const ca = /[&<>'"]/g; const ca = /[&<>'"]/g
const esca = { const esca = {
'&': '&amp;', '&': '&amp;',
@ -9,8 +9,8 @@ const esca = {
'>': '&gt;', '>': '&gt;',
"'": '&#39;', "'": '&#39;',
'"': '&quot;', '"': '&quot;',
}; }
const pe = (m: keyof typeof esca) => esca[m]; const pe = (m: keyof typeof esca) => esca[m]
/** /**
* Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`. * Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
@ -19,4 +19,4 @@ const pe = (m: keyof typeof esca) => esca[m];
* the input type is unexpected, except for boolean and numbers, * the input type is unexpected, except for boolean and numbers,
* converted as string. * converted as string.
*/ */
export const escape = (es: string): string => replace.call(es, ca, pe); export const escape = (es: string): string => replace.call(es, ca, pe)

View File

@ -1,5 +1,5 @@
import { slug } from 'github-slugger'; import { slug } from 'github-slugger'
const kebabCase = (str: string) => slug(str); const kebabCase = (str: string) => slug(str)
export default kebabCase; export default kebabCase

View File

@ -1,4 +1,4 @@
import Link from '@/components/Link'; import Link from '@/components/Link'
export default function FourZeroFour() { export default function FourZeroFour() {
return ( return (
@ -12,9 +12,7 @@ export default function FourZeroFour() {
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl"> <p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
Sorry we couldn't find this page. Sorry we couldn't find this page.
</p> </p>
<p className="mb-8"> <p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
But dont worry, you can find plenty of other things on our homepage.
</p>
<Link href="/"> <Link href="/">
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500"> <button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
Back to homepage Back to homepage
@ -22,5 +20,5 @@ export default function FourZeroFour() {
</Link> </Link>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,20 +1,20 @@
import '@/css/tailwind.css'; import '@/css/tailwind.css'
import '@/css/prism.css'; import '@/css/prism.css'
import 'katex/dist/katex.css'; import 'katex/dist/katex.css'
import '@fontsource/inter/variable.css'; import '@fontsource/inter/variable.css'
import { ThemeProvider } from 'next-themes'; import { ThemeProvider } from 'next-themes'
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app'
import Head from 'next/head'; import Head from 'next/head'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import Analytics from '@/components/analytics'; import Analytics from '@/components/analytics'
import LayoutWrapper from '@/components/LayoutWrapper'; import LayoutWrapper from '@/components/LayoutWrapper'
import { ClientReload } from '@/components/ClientReload'; import { ClientReload } from '@/components/ClientReload'
const isDevelopment = process.env.NODE_ENV === 'development'; const isDevelopment = process.env.NODE_ENV === 'development'
const isSocket = process.env.SOCKET; const isSocket = process.env.SOCKET
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
return ( return (
@ -28,5 +28,5 @@ export default function App({ Component, pageProps }: AppProps) {
<Component {...pageProps} /> <Component {...pageProps} />
</LayoutWrapper> </LayoutWrapper>
</ThemeProvider> </ThemeProvider>
); )
} }

View File

@ -1,15 +1,11 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'; import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document { class MyDocument extends Document {
render() { render() {
return ( return (
<Html lang="en" className="scroll-smooth"> <Html lang="en" className="scroll-smooth">
<Head> <Head>
<link <link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
rel="apple-touch-icon"
sizes="76x76"
href="/static/favicons/apple-touch-icon.png"
/>
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
@ -23,22 +19,10 @@ class MyDocument extends Document {
href="/static/favicons/favicon-16x16.png" href="/static/favicons/favicon-16x16.png"
/> />
<link rel="manifest" href="/static/favicons/site.webmanifest" /> <link rel="manifest" href="/static/favicons/site.webmanifest" />
<link <link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
rel="mask-icon"
href="/static/favicons/safari-pinned-tab.svg"
color="#5bbad5"
/>
<meta name="msapplication-TileColor" content="#000000" /> <meta name="msapplication-TileColor" content="#000000" />
<meta <meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
name="theme-color" <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
media="(prefers-color-scheme: light)"
content="#fff"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#000"
/>
<link rel="alternate" type="application/rss+xml" href="/feed.xml" /> <link rel="alternate" type="application/rss+xml" href="/feed.xml" />
</Head> </Head>
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white"> <body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
@ -46,8 +30,8 @@ class MyDocument extends Document {
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
); )
} }
} }
export default MyDocument; export default MyDocument

View File

@ -1,25 +1,21 @@
import { MDXLayoutRenderer } from '@/components/MDXComponents'; import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { getFileBySlug } from '@/lib/mdx'; import { getFileBySlug } from '@/lib/mdx'
import { GetStaticProps, InferGetStaticPropsType } from 'next'; import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'; import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
const DEFAULT_LAYOUT = 'AuthorLayout'; const DEFAULT_LAYOUT = 'AuthorLayout'
// @ts-ignore // @ts-ignore
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }; authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }
}> = async () => { }> = async () => {
const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', [ const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', ['default'])
'default', const { mdxSource, frontMatter } = authorDetails
]); return { props: { authorDetails: { mdxSource, frontMatter } } }
const { mdxSource, frontMatter } = authorDetails; }
return { props: { authorDetails: { mdxSource, frontMatter } } };
};
export default function About({ export default function About({ authorDetails }: InferGetStaticPropsType<typeof getStaticProps>) {
authorDetails, const { mdxSource, frontMatter } = authorDetails
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, frontMatter } = authorDetails;
return ( return (
<MDXLayoutRenderer <MDXLayoutRenderer
@ -27,5 +23,5 @@ export default function About({
mdxSource={mdxSource} mdxSource={mdxSource}
frontMatter={frontMatter} frontMatter={frontMatter}
/> />
); )
} }

View File

@ -1,15 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next'
// eslint-disable-next-line import/no-anonymous-default-export // eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body; const { email } = req.body
if (!email) { if (!email) {
return res.status(400).json({ error: 'Email is required' }); return res.status(400).json({ error: 'Email is required' })
} }
try { try {
const API_KEY = process.env.BUTTONDOWN_API_KEY; const API_KEY = process.env.BUTTONDOWN_API_KEY
const buttondownRoute = `${process.env.BUTTONDOWN_API_URL}subscribers`; const buttondownRoute = `${process.env.BUTTONDOWN_API_URL}subscribers`
const response = await fetch(buttondownRoute, { const response = await fetch(buttondownRoute, {
body: JSON.stringify({ body: JSON.stringify({
email, email,
@ -19,16 +19,14 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
method: 'POST', method: 'POST',
}); })
if (response.status >= 400) { if (response.status >= 400) {
return res return res.status(500).json({ error: `There was an error subscribing to the list.` })
.status(500)
.json({ error: `There was an error subscribing to the list.` });
} }
return res.status(201).json({ error: '' }); return res.status(201).json({ error: '' })
} catch (error) { } catch (error) {
return res.status(500).json({ error: error.message || error.toString() }); return res.status(500).json({ error: error.message || error.toString() })
} }
}; }

View File

@ -1,20 +1,20 @@
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next'
/* eslint-disable import/no-anonymous-default-export */ /* eslint-disable import/no-anonymous-default-export */
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body; const { email } = req.body
if (!email) { if (!email) {
return res.status(400).json({ error: 'Email is required' }); return res.status(400).json({ error: 'Email is required' })
} }
try { try {
const FORM_ID = process.env.CONVERTKIT_FORM_ID; const FORM_ID = process.env.CONVERTKIT_FORM_ID
const API_KEY = process.env.CONVERTKIT_API_KEY; const API_KEY = process.env.CONVERTKIT_API_KEY
const API_URL = process.env.CONVERTKIT_API_URL; const API_URL = process.env.CONVERTKIT_API_URL
// Send request to ConvertKit // Send request to ConvertKit
const data = { email, api_key: API_KEY }; const data = { email, api_key: API_KEY }
const response = await fetch(`${API_URL}forms/${FORM_ID}/subscribe`, { const response = await fetch(`${API_URL}forms/${FORM_ID}/subscribe`, {
body: JSON.stringify(data), body: JSON.stringify(data),
@ -22,16 +22,16 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
method: 'POST', method: 'POST',
}); })
if (response.status >= 400) { if (response.status >= 400) {
return res.status(400).json({ return res.status(400).json({
error: `There was an error subscribing to the list.`, error: `There was an error subscribing to the list.`,
}); })
} }
return res.status(201).json({ error: '' }); return res.status(201).json({ error: '' })
} catch (error) { } catch (error) {
return res.status(500).json({ error: error.message || error.toString() }); return res.status(500).json({ error: error.message || error.toString() })
} }
}; }

View File

@ -1,15 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next'
/* eslint-disable import/no-anonymous-default-export */ /* eslint-disable import/no-anonymous-default-export */
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body; const { email } = req.body
if (!email) { if (!email) {
return res.status(400).json({ error: 'Email is required' }); return res.status(400).json({ error: 'Email is required' })
} }
try { try {
const API_KEY = process.env.KLAVIYO_API_KEY; const API_KEY = process.env.KLAVIYO_API_KEY
const LIST_ID = process.env.KLAVIYO_LIST_ID; const LIST_ID = process.env.KLAVIYO_LIST_ID
const response = await fetch( const response = await fetch(
`https://a.klaviyo.com/api/v2/list/${LIST_ID}/subscribe?api_key=${API_KEY}`, `https://a.klaviyo.com/api/v2/list/${LIST_ID}/subscribe?api_key=${API_KEY}`,
{ {
@ -24,14 +24,14 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
profiles: [{ email: email }], profiles: [{ email: email }],
}), }),
} }
); )
if (response.status >= 400) { if (response.status >= 400) {
return res.status(400).json({ return res.status(400).json({
error: `There was an error subscribing to the list.`, error: `There was an error subscribing to the list.`,
}); })
} }
return res.status(201).json({ error: '' }); return res.status(201).json({ error: '' })
} catch (error) { } catch (error) {
return res.status(500).json({ error: error.message || error.toString() }); return res.status(500).json({ error: error.message || error.toString() })
} }
}; }

View File

@ -1,26 +1,26 @@
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next'
import mailchimp from '@mailchimp/mailchimp_marketing'; import mailchimp from '@mailchimp/mailchimp_marketing'
mailchimp.setConfig({ mailchimp.setConfig({
apiKey: process.env.MAILCHIMP_API_KEY, apiKey: process.env.MAILCHIMP_API_KEY,
server: process.env.MAILCHIMP_API_SERVER, // E.g. us1 server: process.env.MAILCHIMP_API_SERVER, // E.g. us1
}); })
// eslint-disable-next-line import/no-anonymous-default-export // eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body; const { email } = req.body
if (!email) { if (!email) {
return res.status(400).json({ error: 'Email is required' }); return res.status(400).json({ error: 'Email is required' })
} }
try { try {
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, { await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
email_address: email, email_address: email,
status: 'subscribed', status: 'subscribed',
}); })
return res.status(201).json({ error: '' }); return res.status(201).json({ error: '' })
} catch (error) { } catch (error) {
return res.status(500).json({ error: error.message || error.toString() }); return res.status(500).json({ error: error.message || error.toString() })
} }
}; }

View File

@ -1,26 +1,26 @@
import { getAllFilesFrontMatter } from '@/lib/mdx'; import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'; import ListLayout from '@/layouts/ListLayout'
import { PageSEO } from '@/components/SEO'; import { PageSEO } from '@/components/SEO'
import { GetStaticProps, InferGetStaticPropsType } from 'next'; import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { ComponentProps } from 'react'; import { ComponentProps } from 'react'
export const POSTS_PER_PAGE = 5; export const POSTS_PER_PAGE = 5
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{
posts: ComponentProps<typeof ListLayout>['posts']; posts: ComponentProps<typeof ListLayout>['posts']
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']; initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
pagination: ComponentProps<typeof ListLayout>['pagination']; pagination: ComponentProps<typeof ListLayout>['pagination']
}> = async () => { }> = async () => {
const posts = await getAllFilesFrontMatter('blog'); const posts = await getAllFilesFrontMatter('blog')
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE); const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
const pagination = { const pagination = {
currentPage: 1, currentPage: 1,
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE), totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
}; }
return { props: { initialDisplayPosts, posts, pagination } }; return { props: { initialDisplayPosts, posts, pagination } }
}; }
export default function Blog({ export default function Blog({
posts, posts,
@ -29,10 +29,7 @@ export default function Blog({
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<> <>
<PageSEO <PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
title={`Blog - ${siteMetadata.author}`}
description={siteMetadata.description}
/>
<ListLayout <ListLayout
posts={posts} posts={posts}
initialDisplayPosts={initialDisplayPosts} initialDisplayPosts={initialDisplayPosts}
@ -40,5 +37,5 @@ export default function Blog({
title="All Posts" title="All Posts"
/> />
</> </>
); )
} }

View File

@ -1,22 +1,17 @@
import fs from 'fs'; import fs from 'fs'
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle'
import generateRss from '@/lib/generate-rss'; import generateRss from '@/lib/generate-rss'
import { MDXLayoutRenderer } from '@/components/MDXComponents'; import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
formatSlug, import { GetStaticProps, InferGetStaticPropsType } from 'next'
getAllFilesFrontMatter, import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
getFileBySlug, import { PostFrontMatter } from 'types/PostFrontMatter'
getFiles, import { Toc } from 'types/Toc'
} from '@/lib/mdx';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
import { PostFrontMatter } from 'types/PostFrontMatter';
import { Toc } from 'types/Toc';
const DEFAULT_LAYOUT = 'PostLayout'; const DEFAULT_LAYOUT = 'PostLayout'
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = getFiles('blog'); const posts = getFiles('blog')
return { return {
paths: posts.map((p) => ({ paths: posts.map((p) => ({
params: { params: {
@ -24,38 +19,34 @@ export async function getStaticPaths() {
}, },
})), })),
fallback: false, fallback: false,
}; }
} }
// @ts-ignore // @ts-ignore
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }; post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }
authorDetails: AuthorFrontMatter[]; authorDetails: AuthorFrontMatter[]
prev?: { slug: string; title: string }; prev?: { slug: string; title: string }
next?: { slug: string; title: string }; next?: { slug: string; title: string }
}> = async ({ params }) => { }> = async ({ params }) => {
const slug = (params.slug as string[]).join('/'); const slug = (params.slug as string[]).join('/')
const allPosts = await getAllFilesFrontMatter('blog'); const allPosts = await getAllFilesFrontMatter('blog')
const postIndex = allPosts.findIndex( const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === slug)
(post) => formatSlug(post.slug) === slug const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
); const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null; const post = await getFileBySlug<PostFrontMatter>('blog', slug)
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null;
const post = await getFileBySlug<PostFrontMatter>('blog', slug);
// @ts-ignore // @ts-ignore
const authorList = post.frontMatter.authors || ['default']; const authorList = post.frontMatter.authors || ['default']
const authorPromise = authorList.map(async (author) => { const authorPromise = authorList.map(async (author) => {
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [ const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
author, return authorResults.frontMatter
]); })
return authorResults.frontMatter; const authorDetails = await Promise.all(authorPromise)
});
const authorDetails = await Promise.all(authorPromise);
// rss // rss
if (allPosts.length > 0) { if (allPosts.length > 0) {
const rss = generateRss(allPosts); const rss = generateRss(allPosts)
fs.writeFileSync('./public/feed.xml', rss); fs.writeFileSync('./public/feed.xml', rss)
} }
return { return {
@ -65,8 +56,8 @@ export const getStaticProps: GetStaticProps<{
prev, prev,
next, next,
}, },
}; }
}; }
export default function Blog({ export default function Blog({
post, post,
@ -74,7 +65,7 @@ export default function Blog({
prev, prev,
next, next,
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, toc, frontMatter } = post; const { mdxSource, toc, frontMatter } = post
return ( return (
<> <>
@ -99,5 +90,5 @@ export default function Blog({
</div> </div>
)} )}
</> </>
); )
} }

View File

@ -1,42 +1,42 @@
import { PageSEO } from '@/components/SEO'; import { PageSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'; import { getAllFilesFrontMatter } from '@/lib/mdx'
import ListLayout from '@/layouts/ListLayout'; import ListLayout from '@/layouts/ListLayout'
import { POSTS_PER_PAGE } from '../../blog'; import { POSTS_PER_PAGE } from '../../blog'
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'; import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => { export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
const totalPosts = await getAllFilesFrontMatter('blog'); const totalPosts = await getAllFilesFrontMatter('blog')
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE); const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
const paths = Array.from({ length: totalPages }, (_, i) => ({ const paths = Array.from({ length: totalPages }, (_, i) => ({
params: { page: (i + 1).toString() }, params: { page: (i + 1).toString() },
})); }))
return { return {
paths, paths,
fallback: false, fallback: false,
}; }
}; }
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{
posts: PostFrontMatter[]; posts: PostFrontMatter[]
initialDisplayPosts: PostFrontMatter[]; initialDisplayPosts: PostFrontMatter[]
pagination: { currentPage: number; totalPages: number }; pagination: { currentPage: number; totalPages: number }
}> = async (context) => { }> = async (context) => {
const { const {
params: { page }, params: { page },
} = context; } = context
const posts = await getAllFilesFrontMatter('blog'); const posts = await getAllFilesFrontMatter('blog')
const pageNumber = parseInt(page as string); const pageNumber = parseInt(page as string)
const initialDisplayPosts = posts.slice( const initialDisplayPosts = posts.slice(
POSTS_PER_PAGE * (pageNumber - 1), POSTS_PER_PAGE * (pageNumber - 1),
POSTS_PER_PAGE * pageNumber POSTS_PER_PAGE * pageNumber
); )
const pagination = { const pagination = {
currentPage: pageNumber, currentPage: pageNumber,
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE), totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
}; }
return { return {
props: { props: {
@ -44,8 +44,8 @@ export const getStaticProps: GetStaticProps<{
initialDisplayPosts, initialDisplayPosts,
pagination, pagination,
}, },
}; }
}; }
export default function PostPage({ export default function PostPage({
posts, posts,
@ -54,10 +54,7 @@ export default function PostPage({
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<> <>
<PageSEO <PageSEO title={siteMetadata.title} description={siteMetadata.description} />
title={siteMetadata.title}
description={siteMetadata.description}
/>
<ListLayout <ListLayout
posts={posts} posts={posts}
initialDisplayPosts={initialDisplayPosts} initialDisplayPosts={initialDisplayPosts}
@ -65,5 +62,5 @@ export default function PostPage({
title="All Posts" title="All Posts"
/> />
</> </>
); )
} }

View File

@ -1,32 +1,25 @@
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 { getAllFilesFrontMatter } from '@/lib/mdx'
import formatDate from '@/lib/utils/formatDate'; import formatDate from '@/lib/utils/formatDate'
import { GetStaticProps, InferGetStaticPropsType } from 'next'; import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
import NewsletterForm from '@/components/NewsletterForm'; import NewsletterForm from '@/components/NewsletterForm'
const MAX_DISPLAY = 5; const MAX_DISPLAY = 5
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
posts: PostFrontMatter[]; const posts = await getAllFilesFrontMatter('blog')
}> = async () => {
const posts = await getAllFilesFrontMatter('blog');
return { props: { posts } }; return { props: { posts } }
}; }
export default function Home({ export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
posts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<> <>
<PageSEO <PageSEO title={siteMetadata.title} description={siteMetadata.description} />
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 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"> <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">
@ -39,7 +32,7 @@ export default function Home({
<ul className="divide-y divide-gray-200 dark:divide-gray-700"> <ul className="divide-y divide-gray-200 dark:divide-gray-700">
{!posts.length && '没有找到文章。 😭'} {!posts.length && '没有找到文章。 😭'}
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => { {posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
const { slug, date, title, summary, tags } = frontMatter; const { slug, date, title, summary, tags } = frontMatter
return ( return (
<li key={slug} className="py-12"> <li key={slug} className="py-12">
<article> <article>
@ -56,7 +49,8 @@ export default function Home({
<h2 className="text-2xl font-bold leading-8 tracking-tight"> <h2 className="text-2xl font-bold leading-8 tracking-tight">
<Link <Link
href={`/blog/${slug}`} href={`/blog/${slug}`}
className="text-gray-900 dark:text-gray-100"> className="text-gray-900 dark:text-gray-100"
>
{title} {title}
</Link> </Link>
</h2> </h2>
@ -74,7 +68,8 @@ export default function Home({
<Link <Link
href={`/blog/${slug}`} href={`/blog/${slug}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label={`Read "${title}"`}> aria-label={`Read "${title}"`}
>
Read more &rarr; Read more &rarr;
</Link> </Link>
</div> </div>
@ -82,7 +77,7 @@ export default function Home({
</div> </div>
</article> </article>
</li> </li>
); )
})} })}
</ul> </ul>
</div> </div>
@ -91,7 +86,8 @@ export default function Home({
<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 &rarr; All Posts &rarr;
</Link> </Link>
</div> </div>
@ -102,5 +98,5 @@ export default function Home({
</div> </div>
)} )}
</> </>
); )
} }

View File

@ -1,15 +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'; import { PageSEO } from '@/components/SEO'
export default function Projects() { export default function Projects() {
return ( return (
<> <>
<PageSEO <PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
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 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"> <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">
@ -34,5 +31,5 @@ export default function Projects() {
</div> </div>
</div> </div>
</> </>
); )
} }

View File

@ -1,29 +1,22 @@
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 { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'; import kebabCase from '@/lib/utils/kebabCase'
import { GetStaticProps, InferGetStaticPropsType } from 'next'; import { GetStaticProps, InferGetStaticPropsType } from 'next'
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
tags: Record<string, number>; const tags = await getAllTags('blog')
}> = async () => {
const tags = await getAllTags('blog');
return { props: { tags } }; return { props: { tags } }
}; }
export default function Tags({ export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
tags, const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
}: InferGetStaticPropsType<typeof getStaticProps>) {
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a]);
return ( return (
<> <>
<PageSEO <PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
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 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: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">
@ -38,14 +31,15 @@ export default function Tags({
<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"
>
{` (${tags[t]})`} {` (${tags[t]})`}
</Link> </Link>
</div> </div>
); )
})} })}
</div> </div>
</div> </div>
</> </>
); )
} }

View File

@ -1,19 +1,19 @@
import { TagSEO } from '@/components/SEO'; import { TagSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'; import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'; import ListLayout from '@/layouts/ListLayout'
import generateRss from '@/lib/generate-rss'; import generateRss from '@/lib/generate-rss'
import { getAllFilesFrontMatter } from '@/lib/mdx'; import { getAllFilesFrontMatter } from '@/lib/mdx'
import { getAllTags } from '@/lib/tags'; import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'; import kebabCase from '@/lib/utils/kebabCase'
import fs from 'fs'; import fs from 'fs'
import { GetStaticProps, InferGetStaticPropsType } from 'next'; import { GetStaticProps, InferGetStaticPropsType } from 'next'
import path from 'path'; import path from 'path'
import { PostFrontMatter } from 'types/PostFrontMatter'; import { PostFrontMatter } from 'types/PostFrontMatter'
const root = process.cwd(); const root = process.cwd()
export async function getStaticPaths() { export async function getStaticPaths() {
const tags = await getAllTags('blog'); const tags = await getAllTags('blog')
return { return {
paths: Object.keys(tags).map((tag) => ({ paths: Object.keys(tags).map((tag) => ({
@ -22,37 +22,32 @@ export async function getStaticPaths() {
}, },
})), })),
fallback: false, fallback: false,
}; }
} }
export const getStaticProps: GetStaticProps<{ export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
posts: PostFrontMatter[]; context
tag: string; ) => {
}> = async (context) => { const tag = context.params.tag as string
const tag = context.params.tag as string; const allPosts = await getAllFilesFrontMatter('blog')
const allPosts = await getAllFilesFrontMatter('blog');
const filteredPosts = allPosts.filter( const filteredPosts = allPosts.filter(
(post) => (post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag) )
);
// rss // rss
if (filteredPosts.length > 0) { if (filteredPosts.length > 0) {
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`); const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
const rssPath = path.join(root, 'public', 'tags', tag); const rssPath = path.join(root, 'public', 'tags', tag)
fs.mkdirSync(rssPath, { recursive: true }); fs.mkdirSync(rssPath, { recursive: true })
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss); fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
} }
return { props: { posts: filteredPosts, tag } }; return { props: { posts: filteredPosts, tag } }
}; }
export default function Tag({ export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
posts,
tag,
}: InferGetStaticPropsType<typeof getStaticProps>) {
// Capitalize first letter and convert space to dash // Capitalize first letter and convert space to dash
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1); const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
return ( return (
<> <>
<TagSEO <TagSEO
@ -61,5 +56,5 @@ export default function Tag({
/> />
<ListLayout posts={posts} title={title} /> <ListLayout posts={posts} title={title} />
</> </>
); )
} }

View File

@ -1,39 +1,36 @@
const fs = require('fs'); const fs = require('fs')
const path = require('path'); const path = require('path')
const inquirer = require('inquirer'); const inquirer = require('inquirer')
const dedent = require('dedent'); const dedent = require('dedent')
const root = process.cwd(); const root = process.cwd()
const getAuthors = () => { const getAuthors = () => {
const authorPath = path.join(root, 'data', 'authors'); const authorPath = path.join(root, 'data', 'authors')
const authorList = fs const authorList = fs.readdirSync(authorPath).map((filename) => path.parse(filename).name)
.readdirSync(authorPath) return authorList
.map((filename) => path.parse(filename).name); }
return authorList;
};
const getLayouts = () => { const getLayouts = () => {
const layoutPath = path.join(root, 'layouts'); const layoutPath = path.join(root, 'layouts')
const layoutList = fs const layoutList = fs
.readdirSync(layoutPath) .readdirSync(layoutPath)
.map((filename) => path.parse(filename).name) .map((filename) => path.parse(filename).name)
.filter((file) => file.toLowerCase().includes('post')); .filter((file) => file.toLowerCase().includes('post'))
return layoutList; return layoutList
}; }
const genFrontMatter = (answers) => { const genFrontMatter = (answers) => {
let d = new Date(); let d = new Date()
const date = [ const date = [
d.getFullYear(), d.getFullYear(),
('0' + (d.getMonth() + 1)).slice(-2), ('0' + (d.getMonth() + 1)).slice(-2),
('0' + d.getDate()).slice(-2), ('0' + d.getDate()).slice(-2),
].join('-'); ].join('-')
const tagArray = answers.tags.split(','); const tagArray = answers.tags.split(',')
tagArray.forEach((tag, index) => (tagArray[index] = tag.trim())); tagArray.forEach((tag, index) => (tagArray[index] = tag.trim()))
const tags = "'" + tagArray.join("','") + "'"; const tags = "'" + tagArray.join("','") + "'"
const authorArray = const authorArray = answers.authors.length > 0 ? "'" + answers.authors.join("','") + "'" : ''
answers.authors.length > 0 ? "'" + answers.authors.join("','") + "'" : '';
let frontMatter = dedent`--- let frontMatter = dedent`---
title: ${answers.title ? answers.title : 'Untitled'} title: ${answers.title ? answers.title : 'Untitled'}
@ -44,16 +41,16 @@ const genFrontMatter = (answers) => {
images: [] images: []
layout: ${answers.layout} layout: ${answers.layout}
canonicalUrl: ${answers.canonicalUrl} canonicalUrl: ${answers.canonicalUrl}
`; `
if (answers.authors.length > 0) { if (answers.authors.length > 0) {
frontMatter = frontMatter + '\n' + `authors: [${authorArray}]`; frontMatter = frontMatter + '\n' + `authors: [${authorArray}]`
} }
frontMatter = frontMatter + '\n---'; frontMatter = frontMatter + '\n---'
return frontMatter; return frontMatter
}; }
inquirer inquirer
.prompt([ .prompt([
@ -108,25 +105,24 @@ inquirer
.toLowerCase() .toLowerCase()
.replace(/[^a-zA-Z0-9 ]/g, '') .replace(/[^a-zA-Z0-9 ]/g, '')
.replace(/ /g, '-') .replace(/ /g, '-')
.replace(/-+/g, '-'); .replace(/-+/g, '-')
const frontMatter = genFrontMatter(answers); const frontMatter = genFrontMatter(answers)
if (!fs.existsSync('data/blog')) if (!fs.existsSync('data/blog')) fs.mkdirSync('data/blog', { recursive: true })
fs.mkdirSync('data/blog', { recursive: true });
const filePath = `data/blog/${fileName ? fileName : 'untitled'}.${ const filePath = `data/blog/${fileName ? fileName : 'untitled'}.${
answers.extension ? answers.extension : 'md' answers.extension ? answers.extension : 'md'
}`; }`
fs.writeFile(filePath, frontMatter, { flag: 'wx' }, (err) => { fs.writeFile(filePath, frontMatter, { flag: 'wx' }, (err) => {
if (err) { if (err) {
throw err; throw err
} else { } else {
console.log(`Blog post generated successfully at ${filePath}`); console.log(`Blog post generated successfully at ${filePath}`)
} }
}); })
}) })
.catch((error) => { .catch((error) => {
if (error.isTtyError) { if (error.isTtyError) {
console.log("Prompt couldn't be rendered in the current environment"); console.log("Prompt couldn't be rendered in the current environment")
} else { } else {
console.log('Something went wrong, sorry!'); console.log('Something went wrong, sorry!')
} }
}); })

View File

@ -1,11 +1,11 @@
const fs = require('fs'); const fs = require('fs')
const globby = require('globby'); const globby = require('globby')
const matter = require('gray-matter'); const matter = require('gray-matter')
const prettier = require('prettier'); const prettier = require('prettier')
const siteMetadata = require('../data/siteMetadata'); const siteMetadata = require('../data/siteMetadata')
(async () => { ;(async () => {
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js'); const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
const pages = await globby([ const pages = await globby([
'pages/*.js', 'pages/*.js',
'pages/*.tsx', 'pages/*.tsx',
@ -15,7 +15,7 @@ const siteMetadata = require('../data/siteMetadata');
'!pages/_*.js', '!pages/_*.js',
'!pages/_*.tsx', '!pages/_*.tsx',
'!pages/api', '!pages/api',
]); ])
const sitemap = ` const sitemap = `
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
@ -24,13 +24,13 @@ const siteMetadata = require('../data/siteMetadata');
.map((page) => { .map((page) => {
// Exclude drafts from the sitemap // Exclude drafts from the sitemap
if (page.search('.md') >= 1 && fs.existsSync(page)) { if (page.search('.md') >= 1 && fs.existsSync(page)) {
const source = fs.readFileSync(page, 'utf8'); const source = fs.readFileSync(page, 'utf8')
const fm = matter(source); const fm = matter(source)
if (fm.data.draft) { if (fm.data.draft) {
return; return
} }
if (fm.data.canonicalUrl) { if (fm.data.canonicalUrl) {
return; return
} }
} }
const path = page const path = page
@ -41,29 +41,26 @@ const siteMetadata = require('../data/siteMetadata');
.replace('.tsx', '') .replace('.tsx', '')
.replace('.mdx', '') .replace('.mdx', '')
.replace('.md', '') .replace('.md', '')
.replace('/feed.xml', ''); .replace('/feed.xml', '')
const route = path === '/index' ? '' : path; const route = path === '/index' ? '' : path
if ( if (page.search('pages/404.') > -1 || page.search(`pages/blog/[...slug].`) > -1) {
page.search('pages/404.') > -1 || return
page.search(`pages/blog/[...slug].`) > -1
) {
return;
} }
return ` return `
<url> <url>
<loc>${siteMetadata.siteUrl}${route}</loc> <loc>${siteMetadata.siteUrl}${route}</loc>
</url> </url>
`; `
}) })
.join('')} .join('')}
</urlset> </urlset>
`; `
const formatted = prettier.format(sitemap, { const formatted = prettier.format(sitemap, {
...prettierConfig, ...prettierConfig,
parser: 'html', parser: 'html',
}); })
// eslint-disable-next-line no-sync // eslint-disable-next-line no-sync
fs.writeFileSync('public/sitemap.xml', formatted); fs.writeFileSync('public/sitemap.xml', formatted)
})(); })()

View File

@ -5,130 +5,116 @@
// The app listens to the event and triggers a client-side router refresh // The app listens to the event and triggers a client-side router refresh
// see components/ClientReload.js // see components/ClientReload.js
const chalk = require('chalk'); const chalk = require('chalk')
const chokidar = require('chokidar'); const chokidar = require('chokidar')
const program = require('commander'); const program = require('commander')
const http = require('http'); const http = require('http')
const SocketIO = require('socket.io'); const SocketIO = require('socket.io')
const express = require('express'); const express = require('express')
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn
const next = require('next'); const next = require('next')
const path = require('path'); const path = require('path')
const { parse } = require('url'); const { parse } = require('url')
const pkg = require('../package.json'); const pkg = require('../package.json')
const defaultWatchEvent = 'change'; const defaultWatchEvent = 'change'
program.storeOptionsAsProperties().version(pkg.version); program.storeOptionsAsProperties().version(pkg.version)
program program
.option('-r, --root [dir]', 'root directory of your nextjs app') .option('-r, --root [dir]', 'root directory of your nextjs app')
.option( .option('-s, --script [path]', 'path to the script you want to trigger on a watcher event', false)
'-s, --script [path]',
'path to the script you want to trigger on a watcher event',
false
)
.option('-c, --command [cmd]', 'command to execute on a watcher event', false) .option('-c, --command [cmd]', 'command to execute on a watcher event', false)
.option( .option(
'-e, --event [name]', '-e, --event [name]',
`name of event to watch, defaults to ${defaultWatchEvent}`, `name of event to watch, defaults to ${defaultWatchEvent}`,
defaultWatchEvent defaultWatchEvent
) )
.option( .option('-p, --polling [name]', `use polling for the watcher, defaults to false`, false)
'-p, --polling [name]', .parse(process.argv)
`use polling for the watcher, defaults to false`,
false
)
.parse(process.argv);
const shell = process.env.SHELL; const shell = process.env.SHELL
const app = next({ dev: true, dir: program.root || process.cwd() }); const app = next({ dev: true, dir: program.root || process.cwd() })
const port = parseInt(process.env.PORT, 10) || 3000; const port = parseInt(process.env.PORT, 10) || 3000
const handle = app.getRequestHandler(); const handle = app.getRequestHandler()
app.prepare().then(() => { app.prepare().then(() => {
// if directories are provided, watch them for changes and trigger reload // if directories are provided, watch them for changes and trigger reload
if (program.args.length > 0) { if (program.args.length > 0) {
chokidar chokidar
.watch(program.args, { usePolling: Boolean(program.polling) }) .watch(program.args, { usePolling: Boolean(program.polling) })
.on( .on(program.event, async (filePathContext, eventContext = defaultWatchEvent) => {
program.event, // Emit changes via socketio
async (filePathContext, eventContext = defaultWatchEvent) => { io.sockets.emit('reload', filePathContext)
// Emit changes via socketio app.server.hotReloader.send('building')
io.sockets.emit('reload', filePathContext);
app.server.hotReloader.send('building');
if (program.command) { if (program.command) {
// Use spawn here so that we can pipe stdio from the command without buffering // Use spawn here so that we can pipe stdio from the command without buffering
spawn( spawn(
shell, shell,
[ [
'-c', '-c',
program.command program.command
.replace(/\{event\}/gi, filePathContext) .replace(/\{event\}/gi, filePathContext)
.replace(/\{path\}/gi, eventContext), .replace(/\{path\}/gi, eventContext),
], ],
{ {
stdio: 'inherit', stdio: 'inherit',
}
);
}
if (program.script) {
try {
// find the path of your --script script
const scriptPath = path.join(
process.cwd(),
program.script.toString()
);
// require your --script script
const executeFile = require(scriptPath);
// run the exported function from your --script script
executeFile(filePathContext, eventContext);
} catch (e) {
console.error('Remote script failed');
console.error(e);
return e;
} }
} )
app.server.hotReloader.send('reloadPage');
} }
);
if (program.script) {
try {
// find the path of your --script script
const scriptPath = path.join(process.cwd(), program.script.toString())
// require your --script script
const executeFile = require(scriptPath)
// run the exported function from your --script script
executeFile(filePathContext, eventContext)
} catch (e) {
console.error('Remote script failed')
console.error(e)
return e
}
}
app.server.hotReloader.send('reloadPage')
})
} }
// create an express server // create an express server
const expressApp = express(); const expressApp = express()
const server = http.createServer(expressApp); const server = http.createServer(expressApp)
// watch files with socketIO // watch files with socketIO
const io = SocketIO(server); const io = SocketIO(server)
// special handling for mdx reload route // special handling for mdx reload route
const reloadRoute = express.Router(); const reloadRoute = express.Router()
reloadRoute.use(express.json()); reloadRoute.use(express.json())
reloadRoute.all('/', (req, res) => { reloadRoute.all('/', (req, res) => {
// log message if present // log message if present
const msg = req.body.message; const msg = req.body.message
const color = req.body.color; const color = req.body.color
msg && console.log(color ? chalk[color](msg) : msg); msg && console.log(color ? chalk[color](msg) : msg)
// reload the nextjs app // reload the nextjs app
app.server.hotReloader.send('building'); app.server.hotReloader.send('building')
app.server.hotReloader.send('reloadPage'); app.server.hotReloader.send('reloadPage')
res.end('Reload initiated'); res.end('Reload initiated')
}); })
expressApp.use('/__next_reload', reloadRoute); expressApp.use('/__next_reload', reloadRoute)
// handle all other routes with next.js // handle all other routes with next.js
expressApp.all('*', (req, res) => handle(req, res, parse(req.url, true))); expressApp.all('*', (req, res) => handle(req, res, parse(req.url, true)))
// fire it up // fire it up
server.listen(port, (err) => { server.listen(port, (err) => {
if (err) throw err; if (err) throw err
console.log(`> Ready on http://localhost:${port}`); console.log(`> Ready on http://localhost:${port}`)
}); })
}); })