├── .eslintrc.json ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── articles ├── post-one.mdx ├── post-three.mdx └── post-two.mdx ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [slug].js ├── _app.js ├── _document.js └── index.js ├── public ├── favicon.ico └── vercel.svg ├── src ├── components │ └── BlogCard │ │ ├── index.js │ │ └── style │ │ └── blogcard.styled.js └── utils │ └── mdx.js └── styles ├── globals.scss └── variables.scss /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing. 2 | 3 | Hey there, thank you for taking interest in this project. I'd list the steps you can follow to get started in contributing. 4 | 5 | - First, you have to fork this repository. When you do that, you're creating a copy of this repository under your own github account. 6 | - Clone the repository (your own forked version), by doing this... 7 | 8 | ```bash 9 | git clone https://github.com/kaf-lamed-beyt/nextjs-blog-template.git 10 | ``` 11 | 12 | - Set the remote upstream of your forked repository to the base respository (i.e. the original repository) 13 | 14 | ```bash 15 | git remote add upstream https://github.com/kaf-lamed-beyt/nextjs-blog-template.git 16 | ``` 17 | 18 | - Create a branch and start working your feature. 19 | 20 | ```bash 21 | git checkout -b [branch-name] 22 | ``` 23 | 24 | - Save and commit your changes 25 | 26 | ```bash 27 | git add --all 28 | 29 | git commit -m "your message" 30 | ``` 31 | 32 | - Push to your branch, create a pull request and wait for it to be merged. 33 | 34 | ```bash 35 | git push origin [branch name] 36 | ``` 37 | 38 | ## Happy Hacking! 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | If you've always wanted to have a dev blog of your own, this repository is a blog template that I built with Next.js and MDX. 2 | 3 | You can fork or clone the repository and use it and use it as you want. 4 | 5 | ## Overview of the folders and files 6 | 7 | - articles: this directory is where you'll be putting your articles. In it currently are some dummy files `post-one.mdx`, `post-two.mdx`, `post-three.mdx`. 8 | 9 | - src: Is where the magic happens. In it, there are two folders, `components` and `utils`. 10 | 11 | - In the `components` directory, general components like the `BlogCard` component can sit in the root directory of this folder. But, 12 | 13 | - The custom components that are meant for the articles should go into the `mdx-components` directory. This pattern is just so you can keep track of components that can be used without mixing them up. 14 | 15 | You can have components like the custom `codeblock` that holds your code snippets 16 | 17 | ```jsx 18 | import React from "react"; 19 | import propTypes from "prop-types"; 20 | import { Block } from "./style/codeblock.styled.js"; 21 | 22 | const CodeBlock = ({ children, ...props }) => { 23 | return {children}; 24 | }; 25 | 26 | export default CodeBlock; 27 | ``` 28 | 29 | You can add as many custom components that you want in the `src/components/mdx-components` directory. 30 | 31 | The custom MDX components can be imported in `[slug].js` which would eventaully make it available for use in all the `.mdx` files you create in the "articles" directory. 32 | 33 | the generic styles for the codeblock and the unique article itself can be found in `styles/globals.scss` 34 | 35 | - utils: this folder houses the main logic behind this blog template 36 | 37 | - jsconfig.json: gives you the flexibility that doesn't come with traversing the directories in your codebase. With this config file I've been able to create file mappings based on the `baseUrl` of this project. 38 | 39 | So instead of doing something like the snippet below 40 | 41 | ```jsx 42 | import SomeComponent from "./../../path/to/directory/"; 43 | ``` 44 | 45 | You can simply do this. 46 | 47 | ```jsx 48 | import SomeComponent from "@componentDir/component-name"; 49 | ``` 50 | 51 | You can also tweak the config file to your taste. 52 | 53 | ```json 54 | { 55 | "compilerOptions": { 56 | "baseUrl": "./", 57 | "paths": { 58 | "@components/*": ["src/components/*"], 59 | "@mdx-components/*": ["src/components/mdx-components/*"], 60 | "@styles/*": ["/styles/*"], 61 | "@utils/*": ["src/utils/*"] 62 | // you can add your file path mappings here 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | ### The tools used in building this template 69 | 70 | - gray-matter: parses the content in the .mdx files to readable HTML content. 71 | 72 | ```bash 73 | npm install gray-matter 74 | ``` 75 | 76 | - reading-time: assigns an approximate time to read a blog post or article based on the word count. 77 | 78 | ```bash 79 | npm install reading-time 80 | ``` 81 | 82 | - next-mdx-remote: does the background compilation of the MDX files by allowing them to be loaded within Next.js' `getStaticProps` or `getServerSideProps` data-fetching method, and hydrated properly on the client. 83 | 84 | ```bash 85 | npm install next-mdx-remote@3.0.8 86 | ``` 87 | 88 | Check this [github issue](https://github.com/vercel/next.js/issues/36646) to understand why you shouldn't install the latest version 89 | 90 | - glob: gives us access to match the file patterns in data/articles, which we'll be using as the slug of the article. 91 | 92 | ```bash 93 | npm install glob 94 | ``` 95 | 96 | - dayjs: a JavaScript library that helps to parse, manipulate, validate, and display dates that we would be adding to the metadata of each article. 97 | 98 | ```bash 99 | npm install dayjs 100 | ``` 101 | 102 | - rehype-highlight: adds syntax highlighting to our code blocks 103 | 104 | ```bash 105 | npm install rehype-highlight 106 | ``` 107 | 108 | - rehype-autolink-headings: is a plugin that adds links to headings from h1 to h6 109 | 110 | ```bash 111 | npm install rehype-autolink-headings 112 | ``` 113 | 114 | - rehype-code-titles: adds language/file titles to your code snippets 115 | 116 | ```bash 117 | npm install rehype-code-titles 118 | ``` 119 | 120 | - rehype-slug is a plugin that adds an id attributes to headings 121 | 122 | ```bash 123 | npm install rehype-slug 124 | ``` 125 | 126 | ### Want to contribute? 127 | 128 | Read this [guide](CONTRIBUTING.md) to see how you can contribute to this project. 129 | -------------------------------------------------------------------------------- /articles/post-one.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example post one" 3 | publishedAt: "2022-02-16" 4 | excerpt: "example post one" 5 | cover_image: "path/to/where/image/is/stored" 6 | --- 7 | 8 | rest of the content falls here 9 | -------------------------------------------------------------------------------- /articles/post-three.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example post three" 3 | publishedAt: "2022-04-22" 4 | excerpt: "example post three" 5 | cover_image: "path/to/where/image/is/stored" 6 | --- 7 | 8 | rest of the content falls here 9 | 10 | This is an `inline code` 11 | 12 | Below is an example of a codeblock 13 | 14 | ```js 15 | let name = "MDX Blog"; 16 | 17 | console.log(name); 18 | ``` 19 | -------------------------------------------------------------------------------- /articles/post-two.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example post two" 3 | publishedAt: "2022-04-20" 4 | excerpt: "example post two" 5 | cover_image: "path/to/where/image/is/stored" 6 | --- 7 | 8 | rest of the content falls here 9 | 10 | ```jsx 11 | import React from "react"; 12 | 13 | const Hello = ({ text }) => { 14 | return

{text}

; 15 | }; 16 | 17 | export default Hello; 18 | ``` 19 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@components/*": ["src/components/*"], 6 | "@mdx-components/*": ["src/components/mdx-components/*"], 7 | "@styles/*": ["/styles/*"], 8 | "@utils/*": ["src/utils/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | 8 | // module.exports = { 9 | // reactStrictMode: true, 10 | 11 | // webpack: (config) => { 12 | // config.resolve.alias = { 13 | // ...config.resolve.alias, 14 | // "react/jsx-runtime.js": require.resolve("react/jsx-runtime"), 15 | // }; 16 | 17 | // config.resolve = { 18 | // ...config.resolve, 19 | 20 | // fallback: { 21 | // ...config.resolve.fallback, 22 | // child_process: false, 23 | // fs: false, 24 | // // 'builtin-modules': false, 25 | // // worker_threads: false, 26 | // // pnpapi: false, 27 | // }, 28 | // }; 29 | 30 | // return config; 31 | // }, 32 | // }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "dayjs": "^1.11.1", 13 | "glob": "^8.0.1", 14 | "gray-matter": "^4.0.3", 15 | "next": "^12.1.7-canary.0", 16 | "next-mdx-remote": "^3.0.8", 17 | "node-sass": "^7.0.1", 18 | "react": "^18.1.0", 19 | "react-dom": "18.1.0", 20 | "reading-time": "^1.5.0", 21 | "rehype-autolink-headings": "^6.1.1", 22 | "rehype-code-titles": "^1.0.3", 23 | "rehype-highlight": "^5.0.2", 24 | "rehype-slug": "^5.0.1", 25 | "styled-components": "^5.3.5" 26 | }, 27 | "devDependencies": { 28 | "eslint": "8.14.0", 29 | "eslint-config-next": "12.1.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/[slug].js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import React from "react"; 3 | import Head from "next/head"; 4 | import Image from "next/image"; 5 | import rehypeSlug from "rehype-slug"; 6 | import { MDXRemote } from "next-mdx-remote"; 7 | import rehypeHighlight from "rehype-highlight"; 8 | import rehypeCodeTitles from "rehype-code-titles"; 9 | import { serialize } from "next-mdx-remote/serialize"; 10 | import { getSlug, getArticleFromSlug } from "@utils/mdx"; 11 | import "highlight.js/styles/atom-one-dark-reasonable.css"; 12 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 13 | 14 | export default function BlogPost({ post: { source, frontmatter } }) { 15 | return ( 16 | 17 | 18 | {frontmatter.title} | My blog 19 | 20 |
21 |

{frontmatter.title}

22 |

23 | {dayjs(frontmatter.publishedAt).format("MMMM D, YYYY")} —{" "} 24 | {frontmatter.readingTime} 25 |

26 |
27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | export async function getStaticProps({ params }) { 35 | //fetch the particular file based on the slug 36 | const { slug } = params; 37 | const { content, frontmatter } = await getArticleFromSlug(slug); 38 | 39 | const mdxSource = await serialize(content, { 40 | mdxOptions: { 41 | rehypePlugins: [ 42 | rehypeSlug, 43 | [ 44 | rehypeAutolinkHeadings, 45 | { 46 | properties: { className: ["anchor"] }, 47 | }, 48 | { behaviour: "wrap" }, 49 | ], 50 | rehypeHighlight, 51 | rehypeCodeTitles, 52 | ], 53 | }, 54 | }); 55 | 56 | return { 57 | props: { 58 | post: { 59 | source: mdxSource, 60 | frontmatter, 61 | }, 62 | }, 63 | }; 64 | } 65 | 66 | // dynamically generate the slugs for each article(s) 67 | export async function getStaticPaths() { 68 | // getting all paths of each article as an array of 69 | // objects with their unique slugs 70 | const paths = (await getSlug()).map((slug) => ({ params: { slug } })); 71 | 72 | return { 73 | paths, 74 | // in situations where you try to access a path 75 | // that does not exist. it'll return a 404 page 76 | fallback: false, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@styles/globals.scss"; 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document from "next/document"; 2 | import { ServerStyleSheet } from "styled-components"; 3 | 4 | export default class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const sheet = new ServerStyleSheet(); 7 | const originalRenderPage = ctx.renderPage; 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: (App) => (props) => 13 | sheet.collectStyles(), 14 | }); 15 | 16 | const initialProps = await Document.getInitialProps(ctx); 17 | return { 18 | ...initialProps, 19 | styles: ( 20 | <> 21 | {initialProps.styles} 22 | {sheet.getStyleElement()} 23 | 24 | ), 25 | }; 26 | } finally { 27 | sheet.seal(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { getAllArticles } from "@utils/mdx"; 4 | import BlogCard from "@components/BlogCard"; 5 | import styled from "styled-components"; 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | flex-flow: column; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | width: 100%; 13 | height: 100%; 14 | padding: 0 180px; 15 | padding-top: 100px; 16 | padding-bottom: 50px; 17 | 18 | @media only screen and (min-width: 0px) and (max-width: 576px) { 19 | padding: 75px 10px; 20 | } 21 | 22 | @media only screen and (min-width: 576px) and (max-width: 992px) { 23 | padding: 105px 9px; 24 | } 25 | `; 26 | 27 | export default function Blog({ posts }) { 28 | return ( 29 | 30 | 31 | My blog 32 | 33 | 34 | {posts.map((frontmatter) => { 35 | return ; 36 | })} 37 | 38 | 39 | ); 40 | } 41 | 42 | export async function getStaticProps() { 43 | let articles = await getAllArticles(); 44 | 45 | const sortedArticles = articles.map((article) => article); 46 | 47 | sortedArticles.sort((a, b) => { 48 | return new Date(b.publishedAt) - new Date(a.publishedAt); 49 | }); 50 | 51 | return { 52 | props: { 53 | posts: sortedArticles, 54 | }, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaf-lamed-beyt/nextjs-blog-template/2cd18e2b1734e8cfa7b917ea80d95f0b8ffba804/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/BlogCard/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import dayjs from "dayjs"; 4 | import { Card } from "./style/blogcard.styled"; 5 | 6 | const BlogCard = ({ data }) => { 7 | return ( 8 | <> 9 | 10 | 11 |

{data.title}

12 |

{data.excerpt}

13 |

14 | {dayjs(data.publishedAt).format("MMMM D, YYYY")} —{" "} 15 | {data.readingTime} 16 |

17 |
18 | 19 | 20 | ); 21 | }; 22 | 23 | export default BlogCard; 24 | -------------------------------------------------------------------------------- /src/components/BlogCard/style/blogcard.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Card = styled.div` 4 | width: 100%; 5 | height: 100%; 6 | margin-top: -10px; 7 | padding: 10px 18px; 8 | margin-bottom: 34px; 9 | 10 | .title { 11 | color: #000; 12 | font-size: 45px; 13 | padding: 12px 0; 14 | } 15 | 16 | .summary { 17 | font-size: 16px; 18 | padding: 14px 0; 19 | } 20 | 21 | .foot-info { 22 | display: flex; 23 | justify-content: space-between; 24 | padding: 15px 0 5px 0; 25 | 26 | ul { 27 | display: flex; 28 | 29 | li { 30 | list-style: none; 31 | padding: 0px 8px 0 0; 32 | } 33 | } 34 | } 35 | 36 | :hover { 37 | cursor: pointer; 38 | } 39 | 40 | @media only screen and (min-width: 0px) and (max-width: 576px) { 41 | width: 100%; 42 | padding: 10px 0px; 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /src/utils/mdx.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import matter from "gray-matter"; 4 | import readingTime from "reading-time"; 5 | import { sync } from "glob"; 6 | 7 | const articlesPath = path.join(process.cwd(), "articles"); 8 | 9 | // get unique article, when it is clicked on, by the user 10 | // on the blog page 11 | export async function getSlug() { 12 | const paths = sync(`${articlesPath}/*.mdx`); 13 | 14 | return paths.map((path) => { 15 | // holds the paths to the directory of the article 16 | const parts = path.split("/"); 17 | const fileName = parts[parts.length - 1]; // gets the last part of path with /name.mdx 18 | const [slug, _extension] = fileName.split("."); 19 | 20 | return slug; 21 | }); 22 | } 23 | 24 | export async function getArticleFromSlug(slug) { 25 | const articleDir = path.join(articlesPath, `${slug}.mdx`); 26 | const source = fs.readFileSync(articleDir); 27 | const { content, data } = matter(source); 28 | 29 | // console.log(data) 30 | 31 | return { 32 | content, 33 | frontmatter: { 34 | slug, 35 | excerpt: data.excerpt, 36 | title: data.title, 37 | publishedAt: data.publishedAt, 38 | readingTime: readingTime(source).text, 39 | ...data, 40 | }, 41 | }; 42 | } 43 | 44 | // get the path that stores all the articles or blog post 45 | export async function getAllArticles() { 46 | const articles = fs.readdirSync(path.join(process.cwd(), "/articles")); 47 | 48 | return articles.reduce((allArticles, articleSlug) => { 49 | // get parsed data from mdx files in the "articles" dir 50 | const source = fs.readFileSync( 51 | path.join(process.cwd(), "/articles", articleSlug), 52 | "utf-8" 53 | ); 54 | const { data } = matter(source); 55 | 56 | return [ 57 | { 58 | ...data, 59 | slug: articleSlug.replace(".mdx", ""), 60 | readingTime: readingTime(source).text, 61 | }, 62 | ...allArticles, 63 | ]; 64 | }, []); 65 | } 66 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@200;300;400;500;600;700;800;900&display=swap"); 2 | @import "./variables.scss"; 3 | 4 | * { 5 | box-sizing: border-box; 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | html, 11 | body { 12 | font-family: var(--montserrat-alt); 13 | } 14 | 15 | a { 16 | color: inherit; 17 | text-decoration: none; 18 | } 19 | 20 | // global styles that'll be used for the articles 21 | .article-container { 22 | padding: 20px 300px; 23 | margin-top: 50px; 24 | 25 | .article-title { 26 | font-size: 60px; 27 | } 28 | 29 | .content { 30 | padding: 50px 0; 31 | 32 | p { 33 | line-height: 37px; 34 | } 35 | 36 | pre { 37 | width: 100%; 38 | height: 100%; 39 | background: var(--lght-grey); 40 | padding: 25px 20px; 41 | margin: 20px 0; 42 | border-radius: 7px; 43 | overflow: auto; 44 | 45 | code { 46 | font-family: var(--fira-code); 47 | counter-reset: line; 48 | color: #fff; 49 | line-height: 20px; 50 | } 51 | 52 | @media only screen and (min-width: 0px) and (max-width: 576px) { 53 | overflow: auto; 54 | padding: 25px 14px; 55 | 56 | ::-webkit-scrollbar { 57 | width: 0 !important; 58 | } 59 | } 60 | 61 | @media only screen and (min-width: 992px) and (max-width: 1024px) { 62 | overflow: auto; 63 | } 64 | } 65 | 66 | code { 67 | background: var(--grey); 68 | padding: 5px 3px; 69 | border-radius: 3px; 70 | color: #fff; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /styles/variables.scss: -------------------------------------------------------------------------------- 1 | // you can add your css variables here 2 | :root { 3 | // font families 4 | --montserrat-alt: "Montserrat Alternates", sans-serif; 5 | 6 | // colors 7 | --dark-primary: #000009; 8 | --lght-grey: #100f0f; 9 | --lght-grey-dark: #0e0e0e; 10 | --grey: #0f0f0f; 11 | --alt-text: rgba(189, 181, 181, 0.804); 12 | --text-primary: #fff; 13 | --secondary: ghostwhite; 14 | } 15 | --------------------------------------------------------------------------------