refactor: 改用 TypeScript。close #1.
This commit is contained in:
parent
11b9017a07
commit
1dfd5e5271
57
README.md
57
README.md
@ -1,8 +1,13 @@
|
|||||||
![tailwind-nextjs-banner](/public/static/images/twitter-card.png)
|
![tailwind-nextjs-banner](/public/static/images/twitter-card.png)
|
||||||
|
|
||||||
# Ivan Li's Blog
|
# Tailwind Nextjs Starter Blog
|
||||||
|
|
||||||
[![Build Status](https://ci.ivanli.cc/api/badges/Ivan/tailwind-nextjs-blog/status.svg)](https://ci.ivanli.cc/Ivan/tailwind-nextjs-blog)
|
[![GitHub Repo stars](https://img.shields.io/github/stars/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/stargazers/)
|
||||||
|
[![GitHub forks](https://img.shields.io/github/forks/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/network/)
|
||||||
|
[![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftimlrxx)](https://twitter.com/timlrxx)
|
||||||
|
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/timlrx)](https://github.com/sponsors/timlrx)
|
||||||
|
|
||||||
|
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/timlrx/tailwind-nextjs-starter-blog)
|
||||||
|
|
||||||
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature-rich Next.js markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
|
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature-rich Next.js markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
|
||||||
|
|
||||||
@ -26,32 +31,13 @@ Feature request? Check the past discussions to see if it has been brought up pre
|
|||||||
- [irvin.dev](https://www.irvin.dev/) - Irvin Lin's personal site. Added YouTube embedding.
|
- [irvin.dev](https://www.irvin.dev/) - Irvin Lin's personal site. Added YouTube embedding.
|
||||||
- [the all JavaScript Blog](https://the-all-javascript-blog.vercel.app/) - a JavaScript enlightenment blog uses this
|
- [the all JavaScript Blog](https://the-all-javascript-blog.vercel.app/) - a JavaScript enlightenment blog uses this
|
||||||
- [KirillSo.com](https://www.kirillso.com/) - Personal blog & website.
|
- [KirillSo.com](https://www.kirillso.com/) - Personal blog & website.
|
||||||
- [ghali.dev](https://ghali.dev) - Cyril's Blog
|
|
||||||
- [DevBoy Blog](https://devboy.vercel.app/) - M.Reza's personal blog
|
- [DevBoy Blog](https://devboy.vercel.app/) - M.Reza's personal blog
|
||||||
- [slightlysharpe.com](https://slightlysharpe.com) - [Tincre's](https://tincre.com) main company blog
|
- [slightlysharpe.com](https://slightlysharpe.com) - [Tincre's](https://tincre.com) main company blog
|
||||||
- [blog.b00st.com](https://blog.b00st.com) - [b00st.com's](https://b00st.com) main music promotion blog
|
- [blog.b00st.com](https://blog.b00st.com) - [b00st.com's](https://b00st.com) main music promotion blog
|
||||||
- [astrosaurus.me](https://astrosaurus.me/) - Ephraim Atta-Duncan's Personal Blog
|
- [astrosaurus.me](https://astrosaurus.me/) - Ephraim Atta-Duncan's Personal Blog
|
||||||
- [dhanrajsp.me](https://dhanrajsp.me/) - Dhanraj's personal site and blog.
|
- [dhanrajsp.me](https://dhanrajsp.me/) - Dhanraj's personal site and blog.
|
||||||
- [blog.r00ks.io](https://blog.r00ks.io/) - Austin Rooks's personal blog ([source code](https://github.com/Austionian/blog.r00ks)).
|
- [blog.r00ks.io](https://blog.r00ks.io/) - Austin Rooks's personal blog ([source code](https://github.com/Austionian/blog.r00ks)).
|
||||||
- [honghong.me](https://honghong.me) - Tszhong's personal website ([source code](https://github.com/tszhong0411/home))
|
|
||||||
- [alfoncode.com](https://alfoncode.com) - Alfonso García's personar website. Customized design ([source code](https://github.com/alfoncode/personal-web))
|
- [alfoncode.com](https://alfoncode.com) - Alfonso García's personar website. Customized design ([source code](https://github.com/alfoncode/personal-web))
|
||||||
- [marceloformentao.dev](https://marceloformentao.dev) - Marcelo Formentão personal website ([source code](https://github.com/marceloavf/marceloformentao.dev)).
|
|
||||||
- [abiraja.com](https://www.abiraja.com/) - with a [runnable JS code snippet component!](https://www.abiraja.com/blog/querying-solana-blockchain)
|
|
||||||
- [einargudni.com](https://www.einargudni.com) - with a customized theme, command pallette and more ([source code](https://github.com/einargudnig/einargudni.com))
|
|
||||||
- [bpiggin.com](https://www.bpiggin.com) - Ben Piggin's personal blog
|
|
||||||
- [maqib.cn](https://maqib.cn) - A blog of Chinese front-end developers 狂奔小马的博客 ([源码](https://github.com/maqi1520/nextjs-tailwind-blog))
|
|
||||||
- [ambilena.com](https://ambilena.com/) - Electronic Music Blog & imprint for upcoming musicians.
|
|
||||||
- [kittan.ru](https://www.kittan.ru/) - Kittanb's personal blog about linux ([source code](https://github.com/kittanb/blog))
|
|
||||||
- [nchristopher.me](https://nchristopher.me) - Nicholas Christopher's personal website and blog ([source code](https://github.com/nchristopher/blog))
|
|
||||||
- [dalelarroder.com](https://dalelarroder.com) - Dale Larroder's personal website and blog ([source code](https://github.com/dlarroder/dalelarroder))
|
|
||||||
- [devahoy.com](https://devahoy.com) - Chai's personal blog (Thai language)
|
|
||||||
- [0xchai.io](https://0xchai.io) - Chai's personal blog
|
|
||||||
- [techipedia](https://techipedia.vercel.app) - Simple blogging progressive web app with custom installation button and top progress bar
|
|
||||||
- [reubence.com](https://reubence.com) - Reuben Rapose's Digital Garden
|
|
||||||
- [axolo.co/blog](https://axolo.co/blog) - Engineering management news & axolo.co updates (with image preview for article in the home page)
|
|
||||||
- [musing.vercel.app](https://musing.vercel.app/) - Parth Desai's personal blog ([source code](https://github.com/pycoder2000/blog))
|
|
||||||
- [onyourmental.com](https://www.onyourmental.com/) - [Curtis Warcup's](https://github.com/Cwarcup) website for the On Your Mental Podcast ([source code](https://github.com/Cwarcup/on-your-mental))
|
|
||||||
- [cwarcup.com](https://www.cwarcup.com/) - Curtis Warcup's personal website and blog ([source code](https://github.com/Cwarcup/personal-blog).
|
|
||||||
|
|
||||||
Using the template? Feel free to create a PR and add your blog to this list.
|
Using the template? Feel free to create a PR and add your blog to this list.
|
||||||
|
|
||||||
@ -81,7 +67,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
|||||||
- Blog templates
|
- Blog templates
|
||||||
- TOC component
|
- TOC component
|
||||||
- Support for nested routing of blog posts
|
- Support for nested routing of blog posts
|
||||||
- Newsletter component with support for mailchimp, buttondown, convertkit, klaviyo, revue, and emailoctopus
|
- Newsletter component with support for mailchimp, buttondown, convertkit and klaviyo
|
||||||
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
||||||
- Projects page
|
- Projects page
|
||||||
- Preconfigured security headers
|
- Preconfigured security headers
|
||||||
@ -98,27 +84,18 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
|||||||
|
|
||||||
## Quick Start Guide
|
## Quick Start Guide
|
||||||
|
|
||||||
1. Try installing the starter using the new [Pliny project CLI](https://github.com/timlrx/pliny):
|
1. JS (official support)
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i -g @pliny/cli
|
|
||||||
pliny new --template=starter-blog my-blog
|
|
||||||
```
|
|
||||||
|
|
||||||
It supports the updated version of the blog with Contentlayer, optional choice of TS/JS and different package managers as well as more modularized components which will be the basis of the template going forward.
|
|
||||||
|
|
||||||
Alternatively to stick with the current version, TypeScript and Contentlayer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx degit 'timlrx/tailwind-nextjs-starter-blog#contentlayer'
|
|
||||||
```
|
|
||||||
|
|
||||||
or JS (official support)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git
|
npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or with TypeScript (community support)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit timlrx/tailwind-nextjs-starter-blog#typescript
|
||||||
|
```
|
||||||
|
|
||||||
2. Personalize `siteMetadata.js` (site related information)
|
2. Personalize `siteMetadata.js` (site related information)
|
||||||
3. Modify the content security policy in `next.config.js` if you want to use
|
3. Modify the content security policy in `next.config.js` if you want to use
|
||||||
any analytics provider or a commenting solution other than giscus.
|
any analytics provider or a commenting solution other than giscus.
|
||||||
@ -188,7 +165,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
|
|||||||
|
|
||||||
Frontmatter follows [Hugo's standards](https://gohugo.io/content-management/front-matter/).
|
Frontmatter follows [Hugo's standards](https://gohugo.io/content-management/front-matter/).
|
||||||
|
|
||||||
Currently 7 fields are supported.
|
Currently 10 fields are supported.
|
||||||
|
|
||||||
```
|
```
|
||||||
title (required)
|
title (required)
|
||||||
@ -242,4 +219,4 @@ Using the template? Support this effort by giving a star on GitHub, sharing your
|
|||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timlrx.com)
|
[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timrlx.com)
|
||||||
|
@ -11,7 +11,7 @@ export const ClientReload = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import('socket.io-client').then((module) => {
|
import('socket.io-client').then((module) => {
|
||||||
const socket = module.io()
|
const socket = module.io()
|
||||||
socket.on('reload', (data) => {
|
socket.on('reload', () => {
|
||||||
Router.replace(Router.asPath, undefined, {
|
Router.replace(Router.asPath, undefined, {
|
||||||
scroll: false,
|
scroll: false,
|
||||||
})
|
})
|
@ -7,12 +7,12 @@ export default function Footer() {
|
|||||||
<footer>
|
<footer>
|
||||||
<div className="mt-16 flex flex-col items-center">
|
<div className="mt-16 flex flex-col items-center">
|
||||||
<div className="mb-3 flex space-x-4">
|
<div className="mb-3 flex space-x-4">
|
||||||
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size="6" />
|
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
|
||||||
<SocialIcon kind="github" href={siteMetadata.github} size="6" />
|
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
|
||||||
<SocialIcon kind="facebook" href={siteMetadata.facebook} size="6" />
|
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
|
||||||
<SocialIcon kind="youtube" href={siteMetadata.youtube} size="6" />
|
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
|
||||||
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size="6" />
|
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
|
||||||
<SocialIcon kind="twitter" href={siteMetadata.twitter} size="6" />
|
<SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<div>{siteMetadata.author}</div>
|
<div>{siteMetadata.author}</div>
|
@ -1,6 +0,0 @@
|
|||||||
import NextImage from 'next/image'
|
|
||||||
|
|
||||||
// eslint-disable-next-line jsx-a11y/alt-text
|
|
||||||
const Image = ({ ...rest }) => <NextImage {...rest} />
|
|
||||||
|
|
||||||
export default Image
|
|
5
components/Image.tsx
Normal file
5
components/Image.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import NextImage, { ImageProps } from 'next/image'
|
||||||
|
|
||||||
|
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
|
||||||
|
|
||||||
|
export default Image
|
@ -6,8 +6,13 @@ import SectionContainer from './SectionContainer'
|
|||||||
import Footer from './Footer'
|
import Footer from './Footer'
|
||||||
import MobileNav from './MobileNav'
|
import MobileNav from './MobileNav'
|
||||||
import ThemeSwitch from './ThemeSwitch'
|
import ThemeSwitch from './ThemeSwitch'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
const LayoutWrapper = ({ children }) => {
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayoutWrapper = ({ children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<div className="flex h-screen flex-col justify-between">
|
<div className="flex h-screen flex-col justify-between">
|
@ -1,7 +1,11 @@
|
|||||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
|
||||||
|
|
||||||
const CustomLink = ({ href, ...rest }) => {
|
const CustomLink = ({
|
||||||
|
href,
|
||||||
|
...rest
|
||||||
|
}: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
|
||||||
const isInternalLink = href && href.startsWith('/')
|
const isInternalLink = href && href.startsWith('/')
|
||||||
const isAnchorLink = href && href.startsWith('#')
|
const isAnchorLink = href && href.startsWith('#')
|
||||||
|
|
@ -1,26 +0,0 @@
|
|||||||
/* eslint-disable react/display-name */
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { getMDXComponent } from 'mdx-bundler/client'
|
|
||||||
import Image from './Image'
|
|
||||||
import CustomLink from './Link'
|
|
||||||
import TOCInline from './TOCInline'
|
|
||||||
import Pre from './Pre'
|
|
||||||
import { BlogNewsletterForm } from './NewsletterForm'
|
|
||||||
|
|
||||||
export const MDXComponents = {
|
|
||||||
Image,
|
|
||||||
TOCInline,
|
|
||||||
a: CustomLink,
|
|
||||||
pre: Pre,
|
|
||||||
BlogNewsletterForm: BlogNewsletterForm,
|
|
||||||
wrapper: ({ components, layout, ...rest }) => {
|
|
||||||
const Layout = require(`../layouts/${layout}`).default
|
|
||||||
return <Layout {...rest} />
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
|
|
||||||
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
|
|
||||||
|
|
||||||
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
|
|
||||||
}
|
|
36
components/MDXComponents.tsx
Normal file
36
components/MDXComponents.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* 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'
|
||||||
|
|
||||||
|
const Wrapper: React.ComponentType<{ layout: string }> = ({ layout, ...rest }) => {
|
||||||
|
const Layout = require(`../layouts/${layout}`).default
|
||||||
|
return <Layout {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MDXComponents: ComponentMap = {
|
||||||
|
Image,
|
||||||
|
//@ts-ignore
|
||||||
|
TOCInline,
|
||||||
|
a: CustomLink,
|
||||||
|
pre: Pre,
|
||||||
|
wrapper: Wrapper,
|
||||||
|
//@ts-ignore
|
||||||
|
BlogNewsletterForm,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
layout: string
|
||||||
|
mdxSource: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }: Props) => {
|
||||||
|
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
|
||||||
|
|
||||||
|
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
|
||||||
|
}
|
@ -31,39 +31,32 @@ const MobileNav = () => {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-gray-900 dark:text-gray-100"
|
className="text-gray-900 dark:text-gray-100"
|
||||||
>
|
>
|
||||||
<path
|
{navShow ? (
|
||||||
fillRule="evenodd"
|
<path
|
||||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
fillRule="evenodd"
|
||||||
clipRule="evenodd"
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
/>
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 left-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
|
className={`fixed top-24 right-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
|
||||||
navShow ? 'translate-x-0' : 'translate-x-full'
|
navShow ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-end">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
aria-label="toggle modal"
|
||||||
className="mr-5 mt-11 h-8 w-8 rounded"
|
className="fixed h-full w-full cursor-auto focus:outline-none"
|
||||||
aria-label="Toggle Menu"
|
onClick={onToggleNav}
|
||||||
onClick={onToggleNav}
|
></button>
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="text-gray-900 dark:text-gray-100"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<nav className="fixed mt-8 h-full">
|
<nav className="fixed mt-8 h-full">
|
||||||
{headerNavLinks.map((link) => (
|
{headerNavLinks.map((link) => (
|
||||||
<div key={link.title} className="px-12 py-4">
|
<div key={link.title} className="px-12 py-4">
|
@ -1,14 +1,14 @@
|
|||||||
import { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
||||||
const inputEl = useRef(null)
|
const inputEl = useRef<HTMLInputElement>(null)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [subscribed, setSubscribed] = useState(false)
|
const [subscribed, setSubscribed] = useState(false)
|
||||||
|
|
||||||
const subscribe = async (e) => {
|
const subscribe = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
|
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
|
@ -1,4 +1,10 @@
|
|||||||
export default function PageTitle({ children }) {
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PageTitle({ children }: Props) {
|
||||||
return (
|
return (
|
||||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
|
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
|
||||||
{children}
|
{children}
|
@ -1,33 +1,38 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link'
|
||||||
|
|
||||||
export default function Pagination({ totalPages, currentPage }) {
|
interface Props {
|
||||||
const prevPage = parseInt(currentPage) - 1 > 0
|
totalPages: number
|
||||||
const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages)
|
currentPage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Pagination({ totalPages, currentPage }: Props) {
|
||||||
|
const prevPage = currentPage - 1 > 0
|
||||||
|
const nextPage = currentPage + 1 <= totalPages
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||||
<nav className="flex justify-between">
|
<nav className="flex justify-between">
|
||||||
{!prevPage && (
|
{!prevPage && (
|
||||||
<button rel="previous" className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
<button className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{prevPage && (
|
{prevPage && (
|
||||||
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
|
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
|
||||||
<button rel="previous">Previous</button>
|
<button>Previous</button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{currentPage} of {totalPages}
|
{currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
{!nextPage && (
|
{!nextPage && (
|
||||||
<button rel="next" className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
<button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{nextPage && (
|
{nextPage && (
|
||||||
<Link href={`/blog/page/${currentPage + 1}`}>
|
<Link href={`/blog/page/${currentPage + 1}`}>
|
||||||
<button rel="next">Next</button>
|
<button>Next</button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
@ -1,6 +1,10 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useState, useRef, ReactNode } from 'react'
|
||||||
|
|
||||||
const Pre = (props) => {
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pre = ({ children }: Props) => {
|
||||||
const textInput = useRef(null)
|
const textInput = useRef(null)
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
@ -63,7 +67,7 @@ const Pre = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<pre>{props.children}</pre>
|
<pre>{children}</pre>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,8 +1,31 @@
|
|||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl }) => {
|
interface CommonSEOProps {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
ogType: string
|
||||||
|
ogImage:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
'@type': string
|
||||||
|
url: string
|
||||||
|
}[]
|
||||||
|
twImage: string
|
||||||
|
canonicalUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommonSEO = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ogType,
|
||||||
|
ogImage,
|
||||||
|
twImage,
|
||||||
|
canonicalUrl,
|
||||||
|
}: CommonSEOProps) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
@ -14,7 +37,7 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
|
|||||||
<meta property="og:site_name" content={siteMetadata.title} />
|
<meta property="og:site_name" content={siteMetadata.title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
{ogImage.constructor.name === 'Array' ? (
|
{Array.isArray(ogImage) ? (
|
||||||
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
|
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
|
||||||
) : (
|
) : (
|
||||||
<meta property="og:image" content={ogImage} key={ogImage} />
|
<meta property="og:image" content={ogImage} key={ogImage} />
|
||||||
@ -32,7 +55,12 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageSEO = ({ title, description }) => {
|
interface PageSEOProps {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageSEO = ({ title, description }: PageSEOProps) => {
|
||||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||||
return (
|
return (
|
||||||
@ -46,7 +74,7 @@ export const PageSEO = ({ title, description }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagSEO = ({ title, description }) => {
|
export const TagSEO = ({ title, description }: PageSEOProps) => {
|
||||||
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||||
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -71,6 +99,11 @@ export const TagSEO = ({ title, description }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BlogSeoProps extends PostFrontMatter {
|
||||||
|
authorDetails?: AuthorFrontMatter[]
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
export const BlogSEO = ({
|
export const BlogSEO = ({
|
||||||
authorDetails,
|
authorDetails,
|
||||||
title,
|
title,
|
||||||
@ -80,11 +113,10 @@ export const BlogSEO = ({
|
|||||||
url,
|
url,
|
||||||
images = [],
|
images = [],
|
||||||
canonicalUrl,
|
canonicalUrl,
|
||||||
}) => {
|
}: BlogSeoProps) => {
|
||||||
const router = useRouter()
|
|
||||||
const publishedAt = new Date(date).toISOString()
|
const publishedAt = new Date(date).toISOString()
|
||||||
const modifiedAt = new Date(lastmod || date).toISOString()
|
const modifiedAt = new Date(lastmod || date).toISOString()
|
||||||
let imagesArr =
|
const imagesArr =
|
||||||
images.length === 0
|
images.length === 0
|
||||||
? [siteMetadata.socialBanner]
|
? [siteMetadata.socialBanner]
|
||||||
: typeof images === 'string'
|
: typeof images === 'string'
|
||||||
@ -94,7 +126,7 @@ export const BlogSEO = ({
|
|||||||
const featuredImages = imagesArr.map((img) => {
|
const featuredImages = imagesArr.map((img) => {
|
||||||
return {
|
return {
|
||||||
'@type': 'ImageObject',
|
'@type': 'ImageObject',
|
||||||
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
|
url: `${siteMetadata.siteUrl}${img}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1,4 +1,3 @@
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const ScrollTopAndComment = () => {
|
const ScrollTopAndComment = () => {
|
||||||
@ -15,7 +14,7 @@ const ScrollTopAndComment = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleScrollTop = () => {
|
const handleScrollTop = () => {
|
||||||
window.scrollTo({ top: 0 })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
const handleScrollToComment = () => {
|
const handleScrollToComment = () => {
|
||||||
document.getElementById('comment').scrollIntoView()
|
document.getElementById('comment').scrollIntoView()
|
||||||
@ -24,22 +23,20 @@ const ScrollTopAndComment = () => {
|
|||||||
<div
|
<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'}`}
|
||||||
>
|
>
|
||||||
{siteMetadata.comment.provider && (
|
<button
|
||||||
<button
|
aria-label="Scroll To Comment"
|
||||||
aria-label="Scroll To Comment"
|
type="button"
|
||||||
type="button"
|
onClick={handleScrollToComment}
|
||||||
onClick={handleScrollToComment}
|
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
|
||||||
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
|
>
|
||||||
>
|
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<path
|
||||||
<path
|
fillRule="evenodd"
|
||||||
fillRule="evenodd"
|
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
|
||||||
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
|
clipRule="evenodd"
|
||||||
clipRule="evenodd"
|
/>
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
aria-label="Scroll To Top"
|
aria-label="Scroll To Top"
|
||||||
type="button"
|
type="button"
|
@ -1,3 +0,0 @@
|
|||||||
export default function SectionContainer({ children }) {
|
|
||||||
return <div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
|
|
||||||
}
|
|
9
components/SectionContainer.tsx
Normal file
9
components/SectionContainer.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
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>
|
||||||
|
}
|
@ -1,23 +1,27 @@
|
|||||||
/**
|
import { Toc } from 'types/Toc'
|
||||||
* @typedef TocHeading
|
|
||||||
* @prop {string} value
|
interface TOCInlineProps {
|
||||||
* @prop {number} depth
|
toc: Toc
|
||||||
* @prop {string} url
|
indentDepth?: number
|
||||||
*/
|
fromHeading?: number
|
||||||
|
toHeading?: number
|
||||||
|
asDisclosure?: boolean
|
||||||
|
exclude?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an inline table of contents
|
* Generates an inline table of contents
|
||||||
* Exclude titles matching this string (new RegExp('^(' + string + ')$', 'i')).
|
* Exclude titles matching this string (new RegExp('^(' + string + ')$', 'i')).
|
||||||
* If an array is passed the array gets joined with a pipe (new RegExp('^(' + array.join('|') + ')$', 'i')).
|
* If an array is passed the array gets joined with a pipe (new RegExp('^(' + array.join('|') + ')$', 'i')).
|
||||||
*
|
*
|
||||||
* @param {{
|
* @param {TOCInlineProps} {
|
||||||
* toc: TocHeading[],
|
* toc,
|
||||||
* indentDepth?: number,
|
* indentDepth = 3,
|
||||||
* fromHeading?: number,
|
* fromHeading = 1,
|
||||||
* toHeading?: number,
|
* toHeading = 6,
|
||||||
* asDisclosure?: boolean,
|
* asDisclosure = false,
|
||||||
* exclude?: string|string[]
|
* exclude = '',
|
||||||
* }} props
|
* }
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const TOCInline = ({
|
const TOCInline = ({
|
||||||
@ -27,7 +31,7 @@ const TOCInline = ({
|
|||||||
toHeading = 6,
|
toHeading = 6,
|
||||||
asDisclosure = false,
|
asDisclosure = false,
|
||||||
exclude = '',
|
exclude = '',
|
||||||
}) => {
|
}: TOCInlineProps) => {
|
||||||
const re = Array.isArray(exclude)
|
const re = Array.isArray(exclude)
|
||||||
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
|
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
|
||||||
: new RegExp('^(' + exclude + ')$', 'i')
|
: new RegExp('^(' + exclude + ')$', 'i')
|
@ -1,7 +1,11 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import kebabCase from '@/lib/utils/kebabCase'
|
import kebabCase from '@/lib/utils/kebabCase'
|
||||||
|
|
||||||
const Tag = ({ text }) => {
|
interface Props {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tag = ({ text }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Link href={`/tags/${kebabCase(text)}`}>
|
<Link href={`/tags/${kebabCase(text)}`}>
|
||||||
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
@ -1,18 +0,0 @@
|
|||||||
import Script from 'next/script'
|
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
|
||||||
|
|
||||||
const PosthogScript = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Script strategy="lazyOnload" id="posthog-script">
|
|
||||||
{`
|
|
||||||
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
||||||
posthog.init('${siteMetadata.analytics.posthogAnalyticsId}',{api_host:'https://app.posthog.com'})
|
|
||||||
`}
|
|
||||||
</Script>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PosthogScript
|
|
@ -2,9 +2,16 @@ import GA from './GoogleAnalytics'
|
|||||||
import Plausible from './Plausible'
|
import Plausible from './Plausible'
|
||||||
import SimpleAnalytics from './SimpleAnalytics'
|
import SimpleAnalytics from './SimpleAnalytics'
|
||||||
import Umami from './Umami'
|
import Umami from './Umami'
|
||||||
import Posthog from './Posthog'
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
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 = () => {
|
const Analytics = () => {
|
||||||
@ -14,7 +21,6 @@ const Analytics = () => {
|
|||||||
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
|
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
|
||||||
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
|
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
|
||||||
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
|
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
|
||||||
{isProduction && siteMetadata.analytics.posthogAnalyticsId && <Posthog />}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,8 +1,13 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
const Disqus = ({ frontMatter }) => {
|
interface Props {
|
||||||
|
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'
|
||||||
@ -10,18 +15,22 @@ const Disqus = ({ frontMatter }) => {
|
|||||||
function LoadComments() {
|
function LoadComments() {
|
||||||
setEnabledLoadComments(false)
|
setEnabledLoadComments(false)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
window.disqus_config = function () {
|
window.disqus_config = function () {
|
||||||
this.page.url = window.location.href
|
this.page.url = window.location.href
|
||||||
this.page.identifier = frontMatter.slug
|
this.page.identifier = frontMatter.slug
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
if (window.DISQUS === undefined) {
|
if (window.DISQUS === undefined) {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
|
script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
|
||||||
|
// @ts-ignore
|
||||||
script.setAttribute('data-timestamp', +new Date())
|
script.setAttribute('data-timestamp', +new Date())
|
||||||
script.setAttribute('crossorigin', 'anonymous')
|
script.setAttribute('crossorigin', 'anonymous')
|
||||||
script.async = true
|
script.async = true
|
||||||
document.body.appendChild(script)
|
document.body.appendChild(script)
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
window.DISQUS.reset({ reload: true })
|
window.DISQUS.reset({ reload: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,11 @@ import { useTheme } from 'next-themes'
|
|||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
const Giscus = () => {
|
interface Props {
|
||||||
|
mapping: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Giscus = ({ mapping }: Props) => {
|
||||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||||
const { theme, resolvedTheme } = useTheme()
|
const { theme, resolvedTheme } = useTheme()
|
||||||
const commentsTheme =
|
const commentsTheme =
|
||||||
@ -17,30 +21,15 @@ const Giscus = () => {
|
|||||||
|
|
||||||
const LoadComments = useCallback(() => {
|
const LoadComments = useCallback(() => {
|
||||||
setEnabledLoadComments(false)
|
setEnabledLoadComments(false)
|
||||||
|
|
||||||
const {
|
|
||||||
repo,
|
|
||||||
repositoryId,
|
|
||||||
category,
|
|
||||||
categoryId,
|
|
||||||
mapping,
|
|
||||||
reactions,
|
|
||||||
metadata,
|
|
||||||
inputPosition,
|
|
||||||
lang,
|
|
||||||
} = siteMetadata?.comment?.giscusConfig
|
|
||||||
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = 'https://giscus.app/client.js'
|
script.src = 'https://giscus.app/client.js'
|
||||||
script.setAttribute('data-repo', repo)
|
script.setAttribute('data-repo', siteMetadata.comment.giscusConfig.repo)
|
||||||
script.setAttribute('data-repo-id', repositoryId)
|
script.setAttribute('data-repo-id', siteMetadata.comment.giscusConfig.repositoryId)
|
||||||
script.setAttribute('data-category', category)
|
script.setAttribute('data-category', siteMetadata.comment.giscusConfig.category)
|
||||||
script.setAttribute('data-category-id', categoryId)
|
script.setAttribute('data-category-id', siteMetadata.comment.giscusConfig.categoryId)
|
||||||
script.setAttribute('data-mapping', mapping)
|
script.setAttribute('data-mapping', mapping)
|
||||||
script.setAttribute('data-reactions-enabled', reactions)
|
script.setAttribute('data-reactions-enabled', siteMetadata.comment.giscusConfig.reactions)
|
||||||
script.setAttribute('data-emit-metadata', metadata)
|
script.setAttribute('data-emit-metadata', siteMetadata.comment.giscusConfig.metadata)
|
||||||
script.setAttribute('data-input-position', inputPosition)
|
|
||||||
script.setAttribute('data-lang', lang)
|
|
||||||
script.setAttribute('data-theme', commentsTheme)
|
script.setAttribute('data-theme', commentsTheme)
|
||||||
script.setAttribute('crossorigin', 'anonymous')
|
script.setAttribute('crossorigin', 'anonymous')
|
||||||
script.async = true
|
script.async = true
|
||||||
@ -52,7 +41,7 @@ const Giscus = () => {
|
|||||||
const comments = document.getElementById(COMMENTS_ID)
|
const comments = document.getElementById(COMMENTS_ID)
|
||||||
if (comments) comments.innerHTML = ''
|
if (comments) comments.innerHTML = ''
|
||||||
}
|
}
|
||||||
}, [commentsTheme])
|
}, [commentsTheme, mapping])
|
||||||
|
|
||||||
// Reload on theme change
|
// Reload on theme change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
@ -3,7 +3,11 @@ import { useTheme } from 'next-themes'
|
|||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
const Utterances = () => {
|
interface Props {
|
||||||
|
issueTerm: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Utterances = ({ issueTerm }: Props) => {
|
||||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||||
const { theme, resolvedTheme } = useTheme()
|
const { theme, resolvedTheme } = useTheme()
|
||||||
const commentsTheme =
|
const commentsTheme =
|
||||||
@ -18,7 +22,7 @@ const Utterances = () => {
|
|||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = 'https://utteranc.es/client.js'
|
script.src = 'https://utteranc.es/client.js'
|
||||||
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
|
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
|
||||||
script.setAttribute('issue-term', siteMetadata.comment.utterancesConfig.issueTerm)
|
script.setAttribute('issue-term', issueTerm)
|
||||||
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
|
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
|
||||||
script.setAttribute('theme', commentsTheme)
|
script.setAttribute('theme', commentsTheme)
|
||||||
script.setAttribute('crossorigin', 'anonymous')
|
script.setAttribute('crossorigin', 'anonymous')
|
||||||
@ -31,7 +35,7 @@ const Utterances = () => {
|
|||||||
const comments = document.getElementById(COMMENTS_ID)
|
const comments = document.getElementById(COMMENTS_ID)
|
||||||
if (comments) comments.innerHTML = ''
|
if (comments) comments.innerHTML = ''
|
||||||
}
|
}
|
||||||
}, [commentsTheme])
|
}, [commentsTheme, issueTerm])
|
||||||
|
|
||||||
// Reload on theme change
|
// Reload on theme change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
@ -1,5 +1,10 @@
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
frontMatter: PostFrontMatter
|
||||||
|
}
|
||||||
|
|
||||||
const UtterancesComponent = dynamic(
|
const UtterancesComponent = dynamic(
|
||||||
() => {
|
() => {
|
||||||
@ -20,14 +25,29 @@ const DisqusComponent = dynamic(
|
|||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
const Comments = ({ frontMatter }) => {
|
const Comments = ({ frontMatter }: Props) => {
|
||||||
const comment = siteMetadata?.comment
|
let term
|
||||||
if (!comment || Object.keys(comment).length === 0) return <></>
|
switch (
|
||||||
|
siteMetadata.comment.giscusConfig.mapping ||
|
||||||
|
siteMetadata.comment.utterancesConfig.issueTerm
|
||||||
|
) {
|
||||||
|
case 'pathname':
|
||||||
|
term = frontMatter.slug
|
||||||
|
break
|
||||||
|
case 'url':
|
||||||
|
term = window.location.href
|
||||||
|
break
|
||||||
|
case 'title':
|
||||||
|
term = frontMatter.title
|
||||||
|
break
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div id="comment">
|
<div id="comment">
|
||||||
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && <GiscusComponent />}
|
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && (
|
||||||
|
<GiscusComponent mapping={term} />
|
||||||
|
)}
|
||||||
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
|
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
|
||||||
<UtterancesComponent />
|
<UtterancesComponent issueTerm={term} />
|
||||||
)}
|
)}
|
||||||
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
|
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
|
||||||
<DisqusComponent frontMatter={frontMatter} />
|
<DisqusComponent frontMatter={frontMatter} />
|
@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlight-line {
|
.highlight-line {
|
||||||
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
|
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-number::before {
|
.line-number::before {
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
const projectsData = [
|
|
||||||
{
|
|
||||||
title: 'UPS',
|
|
||||||
description: `一个不间断电源(UPS)的全栈项目。核心硬件使用乐鑫×安信可的 ESP32-C3-32S 模块作为主控,软件部分使用了 Rust + ESP-IDF 开发。`,
|
|
||||||
// imgSrc: '/static/images/google.png',
|
|
||||||
href: 'https://git.ivanli.cc/Ivan/ups-esp32c3-rust',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: 'The Time Machine',
|
|
||||||
// description: `Imagine being able to travel back in time or to the future. Simple turn the knob
|
|
||||||
// to the desired date and press "Go". No more worrying about lost keys or
|
|
||||||
// forgotten headphones with this simple yet affordable solution.`,
|
|
||||||
// imgSrc: '/static/images/time-machine.jpg',
|
|
||||||
// href: '/blog/the-time-machine',
|
|
||||||
// },
|
|
||||||
]
|
|
||||||
|
|
||||||
export default projectsData
|
|
10
data/projectsData.ts
Normal file
10
data/projectsData.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const projectsData = [
|
||||||
|
{
|
||||||
|
title: 'UPS',
|
||||||
|
description: `一个不间断电源(UPS)的全栈项目。核心硬件使用乐鑫×安信可的 ESP32-C3-32S 模块作为主控,软件部分使用了 Rust + ESP-IDF 开发。`,
|
||||||
|
// imgSrc: '/static/images/google.png',
|
||||||
|
href: 'https://git.ivanli.cc/Ivan/ups-esp32c3-rust',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default projectsData
|
@ -30,4 +30,4 @@
|
|||||||
author={Xie, Yihui},
|
author={Xie, Yihui},
|
||||||
year={2016},
|
year={2016},
|
||||||
publisher={CRC Press}
|
publisher={CRC Press}
|
||||||
}
|
}
|
@ -33,14 +33,14 @@ const siteMetadata = {
|
|||||||
newsletter: {
|
newsletter: {
|
||||||
// supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus
|
// supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus
|
||||||
// Please add your .env file and modify it according to your selection
|
// Please add your .env file and modify it according to your selection
|
||||||
provider: 'buttondown',
|
provider: '',
|
||||||
},
|
},
|
||||||
comment: {
|
comment: {
|
||||||
// If you want to use a commenting system other than giscus you have to add it to the
|
// If you want to use a commenting system other than giscus you have to add it to the
|
||||||
// content security policy in the `next.config.js` file.
|
// content security policy in the `next.config.js` file.
|
||||||
// Select a provider and use the environment variables associated to it
|
// Select a provider and use the environment variables associated to it
|
||||||
// https://vercel.com/docs/environment-variables
|
// https://vercel.com/docs/environment-variables
|
||||||
provider: 'giscus', // supported providers: giscus, utterances, disqus
|
provider: '', // supported providers: giscus, utterances, disqus
|
||||||
giscusConfig: {
|
giscusConfig: {
|
||||||
// Visit the link below, and follow the steps in the 'configuration' section
|
// Visit the link below, and follow the steps in the 'configuration' section
|
||||||
// https://giscus.app/
|
// https://giscus.app/
|
||||||
@ -55,10 +55,6 @@ const siteMetadata = {
|
|||||||
// theme example: light, dark, dark_dimmed, dark_high_contrast
|
// theme example: light, dark, dark_dimmed, dark_high_contrast
|
||||||
// transparent_dark, preferred_color_scheme, custom
|
// transparent_dark, preferred_color_scheme, custom
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
// Place the comment box above the comments. options: bottom, top
|
|
||||||
inputPosition: 'bottom',
|
|
||||||
// Choose the language giscus will be displayed in. options: en, es, zh-CN, zh-TW, ko, ja etc
|
|
||||||
lang: 'en',
|
|
||||||
// theme when dark mode
|
// theme when dark mode
|
||||||
darkTheme: 'transparent_dark',
|
darkTheme: 'transparent_dark',
|
||||||
// If the theme option above is set to 'custom`
|
// If the theme option above is set to 'custom`
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
import SocialIcon from '@/components/social-icons'
|
import SocialIcon from '@/components/social-icons'
|
||||||
import Image from '@/components/Image'
|
import Image from '@/components/Image'
|
||||||
import { PageSEO } from '@/components/SEO'
|
import { PageSEO } from '@/components/SEO'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||||
|
|
||||||
export default function AuthorLayout({ children, frontMatter }) {
|
interface Props {
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSEO title={`About - ${name}`} description={`About me - ${name}`} />
|
<PageSEO title={`About - ${name}`} description={`About me - ${name}`} />
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="divide-y">
|
||||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
||||||
About
|
About
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
|
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
|
||||||
<div className="flex flex-col items-center pt-8">
|
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||||
<Image
|
<Image
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt="avatar"
|
alt="avatar"
|
@ -1,11 +1,17 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link'
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag'
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import { ComponentProps, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
import formatDate from '@/lib/utils/formatDate'
|
import formatDate from '@/lib/utils/formatDate'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
interface Props {
|
||||||
|
posts: PostFrontMatter[]
|
||||||
|
title: string
|
||||||
|
initialDisplayPosts?: PostFrontMatter[]
|
||||||
|
pagination?: ComponentProps<typeof Pagination>
|
||||||
|
}
|
||||||
|
|
||||||
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
|
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }: Props) {
|
||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('')
|
||||||
const filteredBlogPosts = posts.filter((frontMatter) => {
|
const filteredBlogPosts = posts.filter((frontMatter) => {
|
||||||
const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')
|
const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')
|
||||||
@ -18,7 +24,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="divide-y">
|
||||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
||||||
{title}
|
{title}
|
@ -7,7 +7,9 @@ import Tag from '@/components/Tag'
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import Comments from '@/components/comments'
|
import Comments from '@/components/comments'
|
||||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||||
import { useMemo } from 'react'
|
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) =>
|
const discussUrl = (slug) =>
|
||||||
@ -30,9 +32,22 @@ const Copyright = () => (
|
|||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
||||||
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
|
const postDateTemplate: Intl.DateTimeFormatOptions = {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}
|
||||||
|
|
||||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
interface Props {
|
||||||
|
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
|
const { slug, fileName, date, title, images, tags } = frontMatter
|
||||||
|
|
||||||
const headerStyles = useMemo(
|
const headerStyles = useMemo(
|
||||||
@ -123,9 +138,8 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||||||
</dl>
|
</dl>
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
|
||||||
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
|
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
|
||||||
<div className="flex items-center pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
|
<div className="flex items-center gap-4 pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
|
||||||
<Copyright />
|
<Copyright />
|
||||||
{` • `}
|
|
||||||
<Link href={editUrl(fileName)}>{'View source'}</Link>
|
<Link href={editUrl(fileName)}>{'View source'}</Link>
|
||||||
</div>
|
</div>
|
||||||
<Comments frontMatter={frontMatter} />
|
<Comments frontMatter={frontMatter} />
|
@ -6,13 +6,22 @@ import siteMetadata from '@/data/siteMetadata'
|
|||||||
import formatDate from '@/lib/utils/formatDate'
|
import formatDate from '@/lib/utils/formatDate'
|
||||||
import Comments from '@/components/comments'
|
import Comments from '@/components/comments'
|
||||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
interface Props {
|
||||||
const { date, title } = frontMatter
|
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
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
|
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${slug}`} {...frontMatter} />
|
||||||
<ScrollTopAndComment />
|
<ScrollTopAndComment />
|
||||||
<article>
|
<article>
|
||||||
<div>
|
<div>
|
@ -1,8 +1,9 @@
|
|||||||
import { escape } from '@/lib/utils/htmlEscaper'
|
import { escape } from '@/lib/utils/htmlEscaper'
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
const generateRssItem = (post) => `
|
const generateRssItem = (post: PostFrontMatter) => `
|
||||||
<item>
|
<item>
|
||||||
<guid>${siteMetadata.siteUrl}/blog/${post.slug}</guid>
|
<guid>${siteMetadata.siteUrl}/blog/${post.slug}</guid>
|
||||||
<title>${escape(post.title)}</title>
|
<title>${escape(post.title)}</title>
|
||||||
@ -14,7 +15,7 @@ const generateRssItem = (post) => `
|
|||||||
</item>
|
</item>
|
||||||
`
|
`
|
||||||
|
|
||||||
const generateRss = (posts, page = 'feed.xml') => `
|
const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
|
||||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
<channel>
|
<channel>
|
||||||
<title>${escape(siteMetadata.title)}</title>
|
<title>${escape(siteMetadata.title)}</title>
|
@ -3,8 +3,10 @@ import fs from 'fs'
|
|||||||
import matter from 'gray-matter'
|
import matter from 'gray-matter'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import readingTime from 'reading-time'
|
import readingTime from 'reading-time'
|
||||||
import { visit } from 'unist-util-visit'
|
|
||||||
import getAllFilesRecursively from './utils/files'
|
import getAllFilesRecursively from './utils/files'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||||
|
import { Toc } from 'types/Toc'
|
||||||
// Remark packages
|
// Remark packages
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkFootnotes from 'remark-footnotes'
|
import remarkFootnotes from 'remark-footnotes'
|
||||||
@ -23,24 +25,24 @@ import rehypePresetMinify from 'rehype-preset-minify'
|
|||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
|
|
||||||
export function getFiles(type) {
|
export function getFiles(type: 'blog' | 'authors') {
|
||||||
const prefixPaths = path.join(root, 'data', type)
|
const prefixPaths = path.join(root, 'data', type)
|
||||||
const files = getAllFilesRecursively(prefixPaths)
|
const files = getAllFilesRecursively(prefixPaths)
|
||||||
// Only want to return blog/path and ignore root, replace is needed to work on Windows
|
// Only want to return blog/path and ignore root, replace is needed to work on Windows
|
||||||
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
|
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSlug(slug) {
|
export function formatSlug(slug: string) {
|
||||||
return slug.replace(/\.(mdx|md)/, '')
|
return slug.replace(/\.(mdx|md)/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateSortDesc(a, b) {
|
export function dateSortDesc(a: string, b: string) {
|
||||||
if (a > b) return -1
|
if (a > b) return -1
|
||||||
if (a < b) return 1
|
if (a < b) return 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileBySlug(type, slug) {
|
export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string | string[]) {
|
||||||
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
|
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
|
||||||
const mdPath = path.join(root, 'data', type, `${slug}.md`)
|
const mdPath = path.join(root, 'data', type, `${slug}.md`)
|
||||||
const source = fs.existsSync(mdxPath)
|
const source = fs.existsSync(mdxPath)
|
||||||
@ -54,7 +56,7 @@ export async function getFileBySlug(type, slug) {
|
|||||||
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
let toc = []
|
const toc: Toc = []
|
||||||
|
|
||||||
const { code, frontmatter } = await bundleMDX({
|
const { code, frontmatter } = await bundleMDX({
|
||||||
source,
|
source,
|
||||||
@ -107,14 +109,14 @@ export async function getFileBySlug(type, slug) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllFilesFrontMatter(folder) {
|
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 = []
|
const allFrontMatter: PostFrontMatter[] = []
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file: string) => {
|
||||||
// Replace is needed to work on Windows
|
// Replace is needed to work on Windows
|
||||||
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
|
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
|
||||||
// Remove Unexpected File
|
// Remove Unexpected File
|
||||||
@ -122,8 +124,9 @@ export async function getAllFilesFrontMatter(folder) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const source = fs.readFileSync(file, 'utf8')
|
const source = fs.readFileSync(file, 'utf8')
|
||||||
const { data: frontmatter } = matter(source)
|
const matterFile = matter(source)
|
||||||
if (frontmatter.draft !== true) {
|
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter
|
||||||
|
if ('draft' in frontmatter && frontmatter.draft !== true) {
|
||||||
allFrontMatter.push({
|
allFrontMatter.push({
|
||||||
...frontmatter,
|
...frontmatter,
|
||||||
slug: formatSlug(fileName),
|
slug: formatSlug(fileName),
|
@ -1,8 +1,9 @@
|
|||||||
|
import { Parent } from 'unist'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
export default function remarkCodeTitles() {
|
export default function remarkCodeTitles() {
|
||||||
return (tree) =>
|
return (tree: Parent & { lang?: string }) =>
|
||||||
visit(tree, 'code', (node, index, parent) => {
|
visit(tree, 'code', (node: Parent & { lang?: string }, index, parent: Parent) => {
|
||||||
const nodeLang = node.lang || ''
|
const nodeLang = node.lang || ''
|
||||||
let language = ''
|
let language = ''
|
||||||
let title = ''
|
let title = ''
|
@ -1,9 +1,12 @@
|
|||||||
|
import { Parent } from 'unist'
|
||||||
|
import { VFile } from 'vfile'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
import { load } from 'js-yaml'
|
import { load } from 'js-yaml'
|
||||||
|
|
||||||
export default function extractFrontmatter() {
|
export default function extractFrontmatter() {
|
||||||
return (tree, file) => {
|
return (tree: Parent, file: VFile) => {
|
||||||
visit(tree, 'yaml', (node, index, parent) => {
|
visit(tree, 'yaml', (node: Parent) => {
|
||||||
|
//@ts-ignore
|
||||||
file.data.frontmatter = load(node.value)
|
file.data.frontmatter = load(node.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,15 +1,24 @@
|
|||||||
|
import { Parent, Node, Literal } from 'unist'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
import sizeOf from 'image-size'
|
import sizeOf from 'image-size'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
|
type ImageNode = Parent & {
|
||||||
|
url: string
|
||||||
|
alt: string
|
||||||
|
name: string
|
||||||
|
attributes: (Literal & { name: string })[]
|
||||||
|
}
|
||||||
|
|
||||||
export default function remarkImgToJsx() {
|
export default function remarkImgToJsx() {
|
||||||
return (tree) => {
|
return (tree: Node) => {
|
||||||
visit(
|
visit(
|
||||||
tree,
|
tree,
|
||||||
// only visit p tags that contain an img element
|
// only visit p tags that contain an img element
|
||||||
(node) => node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
|
(node: Parent): node is Parent =>
|
||||||
(node) => {
|
node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
|
||||||
const imageNode = node.children.find((n) => n.type === 'image')
|
(node: Parent) => {
|
||||||
|
const imageNode = node.children.find((n) => n.type === 'image') as ImageNode
|
||||||
|
|
||||||
// only local files
|
// only local files
|
||||||
if (fs.existsSync(`${process.cwd()}/public${imageNode.url}`)) {
|
if (fs.existsSync(`${process.cwd()}/public${imageNode.url}`)) {
|
@ -1,10 +1,12 @@
|
|||||||
|
//@ts-nocheck
|
||||||
|
import { Parent } from 'unist'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
import { slug } from 'github-slugger'
|
import { slug } from 'github-slugger'
|
||||||
import { toString } from 'mdast-util-to-string'
|
import { toString } from 'mdast-util-to-string'
|
||||||
|
|
||||||
export default function remarkTocHeadings(options) {
|
export default function remarkTocHeadings(options) {
|
||||||
return (tree) =>
|
return (tree: Parent) =>
|
||||||
visit(tree, 'heading', (node, index, parent) => {
|
visit(tree, 'heading', (node) => {
|
||||||
const textContent = toString(node)
|
const textContent = toString(node)
|
||||||
options.exportRef.push({
|
options.exportRef.push({
|
||||||
value: textContent,
|
value: textContent,
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import matter from 'gray-matter'
|
import matter from 'gray-matter'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
@ -6,14 +7,15 @@ import kebabCase from './utils/kebabCase'
|
|||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
|
|
||||||
export async function getAllTags(type) {
|
export async function getAllTags(type: 'blog' | 'authors') {
|
||||||
const files = await getFiles(type)
|
const files = getFiles(type)
|
||||||
|
|
||||||
let tagCount = {}
|
const tagCount: Record<string, number> = {}
|
||||||
// Iterate through each post, putting all found tags into `tags`
|
// Iterate through each post, putting all found tags into `tags`
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
|
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
|
||||||
const { data } = matter(source)
|
const matterFile = matter(source)
|
||||||
|
const data = matterFile.data as PostFrontMatter
|
||||||
if (data.tags && data.draft !== true) {
|
if (data.tags && data.draft !== true) {
|
||||||
data.tags.forEach((tag) => {
|
data.tags.forEach((tag) => {
|
||||||
const formattedTag = kebabCase(tag)
|
const formattedTag = kebabCase(tag)
|
@ -11,13 +11,13 @@ const flattenArray = (input) =>
|
|||||||
|
|
||||||
const map = (fn) => (input) => input.map(fn)
|
const map = (fn) => (input) => input.map(fn)
|
||||||
|
|
||||||
const walkDir = (fullPath) => {
|
const walkDir = (fullPath: string) => {
|
||||||
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
|
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathJoinPrefix = (prefix) => (extraPath) => path.join(prefix, extraPath)
|
const pathJoinPrefix = (prefix: string) => (extraPath: string) => path.join(prefix, extraPath)
|
||||||
|
|
||||||
const getAllFilesRecursively = (folder) =>
|
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,7 +1,7 @@
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
const formatDate = (date) => {
|
const formatDate = (date: string) => {
|
||||||
const options = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
@ -1,7 +1,6 @@
|
|||||||
const { replace } = ''
|
const { replace } = ''
|
||||||
|
|
||||||
// escape
|
// escape
|
||||||
const es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g
|
|
||||||
const ca = /[&<>'"]/g
|
const ca = /[&<>'"]/g
|
||||||
|
|
||||||
const esca = {
|
const esca = {
|
||||||
@ -11,7 +10,7 @@ const esca = {
|
|||||||
"'": ''',
|
"'": ''',
|
||||||
'"': '"',
|
'"': '"',
|
||||||
}
|
}
|
||||||
const pe = (m) => esca[m]
|
const pe = (m: keyof typeof esca) => esca[m]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
|
* Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
|
||||||
@ -20,4 +19,4 @@ const pe = (m) => esca[m]
|
|||||||
* the input type is unexpected, except for boolean and numbers,
|
* the input type is unexpected, except for boolean and numbers,
|
||||||
* converted as string.
|
* converted as string.
|
||||||
*/
|
*/
|
||||||
export const escape = (es) => 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) => slug(str)
|
const kebabCase = (str: string) => slug(str)
|
||||||
|
|
||||||
export default kebabCase
|
export default kebabCase
|
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -52,9 +52,12 @@ const securityHeaders = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('next/dist/next-server/server/config').NextConfig}
|
||||||
|
**/
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
|
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
|
||||||
eslint: {
|
eslint: {
|
||||||
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
||||||
},
|
},
|
||||||
@ -70,6 +73,19 @@ module.exports = withBundleAnalyzer({
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
webpack: (config, { dev, isServer }) => {
|
webpack: (config, { dev, isServer }) => {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.(png|jpe?g|gif|mp4)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
publicPath: '/_next',
|
||||||
|
name: 'static/media/[name].[hash].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
use: ['@svgr/webpack'],
|
use: ['@svgr/webpack'],
|
||||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
18
package.json
18
package.json
@ -12,7 +12,6 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/bundle-analyzer": "12.1.4",
|
|
||||||
"@fontsource/inter": "4.5.2",
|
"@fontsource/inter": "4.5.2",
|
||||||
"@mailchimp/mailchimp_marketing": "^3.0.58",
|
"@mailchimp/mailchimp_marketing": "^3.0.58",
|
||||||
"@tailwindcss/forms": "^0.4.0",
|
"@tailwindcss/forms": "^0.4.0",
|
||||||
@ -23,7 +22,7 @@
|
|||||||
"gray-matter": "^4.0.2",
|
"gray-matter": "^4.0.2",
|
||||||
"image-size": "1.0.0",
|
"image-size": "1.0.0",
|
||||||
"mdx-bundler": "^8.0.0",
|
"mdx-bundler": "^8.0.0",
|
||||||
"next": "12.1.4",
|
"next": "12.0.9",
|
||||||
"next-themes": "^0.0.14",
|
"next-themes": "^0.0.14",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"preact": "^10.6.2",
|
"preact": "^10.6.2",
|
||||||
@ -31,7 +30,7 @@
|
|||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"reading-time": "1.3.0",
|
"reading-time": "1.3.0",
|
||||||
"rehype-autolink-headings": "^6.1.0",
|
"rehype-autolink-headings": "^6.1.0",
|
||||||
"rehype-citation": "^0.4.0",
|
"rehype-citation": "^0.2.0",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"rehype-preset-minify": "6.0.0",
|
"rehype-preset-minify": "6.0.0",
|
||||||
"rehype-prism-plus": "^1.1.3",
|
"rehype-prism-plus": "^1.1.3",
|
||||||
@ -40,15 +39,21 @@
|
|||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"sharp": "^0.28.3",
|
"sharp": "^0.28.3",
|
||||||
"tailwindcss": "^3.0.23",
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
|
"tailwindcss": "^3.0.18",
|
||||||
"unist-util-visit": "^4.0.0"
|
"unist-util-visit": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@next/bundle-analyzer": "12.0.9",
|
||||||
"@svgr/webpack": "^6.1.2",
|
"@svgr/webpack": "^6.1.2",
|
||||||
|
"@types/react": "^17.0.14",
|
||||||
|
"@types/tailwindcss": "^2.2.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||||
|
"@typescript-eslint/parser": "^4.28.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dedent": "^0.7.0",
|
"dedent": "^0.7.0",
|
||||||
"eslint": "^7.29.0",
|
"eslint": "^7.29.0",
|
||||||
"eslint-config-next": "12.1.4",
|
"eslint-config-next": "12.0.9",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
@ -60,7 +65,8 @@
|
|||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.4",
|
"prettier-plugin-tailwindcss": "^0.1.4",
|
||||||
"socket.io": "^4.4.0",
|
"socket.io": "^4.4.0",
|
||||||
"socket.io-client": "^4.4.0"
|
"socket.io-client": "^4.4.0",
|
||||||
|
"typescript": "^4.6.1-rc"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.+(js|jsx|ts|tsx)": [
|
"*.+(js|jsx|ts|tsx)": [
|
||||||
|
31
pages/404.js
31
pages/404.js
@ -1,31 +0,0 @@
|
|||||||
import Link from '@/components/Link'
|
|
||||||
import { PageSEO } from '@/components/SEO'
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
|
||||||
|
|
||||||
export default function FourZeroFour() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageSEO title={`Page Not Found - ${siteMetadata.title}`} />
|
|
||||||
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
|
|
||||||
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
|
|
||||||
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
|
|
||||||
404
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-md">
|
|
||||||
<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 don't worry, you can find plenty of other things on our homepage.
|
|
||||||
</p>
|
|
||||||
<Link href="/">
|
|
||||||
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
|
|
||||||
Back to homepage
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
24
pages/404.tsx
Normal file
24
pages/404.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Link from '@/components/Link'
|
||||||
|
|
||||||
|
export default function FourZeroFour() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
|
||||||
|
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
|
||||||
|
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
|
||||||
|
404
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<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>
|
||||||
|
<Link href="/">
|
||||||
|
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
|
||||||
|
Back to homepage
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -5,6 +5,7 @@ import 'katex/dist/katex.css'
|
|||||||
import '@fontsource/inter/variable-full.css'
|
import '@fontsource/inter/variable-full.css'
|
||||||
|
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
@ -15,7 +16,7 @@ import { ClientReload } from '@/components/ClientReload'
|
|||||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||||
const isSocket = process.env.SOCKET
|
const isSocket = process.env.SOCKET
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
|
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
|
||||||
<Head>
|
<Head>
|
@ -1,4 +1,5 @@
|
|||||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
class MyDocument extends Document {
|
class MyDocument extends Document {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
@ -20,8 +21,7 @@ class MyDocument extends Document {
|
|||||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
<meta name="msapplication-TileColor" content="#000000" />
|
<meta name="msapplication-TileColor" content="#000000" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
|
||||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||||
</Head>
|
</Head>
|
||||||
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
@ -1,21 +0,0 @@
|
|||||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
|
||||||
import { getFileBySlug } from '@/lib/mdx'
|
|
||||||
|
|
||||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
|
||||||
|
|
||||||
export async function getStaticProps() {
|
|
||||||
const authorDetails = await getFileBySlug('authors', ['default'])
|
|
||||||
return { props: { authorDetails } }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function About({ authorDetails }) {
|
|
||||||
const { mdxSource, frontMatter } = authorDetails
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MDXLayoutRenderer
|
|
||||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
|
||||||
mdxSource={mdxSource}
|
|
||||||
frontMatter={frontMatter}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
27
pages/about.tsx
Normal file
27
pages/about.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||||
|
import { getFileBySlug } from '@/lib/mdx'
|
||||||
|
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||||
|
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||||
|
|
||||||
|
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export const getStaticProps: GetStaticProps<{
|
||||||
|
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }
|
||||||
|
}> = async () => {
|
||||||
|
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
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MDXLayoutRenderer
|
||||||
|
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||||
|
mdxSource={mdxSource}
|
||||||
|
frontMatter={frontMatter}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
export default async (req, res) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { email } = req.body
|
const { email } = req.body
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return res.status(400).json({ error: 'Email is required' })
|
return res.status(400).json({ error: 'Email is required' })
|
@ -1,5 +1,7 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
/* eslint-disable import/no-anonymous-default-export */
|
/* eslint-disable import/no-anonymous-default-export */
|
||||||
export default async (req, res) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { email } = req.body
|
const { email } = req.body
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
@ -1,33 +0,0 @@
|
|||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
|
||||||
export default async (req, res) => {
|
|
||||||
const { email } = req.body
|
|
||||||
if (!email) {
|
|
||||||
return res.status(400).json({ error: 'Email is required' })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const API_URL = process.env.EMAILOCTOPUS_API_URL
|
|
||||||
const API_KEY = process.env.EMAILOCTOPUS_API_KEY
|
|
||||||
const LIST_ID = process.env.EMAILOCTOPUS_LIST_ID
|
|
||||||
|
|
||||||
const data = { email_address: email, api_key: API_KEY }
|
|
||||||
|
|
||||||
const API_ROUTE = `${API_URL}lists/${LIST_ID}/contacts`
|
|
||||||
|
|
||||||
const response = await fetch(API_ROUTE, {
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'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(201).json({ error: '' })
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(500).json({ error: error.message || error.toString() })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,7 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
/* eslint-disable import/no-anonymous-default-export */
|
/* eslint-disable import/no-anonymous-default-export */
|
||||||
export default async (req, res) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { email } = req.body
|
const { email } = req.body
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return res.status(400).json({ error: 'Email is required' })
|
return res.status(400).json({ error: 'Email is required' })
|
@ -1,3 +1,4 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import mailchimp from '@mailchimp/mailchimp_marketing'
|
import mailchimp from '@mailchimp/mailchimp_marketing'
|
||||||
|
|
||||||
mailchimp.setConfig({
|
mailchimp.setConfig({
|
||||||
@ -6,7 +7,7 @@ mailchimp.setConfig({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
export default async (req, res) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { email } = req.body
|
const { email } = req.body
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -14,7 +15,7 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const test = await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
||||||
email_address: email,
|
email_address: email,
|
||||||
status: 'subscribed',
|
status: 'subscribed',
|
||||||
})
|
})
|
@ -1,30 +0,0 @@
|
|||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
|
||||||
export default async (req, res) => {
|
|
||||||
const { email } = req.body
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
return res.status(400).json({ error: 'Email is required' })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const API_KEY = process.env.REVUE_API_KEY
|
|
||||||
const revueRoute = `${process.env.REVUE_API_URL}subscribers`
|
|
||||||
|
|
||||||
const response = await fetch(revueRoute, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Token ${API_KEY}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, double_opt_in: false }),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status >= 400) {
|
|
||||||
return res.status(500).json({ error: `There was an error subscribing to the list.` })
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(201).json({ error: '' })
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(500).json({ error: error.message || error.toString() })
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,10 +2,16 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import ListLayout from '@/layouts/ListLayout'
|
import ListLayout from '@/layouts/ListLayout'
|
||||||
import { PageSEO } from '@/components/SEO'
|
import { PageSEO } from '@/components/SEO'
|
||||||
|
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||||
|
import { ComponentProps } from 'react'
|
||||||
|
|
||||||
export const POSTS_PER_PAGE = 5
|
export const POSTS_PER_PAGE = 5
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export const getStaticProps: GetStaticProps<{
|
||||||
|
posts: ComponentProps<typeof ListLayout>['posts']
|
||||||
|
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
|
||||||
|
pagination: ComponentProps<typeof ListLayout>['pagination']
|
||||||
|
}> = async () => {
|
||||||
const posts = await getAllFilesFrontMatter('blog')
|
const posts = await getAllFilesFrontMatter('blog')
|
||||||
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
|
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
|
||||||
const pagination = {
|
const pagination = {
|
||||||
@ -16,7 +22,11 @@ export async function getStaticProps() {
|
|||||||
return { props: { initialDisplayPosts, posts, pagination } }
|
return { props: { initialDisplayPosts, posts, pagination } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Blog({ posts, initialDisplayPosts, pagination }) {
|
export default function Blog({
|
||||||
|
posts,
|
||||||
|
initialDisplayPosts,
|
||||||
|
pagination,
|
||||||
|
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
|
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
|
@ -3,6 +3,10 @@ import PageTitle from '@/components/PageTitle'
|
|||||||
import generateRss from '@/lib/generate-rss'
|
import generateRss from '@/lib/generate-rss'
|
||||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||||
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
|
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'
|
||||||
|
|
||||||
@ -18,15 +22,23 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticProps({ params }) {
|
// @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 }
|
||||||
|
}> = async ({ params }) => {
|
||||||
|
const slug = (params.slug as string[]).join('/')
|
||||||
const allPosts = await getAllFilesFrontMatter('blog')
|
const allPosts = await getAllFilesFrontMatter('blog')
|
||||||
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/'))
|
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === slug)
|
||||||
const prev = allPosts[postIndex + 1] || null
|
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
|
||||||
const next = allPosts[postIndex - 1] || null
|
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
|
||||||
const post = await getFileBySlug('blog', params.slug.join('/'))
|
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 authorPromise = authorList.map(async (author) => {
|
||||||
const authorResults = await getFileBySlug('authors', [author])
|
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
|
||||||
return authorResults.frontMatter
|
return authorResults.frontMatter
|
||||||
})
|
})
|
||||||
const authorDetails = await Promise.all(authorPromise)
|
const authorDetails = await Promise.all(authorPromise)
|
||||||
@ -37,15 +49,27 @@ export async function getStaticProps({ params }) {
|
|||||||
fs.writeFileSync('./public/feed.xml', rss)
|
fs.writeFileSync('./public/feed.xml', rss)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { props: { post, authorDetails, prev, next } }
|
return {
|
||||||
|
props: {
|
||||||
|
post,
|
||||||
|
authorDetails,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Blog({ post, authorDetails, prev, next }) {
|
export default function Blog({
|
||||||
|
post,
|
||||||
|
authorDetails,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const { mdxSource, toc, frontMatter } = post
|
const { mdxSource, toc, frontMatter } = post
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{frontMatter.draft !== true ? (
|
{'draft' in frontMatter && frontMatter.draft !== true ? (
|
||||||
<MDXLayoutRenderer
|
<MDXLayoutRenderer
|
||||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||||
toc={toc}
|
toc={toc}
|
@ -3,8 +3,10 @@ import siteMetadata from '@/data/siteMetadata'
|
|||||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||||
import ListLayout from '@/layouts/ListLayout'
|
import ListLayout from '@/layouts/ListLayout'
|
||||||
import { POSTS_PER_PAGE } from '../../blog'
|
import { POSTS_PER_PAGE } from '../../blog'
|
||||||
|
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
|
||||||
const totalPosts = await getAllFilesFrontMatter('blog')
|
const totalPosts = await getAllFilesFrontMatter('blog')
|
||||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||||
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
||||||
@ -17,12 +19,16 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticProps(context) {
|
export const getStaticProps: GetStaticProps<{
|
||||||
|
posts: PostFrontMatter[]
|
||||||
|
initialDisplayPosts: PostFrontMatter[]
|
||||||
|
pagination: { currentPage: number; totalPages: number }
|
||||||
|
}> = async (context) => {
|
||||||
const {
|
const {
|
||||||
params: { page },
|
params: { page },
|
||||||
} = context
|
} = context
|
||||||
const posts = await getAllFilesFrontMatter('blog')
|
const posts = await getAllFilesFrontMatter('blog')
|
||||||
const pageNumber = parseInt(page)
|
const pageNumber = parseInt(page as string)
|
||||||
const initialDisplayPosts = posts.slice(
|
const initialDisplayPosts = posts.slice(
|
||||||
POSTS_PER_PAGE * (pageNumber - 1),
|
POSTS_PER_PAGE * (pageNumber - 1),
|
||||||
POSTS_PER_PAGE * pageNumber
|
POSTS_PER_PAGE * pageNumber
|
||||||
@ -41,7 +47,11 @@ export async function getStaticProps(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
|
export default function PostPage({
|
||||||
|
posts,
|
||||||
|
initialDisplayPosts,
|
||||||
|
pagination,
|
||||||
|
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
@ -4,25 +4,26 @@ import Tag from '@/components/Tag'
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||||
import formatDate from '@/lib/utils/formatDate'
|
import formatDate from '@/lib/utils/formatDate'
|
||||||
|
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
import NewsletterForm from '@/components/NewsletterForm'
|
import NewsletterForm from '@/components/NewsletterForm'
|
||||||
|
|
||||||
const MAX_DISPLAY = 5
|
const MAX_DISPLAY = 5
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
|
||||||
const posts = await getAllFilesFrontMatter('blog')
|
const posts = await getAllFilesFrontMatter('blog')
|
||||||
|
|
||||||
return { props: { posts } }
|
return { props: { posts } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ posts }) {
|
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
||||||
Hi.
|
Latest
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||||
{siteMetadata.description}
|
{siteMetadata.description}
|
@ -13,7 +13,7 @@ export default function Projects() {
|
|||||||
Projects
|
Projects
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||||
我的项目橱窗,欢迎交流。
|
Showcase your projects with a hero image (16 x 9)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="container py-12">
|
<div className="container py-12">
|
@ -4,14 +4,15 @@ import Tag from '@/components/Tag'
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import { getAllTags } from '@/lib/tags'
|
import { getAllTags } from '@/lib/tags'
|
||||||
import kebabCase from '@/lib/utils/kebabCase'
|
import kebabCase from '@/lib/utils/kebabCase'
|
||||||
|
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
|
||||||
const tags = await getAllTags('blog')
|
const tags = await getAllTags('blog')
|
||||||
|
|
||||||
return { props: { tags } }
|
return { props: { tags } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tags({ tags }) {
|
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -6,7 +6,9 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
|
|||||||
import { getAllTags } from '@/lib/tags'
|
import { getAllTags } from '@/lib/tags'
|
||||||
import kebabCase from '@/lib/utils/kebabCase'
|
import kebabCase from '@/lib/utils/kebabCase'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
|
|
||||||
@ -23,30 +25,33 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticProps({ params }) {
|
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const tag = context.params.tag as string
|
||||||
const allPosts = await getAllFilesFrontMatter('blog')
|
const allPosts = await getAllFilesFrontMatter('blog')
|
||||||
const filteredPosts = allPosts.filter(
|
const filteredPosts = allPosts.filter(
|
||||||
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
|
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
|
||||||
)
|
)
|
||||||
|
|
||||||
// rss
|
// rss
|
||||||
if (filteredPosts.length > 0) {
|
if (filteredPosts.length > 0) {
|
||||||
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
|
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
|
||||||
const rssPath = path.join(root, 'public', 'tags', params.tag)
|
const rssPath = path.join(root, 'public', 'tags', tag)
|
||||||
fs.mkdirSync(rssPath, { recursive: true })
|
fs.mkdirSync(rssPath, { recursive: true })
|
||||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { props: { posts: filteredPosts, tag: params.tag } }
|
return { props: { posts: filteredPosts, tag } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tag({ posts, tag }) {
|
export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
// Capitalize first letter and convert space to dash
|
// Capitalize first letter and convert space to dash
|
||||||
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
|
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TagSEO
|
<TagSEO
|
||||||
title={`${tag} - ${siteMetadata.author}`}
|
title={`${tag} - ${siteMetadata.title}`}
|
||||||
description={`${tag} tags - ${siteMetadata.author}`}
|
description={`${tag} tags - ${siteMetadata.author}`}
|
||||||
/>
|
/>
|
||||||
<ListLayout posts={posts} title={title} />
|
<ListLayout posts={posts} title={title} />
|
@ -43,7 +43,6 @@ const siteMetadata = require('../data/siteMetadata')
|
|||||||
.replace('.md', '')
|
.replace('.md', '')
|
||||||
.replace('/feed.xml', '')
|
.replace('/feed.xml', '')
|
||||||
const route = path === '/index' ? '' : path
|
const route = path === '/index' ? '' : path
|
||||||
|
|
||||||
if (page.search('pages/404.') > -1 || page.search(`pages/blog/[...slug].`) > -1) {
|
if (page.search('pages/404.') > -1 || page.search(`pages/blog/[...slug].`) > -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
// Adapted from https://github.com/hashicorp/next-remote-watch
|
// Adapated from https://github.com/hashicorp/next-remote-watch
|
||||||
// A copy of next-remote-watch with an additional ws reload emitter.
|
// A copy of next-remote-watch with an additional ws reload emitter.
|
||||||
// The app listens to the event and triggers a client-side router refresh
|
// The app listens to the event and triggers a client-side router refresh
|
||||||
// see components/ClientReload.js
|
// see components/ClientReload.js
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
const colors = require('tailwindcss/colors')
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
|
/** @type {import("tailwindcss/tailwind-config").TailwindConfig } */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizeUniversalDefaults: true,
|
optimizeUniversalDefaults: true,
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.js',
|
'./pages/**/*.tsx',
|
||||||
'./components/**/*.js',
|
'./components/**/*.tsx',
|
||||||
'./layouts/**/*.js',
|
'./layouts/**/*.tsx',
|
||||||
'./lib/**/*.js',
|
'./lib/**/*.ts',
|
||||||
'./data/**/*.mdx',
|
'./data/**/*.mdx',
|
||||||
],
|
],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
@ -29,7 +32,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: colors.teal,
|
primary: colors.teal,
|
||||||
gray: colors.neutral,
|
//@ts-ignore
|
||||||
|
gray: colors.neutral, // TODO: Remove ts-ignore after tw types gets updated to v3
|
||||||
},
|
},
|
||||||
typography: (theme) => ({
|
typography: (theme) => ({
|
||||||
DEFAULT: {
|
DEFAULT: {
|
||||||
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"target": "ES6",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/components/*": ["components/*"],
|
||||||
|
"@/data/*": ["data/*"],
|
||||||
|
"@/layouts/*": ["layouts/*"],
|
||||||
|
"@/lib/*": ["lib/*"],
|
||||||
|
"@/css/*": ["css/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
11
types/AuthorFrontMatter.ts
Normal file
11
types/AuthorFrontMatter.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export type AuthorFrontMatter = {
|
||||||
|
layout?: string
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
occupation: string
|
||||||
|
company: string
|
||||||
|
email: string
|
||||||
|
twitter: string
|
||||||
|
linkedin: string
|
||||||
|
github: string
|
||||||
|
}
|
14
types/PostFrontMatter.ts
Normal file
14
types/PostFrontMatter.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export type PostFrontMatter = {
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
tags: string[]
|
||||||
|
lastmod?: string
|
||||||
|
draft?: boolean
|
||||||
|
summary?: string
|
||||||
|
images?: string[]
|
||||||
|
authors?: string[]
|
||||||
|
layout?: string
|
||||||
|
canonicalUrl?: string
|
||||||
|
slug: string
|
||||||
|
fileName: string
|
||||||
|
}
|
5
types/Toc.ts
Normal file
5
types/Toc.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type Toc = {
|
||||||
|
value: string
|
||||||
|
depth: number
|
||||||
|
url: string
|
||||||
|
}[]
|
Loading…
Reference in New Issue
Block a user