style: auto fix.
This commit is contained in:
parent
10f64a9ba4
commit
c081d55a32
@ -36,3 +36,5 @@ yarn-error.log*
|
||||
.env.production.local
|
||||
|
||||
secrets.txt
|
||||
|
||||
.pnpm-store
|
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
# 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
2
.gitignore
vendored
@ -36,3 +36,5 @@ yarn-error.log*
|
||||
.env.production.local
|
||||
|
||||
secrets.txt
|
||||
|
||||
.pnpm-store
|
@ -1,2 +1,6 @@
|
||||
{
|
||||
}
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
trailingCommas: 'all',
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: true,
|
||||
};
|
||||
|
@ -1,13 +1,12 @@
|
||||
import Image from './Image'
|
||||
import Link from './Link'
|
||||
import Image from './Image';
|
||||
import Link from './Link';
|
||||
|
||||
const Card = ({ title, description, imgSrc, href }) => (
|
||||
<div className="md p-4 md:w-1/2" style={{ maxWidth: '544px' }}>
|
||||
<div
|
||||
className={`${
|
||||
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 &&
|
||||
(href ? (
|
||||
<Link href={href} aria-label={`Link to ${title}`}>
|
||||
@ -38,19 +37,20 @@ const Card = ({ title, description, imgSrc, href }) => (
|
||||
title
|
||||
)}
|
||||
</h2>
|
||||
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">{description}</p>
|
||||
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
{href && (
|
||||
<Link
|
||||
href={href}
|
||||
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 →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Card
|
||||
export default Card;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import Router from 'next/router'
|
||||
import { useEffect } from 'react';
|
||||
import Router from 'next/router';
|
||||
|
||||
/**
|
||||
* Client-side complement to next-remote-watch
|
||||
@ -10,14 +10,14 @@ export const ClientReload = () => {
|
||||
// Exclude socket.io from prod bundle
|
||||
useEffect(() => {
|
||||
import('socket.io-client').then((module) => {
|
||||
const socket = module.io()
|
||||
const socket = module.io();
|
||||
socket.on('reload', () => {
|
||||
Router.replace(Router.asPath, undefined, {
|
||||
scroll: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
});
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -1,13 +1,17 @@
|
||||
import Link from './Link'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import SocialIcon from '@/components/social-icons'
|
||||
import Link from './Link';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import SocialIcon from '@/components/social-icons';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer>
|
||||
<div className="mt-16 flex flex-col items-center">
|
||||
<div className="mb-3 flex space-x-4">
|
||||
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
|
||||
<SocialIcon
|
||||
kind="mail"
|
||||
href={`mailto:${siteMetadata.email}`}
|
||||
size={6}
|
||||
/>
|
||||
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
|
||||
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
|
||||
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
|
||||
@ -22,11 +26,13 @@ export default function Footer() {
|
||||
<Link href="/">{siteMetadata.title}</Link>
|
||||
</div>
|
||||
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
<Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog" rel="nofollow">
|
||||
<Link
|
||||
href="https://github.com/timlrx/tailwind-nextjs-starter-blog"
|
||||
rel="nofollow">
|
||||
Tailwind Nextjs Theme
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
import NextImage, { ImageProps } from 'next/image';
|
||||
|
||||
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
|
||||
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />;
|
||||
|
||||
export default Image
|
||||
export default Image;
|
||||
|
@ -1,15 +1,15 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import headerNavLinks from '@/data/headerNavLinks'
|
||||
import Logo from '@/data/logo.svg'
|
||||
import Link from './Link'
|
||||
import SectionContainer from './SectionContainer'
|
||||
import Footer from './Footer'
|
||||
import MobileNav from './MobileNav'
|
||||
import ThemeSwitch from './ThemeSwitch'
|
||||
import { ReactNode } from 'react'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import headerNavLinks from '@/data/headerNavLinks';
|
||||
import Logo from '@/data/logo.svg';
|
||||
import Link from './Link';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import Footer from './Footer';
|
||||
import MobileNav from './MobileNav';
|
||||
import ThemeSwitch from './ThemeSwitch';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const LayoutWrapper = ({ children }: Props) => {
|
||||
@ -39,8 +39,7 @@ const LayoutWrapper = ({ children }: Props) => {
|
||||
<Link
|
||||
key={link.title}
|
||||
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>
|
||||
))}
|
||||
@ -53,7 +52,7 @@ const LayoutWrapper = ({ children }: Props) => {
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutWrapper
|
||||
export default LayoutWrapper;
|
||||
|
@ -1,27 +1,30 @@
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import Link from 'next/link'
|
||||
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
|
||||
import Link from 'next/link';
|
||||
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react';
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
}: DetailedHTMLProps<
|
||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
HTMLAnchorElement
|
||||
>) => {
|
||||
const isInternalLink = href && href.startsWith('/');
|
||||
const isAnchorLink = href && href.startsWith('#');
|
||||
|
||||
if (isInternalLink) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a {...rest} />
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a href={href} {...rest} />
|
||||
return <a href={href} {...rest} />;
|
||||
}
|
||||
|
||||
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
|
||||
}
|
||||
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />;
|
||||
};
|
||||
|
||||
export default CustomLink
|
||||
export default CustomLink;
|
||||
|
@ -1,16 +1,19 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useMemo } from 'react'
|
||||
import { ComponentMap, getMDXComponent } from 'mdx-bundler/client'
|
||||
import Image from './Image'
|
||||
import CustomLink from './Link'
|
||||
import TOCInline from './TOCInline'
|
||||
import Pre from './Pre'
|
||||
import { BlogNewsletterForm } from './NewsletterForm'
|
||||
import React, { useMemo } from 'react';
|
||||
import { ComponentMap, getMDXComponent } from 'mdx-bundler/client';
|
||||
import Image from './Image';
|
||||
import CustomLink from './Link';
|
||||
import TOCInline from './TOCInline';
|
||||
import Pre from './Pre';
|
||||
import { BlogNewsletterForm } from './NewsletterForm';
|
||||
|
||||
const Wrapper: React.ComponentType<{ layout: string }> = ({ layout, ...rest }) => {
|
||||
const Layout = require(`../layouts/${layout}`).default
|
||||
return <Layout {...rest} />
|
||||
}
|
||||
const Wrapper: React.ComponentType<{ layout: string }> = ({
|
||||
layout,
|
||||
...rest
|
||||
}) => {
|
||||
const Layout = require(`../layouts/${layout}`).default;
|
||||
return <Layout {...rest} />;
|
||||
};
|
||||
|
||||
export const MDXComponents: ComponentMap = {
|
||||
Image,
|
||||
@ -21,16 +24,16 @@ export const MDXComponents: ComponentMap = {
|
||||
wrapper: Wrapper,
|
||||
//@ts-ignore
|
||||
BlogNewsletterForm,
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
layout: string
|
||||
mdxSource: string
|
||||
[key: string]: unknown
|
||||
layout: string;
|
||||
mdxSource: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
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} />;
|
||||
};
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { useState } from 'react'
|
||||
import Link from './Link'
|
||||
import headerNavLinks from '@/data/headerNavLinks'
|
||||
import { useState } from 'react';
|
||||
import Link from './Link';
|
||||
import headerNavLinks from '@/data/headerNavLinks';
|
||||
|
||||
const MobileNav = () => {
|
||||
const [navShow, setNavShow] = useState(false)
|
||||
const [navShow, setNavShow] = useState(false);
|
||||
|
||||
const onToggleNav = () => {
|
||||
setNavShow((status) => {
|
||||
if (status) {
|
||||
document.body.style.overflow = 'auto'
|
||||
document.body.style.overflow = 'auto';
|
||||
} else {
|
||||
// Prevent scrolling
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
return !status
|
||||
})
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
return !status;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sm:hidden">
|
||||
@ -23,14 +23,12 @@ const MobileNav = () => {
|
||||
type="button"
|
||||
className="ml-1 mr-1 h-8 w-8 rounded py-1"
|
||||
aria-label="Toggle Menu"
|
||||
onClick={onToggleNav}
|
||||
>
|
||||
onClick={onToggleNav}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
className="text-gray-900 dark:text-gray-100">
|
||||
{navShow ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
@ -49,22 +47,19 @@ const MobileNav = () => {
|
||||
<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 ${
|
||||
navShow ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
}`}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="toggle modal"
|
||||
className="fixed h-full w-full cursor-auto focus:outline-none"
|
||||
onClick={onToggleNav}
|
||||
></button>
|
||||
onClick={onToggleNav}></button>
|
||||
<nav className="fixed mt-8 h-full">
|
||||
{headerNavLinks.map((link) => (
|
||||
<div key={link.title} className="px-12 py-4">
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
|
||||
onClick={onToggleNav}
|
||||
>
|
||||
onClick={onToggleNav}>
|
||||
{link.title}
|
||||
</Link>
|
||||
</div>
|
||||
@ -72,7 +67,7 @@ const MobileNav = () => {
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileNav
|
||||
export default MobileNav;
|
||||
|
@ -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 inputEl = useRef<HTMLInputElement>(null)
|
||||
const [error, setError] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [subscribed, setSubscribed] = useState(false)
|
||||
const inputEl = useRef<HTMLInputElement>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
const subscribe = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
|
||||
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
|
||||
body: JSON.stringify({
|
||||
@ -19,24 +19,28 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
});
|
||||
|
||||
const { error } = await res.json()
|
||||
const { error } = await res.json();
|
||||
if (error) {
|
||||
setError(true)
|
||||
setMessage('Your e-mail address is invalid or you are already subscribed!')
|
||||
return
|
||||
setError(true);
|
||||
setMessage(
|
||||
'Your e-mail address is invalid or you are already subscribed!'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
inputEl.current.value = ''
|
||||
setError(false)
|
||||
setSubscribed(true)
|
||||
setMessage('Successfully! 🎉 You are now subscribed.')
|
||||
}
|
||||
inputEl.current.value = '';
|
||||
setError(false);
|
||||
setSubscribed(true);
|
||||
setMessage('Successfully! 🎉 You are now subscribed.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</div>
|
||||
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">
|
||||
{title}
|
||||
</div>
|
||||
<form className="flex flex-col sm:flex-row" onSubmit={subscribe}>
|
||||
<div>
|
||||
<label className="sr-only" htmlFor="email-input">
|
||||
@ -47,7 +51,9 @@ 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"
|
||||
id="email-input"
|
||||
name="email"
|
||||
placeholder={subscribed ? "You're subscribed ! 🎉" : 'Enter your email'}
|
||||
placeholder={
|
||||
subscribed ? "You're subscribed ! 🎉" : 'Enter your email'
|
||||
}
|
||||
ref={inputEl}
|
||||
required
|
||||
type="email"
|
||||
@ -57,23 +63,26 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
||||
<div className="mt-2 flex w-full rounded-md shadow-sm sm:mt-0 sm:ml-3">
|
||||
<button
|
||||
className={`w-full rounded-md bg-primary-500 py-2 px-4 font-medium text-white sm:py-0 ${
|
||||
subscribed ? 'cursor-default' : 'hover:bg-primary-700 dark:hover:bg-primary-400'
|
||||
subscribed
|
||||
? 'cursor-default'
|
||||
: 'hover:bg-primary-700 dark:hover:bg-primary-400'
|
||||
} focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2 dark:ring-offset-black`}
|
||||
type="submit"
|
||||
disabled={subscribed}
|
||||
>
|
||||
disabled={subscribed}>
|
||||
{subscribed ? 'Thank you!' : 'Sign up'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{error && (
|
||||
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">{message}</div>
|
||||
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsletterForm
|
||||
export default NewsletterForm;
|
||||
|
||||
export const BlogNewsletterForm = ({ title }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
@ -81,4 +90,4 @@ export const BlogNewsletterForm = ({ title }) => (
|
||||
<NewsletterForm title={title} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
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">
|
||||
{children}
|
||||
</h1>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,29 @@
|
||||
import Link from '@/components/Link'
|
||||
import Link from '@/components/Link';
|
||||
|
||||
interface Props {
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
export default function Pagination({ totalPages, currentPage }: Props) {
|
||||
const prevPage = currentPage - 1 > 0
|
||||
const nextPage = currentPage + 1 <= totalPages
|
||||
const prevPage = currentPage - 1 > 0;
|
||||
const nextPage = currentPage + 1 <= totalPages;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<nav className="flex justify-between">
|
||||
{!prevPage && (
|
||||
<button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
||||
<button
|
||||
className="cursor-auto disabled:opacity-50"
|
||||
disabled={!prevPage}>
|
||||
Previous
|
||||
</button>
|
||||
)}
|
||||
{prevPage && (
|
||||
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
|
||||
<Link
|
||||
href={
|
||||
currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`
|
||||
}>
|
||||
<button>Previous</button>
|
||||
</Link>
|
||||
)}
|
||||
@ -26,7 +31,9 @@ export default function Pagination({ totalPages, currentPage }: Props) {
|
||||
{currentPage} of {totalPages}
|
||||
</span>
|
||||
{!nextPage && (
|
||||
<button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
||||
<button
|
||||
className="cursor-auto disabled:opacity-50"
|
||||
disabled={!nextPage}>
|
||||
Next
|
||||
</button>
|
||||
)}
|
||||
@ -37,5 +44,5 @@ export default function Pagination({ totalPages, currentPage }: Props) {
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,31 +1,35 @@
|
||||
import { useState, useRef, ReactNode } from 'react'
|
||||
import { useState, useRef, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Pre = ({ children }: Props) => {
|
||||
const textInput = useRef(null)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const textInput = useRef(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const onEnter = () => {
|
||||
setHovered(true)
|
||||
}
|
||||
setHovered(true);
|
||||
};
|
||||
const onExit = () => {
|
||||
setHovered(false)
|
||||
setCopied(false)
|
||||
}
|
||||
setHovered(false);
|
||||
setCopied(false);
|
||||
};
|
||||
const onCopy = () => {
|
||||
setCopied(true)
|
||||
navigator.clipboard.writeText(textInput.current.textContent)
|
||||
setCopied(true);
|
||||
navigator.clipboard.writeText(textInput.current.textContent);
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
}
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
|
||||
<div
|
||||
ref={textInput}
|
||||
onMouseEnter={onEnter}
|
||||
onMouseLeave={onExit}
|
||||
className="relative">
|
||||
{hovered && (
|
||||
<button
|
||||
aria-label="Copy code"
|
||||
@ -35,15 +39,13 @@ const Pre = ({ children }: Props) => {
|
||||
? 'border-green-400 focus:border-green-400 focus:outline-none'
|
||||
: 'border-gray-300'
|
||||
}`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
onClick={onCopy}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
className={copied ? 'text-green-400' : 'text-gray-300'}
|
||||
>
|
||||
className={copied ? 'text-green-400' : 'text-gray-300'}>
|
||||
{copied ? (
|
||||
<>
|
||||
<path
|
||||
@ -69,7 +71,7 @@ const Pre = ({ children }: Props) => {
|
||||
|
||||
<pre>{children}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Pre
|
||||
export default Pre;
|
||||
|
@ -1,21 +1,21 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
interface CommonSEOProps {
|
||||
title: string
|
||||
description: string
|
||||
ogType: string
|
||||
title: string;
|
||||
description: string;
|
||||
ogType: string;
|
||||
ogImage:
|
||||
| string
|
||||
| {
|
||||
'@type': string
|
||||
url: string
|
||||
}[]
|
||||
twImage: string
|
||||
canonicalUrl?: string
|
||||
'@type': string;
|
||||
url: string;
|
||||
}[];
|
||||
twImage: string;
|
||||
canonicalUrl?: string;
|
||||
}
|
||||
|
||||
const CommonSEO = ({
|
||||
@ -26,19 +26,24 @@ const CommonSEO = ({
|
||||
twImage,
|
||||
canonicalUrl,
|
||||
}: CommonSEOProps) => {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="robots" content="follow, index" />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
|
||||
<meta
|
||||
property="og:url"
|
||||
content={`${siteMetadata.siteUrl}${router.asPath}`}
|
||||
/>
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:site_name" content={siteMetadata.title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
{Array.isArray(ogImage) ? (
|
||||
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
|
||||
ogImage.map(({ url }) => (
|
||||
<meta property="og:image" content={url} key={url} />
|
||||
))
|
||||
) : (
|
||||
<meta property="og:image" content={ogImage} key={ogImage} />
|
||||
)}
|
||||
@ -49,20 +54,24 @@ const CommonSEO = ({
|
||||
<meta name="twitter:image" content={twImage} />
|
||||
<link
|
||||
rel="canonical"
|
||||
href={canonicalUrl ? canonicalUrl : `${siteMetadata.siteUrl}${router.asPath}`}
|
||||
href={
|
||||
canonicalUrl
|
||||
? canonicalUrl
|
||||
: `${siteMetadata.siteUrl}${router.asPath}`
|
||||
}
|
||||
/>
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface PageSEOProps {
|
||||
title: string
|
||||
description: string
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const PageSEO = ({ title, description }: PageSEOProps) => {
|
||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
|
||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
|
||||
return (
|
||||
<CommonSEO
|
||||
title={title}
|
||||
@ -71,13 +80,13 @@ export const PageSEO = ({ title, description }: PageSEOProps) => {
|
||||
ogImage={ogImageUrl}
|
||||
twImage={twImageUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const TagSEO = ({ title, description }: PageSEOProps) => {
|
||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||
const router = useRouter()
|
||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
|
||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<CommonSEO
|
||||
@ -96,12 +105,12 @@ export const TagSEO = ({ title, description }: PageSEOProps) => {
|
||||
/>
|
||||
</Head>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface BlogSeoProps extends PostFrontMatter {
|
||||
authorDetails?: AuthorFrontMatter[]
|
||||
url: string
|
||||
authorDetails?: AuthorFrontMatter[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const BlogSEO = ({
|
||||
@ -114,35 +123,35 @@ export const BlogSEO = ({
|
||||
images = [],
|
||||
canonicalUrl,
|
||||
}: BlogSeoProps) => {
|
||||
const publishedAt = new Date(date).toISOString()
|
||||
const modifiedAt = new Date(lastmod || date).toISOString()
|
||||
const publishedAt = new Date(date).toISOString();
|
||||
const modifiedAt = new Date(lastmod || date).toISOString();
|
||||
const imagesArr =
|
||||
images.length === 0
|
||||
? [siteMetadata.socialBanner]
|
||||
: typeof images === 'string'
|
||||
? [images]
|
||||
: images
|
||||
: images;
|
||||
|
||||
const featuredImages = imagesArr.map((img) => {
|
||||
return {
|
||||
'@type': 'ImageObject',
|
||||
url: `${siteMetadata.siteUrl}${img}`,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
let authorList
|
||||
let authorList;
|
||||
if (authorDetails) {
|
||||
authorList = authorDetails.map((author) => {
|
||||
return {
|
||||
'@type': 'Person',
|
||||
name: author.name,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
} else {
|
||||
authorList = {
|
||||
'@type': 'Person',
|
||||
name: siteMetadata.author,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const structuredData = {
|
||||
@ -166,9 +175,9 @@ export const BlogSEO = ({
|
||||
},
|
||||
},
|
||||
description: summary,
|
||||
}
|
||||
};
|
||||
|
||||
const twImageUrl = featuredImages[0].url
|
||||
const twImageUrl = featuredImages[0].url;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -181,8 +190,12 @@ export const BlogSEO = ({
|
||||
canonicalUrl={canonicalUrl}
|
||||
/>
|
||||
<Head>
|
||||
{date && <meta property="article:published_time" content={publishedAt} />}
|
||||
{lastmod && <meta property="article:modified_time" content={modifiedAt} />}
|
||||
{date && (
|
||||
<meta property="article:published_time" content={publishedAt} />
|
||||
)}
|
||||
{lastmod && (
|
||||
<meta property="article:modified_time" content={modifiedAt} />
|
||||
)}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
@ -191,5 +204,5 @@ export const BlogSEO = ({
|
||||
/>
|
||||
</Head>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -1,34 +1,34 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const ScrollTopAndComment = () => {
|
||||
const [show, setShow] = useState(false)
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWindowScroll = () => {
|
||||
if (window.scrollY > 50) setShow(true)
|
||||
else setShow(false)
|
||||
}
|
||||
if (window.scrollY > 50) setShow(true);
|
||||
else setShow(false);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleWindowScroll)
|
||||
return () => window.removeEventListener('scroll', handleWindowScroll)
|
||||
}, [])
|
||||
window.addEventListener('scroll', handleWindowScroll);
|
||||
return () => window.removeEventListener('scroll', handleWindowScroll);
|
||||
}, []);
|
||||
|
||||
const handleScrollTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
const handleScrollToComment = () => {
|
||||
document.getElementById('comment').scrollIntoView()
|
||||
}
|
||||
document.getElementById('comment').scrollIntoView();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={`fixed right-8 bottom-8 hidden flex-col gap-3 ${show ? 'md:flex' : 'md:hidden'}`}
|
||||
>
|
||||
className={`fixed right-8 bottom-8 hidden flex-col gap-3 ${
|
||||
show ? 'md:flex' : 'md:hidden'
|
||||
}`}>
|
||||
<button
|
||||
aria-label="Scroll To Comment"
|
||||
type="button"
|
||||
onClick={handleScrollToComment}
|
||||
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
|
||||
>
|
||||
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">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
@ -41,8 +41,7 @@ const ScrollTopAndComment = () => {
|
||||
aria-label="Scroll To Top"
|
||||
type="button"
|
||||
onClick={handleScrollTop}
|
||||
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
|
||||
>
|
||||
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">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
@ -52,7 +51,7 @@ const ScrollTopAndComment = () => {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollTopAndComment
|
||||
export default ScrollTopAndComment;
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function SectionContainer({ children }: Props) {
|
||||
return <div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Toc } from 'types/Toc'
|
||||
import { Toc } from 'types/Toc';
|
||||
|
||||
interface TOCInlineProps {
|
||||
toc: Toc
|
||||
indentDepth?: number
|
||||
fromHeading?: number
|
||||
toHeading?: number
|
||||
asDisclosure?: boolean
|
||||
exclude?: string | string[]
|
||||
toc: Toc;
|
||||
indentDepth?: number;
|
||||
fromHeading?: number;
|
||||
toHeading?: number;
|
||||
asDisclosure?: boolean;
|
||||
exclude?: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,35 +34,41 @@ const TOCInline = ({
|
||||
}: TOCInlineProps) => {
|
||||
const re = Array.isArray(exclude)
|
||||
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
|
||||
: new RegExp('^(' + exclude + ')$', 'i')
|
||||
: new RegExp('^(' + exclude + ')$', 'i');
|
||||
|
||||
const filteredToc = toc.filter(
|
||||
(heading) =>
|
||||
heading.depth >= fromHeading && heading.depth <= toHeading && !re.test(heading.value)
|
||||
)
|
||||
heading.depth >= fromHeading &&
|
||||
heading.depth <= toHeading &&
|
||||
!re.test(heading.value)
|
||||
);
|
||||
|
||||
const tocList = (
|
||||
<ul>
|
||||
{filteredToc.map((heading) => (
|
||||
<li key={heading.value} className={`${heading.depth >= indentDepth && 'ml-6'}`}>
|
||||
<li
|
||||
key={heading.value}
|
||||
className={`${heading.depth >= indentDepth && 'ml-6'}`}>
|
||||
<a href={heading.url}>{heading.value}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{asDisclosure ? (
|
||||
<details open>
|
||||
<summary className="ml-6 pt-2 pb-2 text-xl font-bold">Table of Contents</summary>
|
||||
<summary className="ml-6 pt-2 pb-2 text-xl font-bold">
|
||||
Table of Contents
|
||||
</summary>
|
||||
<div className="ml-6">{tocList}</div>
|
||||
</details>
|
||||
) : (
|
||||
tocList
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default TOCInline
|
||||
export default TOCInline;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import Link from 'next/link'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
import Link from 'next/link';
|
||||
import kebabCase from '@/lib/utils/kebabCase';
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Tag = ({ text }: Props) => {
|
||||
@ -12,7 +12,7 @@ const Tag = ({ text }: Props) => {
|
||||
{text.split(' ').join('-')}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Tag
|
||||
export default Tag;
|
||||
|
@ -1,26 +1,28 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
const ThemeSwitch = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme, resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
// When mounted on client, now we can show the UI
|
||||
useEffect(() => setMounted(true), [])
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label="Toggle Dark Mode"
|
||||
type="button"
|
||||
className="ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4"
|
||||
onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
onClick={() =>
|
||||
setTheme(
|
||||
theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark'
|
||||
)
|
||||
}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
className="text-gray-900 dark:text-gray-100">
|
||||
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
@ -32,7 +34,7 @@ const ThemeSwitch = () => {
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSwitch
|
||||
export default ThemeSwitch;
|
||||
|
@ -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 = () => {
|
||||
return (
|
||||
@ -21,10 +21,10 @@ const GAScript = () => {
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default GAScript
|
||||
export default GAScript;
|
||||
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
|
||||
export const logEvent = (action, category, label, value) => {
|
||||
@ -32,5 +32,5 @@ export const logEvent = (action, category, label, value) => {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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 = () => {
|
||||
return (
|
||||
@ -16,12 +16,12 @@ const PlausibleScript = () => {
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PlausibleScript
|
||||
export default PlausibleScript;
|
||||
|
||||
// https://plausible.io/docs/custom-event-goals
|
||||
export const logEvent = (eventName, ...rest) => {
|
||||
return window.plausible?.(eventName, ...rest)
|
||||
}
|
||||
return window.plausible?.(eventName, ...rest);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Script from 'next/script'
|
||||
import Script from 'next/script';
|
||||
|
||||
const SimpleAnalyticsScript = () => {
|
||||
return (
|
||||
@ -8,18 +8,21 @@ 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]};
|
||||
`}
|
||||
</Script>
|
||||
<Script strategy="lazyOnload" src="https://scripts.simpleanalyticscdn.com/latest.js" />
|
||||
<Script
|
||||
strategy="lazyOnload"
|
||||
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// https://docs.simpleanalytics.com/events
|
||||
export const logEvent = (eventName, callback) => {
|
||||
if (callback) {
|
||||
return window.sa_event?.(eventName, callback)
|
||||
return window.sa_event?.(eventName, callback);
|
||||
} else {
|
||||
return window.sa_event?.(eventName)
|
||||
return window.sa_event?.(eventName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default SimpleAnalyticsScript
|
||||
export default SimpleAnalyticsScript;
|
||||
|
@ -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 = () => {
|
||||
return (
|
||||
@ -12,7 +12,7 @@ const UmamiScript = () => {
|
||||
src="https://umami.example.com/umami.js" // Replace with your umami instance
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default UmamiScript
|
||||
export default UmamiScript;
|
||||
|
@ -1,28 +1,32 @@
|
||||
import GA from './GoogleAnalytics'
|
||||
import Plausible from './Plausible'
|
||||
import SimpleAnalytics from './SimpleAnalytics'
|
||||
import Umami from './Umami'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import GA from './GoogleAnalytics';
|
||||
import Plausible from './Plausible';
|
||||
import SimpleAnalytics from './SimpleAnalytics';
|
||||
import Umami from './Umami';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag?: (...args: any[]) => void
|
||||
plausible?: (...args: any[]) => void
|
||||
sa_event?: (...args: any[]) => void
|
||||
gtag?: (...args: any[]) => void;
|
||||
plausible?: (...args: any[]) => void;
|
||||
sa_event?: (...args: any[]) => void;
|
||||
}
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const Analytics = () => {
|
||||
return (
|
||||
<>
|
||||
{isProduction && siteMetadata.analytics.plausibleDataDomain && <Plausible />}
|
||||
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
|
||||
{isProduction && siteMetadata.analytics.plausibleDataDomain && (
|
||||
<Plausible />
|
||||
)}
|
||||
{isProduction && siteMetadata.analytics.simpleAnalytics && (
|
||||
<SimpleAnalytics />
|
||||
)}
|
||||
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
|
||||
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics
|
||||
export default Analytics;
|
||||
|
@ -1,30 +1,33 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { useTheme } from 'next-themes'
|
||||
import ReactCommento from './commento/ReactCommento'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
import { useTheme } from 'next-themes';
|
||||
import ReactCommento from './commento/ReactCommento';
|
||||
|
||||
interface Props {
|
||||
frontMatter: PostFrontMatter
|
||||
frontMatter: PostFrontMatter;
|
||||
}
|
||||
|
||||
const Commento = ({ frontMatter }: Props) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { resolvedTheme } = useTheme();
|
||||
const commentsTheme = useMemo(() => {
|
||||
switch (resolvedTheme) {
|
||||
case 'light':
|
||||
case 'dark':
|
||||
return resolvedTheme
|
||||
return resolvedTheme;
|
||||
default:
|
||||
return 'auto'
|
||||
return 'auto';
|
||||
}
|
||||
}, [resolvedTheme])
|
||||
}, [resolvedTheme]);
|
||||
return (
|
||||
<div className="my-2">
|
||||
<ReactCommento url={siteMetadata.comment.commentoConfig.url} pageId={frontMatter.slug} />
|
||||
<ReactCommento
|
||||
url={siteMetadata.comment.commentoConfig.url}
|
||||
pageId={frontMatter.slug}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Commento
|
||||
export default Commento;
|
||||
|
@ -1,25 +1,25 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { ReactCusdis } from 'react-cusdis'
|
||||
import { useTheme } from 'next-themes'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
import { ReactCusdis } from 'react-cusdis';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
interface Props {
|
||||
frontMatter: PostFrontMatter
|
||||
frontMatter: PostFrontMatter;
|
||||
}
|
||||
|
||||
const Cusdis = ({ frontMatter }: Props) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { resolvedTheme } = useTheme();
|
||||
const commentsTheme = useMemo(() => {
|
||||
switch (resolvedTheme) {
|
||||
case 'light':
|
||||
case 'dark':
|
||||
return resolvedTheme
|
||||
return resolvedTheme;
|
||||
default:
|
||||
return 'auto'
|
||||
return 'auto';
|
||||
}
|
||||
}, [resolvedTheme])
|
||||
}, [resolvedTheme]);
|
||||
return (
|
||||
<div className="my-2">
|
||||
<ReactCusdis
|
||||
@ -35,7 +35,7 @@ const Cusdis = ({ frontMatter }: Props) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Cusdis
|
||||
export default Cusdis;
|
||||
|
@ -1,46 +1,51 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
interface Props {
|
||||
frontMatter: PostFrontMatter
|
||||
frontMatter: PostFrontMatter;
|
||||
}
|
||||
|
||||
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() {
|
||||
setEnabledLoadComments(false)
|
||||
setEnabledLoadComments(false);
|
||||
|
||||
// @ts-ignore
|
||||
window.disqus_config = function () {
|
||||
this.page.url = window.location.href
|
||||
this.page.identifier = frontMatter.slug
|
||||
}
|
||||
this.page.url = window.location.href;
|
||||
this.page.identifier = frontMatter.slug;
|
||||
};
|
||||
// @ts-ignore
|
||||
if (window.DISQUS === undefined) {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
|
||||
const script = document.createElement('script');
|
||||
script.src =
|
||||
'https://' +
|
||||
siteMetadata.comment.disqusConfig.shortname +
|
||||
'.disqus.com/embed.js';
|
||||
// @ts-ignore
|
||||
script.setAttribute('data-timestamp', +new Date())
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.async = true
|
||||
document.body.appendChild(script)
|
||||
script.setAttribute('data-timestamp', +new Date());
|
||||
script.setAttribute('crossorigin', 'anonymous');
|
||||
script.async = true;
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.DISQUS.reset({ reload: true })
|
||||
window.DISQUS.reset({ reload: true });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
{enableLoadComments && (
|
||||
<button onClick={LoadComments}>Load Comments</button>
|
||||
)}
|
||||
<div className="disqus-frame" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Disqus
|
||||
export default Disqus;
|
||||
|
@ -1,61 +1,78 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
interface Props {
|
||||
mapping: string
|
||||
mapping: string;
|
||||
}
|
||||
|
||||
const Giscus = ({ mapping }: Props) => {
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
const { theme, resolvedTheme } = useTheme()
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true);
|
||||
const { theme, resolvedTheme } = useTheme();
|
||||
const commentsTheme =
|
||||
siteMetadata.comment.giscusConfig.themeURL === ''
|
||||
? theme === 'dark' || resolvedTheme === 'dark'
|
||||
? siteMetadata.comment.giscusConfig.darkTheme
|
||||
: siteMetadata.comment.giscusConfig.theme
|
||||
: siteMetadata.comment.giscusConfig.themeURL
|
||||
: siteMetadata.comment.giscusConfig.themeURL;
|
||||
|
||||
const COMMENTS_ID = 'comments-container'
|
||||
const COMMENTS_ID = 'comments-container';
|
||||
|
||||
const LoadComments = useCallback(() => {
|
||||
setEnabledLoadComments(false)
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://giscus.app/client.js'
|
||||
script.setAttribute('data-repo', siteMetadata.comment.giscusConfig.repo)
|
||||
script.setAttribute('data-repo-id', siteMetadata.comment.giscusConfig.repositoryId)
|
||||
script.setAttribute('data-category', siteMetadata.comment.giscusConfig.category)
|
||||
script.setAttribute('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
|
||||
setEnabledLoadComments(false);
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://giscus.app/client.js';
|
||||
script.setAttribute('data-repo', siteMetadata.comment.giscusConfig.repo);
|
||||
script.setAttribute(
|
||||
'data-repo-id',
|
||||
siteMetadata.comment.giscusConfig.repositoryId
|
||||
);
|
||||
script.setAttribute(
|
||||
'data-category',
|
||||
siteMetadata.comment.giscusConfig.category
|
||||
);
|
||||
script.setAttribute(
|
||||
'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)
|
||||
if (comments) comments.appendChild(script)
|
||||
const comments = document.getElementById(COMMENTS_ID);
|
||||
if (comments) comments.appendChild(script);
|
||||
|
||||
return () => {
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.innerHTML = ''
|
||||
}
|
||||
}, [commentsTheme, mapping])
|
||||
const comments = document.getElementById(COMMENTS_ID);
|
||||
if (comments) comments.innerHTML = '';
|
||||
};
|
||||
}, [commentsTheme, mapping]);
|
||||
|
||||
// Reload on theme change
|
||||
useEffect(() => {
|
||||
const iframe = document.querySelector('iframe.giscus-frame')
|
||||
if (!iframe) return
|
||||
LoadComments()
|
||||
}, [LoadComments])
|
||||
const iframe = document.querySelector('iframe.giscus-frame');
|
||||
if (!iframe) return;
|
||||
LoadComments();
|
||||
}, [LoadComments]);
|
||||
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
{enableLoadComments && (
|
||||
<button onClick={LoadComments}>Load Comments</button>
|
||||
)}
|
||||
<div className="giscus" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Giscus
|
||||
export default Giscus;
|
||||
|
@ -1,56 +1,58 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
interface Props {
|
||||
issueTerm: string
|
||||
issueTerm: string;
|
||||
}
|
||||
|
||||
const Utterances = ({ issueTerm }: Props) => {
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
const { theme, resolvedTheme } = useTheme()
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true);
|
||||
const { theme, resolvedTheme } = useTheme();
|
||||
const commentsTheme =
|
||||
theme === 'dark' || resolvedTheme === 'dark'
|
||||
? 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(() => {
|
||||
setEnabledLoadComments(false)
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://utteranc.es/client.js'
|
||||
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
|
||||
script.setAttribute('issue-term', issueTerm)
|
||||
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
|
||||
script.setAttribute('theme', commentsTheme)
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.async = true
|
||||
setEnabledLoadComments(false);
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://utteranc.es/client.js';
|
||||
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo);
|
||||
script.setAttribute('issue-term', issueTerm);
|
||||
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label);
|
||||
script.setAttribute('theme', commentsTheme);
|
||||
script.setAttribute('crossorigin', 'anonymous');
|
||||
script.async = true;
|
||||
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.appendChild(script)
|
||||
const comments = document.getElementById(COMMENTS_ID);
|
||||
if (comments) comments.appendChild(script);
|
||||
|
||||
return () => {
|
||||
const comments = document.getElementById(COMMENTS_ID)
|
||||
if (comments) comments.innerHTML = ''
|
||||
}
|
||||
}, [commentsTheme, issueTerm])
|
||||
const comments = document.getElementById(COMMENTS_ID);
|
||||
if (comments) comments.innerHTML = '';
|
||||
};
|
||||
}, [commentsTheme, issueTerm]);
|
||||
|
||||
// Reload on theme change
|
||||
useEffect(() => {
|
||||
const iframe = document.querySelector('iframe.utterances-frame')
|
||||
if (!iframe) return
|
||||
LoadComments()
|
||||
}, [LoadComments])
|
||||
const iframe = document.querySelector('iframe.utterances-frame');
|
||||
if (!iframe) return;
|
||||
LoadComments();
|
||||
}, [LoadComments]);
|
||||
|
||||
// Added `relative` to fix a weird bug with `utterances-frame` position
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
{enableLoadComments && (
|
||||
<button onClick={LoadComments}>Load Comments</button>
|
||||
)}
|
||||
<div className="utterances-frame relative" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Utterances
|
||||
export default Utterances;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createRef } from 'preact'
|
||||
import React, { useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import { createRef } from 'preact';
|
||||
import React, { useLayoutEffect, useMemo, useRef } from 'react';
|
||||
|
||||
interface DataAttributes {
|
||||
[key: string]: string | boolean | undefined
|
||||
[key: string]: string | boolean | undefined;
|
||||
}
|
||||
|
||||
const insertScript = (
|
||||
@ -11,28 +11,28 @@ const insertScript = (
|
||||
dataAttributes: DataAttributes,
|
||||
onload = () => {}
|
||||
) => {
|
||||
const script = window.document.createElement('script')
|
||||
script.async = true
|
||||
script.src = src
|
||||
script.id = id
|
||||
const script = window.document.createElement('script');
|
||||
script.async = true;
|
||||
script.src = src;
|
||||
script.id = 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]) => {
|
||||
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 () => {
|
||||
script.remove()
|
||||
}
|
||||
}
|
||||
script.remove();
|
||||
};
|
||||
};
|
||||
|
||||
const ReactCommento = ({
|
||||
url,
|
||||
@ -42,22 +42,25 @@ const ReactCommento = ({
|
||||
hideDeleted,
|
||||
pageId,
|
||||
}: {
|
||||
url: string
|
||||
cssOverride?: string
|
||||
autoInit?: boolean
|
||||
noFonts?: boolean
|
||||
hideDeleted?: boolean
|
||||
pageId?: string
|
||||
url: string;
|
||||
cssOverride?: string;
|
||||
autoInit?: boolean;
|
||||
noFonts?: boolean;
|
||||
hideDeleted?: boolean;
|
||||
pageId?: string;
|
||||
}) => {
|
||||
const containerId = useMemo(() => `commento-${Math.random().toString().slice(2, 8)}`, [])
|
||||
const container = createRef<HTMLDivElement>()
|
||||
const containerId = useMemo(
|
||||
() => `commento-${Math.random().toString().slice(2, 8)}`,
|
||||
[]
|
||||
);
|
||||
const container = createRef<HTMLDivElement>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!window) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
window['commento'] = container.current
|
||||
window['commento'] = container.current;
|
||||
|
||||
const removeScript = insertScript(
|
||||
url,
|
||||
@ -71,11 +74,20 @@ const ReactCommento = ({
|
||||
'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} />
|
||||
}
|
||||
export default ReactCommento
|
||||
return <div ref={container} id={containerId} />;
|
||||
};
|
||||
export default ReactCommento;
|
||||
|
@ -1,66 +1,67 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
interface Props {
|
||||
frontMatter: PostFrontMatter
|
||||
frontMatter: PostFrontMatter;
|
||||
}
|
||||
|
||||
const UtterancesComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Utterances')
|
||||
return import('@/components/comments/Utterances');
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
);
|
||||
const GiscusComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Giscus')
|
||||
return import('@/components/comments/Giscus');
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
);
|
||||
const DisqusComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Disqus')
|
||||
return import('@/components/comments/Disqus');
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
);
|
||||
const CusdisComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Cusdis')
|
||||
return import('@/components/comments/Cusdis');
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
);
|
||||
const CommentoComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/comments/Commento')
|
||||
return import('@/components/comments/Commento');
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
);
|
||||
|
||||
const Comments = ({ frontMatter }: Props) => {
|
||||
let term
|
||||
let term;
|
||||
switch (
|
||||
siteMetadata.comment.giscusConfig.mapping ||
|
||||
siteMetadata.comment.utterancesConfig.issueTerm
|
||||
) {
|
||||
case 'pathname':
|
||||
term = frontMatter.slug
|
||||
break
|
||||
term = frontMatter.slug;
|
||||
break;
|
||||
case 'url':
|
||||
term = window.location.href
|
||||
break
|
||||
term = window.location.href;
|
||||
break;
|
||||
case 'title':
|
||||
term = frontMatter.title
|
||||
break
|
||||
term = frontMatter.title;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div id="comment">
|
||||
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && (
|
||||
<GiscusComponent mapping={term} />
|
||||
)}
|
||||
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
|
||||
{siteMetadata.comment &&
|
||||
siteMetadata.comment.provider === 'utterances' && (
|
||||
<UtterancesComponent issueTerm={term} />
|
||||
)}
|
||||
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
|
||||
@ -73,7 +74,7 @@ const Comments = ({ frontMatter }: Props) => {
|
||||
<CommentoComponent frontMatter={frontMatter} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Comments
|
||||
export default Comments;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import Mail from './mail.svg'
|
||||
import Github from './github.svg'
|
||||
import Facebook from './facebook.svg'
|
||||
import Youtube from './youtube.svg'
|
||||
import Linkedin from './linkedin.svg'
|
||||
import Twitter from './twitter.svg'
|
||||
import Mail from './mail.svg';
|
||||
import Github from './github.svg';
|
||||
import Facebook from './facebook.svg';
|
||||
import Youtube from './youtube.svg';
|
||||
import Linkedin from './linkedin.svg';
|
||||
import Twitter from './twitter.svg';
|
||||
|
||||
// Icons taken from: https://simpleicons.org/
|
||||
|
||||
@ -14,27 +14,30 @@ const components = {
|
||||
youtube: Youtube,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
}
|
||||
};
|
||||
|
||||
const SocialIcon = ({ kind, href, size = 8 }) => {
|
||||
if (!href || (kind === 'mail' && !/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href)))
|
||||
return null
|
||||
if (
|
||||
!href ||
|
||||
(kind === 'mail' &&
|
||||
!/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
|
||||
)
|
||||
return null;
|
||||
|
||||
const SocialSvg = components[kind]
|
||||
const SocialSvg = components[kind];
|
||||
|
||||
return (
|
||||
<a
|
||||
className="text-sm text-gray-500 transition hover:text-gray-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
>
|
||||
href={href}>
|
||||
<span className="sr-only">{kind}</span>
|
||||
<SocialSvg
|
||||
className={`fill-current text-gray-700 hover:text-blue-500 dark:text-gray-200 dark:hover:text-blue-400 h-${size} w-${size}`}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialIcon
|
||||
export default SocialIcon;
|
||||
|
@ -1,16 +1,25 @@
|
||||
import SocialIcon from '@/components/social-icons'
|
||||
import Image from '@/components/Image'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import { ReactNode } from 'react'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import SocialIcon from '@/components/social-icons';
|
||||
import Image from '@/components/Image';
|
||||
import { PageSEO } from '@/components/SEO';
|
||||
import { ReactNode } from 'react';
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
frontMatter: AuthorFrontMatter
|
||||
children: ReactNode;
|
||||
frontMatter: AuthorFrontMatter;
|
||||
}
|
||||
|
||||
export default function AuthorLayout({ children, frontMatter }: Props) {
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
|
||||
const {
|
||||
name,
|
||||
avatar,
|
||||
occupation,
|
||||
company,
|
||||
email,
|
||||
twitter,
|
||||
linkedin,
|
||||
github,
|
||||
} = frontMatter;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -30,7 +39,9 @@ export default function AuthorLayout({ children, frontMatter }: Props) {
|
||||
height="192px"
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">{name}</h3>
|
||||
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">
|
||||
{name}
|
||||
</h3>
|
||||
<div className="text-gray-500 dark:text-gray-400">{occupation}</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">{company}</div>
|
||||
<div className="flex space-x-3 pt-6">
|
||||
@ -40,9 +51,11 @@ export default function AuthorLayout({ children, frontMatter }: Props) {
|
||||
<SocialIcon kind="twitter" href={twitter} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose max-w-none pt-8 pb-8 dark:prose-dark xl:col-span-2">{children}</div>
|
||||
<div className="prose max-w-none pt-8 pb-8 dark:prose-dark xl:col-span-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,26 +1,34 @@
|
||||
import Link from '@/components/Link'
|
||||
import Tag from '@/components/Tag'
|
||||
import { ComponentProps, useState } from 'react'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import Link from '@/components/Link';
|
||||
import Tag from '@/components/Tag';
|
||||
import { ComponentProps, useState } from 'react';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import formatDate from '@/lib/utils/formatDate';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
interface Props {
|
||||
posts: PostFrontMatter[]
|
||||
title: string
|
||||
initialDisplayPosts?: PostFrontMatter[]
|
||||
pagination?: ComponentProps<typeof Pagination>
|
||||
posts: PostFrontMatter[];
|
||||
title: string;
|
||||
initialDisplayPosts?: PostFrontMatter[];
|
||||
pagination?: ComponentProps<typeof Pagination>;
|
||||
}
|
||||
|
||||
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }: Props) {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
export default function ListLayout({
|
||||
posts,
|
||||
title,
|
||||
initialDisplayPosts = [],
|
||||
pagination,
|
||||
}: Props) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const filteredBlogPosts = posts.filter((frontMatter) => {
|
||||
const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')
|
||||
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
|
||||
})
|
||||
const searchContent =
|
||||
frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ');
|
||||
return searchContent.toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
|
||||
// If initialDisplayPosts exist, display it if no searchValue is specified
|
||||
const displayPosts =
|
||||
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
|
||||
initialDisplayPosts.length > 0 && !searchValue
|
||||
? initialDisplayPosts
|
||||
: filteredBlogPosts;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -42,8 +50,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@ -56,7 +63,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
||||
<ul>
|
||||
{!filteredBlogPosts.length && 'No posts found.'}
|
||||
{displayPosts.map((frontMatter) => {
|
||||
const { slug, date, title, summary, tags } = frontMatter
|
||||
const { slug, date, title, summary, tags } = frontMatter;
|
||||
return (
|
||||
<li key={slug} className="py-4">
|
||||
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
|
||||
@ -69,7 +76,9 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
||||
<div className="space-y-3 xl:col-span-3">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-8 tracking-tight">
|
||||
<Link href={`/blog/${slug}`} className="text-gray-900 dark:text-gray-100">
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</Link>
|
||||
</h3>
|
||||
@ -85,13 +94,16 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{pagination && pagination.totalPages > 1 && !searchValue && (
|
||||
<Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
|
||||
<Pagination
|
||||
currentPage={pagination.currentPage}
|
||||
totalPages={pagination.totalPages}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,27 +1,27 @@
|
||||
import Link from '@/components/Link'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import { BlogSEO } from '@/components/SEO'
|
||||
import Image from '@/components/Image'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import Comments from '@/components/comments'
|
||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import Link from '@/components/Link';
|
||||
import PageTitle from '@/components/PageTitle';
|
||||
import SectionContainer from '@/components/SectionContainer';
|
||||
import { BlogSEO } from '@/components/SEO';
|
||||
import Image from '@/components/Image';
|
||||
import Tag from '@/components/Tag';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import Comments from '@/components/comments';
|
||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
|
||||
|
||||
const editUrl = (fileName) => `${siteMetadata.siteRepo}/raw/master/data/blog/${fileName}`
|
||||
const editUrl = (fileName) =>
|
||||
`${siteMetadata.siteRepo}/raw/master/data/blog/${fileName}`;
|
||||
const discussUrl = (slug) =>
|
||||
`https://mobile.twitter.com/search?q=${encodeURIComponent(
|
||||
`${siteMetadata.siteUrl}/blog/${slug}`
|
||||
)}`
|
||||
)}`;
|
||||
const Copyright = () => (
|
||||
<a
|
||||
rel="license"
|
||||
href="http://creativecommons.org/licenses/by-sa/4.0/"
|
||||
className="inline-flex self-center"
|
||||
>
|
||||
className="inline-flex self-center">
|
||||
<Image
|
||||
className="border-0"
|
||||
alt="知识共享许可协议"
|
||||
@ -30,25 +30,31 @@ const Copyright = () => (
|
||||
src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
);
|
||||
|
||||
const postDateTemplate: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
frontMatter: PostFrontMatter
|
||||
authorDetails: AuthorFrontMatter[]
|
||||
next?: { slug: string; title: string }
|
||||
prev?: { slug: string; title: string }
|
||||
children: ReactNode
|
||||
frontMatter: PostFrontMatter;
|
||||
authorDetails: AuthorFrontMatter[];
|
||||
next?: { slug: string; title: string };
|
||||
prev?: { slug: string; title: string };
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }: 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(
|
||||
() =>
|
||||
@ -58,7 +64,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
}
|
||||
: {},
|
||||
[images]
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
@ -87,7 +93,10 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>
|
||||
{new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
|
||||
{new Date(date).toLocaleDateString(
|
||||
siteMetadata.locale,
|
||||
postDateTemplate
|
||||
)}
|
||||
</time>
|
||||
</dd>
|
||||
</div>
|
||||
@ -99,14 +108,15 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</header>
|
||||
<div
|
||||
className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0"
|
||||
style={{ gridTemplateRows: 'auto 1fr' }}
|
||||
>
|
||||
style={{ gridTemplateRows: 'auto 1fr' }}>
|
||||
<dl className="pt-6 pb-10 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700">
|
||||
<dt className="sr-only">Authors</dt>
|
||||
<dd>
|
||||
<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) => (
|
||||
<li className="flex items-center space-x-2" key={author.name}>
|
||||
<li
|
||||
className="flex items-center space-x-2"
|
||||
key={author.name}>
|
||||
{author.avatar && (
|
||||
<Image
|
||||
src={author.avatar}
|
||||
@ -118,15 +128,19 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
)}
|
||||
<dl className="whitespace-nowrap text-sm font-medium leading-5">
|
||||
<dt className="sr-only">Name</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
|
||||
<dd className="text-gray-900 dark:text-gray-100">
|
||||
{author.name}
|
||||
</dd>
|
||||
<dt className="sr-only">Twitter</dt>
|
||||
<dd>
|
||||
{author.twitter && (
|
||||
<Link
|
||||
href={author.twitter}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
{author.twitter.replace('https://twitter.com/', '@')}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
{author.twitter.replace(
|
||||
'https://twitter.com/',
|
||||
'@'
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</dd>
|
||||
@ -137,7 +151,9 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
||||
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
|
||||
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
<Copyright />
|
||||
<Link href={editUrl(fileName)}>{'View source'}</Link>
|
||||
@ -186,8 +202,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
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">
|
||||
← Back to the blog
|
||||
</Link>
|
||||
</div>
|
||||
@ -196,5 +211,5 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
||||
</div>
|
||||
</article>
|
||||
</SectionContainer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,23 +1,28 @@
|
||||
import Link from '@/components/Link'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import { BlogSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import Comments from '@/components/comments'
|
||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||
import { ReactNode } from 'react'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import Link from '@/components/Link';
|
||||
import PageTitle from '@/components/PageTitle';
|
||||
import SectionContainer from '@/components/SectionContainer';
|
||||
import { BlogSEO } from '@/components/SEO';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import formatDate from '@/lib/utils/formatDate';
|
||||
import Comments from '@/components/comments';
|
||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
|
||||
import { ReactNode } from 'react';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
interface Props {
|
||||
frontMatter: PostFrontMatter
|
||||
children: ReactNode
|
||||
next?: { slug: string; title: string }
|
||||
prev?: { slug: string; title: string }
|
||||
frontMatter: PostFrontMatter;
|
||||
children: ReactNode;
|
||||
next?: { slug: string; title: string };
|
||||
prev?: { slug: string; title: string };
|
||||
}
|
||||
|
||||
export default function PostLayout({ frontMatter, next, prev, children }: Props) {
|
||||
const { slug, date, title } = frontMatter
|
||||
export default function PostLayout({
|
||||
frontMatter,
|
||||
next,
|
||||
prev,
|
||||
children,
|
||||
}: Props) {
|
||||
const { slug, date, title } = frontMatter;
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
@ -42,10 +47,11 @@ export default function PostLayout({ frontMatter, next, prev, children }: Props)
|
||||
</header>
|
||||
<div
|
||||
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="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
|
||||
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<Comments frontMatter={frontMatter} />
|
||||
<footer>
|
||||
@ -54,8 +60,7 @@ export default function PostLayout({ frontMatter, next, prev, children }: Props)
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
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">
|
||||
← {prev.title}
|
||||
</Link>
|
||||
</div>
|
||||
@ -64,8 +69,7 @@ export default function PostLayout({ frontMatter, next, prev, children }: Props)
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
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} →
|
||||
</Link>
|
||||
</div>
|
||||
@ -76,5 +80,5 @@ export default function PostLayout({ frontMatter, next, prev, children }: Props)
|
||||
</div>
|
||||
</article>
|
||||
</SectionContainer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { escape } from '@/lib/utils/htmlEscaper'
|
||||
import { escape } from '@/lib/utils/htmlEscaper';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
const generateRssItem = (post: PostFrontMatter) => `
|
||||
<item>
|
||||
@ -13,7 +13,7 @@ const generateRssItem = (post: PostFrontMatter) => `
|
||||
<author>${siteMetadata.email} (${siteMetadata.author})</author>
|
||||
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
|
||||
</item>
|
||||
`
|
||||
`;
|
||||
|
||||
const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
@ -22,12 +22,16 @@ const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
|
||||
<link>${siteMetadata.siteUrl}/blog</link>
|
||||
<description>${escape(siteMetadata.description)}</description>
|
||||
<language>${siteMetadata.language}</language>
|
||||
<managingEditor>${siteMetadata.email} (${siteMetadata.author})</managingEditor>
|
||||
<managingEditor>${siteMetadata.email} (${
|
||||
siteMetadata.author
|
||||
})</managingEditor>
|
||||
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
|
||||
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
|
||||
<atom:link href="${siteMetadata.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
|
||||
<atom:link href="${
|
||||
siteMetadata.siteUrl
|
||||
}/${page}" rel="self" type="application/rss+xml"/>
|
||||
${posts.map(generateRssItem).join('')}
|
||||
</channel>
|
||||
</rss>
|
||||
`
|
||||
export default generateRss
|
||||
`;
|
||||
export default generateRss;
|
||||
|
130
lib/mdx.ts
130
lib/mdx.ts
@ -1,62 +1,78 @@
|
||||
import { bundleMDX } from 'mdx-bundler'
|
||||
import fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import path from 'path'
|
||||
import readingTime from 'reading-time'
|
||||
import getAllFilesRecursively from './utils/files'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import { Toc } from 'types/Toc'
|
||||
import { bundleMDX } from 'mdx-bundler';
|
||||
import fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import path from 'path';
|
||||
import readingTime from 'reading-time';
|
||||
import getAllFilesRecursively from './utils/files';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
|
||||
import { Toc } from 'types/Toc';
|
||||
// Remark packages
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkFootnotes from 'remark-footnotes'
|
||||
import remarkMath from 'remark-math'
|
||||
import remarkExtractFrontmatter from './remark-extract-frontmatter'
|
||||
import remarkCodeTitles from './remark-code-title'
|
||||
import remarkTocHeadings from './remark-toc-headings'
|
||||
import remarkImgToJsx from './remark-img-to-jsx'
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkFootnotes from 'remark-footnotes';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkExtractFrontmatter from './remark-extract-frontmatter';
|
||||
import remarkCodeTitles from './remark-code-title';
|
||||
import remarkTocHeadings from './remark-toc-headings';
|
||||
import remarkImgToJsx from './remark-img-to-jsx';
|
||||
// Rehype packages
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeCitation from 'rehype-citation'
|
||||
import rehypePrismPlus from 'rehype-prism-plus'
|
||||
import rehypePresetMinify from 'rehype-preset-minify'
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeCitation from 'rehype-citation';
|
||||
import rehypePrismPlus from 'rehype-prism-plus';
|
||||
import rehypePresetMinify from 'rehype-preset-minify';
|
||||
|
||||
const root = process.cwd()
|
||||
const root = process.cwd();
|
||||
|
||||
export function getFiles(type: 'blog' | 'authors') {
|
||||
const prefixPaths = path.join(root, 'data', type)
|
||||
const files = getAllFilesRecursively(prefixPaths)
|
||||
const prefixPaths = path.join(root, 'data', type);
|
||||
const files = getAllFilesRecursively(prefixPaths);
|
||||
// Only want to return blog/path and ignore root, replace is needed to work on Windows
|
||||
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
|
||||
return files.map((file) =>
|
||||
file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
|
||||
);
|
||||
}
|
||||
|
||||
export function formatSlug(slug: string) {
|
||||
return slug.replace(/\.(mdx|md)/, '')
|
||||
return slug.replace(/\.(mdx|md)/, '');
|
||||
}
|
||||
|
||||
export function dateSortDesc(a: string, b: string) {
|
||||
if (a > b) return -1
|
||||
if (a < b) return 1
|
||||
return 0
|
||||
if (a > b) return -1;
|
||||
if (a < b) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string | string[]) {
|
||||
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
|
||||
const mdPath = path.join(root, 'data', type, `${slug}.md`)
|
||||
export async function getFileBySlug<T>(
|
||||
type: 'authors' | 'blog',
|
||||
slug: string | string[]
|
||||
) {
|
||||
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`);
|
||||
const mdPath = path.join(root, 'data', type, `${slug}.md`);
|
||||
const source = fs.existsSync(mdxPath)
|
||||
? fs.readFileSync(mdxPath, 'utf8')
|
||||
: fs.readFileSync(mdPath, 'utf8')
|
||||
: fs.readFileSync(mdPath, 'utf8');
|
||||
|
||||
// https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent
|
||||
if (process.platform === 'win32') {
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'esbuild.exe')
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(
|
||||
root,
|
||||
'node_modules',
|
||||
'esbuild',
|
||||
'esbuild.exe'
|
||||
);
|
||||
} else {
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'bin', 'esbuild')
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(
|
||||
root,
|
||||
'node_modules',
|
||||
'esbuild',
|
||||
'bin',
|
||||
'esbuild'
|
||||
);
|
||||
}
|
||||
|
||||
const toc: Toc = []
|
||||
const toc: Toc = [];
|
||||
|
||||
const { code, frontmatter } = await bundleMDX({
|
||||
source,
|
||||
@ -75,7 +91,7 @@ export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string |
|
||||
[remarkFootnotes, { inlineNotes: true }],
|
||||
remarkMath,
|
||||
remarkImgToJsx,
|
||||
]
|
||||
];
|
||||
options.rehypePlugins = [
|
||||
...(options.rehypePlugins ?? []),
|
||||
rehypeSlug,
|
||||
@ -84,17 +100,17 @@ export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string |
|
||||
[rehypeCitation, { path: path.join(root, 'data') }],
|
||||
[rehypePrismPlus, { ignoreMissing: true }],
|
||||
rehypePresetMinify,
|
||||
]
|
||||
return options
|
||||
];
|
||||
return options;
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.loader = {
|
||||
...options.loader,
|
||||
'.js': 'jsx',
|
||||
}
|
||||
return options
|
||||
};
|
||||
return options;
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
mdxSource: code,
|
||||
@ -106,34 +122,36 @@ export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string |
|
||||
...frontmatter,
|
||||
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
// 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
|
||||
if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const source = fs.readFileSync(file, 'utf8')
|
||||
const matterFile = matter(source)
|
||||
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const matterFile = matter(source);
|
||||
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter;
|
||||
if ('draft' in frontmatter && frontmatter.draft !== true) {
|
||||
allFrontMatter.push({
|
||||
...frontmatter,
|
||||
slug: formatSlug(fileName),
|
||||
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
|
||||
})
|
||||
date: frontmatter.date
|
||||
? 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));
|
||||
}
|
||||
|
@ -1,32 +1,38 @@
|
||||
import { visit, Parent } from 'unist-util-visit'
|
||||
import { visit, Parent } from 'unist-util-visit';
|
||||
|
||||
export default function remarkCodeTitles() {
|
||||
return (tree: Parent & { lang?: string }) =>
|
||||
visit(tree, 'code', (node: Parent & { lang?: string }, index, parent: Parent) => {
|
||||
const nodeLang = node.lang || ''
|
||||
let language = ''
|
||||
let title = ''
|
||||
visit(
|
||||
tree,
|
||||
'code',
|
||||
(node: Parent & { lang?: string }, index, parent: Parent) => {
|
||||
const nodeLang = node.lang || '';
|
||||
let language = '';
|
||||
let title = '';
|
||||
|
||||
if (nodeLang.includes(':')) {
|
||||
language = nodeLang.slice(0, nodeLang.search(':'))
|
||||
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length)
|
||||
language = nodeLang.slice(0, nodeLang.search(':'));
|
||||
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length);
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const className = 'remark-code-title'
|
||||
const className = 'remark-code-title';
|
||||
|
||||
const titleNode = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'div',
|
||||
attributes: [{ type: 'mdxJsxAttribute', name: 'className', value: className }],
|
||||
attributes: [
|
||||
{ type: 'mdxJsxAttribute', name: 'className', value: className },
|
||||
],
|
||||
children: [{ type: 'text', value: title }],
|
||||
data: { _xdmExplicitJsx: true },
|
||||
}
|
||||
};
|
||||
|
||||
parent.children.splice(index, 0, titleNode)
|
||||
node.lang = language
|
||||
})
|
||||
parent.children.splice(index, 0, titleNode);
|
||||
node.lang = language;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { VFile } from 'vfile'
|
||||
import { visit, Parent } from 'unist-util-visit'
|
||||
import { load } from 'js-yaml'
|
||||
import { VFile } from 'vfile';
|
||||
import { visit, Parent } from 'unist-util-visit';
|
||||
import { load } from 'js-yaml';
|
||||
|
||||
export default function extractFrontmatter() {
|
||||
return (tree: Parent, file: VFile) => {
|
||||
visit(tree, 'yaml', (node: Parent) => {
|
||||
//@ts-ignore
|
||||
file.data.frontmatter = load(node.value)
|
||||
})
|
||||
}
|
||||
file.data.frontmatter = load(node.value);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Literal } from 'unist'
|
||||
import { visit, Parent, Node } from 'unist-util-visit'
|
||||
import sizeOf from 'image-size'
|
||||
import fs from 'fs'
|
||||
import { Literal } from 'unist';
|
||||
import { visit, Parent, Node } from 'unist-util-visit';
|
||||
import sizeOf from 'image-size';
|
||||
import fs from 'fs';
|
||||
|
||||
type ImageNode = Parent & {
|
||||
url: string
|
||||
alt: string
|
||||
name: string
|
||||
attributes: (Literal & { name: string })[]
|
||||
}
|
||||
url: string;
|
||||
alt: string;
|
||||
name: string;
|
||||
attributes: (Literal & { name: string })[];
|
||||
};
|
||||
|
||||
export default function remarkImgToJsx() {
|
||||
return (tree: Node) => {
|
||||
@ -16,29 +16,40 @@ export default function remarkImgToJsx() {
|
||||
tree,
|
||||
// only visit p tags that contain an img element
|
||||
(node: Parent): node is Parent =>
|
||||
node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
|
||||
node.type === 'paragraph' &&
|
||||
node.children.some((n) => n.type === 'image'),
|
||||
(node: Parent) => {
|
||||
const imageNode = node.children.find((n) => n.type === 'image') as ImageNode
|
||||
const imageNode = node.children.find(
|
||||
(n) => n.type === 'image'
|
||||
) as ImageNode;
|
||||
|
||||
// only local files
|
||||
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
|
||||
;(imageNode.type = 'mdxJsxFlowElement'),
|
||||
(imageNode.type = 'mdxJsxFlowElement'),
|
||||
(imageNode.name = 'Image'),
|
||||
(imageNode.attributes = [
|
||||
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
|
||||
{ type: 'mdxJsxAttribute', name: 'src', value: imageNode.url },
|
||||
{ type: 'mdxJsxAttribute', name: 'width', value: dimensions.width },
|
||||
{ type: 'mdxJsxAttribute', name: 'height', value: dimensions.height },
|
||||
])
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'width',
|
||||
value: dimensions.width,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'height',
|
||||
value: dimensions.height,
|
||||
},
|
||||
]);
|
||||
|
||||
// Change node type from p to div to avoid nesting error
|
||||
node.type = 'div'
|
||||
node.children = [imageNode]
|
||||
node.type = 'div';
|
||||
node.children = [imageNode];
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
//@ts-nocheck
|
||||
import { Parent } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { slug } from 'github-slugger'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import { Parent } from 'unist';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { slug } from 'github-slugger';
|
||||
import { toString } from 'mdast-util-to-string';
|
||||
|
||||
export default function remarkTocHeadings(options) {
|
||||
return (tree: Parent) =>
|
||||
visit(tree, 'heading', (node) => {
|
||||
const textContent = toString(node)
|
||||
const textContent = toString(node);
|
||||
options.exportRef.push({
|
||||
value: textContent,
|
||||
url: '#' + slug(textContent),
|
||||
depth: node.depth,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
36
lib/tags.ts
36
lib/tags.ts
@ -1,32 +1,32 @@
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import path from 'path'
|
||||
import { getFiles } from './mdx'
|
||||
import kebabCase from './utils/kebabCase'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
import fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import path from 'path';
|
||||
import { getFiles } from './mdx';
|
||||
import kebabCase from './utils/kebabCase';
|
||||
|
||||
const root = process.cwd()
|
||||
const root = process.cwd();
|
||||
|
||||
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`
|
||||
files.forEach((file) => {
|
||||
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
|
||||
const matterFile = matter(source)
|
||||
const data = matterFile.data as PostFrontMatter
|
||||
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8');
|
||||
const matterFile = matter(source);
|
||||
const data = matterFile.data as PostFrontMatter;
|
||||
if (data.tags && data.draft !== true) {
|
||||
data.tags.forEach((tag) => {
|
||||
const formattedTag = kebabCase(tag)
|
||||
const formattedTag = kebabCase(tag);
|
||||
if (formattedTag in tagCount) {
|
||||
tagCount[formattedTag] += 1
|
||||
tagCount[formattedTag] += 1;
|
||||
} else {
|
||||
tagCount[formattedTag] = 1
|
||||
tagCount[formattedTag] = 1;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return tagCount
|
||||
return tagCount;
|
||||
}
|
||||
|
@ -1,23 +1,33 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const pipe =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduce((v, f) => f(v), x)
|
||||
fns.reduce((v, f) => f(v), x);
|
||||
|
||||
const flattenArray = (input) =>
|
||||
input.reduce((acc, item) => [...acc, ...(Array.isArray(item) ? item : [item])], [])
|
||||
input.reduce(
|
||||
(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) => {
|
||||
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
|
||||
}
|
||||
return fs.statSync(fullPath).isFile()
|
||||
? fullPath
|
||||
: getAllFilesRecursively(fullPath);
|
||||
};
|
||||
|
||||
const pathJoinPrefix = (prefix: string) => (extraPath: string) => path.join(prefix, extraPath)
|
||||
const pathJoinPrefix = (prefix: string) => (extraPath: string) =>
|
||||
path.join(prefix, extraPath);
|
||||
|
||||
const getAllFilesRecursively = (folder: string): string[] =>
|
||||
pipe(fs.readdirSync, map(pipe(pathJoinPrefix(folder), walkDir)), flattenArray)(folder)
|
||||
pipe(
|
||||
fs.readdirSync,
|
||||
map(pipe(pathJoinPrefix(folder), walkDir)),
|
||||
flattenArray
|
||||
)(folder);
|
||||
|
||||
export default getAllFilesRecursively
|
||||
export default getAllFilesRecursively;
|
||||
|
@ -1,14 +1,14 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
const { replace } = ''
|
||||
const { replace } = '';
|
||||
|
||||
// escape
|
||||
const ca = /[&<>'"]/g
|
||||
const ca = /[&<>'"]/g;
|
||||
|
||||
const esca = {
|
||||
'&': '&',
|
||||
@ -9,8 +9,8 @@ const esca = {
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"',
|
||||
}
|
||||
const pe = (m: keyof typeof esca) => esca[m]
|
||||
};
|
||||
const pe = (m: keyof typeof esca) => esca[m];
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* 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);
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Link from '@/components/Link'
|
||||
import Link from '@/components/Link';
|
||||
|
||||
export default function FourZeroFour() {
|
||||
return (
|
||||
@ -12,7 +12,9 @@ export default function FourZeroFour() {
|
||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
||||
Sorry we couldn't find this page.
|
||||
</p>
|
||||
<p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
|
||||
<p className="mb-8">
|
||||
But dont worry, you can find plenty of other things on our homepage.
|
||||
</p>
|
||||
<Link href="/">
|
||||
<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
|
||||
@ -20,5 +22,5 @@ export default function FourZeroFour() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import '@/css/tailwind.css'
|
||||
import '@/css/prism.css'
|
||||
import 'katex/dist/katex.css'
|
||||
import '@/css/tailwind.css';
|
||||
import '@/css/prism.css';
|
||||
import 'katex/dist/katex.css';
|
||||
|
||||
import '@fontsource/inter/variable.css'
|
||||
import '@fontsource/inter/variable.css';
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import type { AppProps } from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import Analytics from '@/components/analytics'
|
||||
import LayoutWrapper from '@/components/LayoutWrapper'
|
||||
import { ClientReload } from '@/components/ClientReload'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import Analytics from '@/components/analytics';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper';
|
||||
import { ClientReload } from '@/components/ClientReload';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isSocket = process.env.SOCKET
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const isSocket = process.env.SOCKET;
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
@ -28,5 +28,5 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
<Component {...pageProps} />
|
||||
</LayoutWrapper>
|
||||
</ThemeProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en" className="scroll-smooth">
|
||||
<Head>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/static/favicons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
@ -19,10 +23,22 @@ class MyDocument extends Document {
|
||||
href="/static/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/static/favicons/safari-pinned-tab.svg"
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fff"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#000"
|
||||
/>
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
</Head>
|
||||
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
||||
@ -30,8 +46,8 @@ class MyDocument extends Document {
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
||||
export default MyDocument;
|
||||
|
@ -1,21 +1,25 @@
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { getFileBySlug } from '@/lib/mdx'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents';
|
||||
import { getFileBySlug } from '@/lib/mdx';
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next';
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
|
||||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout';
|
||||
|
||||
// @ts-ignore
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }
|
||||
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter };
|
||||
}> = async () => {
|
||||
const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', ['default'])
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
return { props: { authorDetails: { mdxSource, frontMatter } } }
|
||||
}
|
||||
const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', [
|
||||
'default',
|
||||
]);
|
||||
const { mdxSource, frontMatter } = authorDetails;
|
||||
return { props: { authorDetails: { mdxSource, frontMatter } } };
|
||||
};
|
||||
|
||||
export default function About({ authorDetails }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
export default function About({
|
||||
authorDetails,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { mdxSource, frontMatter } = authorDetails;
|
||||
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
@ -23,5 +27,5 @@ export default function About({ authorDetails }: InferGetStaticPropsType<typeof
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
||||
return res.status(400).json({ error: 'Email is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const API_KEY = process.env.BUTTONDOWN_API_KEY
|
||||
const buttondownRoute = `${process.env.BUTTONDOWN_API_URL}subscribers`
|
||||
const API_KEY = process.env.BUTTONDOWN_API_KEY;
|
||||
const buttondownRoute = `${process.env.BUTTONDOWN_API_URL}subscribers`;
|
||||
const response = await fetch(buttondownRoute, {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
@ -19,14 +19,16 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
return res.status(500).json({ error: `There was an error subscribing to the list.` })
|
||||
return res
|
||||
.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) {
|
||||
return res.status(500).json({ error: error.message || error.toString() })
|
||||
return res.status(500).json({ error: error.message || error.toString() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
||||
return res.status(400).json({ error: 'Email is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const FORM_ID = process.env.CONVERTKIT_FORM_ID
|
||||
const API_KEY = process.env.CONVERTKIT_API_KEY
|
||||
const API_URL = process.env.CONVERTKIT_API_URL
|
||||
const FORM_ID = process.env.CONVERTKIT_FORM_ID;
|
||||
const API_KEY = process.env.CONVERTKIT_API_KEY;
|
||||
const API_URL = process.env.CONVERTKIT_API_URL;
|
||||
|
||||
// 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`, {
|
||||
body: JSON.stringify(data),
|
||||
@ -22,16 +22,16 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
return res.status(400).json({
|
||||
error: `There was an error subscribing to the list.`,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({ error: '' })
|
||||
return res.status(201).json({ error: '' });
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message || error.toString() })
|
||||
return res.status(500).json({ error: error.message || error.toString() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
||||
return res.status(400).json({ error: 'Email is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const API_KEY = process.env.KLAVIYO_API_KEY
|
||||
const LIST_ID = process.env.KLAVIYO_LIST_ID
|
||||
const API_KEY = process.env.KLAVIYO_API_KEY;
|
||||
const LIST_ID = process.env.KLAVIYO_LIST_ID;
|
||||
const response = await fetch(
|
||||
`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 }],
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
if (response.status >= 400) {
|
||||
return res.status(400).json({
|
||||
error: `There was an error subscribing to the list.`,
|
||||
})
|
||||
});
|
||||
}
|
||||
return res.status(201).json({ error: '' })
|
||||
return res.status(201).json({ error: '' });
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message || error.toString() })
|
||||
return res.status(500).json({ error: error.message || error.toString() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import mailchimp from '@mailchimp/mailchimp_marketing'
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import mailchimp from '@mailchimp/mailchimp_marketing';
|
||||
|
||||
mailchimp.setConfig({
|
||||
apiKey: process.env.MAILCHIMP_API_KEY,
|
||||
server: process.env.MAILCHIMP_API_SERVER, // E.g. us1
|
||||
})
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
||||
return res.status(400).json({ error: 'Email is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
||||
email_address: email,
|
||||
status: 'subscribed',
|
||||
})
|
||||
return res.status(201).json({ error: '' })
|
||||
});
|
||||
return res.status(201).json({ error: '' });
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message || error.toString() })
|
||||
return res.status(500).json({ error: error.message || error.toString() });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { ComponentProps } from 'react'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import ListLayout from '@/layouts/ListLayout';
|
||||
import { PageSEO } from '@/components/SEO';
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export const POSTS_PER_PAGE = 5
|
||||
export const POSTS_PER_PAGE = 5;
|
||||
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
posts: ComponentProps<typeof ListLayout>['posts']
|
||||
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
|
||||
pagination: ComponentProps<typeof ListLayout>['pagination']
|
||||
posts: ComponentProps<typeof ListLayout>['posts'];
|
||||
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts'];
|
||||
pagination: ComponentProps<typeof ListLayout>['pagination'];
|
||||
}> = async () => {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
|
||||
const posts = await getAllFilesFrontMatter('blog');
|
||||
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE);
|
||||
const pagination = {
|
||||
currentPage: 1,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
};
|
||||
|
||||
return { props: { initialDisplayPosts, posts, pagination } }
|
||||
}
|
||||
return { props: { initialDisplayPosts, posts, pagination } };
|
||||
};
|
||||
|
||||
export default function Blog({
|
||||
posts,
|
||||
@ -29,7 +29,10 @@ export default function Blog({
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
|
||||
<PageSEO
|
||||
title={`Blog - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description}
|
||||
/>
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
@ -37,5 +40,5 @@ export default function Blog({
|
||||
title="All Posts"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,22 @@
|
||||
import fs from 'fs'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import generateRss from '@/lib/generate-rss'
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { Toc } from 'types/Toc'
|
||||
import fs from 'fs';
|
||||
import PageTitle from '@/components/PageTitle';
|
||||
import generateRss from '@/lib/generate-rss';
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents';
|
||||
import {
|
||||
formatSlug,
|
||||
getAllFilesFrontMatter,
|
||||
getFileBySlug,
|
||||
getFiles,
|
||||
} 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() {
|
||||
const posts = getFiles('blog')
|
||||
const posts = getFiles('blog');
|
||||
return {
|
||||
paths: posts.map((p) => ({
|
||||
params: {
|
||||
@ -19,34 +24,38 @@ export async function getStaticPaths() {
|
||||
},
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }
|
||||
authorDetails: AuthorFrontMatter[]
|
||||
prev?: { slug: string; title: string }
|
||||
next?: { slug: string; title: string }
|
||||
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter };
|
||||
authorDetails: AuthorFrontMatter[];
|
||||
prev?: { slug: string; title: string };
|
||||
next?: { slug: string; title: string };
|
||||
}> = async ({ params }) => {
|
||||
const slug = (params.slug as string[]).join('/')
|
||||
const allPosts = await getAllFilesFrontMatter('blog')
|
||||
const postIndex = allPosts.findIndex((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 post = await getFileBySlug<PostFrontMatter>('blog', slug)
|
||||
const slug = (params.slug as string[]).join('/');
|
||||
const allPosts = await getAllFilesFrontMatter('blog');
|
||||
const postIndex = allPosts.findIndex(
|
||||
(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 post = await getFileBySlug<PostFrontMatter>('blog', slug);
|
||||
// @ts-ignore
|
||||
const authorList = post.frontMatter.authors || ['default']
|
||||
const authorList = post.frontMatter.authors || ['default'];
|
||||
const authorPromise = authorList.map(async (author) => {
|
||||
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
|
||||
return authorResults.frontMatter
|
||||
})
|
||||
const authorDetails = await Promise.all(authorPromise)
|
||||
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [
|
||||
author,
|
||||
]);
|
||||
return authorResults.frontMatter;
|
||||
});
|
||||
const authorDetails = await Promise.all(authorPromise);
|
||||
|
||||
// rss
|
||||
if (allPosts.length > 0) {
|
||||
const rss = generateRss(allPosts)
|
||||
fs.writeFileSync('./public/feed.xml', rss)
|
||||
const rss = generateRss(allPosts);
|
||||
fs.writeFileSync('./public/feed.xml', rss);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -56,8 +65,8 @@ export const getStaticProps: GetStaticProps<{
|
||||
prev,
|
||||
next,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default function Blog({
|
||||
post,
|
||||
@ -65,7 +74,7 @@ export default function Blog({
|
||||
prev,
|
||||
next,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { mdxSource, toc, frontMatter } = post
|
||||
const { mdxSource, toc, frontMatter } = post;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -90,5 +99,5 @@ export default function Blog({
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,42 +1,42 @@
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { POSTS_PER_PAGE } from '../../blog'
|
||||
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { PageSEO } from '@/components/SEO';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx';
|
||||
import ListLayout from '@/layouts/ListLayout';
|
||||
import { POSTS_PER_PAGE } from '../../blog';
|
||||
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
|
||||
const totalPosts = await getAllFilesFrontMatter('blog')
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||
const totalPosts = await getAllFilesFrontMatter('blog');
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE);
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
||||
params: { page: (i + 1).toString() },
|
||||
}))
|
||||
}));
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
posts: PostFrontMatter[]
|
||||
initialDisplayPosts: PostFrontMatter[]
|
||||
pagination: { currentPage: number; totalPages: number }
|
||||
posts: PostFrontMatter[];
|
||||
initialDisplayPosts: PostFrontMatter[];
|
||||
pagination: { currentPage: number; totalPages: number };
|
||||
}> = async (context) => {
|
||||
const {
|
||||
params: { page },
|
||||
} = context
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
const pageNumber = parseInt(page as string)
|
||||
} = context;
|
||||
const posts = await getAllFilesFrontMatter('blog');
|
||||
const pageNumber = parseInt(page as string);
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
POSTS_PER_PAGE * pageNumber
|
||||
)
|
||||
);
|
||||
const pagination = {
|
||||
currentPage: pageNumber,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
props: {
|
||||
@ -44,8 +44,8 @@ export const getStaticProps: GetStaticProps<{
|
||||
initialDisplayPosts,
|
||||
pagination,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default function PostPage({
|
||||
posts,
|
||||
@ -54,7 +54,10 @@ export default function PostPage({
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
||||
<PageSEO
|
||||
title={siteMetadata.title}
|
||||
description={siteMetadata.description}
|
||||
/>
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
@ -62,5 +65,5 @@ export default function PostPage({
|
||||
title="All Posts"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,25 +1,32 @@
|
||||
import Link from '@/components/Link'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import NewsletterForm from '@/components/NewsletterForm'
|
||||
import Link from '@/components/Link';
|
||||
import { PageSEO } from '@/components/SEO';
|
||||
import Tag from '@/components/Tag';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx';
|
||||
import formatDate from '@/lib/utils/formatDate';
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
import NewsletterForm from '@/components/NewsletterForm';
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
const MAX_DISPLAY = 5;
|
||||
|
||||
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
posts: PostFrontMatter[];
|
||||
}> = async () => {
|
||||
const posts = await getAllFilesFrontMatter('blog');
|
||||
|
||||
return { props: { posts } }
|
||||
}
|
||||
return { props: { posts } };
|
||||
};
|
||||
|
||||
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
export default function Home({
|
||||
posts,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
||||
<PageSEO
|
||||
title={siteMetadata.title}
|
||||
description={siteMetadata.description}
|
||||
/>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<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">
|
||||
@ -32,7 +39,7 @@ export default function Home({ posts }: InferGetStaticPropsType<typeof getStatic
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{!posts.length && '没有找到文章。 😭'}
|
||||
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
|
||||
const { slug, date, title, summary, tags } = frontMatter
|
||||
const { slug, date, title, summary, tags } = frontMatter;
|
||||
return (
|
||||
<li key={slug} className="py-12">
|
||||
<article>
|
||||
@ -49,8 +56,7 @@ export default function Home({ posts }: InferGetStaticPropsType<typeof getStatic
|
||||
<h2 className="text-2xl font-bold leading-8 tracking-tight">
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
className="text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</Link>
|
||||
</h2>
|
||||
@ -68,8 +74,7 @@ export default function Home({ posts }: InferGetStaticPropsType<typeof getStatic
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Read "${title}"`}
|
||||
>
|
||||
aria-label={`Read "${title}"`}>
|
||||
Read more →
|
||||
</Link>
|
||||
</div>
|
||||
@ -77,7 +82,7 @@ export default function Home({ posts }: InferGetStaticPropsType<typeof getStatic
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
@ -86,8 +91,7 @@ export default function Home({ posts }: InferGetStaticPropsType<typeof getStatic
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="all posts"
|
||||
>
|
||||
aria-label="all posts">
|
||||
All Posts →
|
||||
</Link>
|
||||
</div>
|
||||
@ -98,5 +102,5 @@ export default function Home({ posts }: InferGetStaticPropsType<typeof getStatic
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import projectsData from '@/data/projectsData'
|
||||
import Card from '@/components/Card'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import projectsData from '@/data/projectsData';
|
||||
import Card from '@/components/Card';
|
||||
import { PageSEO } from '@/components/SEO';
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
|
||||
<PageSEO
|
||||
title={`Projects - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description}
|
||||
/>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<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">
|
||||
@ -31,5 +34,5 @@ export default function Projects() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,22 +1,29 @@
|
||||
import Link from '@/components/Link'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllTags } from '@/lib/tags'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import Link from '@/components/Link';
|
||||
import { PageSEO } from '@/components/SEO';
|
||||
import Tag from '@/components/Tag';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import { getAllTags } from '@/lib/tags';
|
||||
import kebabCase from '@/lib/utils/kebabCase';
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next';
|
||||
|
||||
export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
|
||||
const tags = await getAllTags('blog')
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
tags: Record<string, number>;
|
||||
}> = async () => {
|
||||
const tags = await getAllTags('blog');
|
||||
|
||||
return { props: { tags } }
|
||||
}
|
||||
return { props: { tags } };
|
||||
};
|
||||
|
||||
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||
export default function Tags({
|
||||
tags,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a]);
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
|
||||
<PageSEO
|
||||
title={`Tags - ${siteMetadata.author}`}
|
||||
description="Things I blog about"
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-start divide-y divide-gray-200 dark:divide-gray-700 md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6 md:divide-y-0">
|
||||
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
|
||||
<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">
|
||||
@ -31,15 +38,14 @@ export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticP
|
||||
<Tag text={t} />
|
||||
<Link
|
||||
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]})`}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { TagSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import generateRss from '@/lib/generate-rss'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import { getAllTags } from '@/lib/tags'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
import fs from 'fs'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import path from 'path'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { TagSEO } from '@/components/SEO';
|
||||
import siteMetadata from '@/data/siteMetadata';
|
||||
import ListLayout from '@/layouts/ListLayout';
|
||||
import generateRss from '@/lib/generate-rss';
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx';
|
||||
import { getAllTags } from '@/lib/tags';
|
||||
import kebabCase from '@/lib/utils/kebabCase';
|
||||
import fs from 'fs';
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next';
|
||||
import path from 'path';
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter';
|
||||
|
||||
const root = process.cwd()
|
||||
const root = process.cwd();
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const tags = await getAllTags('blog')
|
||||
const tags = await getAllTags('blog');
|
||||
|
||||
return {
|
||||
paths: Object.keys(tags).map((tag) => ({
|
||||
@ -22,32 +22,37 @@ export async function getStaticPaths() {
|
||||
},
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
|
||||
context
|
||||
) => {
|
||||
const tag = context.params.tag as string
|
||||
const allPosts = await getAllFilesFrontMatter('blog')
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
posts: PostFrontMatter[];
|
||||
tag: string;
|
||||
}> = async (context) => {
|
||||
const tag = context.params.tag as string;
|
||||
const allPosts = await getAllFilesFrontMatter('blog');
|
||||
const filteredPosts = allPosts.filter(
|
||||
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
|
||||
)
|
||||
(post) =>
|
||||
post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
|
||||
);
|
||||
|
||||
// rss
|
||||
if (filteredPosts.length > 0) {
|
||||
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
|
||||
const rssPath = path.join(root, 'public', 'tags', tag)
|
||||
fs.mkdirSync(rssPath, { recursive: true })
|
||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
||||
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`);
|
||||
const rssPath = path.join(root, 'public', 'tags', tag);
|
||||
fs.mkdirSync(rssPath, { recursive: true });
|
||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss);
|
||||
}
|
||||
|
||||
return { props: { posts: filteredPosts, tag } }
|
||||
}
|
||||
return { props: { posts: filteredPosts, tag } };
|
||||
};
|
||||
|
||||
export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
export default function Tag({
|
||||
posts,
|
||||
tag,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
// 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 (
|
||||
<>
|
||||
<TagSEO
|
||||
@ -56,5 +61,5 @@ export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getSt
|
||||
/>
|
||||
<ListLayout posts={posts} title={title} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,36 +1,39 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const inquirer = require('inquirer')
|
||||
const dedent = require('dedent')
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const inquirer = require('inquirer');
|
||||
const dedent = require('dedent');
|
||||
|
||||
const root = process.cwd()
|
||||
const root = process.cwd();
|
||||
|
||||
const getAuthors = () => {
|
||||
const authorPath = path.join(root, 'data', 'authors')
|
||||
const authorList = fs.readdirSync(authorPath).map((filename) => path.parse(filename).name)
|
||||
return authorList
|
||||
}
|
||||
const authorPath = path.join(root, 'data', 'authors');
|
||||
const authorList = fs
|
||||
.readdirSync(authorPath)
|
||||
.map((filename) => path.parse(filename).name);
|
||||
return authorList;
|
||||
};
|
||||
|
||||
const getLayouts = () => {
|
||||
const layoutPath = path.join(root, 'layouts')
|
||||
const layoutPath = path.join(root, 'layouts');
|
||||
const layoutList = fs
|
||||
.readdirSync(layoutPath)
|
||||
.map((filename) => path.parse(filename).name)
|
||||
.filter((file) => file.toLowerCase().includes('post'))
|
||||
return layoutList
|
||||
}
|
||||
.filter((file) => file.toLowerCase().includes('post'));
|
||||
return layoutList;
|
||||
};
|
||||
|
||||
const genFrontMatter = (answers) => {
|
||||
let d = new Date()
|
||||
let d = new Date();
|
||||
const date = [
|
||||
d.getFullYear(),
|
||||
('0' + (d.getMonth() + 1)).slice(-2),
|
||||
('0' + d.getDate()).slice(-2),
|
||||
].join('-')
|
||||
const tagArray = answers.tags.split(',')
|
||||
tagArray.forEach((tag, index) => (tagArray[index] = tag.trim()))
|
||||
const tags = "'" + tagArray.join("','") + "'"
|
||||
const authorArray = answers.authors.length > 0 ? "'" + answers.authors.join("','") + "'" : ''
|
||||
].join('-');
|
||||
const tagArray = answers.tags.split(',');
|
||||
tagArray.forEach((tag, index) => (tagArray[index] = tag.trim()));
|
||||
const tags = "'" + tagArray.join("','") + "'";
|
||||
const authorArray =
|
||||
answers.authors.length > 0 ? "'" + answers.authors.join("','") + "'" : '';
|
||||
|
||||
let frontMatter = dedent`---
|
||||
title: ${answers.title ? answers.title : 'Untitled'}
|
||||
@ -41,16 +44,16 @@ const genFrontMatter = (answers) => {
|
||||
images: []
|
||||
layout: ${answers.layout}
|
||||
canonicalUrl: ${answers.canonicalUrl}
|
||||
`
|
||||
`;
|
||||
|
||||
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
|
||||
.prompt([
|
||||
@ -105,24 +108,25 @@ inquirer
|
||||
.toLowerCase()
|
||||
.replace(/[^a-zA-Z0-9 ]/g, '')
|
||||
.replace(/ /g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
const frontMatter = genFrontMatter(answers)
|
||||
if (!fs.existsSync('data/blog')) fs.mkdirSync('data/blog', { recursive: true })
|
||||
.replace(/-+/g, '-');
|
||||
const frontMatter = genFrontMatter(answers);
|
||||
if (!fs.existsSync('data/blog'))
|
||||
fs.mkdirSync('data/blog', { recursive: true });
|
||||
const filePath = `data/blog/${fileName ? fileName : 'untitled'}.${
|
||||
answers.extension ? answers.extension : 'md'
|
||||
}`
|
||||
}`;
|
||||
fs.writeFile(filePath, frontMatter, { flag: 'wx' }, (err) => {
|
||||
if (err) {
|
||||
throw err
|
||||
throw err;
|
||||
} else {
|
||||
console.log(`Blog post generated successfully at ${filePath}`)
|
||||
console.log(`Blog post generated successfully at ${filePath}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
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 {
|
||||
console.log('Something went wrong, sorry!')
|
||||
console.log('Something went wrong, sorry!');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
const fs = require('fs')
|
||||
const globby = require('globby')
|
||||
const matter = require('gray-matter')
|
||||
const prettier = require('prettier')
|
||||
const siteMetadata = require('../data/siteMetadata')
|
||||
const fs = require('fs');
|
||||
const globby = require('globby');
|
||||
const matter = require('gray-matter');
|
||||
const prettier = require('prettier');
|
||||
const siteMetadata = require('../data/siteMetadata');
|
||||
|
||||
;(async () => {
|
||||
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
|
||||
(async () => {
|
||||
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js');
|
||||
const pages = await globby([
|
||||
'pages/*.js',
|
||||
'pages/*.tsx',
|
||||
@ -15,7 +15,7 @@ const siteMetadata = require('../data/siteMetadata')
|
||||
'!pages/_*.js',
|
||||
'!pages/_*.tsx',
|
||||
'!pages/api',
|
||||
])
|
||||
]);
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@ -24,13 +24,13 @@ const siteMetadata = require('../data/siteMetadata')
|
||||
.map((page) => {
|
||||
// Exclude drafts from the sitemap
|
||||
if (page.search('.md') >= 1 && fs.existsSync(page)) {
|
||||
const source = fs.readFileSync(page, 'utf8')
|
||||
const fm = matter(source)
|
||||
const source = fs.readFileSync(page, 'utf8');
|
||||
const fm = matter(source);
|
||||
if (fm.data.draft) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (fm.data.canonicalUrl) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
const path = page
|
||||
@ -41,26 +41,29 @@ const siteMetadata = require('../data/siteMetadata')
|
||||
.replace('.tsx', '')
|
||||
.replace('.mdx', '')
|
||||
.replace('.md', '')
|
||||
.replace('/feed.xml', '')
|
||||
const route = path === '/index' ? '' : path
|
||||
if (page.search('pages/404.') > -1 || page.search(`pages/blog/[...slug].`) > -1) {
|
||||
return
|
||||
.replace('/feed.xml', '');
|
||||
const route = path === '/index' ? '' : path;
|
||||
if (
|
||||
page.search('pages/404.') > -1 ||
|
||||
page.search(`pages/blog/[...slug].`) > -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return `
|
||||
<url>
|
||||
<loc>${siteMetadata.siteUrl}${route}</loc>
|
||||
</url>
|
||||
`
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</urlset>
|
||||
`
|
||||
`;
|
||||
|
||||
const formatted = prettier.format(sitemap, {
|
||||
...prettierConfig,
|
||||
parser: 'html',
|
||||
})
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
fs.writeFileSync('public/sitemap.xml', formatted)
|
||||
})()
|
||||
fs.writeFileSync('public/sitemap.xml', formatted);
|
||||
})();
|
||||
|
@ -5,48 +5,58 @@
|
||||
// The app listens to the event and triggers a client-side router refresh
|
||||
// see components/ClientReload.js
|
||||
|
||||
const chalk = require('chalk')
|
||||
const chokidar = require('chokidar')
|
||||
const program = require('commander')
|
||||
const http = require('http')
|
||||
const SocketIO = require('socket.io')
|
||||
const express = require('express')
|
||||
const spawn = require('child_process').spawn
|
||||
const next = require('next')
|
||||
const path = require('path')
|
||||
const { parse } = require('url')
|
||||
const chalk = require('chalk');
|
||||
const chokidar = require('chokidar');
|
||||
const program = require('commander');
|
||||
const http = require('http');
|
||||
const SocketIO = require('socket.io');
|
||||
const express = require('express');
|
||||
const spawn = require('child_process').spawn;
|
||||
const next = require('next');
|
||||
const path = require('path');
|
||||
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
|
||||
.option('-r, --root [dir]', 'root directory of your nextjs app')
|
||||
.option('-s, --script [path]', 'path to the script you want to trigger on a watcher event', false)
|
||||
.option(
|
||||
'-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(
|
||||
'-e, --event [name]',
|
||||
`name of event to watch, defaults to ${defaultWatchEvent}`,
|
||||
defaultWatchEvent
|
||||
)
|
||||
.option('-p, --polling [name]', `use polling for the watcher, defaults to false`, false)
|
||||
.parse(process.argv)
|
||||
.option(
|
||||
'-p, --polling [name]',
|
||||
`use polling for the watcher, defaults to false`,
|
||||
false
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
const shell = process.env.SHELL
|
||||
const app = next({ dev: true, dir: program.root || process.cwd() })
|
||||
const port = parseInt(process.env.PORT, 10) || 3000
|
||||
const handle = app.getRequestHandler()
|
||||
const shell = process.env.SHELL;
|
||||
const app = next({ dev: true, dir: program.root || process.cwd() });
|
||||
const port = parseInt(process.env.PORT, 10) || 3000;
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
app.prepare().then(() => {
|
||||
// if directories are provided, watch them for changes and trigger reload
|
||||
if (program.args.length > 0) {
|
||||
chokidar
|
||||
.watch(program.args, { usePolling: Boolean(program.polling) })
|
||||
.on(program.event, async (filePathContext, eventContext = defaultWatchEvent) => {
|
||||
.on(
|
||||
program.event,
|
||||
async (filePathContext, eventContext = defaultWatchEvent) => {
|
||||
// Emit changes via socketio
|
||||
io.sockets.emit('reload', filePathContext)
|
||||
app.server.hotReloader.send('building')
|
||||
io.sockets.emit('reload', filePathContext);
|
||||
app.server.hotReloader.send('building');
|
||||
|
||||
if (program.command) {
|
||||
// Use spawn here so that we can pipe stdio from the command without buffering
|
||||
@ -61,60 +71,64 @@ app.prepare().then(() => {
|
||||
{
|
||||
stdio: 'inherit',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (program.script) {
|
||||
try {
|
||||
// find the path of your --script script
|
||||
const scriptPath = path.join(process.cwd(), program.script.toString())
|
||||
const scriptPath = path.join(
|
||||
process.cwd(),
|
||||
program.script.toString()
|
||||
);
|
||||
|
||||
// require your --script script
|
||||
const executeFile = require(scriptPath)
|
||||
const executeFile = require(scriptPath);
|
||||
|
||||
// run the exported function from your --script script
|
||||
executeFile(filePathContext, eventContext)
|
||||
executeFile(filePathContext, eventContext);
|
||||
} catch (e) {
|
||||
console.error('Remote script failed')
|
||||
console.error(e)
|
||||
return e
|
||||
console.error('Remote script failed');
|
||||
console.error(e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
app.server.hotReloader.send('reloadPage')
|
||||
})
|
||||
app.server.hotReloader.send('reloadPage');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// create an express server
|
||||
const expressApp = express()
|
||||
const server = http.createServer(expressApp)
|
||||
const expressApp = express();
|
||||
const server = http.createServer(expressApp);
|
||||
|
||||
// watch files with socketIO
|
||||
const io = SocketIO(server)
|
||||
const io = SocketIO(server);
|
||||
|
||||
// special handling for mdx reload route
|
||||
const reloadRoute = express.Router()
|
||||
reloadRoute.use(express.json())
|
||||
const reloadRoute = express.Router();
|
||||
reloadRoute.use(express.json());
|
||||
reloadRoute.all('/', (req, res) => {
|
||||
// log message if present
|
||||
const msg = req.body.message
|
||||
const color = req.body.color
|
||||
msg && console.log(color ? chalk[color](msg) : msg)
|
||||
const msg = req.body.message;
|
||||
const color = req.body.color;
|
||||
msg && console.log(color ? chalk[color](msg) : msg);
|
||||
|
||||
// reload the nextjs app
|
||||
app.server.hotReloader.send('building')
|
||||
app.server.hotReloader.send('reloadPage')
|
||||
res.end('Reload initiated')
|
||||
})
|
||||
app.server.hotReloader.send('building');
|
||||
app.server.hotReloader.send('reloadPage');
|
||||
res.end('Reload initiated');
|
||||
});
|
||||
|
||||
expressApp.use('/__next_reload', reloadRoute)
|
||||
expressApp.use('/__next_reload', reloadRoute);
|
||||
|
||||
// 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
|
||||
server.listen(port, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`> Ready on http://localhost:${port}`)
|
||||
})
|
||||
})
|
||||
if (err) throw err;
|
||||
console.log(`> Ready on http://localhost:${port}`);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user