Compare commits

...

32 commits

Author SHA1 Message Date
Timothy Lin 16e4eaee28 chore: sync with v1.1.0 2021-08-15 23:43:19 +08:00
Timothy Lin de3de08bbc fix: use tsx not jsx 2021-08-08 16:37:18 +08:00
Timothy Lin 4ce77b36c9 chore: sync with v1.0.0 2021-08-08 16:30:49 +08:00
Timothy Lin 742bfdd0e1 fix: use tsx instead of js 2021-08-07 00:03:52 +08:00
Timothy Lin 4f85c2ccad chore: sync with v1.0.0-canary.2 2021-08-06 23:57:48 +08:00
Timothy Lin 1be25408ee chore: upgrade mdx-bundler 2021-07-25 19:12:35 +08:00
Timothy Lin c979da10e1 chore: sync with v1.0.0-canary.1 2021-07-24 14:53:22 +08:00
Timothy c4267ea934
Merge pull request #112 from GautierArcin/typescript
Add typings to lib
2021-07-18 16:34:35 +08:00
Gautier Arcin c6b0f479a9 chore: removed unused variable 2021-07-16 22:38:13 +02:00
Gautier Arcin afeec47c6e chore: minor typing 2021-07-16 22:37:39 +02:00
Juliano Farias 7eb7aac36d Merge branch 'v1' into typescript 2021-07-16 19:52:23 +02:00
Timothy Lin 030d630189 chore: sync with v1.0.0-canary.0 js 2021-07-13 23:18:01 +08:00
Timothy b2755d3fa2
Merge pull request #109 from rsipakov/ts-feed-xml
File index.xml renamed to feed.xml to avoid a 404 page in the development stage
2021-07-11 23:15:33 +08:00
Rostyslav a634839ce6 rename index.xml to feed.xml 2021-07-10 03:15:10 -04:00
Rostyslav 4990ba98d8 File index.xml renamed to feed.xml to avoid a 404 page in the development stage 2021-07-10 02:58:23 -04:00
Rostyslav 08f0fb16e9 typescript 2021-07-09 19:55:16 -04:00
Rostyslav 86f6c14469 Update package-lock.json 2021-07-09 16:56:01 -04:00
Juliano Farias 2f2ae049cd
Merge pull request #99 from frontendwizard/v1-typescript 2021-07-07 09:19:47 +02:00
Juliano Farias 755fbfcfb1 docs: change quick start guide instructions for typescript branch 2021-07-07 09:17:52 +02:00
Juliano Farias 70668b65cb fix: improve mdx.ts types and fix build 2021-07-07 09:12:49 +02:00
Juliano Farias 81bc20bfc3 fix: update MDXComponents types 2021-07-05 18:07:01 +02:00
Juliano Farias 3ec5bd5996 Merge branch 'v1' into v1-typescript 2021-07-05 17:52:08 +02:00
Juliano Farias 41e9b02a2b make sure all pages infer types from getStaticToProps 2021-06-30 14:20:27 +02:00
Juliano Farias 1c87e48735 fix: improve about page typing and pass frontMatter down 2021-06-30 13:44:52 +02:00
Juliano Farias 0870d15994 chore: remove console.log 2021-06-30 13:35:18 +02:00
Juliano Farias 68bda2ba72 chore: remove console.log 2021-06-30 13:34:42 +02:00
Juliano Farias a9becc7c83 fix: cast params.tag as string 2021-06-30 13:32:39 +02:00
Juliano Farias 930860dc52 chore: improve types on blog pages 2021-06-30 13:30:12 +02:00
Juliano Farias c673d4ea38 chore: make pagination data numbers instead of string 2021-06-30 12:11:25 +02:00
Juliano Farias ece93b9ff4 chore: finish typing blog.tsx 2021-06-30 12:10:48 +02:00
Juliano Farias 4e5f6de1e9 fix typescript errors 2021-06-30 12:04:46 +02:00
Juliano Farias c28652d823 refactor: move to typescript
Fix import on generate-rss.ts
2021-06-30 12:01:28 +02:00
75 changed files with 3640 additions and 2102 deletions

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
NEXT_PUBLIC_GISCUS_REPO=
NEXT_PUBLIC_GISCUS_REPOSITORY_ID=
NEXT_PUBLIC_GISCUS_CATEGORY=
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
NEXT_PUBLIC_UTTERANCES_REPO=
NEXT_PUBLIC_DISQUS_SHORTNAME=

View file

@ -1,13 +1,17 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
env: {
browser: true,
amd: true,
node: true,
es6: true,
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended',
'next',
@ -27,5 +31,8 @@ module.exports = {
'react/prop-types': 0,
'no-unused-vars': 0,
'react/no-unescaped-entities': 0,
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
}

4
.gitignore vendored
View file

@ -18,7 +18,7 @@ public/sitemap.xml
/build
*.xml
# rss feed
/public/feed.xml
/public/feed.xml
# misc
.DS_Store
@ -33,4 +33,4 @@ yarn-error.log*
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.production.local

View file

@ -5,25 +5,30 @@
[![GitHub Repo stars](https://img.shields.io/github/stars/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/stargazers/)
[![GitHub forks](https://img.shields.io/github/forks/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/network/)
[![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftimlrxx)](https://twitter.com/timlrxx)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/timlrx)](https://github.com/sponsors/timlrx)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/timlrx/tailwind-nextjs-starter-blog)
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature rich nextjs markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
Check out the documentation below to get started. Facing issues? Checkout of the [FAQ page](https://github.com/timlrx/tailwind-nextjs-starter-blog/wiki) and do a search on past issues. Feel free to open a new issue if none has been posted previously.
Check out the documentation below to get started.
Facing issues? Check the [FAQ page](https://github.com/timlrx/tailwind-nextjs-starter-blog/wiki) and do a search on past issues. Feel free to open a new issue if none has been posted previously.
Feature request? Check the past discussions to see if it has been brough 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!
- [GauthierArcin'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)
Using the template? Happy to accept any PR with modifications made e.g. sub-paths, localization or multiple authors
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.
It is inspired by [Lee Robinson's blog](https://github.com/leerob/leerob.io), but focuses only on static site generation. Design is adapted from [Tailwindlabs blog](https://github.com/tailwindlabs/blog.tailwindcss.com).
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.
@ -31,9 +36,10 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
- Easy styling customization with [Tailwind 2.0](https://blog.tailwindcss.com/tailwindcss-v2) and primary color attribute
- Near perfect lighthouse score - [Lighthouse report](https://www.webpagetest.org/result/210111_DiC1_08f3670c3430bf4a9b76fc3b927716c5/)
- Lightweight, 38kB first load JS, uses Preact in production build
- Lightweight, 39kB first load JS, uses Preact in production build
- Mobile-friendly view
- Light and dark theme
- Supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics
- [MDX - write JSX in markdown documents!](https://mdxjs.com/)
- Server-side syntax highlighting with line numbers and line highlighting via [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus)
- Math display supported via [KaTeX](https://katex.org/)
@ -42,7 +48,9 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
- Support for tags - each unique tag will be its own page
- Support for multiple authors
- Blog templates
- TOC component
- Support for nested routing of blog posts
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
- Projects page
- SEO friendly with RSS feed, sitemaps and more!
@ -58,7 +66,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
## Quick Start Guide
1. JS (official support) - `npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git` or TS (community support) - `npx degit timlrx/tailwind-nextjs-starter-blog#typescript`
2. Personalize `siteMetadata.json` (site related information)
2. Personalize `siteMetadata.js` (site related information)
3. Personalize `authors/default.md` (main author)
4. Modify `projectsData.js`
5. Modify `headerNavLinks.js` to customize navigation links
@ -81,7 +89,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
## Extend / Customize
`data/siteMetadata.json` - contains most of the site related information which should be modified for a user's need.
`data/siteMetadata.js` - contains most of the site related information which should be modified for a user's need.
`data/authors/default.md` - default author information (required). Additional authors can be added as files in `data/authors`.
@ -154,3 +162,11 @@ The easiest way to deploy the template is to use the [Vercel Platform](https://v
**Netlify / Github Pages / Firebase etc.**
As the template uses `next/image` for image optimization, additional configurations has 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.
## Support
Using the template? Support this effort by giving a star on Github, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
## Licence
[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timrlx.com)

View file

@ -7,12 +7,12 @@ export default function Footer() {
<footer>
<div className="flex flex-col items-center mt-16">
<div className="flex mb-3 space-x-4">
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size="6" />
<SocialIcon kind="github" href={siteMetadata.github} size="6" />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size="6" />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size="6" />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size="6" />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size="6" />
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
</div>
<div className="flex mb-2 space-x-2 text-sm text-gray-500 dark:text-gray-400">
<div>{siteMetadata.author}</div>

View file

@ -1,6 +0,0 @@
import NextImage from 'next/image'
// eslint-disable-next-line jsx-a11y/alt-text
const Image = ({ ...rest }) => <NextImage {...rest} />
export default Image

5
components/Image.tsx Normal file
View file

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

View file

@ -6,8 +6,13 @@ import SectionContainer from './SectionContainer'
import Footer from './Footer'
import MobileNav from './MobileNav'
import ThemeSwitch from './ThemeSwitch'
import { ReactNode } from 'react'
const LayoutWrapper = ({ children }) => {
interface Props {
children: ReactNode
}
const LayoutWrapper = ({ children }: Props) => {
return (
<SectionContainer>
<div className="flex flex-col justify-between h-screen">

View file

@ -1,7 +1,11 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import Link from 'next/link'
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
const CustomLink = ({ href, ...rest }) => {
const CustomLink = ({
href,
...rest
}: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
const isInternalLink = href && href.startsWith('/')
const isAnchorLink = href && href.startsWith('#')

View file

@ -1,22 +0,0 @@
/* eslint-disable react/display-name */
import { useMemo } from 'react'
import { getMDXComponent } from 'mdx-bundler/client'
import Image from './Image'
import CustomLink from './Link'
import Pre from './Pre'
export const MDXComponents = {
Image,
a: CustomLink,
pre: Pre,
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} />
}

View file

@ -0,0 +1,33 @@
/* 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'
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,
}
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} />
}

View file

@ -1,4 +1,10 @@
export default function PageTitle({ children }) {
import { ReactNode } from 'react'
interface Props {
children: ReactNode
}
export default function PageTitle({ children }: Props) {
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">
{children}

View file

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

View file

@ -1,6 +1,10 @@
import { useState, useRef } from 'react'
import { useState, useRef, ReactNode } from 'react'
const Pre = (props) => {
interface Props {
children: ReactNode
}
const Pre = ({ children }: Props) => {
const textInput = useRef(null)
const [hovered, setHovered] = useState(false)
const [copied, setCopied] = useState(false)
@ -14,7 +18,7 @@ const Pre = (props) => {
}
const onCopy = () => {
setCopied(true)
navigator.clipboard.writeText(textInput.current.textContent)
navigator.clipboard.writeText(textInput.current.innerText)
setTimeout(() => {
setCopied(false)
}, 2000)
@ -63,7 +67,7 @@ const Pre = (props) => {
</button>
)}
<pre>{props.children}</pre>
<pre>{children}</pre>
</div>
)
}

View file

@ -1,112 +0,0 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import siteMetadata from '@/data/siteMetadata'
export const PageSeo = ({ title, description }) => {
const router = useRouter()
return (
<Head>
<title>{`${title}`}</title>
<meta name="robots" content="follow, index" />
<meta name="description" content={description} />
<meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:image" content={`${siteMetadata.siteUrl}${siteMetadata.socialBanner}`} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteMetadata.twitter} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={`${siteMetadata.siteUrl}${siteMetadata.socialBanner}`} />
</Head>
)
}
export const BlogSeo = ({ authorDetails, title, summary, date, lastmod, url, images = [] }) => {
const router = useRouter()
const publishedAt = new Date(date).toISOString()
const modifiedAt = new Date(lastmod || date).toISOString()
let imagesArr =
images.length === 0
? [siteMetadata.socialBanner]
: typeof images === 'string'
? [images]
: images
const featuredImages = imagesArr.map((img) => {
return {
'@type': 'ImageObject',
url: `${siteMetadata.siteUrl}${img}`,
}
})
let authorList
if (authorDetails) {
authorList = authorDetails.map((author) => {
return {
'@type': 'Person',
name: author.name,
}
})
} else {
authorList = {
'@type': 'Person',
name: siteMetadata.author,
}
}
const structuredData = {
'@context': 'https://schema.org',
'@type': 'Article',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
headline: title,
image: featuredImages,
datePublished: publishedAt,
dateModified: modifiedAt,
author: authorList,
publisher: {
'@type': 'Organization',
name: siteMetadata.author,
logo: {
'@type': 'ImageObject',
url: `${siteMetadata.siteUrl}${siteMetadata.siteLogo}`,
},
},
description: summary,
}
return (
<>
<Head>
<title>{`${title}`}</title>
<meta name="robots" content="follow, index" />
<meta name="description" content={summary} />
<meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
<meta property="og:type" content="article" />
<meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:description" content={summary} />
<meta property="og:title" content={title} />
{featuredImages.map((img) => (
<meta property="og:image" content={img.url} key={img.url} />
))}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteMetadata.twitter} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={summary} />
<meta name="twitter:image" content={featuredImages[0].url} />
{date && <meta property="article:published_time" content={publishedAt} />}
{lastmod && <meta property="article:modified_time" content={modifiedAt} />}
<link rel="canonical" href={`${siteMetadata.siteUrl}${router.asPath}`} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData, null, 2) }}
/>
</Head>
</>
)
}

183
components/SEO.tsx Normal file
View file

@ -0,0 +1,183 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import siteMetadata from '@/data/siteMetadata'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { PostFrontMatter } from 'types/PostFrontMatter'
interface CommonSEOProps {
title: string
description: string
ogType: string
ogImage:
| string
| {
'@type': string
url: string
}[]
twImage: string
}
const CommonSEO = ({ title, description, ogType, ogImage, twImage }: CommonSEOProps) => {
const router = useRouter()
return (
<Head>
<title>{title}</title>
<meta name="robots" content="follow, index" />
<meta name="description" content={description} />
<meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:description" content={description} />
<meta property="og:title" content={title} />
{Array.isArray(ogImage) ? (
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
) : (
<meta property="og:image" content={ogImage} key={ogImage} />
)}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteMetadata.twitter} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={twImage} />
</Head>
)
}
interface PageSEOProps {
title: string
description: string
}
export const PageSEO = ({ title, description }: PageSEOProps) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
return (
<CommonSEO
title={title}
description={description}
ogType="website"
ogImage={ogImageUrl}
twImage={twImageUrl}
/>
)
}
export const TagSEO = ({ title, description }: PageSEOProps) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const router = useRouter()
return (
<>
<CommonSEO
title={title}
description={description}
ogType="website"
ogImage={ogImageUrl}
twImage={twImageUrl}
/>
<Head>
<link
rel="alternate"
type="application/rss+xml"
title={`${description} - RSS feed`}
href={`${siteMetadata.siteUrl}${router.asPath}/feed.xml`}
/>
</Head>
</>
)
}
interface BlogSeoProps extends PostFrontMatter {
authorDetails?: AuthorFrontMatter[]
url: string
}
export const BlogSEO = ({
authorDetails,
title,
summary,
date,
lastmod,
url,
images = [],
}: BlogSeoProps) => {
const router = useRouter()
const publishedAt = new Date(date).toISOString()
const modifiedAt = new Date(lastmod || date).toISOString()
const imagesArr =
images.length === 0
? [siteMetadata.socialBanner]
: typeof images === 'string'
? [images]
: images
const featuredImages = imagesArr.map((img) => {
return {
'@type': 'ImageObject',
url: `${siteMetadata.siteUrl}${img}`,
}
})
let authorList
if (authorDetails) {
authorList = authorDetails.map((author) => {
return {
'@type': 'Person',
name: author.name,
}
})
} else {
authorList = {
'@type': 'Person',
name: siteMetadata.author,
}
}
const structuredData = {
'@context': 'https://schema.org',
'@type': 'Article',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
headline: title,
image: featuredImages,
datePublished: publishedAt,
dateModified: modifiedAt,
author: authorList,
publisher: {
'@type': 'Organization',
name: siteMetadata.author,
logo: {
'@type': 'ImageObject',
url: `${siteMetadata.siteUrl}${siteMetadata.siteLogo}`,
},
},
description: summary,
}
const twImageUrl = featuredImages[0].url
return (
<>
<CommonSEO
title={title}
description={summary}
ogType="article"
ogImage={featuredImages}
twImage={twImageUrl}
/>
<Head>
{date && <meta property="article:published_time" content={publishedAt} />}
{lastmod && <meta property="article:modified_time" content={modifiedAt} />}
<link rel="canonical" href={`${siteMetadata.siteUrl}${router.asPath}`} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData, null, 2),
}}
/>
</Head>
</>
)
}

View file

@ -1,3 +0,0 @@
export default function SectionContainer({ children }) {
return <div className="max-w-3xl px-4 mx-auto sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
}

View file

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

68
components/TOCInline.tsx Normal file
View file

@ -0,0 +1,68 @@
import { Toc } from 'types/Toc'
interface TOCInlineProps {
toc: Toc
indentDepth?: number
fromHeading?: number
toHeading?: number
asDisclosure?: boolean
exclude?: string | string[]
}
/**
* Generates an inline table of contents
* Exclude titles matching this string (new RegExp('^(' + string + ')$', 'i')).
* If an array is passed the array gets joined with a pipe (new RegExp('^(' + array.join('|') + ')$', 'i')).
*
* @param {TOCInlineProps} {
* toc,
* indentDepth = 3,
* fromHeading = 1,
* toHeading = 6,
* asDisclosure = false,
* exclude = '',
* }
*
*/
const TOCInline = ({
toc,
indentDepth = 3,
fromHeading = 1,
toHeading = 6,
asDisclosure = false,
exclude = '',
}: TOCInlineProps) => {
const re = Array.isArray(exclude)
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
: new RegExp('^(' + exclude + ')$', 'i')
const filteredToc = toc.filter(
(heading) =>
heading.depth >= fromHeading && heading.depth <= toHeading && !re.test(heading.value)
)
const tocList = (
<ul>
{filteredToc.map((heading) => (
<li key={heading.value} className={`${heading.depth >= indentDepth && 'ml-6'}`}>
<a href={heading.url}>{heading.value}</a>
</li>
))}
</ul>
)
return (
<>
{asDisclosure ? (
<details open>
<summary className="pt-2 pb-2 ml-6 text-xl font-bold">Table of Contents</summary>
<div className="ml-6">{tocList}</div>
</details>
) : (
tocList
)}
</>
)
}
export default TOCInline

View file

@ -1,7 +1,11 @@
import Link from 'next/link'
import kebabCase from '@/lib/utils/kebabCase'
const Tag = ({ text }) => {
interface Props {
text: string
}
const Tag = ({ text }: Props) => {
return (
<Link href={`/tags/${kebabCase(text)}`}>
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">

View file

@ -0,0 +1,36 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const GAScript = () => {
return (
<>
<Script
strategy="lazyOnload"
src={`https://www.googletagmanager.com/gtag/js?id=${siteMetadata.analytics.googleAnalyticsId}`}
/>
<Script strategy="lazyOnload">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${siteMetadata.analytics.googleAnalyticsId}', {
page_path: window.location.pathname,
});
`}
</Script>
</>
)
}
export default GAScript
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const logEvent = (action, category, label, value) => {
window.gtag?.('event', action, {
event_category: category,
event_label: label,
value: value,
})
}

View file

@ -0,0 +1,27 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const PlausibleScript = () => {
return (
<>
<Script
strategy="lazyOnload"
data-domain={siteMetadata.analytics.plausibleDataDomain}
src="https://plausible.io/js/plausible.js"
/>
<Script strategy="lazyOnload">
{`
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
`}
</Script>
</>
)
}
export default PlausibleScript
// https://plausible.io/docs/custom-event-goals
export const logEvent = (eventName, ...rest) => {
return window.plausible?.(eventName, ...rest)
}

View file

@ -0,0 +1,25 @@
import Script from 'next/script'
const SimpleAnalyticsScript = () => {
return (
<>
<Script strategy="lazyOnload">
{`
window.sa_event=window.sa_event||function(){var a=[].slice.call(arguments);window.sa_event.q?window.sa_event.q.push(a):window.sa_event.q=[a]};
`}
</Script>
<Script strategy="lazyOnload" src="https://scripts.simpleanalyticscdn.com/latest.js" />
</>
)
}
// https://docs.simpleanalytics.com/events
export const logEvent = (eventName, callback) => {
if (callback) {
return window.sa_event?.(eventName, callback)
} else {
return window.sa_event?.(eventName)
}
}
export default SimpleAnalyticsScript

View file

@ -0,0 +1,27 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import GA from './GoogleAnalytics'
import Plausible from './Plausible'
import SimpleAnalytics from './SimpleAnalytics'
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 Analytics = () => {
return (
<>
{isProduction && siteMetadata.analytics.plausibleDataDomain && <Plausible />}
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
</>
)
}
export default Analytics

View file

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

View file

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

View file

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

View file

@ -0,0 +1,59 @@
import siteMetadata from '@/data/siteMetadata'
import dynamic from 'next/dynamic'
import { PostFrontMatter } from 'types/PostFrontMatter'
interface Props {
frontMatter: PostFrontMatter
}
const UtterancesComponent = dynamic(
() => {
return import('@/components/comments/Utterances')
},
{ ssr: false }
)
const GiscusComponent = dynamic(
() => {
return import('@/components/comments/Giscus')
},
{ ssr: false }
)
const DisqusComponent = dynamic(
() => {
return import('@/components/comments/Disqus')
},
{ ssr: false }
)
const Comments = ({ frontMatter }: Props) => {
let term
switch (
siteMetadata.comment.giscusConfig.mapping ||
siteMetadata.comment.utterancesConfig.issueTerm
) {
case 'pathname':
term = frontMatter.slug
break
case 'url':
term = window.location.href
break
case 'title':
term = frontMatter.title
break
}
return (
<>
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && (
<GiscusComponent mapping={term} />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
<UtterancesComponent issueTerm={term} />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
<DisqusComponent frontMatter={frontMatter} />
)}
</>
)
}
export default Comments

View file

@ -1,7 +1,7 @@
---
title: 'Introducing Tailwind Nexjs Starter Blog'
title: 'Introducing Tailwind Nextjs Starter Blog'
date: '2021-01-12'
lastmod: '2021-07-11'
lastmod: '2021-08-08'
tags: ['next-js', 'tailwind', 'guide']
draft: false
summary: 'Looking for a performant, out of the box template, with all the best in web technology to support your blogging needs? Checkout the Tailwind Nextjs Starter Blog template.'
@ -15,20 +15,26 @@ authors: ['default', 'sparrowhawk']
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/timlrx/tailwind-nextjs-starter-blog)
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature rich nextjs markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
Check out the documentation below to get started.
Facing issues? Check the [FAQ page](https://github.com/timlrx/tailwind-nextjs-starter-blog/wiki) and do a search on past issues. Feel free to open a new issue if none has been posted previously.
Feature request? Check the past discussions to see if it has been brough 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!
- [GauthierArcin'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)
Using the template? Happy to accept any PR with modifications made e.g. sub-paths, localization or multiple authors
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.
It is inspired by [Lee Robinson's blog](https://github.com/leerob/leerob.io), but focuses only on static site generation. Design is adapted from [Tailwindlabs blog](https://github.com/tailwindlabs/blog.tailwindcss.com).
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.
@ -36,9 +42,10 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
- Easy styling customization with [Tailwind 2.0](https://blog.tailwindcss.com/tailwindcss-v2) and primary color attribute
- Near perfect lighthouse score - [Lighthouse report](https://www.webpagetest.org/result/210111_DiC1_08f3670c3430bf4a9b76fc3b927716c5/)
- Lightweight, 38kB first load JS, uses Preact in production build
- Lightweight, 39kB first load JS, uses Preact in production build
- Mobile-friendly view
- Light and dark theme
- Supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics
- [MDX - write JSX in markdown documents!](https://mdxjs.com/)
- Server-side syntax highlighting with line numbers and line highlighting via [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus)
- Math display supported via [KaTeX](https://katex.org/)
@ -47,23 +54,25 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
- Support for tags - each unique tag will be its own page
- Support for multiple authors
- Blog templates
- TOC component
- Support for nested routing of blog posts
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
- Projects page
- SEO friendly with RSS feed, sitemaps and more!
## Sample posts
- [A markdown guide](/blog/github-markdown-guide)
- [Learn more about images in Next.js](/blog/guide-to-using-images-in-nextjs)
- [A tour of math typesetting](/blog/deriving-ols-estimator)
- [Simple MDX image grid](/blog/pictures-of-canada)
- [Example of long prose](/blog/the-time-machine)
- [Example of Nested Route Post](/blog/nested-route/introducing-multi-part-posts-with-nested-routing)
- [A markdown guide](https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide)
- [Learn more about images in Next.js](https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs)
- [A tour of math typesetting](https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator)
- [Simple MDX image grid](https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-canada)
- [Example of long prose](https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine)
- [Example of Nested Route Post](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing)
## Quick Start Guide
1. JS (official support) - `npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git` or TS (community support) - `npx degit timlrx/tailwind-nextjs-starter-blog#typescript`
2. Personalize `siteMetadata.json` (site related information)
2. Personalize `siteMetadata.js` (site related information)
3. Personalize `authors/default.md` (main author)
4. Modify `projectsData.js`
5. Modify `headerNavLinks.js` to customize navigation links
@ -86,7 +95,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
## Extend / Customize
`data/siteMetadata.json` - contains most of the site related information which should be modified for a user's need.
`data/siteMetadata.js` - contains most of the site related information which should be modified for a user's need.
`data/authors/default.md` - default author information (required). Additional authors can be added as files in `data/authors`.
@ -159,3 +168,11 @@ The easiest way to deploy the template is to use the [Vercel Platform](https://v
**Netlify / Github Pages / Firebase etc.**
As the template uses `next/image` for image optimization, additional configurations has 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.
## Support
Using the template? Support this effort by giving a star on Github, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
## Licence
[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timrlx.com)

View file

@ -1,6 +1,6 @@
---
title: 'New features in v1'
date: '2021-07-11'
date: 2021-08-07T15:32:14Z
tags: ['next-js', 'tailwind', 'guide']
draft: false
summary: 'An overview of the new features released in v1 - code block copy, multiple authors, frontmatter layout and more'
@ -11,14 +11,9 @@ layout: PostSimple
A post on the new features introduced in v1.0. New features:
- [Theme colors](#theme-colors)
- [Xdm MDX compiler](#xdm-mdx-compiler)
- [Layouts](#layouts)
- [Multiple authors](#multiple-authors)
- [Copy button for code blocks](#copy-button-for-code-blocks)
- [Line highlighting and line numbers](#line-highlighting-and-line-numbers)
<TOCInline toc={props.toc} exclude="Overview" toHeading={2} />
First load JS decreased from 43kB to 38kB despite all the new features added!
First load JS decreased from 43kB to 39kB despite all the new features added!
See [upgrade guide](#upgrade-guide) below if you are migrating from v0 version of the template.
@ -45,41 +40,65 @@ Migrating from v1? You can revert to the previous theme by setting `primary` to
## Xdm MDX compiler
We switch the MDX bundler from [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) to [mdx-bundler](https://github.com/kentcdodds/mdx-bundler).
This uses [xdm](https://github.com/wooorm/xdm) under the hood uses the latest micromark 3 and remark, rehype libraries.
We switched the MDX bundler from [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) to [mdx-bundler](https://github.com/kentcdodds/mdx-bundler).
This uses [xdm](https://github.com/wooorm/xdm) under the hood, the latest micromark 3 and remark, rehype libraries.
**Warning:** If you were using custom remark or rehype libraries, please upgrade to micromark 3 compatible ones. If you are upgrading, please delete `node_modules` and `package-lock.json` to avoid having past dependencies related issues.
[xdm](https://github.com/wooorm/xdm) contains multiple improvements over [@mdx-js/mdx](https://github.com/mdx-js/mdx), the compiler used internally by next-mdx-remote, but there might be some breaking behaviour changes.
Please check your markdown output to verify.
Some new possibilities include loading components directly in the mdx file using the import syntax and including js code which could be compiled at the build step.
Some new possibilities include loading components directly in the mdx file using the import syntax and including js code which could be compiled and bundled at the build step.
For example, the following jsx snippet can be used directly in an MDX file to render the page title component:
```js
import PageTitle from './PageTitle.js'
import PageTitle from './PageTitle.tsx'
;<PageTitle> Using JSX components in MDX </PageTitle>
```
import PageTitle from './PageTitle.js'
import PageTitle from './PageTitle.tsx'
<PageTitle> Using JSX components in MDX </PageTitle>
The default configuration resolves all components relative to the `components` directory.
**Note**:
Components which require external image loaders would require additional esbuild configuration.
Components which require external image loaders also require additional esbuild configuration.
Components which are dependent on global application state on lifecycle like the Nextjs `Link` component would also not work with this setup as each mdx file is built indepedently.
For such cases, it is better to use component substitution.
## Table of contents component
Inspired by [Docusaurus](https://docusaurus.io/docs/next/markdown-features/inline-toc) and Gatsby's [gatsby-remark-table-of-contents](https://www.gatsbyjs.com/plugins/gatsby-remark-table-of-contents/),
the `toc` variable containing all the top level headings of the document is passed to the MDX file and can be styled accordingly.
To make generating a table of contents (TOC) simple, you can use the existing `TOCInline` component.
For example, the TOC in this post was generated with the following code:
```js
<TOCInline toc={props.toc} exclude="Overview" toHeading={2} />
```
You can customise the headings that are displayed by configuring the `fromHeading` and `toHeading` props, or exclude particular headings
by passing a string or a string array to the `exclude` prop. By default, all headings that are of depth 3 or smaller are indented. This can be configured by changing the `indentDepth` property.
A `asDisclosure` prop can be used to render the TOC within an expandable disclosure element.
Here's the full TOC rendered in a disclosure element.
```js
<TOCInline toc={props.toc} asDisclosure />
```
<TOCInline toc={props.toc} asDisclosure />
## Layouts
You can map mdx blog content to layout components by configuring the frontmatter field. For example, this post is written with the new `PostSimple` layout!
### Adding new templates
layout templates are stored in the `./layouts` folder. You can add add your React components that you want to map to markdown content in this folder.
layout templates are stored in the `./layouts` folder. You can add your React components that you want to map to markdown content in this folder.
The component file name must match that specified in the markdown frontmatter `layout` field.
The only required field is `children` which contains the rendered MDX content, though you would probably want to pass in the frontMatter contents and render it in the template.
@ -122,27 +141,100 @@ The `DEFAULT_LAYOUT` for blog posts page is set to `PostLayout`.
### Extend
The layout mapping is handled by the `MDXLayoutRenderer` component.
It's a glue component which imports the specified layout, processes the MDX content before passing it back to the layout component as children.
`layout` is mapped to wrapper which wraps the entire MDX content.
```js
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
const LayoutComponent = require(`../layouts/${layout}`).default
export const MDXComponents = {
Image,
a: CustomLink,
pre: Pre,
wrapper: ({ components, layout, ...rest }) => {
const Layout = require(`../layouts/${layout}`).default
return <Layout {...rest} />
},
}
return (
<LayoutComponent {...rest}>
<MDXRemote {...mdxSource} components={MDXComponents} />
</LayoutComponent>
)
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
}
```
Use the component is a page where you want to accept a layout name to map to the desired layout.
You need to pass the layout name from the layout folder (it has to be an exact match) and the mdxSource content which is an output of the `seralize` function from the `next-mdx-remote` library.
Use the `MDXLayoutRenderer` component in a page where you want to accept a layout name to map to the desired layout.
You need to pass the layout name from the layout folder (it has to be an exact match).
## Analytics
The template now supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics.
Configure `siteMetadata.js` with the settings that correpond with the desired analytics provider.
```js
analytics: {
// supports plausible, simpleAnalytics or googleAnalytics
plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
simpleAnalytics: false, // true or false
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
},
```
Custom events are also supported. You can import the `logEvent` function from `@components/analytics/[ANALYTICS-PROVIDER]` file and call it when
triggering certain events of interest. _Note_: Additional configuration might be required depending on the analytics provider, please check their official
documentation for more information.
## Blog comments system
We have also added support for [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus.
To enable, simply configure `siteMetadata.js` comments property with the desired provider and settings as specified in the config file.
```js
comment: {
// Select a provider and use the environment variables associated to it
// https://vercel.com/docs/environment-variables
provider: 'giscus', // supported providers: giscus, utterances, disqus
giscusConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://giscus.app/
repo: process.env.NEXT_PUBLIC_GISCUS_REPO,
repositoryId: process.env.NEXT_PUBLIC_GISCUS_REPOSITORY_ID,
category: process.env.NEXT_PUBLIC_GISCUS_CATEGORY,
categoryId: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
mapping: 'pathname', // supported options: pathname, url, title
reactions: '1', // Emoji reactions: 1 = enable / 0 = disable
// Send discussion metadata periodically to the parent window: 1 = enable / 0 = disable
metadata: '0',
// theme example: light, dark, dark_dimmed, dark_high_contrast
// transparent_dark, preferred_color_scheme, custom
theme: 'light',
// theme when dark mode
darkTheme: 'transparent_dark',
// If the theme option above is set to 'custom`
// please provide a link below to your custom theme css file.
// example: https://giscus.app/themes/custom_example.css
themeURL: '',
},
utterancesConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://utteranc.es/
repo: process.env.NEXT_PUBLIC_UTTERANCES_REPO,
issueTerm: '', // supported options: pathname, url, title
label: '', // label (optional): Comment 💬
// theme example: github-light, github-dark, preferred-color-scheme
// github-dark-orange, icy-dark, dark-blue, photon-dark, boxy-light
theme: '',
// theme when dark mode
darkTheme: '',
},
disqus: {
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
},
},
```
## Multiple authors
Information on authors is now split from `siteMetadata.json` and stored in its own `data/authors` folder as a markdown file. Minimally, you will need to have a `default.md` file with authorship information. You can create additional files as required and the file name will be used as the reference to the author.
Information on authors is now split from `siteMetadata.js` and stored in its own `data/authors` folder as a markdown file. Minimally, you will need to have a `default.md` file with authorship information. You can create additional files as required and the file name will be used as the reference to the author.
Here's how an author markdown file might looks like:
@ -183,7 +275,7 @@ summary: 'My first post'
authors: ['default', 'sparrowhawk']
```
A demo of a multiple author post is shown in the [Introducing Tailwind Nextjs Starter Blog post](/blog/introducing-tailwind-nextjs-starter-blog).
A demo of a multiple author post is shown in [Introducing Tailwind Nextjs Starter Blog post](/blog/introducing-tailwind-nextjs-starter-blog).
## Copy button for code blocks
@ -240,7 +332,7 @@ There are significant portions of the code that has been changed from v0 to v1 i
There's also no real reason to change if the previous one serves your needs and it might be easier to copy
the component changes you are interested to your existing blog rather than migrating everything over.
Nonetheless if you want to do so and have not changed much of the template, you could clone the new version and copy over the blog post instead.
Nonetheless, if you want to do so and have not changed much of the template, you could clone the new version and copy over the blog post over to the new template.
Another alternative would be to pull the latest tempate version with the following code:
@ -252,7 +344,7 @@ rm -rf node_modules
You can see an example of such a migration in this [commit](https://github.com/timlrx/timlrx.com/commit/bba1c185384fd6d5cdaac15abf802fdcff027286) for my personal blog.
v1 also uses `feed.xml` rather than `index.xml`. If you are migrating you should add a redirect to `next.config.js` like so:
v1 also uses `feed.xml` rather than `index.xml`, to avoid some build issues with vercel. If you are migrating you should add a redirect to `next.config.js` like so:
```
async redirects() {

View file

@ -1,7 +1,7 @@
const projectsData = [
{
title: 'A Search Engine',
description: `What is you could look up any information in the world? Webpages, images, videos
description: `What if you could look up any information in the world? Webpages, images, videos
and more. Google has many features to help you find exactly what you're looking
for.`,
imgSrc: '/static/images/google.png',

69
data/siteMetadata.js Normal file
View file

@ -0,0 +1,69 @@
const siteMetadata = {
title: 'Next.js Starter Blog',
author: 'Tails Azimuth',
headerTitle: 'TailwindBlog',
description: 'A blog created with Next.js and Tailwind.css',
language: 'en-us',
siteUrl: 'https://tailwind-nextjs-starter-blog.vercel.app',
siteRepo: 'https://github.com/timlrx/tailwind-nextjs-starter-blog',
siteLogo: '/static/images/logo.png',
image: '/static/images/avatar.png',
socialBanner: '/static/images/twitter-card.png',
email: 'address@yoursite.com',
github: 'https://github.com',
twitter: 'https://twitter.com/Twitter',
facebook: 'https://facebook.com',
youtube: 'https://youtube.com',
linkedin: 'https://www.linkedin.com',
locale: 'en-US',
analytics: {
// supports plausible, simpleAnalytics or googleAnalytics
plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
simpleAnalytics: false, // true or false
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
},
comment: {
// Select a provider and use the environment variables associated to it
// https://vercel.com/docs/environment-variables
provider: 'giscus', // supported providers: giscus, utterances, disqus
giscusConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://giscus.app/
repo: process.env.NEXT_PUBLIC_GISCUS_REPO,
repositoryId: process.env.NEXT_PUBLIC_GISCUS_REPOSITORY_ID,
category: process.env.NEXT_PUBLIC_GISCUS_CATEGORY,
categoryId: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
mapping: 'pathname', // supported options: pathname, url, title
reactions: '1', // Emoji reactions: 1 = enable / 0 = disable
// Send discussion metadata periodically to the parent window: 1 = enable / 0 = disable
metadata: '0',
// theme example: light, dark, dark_dimmed, dark_high_contrast
// transparent_dark, preferred_color_scheme, custom
theme: 'light',
// theme when dark mode
darkTheme: 'transparent_dark',
// If the theme option above is set to 'custom`
// please provide a link below to your custom theme css file.
// example: https://giscus.app/themes/custom_example.css
themeURL: '',
},
utterancesConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://utteranc.es/
repo: process.env.NEXT_PUBLIC_UTTERANCES_REPO,
issueTerm: '', // supported options: pathname, url, title
label: '', // label (optional): Comment 💬
// theme example: github-light, github-dark, preferred-color-scheme
// github-dark-orange, icy-dark, dark-blue, photon-dark, boxy-light
theme: '',
// theme when dark mode
darkTheme: '',
},
disqus: {
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
},
},
}
module.exports = siteMetadata

View file

@ -1,19 +0,0 @@
{
"title": "Next.js Starter Blog",
"author": "Tails Azimuth",
"headerTitle": "TailwindBlog",
"description": "A blog created with Next.js and Tailwind.css",
"language": "en-us",
"siteUrl": "https://tailwind-nextjs-starter-blog.vercel.app",
"siteRepo": "https://github.com/timlrx/tailwind-nextjs-starter-blog",
"siteLogo": "/static/images/logo.png",
"image": "/static/images/avatar.png",
"socialBanner": "/static/images/twitter-card.png",
"email": "address@yoursite.com",
"github": "https://github.com",
"twitter": "https://twitter.com/Twitter",
"facebook": "https://facebook.com",
"youtube": "https://youtube.com",
"linkedin": "https://www.linkedin.com",
"locale": "en-US"
}

View file

@ -1,13 +1,20 @@
import SocialIcon from '@/components/social-icons'
import Image from '@/components/Image'
import { PageSeo } from '@/components/SEO'
import { PageSEO } from '@/components/SEO'
import { ReactNode } from 'react'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
export default function AuthorLayout({ children, frontMatter }) {
interface Props {
children: ReactNode
frontMatter: AuthorFrontMatter
}
export default function AuthorLayout({ children, frontMatter }: Props) {
const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
return (
<>
<PageSeo title={`About - ${name}`} description={`About me - ${name}`} />
<PageSEO title={`About - ${name}`} description={`About me - ${name}`} />
<div className="divide-y">
<div className="pt-6 pb-8 space-y-2 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">

View file

@ -1,11 +1,17 @@
import Link from '@/components/Link'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { useState } from 'react'
import { ComponentProps, useState } from 'react'
import Pagination from '@/components/Pagination'
import formatDate from '@/lib/utils/formatDate'
import { PostFrontMatter } from 'types/PostFrontMatter'
interface Props {
posts: PostFrontMatter[]
title: string
initialDisplayPosts?: PostFrontMatter[]
pagination?: ComponentProps<typeof Pagination>
}
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }: Props) {
const [searchValue, setSearchValue] = useState('')
const filteredBlogPosts = posts.filter((frontMatter) => {
const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')

View file

@ -1,10 +1,14 @@
import Link from '@/components/Link'
import PageTitle from '@/components/PageTitle'
import SectionContainer from '@/components/SectionContainer'
import { BlogSeo } from '@/components/SEO'
import { BlogSEO } from '@/components/SEO'
import Image from '@/components/Image'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import Comments from '@/components/comments'
import { ReactNode } from 'react'
import { PostFrontMatter } from 'types/PostFrontMatter'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
const editUrl = (fileName) => `${siteMetadata.siteRepo}/blob/master/data/blog/${fileName}`
const discussUrl = (slug) =>
@ -12,14 +16,27 @@ const discussUrl = (slug) =>
`${siteMetadata.siteUrl}/blog/${slug}`
)}`
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
const postDateTemplate: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
interface Props {
frontMatter: PostFrontMatter
authorDetails: AuthorFrontMatter[]
next?: { slug: string; title: string }
prev?: { slug: string; title: string }
children: ReactNode
}
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }: Props) {
const { slug, fileName, date, title, tags } = frontMatter
return (
<SectionContainer>
<BlogSeo
<BlogSEO
url={`${siteMetadata.siteUrl}/blog/${slug}`}
authorDetails={authorDetails}
{...frontMatter}
@ -91,6 +108,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
{``}
<Link href={editUrl(fileName)}>{'View on GitHub'}</Link>
</div>
<Comments frontMatter={frontMatter} />
</div>
<footer>
<div className="text-sm font-medium leading-5 divide-gray-200 xl:divide-y dark:divide-gray-700 xl:col-start-1 xl:row-start-2">

View file

@ -1,16 +1,26 @@
import Link from '@/components/Link'
import PageTitle from '@/components/PageTitle'
import SectionContainer from '@/components/SectionContainer'
import { BlogSeo } from '@/components/SEO'
import { BlogSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import formatDate from '@/lib/utils/formatDate'
import Comments from '@/components/comments'
import { ReactNode } from 'react'
import { PostFrontMatter } from 'types/PostFrontMatter'
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
const { date, title } = frontMatter
interface Props {
frontMatter: PostFrontMatter
children: ReactNode
next?: { slug: string; title: string }
prev?: { slug: string; title: string }
}
export default function PostLayout({ frontMatter, next, prev, children }: Props) {
const { slug, date, title } = frontMatter
return (
<SectionContainer>
<BlogSeo url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${slug}`} {...frontMatter} />
<article>
<div>
<header>
@ -35,6 +45,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:pb-0 xl:col-span-3 xl:row-span-2">
<div className="pt-10 pb-8 prose dark:prose-dark max-w-none">{children}</div>
</div>
<Comments frontMatter={frontMatter} />
<footer>
<div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base">
{prev && (

View file

@ -1,8 +1,9 @@
import { escape } from '@/lib/utils/htmlEscaper'
import siteMetadata from '@/data/siteMetadata'
import { PostFrontMatter } from 'types/PostFrontMatter'
const generateRssItem = (post) => `
const generateRssItem = (post: PostFrontMatter) => `
<item>
<guid>${siteMetadata.siteUrl}/blog/${post.slug}</guid>
<title>${escape(post.title)}</title>
@ -14,7 +15,7 @@ const generateRssItem = (post) => `
</item>
`
const generateRss = (posts, page = 'feed.xml') => `
const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escape(siteMetadata.title)}</title>

View file

@ -1,33 +0,0 @@
const visit = require('unist-util-visit')
const sizeOf = require('image-size')
const fs = require('fs')
module.exports = (options) => (tree) => {
visit(
tree,
// only visit p tags that contain an img element
(node) => node.type === 'paragraph' && node.children.some((n) => n.type === 'image'),
(node) => {
const imageNode = node.children.find((n) => n.type === 'image')
// only local files
if (fs.existsSync(`${process.cwd()}/public${imageNode.url}`)) {
const dimensions = sizeOf(`${process.cwd()}/public${imageNode.url}`)
// Convert original node to next/image
;(imageNode.type = 'mdxJsxFlowElement'),
(imageNode.name = 'Image'),
(imageNode.attributes = [
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
{ type: 'mdxJsxAttribute', name: 'src', value: imageNode.url },
{ type: 'mdxJsxAttribute', name: 'width', value: dimensions.width },
{ type: 'mdxJsxAttribute', name: 'height', value: dimensions.height },
])
// Change node type from p to div to avoid nesting error
node.type = 'div'
node.children = [imageNode]
}
}
)
}

View file

@ -3,10 +3,24 @@ import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'
import readingTime from 'reading-time'
import visit from 'unist-util-visit'
import codeTitles from './remark-code-title'
import imgToJsx from './img-to-jsx'
import { visit } from 'unist-util-visit'
import type { Pluggable } from 'unified'
import getAllFilesRecursively from './utils/files'
import { PostFrontMatter } from 'types/PostFrontMatter'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { Toc } from 'types/Toc'
// Remark packages
import remarkSlug from 'remark-slug'
import remarkAutolinkHeadings from 'remark-autolink-headings'
import remarkGfm from 'remark-gfm'
import remarkFootnotes from 'remark-footnotes'
import remarkMath from 'remark-math'
import remarkCodeTitles from './remark-code-title'
import remarkTocHeadings from './remark-toc-headings'
import remarkImgToJsx from './remark-img-to-jsx'
// Rehype packages
import rehypeKatex from 'rehype-katex'
import rehypePrismPlus from 'rehype-prism-plus'
const root = process.cwd()
@ -24,24 +38,24 @@ const tokenClassNames = {
comment: 'text-gray-400 italic',
}
export function getFiles(type) {
export function getFiles(type: 'blog' | 'authors') {
const prefixPaths = path.join(root, 'data', type)
const files = getAllFilesRecursively(prefixPaths)
// Only want to return blog/path and ignore root, replace is needed to work on Windows
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
}
export function formatSlug(slug) {
export function formatSlug(slug: string) {
return slug.replace(/\.(mdx|md)/, '')
}
export function dateSortDesc(a, b) {
export function dateSortDesc(a: string, b: string) {
if (a > b) return -1
if (a < b) return 1
return 0
}
export async function getFileBySlug(type, slug) {
export async function getFileBySlug<T>(type: 'authors' | 'blog', slug: string | string[]) {
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
const mdPath = path.join(root, 'data', type, `${slug}.md`)
const source = fs.existsSync(mdxPath)
@ -66,6 +80,8 @@ export async function getFileBySlug(type, slug) {
)
}
const toc: Toc = []
const { frontmatter, code } = await bundleMDX(source, {
// mdx imports can be automatically source from the components directory
cwd: path.join(process.cwd(), 'components'),
@ -75,22 +91,23 @@ export async function getFileBySlug(type, slug) {
// plugins in the future.
options.remarkPlugins = [
...(options.remarkPlugins ?? []),
require('remark-slug'),
require('remark-autolink-headings'),
require('remark-gfm'),
codeTitles,
[require('remark-footnotes'), { inlineNotes: true }],
require('remark-math'),
imgToJsx,
remarkSlug,
remarkAutolinkHeadings,
[remarkTocHeadings, { exportRef: toc }],
remarkGfm,
remarkCodeTitles,
[remarkFootnotes, { inlineNotes: true }],
remarkMath,
remarkImgToJsx,
]
options.rehypePlugins = [
...(options.rehypePlugins ?? []),
require('rehype-katex'),
[require('rehype-prism-plus'), { ignoreMissing: true }],
rehypeKatex,
[rehypePrismPlus, { ignoreMissing: true }] as Pluggable,
() => {
return (tree) => {
visit(tree, 'element', (node, index, parent) => {
let [token, type] = node.properties.className || []
visit(tree, 'element', (node) => {
const [token, type] = node.properties.className || []
if (token === 'token') {
node.properties.className = [tokenClassNames[type]]
}
@ -111,23 +128,25 @@ export async function getFileBySlug(type, slug) {
return {
mdxSource: code,
toc,
frontMatter: {
readingTime: readingTime(code),
slug: slug || null,
fileName: fs.existsSync(mdxPath) ? `${slug}.mdx` : `${slug}.md`,
...frontmatter,
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
},
}
}
export async function getAllFilesFrontMatter(folder) {
export async function getAllFilesFrontMatter(folder: 'blog') {
const prefixPaths = path.join(root, 'data', folder)
const files = getAllFilesRecursively(prefixPaths)
const allFrontMatter = []
const allFrontMatter: PostFrontMatter[] = []
files.forEach((file) => {
files.forEach((file: string) => {
// Replace is needed to work on Windows
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
// Remove Unexpected File
@ -135,9 +154,14 @@ export async function getAllFilesFrontMatter(folder) {
return
}
const source = fs.readFileSync(file, 'utf8')
const { data } = matter(source)
if (data.draft !== true) {
allFrontMatter.push({ ...data, slug: formatSlug(fileName) })
const matterFile = matter(source)
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter
if ('draft' in frontmatter && frontmatter.draft !== true) {
allFrontMatter.push({
...frontmatter,
slug: formatSlug(fileName),
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
})
}
})

View file

@ -1,8 +1,9 @@
import visit from 'unist-util-visit'
import { Parent } from 'unist'
import { visit } from 'unist-util-visit'
module.exports = function (options) {
return (tree) =>
visit(tree, 'code', (node, index) => {
export default function remarkCodeTitles() {
return (tree: Parent & { lang?: string }) =>
visit(tree, 'code', (node: Parent & { lang?: string }, index) => {
const nodeLang = node.lang || ''
let language = ''
let title = ''

44
lib/remark-img-to-jsx.ts Normal file
View file

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

View file

@ -0,0 +1,14 @@
//@ts-nocheck
import { Parent } from 'unist'
import { visit } from 'unist-util-visit'
export default function remarkTocHeadings(options) {
return (tree: Parent) =>
visit(tree, 'heading', (node) => {
options.exportRef.push({
value: node.children[0].value || node.children[1].value,
url: node.children[0].url || node.children[1].url,
depth: node.depth,
})
})
}

View file

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

View file

@ -8,13 +8,13 @@ const flattenArray = (input) =>
const map = (fn) => (input) => input.map(fn)
const walkDir = (fullPath) => {
const walkDir = (fullPath: string) => {
return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath)
}
const pathJoinPrefix = (prefix) => (extraPath) => path.join(prefix, extraPath)
const pathJoinPrefix = (prefix: string) => (extraPath: string) => path.join(prefix, extraPath)
const getAllFilesRecursively = (folder) =>
const getAllFilesRecursively = (folder: string): string[] =>
pipe(fs.readdirSync, map(pipe(pathJoinPrefix(folder), walkDir)), flattenArray)(folder)
export default getAllFilesRecursively

View file

@ -1,7 +1,7 @@
import siteMetadata from '@/data/siteMetadata'
const formatDate = (date) => {
const options = {
const formatDate = (date: string) => {
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',

View file

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

View file

@ -1,4 +1,4 @@
const kebabCase = (str) =>
const kebabCase = (str: string) =>
str &&
str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)

6
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -2,12 +2,16 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
/**
* @type {import('next/dist/next-server/server/config').NextConfig}
**/
module.exports = withBundleAnalyzer({
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
eslint: {
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
},
experimental: { esmExternals: true },
webpack: (config, { dev, isServer }) => {
config.module.rules.push({
test: /\.(png|jpe?g|gif|mp4)$/i,

3973
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "tailwind-nextjs-starter-blog",
"version": "0.4.1",
"version": "1.1.0",
"private": true,
"scripts": {
"start": "next-remote-watch ./data",
@ -15,28 +15,35 @@
"@tailwindcss/forms": "^0.3.2",
"@tailwindcss/typography": "^0.4.0",
"autoprefixer": "^10.2.5",
"esbuild": "^0.12.15",
"gray-matter": "^4.0.2",
"image-size": "1.0.0",
"mdx-bundler": "^4.1.0",
"next": "11.0.1",
"mdx-bundler": "^6.0.1",
"next": "11.1.0",
"next-themes": "^0.0.14",
"postcss": "^8.3.5",
"preact": "^10.5.13",
"react": "17.0.2",
"react-dom": "17.0.2",
"reading-time": "1.3.0",
"rehype-katex": "^5.0.0",
"rehype-prism-plus": "0.0.1",
"remark-autolink-headings": "6.0.1",
"remark-footnotes": "^3.0.0",
"remark-gfm": "^1.0.0",
"remark-math": "^4.0.0",
"remark-slug": "6.0.0",
"tailwindcss": "^2.2.2"
"rehype-katex": "^6.0.0",
"rehype-prism-plus": "^0.0.5",
"remark-autolink-headings": "^7.0.0",
"remark-footnotes": "^4.0.0",
"remark-gfm": "^2.0.0",
"remark-math": "^5.0.0",
"remark-slug": "^7.0.0",
"sharp": "^0.28.3",
"tailwindcss": "^2.2.2",
"unist-util-visit": "^4.0.0"
},
"devDependencies": {
"@next/bundle-analyzer": "11.0.1",
"@svgr/webpack": "^5.5.0",
"@types/react": "^17.0.14",
"@types/tailwindcss": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"cross-env": "^7.0.3",
"dedent": "^0.7.0",
"eslint": "^7.29.0",
@ -51,12 +58,7 @@
"lint-staged": "^11.0.0",
"next-remote-watch": "^1.0.0",
"prettier": "2.2.1",
"rehype": "11.0.0",
"remark-frontmatter": "3.0.0",
"remark-parse": "9.0.0",
"remark-stringify": "9.0.1",
"unified": "9.2.1",
"unist-util-visit": "2.0.3"
"typescript": "^4.3.5"
},
"lint-staged": {
"*.+(js|jsx|ts|tsx)": [

View file

@ -1,16 +1,19 @@
import '@/css/tailwind.css'
import { ThemeProvider } from 'next-themes'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import Analytics from '@/components/analytics'
import LayoutWrapper from '@/components/LayoutWrapper'
export default function App({ Component, pageProps }) {
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider attribute="class">
<Head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
</Head>
<Analytics />
<LayoutWrapper>
<Component {...pageProps} />
</LayoutWrapper>

View file

@ -1,5 +1,12 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
// static async getInitialProps(ctx: DocumentContext) {
// const initialProps = await Document.getInitialProps(ctx)
// return initialProps
// }
render() {
return (
<Html lang="en">

View file

@ -1,21 +0,0 @@
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { getFileBySlug } from '@/lib/mdx'
const DEFAULT_LAYOUT = 'AuthorLayout'
export async function getStaticProps() {
const authorDetails = await getFileBySlug('authors', ['default'])
return { props: { authorDetails } }
}
export default function About({ authorDetails }) {
const { mdxSource, frontMatter } = authorDetails
return (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
mdxSource={mdxSource}
frontMatter={frontMatter}
/>
)
}

27
pages/about.tsx Normal file
View file

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

View file

@ -1,11 +1,17 @@
import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'
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 async function getStaticProps() {
export const getStaticProps: GetStaticProps<{
posts: ComponentProps<typeof ListLayout>['posts']
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
pagination: ComponentProps<typeof ListLayout>['pagination']
}> = async () => {
const posts = await getAllFilesFrontMatter('blog')
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
const pagination = {
@ -16,10 +22,14 @@ export async function getStaticProps() {
return { props: { initialDisplayPosts, posts, pagination } }
}
export default function Blog({ posts, initialDisplayPosts, pagination }) {
export default function Blog({
posts,
initialDisplayPosts,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSeo title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
<ListLayout
posts={posts}
initialDisplayPosts={initialDisplayPosts}

View file

@ -3,6 +3,10 @@ import PageTitle from '@/components/PageTitle'
import generateRss from '@/lib/generate-rss'
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { PostFrontMatter } from 'types/PostFrontMatter'
import { Toc } from 'types/Toc'
const DEFAULT_LAYOUT = 'PostLayout'
@ -18,15 +22,23 @@ export async function getStaticPaths() {
}
}
export async function getStaticProps({ params }) {
// @ts-ignore
export const getStaticProps: GetStaticProps<{
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }
authorDetails: AuthorFrontMatter[]
prev?: { slug: string; title: string }
next?: { slug: string; title: string }
}> = async ({ params }) => {
const slug = (params.slug as string[]).join('/')
const allPosts = await getAllFilesFrontMatter('blog')
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/'))
const prev = allPosts[postIndex + 1] || null
const next = allPosts[postIndex - 1] || null
const post = await getFileBySlug('blog', params.slug.join('/'))
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === slug)
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
const post = await getFileBySlug<PostFrontMatter>('blog', slug)
// @ts-ignore
const authorList = post.frontMatter.authors || ['default']
const authorPromise = authorList.map(async (author) => {
const authorResults = await getFileBySlug('authors', [author])
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
return authorResults.frontMatter
})
const authorDetails = await Promise.all(authorPromise)
@ -35,17 +47,30 @@ export async function getStaticProps({ params }) {
const rss = generateRss(allPosts)
fs.writeFileSync('./public/feed.xml', rss)
return { props: { post, authorDetails, prev, next } }
return {
props: {
post,
authorDetails,
prev,
next,
},
}
}
export default function Blog({ post, authorDetails, prev, next }) {
const { mdxSource, frontMatter } = post
export default function Blog({
post,
authorDetails,
prev,
next,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, toc, frontMatter } = post
return (
<>
{frontMatter.draft !== true ? (
{'draft' in frontMatter && frontMatter.draft !== true ? (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
toc={toc}
mdxSource={mdxSource}
frontMatter={frontMatter}
authorDetails={authorDetails}

View file

@ -1,10 +1,12 @@
import { PageSeo } from '@/components/SEO'
import { PageSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import ListLayout from '@/layouts/ListLayout'
import { POSTS_PER_PAGE } from '../../blog'
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'
export async function getStaticPaths() {
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
const totalPosts = await getAllFilesFrontMatter('blog')
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
const paths = Array.from({ length: totalPages }, (_, i) => ({
@ -17,12 +19,16 @@ export async function getStaticPaths() {
}
}
export async function getStaticProps(context) {
export const getStaticProps: GetStaticProps<{
posts: PostFrontMatter[]
initialDisplayPosts: PostFrontMatter[]
pagination: { currentPage: number; totalPages: number }
}> = async (context) => {
const {
params: { page },
} = context
const posts = await getAllFilesFrontMatter('blog')
const pageNumber = parseInt(page)
const pageNumber = parseInt(page as string)
const initialDisplayPosts = posts.slice(
POSTS_PER_PAGE * (pageNumber - 1),
POSTS_PER_PAGE * pageNumber
@ -41,10 +47,14 @@ export async function getStaticProps(context) {
}
}
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
export default function PostPage({
posts,
initialDisplayPosts,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSeo title={siteMetadata.title} description={siteMetadata.description} />
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<ListLayout
posts={posts}
initialDisplayPosts={initialDisplayPosts}

View file

@ -1,22 +1,24 @@
import Link from '@/components/Link'
import { PageSeo } from '@/components/SEO'
import { PageSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import formatDate from '@/lib/utils/formatDate'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'
const MAX_DISPLAY = 5
export async function getStaticProps() {
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
const posts = await getAllFilesFrontMatter('blog')
return { props: { posts } }
}
export default function Home({ posts }) {
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSeo title={siteMetadata.title} description={siteMetadata.description} />
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="pt-6 pb-8 space-y-2 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">

View file

@ -1,14 +1,12 @@
import siteMetadata from '@/data/siteMetadata'
import projectsData from '@/data/projectsData'
import Image from '@/components/Image'
import Link from '@/components/Link'
import Card from '@/components/Card'
import { PageSeo } from '@/components/SEO'
import { PageSEO } from '@/components/SEO'
export default function Projects() {
return (
<>
<PageSeo title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
<PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="pt-6 pb-8 space-y-2 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">

View file

@ -1,21 +1,22 @@
import Link from '@/components/Link'
import { PageSeo } from '@/components/SEO'
import { PageSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
export async function getStaticProps() {
export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
const tags = await getAllTags('blog')
return { props: { tags } }
}
export default function Tags({ tags }) {
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
return (
<>
<PageSeo title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
<PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
<div className="flex flex-col items-start justify-start divide-y divide-gray-200 dark:divide-gray-700 md:justify-center md:items-center md:divide-y-0 md:flex-row md:space-x-6 md:mt-24">
<div className="pt-6 pb-8 space-x-2 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 md:border-r-2 md:px-6">

View file

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

View file

@ -7,8 +7,8 @@ const siteMetadata = require('../data/siteMetadata')
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
const pages = await globby([
'pages/*.js',
'data/**/*.mdx',
'data/**/*.md',
'data/blog/**/*.mdx',
'data/blog/**/*.md',
'public/tags/**/*.xml',
'!pages/_*.js',
'!pages/api',

View file

@ -1,9 +1,13 @@
// @ts-check
/* eslint-disable @typescript-eslint/no-var-requires */
const defaultTheme = require('tailwindcss/defaultTheme')
const colors = require('tailwindcss/colors')
/** @type {import("tailwindcss/tailwind-config").TailwindConfig } */
module.exports = {
mode: 'jit',
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './lib/**/*.js'],
purge: ['./pages/**/*.tsx', './components/**/*.tsx', './layouts/**/*.tsx', './lib/**/*.ts'],
darkMode: 'class',
theme: {
extend: {
@ -74,6 +78,14 @@ module.exports = {
'code:after': {
content: 'none',
},
details: {
backgroundColor: theme('colors.gray.100'),
paddingLeft: '4px',
paddingRight: '4px',
paddingTop: '2px',
paddingBottom: '2px',
borderRadius: '0.25rem',
},
hr: { borderColor: theme('colors.gray.200') },
'ol li:before': {
fontWeight: '600',
@ -119,6 +131,9 @@ module.exports = {
code: {
backgroundColor: theme('colors.gray.800'),
},
details: {
backgroundColor: theme('colors.gray.800'),
},
hr: { borderColor: theme('colors.gray.700') },
'ol li:before': {
fontWeight: '600',

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"incremental": true,
"target": "ES6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/data/*": ["data/*"],
"@/layouts/*": ["layouts/*"],
"@/lib/*": ["lib/*"],
"@/css/*": ["css/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,11 @@
export type AuthorFrontMatter = {
layout?: string
name: string
avatar: string
occupation: string
company: string
email: string
twitter: string
linkedin: string
github: string
}

13
types/PostFrontMatter.ts Normal file
View file

@ -0,0 +1,13 @@
export type PostFrontMatter = {
title: string
date: string
tags: string[]
lastmod?: string
draft?: boolean
summary?: string
images?: string[]
authors?: string[]
layout?: string
slug: string
fileName: string
}

5
types/Toc.ts Normal file
View file

@ -0,0 +1,5 @@
export type Toc = {
value: string
depth: number
url: string
}[]