Compare commits
No commits in common. "31a4cb3bd1455b3d0dd264cf6d6ddfeea503729d" and "11b9017a077e4c2e5d6ba05f45616b0aa33c8d2e" have entirely different histories.
31a4cb3bd1
...
11b9017a07
55
README.md
55
README.md
@ -12,6 +12,55 @@ Facing issues? Check the [FAQ page](https://github.com/timlrx/tailwind-nextjs-st
|
|||||||
|
|
||||||
Feature request? Check the past discussions to see if it has been brought up previously. Otherwise, feel free to start a new discussion thread. All ideas are welcomed!
|
Feature request? Check the past discussions to see if it has been brought up previously. Otherwise, feel free to start a new discussion thread. All ideas are welcomed!
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
- [Demo Blog](https://tailwind-nextjs-starter-blog.vercel.app/) - this repo
|
||||||
|
- [My personal blog](https://www.timlrx.com) - modified to auto-generate blog posts with dates
|
||||||
|
- [Aloisdg's cookbook](https://tambouille.vercel.app/) - with pictures and recipes!
|
||||||
|
- [GautierArcin's demo with next translate](https://tailwind-nextjs-starter-blog-seven.vercel.app/) - includes translation of mdx posts, [source code](https://github.com/GautierArcin/tailwind-nextjs-starter-blog/tree/demo/next-translate)
|
||||||
|
- [David Levai's digital garden](https://davidlevai.com/) - customized design and added email subscriptions
|
||||||
|
- [Thinh's Corner](https://thinhcorner.com/) - [customized layout](https://github.com/Th1nhNg0/th1nhng0.vercel.app/blob/5e73a420828d82f01e7147512a2c3273c4ec19f8/layouts/PostLayout.js) with sticky side table of contents
|
||||||
|
- [Leo's Blog](https://leohuynh.dev) - Tuan Anh Huynh's personal site. Add Snippets Page, Author Profile Card, Image Lightbox, ...
|
||||||
|
- [thvu.dev](https://thvu.dev) - Added `mdx-embed`, view count, reading minutes and more.
|
||||||
|
- [fiqrychoerudin.dev](https://www.fiqrychoerudin.dev/) - simple portfolio.
|
||||||
|
- [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
|
||||||
|
- [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
|
||||||
|
- [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
|
||||||
|
- [astrosaurus.me](https://astrosaurus.me/) - Ephraim Atta-Duncan's Personal 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)).
|
||||||
|
- [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))
|
||||||
|
- [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.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
I wanted to port my existing blog to Nextjs and Tailwind CSS but there was no easy out of the box template to use so I decided to create one. Design is adapted from [Tailwindlabs blog](https://github.com/tailwindlabs/blog.tailwindcss.com).
|
||||||
|
|
||||||
|
I wanted it to be nearly as feature-rich as popular blogging templates like [beautiful-jekyll](https://github.com/daattali/beautiful-jekyll) and [Hugo Academic](https://github.com/wowchemy/wowchemy-hugo-modules) but with the best of React's ecosystem and current web development's best practices.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Easy styling customization with [Tailwind 3.0](https://tailwindcss.com/blog/tailwindcss-v3) and primary color attribute
|
- Easy styling customization with [Tailwind 3.0](https://tailwindcss.com/blog/tailwindcss-v3) and primary color attribute
|
||||||
@ -179,10 +228,10 @@ Follow the interactive prompt to generate a post with pre-filled front matter.
|
|||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
**Vercel**
|
**Vercel**
|
||||||
The easiest way to deploy the template is to use the [Vercel Platform](https://vercel.com) from the creators of Next.js. Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
The easiest way to deploy the template is to use the [Vercel Platform](https://vercel.com) from the creators of Next.js. Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
|
|
||||||
**Netlify / GitHub Pages / Firebase etc.**
|
**Netlify / GitHub Pages / Firebase etc.**
|
||||||
As the template uses `next/image` for image optimization, additional configurations have to be made to deploy on other popular static hosting websites like [Netlify](https://www.netlify.com/) or [GitHub Pages](https://pages.github.com/). An alternative image optimization provider such as Imgix, Cloudinary or Akamai has to be used. Alternatively, replace the `next/image` component with a standard `<img>` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details.
|
As the template uses `next/image` for image optimization, additional configurations have to be made to deploy on other popular static hosting websites like [Netlify](https://www.netlify.com/) or [GitHub Pages](https://pages.github.com/). An alternative image optimization provider such as Imgix, Cloudinary or Akamai has to be used. Alternatively, replace the `next/image` component with a standard `<img>` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details.
|
||||||
|
|
||||||
The API routes used in the newsletter component cannot be used in a static site export. You will need to use a form API endpoint provider and substitute the route in the newsletter component accordingly. Other hosting platforms such as Netlify also offer alternative solutions - please refer to their docs for more information.
|
The API routes used in the newsletter component cannot be used in a static site export. You will need to use a form API endpoint provider and substitute the route in the newsletter component accordingly. Other hosting platforms such as Netlify also offer alternative solutions - please refer to their docs for more information.
|
||||||
@ -191,6 +240,6 @@ The API routes used in the newsletter component cannot be used in a static site
|
|||||||
|
|
||||||
Using the template? Support this effort by giving a star on GitHub, sharing your own blog and giving a shoutout on Twitter or becoming a project [sponsor](https://github.com/sponsors/timlrx).
|
Using the template? Support this effort by giving a star on GitHub, sharing your own blog and giving a shoutout on Twitter or becoming a project [sponsor](https://github.com/sponsors/timlrx).
|
||||||
|
|
||||||
## License
|
## 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.timlrx.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', () => {
|
socket.on('reload', (data) => {
|
||||||
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>
|
6
components/Image.js
Normal file
6
components/Image.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import NextImage from 'next/image'
|
||||||
|
|
||||||
|
// eslint-disable-next-line jsx-a11y/alt-text
|
||||||
|
const Image = ({ ...rest }) => <NextImage {...rest} />
|
||||||
|
|
||||||
|
export default Image
|
@ -1,5 +0,0 @@
|
|||||||
import NextImage, { ImageProps } from 'next/image'
|
|
||||||
|
|
||||||
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
|
|
||||||
|
|
||||||
export default Image
|
|
@ -6,13 +6,8 @@ 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'
|
|
||||||
|
|
||||||
interface Props {
|
const LayoutWrapper = ({ children }) => {
|
||||||
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,11 +1,7 @@
|
|||||||
/* 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 = ({
|
const CustomLink = ({ href, ...rest }) => {
|
||||||
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('#')
|
||||||
|
|
26
components/MDXComponents.js
Normal file
26
components/MDXComponents.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/* 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} />
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
/* 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,32 +31,39 @@ const MobileNav = () => {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-gray-900 dark:text-gray-100"
|
className="text-gray-900 dark:text-gray-100"
|
||||||
>
|
>
|
||||||
{navShow ? (
|
<path
|
||||||
<path
|
fillRule="evenodd"
|
||||||
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"
|
||||||
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"
|
||||||
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-24 right-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
|
className={`fixed top-0 left-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
|
||||||
navShow ? 'translate-x-0' : 'translate-x-full'
|
navShow ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<div className="flex justify-end">
|
||||||
type="button"
|
<button
|
||||||
aria-label="toggle modal"
|
type="button"
|
||||||
className="fixed h-full w-full cursor-auto focus:outline-none"
|
className="mr-5 mt-11 h-8 w-8 rounded"
|
||||||
onClick={onToggleNav}
|
aria-label="Toggle Menu"
|
||||||
></button>
|
onClick={onToggleNav}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<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 React, { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
|
||||||
const inputEl = useRef<HTMLInputElement>(null)
|
const inputEl = useRef(null)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [subscribed, setSubscribed] = useState(false)
|
const [subscribed, setSubscribed] = useState(false)
|
||||||
|
|
||||||
const subscribe = async (e: React.FormEvent<HTMLFormElement>) => {
|
const subscribe = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
|
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
|
@ -1,10 +1,4 @@
|
|||||||
import { ReactNode } from 'react'
|
export default function PageTitle({ children }) {
|
||||||
|
|
||||||
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,38 +1,33 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link'
|
||||||
|
|
||||||
interface Props {
|
export default function Pagination({ totalPages, currentPage }) {
|
||||||
totalPages: number
|
const prevPage = parseInt(currentPage) - 1 > 0
|
||||||
currentPage: number
|
const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages)
|
||||||
}
|
|
||||||
|
|
||||||
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 className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
<button rel="previous" 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>Previous</button>
|
<button rel="previous">Previous</button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{currentPage} of {totalPages}
|
{currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
{!nextPage && (
|
{!nextPage && (
|
||||||
<button className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
<button rel="next" 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>Next</button>
|
<button rel="next">Next</button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
@ -1,10 +1,6 @@
|
|||||||
import { useState, useRef, ReactNode } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
|
|
||||||
interface Props {
|
const Pre = (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)
|
||||||
@ -67,7 +63,7 @@ const Pre = ({ children }: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<pre>{children}</pre>
|
<pre>{props.children}</pre>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,31 +1,8 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
interface CommonSEOProps {
|
const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl }) => {
|
||||||
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>
|
||||||
@ -37,7 +14,7 @@ const CommonSEO = ({
|
|||||||
<meta property="og:site_name" content={siteMetadata.title} />
|
<meta property="og:site_name" content={siteMetadata.title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
{Array.isArray(ogImage) ? (
|
{ogImage.constructor.name === 'Array' ? (
|
||||||
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} />
|
||||||
@ -55,12 +32,7 @@ const CommonSEO = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageSEOProps {
|
export const PageSEO = ({ title, description }) => {
|
||||||
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 (
|
||||||
@ -74,7 +46,7 @@ export const PageSEO = ({ title, description }: PageSEOProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagSEO = ({ title, description }: PageSEOProps) => {
|
export const TagSEO = ({ title, description }) => {
|
||||||
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()
|
||||||
@ -99,11 +71,6 @@ export const TagSEO = ({ title, description }: PageSEOProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlogSeoProps extends PostFrontMatter {
|
|
||||||
authorDetails?: AuthorFrontMatter[]
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BlogSEO = ({
|
export const BlogSEO = ({
|
||||||
authorDetails,
|
authorDetails,
|
||||||
title,
|
title,
|
||||||
@ -113,10 +80,11 @@ 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()
|
||||||
const imagesArr =
|
let imagesArr =
|
||||||
images.length === 0
|
images.length === 0
|
||||||
? [siteMetadata.socialBanner]
|
? [siteMetadata.socialBanner]
|
||||||
: typeof images === 'string'
|
: typeof images === 'string'
|
||||||
@ -126,7 +94,7 @@ export const BlogSEO = ({
|
|||||||
const featuredImages = imagesArr.map((img) => {
|
const featuredImages = imagesArr.map((img) => {
|
||||||
return {
|
return {
|
||||||
'@type': 'ImageObject',
|
'@type': 'ImageObject',
|
||||||
url: `${siteMetadata.siteUrl}${img}`,
|
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const ScrollTopAndComment = () => {
|
const ScrollTopAndComment = () => {
|
||||||
@ -14,7 +15,7 @@ const ScrollTopAndComment = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleScrollTop = () => {
|
const handleScrollTop = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0 })
|
||||||
}
|
}
|
||||||
const handleScrollToComment = () => {
|
const handleScrollToComment = () => {
|
||||||
document.getElementById('comment').scrollIntoView()
|
document.getElementById('comment').scrollIntoView()
|
||||||
@ -23,20 +24,22 @@ 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'}`}
|
||||||
>
|
>
|
||||||
<button
|
{siteMetadata.comment.provider && (
|
||||||
aria-label="Scroll To Comment"
|
<button
|
||||||
type="button"
|
aria-label="Scroll To Comment"
|
||||||
onClick={handleScrollToComment}
|
type="button"
|
||||||
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"
|
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"
|
||||||
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
>
|
||||||
<path
|
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
fillRule="evenodd"
|
<path
|
||||||
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"
|
fillRule="evenodd"
|
||||||
clipRule="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"
|
||||||
/>
|
clipRule="evenodd"
|
||||||
</svg>
|
/>
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
aria-label="Scroll To Top"
|
aria-label="Scroll To Top"
|
||||||
type="button"
|
type="button"
|
3
components/SectionContainer.js
Normal file
3
components/SectionContainer.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
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>
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
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,27 +1,23 @@
|
|||||||
import { Toc } from 'types/Toc'
|
/**
|
||||||
|
* @typedef TocHeading
|
||||||
interface TOCInlineProps {
|
* @prop {string} value
|
||||||
toc: Toc
|
* @prop {number} depth
|
||||||
indentDepth?: number
|
* @prop {string} url
|
||||||
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 {TOCInlineProps} {
|
* @param {{
|
||||||
* toc,
|
* toc: TocHeading[],
|
||||||
* indentDepth = 3,
|
* indentDepth?: number,
|
||||||
* fromHeading = 1,
|
* fromHeading?: number,
|
||||||
* toHeading = 6,
|
* toHeading?: number,
|
||||||
* asDisclosure = false,
|
* asDisclosure?: boolean,
|
||||||
* exclude = '',
|
* exclude?: string|string[]
|
||||||
* }
|
* }} props
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const TOCInline = ({
|
const TOCInline = ({
|
||||||
@ -31,7 +27,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,11 +1,7 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import kebabCase from '@/lib/utils/kebabCase'
|
import kebabCase from '@/lib/utils/kebabCase'
|
||||||
|
|
||||||
interface Props {
|
const Tag = ({ text }) => {
|
||||||
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">
|
18
components/analytics/Posthog.js
Normal file
18
components/analytics/Posthog.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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,16 +2,9 @@ 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 = () => {
|
||||||
@ -21,6 +14,7 @@ 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,13 +1,8 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
interface Props {
|
const Disqus = ({ frontMatter }) => {
|
||||||
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'
|
||||||
@ -15,22 +10,18 @@ const Disqus = ({ frontMatter }: Props) => {
|
|||||||
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,11 +3,7 @@ import { useTheme } from 'next-themes'
|
|||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
interface Props {
|
const Giscus = () => {
|
||||||
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 =
|
||||||
@ -21,15 +17,30 @@ const Giscus = ({ mapping }: Props) => {
|
|||||||
|
|
||||||
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', siteMetadata.comment.giscusConfig.repo)
|
script.setAttribute('data-repo', repo)
|
||||||
script.setAttribute('data-repo-id', siteMetadata.comment.giscusConfig.repositoryId)
|
script.setAttribute('data-repo-id', repositoryId)
|
||||||
script.setAttribute('data-category', siteMetadata.comment.giscusConfig.category)
|
script.setAttribute('data-category', category)
|
||||||
script.setAttribute('data-category-id', siteMetadata.comment.giscusConfig.categoryId)
|
script.setAttribute('data-category-id', categoryId)
|
||||||
script.setAttribute('data-mapping', mapping)
|
script.setAttribute('data-mapping', mapping)
|
||||||
script.setAttribute('data-reactions-enabled', siteMetadata.comment.giscusConfig.reactions)
|
script.setAttribute('data-reactions-enabled', reactions)
|
||||||
script.setAttribute('data-emit-metadata', siteMetadata.comment.giscusConfig.metadata)
|
script.setAttribute('data-emit-metadata', 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
|
||||||
@ -41,7 +52,7 @@ const Giscus = ({ mapping }: Props) => {
|
|||||||
const comments = document.getElementById(COMMENTS_ID)
|
const comments = document.getElementById(COMMENTS_ID)
|
||||||
if (comments) comments.innerHTML = ''
|
if (comments) comments.innerHTML = ''
|
||||||
}
|
}
|
||||||
}, [commentsTheme, mapping])
|
}, [commentsTheme])
|
||||||
|
|
||||||
// Reload on theme change
|
// Reload on theme change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
@ -3,11 +3,7 @@ import { useTheme } from 'next-themes'
|
|||||||
|
|
||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
interface Props {
|
const Utterances = () => {
|
||||||
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 =
|
||||||
@ -22,7 +18,7 @@ const Utterances = ({ issueTerm }: Props) => {
|
|||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = 'https://utteranc.es/client.js'
|
script.src = 'https://utteranc.es/client.js'
|
||||||
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
|
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
|
||||||
script.setAttribute('issue-term', issueTerm)
|
script.setAttribute('issue-term', siteMetadata.comment.utterancesConfig.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')
|
||||||
@ -35,7 +31,7 @@ const Utterances = ({ issueTerm }: Props) => {
|
|||||||
const comments = document.getElementById(COMMENTS_ID)
|
const comments = document.getElementById(COMMENTS_ID)
|
||||||
if (comments) comments.innerHTML = ''
|
if (comments) comments.innerHTML = ''
|
||||||
}
|
}
|
||||||
}, [commentsTheme, issueTerm])
|
}, [commentsTheme])
|
||||||
|
|
||||||
// Reload on theme change
|
// Reload on theme change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
@ -1,10 +1,5 @@
|
|||||||
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(
|
||||||
() => {
|
() => {
|
||||||
@ -25,29 +20,14 @@ const DisqusComponent = dynamic(
|
|||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
const Comments = ({ frontMatter }: Props) => {
|
const Comments = ({ frontMatter }) => {
|
||||||
let term
|
const comment = siteMetadata?.comment
|
||||||
switch (
|
if (!comment || Object.keys(comment).length === 0) return <></>
|
||||||
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' && (
|
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && <GiscusComponent />}
|
||||||
<GiscusComponent mapping={term} />
|
|
||||||
)}
|
|
||||||
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
|
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
|
||||||
<UtterancesComponent issueTerm={term} />
|
<UtterancesComponent />
|
||||||
)}
|
)}
|
||||||
{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 {
|
||||||
|
18
data/projectsData.js
Normal file
18
data/projectsData.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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
|
@ -1,17 +0,0 @@
|
|||||||
interface ProjectData {
|
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
imgSrc?: string
|
|
||||||
href?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectsData: ProjectData[] = [
|
|
||||||
{
|
|
||||||
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: '',
|
provider: 'buttondown',
|
||||||
},
|
},
|
||||||
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: '', // supported providers: giscus, utterances, disqus
|
provider: 'giscus', // 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,6 +55,10 @@ 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,15 +1,8 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
interface Props {
|
export default function AuthorLayout({ children, frontMatter }) {
|
||||||
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 (
|
@ -1,17 +1,11 @@
|
|||||||
import Link from '@/components/Link'
|
import Link from '@/components/Link'
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag'
|
||||||
import { ComponentProps, useState } from 'react'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
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 }: Props) {
|
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
|
||||||
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(' ')
|
@ -7,9 +7,7 @@ import Tag from '@/components/Tag'
|
|||||||
import siteMetadata from '@/data/siteMetadata'
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
import Comments from '@/components/comments'
|
import Comments from '@/components/comments'
|
||||||
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
|
||||||
import { ReactNode, useMemo } from 'react'
|
import { 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) =>
|
||||||
@ -32,22 +30,9 @@ const Copyright = () => (
|
|||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
||||||
const postDateTemplate: Intl.DateTimeFormatOptions = {
|
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||||
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(
|
||||||
@ -138,8 +123,9 @@ 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 gap-4 pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
|
<div className="flex items-center 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,22 +6,13 @@ 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'
|
|
||||||
|
|
||||||
interface Props {
|
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||||
frontMatter: PostFrontMatter
|
const { date, title } = frontMatter
|
||||||
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/${slug}`} {...frontMatter} />
|
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
|
||||||
<ScrollTopAndComment />
|
<ScrollTopAndComment />
|
||||||
<article>
|
<article>
|
||||||
<div>
|
<div>
|
@ -1,9 +1,8 @@
|
|||||||
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: PostFrontMatter) => `
|
const generateRssItem = (post) => `
|
||||||
<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>
|
||||||
@ -15,7 +14,7 @@ const generateRssItem = (post: PostFrontMatter) => `
|
|||||||
</item>
|
</item>
|
||||||
`
|
`
|
||||||
|
|
||||||
const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
|
const generateRss = (posts, 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,10 +3,8 @@ 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'
|
||||||
@ -25,24 +23,24 @@ import rehypePresetMinify from 'rehype-preset-minify'
|
|||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
|
|
||||||
export function getFiles(type: 'blog' | 'authors') {
|
export function getFiles(type) {
|
||||||
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: string) {
|
export function formatSlug(slug) {
|
||||||
return slug.replace(/\.(mdx|md)/, '')
|
return slug.replace(/\.(mdx|md)/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateSortDesc(a: string, b: string) {
|
export function dateSortDesc(a, b) {
|
||||||
if (a > b) return -1
|
if (a > b) return -1
|
||||||
if (a < b) return 1
|
if (a < b) return 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string | string[]) {
|
export async function getFileBySlug(type, slug) {
|
||||||
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)
|
||||||
@ -56,7 +54,7 @@ export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string |
|
|||||||
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'bin', 'esbuild')
|
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'bin', 'esbuild')
|
||||||
}
|
}
|
||||||
|
|
||||||
const toc: Toc = []
|
let toc = []
|
||||||
|
|
||||||
const { code, frontmatter } = await bundleMDX({
|
const { code, frontmatter } = await bundleMDX({
|
||||||
source,
|
source,
|
||||||
@ -109,14 +107,14 @@ export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllFilesFrontMatter(folder: 'blog') {
|
export async function getAllFilesFrontMatter(folder) {
|
||||||
const prefixPaths = path.join(root, 'data', folder)
|
const prefixPaths = path.join(root, 'data', folder)
|
||||||
|
|
||||||
const files = getAllFilesRecursively(prefixPaths)
|
const files = getAllFilesRecursively(prefixPaths)
|
||||||
|
|
||||||
const allFrontMatter: PostFrontMatter[] = []
|
const allFrontMatter = []
|
||||||
|
|
||||||
files.forEach((file: string) => {
|
files.forEach((file) => {
|
||||||
// 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
|
||||||
@ -124,9 +122,8 @@ export async function getAllFilesFrontMatter(folder: 'blog') {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const source = fs.readFileSync(file, 'utf8')
|
const source = fs.readFileSync(file, 'utf8')
|
||||||
const matterFile = matter(source)
|
const { data: frontmatter } = matter(source)
|
||||||
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter
|
if (frontmatter.draft !== true) {
|
||||||
if ('draft' in frontmatter && frontmatter.draft !== true) {
|
|
||||||
allFrontMatter.push({
|
allFrontMatter.push({
|
||||||
...frontmatter,
|
...frontmatter,
|
||||||
slug: formatSlug(fileName),
|
slug: formatSlug(fileName),
|
@ -1,9 +1,8 @@
|
|||||||
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: Parent & { lang?: string }) =>
|
return (tree) =>
|
||||||
visit(tree, 'code', (node: Parent & { lang?: string }, index, parent: Parent) => {
|
visit(tree, 'code', (node, index, parent) => {
|
||||||
const nodeLang = node.lang || ''
|
const nodeLang = node.lang || ''
|
||||||
let language = ''
|
let language = ''
|
||||||
let title = ''
|
let title = ''
|
@ -1,12 +1,9 @@
|
|||||||
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: Parent, file: VFile) => {
|
return (tree, file) => {
|
||||||
visit(tree, 'yaml', (node: Parent) => {
|
visit(tree, 'yaml', (node, index, parent) => {
|
||||||
//@ts-ignore
|
|
||||||
file.data.frontmatter = load(node.value)
|
file.data.frontmatter = load(node.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,24 +1,15 @@
|
|||||||
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: Node) => {
|
return (tree) => {
|
||||||
visit(
|
visit(
|
||||||
tree,
|
tree,
|
||||||
// only visit p tags that contain an img element
|
// only visit p tags that contain an img element
|
||||||
(node: Parent): node is Parent =>
|
(node) => node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
|
||||||
node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
|
(node) => {
|
||||||
(node: Parent) => {
|
const imageNode = node.children.find((n) => n.type === 'image')
|
||||||
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,12 +1,10 @@
|
|||||||
//@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: Parent) =>
|
return (tree) =>
|
||||||
visit(tree, 'heading', (node) => {
|
visit(tree, 'heading', (node, index, parent) => {
|
||||||
const textContent = toString(node)
|
const textContent = toString(node)
|
||||||
options.exportRef.push({
|
options.exportRef.push({
|
||||||
value: textContent,
|
value: textContent,
|
@ -1,4 +1,3 @@
|
|||||||
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'
|
||||||
@ -7,15 +6,14 @@ import kebabCase from './utils/kebabCase'
|
|||||||
|
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
|
|
||||||
export async function getAllTags(type: 'blog' | 'authors') {
|
export async function getAllTags(type) {
|
||||||
const files = getFiles(type)
|
const files = await getFiles(type)
|
||||||
|
|
||||||
const tagCount: Record<string, number> = {}
|
let tagCount = {}
|
||||||
// Iterate through each post, putting all found tags into `tags`
|
// Iterate through each post, putting all found tags into `tags`
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
|
const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8')
|
||||||
const matterFile = matter(source)
|
const { data } = 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: string) => {
|
const walkDir = (fullPath) => {
|
||||||
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
|
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathJoinPrefix = (prefix: string) => (extraPath: string) => path.join(prefix, extraPath)
|
const pathJoinPrefix = (prefix) => (extraPath) => path.join(prefix, extraPath)
|
||||||
|
|
||||||
const getAllFilesRecursively = (folder: string): string[] =>
|
const getAllFilesRecursively = (folder) =>
|
||||||
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: string) => {
|
const formatDate = (date) => {
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options = {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
@ -1,6 +1,7 @@
|
|||||||
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 = {
|
||||||
@ -10,7 +11,7 @@ const esca = {
|
|||||||
"'": ''',
|
"'": ''',
|
||||||
'"': '"',
|
'"': '"',
|
||||||
}
|
}
|
||||||
const pe = (m: keyof typeof esca) => esca[m]
|
const pe = (m) => esca[m]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
|
* Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
|
||||||
@ -19,4 +20,4 @@ const pe = (m: keyof typeof esca) => esca[m]
|
|||||||
* the input type is unexpected, except for boolean and numbers,
|
* the input type is unexpected, except for boolean and numbers,
|
||||||
* converted as string.
|
* converted as string.
|
||||||
*/
|
*/
|
||||||
export const escape = (es: string): string => replace.call(es, ca, pe)
|
export const escape = (es) => replace.call(es, ca, pe)
|
@ -1,5 +1,5 @@
|
|||||||
import { slug } from 'github-slugger'
|
import { slug } from 'github-slugger'
|
||||||
|
|
||||||
const kebabCase = (str: string) => slug(str)
|
const kebabCase = (str) => slug(str)
|
||||||
|
|
||||||
export default kebabCase
|
export default kebabCase
|
5
next-env.d.ts
vendored
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
|||||||
/// <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,12 +52,9 @@ const securityHeaders = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import('next/dist/next-server/server/config').NextConfig}
|
|
||||||
**/
|
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
|
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
|
||||||
eslint: {
|
eslint: {
|
||||||
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
||||||
},
|
},
|
||||||
@ -73,19 +70,6 @@ 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.
84
package.json
84
package.json
@ -12,61 +12,55 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "4.5.13",
|
"@next/bundle-analyzer": "12.1.4",
|
||||||
"@mailchimp/mailchimp_marketing": "^3.0.78",
|
"@fontsource/inter": "4.5.2",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@mailchimp/mailchimp_marketing": "^3.0.58",
|
||||||
"@tailwindcss/typography": "^0.5.7",
|
"@tailwindcss/forms": "^0.4.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"@tailwindcss/typography": "^0.5.0",
|
||||||
"esbuild": "^0.14.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"github-slugger": "^1.4.0",
|
"esbuild": "^0.13.13",
|
||||||
"gray-matter": "^4.0.3",
|
"github-slugger": "^1.3.0",
|
||||||
"image-size": "1.0.2",
|
"gray-matter": "^4.0.2",
|
||||||
"mdx-bundler": "^9.0.1",
|
"image-size": "1.0.0",
|
||||||
"next": "12.3.1",
|
"mdx-bundler": "^8.0.0",
|
||||||
"next-themes": "^0.2.1",
|
"next": "12.1.4",
|
||||||
"postcss": "^8.4.17",
|
"next-themes": "^0.0.14",
|
||||||
"preact": "^10.11.1",
|
"postcss": "^8.4.5",
|
||||||
"react": "18.2.0",
|
"preact": "^10.6.2",
|
||||||
"react-dom": "18.2.0",
|
"react": "17.0.2",
|
||||||
"reading-time": "1.5.0",
|
"react-dom": "17.0.2",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"reading-time": "1.3.0",
|
||||||
|
"rehype-autolink-headings": "^6.1.0",
|
||||||
"rehype-citation": "^0.4.0",
|
"rehype-citation": "^0.4.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.5.0",
|
"rehype-prism-plus": "^1.1.3",
|
||||||
"rehype-slug": "^5.0.1",
|
"rehype-slug": "^5.0.0",
|
||||||
"remark-footnotes": "^4.0.1",
|
"remark-footnotes": "^4.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"sharp": "^0.31.1",
|
"sharp": "^0.28.3",
|
||||||
"smoothscroll-polyfill": "^0.4.4",
|
"tailwindcss": "^3.0.23",
|
||||||
"tailwindcss": "^3.1.8",
|
"unist-util-visit": "^4.0.0"
|
||||||
"unist-util-visit": "^4.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "12.3.1",
|
"@svgr/webpack": "^6.1.2",
|
||||||
"@svgr/webpack": "^6.4.0",
|
|
||||||
"@types/react": "^18.0.21",
|
|
||||||
"@types/tailwindcss": "^3.0.11",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
|
||||||
"@typescript-eslint/parser": "^5.39.0",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dedent": "^0.7.0",
|
"dedent": "^0.7.0",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^7.29.0",
|
||||||
"eslint-config-next": "12.3.1",
|
"eslint-config-next": "12.1.4",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.0.0",
|
||||||
"globby": "13.1.2",
|
"globby": "11.0.3",
|
||||||
"husky": "^8.0.1",
|
"husky": "^6.0.0",
|
||||||
"inquirer": "^9.1.3",
|
"inquirer": "^8.1.1",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^11.0.0",
|
||||||
"next-remote-watch": "^2.0.0",
|
"next-remote-watch": "^1.0.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"prettier-plugin-tailwindcss": "^0.1.4",
|
||||||
"socket.io": "^4.5.2",
|
"socket.io": "^4.4.0",
|
||||||
"socket.io-client": "^4.5.2",
|
"socket.io-client": "^4.4.0"
|
||||||
"typescript": "^4.8.4"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.+(js|jsx|ts|tsx)": [
|
"*.+(js|jsx|ts|tsx)": [
|
||||||
|
31
pages/404.js
Normal file
31
pages/404.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,10 +2,9 @@ import '@/css/tailwind.css'
|
|||||||
import '@/css/prism.css'
|
import '@/css/prism.css'
|
||||||
import 'katex/dist/katex.css'
|
import 'katex/dist/katex.css'
|
||||||
|
|
||||||
import '@fontsource/inter/variable.css'
|
import '@fontsource/inter/variable-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'
|
||||||
@ -16,7 +15,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 }: AppProps) {
|
export default function App({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
|
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
|
||||||
<Head>
|
<Head>
|
@ -1,5 +1,4 @@
|
|||||||
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 (
|
21
pages/about.js
Normal file
21
pages/about.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
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,7 +1,5 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
export default async (req, res) => {
|
||||||
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,7 +1,5 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
/* eslint-disable import/no-anonymous-default-export */
|
/* eslint-disable import/no-anonymous-default-export */
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
export default async (req, res) => {
|
||||||
const { email } = req.body
|
const { email } = req.body
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
33
pages/api/emailoctopus.js
Normal file
33
pages/api/emailoctopus.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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,7 +1,5 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
/* eslint-disable import/no-anonymous-default-export */
|
/* eslint-disable import/no-anonymous-default-export */
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
export default async (req, res) => {
|
||||||
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,4 +1,3 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import mailchimp from '@mailchimp/mailchimp_marketing'
|
import mailchimp from '@mailchimp/mailchimp_marketing'
|
||||||
|
|
||||||
mailchimp.setConfig({
|
mailchimp.setConfig({
|
||||||
@ -7,7 +6,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: NextApiRequest, res: NextApiResponse) => {
|
export default async (req, res) => {
|
||||||
const { email } = req.body
|
const { email } = req.body
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -15,7 +14,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
const test = await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
||||||
email_address: email,
|
email_address: email,
|
||||||
status: 'subscribed',
|
status: 'subscribed',
|
||||||
})
|
})
|
30
pages/api/revue.js
Normal file
30
pages/api/revue.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// 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,16 +2,10 @@ 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 const getStaticProps: GetStaticProps<{
|
export async function 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 = {
|
||||||
@ -22,11 +16,7 @@ export const getStaticProps: GetStaticProps<{
|
|||||||
return { props: { initialDisplayPosts, posts, pagination } }
|
return { props: { initialDisplayPosts, posts, pagination } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Blog({
|
export default function Blog({ posts, initialDisplayPosts, pagination }) {
|
||||||
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,10 +3,6 @@ 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'
|
||||||
|
|
||||||
@ -22,23 +18,15 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
export async function getStaticProps({ params }) {
|
||||||
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) === slug)
|
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/'))
|
||||||
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
|
const prev = allPosts[postIndex + 1] || null
|
||||||
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
|
const next = allPosts[postIndex - 1] || null
|
||||||
const post = await getFileBySlug<PostFrontMatter>('blog', slug)
|
const post = await getFileBySlug('blog', params.slug.join('/'))
|
||||||
// @ts-ignore
|
|
||||||
const authorList = post.frontMatter.authors || ['default']
|
const authorList = post.frontMatter.authors || ['default']
|
||||||
const authorPromise = authorList.map(async (author) => {
|
const authorPromise = authorList.map(async (author) => {
|
||||||
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
|
const authorResults = await getFileBySlug('authors', [author])
|
||||||
return authorResults.frontMatter
|
return authorResults.frontMatter
|
||||||
})
|
})
|
||||||
const authorDetails = await Promise.all(authorPromise)
|
const authorDetails = await Promise.all(authorPromise)
|
||||||
@ -49,27 +37,15 @@ export const getStaticProps: GetStaticProps<{
|
|||||||
fs.writeFileSync('./public/feed.xml', rss)
|
fs.writeFileSync('./public/feed.xml', rss)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { props: { post, authorDetails, prev, next } }
|
||||||
props: {
|
|
||||||
post,
|
|
||||||
authorDetails,
|
|
||||||
prev,
|
|
||||||
next,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Blog({
|
export default function Blog({ post, authorDetails, prev, next }) {
|
||||||
post,
|
|
||||||
authorDetails,
|
|
||||||
prev,
|
|
||||||
next,
|
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
|
||||||
const { mdxSource, toc, frontMatter } = post
|
const { mdxSource, toc, frontMatter } = post
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{'draft' in frontMatter && frontMatter.draft !== true ? (
|
{frontMatter.draft !== true ? (
|
||||||
<MDXLayoutRenderer
|
<MDXLayoutRenderer
|
||||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||||
toc={toc}
|
toc={toc}
|
@ -3,10 +3,8 @@ 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 const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
|
export async function getStaticPaths() {
|
||||||
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) => ({
|
||||||
@ -19,16 +17,12 @@ export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<{
|
export async function getStaticProps(context) {
|
||||||
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 as string)
|
const pageNumber = parseInt(page)
|
||||||
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
|
||||||
@ -47,11 +41,7 @@ export const getStaticProps: GetStaticProps<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostPage({
|
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
|
||||||
posts,
|
|
||||||
initialDisplayPosts,
|
|
||||||
pagination,
|
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
@ -4,33 +4,32 @@ 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 const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
|
export async function getStaticProps() {
|
||||||
const posts = await getAllFilesFrontMatter('blog')
|
const posts = await getAllFilesFrontMatter('blog')
|
||||||
|
|
||||||
return { props: { posts } }
|
return { props: { posts } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Home({ posts }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
<div className="space-y-2 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.
|
||||||
</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}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{!posts.length && '没有找到文章。 😭'}
|
{!posts.length && 'No posts found.'}
|
||||||
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
|
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
|
||||||
const { slug, date, title, summary, tags } = frontMatter
|
const { slug, date, title, summary, tags } = frontMatter
|
||||||
return (
|
return (
|
@ -4,15 +4,14 @@ 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 const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
|
export async function getStaticProps() {
|
||||||
const tags = await getAllTags('blog')
|
const tags = await getAllTags('blog')
|
||||||
|
|
||||||
return { props: { tags } }
|
return { props: { tags } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Tags({ tags }) {
|
||||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -6,9 +6,7 @@ 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()
|
||||||
|
|
||||||
@ -25,33 +23,30 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
|
export async function getStaticProps({ params }) {
|
||||||
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(tag)
|
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
|
||||||
)
|
)
|
||||||
|
|
||||||
// rss
|
// rss
|
||||||
if (filteredPosts.length > 0) {
|
if (filteredPosts.length > 0) {
|
||||||
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
|
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
|
||||||
const rssPath = path.join(root, 'public', 'tags', tag)
|
const rssPath = path.join(root, 'public', 'tags', params.tag)
|
||||||
fs.mkdirSync(rssPath, { recursive: true })
|
fs.mkdirSync(rssPath, { recursive: true })
|
||||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { props: { posts: filteredPosts, tag } }
|
return { props: { posts: filteredPosts, tag: params.tag } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Tag({ posts, tag }) {
|
||||||
// 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.title}`}
|
title={`${tag} - ${siteMetadata.author}`}
|
||||||
description={`${tag} tags - ${siteMetadata.author}`}
|
description={`${tag} tags - ${siteMetadata.author}`}
|
||||||
/>
|
/>
|
||||||
<ListLayout posts={posts} title={title} />
|
<ListLayout posts={posts} title={title} />
|
@ -43,6 +43,7 @@ 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,18 +1,15 @@
|
|||||||
// @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/**/*.tsx',
|
'./pages/**/*.js',
|
||||||
'./components/**/*.tsx',
|
'./components/**/*.js',
|
||||||
'./layouts/**/*.tsx',
|
'./layouts/**/*.js',
|
||||||
'./lib/**/*.ts',
|
'./lib/**/*.js',
|
||||||
'./data/**/*.mdx',
|
'./data/**/*.mdx',
|
||||||
],
|
],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
@ -32,8 +29,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: colors.teal,
|
primary: colors.teal,
|
||||||
//@ts-ignore
|
gray: colors.neutral,
|
||||||
gray: colors.neutral, // TODO: Remove ts-ignore after tw types gets updated to v3
|
|
||||||
},
|
},
|
||||||
typography: (theme) => ({
|
typography: (theme) => ({
|
||||||
DEFAULT: {
|
DEFAULT: {
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
export type AuthorFrontMatter = {
|
|
||||||
layout?: string
|
|
||||||
name: string
|
|
||||||
avatar: string
|
|
||||||
occupation: string
|
|
||||||
company: string
|
|
||||||
email: string
|
|
||||||
twitter: string
|
|
||||||
linkedin: string
|
|
||||||
github: string
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export type Toc = {
|
|
||||||
value: string
|
|
||||||
depth: number
|
|
||||||
url: string
|
|
||||||
}[]
|
|
Loading…
Reference in New Issue
Block a user