Upload starter template

This commit is contained in:
Timothy Lin 2021-01-09 17:50:45 +08:00
parent e332766d0f
commit 9a6f4efbb8
72 changed files with 11703 additions and 0 deletions

190
.Rhistory Normal file
View File

@ -0,0 +1,190 @@
library(blogdown)
serve_site()
installr::install.pandoc()
install.packages("htmlwidgets")
serve_site()
install.packages("plotly")
install.packages("plotly")
serve_site()
install.packages("htmlwidgets")
---
title: "Scraping SG's GDP data using SingStat's API"
author: "Timothy Lin"
date: '2017-05-31'
slug: scraping-sg-s-gdp-data-using-singstat-s-api
subtitle: ''
tags:
- Singapore
- SG Economy
- Web-Scraping
- R
- Dashboard
categories: []
---
```{r global_options, include=FALSE}
knitr::opts_chunk$set(warning=FALSE, message=FALSE, cache=TRUE)
```
I have been trying to catch-up on the latest release of Singapore's economic results. Unfortunately, the official press release or media reports are not very useful. They either contain too much irrelevant information or not enough details for my liking. Maybe I just like looking at the numbers and letting the figures speak for themselves. Hence, I decided to obtain the data from the official SingStat's Table Builder website. Turns out that they have an API available that provides data for selected data series including the [GDP figures](http://www.tablebuilder.singstat.gov.sg/publicfacing/initApiList.action).
This makes it easy to obtain the required data without having to navigate through the complicated tree structure of table builder. The best part about having an API is that I can just re-run the same set of codes and produce my own customise GDP report tailored to my liking! For the first technical post of this blog, I decided to do a write-up on using the API to extract GDP figures and generate some graphs in R. A more detailed set of codes is available on [github](https://github.com/timlrx/sg-econ-api).
I use the `httr` package to read in JSON data before manipulating the data in `dplyr` and `reshape2`. `ggplot2` is used for graphing and to add a little interactivity we can also play around with the `plotly` package. First, we have to read in the JSON data and convert it to string format. I decided to read in both the seasonally-adjusted VA figures and raw VA figures as they are used to calculate quarter-on-quarter and year-on-year growth respectively.
```{r setup}
library(httr)
library(reshape2)
library(dplyr)
library(ggplot2)
url_va_sa <- "http://www.tablebuilder.singstat.gov.sg/publicfacing/api/json/title/12411.json"
url_va <- "http://www.tablebuilder.singstat.gov.sg/publicfacing/api/json/title/12407.json"
raw.result <- GET(url=url_va_sa)
names(raw.result)
raw.content <- rawToChar(raw.result$content) # Convert raw bytes to string
raw.content <- content(raw.result)
# levels correspond to the statistics level of aggregation
# 1 - overall economy, 2 - goods / services, 3 - sector level
```
Browsing the raw.content variable, we can see that it is a messy list of lists.
```{r namescontent}
names(raw.content)
```
Level1, Level2, Level3 contain the data we are interested in at various levels of industry aggregation. Let's convert it to a more managable dataframe format and extract the required data. The do.call function along with lapply helps to unpack the list.
```{r unpacklist}
data <- do.call(what = "rbind", args = lapply(raw.content$Level1, as.data.frame))
data.sector <- do.call(what = "rbind", args = lapply(raw.content$Level3, as.data.frame))
data$value <- as.numeric(as.character(data$value))
data.sector$value <- as.numeric(as.character(data.sector$value))
data <- data[,c("quarter","value")]
data.sector <- data.sector[,c("quarter","value","level_3")]
names(data)[names(data) == 'quarter'] <- 'period'
names(data.sector)[names(data.sector) == 'quarter'] <- 'period'
names(data.sector)[names(data.sector) == 'level_3'] <- 'industry'
```
Now we can use the levels data and calculate the quarter-on-quarter seasonally-adjusted annualised growth rate (qoqsaa).
```{r qoqsaa}
data <- data %>% mutate(qoqsaa = ((value/lag(value))^4-1)*100)
data.sector <- data.sector %>% group_by(industry) %>%
mutate(qoqsaa = ((value/lag(value))^4-1)*100) %>% ungroup()
```
We can repeat the process for the non-seasonally-adjusted data and calculate the year-on-year growth rates.[Code omitted due to repetition.]
```{r nonsa_yoy, include=FALSE}
# Non-seasonally adjusted series for YoY calculations
raw.result <- GET(url=url_va)
names(raw.result)
raw.content <- rawToChar(raw.result$content) # Convert raw bytes to string
raw.content <- content(raw.result)
names(raw.content)
# levels correspond to the statistics level of aggregation
# 1 - overall economy, 2 - goods / services, 3 - sector level
# Converts the list of list into a dataframe
va <- do.call(what = "rbind", args = lapply(raw.content$Level1, as.data.frame))
va.sector <- do.call(what = "rbind", args = lapply(raw.content$Level3, as.data.frame))
va$value <- as.numeric(as.character(va$value))
va.sector$value <- as.numeric(as.character(va.sector$value))
va <- va[,c("quarter","value")]
va.sector <- va.sector[,c("quarter","value","level_3")]
names(va)[names(va) == 'quarter'] <- 'period'
names(va.sector)[names(va.sector) == 'quarter'] <- 'period'
names(va.sector)[names(va.sector) == 'level_3'] <- 'industry'
#calculate qoq SA annualised growth rate (qoqsaa)
va<-va %>% mutate(year = as.numeric(substr(period, 1, 4)), quarter = substr(period, 6, 7)) %>%
group_by(quarter) %>% mutate(yoy = (value/lag(value)-1)*100) %>% ungroup()
va.sector<-va.sector %>% mutate(year = as.numeric(substr(period, 1, 4)), quarter = substr(period, 6, 7)) %>%
group_by(quarter, industry) %>% mutate(yoy = (value/lag(value)-1)*100) %>%
ungroup()
# Combine va data from both series
data$industry<-as.factor("Overall Economy")
va$industry<-as.factor("Overall Economy")
va.sector.comb<-merge(rbind(va, va.sector), rbind(data, data.sector), by=c("industry","period"))
names(va.sector.comb)[names(va.sector.comb) == 'value.x'] <- 'va.sa'
names(va.sector.comb)[names(va.sector.comb) == 'value.y'] <- 'va'
va.sector.comb<-arrange(va.sector.comb, year, quarter)
va.sector.comb$industry.short<-va.sector.comb$industry
va.sector.comb$industry.short<-plyr::revalue(va.sector.comb$industry,
c("Overall Economy"="Overall",
"Manufacturing"="Mfg",
"Other Goods Industries"="Other Goods",
"Wholesale & Retail Trade"="Wholesale Retail",
"Transportation & Storage"="Tpt & Storage",
"Accommodation & Food Services"="Acc & Food",
"Finance & Insurance"="F&I",
"Information & Communications"="Infocomms",
"Business Services"="Business Ser",
"Other Services Industries"="Other Ser"))
```
Let's take a look at some plots of the data.
```{r qoqsaplot, fig.cap='QoQ GDP Trend'}
ggplot(data[(nrow(data)-16+1):nrow(data),], aes(period, qoqsaa, group=1)) +
geom_point() + geom_line() + ylab("QoQ SA (%)") + xlab("Period") + theme_classic() +
theme(axis.text.x=element_text(angle=45,hjust=1,vjust=1))
```
And here's a plot comparing qoq and yoy growth for all the industries:
```{r reshapeplot, echo=FALSE}
melt.va.sector.comb <- melt(va.sector.comb[(nrow(va.sector.comb)-11):nrow(va.sector.comb),],
measure.vars=c("yoy","qoqsaa"))
names(melt.va.sector.comb)[names(melt.va.sector.comb) == 'variable'] <- 'series'
melt.va.sector.comb$industry.short <- factor(melt.va.sector.comb$industry.short,
levels=rev(levels(melt.va.sector.comb$industry.short)))
sector.bar<-ggplot(melt.va.sector.comb,aes(industry.short, value)) +
geom_bar(aes(fill=series), stat="identity", position=position_dodge(width=1)) +
ylab("Growth (%)") + xlab("Industry") + theme(axis.text.x=element_text(angle=45,hjust=1,vjust=1),
axis.title.x=element_blank()) +
theme_classic() + coord_flip()
sector.bar
```
Wondering what the actual percentage change is? We can convert the graph into a more interactive one using a simple line of code. And since it is locally generated, it can be hosted on github as well.
```{r sector_bar_plotly, include=FALSE}
library(plotly)
ggplotly(sector.bar)
```
While the precision of our calculated data is satisfying, the scale of the graph is being skewed by one particular sector. The other goods sector (Agriculture and Fishing, Mining and Quarrying) contracted 16.38% yoy (yikes!).^[I manually verified the figures with the official ones due to this huge number.] The other goods sector is normally not given any coverage as it is a very small component of GDP (just 30m). Could the decline be mostly due to noisy fluctuations?
```{r other_goods_plot}
other.goods.data <- va.sector.comb %>% filter(year>=2012 & industry=="Other Goods Industries") %>% select(period, yoy, qoqsaa) %>% melt(measure.vars=c("yoy","qoqsaa"))
other.goods.plot<-ggplot(other.goods.data, aes(period, value, group=variable, color=variable)) + geom_point() + geom_line() + ylab("QoQ SA (%)") + xlab("Period") + theme_classic() + theme(axis.text.x=element_text(angle=45,hjust=1,vjust=1))
```
Not suprisingly there is a downward trend in the sector, but the recent quarter's dip is the sharpest in all 4 years that I have plotted. The drastic decline in quarter-on-quarter VA figures for the finance and insurance industry is of potentially greater concern. Let's take a closer look by plotting an extended time series.
```{r fi, echo=FALSE, fig.cap=c('yoy plot', 'qoq plot')}
fi.data <- va.sector.comb %>% filter(year>=2010 & industry=="Finance & Insurance") %>%
select(period, yoy, qoqsaa, quarter) %>% melt(measure.vars=c("yoy","qoqsaa"))
## Looks like there is some seasonality in the data
ggplot(fi.data[fi.data$variable=="yoy",], aes(period, value,group=1)) +
geom_point() + geom_line() + ylab("YoY (%)") + xlab("Period") + theme_classic() +
theme(axis.text.x=element_text(angle=45,hjust=1,vjust=1))
library(ggrepel)
ggplot(fi.data[fi.data$variable=="qoqsaa",], aes(period, value,group=1)) +
geom_point() + geom_line() + geom_text_repel(aes(label=quarter)) + ylab("QoQ SA (%)") + xlab("Period") + theme_classic() +
theme(axis.text.x=element_text(angle=45,hjust=1,vjust=1))
```
Interestingly, from the yoy data it appears that the contribution of finance and insurance to Singapore's GDP growth is plateauing off. The qoq data is even more suprising showing a strong cyclical pattern since 2014. This is of particular concern as the F&I figures are not seasonally-adjusted. DOS believes that the sector does not exhibit seasonality, but looking at the recent data, a case could be made that the fundamentals of the sector has changed substantially over the past 3 years and one should definitely treat the qoq figures extremely cautiously.
serve_site()
build_site()
serve_site()
build_site()
build_site()
build_site()
build_site()
library(httr)
library(reshape2)
library(dplyr)
library(ggplot2)
url_va_sa <- "http://www.tablebuilder.singstat.gov.sg/publicfacing/api/json/title/12411.json"
url_va <- "http://www.tablebuilder.singstat.gov.sg/publicfacing/api/json/title/12407.json"
raw.result <- GET(url=url_va_sa)
names(raw.result)
raw.content <- rawToChar(raw.result$content) # Convert raw bytes to string
raw.content <- content(raw.result)
names(raw.content)
serve_site()
install.packages("cowplot")
serve_site()
install.packages("DT")
serve_site()
serve_site()
install.packages("kableExtra")
install.packages("kableExtra")
serve_site()
serve_site()
serve_site()
build_dir("static/dashboard")
build_dir("static/dashboard")
ls
library(blogdown)
build_dir("./static/dashboard")
build_dir("./static/dashboard")

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
public/sitemap.xml
.vercel
# production
/build
# misc
.DS_Store
# debug
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

43
components/BlogSeo.js Normal file
View File

@ -0,0 +1,43 @@
import { NextSeo, ArticleJsonLd } from 'next-seo'
import siteMetadata from '@/data/siteMetadata'
const BlogSeo = ({ title, summary, date, url, image }) => {
const publishedAt = new Date(date).toISOString()
const featuredImage = {
url: `${siteMetadata.url}${image}`,
alt: title,
}
return (
<>
<NextSeo
title={`${title} ${siteMetadata.title}`}
description={summary}
canonical={url}
openGraph={{
type: 'article',
article: {
publishedTime: publishedAt,
},
url,
title,
description: summary,
images: [featuredImage],
}}
/>
<ArticleJsonLd
authorName={siteMetadata.author}
dateModified={publishedAt}
datePublished={publishedAt}
description={summary}
images={[featuredImage]}
publisherLogo="/static/favicons/android-chrome-192x192.png"
publisherName={siteMetadata.author}
title={title}
url={url}
/>
</>
)
}
export default BlogSeo

28
components/Footer.js Normal file
View File

@ -0,0 +1,28 @@
import Link from './Link'
import siteMetadata from '@/data/siteMetadata'
import SocialIcon from '@/components/social-icons'
export default function Footer() {
return (
<footer className="flex flex-col items-center mt-16">
<div className="flex space-x-4 mb-3">
<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 space-x-2 mb-2 text-sm text-gray-500 dark:text-gray-400">
<div>{siteMetadata.author}</div>
<div>{``}</div>
<div>{`© ${new Date().getFullYear()}`}</div>
<div>{``}</div>
<Link href="/">{siteMetadata.title}</Link>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mb-8">
<Link href="/">Tailwind Nextjs Theme</Link>
</div>
</footer>
)
}

7
components/Image.js Normal file
View File

@ -0,0 +1,7 @@
import Img from 'react-optimized-image'
const Image = ({ src, alt }) => {
return <Img src={src} alt={alt} />
}
export default Image

View File

@ -0,0 +1,54 @@
import siteMetadata from '@/data/siteMetadata'
import headerNavLinks from '@/data/headerNavLinks'
import Logo from '@/data/logo.svg'
import Link from './Link'
import SectionContainer from './SectionContainer'
import Footer from './Footer'
import MobileNav from './MobileNav'
import ThemeSwitch from './ThemeSwitch'
const LayoutWrapper = ({ children }) => {
return (
<SectionContainer>
<div className="flex flex-col h-screen justify-between">
<header className="flex justify-between items-center py-10">
<div>
<Link href="/" aria-label="Tailwind CSS Blog">
<div className="flex justify-between items-center">
<div className="mr-3">
<Logo />
</div>
{typeof siteMetadata.headerTitle === 'string' ? (
<div className="hidden sm:block h-6 text-2xl font-semibold">
{siteMetadata.headerTitle}
</div>
) : (
siteMetadata.headerTitle
)}
</div>
</Link>
</div>
<div className="flex items-center text-base leading-5">
<div className="hidden sm:block">
{headerNavLinks.map((link) => (
<Link
key={link.title}
href={link.href}
className="p-1 sm:p-4 font-medium text-gray-900 dark:text-gray-100"
>
{link.title}
</Link>
))}
</div>
<ThemeSwitch />
<MobileNav />
</div>
</header>
<main className="mb-auto">{children}</main>
<Footer />
</div>
</SectionContainer>
)
}
export default LayoutWrapper

22
components/Link.js Normal file
View File

@ -0,0 +1,22 @@
import Link from 'next/link'
const CustomLink = ({ href, ...rest }) => {
const isInternalLink = href && href.startsWith('/')
const isAnchorLink = href && href.startsWith('#')
if (isInternalLink) {
return (
<Link href={href}>
<a {...rest} />
</Link>
)
}
if (isAnchorLink) {
return <a {...rest} />
}
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
}
export default CustomLink

View File

@ -0,0 +1,9 @@
import Image from 'next/image'
import CustomLink from './Link'
const MDXComponents = {
Image,
a: CustomLink,
}
export default MDXComponents

78
components/MobileNav.js Normal file
View File

@ -0,0 +1,78 @@
import { useState } from 'react'
import Link from './Link'
import headerNavLinks from '@/data/headerNavLinks'
const MobileNav = () => {
const [navShow, setNavShow] = useState(false)
const onToggleNav = () => {
setNavShow((status) => {
if (status) {
document.body.style.overflow = 'auto'
} else {
// Prevent scrolling
document.body.style.overflow = 'hidden'
}
return !status
})
}
return (
<div className="sm:hidden">
<button
type="button"
className="rounded ml-1 mr-1 h-8 w-8 focus:outline-none"
aria-label="Toggle Menu"
aria-hidden={!navShow}
onClick={onToggleNav}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100"
>
{navShow ? (
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
) : (
<path
fillRule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clipRule="evenodd"
/>
)}
</svg>
</button>
<div
className={`fixed w-full h-full top-24 right-0 bg-gray-200 dark:bg-gray-800 opacity-95 z-10 transform ease-in-out duration-300 ${
navShow ? 'translate-x-0' : 'translate-x-full'
}`}
>
<button
type="button"
className="w-full h-full fixed cursor-auto focus:outline-none"
onClick={onToggleNav}
></button>
<nav className="h-full mt-8 fixed">
{headerNavLinks.map((link) => (
<div key={link.title} className="py-4 px-12">
<Link
href={link.href}
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
onClick={onToggleNav}
>
{link.title}
</Link>
</div>
))}
</nav>
</div>
</div>
)
}
export default MobileNav

7
components/PageTitle.js Normal file
View File

@ -0,0 +1,7 @@
export default function PageTitle({ children }) {
return (
<h1 className="text-3xl leading-9 font-extrabold text-gray-900 dark:text-gray-100 tracking-tight sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
{children}
</h1>
)
}

29
components/SEO.js Normal file
View File

@ -0,0 +1,29 @@
import siteMetadata from '@/data/siteMetadata'
const SEO = {
title: siteMetadata.title,
description: siteMetadata.description,
canonical: siteMetadata.siteUrl,
openGraph: {
type: 'website',
locale: siteMetadata.language,
url: siteMetadata.siteUrl,
title: siteMetadata.title,
description: siteMetadata.description,
images: [
{
url: siteMetadata.image,
alt: siteMetadata.title,
width: 1280,
height: 720,
},
],
},
twitter: {
handle: siteMetadata.twitter,
site: siteMetadata.twitter,
cardType: 'summary_large_image',
},
}
export default SEO

View File

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

14
components/Tag.js Normal file
View File

@ -0,0 +1,14 @@
import Link from 'next/link'
import kebabCase from 'just-kebab-case'
const Tag = ({ text }) => {
return (
<Link href={`/tags/${kebabCase(text)}`}>
<a className="uppercase text-sm font-medium text-blue-500 hover:text-blue-600 dark:hover:text-blue-400">
{text.split(' ').join('-')}
</a>
</Link>
)
}
export default Tag

33
components/ThemeSwitch.js Normal file
View File

@ -0,0 +1,33 @@
import { useTheme } from 'next-themes'
const ThemeSwitch = () => {
const { theme, setTheme } = useTheme()
return (
<button
aria-label="Toggle Dark Mode"
type="button"
className="rounded ml-1 mr-1 sm:ml-4 p-1 h-8 w-8"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100"
>
{theme === 'dark' ? (
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
) : (
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
)}
</svg>
</button>
)
}
export default ThemeSwitch

View File

@ -0,0 +1 @@
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Facebook icon</title><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@ -0,0 +1,37 @@
import Mail from './mail.svg'
import Github from './github.svg'
import Facebook from './facebook.svg'
import Youtube from './youtube.svg'
import Linkedin from './linkedin.svg'
import Twitter from './twitter.svg'
// Icons taken from: https://simpleicons.org/
const components = {
mail: Mail,
github: Github,
facebook: Facebook,
youtube: Youtube,
linkedin: Linkedin,
twitter: Twitter,
}
const SocialIcon = ({ kind, href, size = 8 }) => {
const SocialSvg = components[kind]
return (
<a
className="text-sm text-gray-500 hover:text-gray-600 transition"
target="_blank"
rel="noopener noreferrer"
href={href}
>
<span className="sr-only">{kind}</span>
<SocialSvg
className={`fill-current text-gray-700 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 h-${size} w-${size}`}
/>
</a>
)
}
export default SocialIcon

View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LinkedIn icon</title><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1 @@
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Twitter icon</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>

After

Width:  |  Height:  |  Size: 607 B

View File

@ -0,0 +1 @@
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>YouTube icon</title><path d="M23.499 6.203a3.008 3.008 0 00-2.089-2.089c-1.87-.501-9.4-.501-9.4-.501s-7.509-.01-9.399.501a3.008 3.008 0 00-2.088 2.09A31.258 31.26 0 000 12.01a31.258 31.26 0 00.523 5.785 3.008 3.008 0 002.088 2.089c1.869.502 9.4.502 9.4.502s7.508 0 9.399-.502a3.008 3.008 0 002.089-2.09 31.258 31.26 0 00.5-5.784 31.258 31.26 0 00-.5-5.808zm-13.891 9.4V8.407l6.266 3.604z"/></svg>

After

Width:  |  Height:  |  Size: 474 B

19
css/tailwind.css Normal file
View File

@ -0,0 +1,19 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.remark-code-title {
@apply text-gray-200 px-5 py-3 rounded-t bg-gray-700 text-sm font-mono font-bold;
}
.remark-code-title + pre {
@apply mt-0 rounded-t-none;
}
.task-list-item:before {
@apply hidden;
}
html {
scroll-behavior: smooth;
}

38
data/blog/code-sample.md Normal file
View File

@ -0,0 +1,38 @@
---
title: Sample .md file
date: '2016-03-08'
tags: ['markdown', 'code', 'features']
draft: false
summary: Example of a markdown file with code blocks and syntax highlighting
---
A sample post with markdown.
## Inline Highlighting
Sample of inline highlighting `sum = parseInt(num1) + parseInt(num2)`
## Code Blocks
Some Javascript code
```javascript
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
```
Some Python code 🐍
```python
def fib():
a, b = 0, 1
while True: # First iteration:
yield a # yield 0 to start with and then
a, b = b, a + b # a will now be 1, and b will also be 1, (0 + 1)
for index, fibonacci_number in zip(range(10), fib()):
print('{i:3}: {f:3}'.format(i=index, f=fibonacci_number))
```

View File

@ -0,0 +1,141 @@
---
title: Deriving the OLS Estimator
date: '2019-11-16'
tags: ['next js', 'math', 'ols']
draft: false
summary: 'How to derive the OLS Estimator with matrix notation and a tour of math typesetting using markdown with the help of KaTeX.'
---
# Introduction
Parsing and display of math equations is included in this blog template. Parsing of math is enabled by `remark-math` and `rehype-katex`.
KaTeX and its associated font is included in `_document.js` so feel free to use it in any pages.
^[For the full list of supported TeX functions, check out the [KaTeX documentation](https://katex.org/docs/supported.html)]
Inline math symbols can be included by enclosing the term between the `$` symbol.
Math code blocks is denoted by `$$`.
The dollar signal displays without issue since only text without space and between two `$` signs are considered as math symbols.[^2]
Inline or manually enumerated footnotes are also supported. Click on the links above to see them in action.
[^2]: Here's $10 and $20.
# Deriving the OLS Estimator
Using matrix notation, let $n$ denote the number of observations and $k$ denote the number of regressors.
The vector of outcome variables $\mathbf{Y}$ is a $n \times 1$ matrix,
```tex
\mathbf{Y} = \left[\begin{array}
{c}
y_1 \\
. \\
. \\
. \\
y_n
\end{array}\right]
```
$$
\mathbf{Y} = \left[\begin{array}
{c}
y_1 \\
. \\
. \\
. \\
y_n
\end{array}\right]
$$
The matrix of regressors $\mathbf{X}$ is a $n \times k$ matrix (or each row is a $k \times 1$ vector),
```latex
\mathbf{X} = \left[\begin{array}
{ccccc}
x_{11} & . & . & . & x_{1k} \\
. & . & . & . & . \\
. & . & . & . & . \\
. & . & . & . & . \\
x_{n1} & . & . & . & x_{nn}
\end{array}\right] =
\left[\begin{array}
{c}
\mathbf{x}'_1 \\
. \\
. \\
. \\
\mathbf{x}'_n
\end{array}\right]
```
$$
\mathbf{X} = \left[\begin{array}
{ccccc}
x_{11} & . & . & . & x_{1k} \\
. & . & . & . & . \\
. & . & . & . & . \\
. & . & . & . & . \\
x_{n1} & . & . & . & x_{nn}
\end{array}\right] =
\left[\begin{array}
{c}
\mathbf{x}'_1 \\
. \\
. \\
. \\
\mathbf{x}'_n
\end{array}\right]
$$
The vector of error terms $\mathbf{U}$ is also a $n \times 1$ matrix.
At times it might be easier to use vector notation. For consistency I will use the bold small x to denote a vector and capital letters to denote a matrix. Single observations are denoted by the subscript.
## Least Squares
**Start**:
$$y_i = \mathbf{x}'_i \beta + u_i$$
**Assumptions**:
1. Linearity (given above)
2. $E(\mathbf{U}|\mathbf{X}) = 0$ (conditional independence)
3. rank($\mathbf{X}$) = $k$ (no multi-collinearity i.e. full rank)
4. $Var(\mathbf{U}|\mathbf{X}) = \sigma^2 I_n$ (Homoskedascity)
**Aim**:
Find $\beta$ that minimises sum of squared errors:
$$
Q = \sum_{i=1}^{n}{u_i^2} = \sum_{i=1}^{n}{(y_i - \mathbf{x}'_i\beta)^2} = (Y-X\beta)'(Y-X\beta)
$$
**Solution**:
Hints: $Q$ is a $1 \times 1$ scalar, by symmetry $\frac{\partial b'Ab}{\partial b} = 2Ab$.
Take matrix derivative w.r.t $\beta$:
```tex
\begin{aligned}
\min Q & = \min_{\beta} \mathbf{Y}'\mathbf{Y} - 2\beta'\mathbf{X}'\mathbf{Y} +
\beta'\mathbf{X}'\mathbf{X}\beta \\
& = \min_{\beta} - 2\beta'\mathbf{X}'\mathbf{Y} + \beta'\mathbf{X}'\mathbf{X}\beta \\
\text{[FOC]}~~~0 & = - 2\mathbf{X}'\mathbf{Y} + 2\mathbf{X}'\mathbf{X}\hat{\beta} \\
\hat{\beta} & = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{Y} \\
& = (\sum^{n} \mathbf{x}_i \mathbf{x}'_i)^{-1} \sum^{n} \mathbf{x}_i y_i
\end{aligned}
```
$$
\begin{aligned}
\min Q & = \min_{\beta} \mathbf{Y}'\mathbf{Y} - 2\beta'\mathbf{X}'\mathbf{Y} +
\beta'\mathbf{X}'\mathbf{X}\beta \\
& = \min_{\beta} - 2\beta'\mathbf{X}'\mathbf{Y} + \beta'\mathbf{X}'\mathbf{X}\beta \\
\text{[FOC]}~~~0 & = - 2\mathbf{X}'\mathbf{Y} + 2\mathbf{X}'\mathbf{X}\hat{\beta} \\
\hat{\beta} & = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{Y} \\
& = (\sum^{n} \mathbf{x}_i \mathbf{x}'_i)^{-1} \sum^{n} \mathbf{x}_i y_i
\end{aligned}
$$

View File

@ -0,0 +1,185 @@
---
title: 'Markdown Guide'
date: '2019-10-11'
tags: ['github', 'guide']
draft: false
summary: 'Markdown cheatsheet for all your blogging needs - headers, lists, images, tables and more! An illustrated guide based on Github Flavored Markdown.'
---
# Introduction
Markdown and Mdx parsing is supported via `unified`, and other remark and rehype packages. `next-mdx-remote` allows us to parse `.mdx` and `.md` files in a more flexible manner without touching webpack.
Github flavored markdown is used. `mdx-prism` provides syntax highlighting capabilities for code blocks. Here's a demo of how everything looks.
The following markdown cheatsheet is adapted from: https://guides.github.com/features/mastering-markdown/
# What is Markdown?
Markdown is a way to style text on the web. You control the display of the document; formatting words as bold or italic, adding images, and creating lists are just a few of the things we can do with Markdown. Mostly, Markdown is just regular text with a few non-alphabetic characters thrown in, like `#` or `*`.
# Syntax guide
Heres an overview of Markdown syntax that you can use anywhere on GitHub.com or in your own text files.
## Headers
```
# This is a h1 tag
## This is a h2 tag
#### This is a h4 tag
```
# This is a h1 tag
## This is a h2 tag
#### This is a h4 tag
## Emphasis
```
_This text will be italic_
**This text will be bold**
_You **can** combine them_
```
_This text will be italic_
**This text will be bold**
_You **can** combine them_
## Lists
### Unordered
```
- Item 1
- Item 2
- Item 2a
- Item 2b
```
- Item 1
- Item 2
- Item 2a
- Item 2b
### Ordered
```
1. Item 1
1. Item 2
1. Item 3
1. Item 3a
1. Item 3b
```
1. Item 1
1. Item 2
1. Item 3
1. Item 3a
1. Item 3b
## Images
```
![GitHub Logo](https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png)
Format: ![Alt Text](url)
```
![GitHub Logo](https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png)
## Links
```
http://github.com - automatic!
[GitHub](http://github.com)
```
http://github.com - automatic!
[GitHub](http://github.com)
## Blockquotes
```
As Kanye West said:
> We're living the future so
> the present is our past.
```
As Kanye West said:
> We're living the future so
> the present is our past.
## Inline code
```
I think you should use an
`<addr>` element here instead.
```
I think you should use an
`<addr>` element here instead.
## Syntax highlighting
Heres an example of how you can use syntax highlighting with [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/):
````
```js:fancyAlert.js
function fancyAlert(arg) {
if (arg) {
$.facebox({ div: '#foo' })
}
}
````
And here's how it looks - nicely colored with styled code titles!
```js:fancyAlert.js
function fancyAlert(arg) {
if (arg) {
$.facebox({ div: '#foo' })
}
}
```
## Task Lists
```
- [x] list syntax required (any unordered or ordered list supported)
- [x] this is a complete item
- [ ] this is an incomplete item
```
- [x] list syntax required (any unordered or ordered list supported)
- [x] this is a complete item
- [ ] this is an incomplete item
## Tables
You can create tables by assembling a list of words and dividing them with hyphens `-` (for the first row), and then separating each column with a pipe `|`:
```
| First Header | Second Header |
| --------------------------- | ---------------------------- |
| Content from cell 1 | Content from cell 2 |
| Content in the first column | Content in the second column |
```
| First Header | Second Header |
| --------------------------- | ---------------------------- |
| Content from cell 1 | Content from cell 2 |
| Content in the first column | Content in the second column |
## Strikethrough
Any word wrapped with two tildes (like `~~this~~`) will appear ~~crossed out~~.

View File

@ -0,0 +1,79 @@
---
title: Images in Next.js
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.'
---
# Introduction
The tailwind starter blog has out of the box support for [Next.js's built-in image component](https://nextjs.org/docs/api-reference/next/image) and automatically swaps out default image tags in markdown or mdx documents to use the Image component provided.
# Usage
To use in a new page route / javascript file, simply import the image component and call it e.g.
```js
import Image from 'next/image'
function Home() {
return (
<>
<h1>My Homepage</h1>
<Image src="/me.png" alt="Picture of the author" width={500} height={500} />
<p>Welcome to my homepage!</p>
</>
)
}
export default Home
```
For a markdown file, the default image tag can be used and the default `img` tag gets replaced by the `Image` component in the build process.
Assuming we have a file called `ocean.jpg` in `data/img/ocean.jpg`, the following line of code would generate the optimized image.
```
![ocean](/static/images/ocean.jpg)
```
Alternatively, since we are using mdx, we can just use the image component directly! Note, that you would have to provide a fixed width and height. The `img` tag method parses the dimension automatically.
```js
<Image alt="ocean" src="/static/images/ocean.jpg" width={256} height={128} />
```
_Note_: If you try to save the image, it is in webp format, if your browser supports it!
![ocean](/static/images/ocean.jpeg)
<p>
Photo by{' '}
<a href="https://unsplash.com/@yucar?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
YUCAR FotoGrafik
</a>{' '}
on{' '}
<a href="https://unsplash.com/s/photos/sea?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Unsplash
</a>
</p>
# Benefits
- Smaller image size with Webp (~30% smaller than jpeg)
- Responsive images - the correct image size is served based on the user's viewport
- Lazy loading - images load as they are scrolled to the viewport
- Avoids [Cumulative Layout Shift](https://web.dev/cls/)
- Optimization on demand instead of build-time - no increase in build time!
# Limitations
- Due to the reliance of `next/image`, unless you are using an external image CDN like Cloudinary or Imgix, it is practically required to use Vercel for hosting. This is because the component acts like a serverless function that calls a highly optimized image CDN.
If you do not want to be tied to Vercel, you can remove `imgToJsx` in `remarkPlugins` in `lib/mdx.js`. This would avoid substituting the default `img` tag.
Alternatively, one could wait for image optimization at build time to be supported. A different library, [next-optimized-images](https://github.com/cyrilwanner/next-optimized-images) does that, although it requires transforming the images through webpack which is not done here.
- Images from external links are not passed through `next/image`
- All images have to be stored in the `public` folder e.g `/static/images/ocean.jpeg`

View File

@ -0,0 +1,100 @@
---
title: O Canada
date: '2017-07-15'
tags: ['holiday', 'canada', 'images']
draft: false
summary: The scenic lands of Canada featuring maple leaves, snow-capped mountains, turquoise lakes and Toronto. Take in the sights in this photo gallery exhibition and see how easy it is to replicate with some MDX magic and tailwind classes.
---
# O Canada
The scenic lands of Canada featuring maple leaves, snow-capped mountains, turquoise lakes and Toronto. Take in the sights in this photo gallery exhibition and see how easy it is to replicate with some MDX magic and tailwind classes.
Features images served using `next/image` component. The locally stored images are located in a folder with the following path: `/static/images/canada/[filename].jpg`
Since we are using mdx, we can create a simple responsive flexbox grid to display our images with a few tailwind css classes.
---
# Gallery
<div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2">
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Maple" src="/static/images/canada/maple.jpg" width={640} height={427} />
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Lake" src="/static/images/canada/lake.jpg" width={640} height={427} />
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Mountains" src="/static/images/canada/mountains.jpg" width={640} height={427} />
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Toronto" src="/static/images/canada/toronto.jpg" width={640} height={427} />
</div>
</div>
# Implementation
```js
<div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2">
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Maple" src="/static/images/canada/maple.jpg" width={640} height={427} />
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Lake" src="/static/images/canada/lake.jpg" width={640} height={427} />
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Mountains" src="/static/images/canada/mountains.jpg" width={640} height={427} />
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
<Image alt="Toronto" src="/static/images/canada/toronto.jpg" width={640} height={427} />
</div>
</div>
```
_Note_: Currently, one has to use the `Image` component instead of the markdown syntax between jsx. Thankfully, it's one of the default components passed to the MDX Provider and can be used directly.
When MDX v2 is ready, one could potentially interleave markdown in jsx directly! Follow [MDX v2 issues](https://github.com/mdx-js/mdx/issues/1041) for updates.
### Photo Credits
<div>
Maple photo by{' '}
<a href="https://unsplash.com/@i_am_g?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Guillaume Jaillet
</a>{' '}
on{' '}
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Unsplash
</a>
</div>
<div>
Mountains photo by{' '}
<a href="https://unsplash.com/@john_artifexfilms?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
John Lee
</a>{' '}
on{' '}
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Unsplash
</a>
</div>
<div>
Lake photo by{' '}
<a href="https://unsplash.com/@tjholowaychuk?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Tj Holowaychuk
</a>{' '}
on{' '}
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Unsplash
</a>
</div>
<div>
Toronto photo by{' '}
<a href="https://unsplash.com/@matthewhenry?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Matthew Henry
</a>{' '}
on{' '}
<a href="https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">
Unsplash
</a>
</div>

View File

@ -0,0 +1,238 @@
---
title: 'The Time Machine'
date: '2018-08-15'
tags: ['writings', 'book', 'reflection']
draft: false
summary: 'The Time Traveller (for so it will be convenient to speak of him) was
expounding a recondite matter to us. His pale grey eyes shone and
twinkled, and his usually pale face was flushed and animated...'
---
# The Time Machine by H. G. Wells
_Title_: The Time Machine
_Author_: H. G. Wells
_Subject_: Science Fiction
_Language_: English
_Source_: [Project Gutenberg](https://www.gutenberg.org/ebooks/35)
## Introduction
The Time Traveller (for so it will be convenient to speak of him) was
expounding a recondite matter to us. His pale grey eyes shone and
twinkled, and his usually pale face was flushed and animated. The fire
burnt brightly, and the soft radiance of the incandescent lights in the
lilies of silver caught the bubbles that flashed and passed in our
glasses. Our chairs, being his patents, embraced and caressed us rather
than submitted to be sat upon, and there was that luxurious
after-dinner atmosphere, when thought runs gracefully free of the
trammels of precision. And he put it to us in this way—marking the
points with a lean forefinger—as we sat and lazily admired his
earnestness over this new paradox (as we thought it) and his fecundity.
“You must follow me carefully. I shall have to controvert one or two
ideas that are almost universally accepted. The geometry, for instance,
they taught you at school is founded on a misconception.”
“Is not that rather a large thing to expect us to begin upon?” said
Filby, an argumentative person with red hair.
“I do not mean to ask you to accept anything without reasonable ground
for it. You will soon admit as much as I need from you. You know of
course that a mathematical line, a line of thickness _nil_, has no real
existence. They taught you that? Neither has a mathematical plane.
These things are mere abstractions.”
“That is all right,” said the Psychologist.
“Nor, having only length, breadth, and thickness, can a cube have a
real existence.”
“There I object,” said Filby. “Of course a solid body may exist. All
real things—”
“So most people think. But wait a moment. Can an _instantaneous_ cube
exist?”
“Dont follow you,” said Filby.
“Can a cube that does not last for any time at all, have a real
existence?”
Filby became pensive. “Clearly,” the Time Traveller proceeded, “any
real body must have extension in _four_ directions: it must have
Length, Breadth, Thickness, and—Duration. But through a natural
infirmity of the flesh, which I will explain to you in a moment, we
incline to overlook this fact. There are really four dimensions, three
which we call the three planes of Space, and a fourth, Time. There is,
however, a tendency to draw an unreal distinction between the former
three dimensions and the latter, because it happens that our
consciousness moves intermittently in one direction along the latter
from the beginning to the end of our lives.”
“That,” said a very young man, making spasmodic efforts to relight his
cigar over the lamp; “that . . . very clear indeed.”
“Now, it is very remarkable that this is so extensively overlooked,”
continued the Time Traveller, with a slight accession of cheerfulness.
“Really this is what is meant by the Fourth Dimension, though some
people who talk about the Fourth Dimension do not know they mean it. It
is only another way of looking at Time. _There is no difference between
Time and any of the three dimensions of Space except that our
consciousness moves along it_. But some foolish people have got hold of
the wrong side of that idea. You have all heard what they have to say
about this Fourth Dimension?”
“_I_ have not,” said the Provincial Mayor.
“It is simply this. That Space, as our mathematicians have it, is
spoken of as having three dimensions, which one may call Length,
Breadth, and Thickness, and is always definable by reference to three
planes, each at right angles to the others. But some philosophical
people have been asking why _three_ dimensions particularly—why not
another direction at right angles to the other three?—and have even
tried to construct a Four-Dimensional geometry. Professor Simon Newcomb
was expounding this to the New York Mathematical Society only a month
or so ago. You know how on a flat surface, which has only two
dimensions, we can represent a figure of a three-dimensional solid, and
similarly they think that by models of three dimensions they could
represent one of four—if they could master the perspective of the
thing. See?”
“I think so,” murmured the Provincial Mayor; and, knitting his brows,
he lapsed into an introspective state, his lips moving as one who
repeats mystic words. “Yes, I think I see it now,” he said after some
time, brightening in a quite transitory manner.
“Well, I do not mind telling you I have been at work upon this geometry
of Four Dimensions for some time. Some of my results are curious. For
instance, here is a portrait of a man at eight years old, another at
fifteen, another at seventeen, another at twenty-three, and so on. All
these are evidently sections, as it were, Three-Dimensional
representations of his Four-Dimensioned being, which is a fixed and
unalterable thing.
“Scientific people,” proceeded the Time Traveller, after the pause
required for the proper assimilation of this, “know very well that Time
is only a kind of Space. Here is a popular scientific diagram, a
weather record. This line I trace with my finger shows the movement of
the barometer. Yesterday it was so high, yesterday night it fell, then
this morning it rose again, and so gently upward to here. Surely the
mercury did not trace this line in any of the dimensions of Space
generally recognised? But certainly it traced such a line, and that
line, therefore, we must conclude, was along the Time-Dimension.”
“But,” said the Medical Man, staring hard at a coal in the fire, “if
Time is really only a fourth dimension of Space, why is it, and why has
it always been, regarded as something different? And why cannot we move
in Time as we move about in the other dimensions of Space?”
The Time Traveller smiled. “Are you so sure we can move freely in
Space? Right and left we can go, backward and forward freely enough,
and men always have done so. I admit we move freely in two dimensions.
But how about up and down? Gravitation limits us there.”
“Not exactly,” said the Medical Man. “There are balloons.”
“But before the balloons, save for spasmodic jumping and the
inequalities of the surface, man had no freedom of vertical movement.”
“Still they could move a little up and down,” said the Medical Man.
“Easier, far easier down than up.”
“And you cannot move at all in Time, you cannot get away from the
present moment.”
“My dear sir, that is just where you are wrong. That is just where the
whole world has gone wrong. We are always getting away from the present
moment. Our mental existences, which are immaterial and have no
dimensions, are passing along the Time-Dimension with a uniform
velocity from the cradle to the grave. Just as we should travel _down_
if we began our existence fifty miles above the earths surface.”
“But the great difficulty is this,” interrupted the Psychologist. You
_can_ move about in all directions of Space, but you cannot move about
in Time.”
“That is the germ of my great discovery. But you are wrong to say that
we cannot move about in Time. For instance, if I am recalling an
incident very vividly I go back to the instant of its occurrence: I
become absent-minded, as you say. I jump back for a moment. Of course
we have no means of staying back for any length of Time, any more than
a savage or an animal has of staying six feet above the ground. But a
civilised man is better off than the savage in this respect. He can go
up against gravitation in a balloon, and why should he not hope that
ultimately he may be able to stop or accelerate his drift along the
Time-Dimension, or even turn about and travel the other way?”
“Oh, _this_,” began Filby, “is all—”
“Why not?” said the Time Traveller.
“Its against reason,” said Filby.
“What reason?” said the Time Traveller.
“You can show black is white by argument,” said Filby, “but you will
never convince me.”
“Possibly not,” said the Time Traveller. “But now you begin to see the
object of my investigations into the geometry of Four Dimensions. Long
ago I had a vague inkling of a machine—”
“To travel through Time!” exclaimed the Very Young Man.
“That shall travel indifferently in any direction of Space and Time, as
the driver determines.”
Filby contented himself with laughter.
“But I have experimental verification,” said the Time Traveller.
“It would be remarkably convenient for the historian,” the Psychologist
suggested. “One might travel back and verify the accepted account of
the Battle of Hastings, for instance!”
“Dont you think you would attract attention?” said the Medical Man.
“Our ancestors had no great tolerance for anachronisms.”
“One might get ones Greek from the very lips of Homer and Plato,” the
Very Young Man thought.
“In which case they would certainly plough you for the Little-go. The
German scholars have improved Greek so much.”
“Then there is the future,” said the Very Young Man. “Just think! One
might invest all ones money, leave it to accumulate at interest, and
hurry on ahead!”
“To discover a society,” said I, “erected on a strictly communistic
basis.”
“Of all the wild extravagant theories!” began the Psychologist.
“Yes, so it seemed to me, and so I never talked of it until—”
“Experimental verification!” cried I. “You are going to verify _that_?”
“The experiment!” cried Filby, who was getting brain-weary.
“Lets see your experiment anyhow,” said the Psychologist, “though its
all humbug, you know.”
The Time Traveller smiled round at us. Then, still smiling faintly, and
with his hands deep in his trousers pockets, he walked slowly out of
the room, and we heard his slippers shuffling down the long passage to
his laboratory.
The Psychologist looked at us. “I wonder what hes got?”
“Some sleight-of-hand trick or other,” said the Medical Man, and Filby
tried to tell us about a conjuror he had seen at Burslem, but before he
had finished his preface the Time Traveller came back, and Filbys
anecdote collapsed.

7
data/headerNavLinks.js Normal file
View File

@ -0,0 +1,7 @@
const headerNavLinks = [
{ href: '/blog', title: 'Blog' },
{ href: '/tags', title: 'Tags' },
{ href: '/about', title: 'About' },
]
export default headerNavLinks

3
data/logo.svg Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="344.5639097744361 330.27819548872174 111.73684210526318 91.21804511278197" width="53.87" height="43.61"><defs><path d="M453.3 331.28L453.3 359.85L388.64 418.5L388.64 388.42L453.3 331.28Z" id="aFZf6T5ED"></path><linearGradient id="gradientb2ThqnP5Op" gradientUnits="userSpaceOnUse" x1="420.97" y1="331.28" x2="420.97" y2="418.5"><stop style="stop-color: #06b6d4;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #67e8f9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M410.23 331.28L410.23 359.85L345.56 418.5L345.56 388.42L410.23 331.28Z" id="a9fehgwfM"></path><linearGradient id="gradientk1wNV9Ostb" gradientUnits="userSpaceOnUse" x1="377.89" y1="331.28" x2="377.89" y2="418.5"><stop style="stop-color: #06b6d4;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #67e8f9;stop-opacity: 1" offset="100%"></stop></linearGradient></defs><g><g><use xlink:href="#aFZf6T5ED" opacity="1" fill="url(#gradientb2ThqnP5Op)"></use></g><g><use xlink:href="#a9fehgwfM" opacity="1" fill="url(#gradientk1wNV9Ostb)"></use></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

16
data/siteMetadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"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://nextjs-starter-blog-demo.com",
"siteRepo": "https://github.com/user/repo",
"image": "/static/images/avatar.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"
}

12
jsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/data/*": ["data/*"],
"@/layouts/*": ["layouts/*"],
"@/lib/*": ["lib/*"],
"@/css/*": ["css/*"],
}
}
}

83
layouts/ListLayout.js Normal file
View File

@ -0,0 +1,83 @@
import { useState } from 'react'
import Link from 'next/link'
import tinytime from 'tinytime'
import Tag from '@/components/Tag'
const postDateTemplate = tinytime('{MMMM} {DD}, {YYYY}')
export default function ListLayout({ posts, title }) {
const [searchValue, setSearchValue] = useState('')
const filteredBlogPosts = posts.filter((frontMatter) =>
frontMatter.title.toLowerCase().includes(searchValue.toLowerCase())
)
return (
<>
<div className="divide-y">
<div className="pt-6 pb-8 space-y-2 md:space-y-5">
<h1 className="text-3xl leading-9 font-extrabold text-gray-900 dark:text-gray-100 tracking-tight sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
{title}
</h1>
<div className="relative max-w-lg">
<input
aria-label="Search articles"
type="text"
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Search articles"
className="px-4 py-2 border border-gray-300 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
<svg
className="absolute right-3 top-3 h-5 w-5 text-gray-400 dark:text-gray-300"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
</div>
<ul>
{!filteredBlogPosts.length && 'No posts found.'}
{filteredBlogPosts.map((frontMatter) => {
const { slug, date, title, summary, tags } = frontMatter
return (
<li key={slug} className="py-4">
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
<dl>
<dt className="sr-only">Published on</dt>
<dd className="text-base leading-6 font-medium text-gray-500 dark:text-gray-400">
<time dateTime={date}>{postDateTemplate.render(new Date(date))}</time>
</dd>
</dl>
<div className="space-y-3 xl:col-span-3">
<div>
<h3 className="text-2xl leading-8 font-bold tracking-tight">
<Link href={`/blog/${slug}`}>
<a className="text-gray-900 dark:text-gray-100">{title}</a>
</Link>
</h3>
<div className="space-x-2">
{tags.map((tag) => (
<Tag key={tag} text={tag} />
))}
</div>
</div>
<div className="prose max-w-none text-gray-500 dark:text-gray-400">
{summary}
</div>
</div>
</article>
</li>
)
})}
</ul>
</div>
</>
)
}

127
layouts/PostLayout.js Normal file
View File

@ -0,0 +1,127 @@
import tinytime from 'tinytime'
import Link from '@/components/Link'
import SectionContainer from '@/components/SectionContainer'
import PageTitle from '@/components/PageTitle'
import BlogSeo from '@/components/BlogSeo'
import Tag from '@/components/Tag'
import siteMetdata from '@/data/siteMetadata'
const editUrl = (slug) => `${siteMetdata.github}/edit/master/data/blog/${slug}.mdx`
const discussUrl = (slug) =>
`https://mobile.twitter.com/search?q=${encodeURIComponent(`${siteMetdata.siteUrl}/blog/${slug}`)}`
const postDateTemplate = tinytime('{dddd}, {MMMM} {DD}, {YYYY}')
export default function PostLayout({ children, frontMatter, next, prev }) {
const { slug, date, title, tags } = frontMatter
return (
<SectionContainer>
<BlogSeo url={`${frontMatter.url}/blog/${frontMatter.slug}`} {...frontMatter} />
<article className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700">
<header className="pt-6 xl:pb-6">
<div className="space-y-1 text-center">
<dl className="space-y-10">
<div>
<dt className="sr-only">Published on</dt>
<dd className="text-base leading-6 font-medium text-gray-500 dark:text-gray-400">
<time dateTime={date}>{postDateTemplate.render(new Date(date))}</time>
</dd>
</div>
</dl>
<div>
<PageTitle>{title}</PageTitle>
</div>
</div>
</header>
<div
className="divide-y xl:divide-y-0 divide-gray-200 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 pb-8"
style={{ gridTemplateRows: 'auto 1fr' }}
>
<dl className="pt-6 pb-10 xl:pt-11 xl:border-b xl:border-gray-200 xl:dark:border-gray-700">
<dt className="sr-only">Authors</dt>
<dd>
<ul className="flex justify-center xl:block space-x-8 sm:space-x-12 xl:space-x-0 xl:space-y-8">
<li className="flex items-center space-x-2">
<img src={siteMetdata.image} 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">{siteMetdata.author}</dd>
<dt className="sr-only">Twitter</dt>
<dd>
<a
href={siteMetdata.twitter}
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
>
{siteMetdata.twitter.replace('https://twitter.com/', '@')}
</a>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:pb-0 xl:col-span-3 xl:row-span-2">
<div className="prose dark:prose-dark max-w-none pt-10 pb-8">{children}</div>
<div className="text-sm pt-6 pb-6 text-gray-700 dark:text-gray-300">
<a href={discussUrl(slug)} target="_blank" rel="noopener noreferrer">
{'Discuss on Twitter'}
</a>
{``}
<a href={editUrl(slug)} target="_blank" rel="noopener noreferrer">
{'Edit on GitHub'}
</a>
</div>
</div>
<footer className="text-sm font-medium leading-5 xl:divide-y divide-gray-200 dark:divide-gray-700 xl:col-start-1 xl:row-start-2">
{tags && (
<div className="py-4 xl:py-8">
<h2 className="text-xs tracking-wide uppercase text-gray-500 dark:text-gray-400">
Tags
</h2>
<div className="space-x-2 xl:flex xl:flex-col xl:space-x-0">
{tags.map((tag) => (
<Tag key={tag} text={tag} />
))}
</div>
</div>
)}
{(next || prev) && (
<div className="flex justify-between py-4 xl:block xl:space-y-8 xl:py-8">
{prev && (
<div>
<h2 className="text-xs tracking-wide uppercase text-gray-500 dark:text-gray-400">
Previous Article
</h2>
<div className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400">
<Link href={`/blog/${prev.slug}`}>{prev.title}</Link>
</div>
</div>
)}
{next && (
<div>
<h2 className="text-xs tracking-wide uppercase text-gray-500 dark:text-gray-400">
Next Article
</h2>
<div className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400">
<Link href={`/blog/${next.slug}`}>{next.title}</Link>
</div>
</div>
)}
</div>
)}
<div className="pt-4 xl:pt-8">
<Link
href="/blog"
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
>
&larr; Back to the blog
</Link>
</div>
</footer>
</div>
</article>
</SectionContainer>
)
}

26
lib/generate-rss.js Normal file
View File

@ -0,0 +1,26 @@
import siteMetadata from '@/data/siteMetadata'
const generateRssItem = (post) => `
<item>
<guid>${siteMetadata.siteUrl}${post.slug}</guid>
<title>${post.title}</title>
<link>${siteMetadata.siteUrl}${post.slug}</link>
<description>${post.summary}</description>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
</item>
`
const generateRss = (posts) => `
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${siteMetadata.title}</title>
<link>${siteMetadata.siteUrl}/blog</link>
<description>${siteMetadata.description}</description>
<language>${siteMetadata.language}</language>
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
<atom:link href="${siteMetadata.siteUrl}/index.xml" rel="self" type="application/rss+xml"/>
${posts.map(generateRssItem).join('')}
</channel>
</rss>
`
export default generateRss

32
lib/img-to-jsx.js Normal file
View File

@ -0,0 +1,32 @@
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 = 'jsx'
imageNode.value = `<Image
alt={\`${imageNode.alt}\`}
src={\`${imageNode.url}\`}
width={${dimensions.width}}
height={${dimensions.height}}
/>`
// Change node type from p to div to avoid nesting error
node.type = 'div'
node.children = [imageNode]
}
}
)
}

101
lib/mdx.js Normal file
View File

@ -0,0 +1,101 @@
import fs from 'fs'
import matter from 'gray-matter'
import visit from 'unist-util-visit'
import path from 'path'
import readingTime from 'reading-time'
import renderToString from 'next-mdx-remote/render-to-string'
import MDXComponents from '@/components/MDXComponents'
import imgToJsx from './img-to-jsx'
const root = process.cwd()
const tokenClassNames = {
tag: 'text-code-red',
'attr-name': 'text-code-yellow',
'attr-value': 'text-code-green',
deleted: 'text-code-red',
inserted: 'text-code-green',
punctuation: 'text-code-white',
keyword: 'text-code-purple',
string: 'text-code-green',
function: 'text-code-blue',
boolean: 'text-code-red',
comment: 'text-gray-400 italic',
}
export async function getFiles(type) {
return fs.readdirSync(path.join(root, 'data', type))
}
export function dateSortDesc(a, b) {
if (a > b) return -1
if (a < b) return 1
return 0
}
export async function getFileBySlug(type, slug) {
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
const mdPath = path.join(root, 'data', type, `${slug}.md`)
const source = fs.existsSync(mdxPath)
? fs.readFileSync(mdxPath, 'utf8')
: fs.readFileSync(mdPath, 'utf8')
const { data, content } = matter(source)
const mdxSource = await renderToString(content, {
components: MDXComponents,
mdxOptions: {
remarkPlugins: [
require('remark-slug'),
require('remark-autolink-headings'),
require('remark-code-titles'),
require('remark-math'),
imgToJsx,
],
inlineNotes: true,
rehypePlugins: [
require('rehype-katex'),
require('@mapbox/rehype-prism'),
() => {
return (tree) => {
visit(tree, 'element', (node, index, parent) => {
let [token, type] = node.properties.className || []
if (token === 'token') {
node.properties.className = [tokenClassNames[type]]
}
})
}
},
],
},
})
return {
mdxSource,
frontMatter: {
wordCount: content.split(/\s+/gu).length,
readingTime: readingTime(content),
slug: slug || null,
...data,
},
}
}
export async function getAllFilesFrontMatter(type) {
const files = fs.readdirSync(path.join(root, 'data', type))
const allFrontMatter = files.reduce((allPosts, postSlug) => {
const source = fs.readFileSync(path.join(root, 'data', type, postSlug), 'utf8')
const { data } = matter(source)
return [
{
...data,
slug: postSlug.replace(/\.(mdx|md)/, ''),
},
...allPosts,
]
}, [])
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
}

29
lib/tags.js Normal file
View File

@ -0,0 +1,29 @@
import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'
import kebabCase from 'just-kebab-case'
const root = process.cwd()
export async function getAllTags(type) {
const files = fs.readdirSync(path.join(root, 'data', type))
let tagCount = {}
// 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)
if (data.tags) {
data.tags.forEach((tag) => {
const formattedTag = kebabCase(tag)
if (formattedTag in tagCount) {
tagCount[formattedTag] += 1
} else {
tagCount[formattedTag] = 1
}
})
}
})
return tagCount
}

44
next.config.js Normal file
View File

@ -0,0 +1,44 @@
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
experimental: {
modern: true,
},
webpack: (config, { dev, isServer }) => {
config.module.rules.push({
test: /\.(png|jpe?g|gif|mp4)$/i,
use: [
{
loader: 'file-loader',
options: {
publicPath: '/_next',
name: 'static/media/[name].[hash].[ext]',
},
},
],
})
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
})
if (!dev && isServer) {
require('./scripts/generate-sitemap')
}
if (!dev && !isServer) {
// Replace React with Preact only in client production build
Object.assign(config.resolve.alias, {
react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat',
})
}
return config
},
})

9040
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "tailwind-blog",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "next dev",
"dev": "next dev",
"build": "next build",
"serve": "next start",
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@mapbox/rehype-prism": "^0.5.0",
"@mdx-js/loader": "^1.6.20",
"@mdx-js/react": "^1.6.22",
"@next/mdx": "^10.0.5",
"@tailwindcss/forms": "^0.2.1",
"@tailwindcss/typography": "^0.3.1",
"autoprefixer": "^10.1.0",
"gray-matter": "^4.0.2",
"image-size": "0.9.3",
"just-kebab-case": "^1.1.0",
"next": "10.0.5",
"next-google-fonts": "^1.2.1",
"next-mdx-remote": "^2.0.0",
"next-seo": "4.17.0",
"next-themes": "^0.0.10",
"postcss": "^8.2.1",
"preact": "^10.5.7",
"react": "16.13.1",
"react-dom": "16.13.1",
"reading-time": "1.2.1",
"rehype-katex": "^4.0.0",
"remark-autolink-headings": "6.0.1",
"remark-code-titles": "0.1.1",
"remark-footnotes": "^2.0.0",
"remark-math": "3.0.1",
"remark-slug": "6.0.0",
"tailwindcss": "^2.0.2",
"tinytime": "^0.2.6"
},
"devDependencies": {
"@next/bundle-analyzer": "^10.0.0",
"@svgr/webpack": "^5.5.0",
"file-loader": "^6.0.0",
"globby": "11.0.1",
"next-compose-plugins": "^2.2.1",
"prettier": "2.2.1",
"rehype": "11.0.0",
"remark-frontmatter": "3.0.0",
"remark-parse": "9.0.0",
"remark-stringify": "9.0.0",
"unified": "9.2.0",
"unist-util-visit": "2.0.3"
}
}

68
pages/_app.js Normal file
View File

@ -0,0 +1,68 @@
import '@/css/tailwind.css'
import { MDXProvider } from '@mdx-js/react'
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'
import MDXComponents from '@/components/MDXComponents'
export default function App({ Component, pageProps }) {
return (
<ThemeProvider attribute="class">
<MDXProvider components={MDXComponents}>
<div className="antialiased">
<Head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
</Head>
<DefaultSeo {...SEO} />
<Head>
<link
rel="apple-touch-icon-precomposed"
sizes="57x57"
href="apple-touch-icon-57x57.png"
/>
<link
rel="apple-touch-icon-precomposed"
sizes="114x114"
href="apple-touch-icon-114x114.png"
/>
<link
rel="apple-touch-icon-precomposed"
sizes="72x72"
href="apple-touch-icon-72x72.png"
/>
<link
rel="apple-touch-icon-precomposed"
sizes="144x144"
href="apple-touch-icon-144x144.png"
/>
<link
rel="apple-touch-icon-precomposed"
sizes="120x120"
href="apple-touch-icon-120x120.png"
/>
<link
rel="apple-touch-icon-precomposed"
sizes="152x152"
href="apple-touch-icon-152x152.png"
/>
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
<link rel="manifest" href="/manifest.json" />
<meta name="application-name" content="&nbsp;" />
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="mstile-144x144.png" />
<meta name="theme-color" content="#ffffff" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
</Head>
<LayoutWrapper>
<Component {...pageProps} />
</LayoutWrapper>
</div>
</MDXProvider>
</ThemeProvider>
)
}

52
pages/_document.js Normal file
View File

@ -0,0 +1,52 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
import GoogleFonts from 'next-google-fonts'
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<GoogleFonts href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" />
<Head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X"
crossOrigin="anonymous"
/>
<link href="/static/favicons/favicon.ico" rel="shortcut icon" />
<link href="/static/favicons/site.webmanifest" rel="manifest" />
<link rel="preconnect" href="https://fonts.gstatic.com/" crossOrigin="" />
<link
href="/static/favicons/apple-touch-icon.png"
rel="apple-touch-icon"
sizes="180x180"
/>
<link
href="/static/favicons/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/>
<link
href="/static/favicons/favicon-16x16.png"
rel="icon"
sizes="16x16"
type="image/png"
/>
<link color="#00aba9" href="/static/favicons/safari-pinned-tab.svg" rel="mask-icon" />
<link rel="alternate" type="application/rss+xml" href="/index.xml" />
<meta content="IE=edge" httpEquiv="X-UA-Compatible" />
<meta content="#ffffff" name="theme-color" />
<meta content="#00aba9" name="msapplication-TileColor" />
<meta content="/static/favicons/browserconfig.xml" name="msapplication-config" />
</Head>
<body className="bg-white dark:bg-gray-900 text-black dark:text-white">
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

60
pages/about.js Normal file
View File

@ -0,0 +1,60 @@
import { NextSeo } from 'next-seo'
import siteMetadata from '@/data/siteMetadata'
import SocialIcon from '@/components/social-icons'
export default function About() {
return (
<>
<NextSeo
title={`About - ${siteMetadata.author}`}
canonical={`${siteMetadata.siteUrl}/about`}
openGraph={{
url: `${siteMetadata.siteUrl}/about`,
title: `About - ${siteMetadata.author}`,
}}
/>
<div className="divide-y">
<div className="pt-6 pb-8 space-y-2 md:space-y-5">
<h1 className="text-3xl leading-9 font-extrabold text-gray-900 dark:text-gray-100 tracking-tight sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
About
</h1>
</div>
<div className="space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0 items-start">
<div className="flex flex-col items-center space-x-2 pt-8">
<img src={siteMetadata.image} alt="avatar" className="w-48 h-48 rounded-full" />
<h3 className="text-2xl leading-8 font-bold tracking-tight pt-4 pb-2">
{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="prose dark:prose-dark max-w-none pt-8 pb-8 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>
</>
)
}

30
pages/blog.js Normal file
View File

@ -0,0 +1,30 @@
import { NextSeo } from 'next-seo'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
export async function getStaticProps() {
const posts = await getAllFilesFrontMatter('blog')
return { props: { posts } }
}
export default function Blog({ posts }) {
return (
<>
<NextSeo
title={`Blog - ${siteMetadata.name}`}
description={siteMetadata.description}
canonical={`${siteMetadata.siteUrl}/blog`}
openGraph={{
url: `${siteMetadata.siteUrl}/blog`,
title: `Blog - ${siteMetadata.name}`,
description: siteMetadata.description,
}}
/>
<ListLayout posts={posts} title="All Posts"/>
</>
)
}

50
pages/blog/[slug].js Normal file
View File

@ -0,0 +1,50 @@
import fs from 'fs'
import hydrate from 'next-mdx-remote/hydrate'
import { getFiles, getFileBySlug, getAllFilesFrontMatter } from '@/lib/mdx'
import PostLayout from '@/layouts/PostLayout'
import MDXComponents from '@/components/MDXComponents'
import generateRss from '@/lib/generate-rss'
export async function getStaticPaths() {
const posts = await getFiles('blog')
return {
paths: posts.map((p) => ({
params: {
slug: p.replace(/\.(mdx|md)/, ''),
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const allPosts = await getAllFilesFrontMatter('blog')
const postIndex = allPosts.findIndex((post) => post.slug === params.slug)
const prev = allPosts[postIndex + 1] || null
const next = allPosts[postIndex - 1] || null
const post = await getFileBySlug('blog', params.slug)
// rss
const rss = generateRss(allPosts)
fs.writeFileSync('./public/index.xml', rss)
return { props: { post, prev, next } }
}
export default function Blog({ post, prev, next }) {
const { mdxSource, frontMatter } = post
const content = hydrate(mdxSource, {
components: MDXComponents,
})
return (
<>
{frontMatter.draft !== true && (
<PostLayout frontMatter={frontMatter} prev={prev} next={next}>
{content}
</PostLayout>
)}
</>
)
}

90
pages/index.js Normal file
View File

@ -0,0 +1,90 @@
import tinytime from 'tinytime'
import Link from 'next/link'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'
import Tag from '@/components/Tag'
const MAX_DISPLAY = 5
const postDateTemplate = tinytime('{MMMM} {DD}, {YYYY}')
export async function getStaticProps() {
const posts = await getAllFilesFrontMatter('blog')
return { props: { posts } }
}
export default function Home({ posts }) {
return (
<div>
<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 leading-9 font-extrabold text-gray-900 dark:text-gray-100 tracking-tight sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
Latest
</h1>
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
{siteMetadata.description}
</p>
</div>
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
{!posts.length && 'No posts found.'}
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
const { slug, date, title, summary, tags } = frontMatter
return (
<li key={slug} className="py-12">
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
<dl>
<dt className="sr-only">Published on</dt>
<dd className="text-base leading-6 font-medium text-gray-500 dark:text-gray-400">
<time dateTime={date}>{postDateTemplate.render(new Date(date))}</time>
</dd>
</dl>
<div className="space-y-5 xl:col-span-3">
<div className="space-y-6">
<div>
<h2 className="text-2xl leading-8 font-bold tracking-tight">
<Link href={`/blog/${slug}`}>
<a className="text-gray-900 dark:text-gray-100">{title}</a>
</Link>
</h2>
<div className="space-x-2">
{tags.map((tag) => (
<Tag key={tag} text={tag} />
))}
</div>
</div>
<div className="prose max-w-none text-gray-500 dark:text-gray-400">
{summary}
</div>
</div>
<div className="text-base leading-6 font-medium">
<Link href={`/blog/${slug}`}>
<a
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
aria-label={`Read "${title}"`}
>
Read more &rarr;
</a>
</Link>
</div>
</div>
</article>
</li>
)
})}
</ul>
</div>
{posts.length > MAX_DISPLAY && (
<div className="flex justify-end text-base leading-6 font-medium">
<Link href="/blog">
<a
className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
aria-label="all posts"
>
All Posts &rarr;
</a>
</Link>
</div>
)}
</div>
)
}

48
pages/tags.js Normal file
View File

@ -0,0 +1,48 @@
import Link from 'next/link'
import kebabCase from 'just-kebab-case'
import { NextSeo } from 'next-seo'
import siteMetadata from '@/data/siteMetadata'
import { getAllTags } from '@/lib/tags'
import Tag from '@/components/Tag'
export async function getStaticProps() {
const tags = await getAllTags('blog')
return { props: { tags } }
}
export default function Tags({ tags }) {
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
return (
<>
<NextSeo
title={`Tags - ${siteMetadata.title}`}
canonical={`${siteMetadata.siteUrl}/tags`}
openGraph={{
url: `${siteMetadata.siteUrl}/tags`,
title: `Tags - ${siteMetadata.title}`,
}}
/>
<div className="flex items-start justify-start flex-col 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 leading-9 font-extrabold text-gray-900 dark:text-gray-100 tracking-tight sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 md:border-r-2 md:px-6">
Tags
</h1>
</div>
<div className="max-w-lg flex flex-wrap">
{Object.keys(tags).length === 0 && 'No tags found.'}
{sortedTags.map((t) => {
return (
<div key={t} className="m-2">
<Tag text={t} />
<Link href={`/tags/${kebabCase(t)}`}>
<a className="uppercase font-semibold text-sm mx-1 text-gray-600 dark:text-gray-300">{` (${tags[t]})`}</a>
</Link>
</div>
)
})}
</div>
</div>
</>
)
}

47
pages/tags/[tag].js Normal file
View File

@ -0,0 +1,47 @@
import { NextSeo } from 'next-seo'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import { getAllTags } from '@/lib/tags'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
import kebabCase from 'just-kebab-case'
export async function getStaticPaths() {
const tags = await getAllTags('blog')
return {
paths: Object.keys(tags).map((tag) => ({
params: {
tag,
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const allPosts = await getAllFilesFrontMatter('blog')
const filteredPosts = allPosts.filter(
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
)
return { props: { posts: filteredPosts, tag: params.tag } }
}
export default function Blog({ posts, tag }) {
// Capitalize first letter and convert space to dash
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
return (
<>
<NextSeo
title={`${tag} - ${siteMetadata.title}`}
description={siteMetadata.description}
canonical={`${siteMetadata.siteUrl}/tags/${tag}`}
openGraph={{
url: `${siteMetadata.siteUrl}/tags/${tag}`,
title: `${tag} - ${siteMetadata.title}`,
}}
/>
<ListLayout posts={posts} title={title} />
</>
)
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

9
prettier.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
semi: false,
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
trailingComma: 'es5',
bracketSpacing: true,
}

60
public/index.xml Normal file
View File

@ -0,0 +1,60 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Next.js Starter Blog</title>
<link>https://nextjs-starter-blog-demo.com/blog</link>
<description>A blog created with Next.js and Tailwind.css</description>
<language>en-us</language>
<lastBuildDate>Wed, 11 Nov 2020 00:00:00 GMT</lastBuildDate>
<atom:link href="https://nextjs-starter-blog-demo.com/index.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://nextjs-starter-blog-demo.comguide-to-using-images-in-nextjs</guid>
<title>Images in Next.js</title>
<link>https://nextjs-starter-blog-demo.comguide-to-using-images-in-nextjs</link>
<description>In this article we introduce adding images in the tailwind starter blog and the benefits and limitations of the next/image component.</description>
<pubDate>Wed, 11 Nov 2020 00:00:00 GMT</pubDate>
</item>
<item>
<guid>https://nextjs-starter-blog-demo.comderiving-ols-estimator</guid>
<title>Deriving the OLS Estimator</title>
<link>https://nextjs-starter-blog-demo.comderiving-ols-estimator</link>
<description>How to derive the OLS Estimator with matrix notation and a tour of math typesetting using markdown with the help of KaTeX.</description>
<pubDate>Sat, 16 Nov 2019 00:00:00 GMT</pubDate>
</item>
<item>
<guid>https://nextjs-starter-blog-demo.comgithub-markdown-guide</guid>
<title>Markdown Guide</title>
<link>https://nextjs-starter-blog-demo.comgithub-markdown-guide</link>
<description>Markdown cheatsheet for all your blogging needs - headers, lists, images, tables and more! An illustrated guide based on Github Flavored Markdown.</description>
<pubDate>Fri, 11 Oct 2019 00:00:00 GMT</pubDate>
</item>
<item>
<guid>https://nextjs-starter-blog-demo.comthe-time-machine</guid>
<title>The Time Machine</title>
<link>https://nextjs-starter-blog-demo.comthe-time-machine</link>
<description>The Time Traveller (for so it will be convenient to speak of him) was expounding a recondite matter to us. His pale grey eyes shone and twinkled, and his usually pale face was flushed and animated...</description>
<pubDate>Wed, 15 Aug 2018 00:00:00 GMT</pubDate>
</item>
<item>
<guid>https://nextjs-starter-blog-demo.compictures-of-canada</guid>
<title>O Canada</title>
<link>https://nextjs-starter-blog-demo.compictures-of-canada</link>
<description>The scenic lands of Canada featuring maple leaves, snow-capped mountains, turquoise lakes and Toronto. Take in the sights in this photo gallery exhibition and see how easy it is to replicate with some MDX magic and tailwind classes.</description>
<pubDate>Sat, 15 Jul 2017 00:00:00 GMT</pubDate>
</item>
<item>
<guid>https://nextjs-starter-blog-demo.comcode-sample</guid>
<title>Sample .md file</title>
<link>https://nextjs-starter-blog-demo.comcode-sample</link>
<description>Example of a markdown file with code blocks and syntax highlighting</description>
<pubDate>Tue, 08 Mar 2016 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,11 @@
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
<meta name="application-name" content="&nbsp;"/>
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="mstile-144x144.png" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

@ -0,0 +1,45 @@
const fs = require('fs')
const globby = require('globby')
const prettier = require('prettier')
const siteMetadata = require('../data/siteMetadata')
;(async () => {
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
const pages = await globby([
'pages/*.js',
'data/**/*.mdx',
'data/**/*.md',
'!pages/_*.js',
'!pages/api',
])
const sitemap = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages
.map((page) => {
const path = page
.replace('pages', '')
.replace('data', '')
.replace('.js', '')
.replace('.mdx', '')
.replace('.md', '')
const route = path === '/index' ? '' : path
return `
<url>
<loc>${`${siteMetadata.siteUrl}${route}`}</loc>
</url>
`
})
.join('')}
</urlset>
`
const formatted = prettier.format(sitemap, {
...prettierConfig,
parser: 'html',
})
// eslint-disable-next-line no-sync
fs.writeFileSync('public/sitemap.xml', formatted)
})()

155
tailwind.config.js Normal file
View File

@ -0,0 +1,155 @@
const defaultTheme = require('tailwindcss/defaultTheme')
const colors = require('tailwindcss/colors')
module.exports = {
purge: {
content: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './lib/**/*.js'],
options: {
safelist: ['type'], // [type='checkbox']
},
},
darkMode: 'class',
theme: {
extend: {
spacing: {
'9/16': '56.25%',
},
lineHeight: {
11: '2.75rem',
12: '3rem',
13: '3.25rem',
14: '3.5rem',
},
fontFamily: {
sans: ['Inter', ...defaultTheme.fontFamily.sans],
},
colors: {
blue: colors.lightBlue,
code: {
green: '#b5f4a5',
yellow: '#ffe484',
purple: '#d9a9ff',
red: '#ff8383',
blue: '#93ddfd',
white: '#fff',
},
},
typography: (theme) => ({
DEFAULT: {
css: {
color: theme('colors.gray.700'),
a: {
color: theme('colors.blue.500'),
'&:hover': {
color: theme('colors.blue.600'),
},
code: { color: theme('colors.blue.400') },
},
h1: {
fontWeight: '700',
letterSpacing: theme('letterSpacing.tight'),
color: theme('colors.gray.900'),
},
h2: {
fontWeight: '700',
letterSpacing: theme('letterSpacing.tight'),
color: theme('colors.gray.900'),
},
h3: {
fontWeight: '600',
color: theme('colors.gray.900'),
},
'h4,h5,h6': {
color: theme('colors.gray.900'),
},
code: {
color: theme('colors.pink.500'),
backgroundColor: theme('colors.gray.100'),
paddingLeft: '4px',
paddingRight: '4px',
paddingTop: '2px',
paddingBottom: '2px',
borderRadius: '0.25rem',
},
'code:before': {
content: 'none',
},
'code:after': {
content: 'none',
},
hr: { borderColor: theme('colors.gray.200') },
'ol li:before': {
fontWeight: '600',
color: theme('colors.gray.500'),
},
'ul li:before': {
backgroundColor: theme('colors.gray.500'),
},
strong: { color: theme('colors.gray.600') },
blockquote: {
color: theme('colors.gray.900'),
borderLeftColor: theme('colors.gray.200'),
},
},
},
dark: {
css: {
color: theme('colors.gray.300'),
a: {
color: theme('colors.blue.500'),
'&:hover': {
color: theme('colors.blue.400'),
},
code: { color: theme('colors.blue.400') },
},
h1: {
fontWeight: '700',
letterSpacing: theme('letterSpacing.tight'),
color: theme('colors.gray.100'),
},
h2: {
fontWeight: '700',
letterSpacing: theme('letterSpacing.tight'),
color: theme('colors.gray.100'),
},
h3: {
fontWeight: '600',
color: theme('colors.gray.100'),
},
'h4,h5,h6': {
color: theme('colors.gray.100'),
},
code: {
backgroundColor: theme('colors.gray.800'),
},
hr: { borderColor: theme('colors.gray.700') },
'ol li:before': {
fontWeight: '600',
color: theme('colors.gray.400'),
},
'ul li:before': {
backgroundColor: theme('colors.gray.400'),
},
strong: { color: theme('colors.gray.100') },
thead: {
color: theme('colors.gray.100'),
},
tbody: {
tr: {
borderBottomColor: theme('colors.gray.700'),
},
},
blockquote: {
color: theme('colors.gray.100'),
borderLeftColor: theme('colors.gray.700'),
},
},
},
}),
},
},
variants: {
typography: ['dark'],
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
}