diff --git a/.gitignore b/.gitignore index 0bc3064..be0f305 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ public/sitemap.xml # production /build *.xml +# rss feed +/public/index.xml # misc .DS_Store diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/README.md b/README.md index f8c2abe..a8eefe9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea - Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization) - Flexible data retrieval with [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) - Support for tags - each unique tag will be its own page +- Support for nested routing of blog posts - Projects page - SEO friendly with RSS feed, sitemaps and more! @@ -44,6 +45,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea - [A tour of math typesetting](https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator) - [Simple MDX image grid](https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-canada) - [Example of long prose](https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine) +- [Example of Nested Route Post](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing) ## Quick Start Guide diff --git a/components/Tag.js b/components/Tag.js index 94192f4..6138210 100644 --- a/components/Tag.js +++ b/components/Tag.js @@ -1,5 +1,5 @@ import Link from 'next/link' -import { kebabCase } from '@/lib/utils' +import kebabCase from '@/lib/utils/kebabCase' const Tag = ({ text }) => { return ( diff --git a/data/blog/introducing-tailwind-nextjs-starter-blog.mdx b/data/blog/introducing-tailwind-nextjs-starter-blog.mdx index f8a0e5d..ce85691 100644 --- a/data/blog/introducing-tailwind-nextjs-starter-blog.mdx +++ b/data/blog/introducing-tailwind-nextjs-starter-blog.mdx @@ -1,7 +1,7 @@ --- title: 'Introducing Tailwind Nexjs Starter Blog' date: '2021-01-12' -lastmod: '2021-01-18' +lastmod: '2021-05-08' tags: ['next-js', 'tailwind', 'guide'] draft: false summary: 'Looking for a performant, out of the box template, with all the best in web technology to support your blogging needs? Checkout the Tailwind Nextjs Starter Blog template.' @@ -44,6 +44,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea - Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization) - Flexible data retrieval with [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) - Support for tags - each unique tag will be its own page +- Support for nested routing of blog posts - SEO friendly with RSS feed, sitemaps and more! ## Sample posts @@ -53,6 +54,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea - [A tour of math typesetting](/blog/deriving-ols-estimator) - [Simple MDX image grid](/blog/pictures-of-canada) - [Example of long prose](/blog/the-time-machine) +- [Example of Nested Route Post](/blog/nested-route/introducing-multi-part-posts-with-nested-routing) ## Quick Start Guide diff --git a/data/blog/nested-route/introducing-multi-part-posts-with-nested-routing.md b/data/blog/nested-route/introducing-multi-part-posts-with-nested-routing.md new file mode 100644 index 0000000..478d242 --- /dev/null +++ b/data/blog/nested-route/introducing-multi-part-posts-with-nested-routing.md @@ -0,0 +1,30 @@ +--- +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. diff --git a/lib/mdx.js b/lib/mdx.js index fb3686d..6ec4fad 100644 --- a/lib/mdx.js +++ b/lib/mdx.js @@ -1,12 +1,12 @@ +import MDXComponents from '@/components/MDXComponents' import fs from 'fs' import matter from 'gray-matter' -import visit from 'unist-util-visit' +import renderToString from 'next-mdx-remote/render-to-string' import path from 'path' import readingTime from 'reading-time' -import renderToString from 'next-mdx-remote/render-to-string' - -import MDXComponents from '@/components/MDXComponents' +import visit from 'unist-util-visit' import imgToJsx from './img-to-jsx' +import getAllFilesRecursively from './utils/files' const root = process.cwd() @@ -24,8 +24,11 @@ const tokenClassNames = { comment: 'text-gray-400 italic', } -export async function getFiles(type) { - return fs.readdirSync(path.join(root, 'data', type)) +export function getFiles(type) { + const prefixPaths = path.join(root, 'data', type) + const files = getAllFilesRecursively(prefixPaths) + // Only want to return blog/path and ignore root + return files.map(file => file.slice(prefixPaths.length + 1)) } export function formatSlug(slug) { @@ -39,8 +42,8 @@ export function dateSortDesc(a, b) { } export async function getFileBySlug(type, slug) { - const mdxPath = path.join(root, 'data', type, `${slug}.mdx`) - const mdPath = path.join(root, 'data', type, `${slug}.md`) + const mdxPath = path.join(root, 'data', type, `${slug.join('/')}.mdx`) + const mdPath = path.join(root, 'data', type, `${slug.join('/')}.md`) const source = fs.existsSync(mdxPath) ? fs.readFileSync(mdxPath, 'utf8') : fs.readFileSync(mdPath, 'utf8') @@ -86,16 +89,19 @@ export async function getFileBySlug(type, slug) { } } -export async function getAllFilesFrontMatter(type) { - const files = fs.readdirSync(path.join(root, 'data', type)) +export async function getAllFilesFrontMatter(folder) { + const prefixPaths = path.join(root, 'data', folder) + + const files = getAllFilesRecursively(prefixPaths) const allFrontMatter = [] files.forEach((file) => { - const source = fs.readFileSync(path.join(root, 'data', type, file), 'utf8') + const fileName = file.slice(prefixPaths.length + 1) + const source = fs.readFileSync(file, 'utf8') const { data } = matter(source) if (data.draft !== true) { - allFrontMatter.push({ ...data, slug: formatSlug(file) }) + allFrontMatter.push({ ...data, slug: formatSlug(fileName) }) } }) diff --git a/lib/tags.js b/lib/tags.js index 091e03d..d32c2bc 100644 --- a/lib/tags.js +++ b/lib/tags.js @@ -1,12 +1,13 @@ import fs from 'fs' import matter from 'gray-matter' import path from 'path' -import { kebabCase } from './utils' +import { getFiles } from './mdx' +import kebabCase from './utils/kebabCase' const root = process.cwd() export async function getAllTags(type) { - const files = fs.readdirSync(path.join(root, 'data', type)) + const files = await getFiles(type) let tagCount = {} // Iterate through each post, putting all found tags into `tags` diff --git a/lib/utils/files.js b/lib/utils/files.js new file mode 100644 index 0000000..205a514 --- /dev/null +++ b/lib/utils/files.js @@ -0,0 +1,20 @@ +import fs from 'fs' +import path from 'path' + +const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x) + +const flatternArray = (input) => + input.reduce((acc, item) => [...acc, ...(Array.isArray(item) ? item : [item])], []) + +const map = (fn) => (input) => input.map(fn) + +const walkDir = (fullPath) => { + return fs.statSync(fullPath).isFile() ? fullPath : getAllFilesRecursively(fullPath) +} + +const pathJoinPrefix = (prefix) => (extraPath) => path.join(prefix, extraPath) + +const getAllFilesRecursively = (folder) => + pipe(fs.readdirSync, map(pipe(pathJoinPrefix(folder), walkDir)), flatternArray)(folder) + +export default getAllFilesRecursively diff --git a/lib/utils.js b/lib/utils/kebabCase.js similarity index 73% rename from lib/utils.js rename to lib/utils/kebabCase.js index d2dc4c0..a135440 100644 --- a/lib/utils.js +++ b/lib/utils/kebabCase.js @@ -1,6 +1,8 @@ -export const kebabCase = (str) => +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('-') + +export default kebabCase diff --git a/pages/blog/[slug].js b/pages/blog/[...slug].js similarity index 84% rename from pages/blog/[slug].js rename to pages/blog/[...slug].js index 0985dd7..1fbfff8 100644 --- a/pages/blog/[slug].js +++ b/pages/blog/[...slug].js @@ -1,18 +1,17 @@ -import fs from 'fs' -import hydrate from 'next-mdx-remote/hydrate' -import { getFiles, getFileBySlug, getAllFilesFrontMatter, formatSlug } from '@/lib/mdx' -import PostLayout from '@/layouts/PostLayout' import MDXComponents from '@/components/MDXComponents' import PageTitle from '@/components/PageTitle' +import PostLayout from '@/layouts/PostLayout' import generateRss from '@/lib/generate-rss' +import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx' +import fs from 'fs' +import hydrate from 'next-mdx-remote/hydrate' export async function getStaticPaths() { - const posts = await getFiles('blog') - + const posts = getFiles('blog') return { paths: posts.map((p) => ({ params: { - slug: formatSlug(p), + slug: formatSlug(p).split('/'), }, })), fallback: false, @@ -21,7 +20,7 @@ export async function getStaticPaths() { export async function getStaticProps({ params }) { const allPosts = await getAllFilesFrontMatter('blog') - const postIndex = allPosts.findIndex((post) => post.slug === params.slug) + 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) diff --git a/pages/tags.js b/pages/tags.js index 4b6bd49..fc51172 100644 --- a/pages/tags.js +++ b/pages/tags.js @@ -1,9 +1,9 @@ -import siteMetadata from '@/data/siteMetadata' -import { kebabCase } from '@/lib/utils' -import { getAllTags } from '@/lib/tags' -import Tag from '@/components/Tag' import Link from '@/components/Link' import { PageSeo } from '@/components/SEO' +import Tag from '@/components/Tag' +import siteMetadata from '@/data/siteMetadata' +import { getAllTags } from '@/lib/tags' +import kebabCase from '@/lib/utils/kebabCase' export async function getStaticProps() { const tags = await getAllTags('blog') diff --git a/pages/tags/[tag].js b/pages/tags/[tag].js index d1ef59c..c4d0791 100644 --- a/pages/tags/[tag].js +++ b/pages/tags/[tag].js @@ -1,12 +1,12 @@ -import fs from 'fs' -import path from 'path' -import { kebabCase } from '@/lib/utils' -import { getAllFilesFrontMatter } from '@/lib/mdx' -import { getAllTags } from '@/lib/tags' +import { PageSeo } from '@/components/SEO' import siteMetadata from '@/data/siteMetadata' import ListLayout from '@/layouts/ListLayout' -import { PageSeo } from '@/components/SEO' import generateRss from '@/lib/generate-rss' +import { getAllFilesFrontMatter } from '@/lib/mdx' +import { getAllTags } from '@/lib/tags' +import kebabCase from '@/lib/utils/kebabCase' +import fs from 'fs' +import path from 'path' const root = process.cwd() diff --git a/public/index.xml b/public/index.xml deleted file mode 100644 index 5e28141..0000000 --- a/public/index.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - Next.js Starter Blog - https://tailwind-nextjs-starter-blog.vercel.app/blog - A blog created with Next.js and Tailwind.css - en-us - address@yoursite.com (Tails Azimuth) - address@yoursite.com (Tails Azimuth) - Tue, 12 Jan 2021 00:00:00 GMT - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/introducing-tailwind-nextjs-starter-blog - Introducing Tailwind Nexjs Starter Blog - https://tailwind-nextjs-starter-blog.vercel.app/blog/introducing-tailwind-nextjs-starter-blog - Looking for a performant, out of the box template, with all the best in web technology to support your blogging needs? Checkout the Tailwind Nextjs Starter Blog template. - Tue, 12 Jan 2021 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - next-jstailwindguide - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs - Images in Next.js - https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs - In this article we introduce adding images in the tailwind starter blog and the benefits and limitations of the next/image component. - Wed, 11 Nov 2020 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - next jsguide - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator - Deriving the OLS Estimator - https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator - How to derive the OLS Estimator with matrix notation and a tour of math typesetting using markdown with the help of KaTeX. - Sat, 16 Nov 2019 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - next jsmathols - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide - Markdown Guide - https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide - Markdown cheatsheet for all your blogging needs - headers, lists, images, tables and more! An illustrated guide based on Github Flavored Markdown. - Fri, 11 Oct 2019 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - githubguide - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine - The Time Machine - https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine - 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... - Wed, 15 Aug 2018 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - writingsbookreflection - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-canada - O Canada - https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-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. - Sat, 15 Jul 2017 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - holidaycanadaimages - - - - https://tailwind-nextjs-starter-blog.vercel.app/blog/code-sample - Sample .md file - https://tailwind-nextjs-starter-blog.vercel.app/blog/code-sample - Example of a markdown file with code blocks and syntax highlighting - Tue, 08 Mar 2016 00:00:00 GMT - address@yoursite.com (Tails Azimuth) - markdowncodefeatures - - - -