├── .gitignore ├── LICENSE ├── README.md ├── assets ├── css │ ├── base.css │ ├── components.css │ └── main.css └── svg │ └── rss.svg ├── components └── common │ ├── Bio │ ├── Bio.js │ └── index.js │ ├── Layout │ ├── Layout.js │ └── index.js │ ├── Seo │ ├── Seo.js │ └── index.js │ └── index.js ├── config └── seo.json ├── content ├── assets │ ├── cat.jpg │ └── profile.png └── posts │ ├── coding-post │ └── coding-post.md │ ├── first-post │ └── first-post.md │ ├── long-post │ └── long-post.md │ └── second-post │ └── second-post.md ├── jsconfig.json ├── netlify.toml ├── next.config.js ├── package.json ├── pages ├── _app.js ├── _document.js ├── index.js └── posts │ └── [slug].js ├── postcss.config.js ├── public ├── favicon.ico ├── rss.xml └── zeit.svg ├── tailwind.config.js ├── utils ├── helpers.js ├── posts.js └── rss.js └── yarn.lock /.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 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jose Felix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Next.js blog starter 3 |

4 | 5 | Start off your writing journey with this Next.js markdown blog template. 6 | 7 | ## ✏ Features 8 | 9 | - Next.js 12! 10 | - RSS Feed 11 | - Streamlined styling experience with [Tailwind.css](https://tailwindcss.com/). 12 | - Customizable typographic defaults with [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) 13 | - Automatic image preview and optimization with [next-optimized-images](https://github.com/cyrilwanner/next-optimized-images). 14 | - Lazyload images. 15 | - Absolute imports. 16 | - SEO friendly. 17 | - Markdown code highlighting with [react-syntax-highlighter](https://www.npmjs.com/package/react-syntax-highlighter) and [PrismJs](https://prismjs.com/). 18 | - Dark Mode 19 | - WebP image support 20 | 21 | ## 🚀 Getting Started 22 | 23 | First, run the development server: 24 | 25 | ```bash 26 | npm run dev 27 | # or 28 | yarn dev 29 | ``` 30 | 31 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 32 | 33 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 34 | 35 | ## ✍ Customizing Tailwind Typography 36 | 37 | [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) is an official tailwind plugin that provides a set of `prose` classes to add beautiful typographic defaults to any vanilla HTML you don't control (like HTML rendered from Markdown, or pulled from a CMS). 38 | 39 | To customize the defaults provided by the plugin, add the overrides under the `typography` key in the theme section of the `tailwind.config.js` file. Refer to its [default styles](https://github.com/tailwindlabs/tailwindcss-typography/blob/master/src/styles.js) for more in-depth examples. 40 | 41 | For more information, please check out Tailwind Typography's [customization section](https://github.com/tailwindlabs/tailwindcss-typography#customization). 42 | 43 | ## 📖 Learn More 44 | 45 | ### Next.js 46 | 47 | To learn more about Next.js, take a look at the following resources: 48 | 49 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 50 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 51 | 52 | You can check out [the Next.js GitHub repository](https://github.com/zeit/next.js/) - your feedback and contributions are welcome! 53 | 54 | ### Tailwind CSS 55 | 56 | To learn more about Tailwind CSS, take a look at the following resources: 57 | 58 | - [Tailwind Documentation](https://tailwindcss.com/) - learn about Tailwind CSS features and API. 59 | 60 | ## ☑ Upcoming features 61 | 62 | - [ ] Add Sitemap 63 | - [x] Add RSS Feed 64 | - [x] Dark Mode 65 | - [x] Add support for WebP images 66 | - [x] Add SEO Component 67 | - [x] Add Dynamic Site Metadata 68 | 69 | ## ☁ Deploy 70 | 71 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/Jfelix61/nextjs-starter-blog) 72 | 73 | [![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/import/project?template=https://github.com/Jfelix61/nextjs-starter-blog) 74 | -------------------------------------------------------------------------------- /assets/css/base.css: -------------------------------------------------------------------------------- 1 | a:hover { 2 | @apply underline; 3 | } 4 | 5 | span, 6 | p { 7 | @apply font-light; 8 | } 9 | -------------------------------------------------------------------------------- /assets/css/components.css: -------------------------------------------------------------------------------- 1 | .blur { 2 | -webkit-filter: blur(5px); 3 | filter: blur(5px); 4 | transition: filter 500ms ease-in, -webkit-filter 500ms ease-in; 5 | } 6 | 7 | .blur.lazyloaded { 8 | -webkit-filter: blur(0); 9 | filter: blur(0); 10 | } 11 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | @import "./base.css"; 7 | 8 | a { 9 | @apply text-neon-orange dark:text-yellow-500; 10 | } 11 | } 12 | 13 | @layer components { 14 | @import "./components.css"; 15 | } 16 | -------------------------------------------------------------------------------- /assets/svg/rss.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /components/common/Bio/Bio.js: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | import Image from "next/image"; 4 | 5 | import { getSiteMetaData } from "@utils/helpers"; 6 | import profilePicture from "@/content/assets/profile.png"; 7 | 8 | export function Bio({ className }) { 9 | const { author, social } = getSiteMetaData(); 10 | 11 | return ( 12 |
13 |
14 | Profile 21 |
22 | 23 |

24 | Written by {author.name}{" "} 25 | {author.summary}{" "} 26 | 27 | Follow him on twitter 28 | 29 |

30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/common/Bio/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Bio"; 2 | -------------------------------------------------------------------------------- /components/common/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import clsx from "clsx"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | import { DarkModeSwitch } from "react-toggle-dark-mode"; 6 | import { useTheme } from "next-themes"; 7 | import Image from "next/image"; 8 | 9 | import RSSIcon from "@assets/svg/rss.svg"; 10 | 11 | export function Layout({ children }) { 12 | return ( 13 |
14 |
15 |
16 |
{children}
17 | 22 |
23 |
24 | ); 25 | } 26 | 27 | const Header = () => { 28 | const { setTheme, resolvedTheme } = useTheme(); 29 | const { pathname } = useRouter(); 30 | const [mounted, setMounted] = useState(false); 31 | 32 | useEffect(() => setMounted(true), []); 33 | 34 | const toggleDarkMode = (checked) => { 35 | const isDarkMode = checked; 36 | 37 | if (isDarkMode) setTheme("dark"); 38 | else setTheme("light"); 39 | }; 40 | 41 | const isRoot = pathname === "/"; 42 | const isDarkMode = resolvedTheme === "dark"; 43 | 44 | return ( 45 |
51 |
52 | {isRoot ? : } 53 |
54 | {mounted && ( 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | )} 64 |
65 | ); 66 | }; 67 | 68 | const LargeTitle = () => ( 69 |

70 | 71 | 78 | Next.Js Starter Blog 79 | 80 | 81 |

82 | ); 83 | 84 | const SmallTitle = () => ( 85 |

86 | 87 | 93 | Next.Js Starter Blog 94 | 95 | 96 |

97 | ); 98 | -------------------------------------------------------------------------------- /components/common/Layout/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Layout"; 2 | -------------------------------------------------------------------------------- /components/common/Seo/Seo.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | import { getSiteMetaData } from "@utils/helpers"; 4 | 5 | export function SEO({ title, description = "" }) { 6 | const siteMetadata = getSiteMetaData(); 7 | 8 | const metaDescription = description || siteMetadata.description; 9 | 10 | return ( 11 | 12 | 13 | {title} | {siteMetadata.title} 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/common/Seo/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Seo"; 2 | -------------------------------------------------------------------------------- /components/common/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Bio"; 2 | export * from "./Layout"; 3 | export * from "./Seo"; 4 | -------------------------------------------------------------------------------- /config/seo.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteMetadata": { 3 | "title": "Next.js Starter Blog", 4 | "author": { 5 | "name": "Jose Felix", 6 | "summary": "who works building clean user interfaces with React.", 7 | "link": "https://jfelix.info" 8 | }, 9 | "description": "A blog created with Next.js and Tailwind.css", 10 | "siteUrl": "https://nextjs-starter-blog-new-demo.vercel.app/", 11 | "language": "en-US", 12 | "social": { 13 | "twitter": "Jose_R_Felix" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /content/assets/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoseRFelix/nextjs-starter-blog/b01937fd7b0d2b7b565eff269e67101b115bdb52/content/assets/cat.jpg -------------------------------------------------------------------------------- /content/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoseRFelix/nextjs-starter-blog/b01937fd7b0d2b7b565eff269e67101b115bdb52/content/assets/profile.png -------------------------------------------------------------------------------- /content/posts/coding-post/coding-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Coding Post 3 | description: Coding is such a blissful activity. 4 | date: 2020-04-19T11:00:00.000Z 5 | --- 6 | 7 | ```jsx 8 | import React from "react"; 9 | 10 | const CoolComponent = () =>
I'm a cool component!!
; 11 | 12 | export default CoolComponent; 13 | ``` 14 | -------------------------------------------------------------------------------- /content/posts/first-post/first-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: First post 3 | description: The first post is the most memorable one. 4 | date: 2020-04-16T11:00:00.000Z 5 | --- 6 | 7 | # h1 8 | 9 | ## h2 10 | 11 | ### h3 12 | 13 | Normal text 14 | 15 | ![Cat](cat.jpg) 16 | -------------------------------------------------------------------------------- /content/posts/long-post/long-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Long Post 3 | date: 2020-07-28T22:40:32.169Z 4 | description: Who doesn't like a long post showcasing the different headings? 5 | --- 6 | 7 | Far far away, behind the word mountains, far from the countries Vokalia and 8 | Consonantia, there live the blind texts. Separated they live in Bookmarksgrove 9 | right at the coast of the Semantics, a large language ocean. A small river named 10 | Duden flows by their place and supplies it with the necessary regelialia. 11 | 12 | ## On deer horse aboard tritely yikes and much 13 | 14 | The Big Oxmox advised her not to do so, because there were thousands of bad 15 | Commas, wild Question Marks and devious Semikoli, but the Little Blind Text 16 | didn’t listen. She packed her seven versalia, put her initial into the belt and 17 | made herself on the way. 18 | 19 | - This however showed weasel 20 | - Well uncritical so misled 21 | - this is very interesting 22 | - Goodness much until that fluid owl 23 | 24 | When she reached the first hills of the **Italic Mountains**, she had a last 25 | view back on the skyline of her hometown _Bookmarksgrove_, the headline of 26 | [Alphabet Village](http://google.com) and the subline of her own road, the Line 27 | Lane. Pityful a rhetoric question ran over her cheek, then she continued her 28 | way. On her way she met a copy. 29 | 30 | ### Overlaid the jeepers uselessly much excluding 31 | 32 | But nothing the copy said could convince her and so it didn’t take long until a 33 | few insidious Copy Writers ambushed her, made her drunk with 34 | [Longe and Parole](http://google.com) and dragged her into their agency, where 35 | they abused her for their projects again and again. And if she hasn’t been 36 | rewritten, then they are still using her. 37 | 38 | > Far far away, behind the word mountains, far from the countries Vokalia and 39 | > Consonantia, there live the blind texts. Separated they live in Bookmarksgrove 40 | > right at the coast of the Semantics, a large language ocean. 41 | 42 | It is a paradisematic country, in which roasted parts of sentences fly into your 43 | mouth. Even the all-powerful Pointing has no control about the blind texts it is 44 | an almost unorthographic life One day however a small line of blind text by the 45 | name of Lorem Ipsum decided to leave for the far World of Grammar. 46 | 47 | ### According a funnily until pre-set or arrogant well cheerful 48 | 49 | The Big Oxmox advised her not to do so, because there were thousands of bad 50 | Commas, wild Question Marks and devious Semikoli, but the Little Blind Text 51 | didn’t listen. She packed her seven versalia, put her initial into the belt and 52 | made herself on the way. 53 | 54 | 1. So baboon this 55 | 2. Mounted militant weasel gregariously admonishingly straightly hey 56 | 3. Dear foresaw hungry and much some overhung 57 | 4. Rash opossum less because less some amid besides yikes jeepers frenetic 58 | impassive fruitlessly shut 59 | 60 | When she reached the first hills of the Italic Mountains, she had a last view 61 | back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet 62 | Village and the subline of her own road, the Line Lane. Pityful a rhetoric 63 | question ran over her cheek, then she continued her way. On her way she met a 64 | copy. 65 | 66 | > The copy warned the Little Blind Text, that where it came from it would have 67 | > been rewritten a thousand times and everything that was left from its origin 68 | > would be the word "and" and the Little Blind Text should turn around and 69 | > return to its own, safe country. 70 | 71 | But nothing the copy said could convince her and so it didn’t take long until a 72 | few insidious Copy Writers ambushed her, made her drunk with Longe and Parole 73 | and dragged her into their agency, where they abused her for their projects 74 | again and again. And if she hasn’t been rewritten, then they are still using 75 | her. Far far away, behind the word mountains, far from the countries Vokalia and 76 | Consonantia, there live the blind texts. 77 | 78 | #### Silent delightfully including because before one up barring chameleon 79 | 80 | Separated they live in Bookmarksgrove right at the coast of the Semantics, a 81 | large language ocean. A small river named Duden flows by their place and 82 | supplies it with the necessary regelialia. It is a paradisematic country, in 83 | which roasted parts of sentences fly into your mouth. 84 | 85 | Even the all-powerful Pointing has no control about the blind texts it is an 86 | almost unorthographic life One day however a small line of blind text by the 87 | name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox 88 | advised her not to do so, because there were thousands of bad Commas, wild 89 | Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. 90 | 91 | #### Wherever far wow thus a squirrel raccoon jeez jaguar this from along 92 | 93 | She packed her seven versalia, put her initial into the belt and made herself on 94 | the way. When she reached the first hills of the Italic Mountains, she had a 95 | last view back on the skyline of her hometown Bookmarksgrove, the headline of 96 | Alphabet Village and the subline of her own road, the Line Lane. Pityful a 97 | rhetoric question ran over her cheek, then she continued her way. On her way she 98 | met a copy. 99 | 100 | #### Slapped cozy a that lightheartedly and far 101 | 102 | The copy warned the Little Blind Text, that where it came from it would have 103 | been rewritten a thousand times and everything that was left from its origin 104 | would be the word "and" and the Little Blind Text should turn around and return 105 | to its own, safe country. But nothing the copy said could convince her and so it 106 | didn’t take long until a few insidious Copy Writers ambushed her, made her drunk 107 | with Longe and Parole and dragged her into their agency, where they abused her 108 | for their projects again and again. -------------------------------------------------------------------------------- /content/posts/second-post/second-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Second post 3 | description: The second post is the least memorable. 4 | date: 2020-04-17T11:00:00.000Z 5 | --- 6 | 7 | # h1 8 | 9 | ## h2 10 | 11 | ### h3 12 | 13 | Normal text 14 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@components/*": ["components/*"], 6 | "@utils/*": ["utils/*"], 7 | "@assets/*": ["assets/*"], 8 | "@config/*": ["config/*"], 9 | "@/*": ["./*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Directory (relative to root of your repo) that contains the deploy-ready 3 | # HTML files and assets generated by the build. If a base directory has 4 | # been specified, include it in the publish directory path. 5 | publish = "out" 6 | 7 | # Default build command. 8 | command = "npm run build && npx next export" 9 | 10 | environment = { NODE_VERSION = "12.13.1" } 11 | 12 | 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack(config) { 3 | config.module.rules.push({ 4 | test: /\.svg$/, 5 | use: ["@svgr/webpack"], 6 | }); 7 | 8 | return config; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-blog-starter", 3 | "version": "0.3.0", 4 | "description": "A Next.js template for a blog website.", 5 | "private": true, 6 | "engines": { 7 | "node": "14" 8 | }, 9 | "scripts": { 10 | "dev": "next dev", 11 | "build": "next build", 12 | "start": "next start" 13 | }, 14 | "dependencies": { 15 | "@svgr/webpack": "^6.1.1", 16 | "clsx": "^1.1.1", 17 | "feed": "^4.2.2", 18 | "next": "12.0.7", 19 | "next-compose-plugins": "^2.2.1", 20 | "next-themes": "^0.0.15", 21 | "react": "17.0.2", 22 | "react-dom": "17.0.2", 23 | "react-markdown": "^5.0.3", 24 | "react-syntax-highlighter": "^15.4.5", 25 | "react-toggle-dark-mode": "^1.0.4", 26 | "typeface-merriweather": "^1.1.13", 27 | "typeface-open-sans": "^1.1.13", 28 | "webp-loader": "^0.6.0" 29 | }, 30 | "devDependencies": { 31 | "@tailwindcss/typography": "^0.4.1", 32 | "autoprefixer": "^10.4.0", 33 | "gray-matter": "^4.0.3", 34 | "postcss": "^8.4.4", 35 | "tailwindcss": "^2.2.19" 36 | }, 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/Jfelix61/nextjs-starter-blog.git" 41 | }, 42 | "homepage": "https://github.com/Jfelix61/nextjs-starter-blog#readme", 43 | "keywords": [ 44 | "next.js", 45 | "blog", 46 | "template" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "next-themes"; 2 | 3 | import "@assets/css/main.css"; 4 | 5 | import "typeface-open-sans"; 6 | import "typeface-merriweather"; 7 | 8 | export default function MyApp({ Component, pageProps }) { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript, Html } from "next/document"; 2 | 3 | import { getSiteMetaData } from "@utils/helpers"; 4 | 5 | export default class MyDocument extends Document { 6 | render() { 7 | const siteMetadata = getSiteMetaData(); 8 | 9 | return ( 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Layout, Bio, SEO } from "@components/common"; 4 | import { getSortedPosts } from "@utils/posts"; 5 | import { generateRssPostsFeed } from "@utils/rss"; 6 | 7 | export default function Home({ posts }) { 8 | return ( 9 | 10 | 11 | 12 | {posts.map(({ frontmatter: { title, description, date }, slug }) => ( 13 |
14 |
15 |

16 | 17 | 18 | {title} 19 | 20 | 21 |

22 | {date} 23 |
24 |
25 |

{description}

26 |
27 |
28 | ))} 29 |
30 | ); 31 | } 32 | 33 | export async function getStaticProps() { 34 | generateRssPostsFeed(); 35 | const posts = getSortedPosts(); 36 | 37 | return { 38 | props: { 39 | posts, 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /pages/posts/[slug].js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import ReactMarkdown from "react-markdown"; 3 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 4 | import style from "react-syntax-highlighter/dist/cjs/styles/prism/dracula"; 5 | import Image from "next/image"; 6 | 7 | import { Layout, SEO, Bio } from "@components/common"; 8 | import { getPostBySlug, getPostsSlugs } from "@utils/posts"; 9 | 10 | export default function Post({ post, frontmatter, nextPost, previousPost }) { 11 | return ( 12 | 13 | 17 | 18 |
19 |
20 |

21 | {frontmatter.title} 22 |

23 |

{frontmatter.date}

24 |
25 | 31 |
32 |
33 | 34 |
35 |
36 | 37 | 55 |
56 | ); 57 | } 58 | 59 | export async function getStaticPaths() { 60 | const paths = getPostsSlugs(); 61 | 62 | return { 63 | paths, 64 | fallback: false, 65 | }; 66 | } 67 | 68 | export async function getStaticProps({ params: { slug } }) { 69 | const postData = getPostBySlug(slug); 70 | 71 | if (!postData.previousPost) { 72 | postData.previousPost = null; 73 | } 74 | 75 | if (!postData.nextPost) { 76 | postData.nextPost = null; 77 | } 78 | 79 | return { props: postData }; 80 | } 81 | 82 | const CodeBlock = ({ language, value }) => { 83 | return ( 84 | 85 | {value} 86 | 87 | ); 88 | }; 89 | 90 | const MarkdownImage = ({ alt, src }) => { 91 | return ( 92 | {alt} 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["tailwindcss", "autoprefixer"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoseRFelix/nextjs-starter-blog/b01937fd7b0d2b7b565eff269e67101b115bdb52/public/favicon.ico -------------------------------------------------------------------------------- /public/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Next.js Starter Blog 5 | https://nextjs-starter-blog-new-demo.vercel.app/ 6 | undefined 7 | Mon, 06 Dec 2021 19:32:09 GMT 8 | https://validator.w3.org/feed/docs/rss2.html 9 | https://github.com/jpmonette/feed 10 | en-US 11 | Copyright © 2021 Jose Felix 12 | 13 | <![CDATA[Long Post]]> 14 | https://nextjs-starter-blog-new-demo.vercel.app/posts/long-post 15 | long-post 16 | Tue, 28 Jul 2020 04:00:00 GMT 17 | 18 | Far far away, behind the word mountains, far from the countries Vokalia and 51 | > Consonantia, there live the blind texts. Separated they live in Bookmarksgrove 52 | > right at the coast of the Semantics, a large language ocean. 53 | 54 | It is a paradisematic country, in which roasted parts of sentences fly into your 55 | mouth. Even the all-powerful Pointing has no control about the blind texts it is 56 | an almost unorthographic life One day however a small line of blind text by the 57 | name of Lorem Ipsum decided to leave for the far World of Grammar. 58 | 59 | ### According a funnily until pre-set or arrogant well cheerful 60 | 61 | The Big Oxmox advised her not to do so, because there were thousands of bad 62 | Commas, wild Question Marks and devious Semikoli, but the Little Blind Text 63 | didn’t listen. She packed her seven versalia, put her initial into the belt and 64 | made herself on the way. 65 | 66 | 1. So baboon this 67 | 2. Mounted militant weasel gregariously admonishingly straightly hey 68 | 3. Dear foresaw hungry and much some overhung 69 | 4. Rash opossum less because less some amid besides yikes jeepers frenetic 70 | impassive fruitlessly shut 71 | 72 | When she reached the first hills of the Italic Mountains, she had a last view 73 | back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet 74 | Village and the subline of her own road, the Line Lane. Pityful a rhetoric 75 | question ran over her cheek, then she continued her way. On her way she met a 76 | copy. 77 | 78 | > The copy warned the Little Blind Text, that where it came from it would have 79 | > been rewritten a thousand times and everything that was left from its origin 80 | > would be the word "and" and the Little Blind Text should turn around and 81 | > return to its own, safe country. 82 | 83 | But nothing the copy said could convince her and so it didn’t take long until a 84 | few insidious Copy Writers ambushed her, made her drunk with Longe and Parole 85 | and dragged her into their agency, where they abused her for their projects 86 | again and again. And if she hasn’t been rewritten, then they are still using 87 | her. Far far away, behind the word mountains, far from the countries Vokalia and 88 | Consonantia, there live the blind texts. 89 | 90 | #### Silent delightfully including because before one up barring chameleon 91 | 92 | Separated they live in Bookmarksgrove right at the coast of the Semantics, a 93 | large language ocean. A small river named Duden flows by their place and 94 | supplies it with the necessary regelialia. It is a paradisematic country, in 95 | which roasted parts of sentences fly into your mouth. 96 | 97 | Even the all-powerful Pointing has no control about the blind texts it is an 98 | almost unorthographic life One day however a small line of blind text by the 99 | name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox 100 | advised her not to do so, because there were thousands of bad Commas, wild 101 | Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. 102 | 103 | #### Wherever far wow thus a squirrel raccoon jeez jaguar this from along 104 | 105 | She packed her seven versalia, put her initial into the belt and made herself on 106 | the way. When she reached the first hills of the Italic Mountains, she had a 107 | last view back on the skyline of her hometown Bookmarksgrove, the headline of 108 | Alphabet Village and the subline of her own road, the Line Lane. Pityful a 109 | rhetoric question ran over her cheek, then she continued her way. On her way she 110 | met a copy. 111 | 112 | #### Slapped cozy a that lightheartedly and far 113 | 114 | The copy warned the Little Blind Text, that where it came from it would have 115 | been rewritten a thousand times and everything that was left from its origin 116 | would be the word "and" and the Little Blind Text should turn around and return 117 | to its own, safe country. But nothing the copy said could convince her and so it 118 | didn’t take long until a few insidious Copy Writers ambushed her, made her drunk 119 | with Longe and Parole and dragged her into their agency, where they abused her 120 | for their projects again and again.]]> 121 | 122 | 123 | <![CDATA[Coding Post]]> 124 | https://nextjs-starter-blog-new-demo.vercel.app/posts/coding-post 125 | coding-post 126 | Sun, 19 Apr 2020 04:00:00 GMT 127 | 128 |
I'm a cool component!!
; 133 | 134 | export default CoolComponent; 135 | ``` 136 | ]]>
137 |
138 | 139 | <![CDATA[Second post]]> 140 | https://nextjs-starter-blog-new-demo.vercel.app/posts/second-post 141 | second-post 142 | Fri, 17 Apr 2020 04:00:00 GMT 143 | 144 | 153 | 154 | 155 | <![CDATA[First post]]> 156 | https://nextjs-starter-blog-new-demo.vercel.app/posts/first-post 157 | first-post 158 | Thu, 16 Apr 2020 04:00:00 GMT 159 | 160 | 171 | 172 |
173 |
-------------------------------------------------------------------------------- /public/zeit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultSans = [ 2 | "system-ui", 3 | "-apple-system", 4 | "BlinkMacSystemFont", 5 | '"Segoe UI"', 6 | "Roboto", 7 | '"Helvetica Neue"', 8 | "Arial", 9 | '"Noto Sans"', 10 | "sans-serif", 11 | '"Apple Color Emoji"', 12 | '"Segoe UI Emoji"', 13 | '"Segoe UI Symbol"', 14 | '"Noto Color Emoji"', 15 | ]; 16 | 17 | const defaultSerif = [ 18 | "Georgia", 19 | "Cambria", 20 | '"Times New Roman"', 21 | "Times", 22 | "serif", 23 | ]; 24 | 25 | module.exports = { 26 | mode: "jit", 27 | purge: [ 28 | "./components/**/*.{js,ts,jsx,tsx,css}", 29 | "./pages/**/*.{js,ts,jsx,tsx}", 30 | ], 31 | darkMode: "class", 32 | theme: { 33 | extend: { 34 | colors: { 35 | "neon-orange": "#f92300", 36 | }, 37 | fontSize: { 38 | "7xl": "4.5rem", 39 | }, 40 | spacing: { 41 | 14: "3.375rem", 42 | }, 43 | typography: (theme) => ({ 44 | DEFAULT: { 45 | css: { 46 | color: theme("colors.gray.800"), 47 | blockquote: { 48 | borderLeftColor: theme("colors.gray.700"), 49 | }, 50 | "ol > li::before": { 51 | color: theme("colors.gray.700"), 52 | }, 53 | "ul > li::before": { 54 | backgroundColor: theme("colors.gray.700"), 55 | }, 56 | a: { 57 | color: theme("colors.neon-orange"), 58 | }, 59 | }, 60 | }, 61 | 62 | dark: { 63 | css: { 64 | color: theme("colors.gray.100"), 65 | blockquote: { 66 | borderLeftColor: theme("colors.gray.300"), 67 | }, 68 | "ol > li::before": { 69 | color: theme("colors.gray.300"), 70 | }, 71 | "ul > li::before": { 72 | backgroundColor: theme("colors.gray.300"), 73 | }, 74 | a: { 75 | color: theme("colors.yellow.500"), 76 | }, 77 | h1: { 78 | color: theme("colors.gray.100"), 79 | }, 80 | h2: { 81 | color: theme("colors.gray.100"), 82 | }, 83 | h3: { 84 | color: theme("colors.gray.100"), 85 | }, 86 | h4: { 87 | color: theme("colors.gray.100"), 88 | }, 89 | h5: { 90 | color: theme("colors.gray.100"), 91 | }, 92 | h6: { 93 | color: theme("colors.gray.100"), 94 | }, 95 | strong: { 96 | color: theme("colors.gray.100"), 97 | }, 98 | code: { 99 | color: theme("colors.gray.100"), 100 | }, 101 | figcaption: { 102 | color: theme("colors.gray.100"), 103 | }, 104 | blockquote: { 105 | color: theme("colors.gray.100"), 106 | borderLeftColor: theme("colors.gray.200"), 107 | }, 108 | }, 109 | }, 110 | }), 111 | }, 112 | fontFamily: { 113 | display: ["Open Sans", ...defaultSans], 114 | body: ["Merriweather", ...defaultSerif], 115 | }, 116 | }, 117 | variants: {}, 118 | plugins: [require("@tailwindcss/typography")], 119 | variants: { 120 | extend: { 121 | typography: ["dark"], 122 | }, 123 | }, 124 | }; 125 | -------------------------------------------------------------------------------- /utils/helpers.js: -------------------------------------------------------------------------------- 1 | import SiteConfig from "@config/seo.json"; 2 | 3 | export function getSiteMetaData() { 4 | return SiteConfig.siteMetadata; 5 | } 6 | -------------------------------------------------------------------------------- /utils/posts.js: -------------------------------------------------------------------------------- 1 | import matter from "gray-matter"; 2 | import fs from "fs"; 3 | 4 | export function getPostsFolders() { 5 | // Get all posts folders located in `content/posts` 6 | const postsFolders = fs 7 | .readdirSync(`${process.cwd()}/content/posts`) 8 | .map((folderName) => ({ 9 | directory: folderName, 10 | filename: `${folderName}.md`, 11 | })); 12 | 13 | return postsFolders; 14 | } 15 | 16 | // Get day in format: Month day, Year. e.g. April 19, 2020 17 | function getFormattedDate(date) { 18 | const options = { year: "numeric", month: "long", day: "numeric" }; 19 | const formattedDate = date.toLocaleDateString("en-US", options); 20 | 21 | return formattedDate; 22 | } 23 | 24 | export function getSortedPosts() { 25 | const postFolders = getPostsFolders(); 26 | 27 | const posts = postFolders 28 | .map(({ filename, directory }) => { 29 | // Get raw content from file 30 | const markdownWithMetadata = fs 31 | .readFileSync(`content/posts/${directory}/${filename}`) 32 | .toString(); 33 | 34 | // Parse markdown, get frontmatter data, excerpt and content. 35 | const { data, excerpt, content } = matter(markdownWithMetadata); 36 | 37 | const frontmatter = { 38 | ...data, 39 | date: getFormattedDate(data.date), 40 | }; 41 | 42 | // Remove .md file extension from post name 43 | const slug = filename.replace(".md", ""); 44 | 45 | return { 46 | slug, 47 | frontmatter, 48 | excerpt, 49 | content, 50 | }; 51 | }) 52 | .sort( 53 | (a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date) 54 | ); 55 | 56 | return posts; 57 | } 58 | 59 | export function getPostsSlugs() { 60 | const postFolders = getPostsFolders(); 61 | 62 | const paths = postFolders.map(({ filename }) => ({ 63 | params: { 64 | slug: filename.replace(".md", ""), 65 | }, 66 | })); 67 | 68 | return paths; 69 | } 70 | 71 | export function getPostBySlug(slug) { 72 | const posts = getSortedPosts(); 73 | 74 | const postIndex = posts.findIndex(({ slug: postSlug }) => postSlug === slug); 75 | 76 | const { frontmatter, content, excerpt } = posts[postIndex]; 77 | 78 | const previousPost = posts[postIndex + 1]; 79 | const nextPost = posts[postIndex - 1]; 80 | 81 | return { frontmatter, post: { content, excerpt }, previousPost, nextPost }; 82 | } 83 | -------------------------------------------------------------------------------- /utils/rss.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import { Feed } from "feed"; 4 | 5 | import { getSiteMetaData } from "./helpers"; 6 | import { getSortedPosts } from "./posts"; 7 | 8 | export function generateRssPostsFeed() { 9 | const { title, siteUrl, language, author } = getSiteMetaData(); 10 | const posts = getSortedPosts(); 11 | 12 | const feed = new Feed({ 13 | title, 14 | id: siteUrl, 15 | link: siteUrl, 16 | language, 17 | copyright: "Copyright © " + new Date().getFullYear() + " " + author.name, 18 | author: { 19 | name: author.name, 20 | link: author?.link, 21 | }, 22 | feedLinks: { 23 | rss2: `${siteUrl}rss.xml`, 24 | }, 25 | }); 26 | 27 | posts.forEach( 28 | ({ frontmatter: { title, description, date }, content, slug }) => { 29 | feed.addItem({ 30 | title, 31 | description, 32 | date: new Date(date), 33 | id: slug, 34 | link: `${siteUrl}posts/${slug}`, 35 | content: content, 36 | }); 37 | } 38 | ); 39 | 40 | // Write the RSS output to a public file, making it 41 | // accessible at siteUrl/rss.xml 42 | fs.writeFileSync("public/rss.xml", feed.rss2()); 43 | } 44 | --------------------------------------------------------------------------------