commit
f6ac722192
@ -4,6 +4,7 @@ module.exports = {
|
||||
browser: true,
|
||||
amd: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,7 +18,7 @@ public/sitemap.xml
|
||||
/build
|
||||
*.xml
|
||||
# rss feed
|
||||
/public/index.xml
|
||||
/public/feed.xml
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
33
README.md
33
README.md
@ -10,6 +10,8 @@
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Examples
|
||||
|
||||
- [Demo Blog](https://tailwind-nextjs-starter-blog.vercel.app/) - this repo
|
||||
@ -27,17 +29,19 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
||||
|
||||
## Features
|
||||
|
||||
- Easy styling customization with [Tailwind 2.0](https://blog.tailwindcss.com/tailwindcss-v2)
|
||||
- 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, 43kB first load JS, uses Preact in production build
|
||||
- Lightweight, 38kB first load JS, uses Preact in production build
|
||||
- Mobile-friendly view
|
||||
- Light and dark theme
|
||||
- [MDX - write JSX in markdown documents!](https://mdxjs.com/)
|
||||
- Server-side syntax highlighting with [rehype-prism](https://github.com/mapbox/rehype-prism)
|
||||
- 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/)
|
||||
- Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization)
|
||||
- Flexible data retrieval with [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote)
|
||||
- Flexible data retrieval with [mdx-bundler](https://github.com/kentcdodds/mdx-bundler)
|
||||
- Support for tags - each unique tag will be its own page
|
||||
- Support for multiple authors
|
||||
- Blog templates
|
||||
- Support for nested routing of blog posts
|
||||
- Projects page
|
||||
- SEO friendly with RSS feed, sitemaps and more!
|
||||
@ -53,12 +57,13 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
1. `npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git`
|
||||
2. Personalize `siteMetadata.json`
|
||||
3. Modify `projectsData.js`
|
||||
4. Modify `headerNavLinks.js` to customize navigation links
|
||||
5. Add blog posts
|
||||
6. Deploy on Vercel
|
||||
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)
|
||||
3. Personalize `authors/default.md` (main author)
|
||||
4. Modify `projectsData.js`
|
||||
5. Modify `headerNavLinks.js` to customize navigation links
|
||||
6. Add blog posts
|
||||
7. Deploy on Vercel
|
||||
|
||||
## Development
|
||||
|
||||
@ -78,6 +83,8 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
|
||||
|
||||
`data/siteMetadata.json` - 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`.
|
||||
|
||||
`data/projectsData.js` - data used to generate styled card in projects page.
|
||||
|
||||
`data/headerNavLinks.js` - navigation links.
|
||||
@ -88,7 +95,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
|
||||
|
||||
`public/static` - store assets such as images and favicons.
|
||||
|
||||
`css/tailwind.css` - contains the tailwind stylesheet which can be modified to change the overall look and feel of the site.
|
||||
`tailwind.config.js` and `css/tailwind.css` - contain the tailwind stylesheet which can be modified to change the overall look and feel of the site.
|
||||
|
||||
`components/social-icons` - to add other icons, simply copy an svg file from [Simple Icons](https://simpleicons.org/) and map them in `index.js`. Other icons uses [heroicons](https://heroicons.com/).
|
||||
|
||||
@ -114,6 +121,8 @@ lastmod (optional)
|
||||
draft (optional)
|
||||
summary (optional)
|
||||
images (optional, if none provided defaults to socialBanner in siteMetadata config)
|
||||
authors (optional list which should correspond to the file names in `data/authors`. Uses `default` if none is specified)
|
||||
layout (optional list which should correspond to the file names in `data/layouts`)
|
||||
```
|
||||
|
||||
Here's an example of a post's frontmatter:
|
||||
@ -127,6 +136,8 @@ 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.'
|
||||
images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg']
|
||||
authors: ['default', 'sparrowhawk']
|
||||
layout: PostLayout
|
||||
---
|
||||
```
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import Image from 'next/image'
|
||||
import Link from '@/components/Link'
|
||||
import Image from './Image'
|
||||
import Link from './Link'
|
||||
|
||||
const Card = ({ title, description, imgSrc, href }) => (
|
||||
<div className="p-4 md:w-1/2 md" style={{ maxWidth: '544px' }}>
|
||||
<div className="h-full border-2 border-gray-200 border-opacity-60 dark:border-gray-700 rounded-md overflow-hidden">
|
||||
<div className="h-full overflow-hidden border-2 border-gray-200 rounded-md border-opacity-60 dark:border-gray-700">
|
||||
{href ? (
|
||||
<Link href={href} aria-label={`Link to ${title}`}>
|
||||
<Image
|
||||
alt={title}
|
||||
src={imgSrc}
|
||||
className="lg:h-48 md:h-36 object-cover object-center"
|
||||
className="object-cover object-center lg:h-48 md:h-36"
|
||||
width={544}
|
||||
height={306}
|
||||
/>
|
||||
@ -18,13 +18,13 @@ const Card = ({ title, description, imgSrc, href }) => (
|
||||
<Image
|
||||
alt={title}
|
||||
src={imgSrc}
|
||||
className="lg:h-48 md:h-36 object-cover object-center"
|
||||
className="object-cover object-center lg:h-48 md:h-36"
|
||||
width={544}
|
||||
height={306}
|
||||
/>
|
||||
)}
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold leading-8 tracking-tight mb-3">
|
||||
<h2 className="mb-3 text-2xl font-bold leading-8 tracking-tight">
|
||||
{href ? (
|
||||
<Link href={href} aria-label={`Link to ${title}`}>
|
||||
{title}
|
||||
@ -33,11 +33,11 @@ const Card = ({ title, description, imgSrc, href }) => (
|
||||
title
|
||||
)}
|
||||
</h2>
|
||||
<p className="prose text-gray-500 max-w-none dark:text-gray-400 mb-3">{description}</p>
|
||||
<p className="mb-3 prose text-gray-500 max-w-none dark:text-gray-400">{description}</p>
|
||||
{href && (
|
||||
<Link
|
||||
href={href}
|
||||
className="text-base font-medium leading-6 text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
className="text-base font-medium leading-6 text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Link to ${title}`}
|
||||
>
|
||||
Learn more →
|
||||
|
@ -1,9 +1,22 @@
|
||||
/* 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'
|
||||
|
||||
const MDXComponents = {
|
||||
export const MDXComponents = {
|
||||
Image,
|
||||
a: CustomLink,
|
||||
pre: Pre,
|
||||
wrapper: ({ components, layout, ...rest }) => {
|
||||
const Layout = require(`../layouts/${layout}`).default
|
||||
return <Layout {...rest} />
|
||||
},
|
||||
}
|
||||
|
||||
export default MDXComponents
|
||||
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
|
||||
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
|
||||
|
||||
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
|
||||
}
|
||||
|
71
components/Pre.js
Normal file
71
components/Pre.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { useState, useRef } from 'react'
|
||||
|
||||
const Pre = (props) => {
|
||||
const textInput = useRef(null)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const onEnter = () => {
|
||||
setHovered(true)
|
||||
}
|
||||
const onExit = () => {
|
||||
setHovered(false)
|
||||
setCopied(false)
|
||||
}
|
||||
const onCopy = () => {
|
||||
setCopied(true)
|
||||
navigator.clipboard.writeText(textInput.current.textContent)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
|
||||
{hovered && (
|
||||
<button
|
||||
aria-label="Copy code"
|
||||
type="button"
|
||||
className={`absolute right-2 top-2 w-8 h-8 p-1 rounded border-2 bg-gray-700 dark:bg-gray-800 ${
|
||||
copied
|
||||
? 'focus:outline-none focus:border-green-400 border-green-400'
|
||||
: 'border-gray-300'
|
||||
}`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
className={copied ? 'text-green-400' : 'text-gray-300'}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<pre>{props.children}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pre
|
@ -1,53 +1,31 @@
|
||||
import { NextSeo, ArticleJsonLd } from 'next-seo'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export const SEO = {
|
||||
title: siteMetadata.title,
|
||||
description: siteMetadata.description,
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: siteMetadata.language,
|
||||
url: siteMetadata.siteUrl,
|
||||
title: siteMetadata.title,
|
||||
description: siteMetadata.description,
|
||||
images: [
|
||||
{
|
||||
url: `${siteMetadata.siteUrl}${siteMetadata.socialBanner}`,
|
||||
alt: siteMetadata.title,
|
||||
width: 1200,
|
||||
height: 600,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
handle: siteMetadata.twitter,
|
||||
site: siteMetadata.twitter,
|
||||
cardType: 'summary_large_image',
|
||||
},
|
||||
additionalMetaTags: [
|
||||
{
|
||||
name: 'author',
|
||||
content: siteMetadata.author,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const PageSeo = ({ title, description, url }) => {
|
||||
export const PageSeo = ({ title, description }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<NextSeo
|
||||
title={`${title} – ${siteMetadata.title}`}
|
||||
description={description}
|
||||
canonical={url}
|
||||
openGraph={{
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
}}
|
||||
/>
|
||||
<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 = ({ title, summary, date, lastmod, url, tags, images = [] }) => {
|
||||
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 =
|
||||
@ -59,47 +37,76 @@ export const BlogSeo = ({ title, summary, date, lastmod, url, tags, images = []
|
||||
|
||||
const featuredImages = imagesArr.map((img) => {
|
||||
return {
|
||||
'@type': 'ImageObject',
|
||||
url: `${siteMetadata.siteUrl}${img}`,
|
||||
alt: title,
|
||||
}
|
||||
})
|
||||
|
||||
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 (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${title} – ${siteMetadata.title}`}
|
||||
description={summary}
|
||||
canonical={url}
|
||||
openGraph={{
|
||||
type: 'article',
|
||||
article: {
|
||||
publishedTime: publishedAt,
|
||||
modifiedTime: modifiedAt,
|
||||
authors: [`${siteMetadata.siteUrl}/about`],
|
||||
tags,
|
||||
},
|
||||
url,
|
||||
title,
|
||||
description: summary,
|
||||
images: featuredImages,
|
||||
}}
|
||||
additionalMetaTags={[
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: featuredImages[0].url,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ArticleJsonLd
|
||||
authorName={siteMetadata.author}
|
||||
dateModified={modifiedAt}
|
||||
datePublished={publishedAt}
|
||||
description={summary}
|
||||
images={featuredImages}
|
||||
publisherName={siteMetadata.author}
|
||||
title={title}
|
||||
url={url}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import kebabCase from '@/lib/utils/kebabCase'
|
||||
const Tag = ({ text }) => {
|
||||
return (
|
||||
<Link href={`/tags/${kebabCase(text)}`}>
|
||||
<a className="mr-3 text-sm font-medium text-blue-500 uppercase hover:text-blue-600 dark:hover:text-blue-400">
|
||||
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
{text.split(' ').join('-')}
|
||||
</a>
|
||||
</Link>
|
||||
|
@ -6,7 +6,7 @@
|
||||
@apply px-5 py-3 font-mono text-sm font-bold text-gray-200 bg-gray-700 rounded-t;
|
||||
}
|
||||
|
||||
.remark-code-title + pre {
|
||||
.remark-code-title + div > pre {
|
||||
@apply mt-0 rounded-t-none;
|
||||
}
|
||||
|
||||
@ -14,6 +14,19 @@
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
@apply pl-4 -mx-4 border-l-4 border-gray-800;
|
||||
}
|
||||
|
||||
.highlight-line {
|
||||
@apply -mx-4 bg-gray-700 bg-opacity-50 border-l-4 border-primary-500;
|
||||
}
|
||||
|
||||
.line-number::before {
|
||||
@apply pr-4 -ml-2 text-gray-400;
|
||||
content: attr(line);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
16
data/authors/default.md
Normal file
16
data/authors/default.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Tails Azimuth
|
||||
avatar: /static/images/avatar.png
|
||||
occupation: Professor of Atmospheric Science
|
||||
company: Stanford University
|
||||
email: address@yoursite.com
|
||||
twitter: https://twitter.com/Twitter
|
||||
linkedin: https://www.linkedin.com
|
||||
github: https://github.com
|
||||
---
|
||||
|
||||
Tails Azimuth is a professor of atmospheric sciences at the Stanford AI Lab. His research interests includes complexity modelling of tailwinds, headwinds and crosswinds.
|
||||
|
||||
He leads the clean energy group which develops 3D air pollution-climate models, writes differential equation solvers, and manufactures titanium plated air ballons. In his free time he bakes raspberry pi.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
|
12
data/authors/sparrowhawk.md
Normal file
12
data/authors/sparrowhawk.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Sparrow Hawk
|
||||
avatar: /static/images/sparrowhawk-avatar.jpg
|
||||
occupation: Wizard of Earthsea
|
||||
company: Earthsea
|
||||
twitter: https://twitter.com/sparrowhawk
|
||||
linkedin: https://www.linkedin.com/sparrowhawk
|
||||
---
|
||||
|
||||
At birth Ged was given the child-name Duny by his mother. He was born on the island of Gont, son of a bronzesmith. His mother died before he reached the age of one. As a small boy, Ged had overheard the village witch, his maternal aunt, using various words of power to call goats. Ged later used the words without understanding of their meanings, to surprising effect.
|
||||
|
||||
The witch knew that using words of power effectively without understanding them required innate power, so she endeavored to teach him what little she knew. After learning more from her, he was able to call animals to him. Particularly, he was seen in the company of wild sparrowhawks so often that his "use name" became Sparrowhawk.
|
@ -140,6 +140,7 @@ function fancyAlert(arg) {
|
||||
$.facebox({ div: '#foo' })
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
And here's how it looks - nicely colored with styled code titles!
|
||||
|
@ -4,6 +4,7 @@ date: '2020-11-11'
|
||||
tags: ['next js', 'guide']
|
||||
draft: false
|
||||
summary: 'In this article we introduce adding images in the tailwind starter blog and the benefits and limitations of the next/image component.'
|
||||
author: sparrowhawk
|
||||
---
|
||||
|
||||
# Introduction
|
||||
@ -49,14 +50,10 @@ _Note_: If you try to save the image, it is in webp format, if your browser supp
|
||||
![ocean](/static/images/ocean.jpeg)
|
||||
|
||||
<p>
|
||||
Photo by{' '}
|
||||
<a href="https://unsplash.com/@yucar?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
YUCAR FotoGrafik
|
||||
</a>{' '}
|
||||
on{' '}
|
||||
<a href="https://unsplash.com/s/photos/sea?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Unsplash
|
||||
</a>
|
||||
Photo by [YUCAR
|
||||
FotoGrafik](https://unsplash.com/@yucar?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/s/photos/sea?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
</p>
|
||||
|
||||
# Benefits
|
||||
|
@ -1,11 +1,12 @@
|
||||
---
|
||||
title: 'Introducing Tailwind Nexjs Starter Blog'
|
||||
date: '2021-01-12'
|
||||
lastmod: '2021-05-08'
|
||||
lastmod: '2021-07-11'
|
||||
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.'
|
||||
images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg']
|
||||
authors: ['default', 'sparrowhawk']
|
||||
---
|
||||
|
||||
![tailwind-nextjs-banner](/static/images/twitter-card.png)
|
||||
@ -33,18 +34,21 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
||||
|
||||
## Features
|
||||
|
||||
- Easy styling customization with [Tailwind 2.0](https://blog.tailwindcss.com/tailwindcss-v2)
|
||||
- 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, 43kB first load JS, uses Preact in production build
|
||||
- Lightweight, 38kB first load JS, uses Preact in production build
|
||||
- Mobile-friendly view
|
||||
- Light and dark theme
|
||||
- [MDX - write JSX in markdown documents!](https://mdxjs.com/)
|
||||
- Server-side syntax highlighting with [rehype-prism](https://github.com/mapbox/rehype-prism)
|
||||
- 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/)
|
||||
- Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization)
|
||||
- Flexible data retrieval with [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote)
|
||||
- Flexible data retrieval with [mdx-bundler](https://github.com/kentcdodds/mdx-bundler)
|
||||
- Support for tags - each unique tag will be its own page
|
||||
- Support for multiple authors
|
||||
- Blog templates
|
||||
- Support for nested routing of blog posts
|
||||
- Projects page
|
||||
- SEO friendly with RSS feed, sitemaps and more!
|
||||
|
||||
## Sample posts
|
||||
@ -58,12 +62,13 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
1. `npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git`
|
||||
2. Personalize `siteMetadata.json`
|
||||
3. Modify `projectsData.js`
|
||||
4. Modify `headerNavLinks.js` to customize navigation links
|
||||
5. Add blog posts
|
||||
6. Deploy on Vercel
|
||||
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)
|
||||
3. Personalize `authors/default.md` (main author)
|
||||
4. Modify `projectsData.js`
|
||||
5. Modify `headerNavLinks.js` to customize navigation links
|
||||
6. Add blog posts
|
||||
7. Deploy on Vercel
|
||||
|
||||
## Development
|
||||
|
||||
@ -83,6 +88,8 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
|
||||
|
||||
`data/siteMetadata.json` - 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`.
|
||||
|
||||
`data/projectsData.js` - data used to generate styled card in projects page.
|
||||
|
||||
`data/headerNavLinks.js` - navigation links.
|
||||
@ -93,7 +100,7 @@ You can start editing the page by modifying `pages/index.js`. The page auto-upda
|
||||
|
||||
`public/static` - store assets such as images and favicons.
|
||||
|
||||
`css/tailwind.css` - contains the tailwind stylesheet which can be modified to change the overall look and feel of the site.
|
||||
`tailwind.config.js` and `css/tailwind.css` - contain the tailwind stylesheet which can be modified to change the overall look and feel of the site.
|
||||
|
||||
`components/social-icons` - to add other icons, simply copy an svg file from [Simple Icons](https://simpleicons.org/) and map them in `index.js`. Other icons uses [heroicons](https://heroicons.com/).
|
||||
|
||||
@ -119,6 +126,8 @@ lastmod (optional)
|
||||
draft (optional)
|
||||
summary (optional)
|
||||
images (optional, if none provided defaults to socialBanner in siteMetadata config)
|
||||
authors (optional list which should correspond to the file names in `data/authors`. Uses `default` if none is specified)
|
||||
layout (optional list which should correspond to the file names in `data/layouts`)
|
||||
```
|
||||
|
||||
Here's an example of a post's frontmatter:
|
||||
@ -132,6 +141,8 @@ 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.'
|
||||
images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg']
|
||||
authors: ['default', 'sparrowhawk']
|
||||
layout: PostLayout
|
||||
---
|
||||
```
|
||||
|
||||
|
267
data/blog/new-features-in-v1.mdx
Normal file
267
data/blog/new-features-in-v1.mdx
Normal file
@ -0,0 +1,267 @@
|
||||
---
|
||||
title: 'New features in v1'
|
||||
date: '2021-07-11'
|
||||
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'
|
||||
layout: PostSimple
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
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)
|
||||
|
||||
First load JS decreased from 43kB to 38kB despite all the new features added!
|
||||
|
||||
See [upgrade guide](#upgrade-guide) below if you are migrating from v0 version of the template.
|
||||
|
||||
## Theme colors
|
||||
|
||||
You can easily modify the theme color by changing the primary attribute in the tailwind config file:
|
||||
|
||||
```js:tailwind.config.js
|
||||
theme: {
|
||||
colors: {
|
||||
primary: colors.teal,
|
||||
gray: colors.trueGray,
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The primary color attribute should be assigned an object with keys from 50, 100, 200 ... 900 and the corresponding color code values.
|
||||
|
||||
Tailwind includes great default color palettes that can be used for theming your own website. Check out [customizing colors documentation page](https://tailwindcss.com/docs/customizing-colors) for the full range of options.
|
||||
|
||||
Migrating from v1? You can revert to the previous theme by setting `primary` to `colors.sky` (Tailwind 2.2.2 and above, otherwise `colors.lightBlue`) and changing gray to `colors.coolGray`.
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
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'
|
||||
;<PageTitle> Using JSX components in MDX </PageTitle>
|
||||
```
|
||||
|
||||
import PageTitle from './PageTitle.js'
|
||||
|
||||
<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 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.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
You can configure the template to take in other fields - see `PostLayout` component for an example.
|
||||
|
||||
Here's an example layout which you can further customise:
|
||||
|
||||
```js
|
||||
export default function ExampleLayout({ frontMatter, children }) {
|
||||
const { date, title } = frontMatter
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<div>{date}</div>
|
||||
<h1>{title}</h1>
|
||||
<div>{children}</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Configuring a blog post frontmatter
|
||||
|
||||
Use the `layout` frontmatter field to specify the template you want to map the markdown post to. Here's how the frontmatter of this post looks like:
|
||||
|
||||
```
|
||||
---
|
||||
title: 'New features in v1'
|
||||
date: '2021-05-26 '
|
||||
tags: ['next-js', 'tailwind', 'guide']
|
||||
draft: false
|
||||
summary: 'Introducing the new layout features - you can map mdx blog content to layout components by configuring the frontmatter field'
|
||||
layout: PostSimple
|
||||
---
|
||||
```
|
||||
|
||||
You can configure the default layout in the respective page section by modifying the `DEFAULT_LAYOUT` variable.
|
||||
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.
|
||||
|
||||
```js
|
||||
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
|
||||
const LayoutComponent = require(`../layouts/${layout}`).default
|
||||
|
||||
return (
|
||||
<LayoutComponent {...rest}>
|
||||
<MDXRemote {...mdxSource} components={MDXComponents} />
|
||||
</LayoutComponent>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
Here's how an author markdown file might looks like:
|
||||
|
||||
```md:default.md
|
||||
---
|
||||
name: Tails Azimuth
|
||||
avatar: /static/images/avatar.png
|
||||
occupation: Professor of Atmospheric Science
|
||||
company: Stanford University
|
||||
email: address@yoursite.com
|
||||
twitter: https://twitter.com/Twitter
|
||||
linkedin: https://www.linkedin.com
|
||||
github: https://github.com
|
||||
---
|
||||
|
||||
A long description of yourself...
|
||||
```
|
||||
|
||||
You can use this information in multiple places across the template. For example in the about section of the page, we grab the default author information with this line of code:
|
||||
|
||||
```
|
||||
const authorDetails = await getFileBySlug('authors', ['default'])
|
||||
```
|
||||
|
||||
This is rendered in the `AuthorLayout` template.
|
||||
|
||||
### Multiple authors in blog post
|
||||
|
||||
The frontmatter of a blog post accepts an optional `authors` arrray field. If no field is specified, it is assumed that the default author is used. Simply pass in an array of authors to render multiple authors associated with post.
|
||||
|
||||
For example, the following frontmatter will display the authors given by `data/authors/default.md` and `data/authors/sparrowhawk.md`
|
||||
|
||||
```
|
||||
title: 'My first post'
|
||||
date: '2021-01-12'
|
||||
draft: false
|
||||
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).
|
||||
|
||||
## Copy button for code blocks
|
||||
|
||||
Hover over a code block and you will notice a Github inspired copy button! You can modify `./components/Pre.js` to further customise it.
|
||||
The component is passed to `MDXComponents` and modifies all `<pre>` blocks.
|
||||
|
||||
## Line highlighting and line numbers
|
||||
|
||||
Line highlighting and line numbers is now supported out of the box thanks to the new [rehype-prism-plus plugin](https://github.com/timlrx/rehype-prism-plus)
|
||||
|
||||
The following javascript code block:
|
||||
|
||||
````
|
||||
```js {1, 3-4} showLineNumbers
|
||||
var num1, num2, sum
|
||||
num1 = prompt('Enter first number')
|
||||
num2 = prompt('Enter second number')
|
||||
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
|
||||
alert('Sum = ' + sum) // "+" means combine into a string
|
||||
```
|
||||
````
|
||||
|
||||
will appear as:
|
||||
|
||||
```js {1,3-4} showLineNumbers
|
||||
var num1, num2, sum
|
||||
num1 = prompt('Enter first number')
|
||||
num2 = prompt('Enter second number')
|
||||
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
|
||||
alert('Sum = ' + sum) // "+" means combine into a string
|
||||
```
|
||||
|
||||
To modify the styles, change the following class selectors in the `tailwind.css` file:
|
||||
|
||||
```css
|
||||
.code-line {
|
||||
@apply pl-4 -mx-4 border-l-4 border-gray-800;
|
||||
}
|
||||
|
||||
.highlight-line {
|
||||
@apply -mx-4 bg-gray-700 bg-opacity-50 border-l-4 border-primary-500;
|
||||
}
|
||||
|
||||
.line-number::before {
|
||||
@apply pr-4 -ml-2 text-gray-400;
|
||||
content: attr(line);
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrade guide
|
||||
|
||||
There are significant portions of the code that has been changed from v0 to v1 including support for layouts and a new mdx engine.
|
||||
|
||||
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.
|
||||
|
||||
Another alternative would be to pull the latest tempate version with the following code:
|
||||
|
||||
```bash
|
||||
git remote add template git@github.com:timlrx/tailwind-nextjs-starter-blog.git
|
||||
git pull template v1 --allow-unrelated-histories
|
||||
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:
|
||||
|
||||
```
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/:path/index.xml',
|
||||
destination: '/:path/feed.xml',
|
||||
permanent: true,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@ -59,42 +59,26 @@ When MDX v2 is ready, one could potentially interleave markdown in jsx directly!
|
||||
### Photo Credits
|
||||
|
||||
<div>
|
||||
Maple photo by{' '}
|
||||
<a href="https://unsplash.com/@i_am_g?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Guillaume Jaillet
|
||||
</a>{' '}
|
||||
on{' '}
|
||||
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Unsplash
|
||||
</a>
|
||||
Maple photo by [Guillaume
|
||||
Jaillet](https://unsplash.com/@i_am_g?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
</div>
|
||||
<div>
|
||||
Mountains photo by{' '}
|
||||
<a href="https://unsplash.com/@john_artifexfilms?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
John Lee
|
||||
</a>{' '}
|
||||
on{' '}
|
||||
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Unsplash
|
||||
</a>
|
||||
Mountains photo by [John
|
||||
Lee](https://unsplash.com/@john_artifexfilms?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
</div>
|
||||
<div>
|
||||
Lake photo by{' '}
|
||||
<a href="https://unsplash.com/@tjholowaychuk?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Tj Holowaychuk
|
||||
</a>{' '}
|
||||
on{' '}
|
||||
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Unsplash
|
||||
</a>
|
||||
Lake photo by [Tj
|
||||
Holowaychuk](https://unsplash.com/@tjholowaychuk?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
</div>
|
||||
<div>
|
||||
Toronto photo by{' '}
|
||||
<a href="https://unsplash.com/@matthewhenry?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Matthew Henry
|
||||
</a>{' '}
|
||||
on{' '}
|
||||
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
|
||||
Unsplash
|
||||
</a>
|
||||
Toronto photo by [Matthew
|
||||
Henry](https://unsplash.com/@matthewhenry?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@
|
||||
"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",
|
||||
|
41
layouts/AuthorLayout.js
Normal file
41
layouts/AuthorLayout.js
Normal file
@ -0,0 +1,41 @@
|
||||
import SocialIcon from '@/components/social-icons'
|
||||
import Image from '@/components/Image'
|
||||
import { PageSeo } from '@/components/SEO'
|
||||
|
||||
export default function AuthorLayout({ children, frontMatter }) {
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
About
|
||||
</h1>
|
||||
</div>
|
||||
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
|
||||
<div className="flex flex-col items-center pt-8 space-x-2">
|
||||
<Image
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
width="192px"
|
||||
height="192px"
|
||||
className="w-48 h-48 rounded-full"
|
||||
/>
|
||||
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">{name}</h3>
|
||||
<div className="text-gray-500 dark:text-gray-400">{occupation}</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">{company}</div>
|
||||
<div className="flex pt-6 space-x-3">
|
||||
<SocialIcon kind="mail" href={`mailto:${email}`} />
|
||||
<SocialIcon kind="github" href={github} />
|
||||
<SocialIcon kind="linkedin" href={linkedin} />
|
||||
<SocialIcon kind="twitter" href={twitter} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-8 pb-8 prose dark:prose-dark max-w-none xl:col-span-2">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -3,8 +3,7 @@ import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { useState } from 'react'
|
||||
import Pagination from '@/components/Pagination'
|
||||
|
||||
const postDateTemplate = { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
|
||||
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
@ -30,7 +29,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
||||
type="text"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder="Search articles"
|
||||
className="block w-full px-4 py-2 text-gray-900 bg-white border border-gray-300 rounded-md dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="block w-full px-4 py-2 text-gray-900 bg-white border border-gray-300 rounded-md dark:border-gray-900 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<svg
|
||||
className="absolute w-5 h-5 text-gray-400 right-3 top-3 dark:text-gray-300"
|
||||
@ -58,9 +57,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>
|
||||
{new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
|
||||
</time>
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-3 xl:col-span-3">
|
||||
|
@ -14,12 +14,16 @@ const discussUrl = (slug) =>
|
||||
|
||||
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
|
||||
|
||||
export default function PostLayout({ children, frontMatter, next, prev }) {
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||
const { slug, fileName, date, title, tags } = frontMatter
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<BlogSeo url={`${siteMetadata.siteUrl}/blog/${slug}`} {...frontMatter} />
|
||||
<BlogSeo
|
||||
url={`${siteMetadata.siteUrl}/blog/${slug}`}
|
||||
authorDetails={authorDetails}
|
||||
{...frontMatter}
|
||||
/>
|
||||
<article>
|
||||
<div className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700">
|
||||
<header className="pt-6 xl:pb-6">
|
||||
@ -47,32 +51,34 @@ export default function PostLayout({ children, frontMatter, next, prev }) {
|
||||
<dt className="sr-only">Authors</dt>
|
||||
<dd>
|
||||
<ul className="flex justify-center space-x-8 xl:block sm:space-x-12 xl:space-x-0 xl:space-y-8">
|
||||
<li className="flex items-center space-x-2">
|
||||
<Image
|
||||
src={siteMetadata.image}
|
||||
width="38px"
|
||||
height="38px"
|
||||
alt="avatar"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<dl className="text-sm font-medium leading-5 whitespace-nowrap">
|
||||
<dt className="sr-only">Name</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">{siteMetadata.author}</dd>
|
||||
{typeof siteMetadata.twitter === 'string' && (
|
||||
<>
|
||||
<dt className="sr-only">Twitter</dt>
|
||||
<dd>
|
||||
<Link
|
||||
href={siteMetadata.twitter}
|
||||
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
{siteMetadata.twitter.replace('https://twitter.com/', '@')}
|
||||
</Link>
|
||||
</dd>
|
||||
</>
|
||||
{authorDetails.map((author) => (
|
||||
<li className="flex items-center space-x-2" key={author.name}>
|
||||
{author.avatar && (
|
||||
<Image
|
||||
src={author.avatar}
|
||||
width="38px"
|
||||
height="38px"
|
||||
alt="avatar"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
)}
|
||||
</dl>
|
||||
</li>
|
||||
<dl className="text-sm font-medium leading-5 whitespace-nowrap">
|
||||
<dt className="sr-only">Name</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
|
||||
<dt className="sr-only">Twitter</dt>
|
||||
<dd>
|
||||
{author.twitter && (
|
||||
<Link
|
||||
href={author.twitter}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
{author.twitter.replace('https://twitter.com/', '@')}
|
||||
</Link>
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
@ -107,7 +113,7 @@ export default function PostLayout({ children, frontMatter, next, prev }) {
|
||||
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
|
||||
Previous Article
|
||||
</h2>
|
||||
<div className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400">
|
||||
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<Link href={`/blog/${prev.slug}`}>{prev.title}</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,7 +123,7 @@ export default function PostLayout({ children, frontMatter, next, prev }) {
|
||||
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
|
||||
Next Article
|
||||
</h2>
|
||||
<div className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400">
|
||||
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<Link href={`/blog/${next.slug}`}>{next.title}</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -128,7 +134,7 @@ export default function PostLayout({ children, frontMatter, next, prev }) {
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
← Back to the blog
|
||||
</Link>
|
||||
|
67
layouts/PostSimple.js
Normal file
67
layouts/PostSimple.js
Normal file
@ -0,0 +1,67 @@
|
||||
import Link from '@/components/Link'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import { BlogSeo } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||
const { date, title } = frontMatter
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<BlogSeo url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
|
||||
<article>
|
||||
<div>
|
||||
<header>
|
||||
<div className="pb-10 space-y-1 text-center border-b border-gray-200 dark:border-gray-700">
|
||||
<dl>
|
||||
<div>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div>
|
||||
<PageTitle>{title}</PageTitle>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="pb-8 divide-y divide-gray-200 xl:divide-y-0 dark:divide-gray-700 "
|
||||
style={{ gridTemplateRows: 'auto 1fr' }}
|
||||
>
|
||||
<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>
|
||||
<footer>
|
||||
<div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base">
|
||||
{prev && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/blog/${prev.slug}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
← {prev.title}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{next && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/blog/${next.slug}`}
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
{next.title} →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
@ -14,7 +14,7 @@ const generateRssItem = (post) => `
|
||||
</item>
|
||||
`
|
||||
|
||||
const generateRss = (posts, page = 'index.xml') => `
|
||||
const generateRss = (posts, page = 'feed.xml') => `
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>${escape(siteMetadata.title)}</title>
|
||||
|
@ -15,13 +15,14 @@ module.exports = (options) => (tree) => {
|
||||
const dimensions = sizeOf(`${process.cwd()}/public${imageNode.url}`)
|
||||
|
||||
// Convert original node to next/image
|
||||
imageNode.type = 'jsx'
|
||||
imageNode.value = `<Image
|
||||
alt={\`${imageNode.alt}\`}
|
||||
src={\`${imageNode.url}\`}
|
||||
width={${dimensions.width}}
|
||||
height={${dimensions.height}}
|
||||
/>`
|
||||
;(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'
|
||||
|
64
lib/mdx.js
64
lib/mdx.js
@ -1,10 +1,10 @@
|
||||
import MDXComponents from '@/components/MDXComponents'
|
||||
import { bundleMDX } from 'mdx-bundler'
|
||||
import fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
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 getAllFilesRecursively from './utils/files'
|
||||
|
||||
@ -48,21 +48,45 @@ export async function getFileBySlug(type, slug) {
|
||||
? fs.readFileSync(mdxPath, 'utf8')
|
||||
: fs.readFileSync(mdPath, 'utf8')
|
||||
|
||||
const { data, content } = matter(source)
|
||||
const mdxSource = await serialize(content, {
|
||||
components: MDXComponents,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
// https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent
|
||||
if (process.platform === 'win32') {
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(
|
||||
process.cwd(),
|
||||
'node_modules',
|
||||
'esbuild',
|
||||
'esbuild.exe'
|
||||
)
|
||||
} else {
|
||||
process.env.ESBUILD_BINARY_PATH = path.join(
|
||||
process.cwd(),
|
||||
'node_modules',
|
||||
'esbuild',
|
||||
'bin',
|
||||
'esbuild'
|
||||
)
|
||||
}
|
||||
|
||||
const { frontmatter, code } = await bundleMDX(source, {
|
||||
// mdx imports can be automatically source from the components directory
|
||||
cwd: path.join(process.cwd(), 'components'),
|
||||
xdmOptions(options) {
|
||||
// this is the recommended way to add custom remark/rehype plugins:
|
||||
// The syntax might look weird, but it protects you in case we add/remove
|
||||
// plugins in the future.
|
||||
options.remarkPlugins = [
|
||||
...(options.remarkPlugins ?? []),
|
||||
require('remark-slug'),
|
||||
require('remark-autolink-headings'),
|
||||
require('remark-code-titles'),
|
||||
require('remark-gfm'),
|
||||
codeTitles,
|
||||
[require('remark-footnotes'), { inlineNotes: true }],
|
||||
require('remark-math'),
|
||||
imgToJsx,
|
||||
],
|
||||
inlineNotes: true,
|
||||
rehypePlugins: [
|
||||
]
|
||||
options.rehypePlugins = [
|
||||
...(options.rehypePlugins ?? []),
|
||||
require('rehype-katex'),
|
||||
require('@mapbox/rehype-prism'),
|
||||
[require('rehype-prism-plus'), { ignoreMissing: true }],
|
||||
() => {
|
||||
return (tree) => {
|
||||
visit(tree, 'element', (node, index, parent) => {
|
||||
@ -73,17 +97,25 @@ export async function getFileBySlug(type, slug) {
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
]
|
||||
return options
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.loader = {
|
||||
...options.loader,
|
||||
'.js': 'jsx',
|
||||
}
|
||||
return options
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
mdxSource,
|
||||
mdxSource: code,
|
||||
frontMatter: {
|
||||
readingTime: readingTime(content),
|
||||
readingTime: readingTime(code),
|
||||
slug: slug || null,
|
||||
fileName: fs.existsSync(mdxPath) ? `${slug}.mdx` : `${slug}.md`,
|
||||
...data,
|
||||
...frontmatter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
32
lib/remark-code-title.js
Normal file
32
lib/remark-code-title.js
Normal file
@ -0,0 +1,32 @@
|
||||
import visit from 'unist-util-visit'
|
||||
|
||||
module.exports = function (options) {
|
||||
return (tree) =>
|
||||
visit(tree, 'code', (node, index) => {
|
||||
const nodeLang = node.lang || ''
|
||||
let language = ''
|
||||
let title = ''
|
||||
|
||||
if (nodeLang.includes(':')) {
|
||||
language = nodeLang.slice(0, nodeLang.search(':'))
|
||||
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length)
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
return
|
||||
}
|
||||
|
||||
const className = 'remark-code-title'
|
||||
|
||||
const titleNode = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'div',
|
||||
attributes: [{ type: 'mdxJsxAttribute', name: 'className', value: className }],
|
||||
children: [{ type: 'text', value: title }],
|
||||
data: { _xdmExplicitJsx: true },
|
||||
}
|
||||
|
||||
tree.children.splice(index, 0, titleNode)
|
||||
node.lang = language
|
||||
})
|
||||
}
|
14
lib/utils/formatDate.js
Normal file
14
lib/utils/formatDate.js
Normal file
@ -0,0 +1,14 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const formatDate = (date) => {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}
|
||||
const now = new Date(date).toLocaleDateString(siteMetadata.locale, options)
|
||||
|
||||
return now
|
||||
}
|
||||
|
||||
export default formatDate
|
@ -38,17 +38,4 @@ module.exports = withBundleAnalyzer({
|
||||
|
||||
return config
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
// Rewrite to prevent a problem when deploying at vercel
|
||||
// which directs a user to the index.xml instead of index.html
|
||||
// https://github.com/timlrx/tailwind-nextjs-starter-blog/issues/16
|
||||
{
|
||||
source: '/',
|
||||
destination: '/index',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
2217
package-lock.json
generated
2217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -12,26 +12,25 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/rehype-prism": "^0.6.0",
|
||||
"@tailwindcss/forms": "^0.3.2",
|
||||
"@tailwindcss/typography": "^0.4.0",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"gray-matter": "^4.0.2",
|
||||
"image-size": "1.0.0",
|
||||
"mdx-bundler": "^4.1.0",
|
||||
"next": "11.0.1",
|
||||
"next-mdx-remote": "^3.0.1",
|
||||
"next-seo": "4.24.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": "^4.0.0",
|
||||
"rehype-katex": "^5.0.0",
|
||||
"rehype-prism-plus": "0.0.1",
|
||||
"remark-autolink-headings": "6.0.1",
|
||||
"remark-code-titles": "0.1.2",
|
||||
"remark-footnotes": "^3.0.0",
|
||||
"remark-math": "^3.0.1",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"remark-math": "^4.0.0",
|
||||
"remark-slug": "6.0.0",
|
||||
"tailwindcss": "^2.2.2"
|
||||
},
|
||||
|
@ -1,10 +1,8 @@
|
||||
import '@/css/tailwind.css'
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import Head from 'next/head'
|
||||
|
||||
import { SEO } from '@/components/SEO'
|
||||
import LayoutWrapper from '@/components/LayoutWrapper'
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
@ -13,7 +11,6 @@ export default function App({ Component, pageProps }) {
|
||||
<Head>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
</Head>
|
||||
<DefaultSeo {...SEO} />
|
||||
<LayoutWrapper>
|
||||
<Component {...pageProps} />
|
||||
</LayoutWrapper>
|
||||
|
@ -21,7 +21,7 @@ class MyDocument extends Document {
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="alternate" type="application/rss+xml" href="/index.xml" />
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
|
||||
|
@ -1,64 +1,21 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import SocialIcon from '@/components/social-icons'
|
||||
import Image from '@/components/Image'
|
||||
import { PageSeo } from '@/components/SEO'
|
||||
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
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={`About - ${siteMetadata.author}`}
|
||||
description={`About me - ${siteMetadata.author}`}
|
||||
url={`${siteMetadata.siteUrl}/about`}
|
||||
/>
|
||||
<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">
|
||||
About
|
||||
</h1>
|
||||
</div>
|
||||
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
|
||||
<div className="flex flex-col items-center pt-8 space-x-2">
|
||||
<Image
|
||||
src={siteMetadata.image}
|
||||
alt="avatar"
|
||||
width="192px"
|
||||
height="192px"
|
||||
className="w-48 h-48 rounded-full"
|
||||
/>
|
||||
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">
|
||||
{siteMetadata.author}
|
||||
</h3>
|
||||
<div className="text-gray-500 dark:text-gray-400">Professor of Atmospheric Science</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">Stanford University</div>
|
||||
<div className="flex pt-6 space-x-3">
|
||||
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} />
|
||||
<SocialIcon kind="github" href={siteMetadata.github} />
|
||||
<SocialIcon kind="facebook" href={siteMetadata.facebook} />
|
||||
<SocialIcon kind="youtube" href={siteMetadata.youtube} />
|
||||
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} />
|
||||
<SocialIcon kind="twitter" href={siteMetadata.twitter} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-8 pb-8 prose dark:prose-dark max-w-none xl:col-span-2">
|
||||
<p>
|
||||
Tails Azimuth is a professor of atmospheric sciences at the Stanford AI Lab. His
|
||||
research interests includes complexity modelling of tailwinds, headwinds and
|
||||
crosswinds.
|
||||
</p>
|
||||
<p>
|
||||
He leads the clean energy group which develops 3D air pollution-climate models, writes
|
||||
differential equation solvers, and manufactures titanium plated air ballons. In his
|
||||
free time he bakes raspberry pi.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique
|
||||
placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem
|
||||
nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<MDXLayoutRenderer
|
||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { PageSeo } from '@/components/SEO'
|
||||
|
||||
export const POSTS_PER_PAGE = 10
|
||||
export const POSTS_PER_PAGE = 5
|
||||
|
||||
export async function getStaticProps() {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
@ -19,11 +19,7 @@ export async function getStaticProps() {
|
||||
export default function Blog({ posts, initialDisplayPosts, pagination }) {
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={`Blog - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description}
|
||||
url={`${siteMetadata.siteUrl}/blog`}
|
||||
/>
|
||||
<PageSeo title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import fs from 'fs'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import MDXComponents from '@/components/MDXComponents'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import PostLayout from '@/layouts/PostLayout'
|
||||
import generateRss from '@/lib/generate-rss'
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
|
||||
|
||||
const DEFAULT_LAYOUT = 'PostLayout'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = getFiles('blog')
|
||||
return {
|
||||
@ -24,23 +24,34 @@ export async function getStaticProps({ params }) {
|
||||
const prev = allPosts[postIndex + 1] || null
|
||||
const next = allPosts[postIndex - 1] || null
|
||||
const post = await getFileBySlug('blog', params.slug.join('/'))
|
||||
const authorList = post.frontMatter.authors || ['default']
|
||||
const authorPromise = authorList.map(async (author) => {
|
||||
const authorResults = await getFileBySlug('authors', [author])
|
||||
return authorResults.frontMatter
|
||||
})
|
||||
const authorDetails = await Promise.all(authorPromise)
|
||||
|
||||
// rss
|
||||
const rss = generateRss(allPosts)
|
||||
fs.writeFileSync('./public/index.xml', rss)
|
||||
fs.writeFileSync('./public/feed.xml', rss)
|
||||
|
||||
return { props: { post, prev, next } }
|
||||
return { props: { post, authorDetails, prev, next } }
|
||||
}
|
||||
|
||||
export default function Blog({ post, prev, next }) {
|
||||
export default function Blog({ post, authorDetails, prev, next }) {
|
||||
const { mdxSource, frontMatter } = post
|
||||
|
||||
return (
|
||||
<>
|
||||
{frontMatter.draft !== true ? (
|
||||
<PostLayout frontMatter={frontMatter} prev={prev} next={next}>
|
||||
<MDXRemote {...mdxSource} components={MDXComponents} />
|
||||
</PostLayout>
|
||||
<MDXLayoutRenderer
|
||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
authorDetails={authorDetails}
|
||||
prev={prev}
|
||||
next={next}
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-24 text-center">
|
||||
<PageTitle>
|
||||
|
@ -44,11 +44,7 @@ export async function getStaticProps(context) {
|
||||
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={siteMetadata.title}
|
||||
description={siteMetadata.description}
|
||||
url={`${siteMetadata.siteUrl}/blog/${pagination.currentPage}`}
|
||||
/>
|
||||
<PageSeo title={siteMetadata.title} description={siteMetadata.description} />
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
|
@ -3,9 +3,9 @@ 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'
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
const postDateTemplate = { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
|
||||
export async function getStaticProps() {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
@ -16,11 +16,7 @@ export async function getStaticProps() {
|
||||
export default function Home({ posts }) {
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={siteMetadata.title}
|
||||
description={siteMetadata.description}
|
||||
url={siteMetadata.siteUrl}
|
||||
/>
|
||||
<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">
|
||||
@ -41,9 +37,7 @@ export default function Home({ posts }) {
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>
|
||||
{new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
|
||||
</time>
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-5 xl:col-span-3">
|
||||
@ -70,7 +64,7 @@ export default function Home({ posts }) {
|
||||
<div className="text-base font-medium leading-6">
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Read "${title}"`}
|
||||
>
|
||||
Read more →
|
||||
@ -88,7 +82,7 @@ export default function Home({ posts }) {
|
||||
<div className="flex justify-end text-base font-medium leading-6">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="all posts"
|
||||
>
|
||||
All Posts →
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
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'
|
||||
@ -8,11 +8,7 @@ import { PageSeo } from '@/components/SEO'
|
||||
export default function Projects() {
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={`Projects - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description}
|
||||
url={`${siteMetadata.siteUrl}/projects`}
|
||||
/>
|
||||
<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">
|
||||
|
@ -15,11 +15,7 @@ export default function Tags({ tags }) {
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={`Tags - ${siteMetadata.author}`}
|
||||
description="Things I blog about"
|
||||
url={`${siteMetadata.siteUrl}/tags`}
|
||||
/>
|
||||
<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">
|
||||
|
@ -30,10 +30,10 @@ export async function getStaticProps({ params }) {
|
||||
)
|
||||
|
||||
// rss
|
||||
const rss = generateRss(filteredPosts, `tags/${params.tag}/index.xml`)
|
||||
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
|
||||
const rssPath = path.join(root, 'public', 'tags', params.tag)
|
||||
fs.mkdirSync(rssPath, { recursive: true })
|
||||
fs.writeFileSync(path.join(rssPath, 'index.xml'), rss)
|
||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
||||
|
||||
return { props: { posts: filteredPosts, tag: params.tag } }
|
||||
}
|
||||
@ -46,7 +46,6 @@ export default function Tag({ posts, tag }) {
|
||||
<PageSeo
|
||||
title={`${tag} - ${siteMetadata.title}`}
|
||||
description={`${tag} tags - ${siteMetadata.title}`}
|
||||
url={`${siteMetadata.siteUrl}/tags/${tag}`}
|
||||
/>
|
||||
<ListLayout posts={posts} title={title} />
|
||||
</>
|
||||
|
BIN
public/static/images/logo.png
Normal file
BIN
public/static/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
public/static/images/sparrowhawk-avatar.jpg
Normal file
BIN
public/static/images/sparrowhawk-avatar.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -1,7 +1,25 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const inquirer = require('inquirer')
|
||||
const dedent = require('dedent')
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
const getAuthors = () => {
|
||||
const authorPath = path.join(root, 'data', 'authors')
|
||||
const authorList = fs.readdirSync(authorPath).map((filename) => path.parse(filename).name)
|
||||
return authorList
|
||||
}
|
||||
|
||||
const getLayouts = () => {
|
||||
const layoutPath = path.join(root, 'layouts')
|
||||
const layoutList = fs
|
||||
.readdirSync(layoutPath)
|
||||
.map((filename) => path.parse(filename).name)
|
||||
.filter((file) => file.toLowerCase().includes('post'))
|
||||
return layoutList
|
||||
}
|
||||
|
||||
const genFrontMatter = (answers) => {
|
||||
let d = new Date()
|
||||
const date = [
|
||||
@ -9,18 +27,27 @@ const genFrontMatter = (answers) => {
|
||||
('0' + (d.getMonth() + 1)).slice(-2),
|
||||
('0' + d.getDate()).slice(-2),
|
||||
].join('-')
|
||||
var tagArray = answers.tags.split(',')
|
||||
const tagArray = answers.tags.split(',')
|
||||
tagArray.forEach((tag, index) => (tagArray[index] = tag.trim()))
|
||||
const tags = "'" + tagArray.join("','") + "'"
|
||||
const frontMatter = dedent`---
|
||||
const authorArray = answers.authors.length > 0 ? "'" + answers.authors.join("','") + "'" : ''
|
||||
|
||||
let frontMatter = dedent`---
|
||||
title: ${answers.title ? answers.title : 'Untitled'}
|
||||
date: '${date}'
|
||||
tags: [${answers.tags ? tags : ''}]
|
||||
draft: ${answers.draft === 'yes' ? true : false}
|
||||
summary: ${answers.summary ? answers.summary : ' '}
|
||||
images: []
|
||||
---
|
||||
layout: ${answers.layout}
|
||||
`
|
||||
|
||||
if (answers.authors.length > 0) {
|
||||
frontMatter = frontMatter + '\n' + `authors: [${authorArray}]`
|
||||
}
|
||||
|
||||
frontMatter = frontMatter + '\n---'
|
||||
|
||||
return frontMatter
|
||||
}
|
||||
|
||||
@ -37,6 +64,12 @@ inquirer
|
||||
type: 'list',
|
||||
choices: ['mdx', 'md'],
|
||||
},
|
||||
{
|
||||
name: 'authors',
|
||||
message: 'Choose authors:',
|
||||
type: 'checkbox',
|
||||
choices: getAuthors,
|
||||
},
|
||||
{
|
||||
name: 'summary',
|
||||
message: 'Enter post summary:',
|
||||
@ -53,6 +86,12 @@ inquirer
|
||||
message: 'Any Tags? Separate them with , or leave empty if no tags.',
|
||||
type: 'input',
|
||||
},
|
||||
{
|
||||
name: 'layout',
|
||||
message: 'Select layout',
|
||||
type: 'list',
|
||||
choices: getLayouts,
|
||||
},
|
||||
])
|
||||
.then((answers) => {
|
||||
// Remove special characters and replace space with -
|
||||
|
@ -26,7 +26,7 @@ const siteMetadata = require('../data/siteMetadata')
|
||||
.replace('.js', '')
|
||||
.replace('.mdx', '')
|
||||
.replace('.md', '')
|
||||
.replace('/index.xml', '')
|
||||
.replace('/feed.xml', '')
|
||||
const route = path === '/index' ? '' : path
|
||||
if (page === `pages/404.js` || page === `pages/blog/[...slug].js`) {
|
||||
return
|
||||
|
@ -20,7 +20,8 @@ module.exports = {
|
||||
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
blue: colors.sky,
|
||||
primary: colors.teal,
|
||||
gray: colors.trueGray,
|
||||
code: {
|
||||
green: '#b5f4a5',
|
||||
yellow: '#ffe484',
|
||||
@ -35,11 +36,11 @@ module.exports = {
|
||||
css: {
|
||||
color: theme('colors.gray.700'),
|
||||
a: {
|
||||
color: theme('colors.blue.500'),
|
||||
color: theme('colors.primary.500'),
|
||||
'&:hover': {
|
||||
color: theme('colors.blue.600'),
|
||||
color: theme('colors.primary.600'),
|
||||
},
|
||||
code: { color: theme('colors.blue.400') },
|
||||
code: { color: theme('colors.primary.400') },
|
||||
},
|
||||
h1: {
|
||||
fontWeight: '700',
|
||||
@ -92,11 +93,11 @@ module.exports = {
|
||||
css: {
|
||||
color: theme('colors.gray.300'),
|
||||
a: {
|
||||
color: theme('colors.blue.500'),
|
||||
color: theme('colors.primary.500'),
|
||||
'&:hover': {
|
||||
color: theme('colors.blue.400'),
|
||||
color: theme('colors.primary.400'),
|
||||
},
|
||||
code: { color: theme('colors.blue.400') },
|
||||
code: { color: theme('colors.primary.400') },
|
||||
},
|
||||
h1: {
|
||||
fontWeight: '700',
|
||||
|
Loading…
x
Reference in New Issue
Block a user