Compare commits
67 commits
master
...
demo/trans
Author | SHA1 | Date | |
---|---|---|---|
de8752b8f9 | |||
2111bf3599 | |||
4b80ec51de | |||
13b6981c15 | |||
f66ae87ae0 | |||
c6bbfa58b6 | |||
592e1a4c08 | |||
71c20d7505 | |||
ece3a54732 | |||
4953ebc53d | |||
869772a328 | |||
ffa5d6e9cd | |||
85c2b00c86 | |||
1911a5e102 | |||
910f4c5483 | |||
2243d1cabc | |||
0e14f9ec8d | |||
08156d5ef7 | |||
7ac6057234 | |||
b7d7f882a3 | |||
6078ae610f | |||
4e6352b1f7 | |||
d2f8982267 | |||
1d8ca0a2c5 | |||
3dbd14c79d | |||
7270ba10ad | |||
c15e764e41 | |||
1fbb447697 | |||
e7b8964db6 | |||
549fee4fc8 | |||
c543762db3 | |||
d61c82301b | |||
2a1f0a04fa | |||
e5ad9683fc | |||
fc33a761f7 | |||
b3ae2cfd21 | |||
b9f34e09b6 | |||
ecf06036af | |||
ebd24e3a9d | |||
466e873bbe | |||
c63829309a | |||
264fcb97ed | |||
7ec7a28cfa | |||
77ea66192a | |||
05f2be482b | |||
f52f5a38f8 | |||
f860b91142 | |||
01a50c5f48 | |||
be98630ba4 | |||
6a2c20de11 | |||
b58801ed68 | |||
933ac97af6 | |||
0e931a6221 | |||
c084276a18 | |||
1478a0302b | |||
72102031b2 | |||
b665fff449 | |||
6a2dcd276a | |||
2651376f01 | |||
08fc841c6e | |||
0a8692c3db | |||
c57a43f3c5 | |||
9ccf1bc171 | |||
7eda123f1a | |||
147657c700 | |||
32756e2e11 | |||
2f336fcd9f |
|
@ -1,8 +1,10 @@
|
|||
import Link from './Link'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import SocialIcon from '@/components/social-icons'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Footer() {
|
||||
const { locale } = useRouter()
|
||||
return (
|
||||
<footer>
|
||||
<div className="flex flex-col items-center mt-16">
|
||||
|
@ -19,7 +21,7 @@ export default function Footer() {
|
|||
<div>{` • `}</div>
|
||||
<div>{`© ${new Date().getFullYear()}`}</div>
|
||||
<div>{` • `}</div>
|
||||
<Link href="/">{siteMetadata.title}</Link>
|
||||
<Link href="/">{siteMetadata.title[locale]}</Link>
|
||||
</div>
|
||||
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
<Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/no-onchange */
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import headerNavLinks from '@/data/headerNavLinks'
|
||||
import Logo from '@/data/logo.svg'
|
||||
|
@ -7,7 +8,19 @@ import Footer from './Footer'
|
|||
import MobileNav from './MobileNav'
|
||||
import ThemeSwitch from './ThemeSwitch'
|
||||
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const LayoutWrapper = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { locale, locales, defaultLocale } = router
|
||||
|
||||
const changeLanguage = (e) => {
|
||||
const locale = e.target.value
|
||||
router.push(router.asPath, router.asPath, { locale })
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<div className="flex flex-col justify-between h-screen">
|
||||
|
@ -20,10 +33,10 @@ const LayoutWrapper = ({ children }) => {
|
|||
</div>
|
||||
{typeof siteMetadata.headerTitle === 'string' ? (
|
||||
<div className="hidden h-6 text-2xl font-semibold sm:block">
|
||||
{siteMetadata.headerTitle}
|
||||
{siteMetadata.headerTitle[locale]}
|
||||
</div>
|
||||
) : (
|
||||
siteMetadata.headerTitle
|
||||
siteMetadata.headerTitle[locale]
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
@ -36,10 +49,22 @@ const LayoutWrapper = ({ children }) => {
|
|||
href={link.href}
|
||||
className="p-1 font-medium text-gray-900 sm:p-4 dark:text-gray-100"
|
||||
>
|
||||
{link.title}
|
||||
{t(`headerNavLinks:${link.title.toLowerCase()}`)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<select
|
||||
onChange={changeLanguage}
|
||||
defaultValue={locale}
|
||||
style={{ textAlignLast: 'center' }}
|
||||
className="text-gray-900 dark:text-gray-100 text-shadow-sm text-sm bg-transparent tracking-wide"
|
||||
>
|
||||
{locales.map((e) => (
|
||||
<option value={e} key={e}>
|
||||
{e}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ThemeSwitch />
|
||||
<MobileNav />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import Link from '@/components/Link'
|
||||
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export default function Pagination({ totalPages, currentPage }) {
|
||||
const { t } = useTranslation()
|
||||
const prevPage = parseInt(currentPage) - 1 > 0
|
||||
const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages)
|
||||
|
||||
|
@ -9,25 +12,25 @@ export default function Pagination({ totalPages, currentPage }) {
|
|||
<nav className="flex justify-between">
|
||||
{!prevPage && (
|
||||
<button rel="previous" className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
|
||||
Previous
|
||||
{t('common:prevp')}
|
||||
</button>
|
||||
)}
|
||||
{prevPage && (
|
||||
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
|
||||
<button rel="previous">Previous</button>
|
||||
<button rel="previous">{t('common:prevp')}</button>
|
||||
</Link>
|
||||
)}
|
||||
<span>
|
||||
{currentPage} of {totalPages}
|
||||
{currentPage} {t('common:of')} {totalPages}
|
||||
</span>
|
||||
{!nextPage && (
|
||||
<button rel="next" className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
|
||||
Next
|
||||
{t('common:nextp')}
|
||||
</button>
|
||||
)}
|
||||
{nextPage && (
|
||||
<Link href={`/blog/page/${currentPage + 1}`}>
|
||||
<button rel="next">Next</button>
|
||||
<button rel="next">{t('common:nextp')}</button>
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
|
|
18
components/Rss.js
Normal file
18
components/Rss.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useRouter } from 'next/router'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const Rss = () => {
|
||||
const { locale, defaultLocale } = useRouter()
|
||||
return (
|
||||
<link
|
||||
key={locale}
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={`${siteMetadata.title[locale]} - ${locale.toLocaleUpperCase()} RSS feed`}
|
||||
href={`/feed${locale === defaultLocale ? '' : `.${locale}`}.xml`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Rss
|
|
@ -2,7 +2,28 @@ import Head from 'next/head'
|
|||
import { useRouter } from 'next/router'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export const PageSeo = ({ title, description }) => {
|
||||
const generateLinks = (router, availableLocales) =>
|
||||
availableLocales.map((locale) => (
|
||||
<link
|
||||
key={locale}
|
||||
rel={
|
||||
// Here we do as follow: Default langage is canonical
|
||||
// if default langage is not present, we get the first element of the langage array by default
|
||||
// Because the functions should be deterministic, it keep the same(s) link as canonical or alternante
|
||||
locale === router.defaultLocale
|
||||
? 'canonical'
|
||||
: !availableLocales.includes(router.defaultLocale) && locale === availableLocales[0]
|
||||
? 'canonical'
|
||||
: 'alternate'
|
||||
}
|
||||
hrefLang={locale}
|
||||
href={`${siteMetadata.siteUrl}${locale === router.defaultLocale ? '' : `/${locale}`}${
|
||||
router.asPath
|
||||
}`}
|
||||
/>
|
||||
))
|
||||
|
||||
export const PageSeo = ({ title, description, availableLocales }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Head>
|
||||
|
@ -11,20 +32,72 @@ export const PageSeo = ({ title, description }) => {
|
|||
<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:site_name" content={siteMetadata.title[router.locale]} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:image" content={`${siteMetadata.siteUrl}${siteMetadata.socialBanner}`} />
|
||||
<meta property="og:locale" content={router.locale} />
|
||||
{availableLocales &&
|
||||
availableLocales
|
||||
.filter((locale) => locale !== router.locale)
|
||||
.map((locale) => <meta key={locale} property="og:locale:alternate" content={locale} />)}
|
||||
<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}`} />
|
||||
{availableLocales && generateLinks(router, availableLocales)}
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
export const BlogSeo = ({ authorDetails, title, summary, date, lastmod, url, images = [] }) => {
|
||||
export const TagSeo = ({ title, description, availableLocales }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Head>
|
||||
<title>{`${title}`}</title>
|
||||
<meta name="robots" content="follow, index" />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={siteMetadata.title[router.locale]} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:image" content={`${siteMetadata.siteUrl}${siteMetadata.socialBanner}`} />
|
||||
<meta property="og:locale" content={router.locale} />
|
||||
{availableLocales &&
|
||||
availableLocales
|
||||
.filter((locale) => locale !== router.locale)
|
||||
.map((locale) => <meta key={locale} property="og:locale:alternate" content={locale} />)}
|
||||
<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}`} />
|
||||
<link
|
||||
key={router.locale}
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={`${description} - RSS feed`}
|
||||
href={`${siteMetadata.siteUrl}${router.asPath}/feed${
|
||||
router.locale === router.defaultLocale ? '' : `.${router.locale}`
|
||||
}.xml`}
|
||||
/>
|
||||
{availableLocales && generateLinks(router, availableLocales)}
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
export const BlogSeo = ({
|
||||
authorDetails,
|
||||
title,
|
||||
summary,
|
||||
date,
|
||||
lastmod,
|
||||
url,
|
||||
availableLocales,
|
||||
images = [],
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const publishedAt = new Date(date).toISOString()
|
||||
const modifiedAt = new Date(lastmod || date).toISOString()
|
||||
|
@ -88,12 +161,17 @@ export const BlogSeo = ({ authorDetails, title, summary, date, lastmod, url, ima
|
|||
<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:site_name" content={siteMetadata.title[router.locale]} />
|
||||
<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 property="og:locale" content={router.locale} />
|
||||
{availableLocales &&
|
||||
availableLocales
|
||||
.filter((locale) => locale !== router.locale)
|
||||
.map((locale) => <meta key={locale} property="og:locale:alternate" content={locale} />)}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content={siteMetadata.twitter} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
|
@ -101,7 +179,7 @@ export const BlogSeo = ({ authorDetails, title, summary, date, lastmod, url, ima
|
|||
<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}`} />
|
||||
{availableLocales && generateLinks(router, availableLocales)}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData, null, 2) }}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
const Disqus = ({ frontMatter }) => {
|
||||
const { t } = useTranslation()
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
|
||||
const COMMENTS_ID = 'disqus_thread'
|
||||
|
||||
function LoadComments() {
|
||||
|
@ -28,7 +29,7 @@ const Disqus = ({ frontMatter }) => {
|
|||
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
{enableLoadComments && <button onClick={LoadComments}>{t('common:comment')}</button>}
|
||||
<div className="disqus-frame" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -2,8 +2,10 @@ import React, { useState } from 'react'
|
|||
import { useTheme } from 'next-themes'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
const Giscus = ({ mapping }) => {
|
||||
const { t } = useTranslation()
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
const { theme, resolvedTheme } = useTheme()
|
||||
const commentsTheme =
|
||||
|
@ -41,7 +43,7 @@ const Giscus = ({ mapping }) => {
|
|||
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
{enableLoadComments && <button onClick={LoadComments}>{t('common:comment')}</button>}
|
||||
<div className="giscus" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -2,8 +2,10 @@ import React, { useState } from 'react'
|
|||
import { useTheme } from 'next-themes'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
const Utterances = ({ issueTerm }) => {
|
||||
const { t } = useTranslation()
|
||||
const [enableLoadComments, setEnabledLoadComments] = useState(true)
|
||||
const { theme, resolvedTheme } = useTheme()
|
||||
const commentsTheme =
|
||||
|
@ -36,7 +38,7 @@ const Utterances = ({ issueTerm }) => {
|
|||
// Added `relative` to fix a weird bug with `utterances-frame` position
|
||||
return (
|
||||
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
|
||||
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
|
||||
{enableLoadComments && <button onClick={LoadComments}>{t('common:comment')}</button>}
|
||||
<div className="utterances-frame relative" id={COMMENTS_ID} />
|
||||
</div>
|
||||
)
|
||||
|
|
16
data/authors/default.fr.md
Normal file
16
data/authors/default.fr.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
name: Tails Azimuth
|
||||
avatar: /static/images/avatar.png
|
||||
occupation: Professeur des sciences atmosphériques
|
||||
company: Université de Stanford
|
||||
email: address@yoursite.com
|
||||
twitter: https://twitter.com/Twitter
|
||||
linkedin: https://www.linkedin.com
|
||||
github: https://github.com
|
||||
---
|
||||
|
||||
Tails Azimuth est un professeur des sciences atmosphériques au laboratoire d'IA de Stanford. Ses recherches portent sur la modélisation complexe des vents arrière, des vents avant et des vents arrière.
|
||||
|
||||
Version française
|
||||
|
||||
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.
|
10
data/authors/sparrowhawk.fr.md
Normal file
10
data/authors/sparrowhawk.fr.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Sparrow Hawk
|
||||
avatar: /static/images/sparrowhawk-avatar.jpg
|
||||
occupation: Magicien de la mer de terre
|
||||
company: mer de terre
|
||||
twitter: https://twitter.com/sparrowhawk
|
||||
linkedin: https://www.linkedin.com/sparrowhawk
|
||||
---
|
||||
|
||||
Sparrowok en français
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
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))
|
||||
```
|
|
@ -1,141 +0,0 @@
|
|||
---
|
||||
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}
|
||||
$$
|
|
@ -1,186 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
||||
Here’s 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
|
||||
|
||||
Here’s 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~~.
|
|
@ -1,76 +0,0 @@
|
|||
---
|
||||
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.'
|
||||
author: sparrowhawk
|
||||
---
|
||||
|
||||
# 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 [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
|
||||
|
||||
- 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`
|
16
data/blog/introducing-tailwind-nextjs-starter-blog.fr.mdx
Normal file
16
data/blog/introducing-tailwind-nextjs-starter-blog.fr.mdx
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
title: 'Introduction du blog tailwind en français'
|
||||
date: '2021-01-12'
|
||||
lastmod: '2021-08-02'
|
||||
tags: ['next-js', 'tailwind', 'guide']
|
||||
draft: false
|
||||
summary: 'Lorem Ipsum français'
|
||||
images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg']
|
||||
authors: ['default', 'sparrowhawk']
|
||||
---
|
||||
|
||||
![tailwind-nextjs-banner](/static/images/twitter-card.png)
|
||||
|
||||
# tailwind
|
||||
|
||||
lorem ipsu français
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
title: My fancy title
|
||||
date: '2021-01-31'
|
||||
tags: ['hello']
|
||||
draft: true
|
||||
summary:
|
||||
images: []
|
||||
---
|
||||
|
||||
Draft post which should not display
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: Introducing Multi-part Posts with Nested Routing
|
||||
date: '2021-05-02'
|
||||
tags: ['multi-author', 'next-js', 'feature']
|
||||
draft: false
|
||||
summary: 'The blog template supports posts in nested sub-folders. This can be used to group posts of similar content e.g. a multi-part course. This post is itself an example of a nested route!'
|
||||
---
|
||||
|
||||
# Nested Routes
|
||||
|
||||
The blog template supports posts in nested sub-folders. This helps in organisation and can be used to group posts of similar content e.g. a multi-part series. This post is itself an example of a nested route! It's located in the `/data/blog/nested-route` folder.
|
||||
|
||||
## How
|
||||
|
||||
Simplify create multiple folders inside the main `/data/blog` folder and add your `.md`/`.mdx` files to them. You can even create something like `/data/blog/nested-route/deeply-nested-route/my-post.md`
|
||||
|
||||
We use Next.js catch all routes to handle the routing and path creations.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Here's some reasons to use nested routes
|
||||
|
||||
- More logical content organisation (blogs will still be displayed based on the created date)
|
||||
- Multi-part posts
|
||||
- Different sub-routes for each author
|
||||
- Internationalization (though it would be recommended to use [Next.js built in i8n routing](https://nextjs.org/docs/advanced-features/i18n-routing))
|
||||
|
||||
## Note
|
||||
|
||||
- The previous/next post links at bottom of the template is currently sorted by date. One could explore modifying the template to refer the reader to the previous/next post in the series, rather than by date.
|
10
data/blog/new-features-in-v1.fr.mdx
Normal file
10
data/blog/new-features-in-v1.fr.mdx
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: 'Nouvelles fonctionnalités de la v1'
|
||||
date: 2021-07-28T15:32:14Z
|
||||
tags: ['tag francais']
|
||||
draft: false
|
||||
summary: 'Un aperçu des nouvelles fonctionnalités publiées dans la v1 - copie de bloc de code, auteurs multiples, mise en page frontale et plus encore'
|
||||
layout: PostSimple
|
||||
---
|
||||
|
||||
Mon article en français...
|
10
data/blog/new-features-in-v2.mdx
Normal file
10
data/blog/new-features-in-v2.mdx
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: 'New features in v2'
|
||||
date: 2021-17-28T15:32:14Z
|
||||
tags: ['common tag']
|
||||
draft: false
|
||||
summary: 'test for fallback behavior of internatlization'
|
||||
layout: PostSimple
|
||||
---
|
||||
|
||||
## Cc
|
|
@ -1,84 +0,0 @@
|
|||
---
|
||||
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 [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 [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 [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 [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>
|
|
@ -1,238 +0,0 @@
|
|||
---
|
||||
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?”
|
||||
|
||||
“Don’t 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 earth’s 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.
|
||||
|
||||
“It’s 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!”
|
||||
|
||||
“Don’t you think you would attract attention?” said the Medical Man.
|
||||
“Our ancestors had no great tolerance for anachronisms.”
|
||||
|
||||
“One might get one’s 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 one’s 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.
|
||||
|
||||
“Let’s see your experiment anyhow,” said the Psychologist, “though it’s
|
||||
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 he’s 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 Filby’s
|
||||
anecdote collapsed.
|
|
@ -1,20 +1,40 @@
|
|||
const projectsData = [
|
||||
{
|
||||
title: 'A Search Engine',
|
||||
description: `What if you could look up any information in the world? Webpages, images, videos
|
||||
and more. Google has many features to help you find exactly what you're looking
|
||||
for.`,
|
||||
imgSrc: '/static/images/google.png',
|
||||
href: 'https://www.google.com',
|
||||
},
|
||||
{
|
||||
title: 'The Time Machine',
|
||||
description: `Imagine being able to travel back in time or to the future. Simple turn the knob
|
||||
to the desired date and press "Go". No more worrying about lost keys or
|
||||
forgotten handphones with this simple yet affordable solution.`,
|
||||
imgSrc: '/static/images/time-machine.jpg',
|
||||
href: '/blog/the-time-machine',
|
||||
},
|
||||
]
|
||||
const projectsData = {
|
||||
en: [
|
||||
{
|
||||
title: 'A Search Engine',
|
||||
description: `What if you could look up any information in the world? Webpages, images, videos
|
||||
and more. Google has many features to help you find exactly what you're looking
|
||||
for.`,
|
||||
imgSrc: '/static/images/google.png',
|
||||
href: 'https://www.google.com',
|
||||
},
|
||||
{
|
||||
title: 'The Time Machine',
|
||||
description: `Imagine being able to travel back in time or to the future. Simple turn the knob
|
||||
to the desired date and press "Go". No more worrying about lost keys or
|
||||
forgotten handphones with this simple yet affordable solution.`,
|
||||
imgSrc: '/static/images/time-machine.jpg',
|
||||
href: '/blog/the-time-machine',
|
||||
},
|
||||
],
|
||||
fr: [
|
||||
{
|
||||
title: 'Un moteur de recherche',
|
||||
description: `Et si vous pouviez rechercher n'importe quelle information dans le monde ? Pages web, images, vidéos
|
||||
et bien d'autres choses encore. Google propose de nombreuses fonctionnalités pour vous aider à trouver exactement ce que vous recherchez.
|
||||
pour.`,
|
||||
imgSrc: '/static/images/google.png',
|
||||
href: 'https://www.google.com',
|
||||
},
|
||||
{
|
||||
title: 'La machine à remonter le temps',
|
||||
description: `Imaginez que vous puissiez voyager dans le passé ou dans le futur. Il suffit de tourner le bouton
|
||||
sur la date souhaitée et d'appuyer sur "Go". Plus besoin de se soucier des clés perdues ou des téléphones portables oubliés avec cette solution simple et abordable.
|
||||
ou de téléphones portables oubliés grâce à cette solution simple et abordable.`,
|
||||
imgSrc: '/static/images/time-machine.jpg',
|
||||
href: '/blog/the-time-machine',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default projectsData
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
const siteMetadata = {
|
||||
title: 'Next.js Starter Blog',
|
||||
title: { en: 'Next.js Starter Blog', fr: 'Starter de blog NextJs' },
|
||||
author: 'Tails Azimuth',
|
||||
headerTitle: 'TailwindBlog',
|
||||
description: 'A blog created with Next.js and Tailwind.css',
|
||||
headerTitle: { en: 'TailwindBlog', fr: 'blog Tailwind' },
|
||||
// description: 'A blog created with Next.js and Tailwind.css',
|
||||
description: { en: 'en description', fr: 'Description en fr' },
|
||||
language: 'en-us',
|
||||
siteUrl: 'https://tailwind-nextjs-starter-blog.vercel.app',
|
||||
siteRepo: 'https://github.com/timlrx/tailwind-nextjs-starter-blog',
|
||||
|
|
8
i18n.json
Normal file
8
i18n.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"locales": ["en", "fr"],
|
||||
"defaultLocale": "en",
|
||||
"pages": {
|
||||
"*": ["common", "headerNavLinks", "404"],
|
||||
"/projects": ["projects"]
|
||||
}
|
||||
}
|
|
@ -2,16 +2,23 @@ import SocialIcon from '@/components/social-icons'
|
|||
import Image from '@/components/Image'
|
||||
import { PageSeo } from '@/components/SEO'
|
||||
|
||||
export default function AuthorLayout({ children, frontMatter }) {
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export default function AuthorLayout({ children, frontMatter, availableLocales }) {
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSeo title={`About - ${name}`} description={`About me - ${name}`} />
|
||||
<PageSeo
|
||||
title={`About - ${name}`}
|
||||
description={`About me - ${name}`}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<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
|
||||
{t('headerNavLinks:about')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
|
||||
|
|
|
@ -4,6 +4,8 @@ import siteMetadata from '@/data/siteMetadata'
|
|||
import { useState } from 'react'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
|
@ -12,6 +14,9 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
|||
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
|
||||
})
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useRouter()
|
||||
|
||||
// If initialDisplayPosts exist, display it if no searchValue is specified
|
||||
const displayPosts =
|
||||
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
|
||||
|
@ -28,7 +33,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
|||
aria-label="Search articles"
|
||||
type="text"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder="Search articles"
|
||||
placeholder={t('common:search')}
|
||||
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
|
||||
|
@ -55,9 +60,9 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
|
|||
<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>
|
||||
<dt className="sr-only">{t('common:pub')}</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
<time dateTime={date}>{formatDate(date, locale)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-3 xl:col-span-3">
|
||||
|
|
|
@ -6,6 +6,9 @@ import Image from '@/components/Image'
|
|||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import Comments from '@/components/comments'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const editUrl = (fileName) => `${siteMetadata.siteRepo}/blob/master/data/blog/${fileName}`
|
||||
const discussUrl = (slug) =>
|
||||
|
@ -15,14 +18,24 @@ const discussUrl = (slug) =>
|
|||
|
||||
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
|
||||
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||
export default function PostLayout({
|
||||
frontMatter,
|
||||
authorDetails,
|
||||
next,
|
||||
prev,
|
||||
availableLocales,
|
||||
children,
|
||||
}) {
|
||||
const { slug, fileName, date, title, tags } = frontMatter
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useRouter()
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<BlogSeo
|
||||
url={`${siteMetadata.siteUrl}/blog/${slug}`}
|
||||
authorDetails={authorDetails}
|
||||
availableLocales={availableLocales}
|
||||
{...frontMatter}
|
||||
/>
|
||||
<article>
|
||||
|
@ -31,11 +44,9 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
<div className="space-y-1 text-center">
|
||||
<dl className="space-y-10">
|
||||
<div>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dt className="sr-only">{t('common:pub')}</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(new Date(date), locale)}</time>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
@ -49,7 +60,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
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>
|
||||
<dt className="sr-only">{t('common: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">
|
||||
{authorDetails.map((author) => (
|
||||
|
@ -64,7 +75,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
/>
|
||||
)}
|
||||
<dl className="text-sm font-medium leading-5 whitespace-nowrap">
|
||||
<dt className="sr-only">Name</dt>
|
||||
<dt className="sr-only">{t('common:name')}</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
|
||||
<dt className="sr-only">Twitter</dt>
|
||||
<dd>
|
||||
|
@ -87,10 +98,10 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
<div className="pt-10 pb-8 prose dark:prose-dark max-w-none">{children}</div>
|
||||
<div className="pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
<Link href={discussUrl(slug)} rel="nofollow">
|
||||
{'Discuss on Twitter'}
|
||||
{t('common:twitter')}
|
||||
</Link>
|
||||
{` • `}
|
||||
<Link href={editUrl(fileName)}>{'View on GitHub'}</Link>
|
||||
<Link href={editUrl(fileName)}>{t('common:github')}</Link>
|
||||
</div>
|
||||
<Comments frontMatter={frontMatter} />
|
||||
</div>
|
||||
|
@ -113,7 +124,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
{prev && (
|
||||
<div>
|
||||
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
|
||||
Previous Article
|
||||
{t('common:preva')}
|
||||
</h2>
|
||||
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<Link href={`/blog/${prev.slug}`}>{prev.title}</Link>
|
||||
|
@ -123,7 +134,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
{next && (
|
||||
<div>
|
||||
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
|
||||
Next Article
|
||||
{t('common:nexta')}
|
||||
</h2>
|
||||
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
|
||||
<Link href={`/blog/${next.slug}`}>{next.title}</Link>
|
||||
|
@ -138,7 +149,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
|
|||
href="/blog"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
>
|
||||
← Back to the blog
|
||||
← {t('common:back')}
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -5,22 +5,37 @@ import { BlogSeo } from '@/components/SEO'
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import Comments from '@/components/comments'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
|
||||
export default function PostLayout({
|
||||
frontMatter,
|
||||
authorDetails,
|
||||
next,
|
||||
prev,
|
||||
availableLocales,
|
||||
children,
|
||||
}) {
|
||||
const { date, title } = frontMatter
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useRouter()
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<BlogSeo url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
|
||||
<BlogSeo
|
||||
availableLocales={availableLocales}
|
||||
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>
|
||||
<dt className="sr-only">{t('common:pub')}</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
<time dateTime={date}>{formatDate(date, locale)}</time>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
|
|
@ -1,31 +1,37 @@
|
|||
import { escape } from '@/lib/utils/htmlEscaper'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const generateRssItem = (post) => `
|
||||
const generateRssItem = (post, locale, defaultLocale) => `
|
||||
<item>
|
||||
<guid>${siteMetadata.siteUrl}/blog/${post.slug}</guid>
|
||||
<guid>${siteMetadata.siteUrl}${defaultLocale === locale ? '' : '/' + locale}/blog/${
|
||||
post.slug
|
||||
}</guid>
|
||||
<title>${escape(post.title)}</title>
|
||||
<link>${siteMetadata.siteUrl}/blog/${post.slug}</link>
|
||||
<link>${siteMetadata.siteUrl}${defaultLocale === locale ? '' : '/' + locale}/blog/${
|
||||
post.slug
|
||||
}</link>
|
||||
${post.summary && `<description>${escape(post.summary)}</description>`}
|
||||
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
|
||||
<pubDate>${new Date(post.date).toLocaleDateString(locale)}</pubDate>
|
||||
<author>${siteMetadata.email} (${siteMetadata.author})</author>
|
||||
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
|
||||
</item>
|
||||
`
|
||||
|
||||
const generateRss = (posts, page = 'feed.xml') => `
|
||||
const generateRss = (posts, locale, defaultLocale, page = 'feed.xml') => `
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>${escape(siteMetadata.title)}</title>
|
||||
<link>${siteMetadata.siteUrl}/blog</link>
|
||||
<description>${escape(siteMetadata.description)}</description>
|
||||
<language>${siteMetadata.language}</language>
|
||||
<title>${escape(siteMetadata.title[locale])}</title>
|
||||
<link>${siteMetadata.siteUrl}${defaultLocale === locale ? '' : '/' + locale}/blog</link>
|
||||
<description>${escape(siteMetadata.description[locale])}</description>
|
||||
<language>${locale}</language>
|
||||
<managingEditor>${siteMetadata.email} (${siteMetadata.author})</managingEditor>
|
||||
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
|
||||
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
|
||||
<atom:link href="${siteMetadata.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
|
||||
${posts.map(generateRssItem).join('')}
|
||||
<lastBuildDate>${new Date(posts[0].date).toLocaleDateString(locale)}</lastBuildDate>
|
||||
<atom:link href="${siteMetadata.siteUrl}/${page.replace(
|
||||
'.xml',
|
||||
defaultLocale === locale ? '.xml' : '.' + locale + '.xml'
|
||||
)}" rel="self" type="application/rss+xml"/>
|
||||
${posts.map((post) => generateRssItem(post, locale, defaultLocale)).join('')}
|
||||
</channel>
|
||||
</rss>
|
||||
`
|
||||
|
|
38
lib/mdx.js
38
lib/mdx.js
|
@ -25,15 +25,23 @@ const tokenClassNames = {
|
|||
comment: 'text-gray-400 italic',
|
||||
}
|
||||
|
||||
export function getFiles(type) {
|
||||
export function getFiles(type, otherLocale = '') {
|
||||
const prefixPaths = path.join(root, 'data', type)
|
||||
const files = getAllFilesRecursively(prefixPaths)
|
||||
|
||||
const allFiles = getAllFilesRecursively(prefixPaths)
|
||||
const files =
|
||||
otherLocale === ''
|
||||
? getAllFilesRecursively(prefixPaths).filter((path) => (path.match(/\./g) || []).length === 1)
|
||||
: getAllFilesRecursively(prefixPaths).filter((path) => path.includes(`.${otherLocale}.md`))
|
||||
|
||||
// Only want to return blog/path and ignore root, replace is needed to work on Windows
|
||||
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
|
||||
}
|
||||
|
||||
export function formatSlug(slug) {
|
||||
return slug.replace(/\.(mdx|md)/, '')
|
||||
// return slug.replace(/\.(mdx|md)/, '')
|
||||
// take the main root of slug e.g. post-name in post-name.en.mdx
|
||||
return slug.split('.')[0]
|
||||
}
|
||||
|
||||
export function dateSortDesc(a, b) {
|
||||
|
@ -42,9 +50,16 @@ export function dateSortDesc(a, b) {
|
|||
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`)
|
||||
// otherLocale === locale if locale !== defaultLocale
|
||||
export async function getFileBySlug(type, slug, otherLocale = '') {
|
||||
const [mdxPath, mdPath] =
|
||||
otherLocale === ''
|
||||
? [path.join(root, 'data', type, `${slug}.mdx`), path.join(root, 'data', type, `${slug}.md`)]
|
||||
: [
|
||||
path.join(root, 'data', type, `${slug}.${otherLocale}.mdx`),
|
||||
path.join(root, 'data', type, `${slug}.${otherLocale}.md`),
|
||||
]
|
||||
|
||||
const source = fs.existsSync(mdxPath)
|
||||
? fs.readFileSync(mdxPath, 'utf8')
|
||||
: fs.readFileSync(mdPath, 'utf8')
|
||||
|
@ -126,10 +141,17 @@ export async function getFileBySlug(type, slug) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getAllFilesFrontMatter(folder) {
|
||||
// otherLocale === locale if locale !== defaultLocale
|
||||
export async function getAllFilesFrontMatter(folder, otherLocale) {
|
||||
const prefixPaths = path.join(root, 'data', folder)
|
||||
|
||||
const files = getAllFilesRecursively(prefixPaths)
|
||||
const allFiles = getAllFilesRecursively(prefixPaths)
|
||||
const files =
|
||||
otherLocale === ''
|
||||
? getAllFilesRecursively(prefixPaths).filter((path) => (path.match(/\./g) || []).length === 1)
|
||||
: getAllFilesRecursively(prefixPaths).filter((path) => path.includes(`.${otherLocale}.md`))
|
||||
|
||||
// Check if the file exist in the otherlocale. If not, fallback to defaultLangage
|
||||
|
||||
const allFrontMatter = []
|
||||
|
||||
|
|
|
@ -6,9 +6,8 @@ import kebabCase from './utils/kebabCase'
|
|||
|
||||
const root = process.cwd()
|
||||
|
||||
export async function getAllTags(type) {
|
||||
const files = await getFiles(type)
|
||||
|
||||
export async function getAllTags(type, otherLocale) {
|
||||
const files = await getFiles(type, otherLocale)
|
||||
let tagCount = {}
|
||||
// Iterate through each post, putting all found tags into `tags`
|
||||
files.forEach((file) => {
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
const formatDate = (date) => {
|
||||
const formatDate = (date, locale) => {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}
|
||||
const now = new Date(date).toLocaleDateString(siteMetadata.locale, options)
|
||||
const now = new Date(date)
|
||||
.toLocaleDateString(locale, options)
|
||||
.split(' ') // needed to be congruent with en (uperCamelCase)
|
||||
.map((e) => e[0].toUpperCase() + e.substring(1))
|
||||
.join(' ')
|
||||
|
||||
return now
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
const kebabCase = (str) =>
|
||||
str &&
|
||||
str
|
||||
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
||||
.map((x) => x.toLowerCase())
|
||||
.join('-')
|
||||
import { slug } from 'github-slugger'
|
||||
|
||||
const kebabCase = (str) => slug(str)
|
||||
|
||||
export default kebabCase
|
||||
|
|
5
locales/en/404.json
Normal file
5
locales/en/404.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"bigText": "Sorry we couldn't find this page.",
|
||||
"littleText": "But dont worry, you can find plenty of other things on our homepage.",
|
||||
"backButton": "Back to homepage"
|
||||
}
|
18
locales/en/common.json
Normal file
18
locales/en/common.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"greeting": "Latest",
|
||||
"more": "Read more",
|
||||
"all": "All Posts",
|
||||
"search": "Search articles",
|
||||
"comment": "Load Comments",
|
||||
"pub": "Publié le",
|
||||
"authors": "Authors",
|
||||
"name": "Name",
|
||||
"nexta": "Next Article",
|
||||
"preva": "Previous Article",
|
||||
"nextp": "Next",
|
||||
"prevp": "Previous",
|
||||
"of": "of",
|
||||
"back": "Back to the blog",
|
||||
"twitter": "Discuss on Twitter",
|
||||
"github": "View on GitHub"
|
||||
}
|
6
locales/en/headerNavLinks.json
Normal file
6
locales/en/headerNavLinks.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"blog": "Blog",
|
||||
"tags": "Tags",
|
||||
"projects": "Projects",
|
||||
"about": "About"
|
||||
}
|
4
locales/en/projects.json
Normal file
4
locales/en/projects.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"title": "Projects",
|
||||
"subtitle": "Showcase your projects with a hero image (16 x 9)."
|
||||
}
|
5
locales/fr/404.json
Normal file
5
locales/fr/404.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"bigText": "Désolé, nous n'avons pas pu trouver cette page.",
|
||||
"littleText": "Ne vous inquiétez pas, vous pourrez trouver plein d'autres choses sur notre page d'accueil.",
|
||||
"backButton": "Retour à la page d'accueil"
|
||||
}
|
18
locales/fr/common.json
Normal file
18
locales/fr/common.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"greeting": "Les plus récents",
|
||||
"more": "Lire la suite",
|
||||
"all": "Tous Les Articles",
|
||||
"search": "Recherche",
|
||||
"comment": "Charger les commentaires",
|
||||
"pub": "Publié le",
|
||||
"authors": "Auteurs",
|
||||
"name": "Nom",
|
||||
"nexta": "Article Suivant",
|
||||
"preva": "Article Précédent",
|
||||
"nextp": "Suivant",
|
||||
"prevp": "Précédent",
|
||||
"of": "sur",
|
||||
"back": "Retour sur le blog",
|
||||
"twitter": "Discuter sur Twitter",
|
||||
"github": "Voir sur GitHub"
|
||||
}
|
6
locales/fr/headerNavLinks.json
Normal file
6
locales/fr/headerNavLinks.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"blog": "Blog",
|
||||
"tags": "Tags",
|
||||
"projects": "Projets",
|
||||
"about": "À Propos"
|
||||
}
|
4
locales/fr/projects.json
Normal file
4
locales/fr/projects.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"title": "Projets",
|
||||
"subtitle": "Montrez vos projects avec une image (16 x 9)."
|
||||
}
|
|
@ -2,40 +2,44 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|||
enabled: process.env.ANALYZE === 'true',
|
||||
})
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
|
||||
eslint: {
|
||||
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
||||
},
|
||||
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]',
|
||||
const nextTranslate = require('next-translate')
|
||||
|
||||
module.exports = nextTranslate(
|
||||
withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx'],
|
||||
eslint: {
|
||||
dirs: ['pages', 'components', 'lib', 'layouts', 'scripts'],
|
||||
},
|
||||
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) {
|
||||
// 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
|
||||
},
|
||||
})
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
})
|
||||
|
||||
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
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
11497
package-lock.json
generated
11497
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -21,6 +21,7 @@
|
|||
"mdx-bundler": "^5.1.2",
|
||||
"next": "11.0.1",
|
||||
"next-themes": "^0.0.14",
|
||||
"next-translate": "^1.0.7",
|
||||
"postcss": "^8.3.5",
|
||||
"preact": "^10.5.13",
|
||||
"react": "17.0.2",
|
||||
|
|
11
pages/404.js
11
pages/404.js
|
@ -1,6 +1,9 @@
|
|||
import Link from '@/components/Link'
|
||||
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export default function FourZeroFour() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start md:justify-center md:items-center md:flex-row md:space-x-6 md:mt-24">
|
||||
<div className="pt-6 pb-8 space-x-2 md:space-y-5">
|
||||
|
@ -9,13 +12,11 @@ export default function FourZeroFour() {
|
|||
</h1>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
||||
Sorry we couldn't find this page.
|
||||
</p>
|
||||
<p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
|
||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">{t('404:bigText')}</p>
|
||||
<p className="mb-8">{t('404:littleText')}</p>
|
||||
<Link href="/">
|
||||
<button className="inline px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-600 border border-transparent rounded-lg shadow focus:outline-none focus:shadow-outline-blue hover:bg-blue-700 dark:hover:bg-blue-500">
|
||||
Back to homepage
|
||||
{t('404:backButton')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import Head from 'next/head'
|
|||
|
||||
import Analytics from '@/components/analytics'
|
||||
import LayoutWrapper from '@/components/LayoutWrapper'
|
||||
import RSS from '@/components/Rss'
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
|
@ -16,6 +17,7 @@ export default function App({ Component, pageProps }) {
|
|||
<LayoutWrapper>
|
||||
<Component {...pageProps} />
|
||||
</LayoutWrapper>
|
||||
<RSS />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'
|
|||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link
|
||||
|
@ -21,7 +21,6 @@ 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="/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"
|
||||
|
|
|
@ -3,12 +3,13 @@ import { getFileBySlug } from '@/lib/mdx'
|
|||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const authorDetails = await getFileBySlug('authors', ['default'])
|
||||
return { props: { authorDetails } }
|
||||
export async function getStaticProps({ locale, defaultLocale, locales }) {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const authorDetails = await getFileBySlug('authors', [`sparrowhawk`], otherLocale)
|
||||
return { props: { authorDetails, availableLocales: locales } }
|
||||
}
|
||||
|
||||
export default function About({ authorDetails }) {
|
||||
export default function About({ authorDetails, availableLocales }) {
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
|
||||
return (
|
||||
|
@ -16,6 +17,7 @@ export default function About({ authorDetails }) {
|
|||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,28 +3,38 @@ import siteMetadata from '@/data/siteMetadata'
|
|||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { PageSeo } from '@/components/SEO'
|
||||
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export const POSTS_PER_PAGE = 5
|
||||
|
||||
export async function getStaticProps() {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
export async function getStaticProps({ locale, defaultLocale, locales }) {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const posts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
|
||||
const pagination = {
|
||||
currentPage: 1,
|
||||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
|
||||
return { props: { initialDisplayPosts, posts, pagination } }
|
||||
return {
|
||||
props: { initialDisplayPosts, posts, pagination, locale, availableLocales: locales },
|
||||
}
|
||||
}
|
||||
|
||||
export default function Blog({ posts, initialDisplayPosts, pagination }) {
|
||||
export default function Blog({ posts, initialDisplayPosts, pagination, locale, availableLocales }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<PageSeo title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
|
||||
<PageSeo
|
||||
title={`Blog - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description[locale]}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
title={t('common:all')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -6,41 +6,59 @@ import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/l
|
|||
|
||||
const DEFAULT_LAYOUT = 'PostLayout'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = getFiles('blog')
|
||||
export async function getStaticPaths({ locales, defaultLocale }) {
|
||||
const localesPost = locales
|
||||
.map((locale) => {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const posts = getFiles('blog', otherLocale)
|
||||
return posts.map((post) => [post, locale])
|
||||
})
|
||||
.flat()
|
||||
|
||||
return {
|
||||
paths: posts.map((p) => ({
|
||||
paths: localesPost.map(([p, l]) => ({
|
||||
params: {
|
||||
slug: formatSlug(p).split('/'),
|
||||
},
|
||||
locale: l,
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
const allPosts = await getAllFilesFrontMatter('blog')
|
||||
export async function getStaticProps({ defaultLocale, locales, locale, params }) {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const allPosts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/'))
|
||||
const prev = allPosts[postIndex + 1] || null
|
||||
const next = allPosts[postIndex - 1] || null
|
||||
const post = await getFileBySlug('blog', params.slug.join('/'))
|
||||
const post = await getFileBySlug('blog', params.slug.join('/'), otherLocale)
|
||||
const authorList = post.frontMatter.authors || ['default']
|
||||
const authorPromise = authorList.map(async (author) => {
|
||||
const authorResults = await getFileBySlug('authors', [author])
|
||||
const authorResults = await getFileBySlug('authors', [author], otherLocale)
|
||||
return authorResults.frontMatter
|
||||
})
|
||||
const authorDetails = await Promise.all(authorPromise)
|
||||
|
||||
// rss
|
||||
const rss = generateRss(allPosts)
|
||||
fs.writeFileSync('./public/feed.xml', rss)
|
||||
const rss = generateRss(allPosts, locale, defaultLocale)
|
||||
fs.writeFileSync(`./public/feed${otherLocale === '' ? '' : `.${otherLocale}`}.xml`, rss)
|
||||
|
||||
return { props: { post, authorDetails, prev, next } }
|
||||
// Checking if available in other locale for SEO
|
||||
const availableLocales = []
|
||||
await locales.forEach(async (ilocal) => {
|
||||
const otherLocale = ilocal !== defaultLocale ? ilocal : ''
|
||||
const iAllPosts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
iAllPosts.map((ipost) => {
|
||||
if (ipost.slug === post.frontMatter.slug && ipost.slug !== '') availableLocales.push(ilocal)
|
||||
})
|
||||
})
|
||||
|
||||
return { props: { post, authorDetails, prev, next, availableLocales } }
|
||||
}
|
||||
|
||||
export default function Blog({ post, authorDetails, prev, next }) {
|
||||
export default function Blog({ post, authorDetails, prev, next, availableLocales }) {
|
||||
const { mdxSource, toc, frontMatter } = post
|
||||
|
||||
return (
|
||||
<>
|
||||
{frontMatter.draft !== true ? (
|
||||
|
@ -52,6 +70,7 @@ export default function Blog({ post, authorDetails, prev, next }) {
|
|||
authorDetails={authorDetails}
|
||||
prev={prev}
|
||||
next={next}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-24 text-center">
|
||||
|
|
|
@ -4,15 +4,27 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
|
|||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { POSTS_PER_PAGE } from '../../blog'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const totalPosts = await getAllFilesFrontMatter('blog')
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
||||
params: { page: (i + 1).toString() },
|
||||
}))
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { createPortal } from 'react-dom'
|
||||
export async function getStaticPaths({ locales, defaultLocale }) {
|
||||
const paths = (
|
||||
await Promise.all(
|
||||
locales.map(async (locale) => {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const totalPosts = await getAllFilesFrontMatter('blog', otherLocale) // don't forget to useotherLocale
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||
return Array.from({ length: totalPages }, (_, i) => [(i + 1).toString(), locale])
|
||||
})
|
||||
)
|
||||
).flat()
|
||||
|
||||
return {
|
||||
paths,
|
||||
paths: paths.map(([page, locale]) => ({
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
locale,
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +32,12 @@ export async function getStaticPaths() {
|
|||
export async function getStaticProps(context) {
|
||||
const {
|
||||
params: { page },
|
||||
defaultLocale,
|
||||
locales,
|
||||
locale,
|
||||
} = context
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const posts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
const pageNumber = parseInt(page)
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
|
@ -32,24 +48,51 @@ export async function getStaticProps(context) {
|
|||
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
|
||||
}
|
||||
|
||||
// Checking if available in other locale for SEO
|
||||
const availableLocales = []
|
||||
await locales.forEach(async (ilocal) => {
|
||||
const otherLocale = ilocal !== defaultLocale ? ilocal : ''
|
||||
const iAllPosts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
iAllPosts.forEach(() => {
|
||||
if (
|
||||
pageNumber <= Math.ceil(iAllPosts.length / POSTS_PER_PAGE) &&
|
||||
!availableLocales.includes(ilocal)
|
||||
)
|
||||
availableLocales.push(ilocal)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
posts,
|
||||
initialDisplayPosts,
|
||||
pagination,
|
||||
locale,
|
||||
availableLocales,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
|
||||
export default function PostPage({
|
||||
posts,
|
||||
initialDisplayPosts,
|
||||
pagination,
|
||||
locale,
|
||||
availableLocales,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<PageSeo title={siteMetadata.title} description={siteMetadata.description} />
|
||||
<PageSeo
|
||||
title={siteMetadata.title[locale]}
|
||||
description={siteMetadata.description[locale]}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
title={t('common:all')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -4,26 +4,34 @@ import Tag from '@/components/Tag'
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
|
||||
export async function getStaticProps() {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
export async function getStaticProps({ locale, defaultLocale, locales }) {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const posts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
|
||||
return { props: { posts } }
|
||||
return { props: { posts, locale, availableLocales: locales } }
|
||||
}
|
||||
|
||||
export default function Home({ posts }) {
|
||||
export default function Home({ posts, locale, availableLocales }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSeo title={siteMetadata.title} description={siteMetadata.description} />
|
||||
<PageSeo
|
||||
title={siteMetadata.title[locale]}
|
||||
description={siteMetadata.description[locale]}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<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">
|
||||
Latest
|
||||
{t('common:greeting')}
|
||||
</h1>
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
{siteMetadata.description}
|
||||
{siteMetadata.description[locale]}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
|
@ -35,9 +43,9 @@ export default function Home({ posts }) {
|
|||
<article>
|
||||
<div 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>
|
||||
<dt className="sr-only">{t('common:pub')}</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date)}</time>
|
||||
<time dateTime={date}>{formatDate(date, locale)}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="space-y-5 xl:col-span-3">
|
||||
|
@ -67,7 +75,7 @@ export default function Home({ posts }) {
|
|||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label={`Read "${title}"`}
|
||||
>
|
||||
Read more →
|
||||
{t('common:more')} →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -85,7 +93,7 @@ export default function Home({ posts }) {
|
|||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="all posts"
|
||||
>
|
||||
All Posts →
|
||||
{t('common:all')} →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -4,23 +4,33 @@ import Image from '@/components/Image'
|
|||
import Link from '@/components/Link'
|
||||
import Card from '@/components/Card'
|
||||
import { PageSeo } from '@/components/SEO'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export default function Projects() {
|
||||
export async function getStaticProps({ locale, locales }) {
|
||||
return { props: { locale, availableLocales: locales } }
|
||||
}
|
||||
|
||||
export default function Projects({ locale, availableLocales }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<PageSeo title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
|
||||
<PageSeo
|
||||
title={`Projects - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description[locale]}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<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">
|
||||
Projects
|
||||
{t('projects:title')}
|
||||
</h1>
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
Showcase your projects with a hero image (16 x 9)
|
||||
{t('projects:subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="container py-12">
|
||||
<div className="flex flex-wrap -m-4">
|
||||
{projectsData.map((d) => (
|
||||
{projectsData[locale]?.map((d) => (
|
||||
<Card
|
||||
key={d.title}
|
||||
title={d.title}
|
||||
|
|
|
@ -5,17 +5,22 @@ import siteMetadata from '@/data/siteMetadata'
|
|||
import { getAllTags } from '@/lib/tags'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const tags = await getAllTags('blog')
|
||||
export async function getStaticProps({ defaultLocale, locale, locales }) {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const tags = await getAllTags('blog', otherLocale)
|
||||
|
||||
return { props: { tags } }
|
||||
return { props: { tags, availableLocales: locales } }
|
||||
}
|
||||
|
||||
export default function Tags({ tags }) {
|
||||
export default function Tags({ tags, availableLocales }) {
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||
return (
|
||||
<>
|
||||
<PageSeo title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
|
||||
<PageSeo
|
||||
title={`Tags - ${siteMetadata.author}`}
|
||||
description="Things I blog about"
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<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">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PageSeo } from '@/components/SEO'
|
||||
import { TagSeo } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import generateRss from '@/lib/generate-rss'
|
||||
|
@ -10,42 +10,63 @@ import path from 'path'
|
|||
|
||||
const root = process.cwd()
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const tags = await getAllTags('blog')
|
||||
export async function getStaticPaths({ locales, defaultLocale }) {
|
||||
const tags = await Promise.all(
|
||||
locales.map(async (locale) => {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const tags = await getAllTags('blog', otherLocale)
|
||||
return Object.entries(tags).map((k) => [k[0], locale])
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
paths: Object.keys(tags).map((tag) => ({
|
||||
paths: tags.flat().map(([tag, locale]) => ({
|
||||
params: {
|
||||
tag,
|
||||
},
|
||||
locale,
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
const allPosts = await getAllFilesFrontMatter('blog')
|
||||
export async function getStaticProps({ params, defaultLocale, locale, locales }) {
|
||||
const otherLocale = locale !== defaultLocale ? locale : ''
|
||||
const allPosts = await getAllFilesFrontMatter('blog', otherLocale)
|
||||
const filteredPosts = allPosts.filter(
|
||||
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
|
||||
)
|
||||
|
||||
// rss
|
||||
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
|
||||
const rss = generateRss(filteredPosts, locale, defaultLocale, `tags/${params.tag}/feed.xml`)
|
||||
const rssPath = path.join(root, 'public', 'tags', params.tag)
|
||||
fs.mkdirSync(rssPath, { recursive: true })
|
||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
||||
fs.writeFileSync(
|
||||
path.join(rssPath, `feed${otherLocale === '' ? '' : `.${otherLocale}`}.xml`),
|
||||
rss
|
||||
)
|
||||
|
||||
return { props: { posts: filteredPosts, tag: params.tag } }
|
||||
// Checking if available in other locale for SEO
|
||||
const availableLocales = []
|
||||
await locales.forEach(async (ilocal) => {
|
||||
const otherLocale = ilocal !== defaultLocale ? ilocal : ''
|
||||
const itags = await getAllTags('blog', otherLocale)
|
||||
Object.entries(itags).map((itag) => {
|
||||
if (itag[0] === params.tag) availableLocales.push(ilocal)
|
||||
})
|
||||
})
|
||||
|
||||
return { props: { posts: filteredPosts, tag: params.tag, locale, availableLocales } }
|
||||
}
|
||||
|
||||
export default function Tag({ posts, tag }) {
|
||||
// Capitalize first letter and convert space to dash
|
||||
export default function Tag({ posts, tag, locale, availableLocales }) {
|
||||
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
|
||||
return (
|
||||
<>
|
||||
<PageSeo
|
||||
title={`${tag} - ${siteMetadata.title}`}
|
||||
description={`${tag} tags - ${siteMetadata.title}`}
|
||||
<TagSeo
|
||||
title={`${tag} - ${siteMetadata.title[locale]}`}
|
||||
description={`${tag} tags - ${siteMetadata.title[locale]}`}
|
||||
availableLocales={availableLocales}
|
||||
/>
|
||||
<ListLayout posts={posts} title={title} />
|
||||
</>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
const fs = require('fs')
|
||||
const globby = require('globby')
|
||||
const path = require('path')
|
||||
const prettier = require('prettier')
|
||||
const { cpuUsage } = require('process')
|
||||
const siteMetadata = require('../data/siteMetadata')
|
||||
const i18nConfig = require('../i18n.json')
|
||||
|
||||
;(async () => {
|
||||
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
|
||||
|
@ -14,28 +17,104 @@ const siteMetadata = require('../data/siteMetadata')
|
|||
'!pages/api',
|
||||
])
|
||||
|
||||
const { locales, defaultLocale } = i18nConfig
|
||||
|
||||
const pagesWithLoc = pages
|
||||
.map((page) => {
|
||||
if (page.includes('pages')) {
|
||||
return locales.map((locale) => [page, locale])
|
||||
}
|
||||
|
||||
if (page.includes('data') || page.includes('.xml')) {
|
||||
for (let i = 0; i < locales.length; i++) {
|
||||
if (page.includes(`.${locales[i]}.`)) {
|
||||
return [[page, locales[i]]]
|
||||
}
|
||||
}
|
||||
return [[page, defaultLocale]]
|
||||
}
|
||||
|
||||
throw new Error('Sitemap case missing, please check scripts/generate-sitemap.js')
|
||||
})
|
||||
.flat()
|
||||
.map(([page, loc]) => [
|
||||
(page =
|
||||
(loc !== defaultLocale ? `/${loc}` : '') +
|
||||
page
|
||||
.replace('pages/', '/')
|
||||
.replace('data/blog', '/blog')
|
||||
.replace('public/', '/')
|
||||
.replace('.js', '')
|
||||
.replace('.mdx', '')
|
||||
.replace('.md', '')
|
||||
.replace(`.${loc}`, '')
|
||||
.replace('/feed', '')
|
||||
.replace('.xml', '')),
|
||||
loc,
|
||||
false, // Indicate if the element is already present or not
|
||||
])
|
||||
|
||||
if (siteMetadata.siteUrl[siteMetadata.siteUrl.length - 1] == '/') {
|
||||
console.error("/!\\: siteUrl in siteMetadata has an '/' at the end. Please remove it.")
|
||||
}
|
||||
const siteUrl = siteMetadata
|
||||
|
||||
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/blog', '/blog')
|
||||
.replace('public/', '/')
|
||||
.replace('.js', '')
|
||||
.replace('.mdx', '')
|
||||
.replace('.md', '')
|
||||
.replace('/feed.xml', '')
|
||||
const route = path === '/index' ? '' : path
|
||||
if (page === `pages/404.js` || page === `pages/blog/[...slug].js`) {
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd
|
||||
http://www.w3.org/1999/xhtml
|
||||
http://www.w3.org/2002/08/xhtml/xhtml1-strict.xsd">
|
||||
${pagesWithLoc
|
||||
.map(([path, loc, alreadyPresent]) => {
|
||||
// @todo: Can you check especially here ?
|
||||
const route = path.includes('/index') ? path.replace('/index', '') : path
|
||||
if (
|
||||
path.includes(`/404.js`) ||
|
||||
path.includes(`/blog/[...slug].js`) ||
|
||||
alreadyPresent
|
||||
) {
|
||||
// Not sure about the [...slug] condition...
|
||||
return
|
||||
}
|
||||
return `
|
||||
const routeMultiLang = pagesWithLoc.filter(
|
||||
([ipath, iloc, _]) => ipath.replace(`/${iloc}`, '') == path.replace(`/${loc}`, '')
|
||||
)
|
||||
const test = routeMultiLang.filter(([path, loc]) =>
|
||||
loc === defaultLocale ? path : ''
|
||||
)
|
||||
routeMultiLang.map((e) => (e[2] = true)) //making allreadyPresnt to true
|
||||
if (routeMultiLang.length === 1)
|
||||
return `
|
||||
<url>
|
||||
<loc>${siteMetadata.siteUrl}${route}</loc>
|
||||
<loc>${siteUrl}${route}</loc>
|
||||
</url>
|
||||
`
|
||||
return `
|
||||
<url>
|
||||
<loc>${siteUrl}${
|
||||
routeMultiLang.filter(([path, loc]) => (loc === defaultLocale ? path : ''))
|
||||
.length !== 0
|
||||
? routeMultiLang.filter(([path, loc]) =>
|
||||
loc === defaultLocale ? path : ''
|
||||
)[0][0]
|
||||
: routeMultiLang[0][0] // Fallaback in a very particular case where there is two local but not default local
|
||||
}</loc>
|
||||
${routeMultiLang
|
||||
.map(
|
||||
([xe, xloc]) =>
|
||||
` <xhtml:link
|
||||
rel="alternate"
|
||||
hreflang="${xloc}"
|
||||
href="${siteUrl}${xe}"/>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</url>
|
||||
`
|
||||
})
|
||||
.join('')}
|
||||
</urlset>
|
||||
|
|
Loading…
Reference in a new issue