├── .env.local.example ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .idea ├── .gitignore ├── modules.xml ├── sandypockets-blog-v2.iml └── vcs.xml ├── .prettierignore ├── .prettierrc.js ├── .storybook ├── main.js └── preview.js ├── README.md ├── SECURITY.md ├── _posts ├── a-nextjs-blog-starter-you-actually-want-to-use.md ├── getting-started.md └── markdown-reference.md ├── components ├── Home │ ├── HeroPost.jsx │ ├── Intro.jsx │ ├── MoreStories.jsx │ └── PostPreview.jsx ├── Icons │ ├── MailIcon.jsx │ ├── MapPinIcon.jsx │ ├── MenuIcon.jsx │ └── XMarkIcon.jsx ├── Image │ ├── Avatar.jsx │ └── CoverImage.jsx ├── Layout │ ├── Container.jsx │ ├── Footer.jsx │ ├── Layout.jsx │ ├── Meta.jsx │ └── Nav.jsx ├── PageHeading.jsx ├── Post │ ├── Header.jsx │ ├── PostBody.jsx │ ├── PostHeader.jsx │ ├── PostTitle.jsx │ └── markdown-styles.module.css └── Utils │ ├── ContactForm │ ├── ContactDetails.jsx │ ├── ContactForm.jsx │ └── ContactPage.jsx │ ├── DateFormatter.jsx │ ├── Highlight.jsx │ ├── SectionSeparator.jsx │ └── Toggle.jsx ├── docs ├── dracula-syntax-highlighting.png ├── nextjs-blog-starter-about-page.png ├── nextjs-blog-starter-contact-page.png ├── nextjs-blog-starter-homepage.png └── nextjs-blog-syntax-highlighting.png ├── lib ├── api.js ├── constants.js ├── gtag.js └── markdownToHtml.js ├── license.md ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── about.jsx ├── api │ └── email.js ├── contact.jsx ├── index.jsx └── posts │ └── [slug].js ├── postcss.config.js ├── public ├── assets │ └── blog │ │ ├── a-nextjs-blog-starter-you-actually-want-to-use │ │ └── tree-minimal.jpg │ │ ├── authors │ │ └── sandypockets_avatar.jpg │ │ ├── getting-started │ │ └── snowy-mountain.jpg │ │ └── markdown-reference │ │ └── book.jpg └── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── scripts ├── generate-post.js ├── generate-rss.mjs └── generate-sitemap.mjs ├── stories ├── Button.jsx ├── Button.stories.jsx ├── Header.jsx ├── Header.stories.jsx ├── Page.jsx ├── Page.stories.jsx ├── assets │ ├── accessibility.png │ ├── accessibility.svg │ ├── addon-library.png │ ├── assets.png │ ├── avif-test-image.avif │ ├── code-brackets.svg │ ├── colors.svg │ ├── comments.svg │ ├── context.png │ ├── direction.svg │ ├── discord.svg │ ├── docs.png │ ├── figma-plugin.png │ ├── flow.svg │ ├── github.svg │ ├── plugin.svg │ ├── repo.svg │ ├── share.png │ ├── stackalt.svg │ ├── styling.png │ ├── testing.png │ ├── theming.png │ ├── tutorials.svg │ └── youtube.svg ├── button.css ├── header.css └── page.css ├── styles └── global.css └── tailwind.config.js /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX 2 | SENDGRID_API_KEY=REPLACE-WITH-YOUR-API-KEY 3 | SENDGRID_TO_EMAIL=email@example.com 4 | SENDGRID_FROM_EMAIL=email@example.com 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "plugin:storybook/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | public/sitemap.xml 37 | public/robots.txt 38 | public/feed.xml 39 | 40 | *storybook.log -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # GitHub Copilot persisted chat sessions 7 | /copilot/chatSessions 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sandypockets-blog-v2.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | _posts/ 2 | *.md 3 | .next/ 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | stories: [ 5 | '../stories/**/*.mdx', 6 | '../stories/**/*.md', 7 | '../stories/**/*.stories.@(js|jsx|ts|tsx)', 8 | ], 9 | addons: [ 10 | '@storybook/addon-onboarding', 11 | '@storybook/addon-links', 12 | '@storybook/addon-essentials', 13 | '@chromatic-com/storybook', 14 | '@storybook/addon-interactions', 15 | ], 16 | framework: '@storybook/nextjs', 17 | docs: { 18 | // Your docs configuration 19 | autodocs: 'tag', 20 | }, 21 | staticDirs: ['../public'], 22 | 23 | webpackFinal: async (config, { configType }) => { 24 | // Locate the rules section of the webpack config 25 | const rules = config.module.rules 26 | 27 | // Add MDX loader 28 | rules.push({ 29 | test: /\.mdx$/, 30 | use: [ 31 | { 32 | loader: 'babel-loader', 33 | options: { 34 | presets: ['@babel/preset-env', '@babel/preset-react'], 35 | }, 36 | }, 37 | { 38 | loader: '@mdx-js/loader', 39 | options: { 40 | remarkPlugins: [require('remark-prism')], 41 | }, 42 | }, 43 | ], 44 | }) 45 | 46 | // Return the updated webpack config 47 | return config 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../styles/global.css' 2 | 3 | // v7-style sort 4 | function storySort(a, b) { 5 | return a.title === b.title 6 | ? 0 7 | : a.id.localeCompare(b.id, undefined, { numeric: true }) 8 | } 9 | 10 | export const parameters = { 11 | actions: { 12 | // Remove argTypesRegex 13 | // argTypesRegex: '^on[A-Z].*', 14 | // Add explicit actions using the fn function if necessary 15 | // fn: 'The function to call', 16 | // Add other action configurations as needed 17 | // disabled: true, // If you want to disable the actions addon 18 | }, 19 | controls: { 20 | matchers: { 21 | color: /(background|color)$/i, 22 | date: /Date$/, 23 | }, 24 | }, 25 | options: { 26 | storySort: (a, b) => 27 | a.title === b.title 28 | ? 0 29 | : a.id.localeCompare(b.id, undefined, { numeric: true }), 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Blog Starter 2 | A custom Next.js blog starter for use with [`create next app`](https://nextjs.org/docs/api-reference/create-next-app). This starter design is based on the original blog starter provided by Next, but includes many extra features and performance improvements that are nice to have right out of the box. 3 | 4 | Run this in your terminal to get started: 5 | 6 | ```shell 7 | npx create-next-app --example https://github.com/sandypockets/nextjs-blog-starter/tree/main nextjs-blog-starter 8 | ``` 9 | 10 | Was this blog starter what you were looking for? 11 | 12 | Buy Me A Coffee 13 | 14 | # Contents 15 | * [Preview](https://github.com/sandypockets/nextjs-blog-starter#preview) 16 | * [Live demo](https://github.com/sandypockets/nextjs-blog-starter#live-demo) 17 | * [Deploy your own](https://github.com/sandypockets/nextjs-blog-starter#deploy-your-own) 18 | * [Built with](https://github.com/sandypockets/nextjs-blog-starter#built-with) 19 | * [Core features](https://github.com/sandypockets/nextjs-blog-starter#core-features) 20 | * [Get started](https://github.com/sandypockets/nextjs-blog-starter#get-started) 21 | * [create next app](https://github.com/sandypockets/nextjs-blog-starter#create-next-app) 22 | * [Set up constants](https://github.com/sandypockets/nextjs-blog-starter#constants) 23 | * [Generate a sitemap and robots.txt](https://github.com/sandypockets/nextjs-blog-starter#generate-a-sitemap-and-robotstxt) 24 | * [RSS feed](https://github.com/sandypockets/nextjs-blog-starter#rss-feed) 25 | * [Set up Google Analytics](https://github.com/sandypockets/nextjs-blog-starter#set-up-google-analytics) 26 | * [Set up SendGrid](https://github.com/sandypockets/nextjs-blog-starter#set-up-sendgrid) 27 | * [Customize code syntax highlighting](https://github.com/sandypockets/nextjs-blog-starter#customize-syntax-highlighting) 28 | * [Changing the theme](https://github.com/sandypockets/nextjs-blog-starter#changing-the-theme) 29 | * [Dependencies](https://github.com/sandypockets/nextjs-blog-starter#dependencies) 30 | * [Dev dependencies](https://github.com/sandypockets/nextjs-blog-starter#dev-dependencies) 31 | * [How it works](https://github.com/sandypockets/nextjs-blog-starter#how-it-works) 32 | * [Front matter](https://github.com/sandypockets/nextjs-blog-starter#front-matter) 33 | * [Create a new article](https://github.com/sandypockets/nextjs-blog-starter#create-a-new-article) 34 | * [Scaffolding with a script](https://github.com/sandypockets/nextjs-blog-starter#using-the-script) 35 | * [Create an article without scaffolding](https://github.com/sandypockets/nextjs-blog-starter#create-a-new-article-manually) 36 | * [Contributing](https://github.com/sandypockets/nextjs-blog-starter#contributing) 37 | * [Screenshots](https://github.com/sandypockets/nextjs-blog-starter#screenshots) 38 | 39 | ## Preview 40 | Preview the example live on [StackBlitz](http://stackblitz.com/): 41 | 42 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/sandypockets/nextjs-blog-starter/tree/main) 43 | 44 | ## Live demo 45 | 46 | Check out the live demo at [blog-starter.sandypockets.dev](https://blog-starter.sandypockets.dev/), or see it in production as my actual blog at [sandypockets.dev](https://sandypockets.dev), generated using [`create-next-app`](https://github.com/sandypockets/nextjs-blog-starter#create-next-app) 47 | 48 | ## Deploy your own 49 | 50 | Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=sandypockets): 51 | 52 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/sandypockets/nextjs-blog-starter/tree/main&project-name=sandypockets-blog-starter&repository-name=sandypockets-blog-starter) 53 | 54 | [![Deploy with Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/sandypockets/nextjs-blog-starter/tree/main) 55 | 56 | 57 | ## Built with 58 | 59 | * [React](https://reactjs.org/docs/getting-started.html) 60 | * [Next.js](https://nextjs.org/docs/getting-started) 61 | * [Tailwind CSS](https://tailwindcss.com/docs/utility-first) 62 | * [Remark](https://remark.js.org/) 63 | * [Gray Matter](https://github.com/jonschlinkert/gray-matter) 64 | 65 | ## Core features 66 | * Write articles in markdown 67 | * Markdown is already styled. Just start writing. 68 | * Dark mode based on OS preference, with toggle to manually change. 69 | * Google Analytics 70 | * Email contact form (using SendGrid) 71 | * Tailwind CSS v3.0 72 | * Preformatted code syntax highlighting 73 | * Priority image downloads for content above the fold, deferred downloads for below it 74 | * Automatically generated sitemap and robots.txt 75 | * Automatically generated RSS feed 76 | * Storybook.js 77 | 78 | ## Get started 79 | This README will guide you through the basic set up. However, please refer to the blog posts in the [live demo](https://github.com/sandypockets/nextjs-blog-starter#live-demo) for more information. Don't worry, no lorem ipsums here. Each post contains real, actually helpful content. 80 | 81 | Built and tested with Node `v20.11.1` 82 | 83 | ### Create Next App 84 | 1. Bootstrap this starter using `create next app`. 85 | ```shell 86 | npx create-next-app --example https://github.com/sandypockets/nextjs-blog-starter/tree/main nextjs-blog-starter 87 | ``` 88 | 89 | 2. Change into the new project directory and install dependencies. 90 | ```shell 91 | cd nextjs-blog-starter && npm install 92 | ``` 93 | 94 | 3. Start the development server. 95 | 96 | ```shell 97 | npm run dev 98 | ``` 99 | 100 | 5. Once the server is running, visit [http://localhost:3000](http://localhost:3000) in your browser. 101 | 6. Set up constant variables, and prepare to generate a sitemap. 102 | 7. [Set up Google Analytics](https://github.com/sandypockets/nextjs-blog-starter#set-up-google-analytics) 103 | 8. [Set up SendGrid](https://github.com/sandypockets/nextjs-blog-starter#set-up-sendgrid). 104 | 105 | ### Constants 106 | 107 | Set up each of the constants, much like you would a `.env`, in the `lib/constants.js` file. 108 | 109 | ```javascript 110 | export const EXAMPLE_PATH = 'blog-starter' 111 | export const CMS_NAME = 'Markdown' 112 | export const HOME_OG_IMAGE_URL = 'https://og-image.vercel.app/Next.js%20Blog%20Starter%20Example.png?theme=light&md=1&fontSize=100px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg' 113 | export const BLOG_NAME = 'Next.js Blog Starter' 114 | export const KEYWORDS = 'starter, blog, next.js, template' 115 | export const DESCRIPTION = 'A starter blog template for Next.js' 116 | export const AUTHOR = 'sandypockets' 117 | export const LANG = 'en-CA' 118 | export const GITHUB_REPO = 'https://github.com/sandypockets' 119 | ``` 120 | 121 | ### Generate a sitemap and robots.txt 122 | 123 | Sitemaps are an important part of SEO. This section walks through adding your base URL to the sitemap generator. The generator runs automatically after each build, generating a new sitemap each time you update your blog. 124 | 125 | The generated sitemap can be found in `public/sitemap.xml`. This command also generates a new `public/robots.txt` file. 126 | 127 | 1. Open the `scripts/generate.sitemap.mjs` file. 128 | 2. On Line 5, replace `https://blog-starter.sandypockets.dev` with your own blog's website. 129 | 3. On Line 6, replace `en-CA` with your preferred locale (`en-UK`, `en-US`, etc.). This is used to determine the format of the date stamp in the sitemap. 130 | 131 | When you're ready to test it: 132 | 133 | 1. Run `npm run build` 134 | 2. Check the `public/` directory for the `sitemap.xml` file and `robots.txt` file. 135 | 3. Run `npm run start` 136 | 4. Visit `http://localhost:3000/sitemap.xml` 137 | 138 | If you see the xml sitemap, then it was successful. 139 | 140 | ### RSS Feed 141 | An RSS feed is available for the blog at `/feed.xml`. However, you must first configure the RSS generator to use your own URL. 142 | 143 | 1. Open the `scripts/generate-rss.mjs` file. 144 | 2. On Line 7, replace the `https://blog-starter.sandypockets.dev` value of `BLOG_URL` with your own. 145 | 146 | When you're ready to test it: 147 | 148 | 1. Run `npm run build` 149 | 2. Check the `public/` directory for the `feed.xml` file. 150 | 3. Run `npm run start` 151 | 4. Visit `http://localhost:3000/feed.xml` 152 | 153 | If you see the xml RSS feed, then it was successful. 154 | 155 | ### Set up Google Analytics 156 | 157 | You will need to have your Google tag ID. If you do not have one, or do not have a Google Analytics account, you can sign up at [analytics.google.com](https://analytics.google.com/) 158 | 159 | 1. Create a copy of the `.env.local.example` file, and name it `.env.local`. To do so in the terminal, run: 160 | 161 | ```shell 162 | cp .env.local.example .env.local 163 | ``` 164 | 165 | 2. Grab your Google tag ID from your Analytics account, and replace the `G-XXXXXXXXXX` in the new `.env.local` file you just created. 166 | 167 | ### Set up SendGrid 168 | You will need a free SendGrid account, which allows you to send up to 100 emails each day. 169 | 170 | 1. Get your SendGrid API key from your SendGrid account. 171 | 2. Open the `.env.local` file that you created when setting up Google Analytics. 172 | 3. Replace `REPLACE-WITH-YOUR-API-KEY` with your actual API key from SendGrid. 173 | 174 | ## Customize Syntax Highlighting 175 | When you use inline code or codeblocks on your blog, they'll be highlighted with the Dracula theme style, like this: 176 | 177 | ![dracula prism syntax highlighting](./docs/dracula-syntax-highlighting.png) 178 | 179 | However, you can choose from [over 38 other themes](https://github.com/PrismJS/prism-themes) that are ready to go right out of the box. 180 | 181 | ### Changing the theme 182 | 1. Open the `pages/_app.js` file. 183 | 2. On Line 5, note the import of `'prism-themes/themes/prism-dracula.css'` 184 | 3. To change the theme, simply replace the `prism-dracula.css` portion with the name of the new theme file as shown [on this page](https://github.com/PrismJS/prism-themes/tree/master/themes). 185 | 186 | For example, if you want to use the `prism-duotone-sea` theme, then adjust the import statement on Line 5 to be `import 'prism-themes/themes/prism-duotone-sea.css'` 187 | 188 | ## Dependencies 189 | - **@headlessui/react**: ^1.7.18 190 | - **@heroicons/react**: ^2.1.3 191 | - **@sendgrid/mail**: ^8.1.1 192 | - **@tailwindcss/forms**: ^0.5.7 193 | - **axios**: ^1.6.8 194 | - **classnames**: 2.2.6 195 | - **date-fns**: 3.6.0 196 | - **gray-matter**: 4.0.3 197 | - **next**: latest 198 | - **prism-themes**: ^1.9.0 199 | - **react**: ^18.2.0 200 | - **react-dom**: 18.2.0 201 | - **remark**: 15.0.1 202 | - **remark-gfm**: ^4.0.0 203 | - **remark-html**: ^16.0.1 204 | - **remark-prism**: ^1.3.6 205 | - **sharp**: ^0.33.3 206 | 207 | ### Dev Dependencies 208 | - **@babel/core**: ^7.15.0 209 | - **@chromatic-com/storybook**: ^1.2.25 210 | - **@mdx-js/loader**: ^3.0.1 211 | - **@storybook/addon-actions**: ^8.0.4 212 | - **@storybook/addon-essentials**: ^8.0.4 213 | - **@storybook/addon-interactions**: ^8.0.4 214 | - **@storybook/addon-links**: ^8.0.4 215 | - **@storybook/addon-onboarding**: ^8.0.4 216 | - **@storybook/blocks**: ^8.0.4 217 | - **@storybook/cli**: ^8.0.4 218 | - **@storybook/nextjs**: ^8.0.4 219 | - **@storybook/react**: ^8.0.4 220 | - **@storybook/test**: ^8.0.4 221 | - **autoprefixer**: ^10.4.19 222 | - **babel-loader**: ^8.2.2 223 | - **eslint**: ^8.57.0 224 | - **eslint-config-next**: 14.1.4 225 | - **eslint-plugin-storybook**: ^0.8.0 226 | - **globby**: ^14.0.1 227 | - **postcss**: ^8.4.38 228 | - **prettier**: ^3.2.5 229 | - **rss**: ^1.2.2 230 | - **storybook**: ^8.0.4 231 | - **tailwindcss**: ^3.4.1 232 | 233 | ## How it works 234 | Blog posts are stored in the `/_posts` directory as Markdown files. Each post must include the appropriate front matter. 235 | 236 | To create the blog posts we use [`remark`](https://github.com/remarkjs/remark) and [`remark-html`](https://github.com/remarkjs/remark-html) to convert the Markdown files into an HTML string, and then send it down as a prop to the page. The metadata of every post is handled by [`gray-matter`](https://github.com/jonschlinkert/gray-matter) and also sent in props to the page. 237 | 238 | ### Front matter 239 | An example of the required front matter: 240 | 241 | > Note: The formatting of the front matter is important. Ensure the indentation, and quotes remain the same. 242 | 243 | ```text 244 | --- 245 | title: 'A blog starter you actually want to use' 246 | excerpt: 'There are hundreds of different blog starters out there. But none felt quite right. So I built my own. Based off the basic Next.js Blog Starter, but now with several handy features like dark mode (using local storage) or Google Analytics. It comes with Storybook too.' 247 | coverImage: '/assets/blog/a-nextjs-blog-starter-you-actually-want-to-use/tree-minimal.jpg' 248 | date: '2021-08-24T05:35:07.322Z' 249 | author: 250 | name: sandypockets 251 | picture: '/assets/blog/authors/sandypockets_avatar.jpg' 252 | ogImage: 253 | url: '/assets/blog/a-nextjs-blog-starter-you-actually-want-to-use/tree-minimal.jpg' 254 | --- 255 | ``` 256 | 257 | Adjust the value of each key as needed. 258 | 259 | ### Create a new article 260 | There are two ways to create a new article: manually, or using the included script to scaffold one out. 261 | 262 | #### Using the script 263 | First, you should replace the placeholder `sandypockets` information in the `scripts/generate-post.js` script with your own info. Then, it's as simple as running the script: 264 | 265 | ```shell 266 | npm run new your-post-title 267 | ``` 268 | 269 | The script will generate a new post in the `/_posts` directory with the title used in the command above, and a default front matter. 270 | 271 | #### Create a new article manually 272 | 1. Add a new Markdown file (`.md`) to the `/_posts` directory. 273 | 2. Add the required front matter (described above) and adjust it as needed. 274 | 3. Images related to the post should be stored in within the appropriate the `/public/assets/blog` directory. It is recommended you create a new folder for each blog post to prevent the images files from growing unwieldy. 275 | 4. That's it. Your new post will show up alongside the others on your homepage. 276 | 277 | > Note: The slug is the path that will be displayed, and is based on the title of your `.md` file. For example, `kobe.md` becomes `localhost:3000/posts/kobe` 278 | 279 | ## Contributing 280 | Contributions are what make the open source community such an amazing place to be, learn, inspire, and create. Any contributions you make are greatly appreciated. 281 | 282 | 1. Fork the Project 283 | 2. Create your Feature or Fix Branch (`git checkout -b feature/AmazingFeature` or `git checkout -b fix/ContactForm` ) 284 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 285 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 286 | 5. Open a Pull Request 287 | 288 | ## Screenshots 289 | #### Homepage 290 | ![](docs/nextjs-blog-starter-homepage.png) 291 | 292 | #### About page 293 | ![](docs/nextjs-blog-starter-about-page.png) 294 | 295 | #### Contact page 296 | ![](docs/nextjs-blog-starter-contact-page.png) 297 | 298 | #### Syntax highlighting example 299 | ![](docs/nextjs-blog-syntax-highlighting.png) 300 | 301 | Want more themes for your syntax highlighting? Check out [Customize code syntax highlighting](https://github.com/sandypockets/nextjs-blog-starter#customize-syntax-highlighting). 302 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Before deciding whether to report a security vulnerability, be sure to check whether the version of `nextjs-blog-starter` is supported. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x.x | :x: | 10 | | < 2.0.2 | :white_check_mark: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you discover a security vulnerability within `nextjs-blog-starter`, please bring it to my attention by [submitting a security report](https://github.com/sandypockets/nextjs-blog-starter/security/advisories/new). 15 | 16 | When reporting a vulnerability, please provide detailed information, including steps to reproduce the issue and any potential impact. I'll acknowledge your report as soon as I can, and we'll determine the best way to proceed and keep the project safe. 17 | -------------------------------------------------------------------------------- /_posts/a-nextjs-blog-starter-you-actually-want-to-use.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'A blog starter you actually want to use' 3 | excerpt: 'There are hundreds of different blog starters out there. But none felt quite right. So I built my own. Based off the basic Next.js Blog Starter, but now with several handy features like dark mode (using local storage) or Google Analytics. It comes with Storybook too.' 4 | coverImage: '/assets/blog/a-nextjs-blog-starter-you-actually-want-to-use/tree-minimal.jpg' 5 | date: '2021-08-24T05:35:07.322Z' 6 | author: 7 | name: sandypockets 8 | picture: '/assets/blog/authors/sandypockets_avatar.jpg' 9 | ogImage: 10 | url: '/assets/blog/a-nextjs-blog-starter-you-actually-want-to-use/tree.jpg' 11 | --- 12 | 13 | There are hundreds of Next.js blog starters out there. I've tinkered with a few dozen of them, and while there are many that are quite good, they just didn't seem to fit what I wanted to do with them. Nothing was ready out of the box. That's fine for some projects, but for others, I'm not looking to reinvent the wheel. 14 | 15 | I've always been drawn to the Next.js blog starter. The overall design is clean, and it supports markdown, though it doesn't come with much styling for it. But still lacked features, like Google Analytics, dark mode, or syntax highlighting, that I'd become accustomed to with my own bespoke templates. _This_ starter, is the result of combining the features I've come to rely on with much of the OG Next blog starter's slick layout. 16 | 17 | ## Features ✨ 18 | 19 | ##### Dark mode 20 | The app defaults to the user's OS preferences. If the user hasn't selected an OS preference, the theme defaults to light mode. The user can switch between light and dark mode using the toggle in the nav. Dark/light theme preferences are stored on the client, in `localStorage.theme` 21 | 22 | Adding new dark classes is easy. Simply prefix the Tailwind CSS class name with `dark:` and it will be applied during dark mode only. 23 | 24 | ##### Google Analytics 25 | Understanding traffic is a big part of blogging. Simply copy your Google ID, paste it into the `.env.local` file, and you're all set. The Google tag is set up to track a number of events, including form submissions. 26 | 27 | ##### A working contact form 28 | The contact form uses [SendGrid](https://sendgrid.com/) to send emails from your contact form on your behalf. Their free plan is suitable. Just add your SendGrid API key, and your "to" and "from" email addresses to the `.env.local` file, and you're all set. Submissions to the contact form will be emailed directly to your inbox. And, as mentioned above, all submissions on the contact form are tracked as Google Analytics events. 29 | 30 | ##### Markdown 31 | It's a blog. We're here for the content. You shouldn't be wrangling HTML or CSS. You should be writing. Creating. That's it. And that's exactly what markdown lets you do. But, unlike many blog starters touting markdown, _this_ starter actually comes with built-in styling. Take a look at the [Markdown Guide](/posts/markdown-guide) for examples. 32 | 33 | ##### Syntax highlighting 34 | Is your blog code heavy? Tutorials? All preformatted (`
`) code blocks are highlighted with Prism. The theme can be adjusted in the Meta component.
35 | 
36 | ##### Tailwind CSS
37 | Tailwind keeps CSS manageable, in an understandable, and scalable way. If you're not already familiar with Tailwind, check it out [here](https://tailwindcss.com), and never look back.
38 | 
39 | ##### Storybook
40 | Storybook helps you build more composable components, by crafting and testing them in isolation. Run `yarn storybook` from the root directory to start Storybook and view stories.
41 | 
42 | ##### Statically generated
43 | Need to show the same page to a bunch of users? Generate it at build. When your users ask for your content, deliver it lightning fast.
44 | 
45 | ##### Image optimization
46 | Defer images you don't need, and prioritize the ones you do. All the while, making sure you're serving up the smallest size possible without sacrificing quality. For example, the hero image on the homepage receives priority (since it's above the fold), while the images below the fold, in the "More stories" section, will be deferred. The same is true for the hero image on each blog post's page.
47 | 
48 | ##### React
49 | A fast, robust library. Popular enough that you'll always be able to find the tools and packages you need to build out custom features. 


--------------------------------------------------------------------------------
/_posts/getting-started.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | title: 'Getting started with this template'
  3 | excerpt: 'Everything you need to know to get a Next.js blog up and running.'
  4 | coverImage: '/assets/blog/getting-started/snowy-mountain.jpg'
  5 | date: '2021-08-24T05:35:07.322Z'
  6 | author:
  7 |   name: sandypockets
  8 |   picture: '/assets/blog/authors/sandypockets_avatar.jpg'
  9 | ogImage:
 10 |   url: '/assets/blog/getting-started/cover.jpg'
 11 | ---
 12 | 
 13 | # Getting started
 14 | These steps will guide you through creating your own local copy of the project. You'll learn how to quickly get it up and running, so you can get back to creating your content.
 15 | 
 16 | This app was built and tested with Node `14`. It uses React `17.0.2`, the latest version of `Next`, and Tailwind CSS `2.2.4`. Check out the `package.json` file for a complete list of dependencies.
 17 | 
 18 | ## Use create-next-app
 19 | 
 20 | Create Next App is the fastest way to begin using this blog starter. You can use NPM or Yarn, but Yarn is recommended. 
 21 | 
 22 | 1. Replace `my-new-blog` in either of the examples below (with whatever you'd like to use as the root directory), and run the command. 
 23 | 
 24 | ```
 25 | # with npm
 26 | npx create-next-app --example https://github.com/sandypockets/nextjs-blog-starter/tree/main my-new-blog
 27 | ```
 28 | 
 29 | 
30 | 31 | ```shell 32 | # with yarn 33 | yarn create next-app --example https://github.com/sandypockets/nextjs-blog-starter/tree/main my-new-blog 34 | ``` 35 | 36 | 2. Once the project is created, change into the directory. 37 | 38 | ```shell 39 | cd my-new-blog 40 | ``` 41 | 42 | 3. Install dependencies. 43 | 44 | ```shell 45 | yarn install 46 | ``` 47 | 48 | or if you used npm 49 | 50 | ```shell 51 | npm install 52 | ``` 53 | 54 | ### Development server 55 | 56 | You can start the development server with `yarn`, `npm`, or `next`. 57 | 58 | ```shell 59 | # yarn 60 | yarn dev 61 | 62 | # npm 63 | npm dev 64 | 65 | # next 66 | next dev 67 | ``` 68 | 69 | Once the server is running, visit [http://localhost:3000](http://localhost:3000) in your browser. 70 | 71 | ### Constants 72 | 73 | Set up each of the constants, much like you would a `.env`, in the `lib/constants.js` file. 74 | 75 | ```javascript 76 | export const EXAMPLE_PATH = 'blog-starter' 77 | export const CMS_NAME = 'Markdown' 78 | export const HOME_OG_IMAGE_URL = 'https://og-image.vercel.app/Next.js%20Blog%20Starter%20Example.png?theme=light&md=1&fontSize=100px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg' 79 | export const BLOG_NAME = 'Next.js Blog Starter' 80 | export const KEYWORDS = 'starter, blog, next.js, template' 81 | export const DESCRIPTION = 'A starter blog template for Next.js' 82 | export const AUTHOR = 'sandypockets' 83 | export const LANG = 'en-CA' 84 | export const GITHUB_REPO = 'https://github.com/sandypockets' 85 | ``` 86 | 87 | ### Generate a sitemap and robots.txt 88 | 89 | Sitemaps are an important part of SEO. This section walks through adding your base URL to the sitemap generator. The generator runs automatically after each build, generating a new sitemap each time you update your blog. 90 | 91 | The generated sitemap can be found in `public/sitemap.xml`. This command also generates a new `public/robots.txt` file. 92 | 93 | 1. Open the `scripts/generate.sitemap.mjs` file. 94 | 2. On Line 5, replace `https://blog-starter.sandypockets.dev` with your own blog's website. 95 | 3. On Line 6, replace `en-CA` with your preferred locale (`en-UK`, `en-US`, etc.). This is used to determine the format of the date stamp in the sitemap. 96 | 97 | When you're ready to test it: 98 | 99 | 1. Run `yarn build` 100 | 2. Check the `public/` directory for the `sitemap.xml` file and `robots.txt` file. 101 | 3. Run `yarn start` 102 | 4. Visit `http://localhost:3000/sitemap.xml` 103 | 104 | If you see the xml sitemap, then it was successful. 105 | 106 | ### RSS Feed 107 | An RSS feed is available for the blog at `/feed.xml`. However, you must first configure the RSS generator to use your own URL. 108 | 109 | 1. Open the `scripts/generate-rss.mjs` file. 110 | 2. On Line 7, replace the `https://blog-starter.sandypockets.dev` value of `BLOG_URL` with your own. 111 | 112 | When you're ready to test it: 113 | 114 | 1. Run `yarn build` 115 | 2. Check the `public/` directory for the `feed.xml` file. 116 | 3. Run `yarn start` 117 | 4. Visit `http://localhost:3000/feed.xml` 118 | 119 | If you see the xml RSS feed, then it was successful. 120 | 121 | ### Set up Google Analytics 122 | 123 | You will need to have your Google tag ID. If you do not have one, or do not have a Google Analytics account, you can sign up at [analytics.google.com](https://analytics.google.com/) 124 | 125 | 1. Create a copy of the `.env.local.example` file, and name it `.env.local`. In the terminal, run: 126 | 127 | ```shell 128 | cp .env.local.example .env.local 129 | ``` 130 | 131 | 2. Grab your Google tag ID from your Analytics account, and replace the `G-XXXXXXXXXX` in the new `.env.local` file you just created. 132 | 133 | ### Set up SendGrid 134 | You will need a free SendGrid account, which allows you to send up to 100 emails each day. Replace the `REPLACE-WITH-YOUR-API-KEY` text in the `.env.exa 135 | 136 | 1. Get your SendGrid API key from your SendGrid account. 137 | 2. Open the `.env.local` file that you created when setting up Google Analytics. 138 | 3. Replace `REPLACE-WITH-YOUR-API-KEY` with your actual API key from SendGrid. 139 | 140 | ### Storybook 141 | 142 | Storybook is handy for crafting and tweaking components. Styling components in isolation this way can help make them more composable, since they're less reliant on the environment they're rendered in. Storybook runs on a separate server, so you can run it alongside the development server if you prefer. 143 | 144 | 1. To use Storybook, start the Storybook server. 145 | 146 | ```shell 147 | yarn storybook 148 | ``` 149 | 150 | 2. Visit [http://localhost:6006/](http://localhost:6006/) in your browser. 151 | 152 | ## Building for production 153 | 154 | While it's always a good idea to check out the build, it is especially important with Tailwind CSS. Tailwind purges unused classes to keep the build size light. However, if you've added classes dynamically with JavaScript, then you should be sure those classes weren't removed during build. Learn more in the [Optimizing for production](https://tailwindcss.com/docs/optimizing-for-production) section of the Tailwind docs. 155 | 156 | 1. Start building. 157 | 158 | ```shell 159 | yarn build 160 | ``` 161 | 162 | 2. Once building is complete, run the build. 163 | 164 | ```shell 165 | yarn start 166 | ``` 167 | 168 | 3. Visit [http://localhost:3000](http://localhost:3000) in your browser. 169 | 170 | ## Adding content 171 | 172 | Learn how to add content like blog posts or pages. 173 | 174 | ### Add a blog post 175 | 176 | 1. Add a new Markdown file (`.md`) to the `/_posts` directory. 177 | 2. Add the required front matter (described below) and adjust it as needed. 178 | 3. Images related to the post should be stored in within the appropriate the `/public/assets/blog` directory. It is recommended you create a new folder for each blog post to prevent the images files from growing unwieldy. 179 | 4. That's it. Your new post will show up alongside the others on your homepage. 180 | 181 | > Note: The slug is the path that will be displayed, and is based on the title of your `.md` file. For example, `kobe.md` becomes `localhost:3000/posts/kobe` 182 | 183 | #### How it works 184 | Blog posts are stored in the `/_posts` directory as Markdown files. To create a new post, simply add a new markdown file (`.md`) to the `/_posts` directory. Each post must include the appropriate front matter. 185 | 186 | To create the blog posts we use [`remark`](https://github.com/remarkjs/remark) and [`remark-html`](https://github.com/remarkjs/remark-html) to convert the Markdown files into an HTML string, and then send it down as a prop to the page. The metadata of every post is handled by [`gray-matter`](https://github.com/jonschlinkert/gray-matter) and also sent in props to the page. 187 | 188 | ##### Front matter 189 | An example of the required front matter: 190 | 191 | > Note: The formatting of the front matter is important. Ensure the indentation remains the same. 192 | 193 | ```text 194 | --- 195 | title: 'A blog starter you actually want to use' 196 | excerpt: 'There are hundreds of different blog starters out there. But none felt quite right. So I built my own. Based off the basic Next.js Blog Starter, but now with several handy features like dark mode (using local storage) or Google Analytics. It comes with Storybook too.' 197 | coverImage: '/assets/blog/dynamic-routing/tree.jpg' 198 | date: '2021-08-24T05:35:07.322Z' 199 | author: 200 | name: sandypockets 201 | picture: '/assets/blog/authors/sandypockets_avatar.jpg' 202 | ogImage: 203 | url: '/assets/blog/dynamic-routing/tree.jpg' 204 | --- 205 | ``` 206 | 207 | Adjust the value of each key as needed. 208 | 209 | ### Add a page 210 | 211 | Next.js makes routing easy. All pages live in the `/pages` directory. The file names used in that directory will map to the path in the browser. 212 | 213 | > Example: `/pages/cool-page.jsx` becomes `localhost:3000/pages/cool-page` 214 | 215 | ### Manage global data 216 | 217 | Global variables, like your blog's name, or your GitHub URL, can be managed in `/lib/constants.js` 218 | 219 | You can adjust the blog's metadata (some of which relies on the global variables described above) can be adjusted in the `/components/Layout/Meta.jsx` component. -------------------------------------------------------------------------------- /_posts/markdown-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Markdown reference' 3 | excerpt: "Sick of starters that accept markdown, but don't come with any built-in styling? Check out what works right out of the box." 4 | coverImage: '/assets/blog/markdown-reference/book.jpg' 5 | date: '2021-08-24T05:35:07.322Z' 6 | author: 7 | name: sandypockets 8 | picture: '/assets/blog/authors/sandypockets_avatar.jpg' 9 | ogImage: 10 | url: '/assets/blog/nextjs-for-blogs/cover.jpg' 11 | --- 12 | 13 | # Markdown examples 14 | Plenty of markdown starters don't come with any markdown styling. Your content to falls flat, and instead of spending the afternoon writing, you're nitpicking about padding around bullet points. Not this starter, everything is ready right out of the box. Check out how each element is styled below. 15 | 16 | --- 17 | 18 | ### Headings 19 | 20 | ```markdown 21 | # Heading 1 22 | 23 | ## Heading 2 24 | 25 | ### Heading 3 26 | 27 | #### Heading 4 28 | 29 | ##### Heading 5 30 | 31 | ###### Heading 6 32 | ``` 33 | 34 | # Heading 1 35 | 36 | ## Heading 2 37 | 38 | ### Heading 3 39 | 40 | #### Heading 4 41 | 42 | ##### Heading 5 43 | 44 | ###### Heading 6 45 | 46 | --- 47 | 48 | ### Emphasis 49 | 50 | ```markdown 51 | Emphasis, aka italics, with *asterisks* or _underscores_. 52 | 53 | Strong emphasis, aka bold, with **asterisks** or __underscores__. 54 | 55 | Combined emphasis with **asterisks and _underscores_**. 56 | 57 | ``` 58 | 59 | Emphasis, aka italics, with *asterisks* or _underscores_. 60 | 61 | Strong emphasis, aka bold, with **asterisks** or __underscores__. 62 | 63 | Combined emphasis with **asterisks and _underscores_**. 64 | 65 | --- 66 | 67 | ### Blockquotes 68 | 69 | ```markdown 70 | > This is a block quote 71 | 72 | > Another blockquote 73 | > > With a nested reply 74 | ``` 75 | 76 | > This is a block quote 77 | 78 | > Another blockquote 79 | > > With a nested reply 80 | 81 | --- 82 | 83 | ### Links 84 | 85 | ```markdown 86 | [I'm an inline-style link](https://www.google.com) 87 | 88 | [I'm an inline-style link with title](https://www.google.com "Google's Homepage") 89 | 90 | [I'm a relative reference to a local file or page](../about) 91 | 92 | URLs in angle brackets will automatically get turned into links. Check out 93 | http://www.example.com or and sometimes 94 | example.com. 95 | ``` 96 | 97 | [I'm an inline-style link](https://www.google.com) 98 | 99 | [I'm an inline-style link with title](https://www.google.com "Google's Homepage") 100 | 101 | [I'm a relative reference to a local file or page](../about) 102 | 103 | URLs in angle brackets will automatically get turned into links. Check out 104 | http://www.example.com or and sometimes 105 | example.com. 106 | 107 | --- 108 | 109 | ### Lists 110 | 111 | ##### Ordered 112 | 113 | 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 114 | 2. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 115 | 3. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 116 | 117 | ##### Unordered 118 | 119 | - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 120 | - Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 121 | - Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 122 | 123 | --- 124 | 125 | ### Images 126 | 127 | ```markdown 128 | ![Some image alt](https://images.unsplash.com/photo-1487700160041-babef9c3cb55?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1635&q=80) 129 | ``` 130 | 131 | ![Some image alt](https://images.unsplash.com/photo-1487700160041-babef9c3cb55?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1635&q=80) 132 | 133 | You can use relative image paths too. 134 | 135 | ```markdown 136 | ![Some alt text](/assets/blog/a-nextjs-blog-starter-you-actually-want-to-use/tree-minimal.jpg) 137 | ``` 138 | 139 | ![Some alt text](/assets/blog/a-nextjs-blog-starter-you-actually-want-to-use/tree-minimal.jpg) 140 | 141 | --- 142 | 143 | ### Horizontal rule 144 | 145 | The horizontal rule is what's used on this page to separate each section. 146 | 147 | ```markdown 148 | --- 149 | ``` 150 | 151 | --- 152 | 153 | ### Code 154 | 155 | ```markdown 156 | Inline `code` has `back-ticks around` it. 157 | ``` 158 | 159 | Inline `code` has `back-ticks around` it. 160 | 161 | Or multi line, with built-in syntax highlighting: 162 | 163 | ##### JSX 164 | 165 | ```jsx 166 | export default function SomeComponent() { 167 | return ( 168 | <> 169 |

The title

170 |
171 |

Some content

172 |

Some other content

173 |
174 | 175 | ) 176 | } 177 | ``` 178 | 179 | The syntax highlighting isn't just for React either. It supports most common languages. 180 | 181 | ##### HTML 182 | 183 | ```html 184 | 185 | 186 | 187 |

Some title

188 |

Some content

189 | 190 | 191 | ``` 192 | 193 | ##### Sass 194 | 195 | ```sass 196 | 197 | $primaryYellow: #FFC017; 198 | $primaryBlue: #174BFF; 199 | $primaryFont: Newsreader; 200 | 201 | div.topics-container { 202 | display: flex; 203 | justify-content: center; 204 | margin: 0 0 4rem 0; 205 | 206 | nav.topics { 207 | display: flex; 208 | flex-direction: row; 209 | justify-content: center; 210 | flex-wrap: wrap; 211 | max-width: 50vw; 212 | 213 | a { 214 | color: black; 215 | text-decoration: none; 216 | &.active { 217 | color: $primaryYellow; 218 | } 219 | } 220 | } 221 | } 222 | ``` 223 | 224 | ##### Ruby 225 | 226 | ```ruby 227 | class ApplicationController < ActionController::Base 228 | 229 | before_action :find_current_user 230 | 231 | helper_method :has_session? 232 | 233 | def logged_in 234 | redirect_to new_session_path unless has_session? 235 | end 236 | 237 | def find_current_user 238 | @current_user = (User.find(session[:user_id]) if has_session?) 239 | end 240 | 241 | def has_session? 242 | session[:user_id].present? 243 | end 244 | end 245 | ``` 246 | 247 | And many more. Check out the [full list of supported langauges](https://prismjs.com/#supported-languages) on PrismJS. 248 | 249 | --- 250 | 251 | ### Collapsible summaries 252 | 253 | ```html 254 |
255 | Click to open or close 256 | ```ejs 257 |
258 | <% for(let item in product) { %> 259 |
260 |

<%= product[item].id %>

261 | ... 262 |

263 | <%= product[item].name %> 264 |

265 |

266 | <%= product[item].description %> 267 |

268 | 273 |
274 | <% } %> 275 |
276 | ``` 277 |
278 | ``` 279 | 280 |
281 | 282 |
283 | Click to open or close 284 | ```ejs 285 |
286 | <% for(let item in product) { %> 287 |
288 |

<%= product[item].id %>

289 | ... 290 |

291 | <%= product[item].name %> 292 |

293 |

294 | <%= product[item].description %> 295 |

296 | 301 |
302 | <% } %> 303 |
304 | ``` 305 | 306 |
307 | 308 | -------------------------------------------------------------------------------- /components/Home/HeroPost.jsx: -------------------------------------------------------------------------------- 1 | import Avatar from '../Image/Avatar' 2 | import DateFormatter from '../Utils/DateFormatter' 3 | import CoverImage from '../Image/CoverImage' 4 | import Link from 'next/link' 5 | 6 | export default function HeroPost({ 7 | title, 8 | coverImage, 9 | date, 10 | excerpt, 11 | author, 12 | slug, 13 | }) { 14 | return ( 15 |
16 |
17 | 25 |
26 |
27 |
28 |

29 | 30 | {title} 31 | 32 |

33 |
34 | 35 |
36 |
37 |
38 |

{excerpt}

39 | 40 |
41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /components/Home/Intro.jsx: -------------------------------------------------------------------------------- 1 | export default function Intro() { 2 | return ( 3 |
4 |

5 | Blog. 6 |

7 |

8 | Get a beautiful, feature-rich blog up and running. Fast. 9 |

10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/Home/MoreStories.jsx: -------------------------------------------------------------------------------- 1 | import PostPreview from './PostPreview' 2 | 3 | export default function MoreStories({ posts }) { 4 | return ( 5 |
6 |

7 | More Stories 8 |

9 |
10 | {posts.map((post) => ( 11 | 20 | ))} 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/Home/PostPreview.jsx: -------------------------------------------------------------------------------- 1 | import Avatar from '../Image/Avatar' 2 | import DateFormatter from '../Utils/DateFormatter' 3 | import CoverImage from '../Image/CoverImage' 4 | import Link from 'next/link' 5 | 6 | export default function PostPreview({ 7 | title, 8 | coverImage, 9 | date, 10 | excerpt, 11 | author, 12 | slug, 13 | }) { 14 | return ( 15 |
16 |
17 | 24 |
25 |

26 | 27 | {title} 28 | 29 |

30 |
31 | 32 |
33 |

{excerpt}

34 | 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/Icons/MailIcon.jsx: -------------------------------------------------------------------------------- 1 | export default function Thing({ className }) { 2 | return ( 3 | 11 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/Icons/MapPinIcon.jsx: -------------------------------------------------------------------------------- 1 | export default function MapPinIcon({ className }) { 2 | return ( 3 | 11 | 16 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/Icons/MenuIcon.jsx: -------------------------------------------------------------------------------- 1 | export default function MenuIcon({ className }) { 2 | return ( 3 | 11 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/Icons/XMarkIcon.jsx: -------------------------------------------------------------------------------- 1 | export default function XMarkIcon({ className }) { 2 | return ( 3 | 11 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/Image/Avatar.jsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export default function Avatar({ name, picture }) { 4 | return ( 5 |
6 | {name} 13 |
{name}
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /components/Image/CoverImage.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Image from 'next/image' 3 | 4 | export default function CoverImage({ 5 | title, 6 | src, 7 | slug, 8 | height, 9 | width, 10 | coverImagePriority, 11 | }) { 12 | const image = ( 13 | {`Cover 23 | ) 24 | return ( 25 |
26 | {slug ? ( 27 | 28 | {image} 29 | 30 | ) : ( 31 | image 32 | )} 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/Layout/Container.jsx: -------------------------------------------------------------------------------- 1 | export default function Container({ children }) { 2 | return
{children}
3 | } 4 | -------------------------------------------------------------------------------- /components/Layout/Footer.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Container from './Container' 3 | import Highlight from '../Utils/Highlight' 4 | import { BLOG_NAME } from '../../lib/constants' 5 | 6 | const navigation = { 7 | main: [ 8 | { name: 'Home', href: '/' }, 9 | { name: 'About', href: '/about' }, 10 | { name: 'Contact', href: '/contact' }, 11 | ], 12 | social: [ 13 | { 14 | name: 'Facebook', 15 | href: '#', 16 | icon: (props) => ( 17 | 18 | 23 | 24 | ), 25 | }, 26 | { 27 | name: 'Instagram', 28 | href: '#', 29 | icon: (props) => ( 30 | 31 | 36 | 37 | ), 38 | }, 39 | { 40 | name: 'Twitter', 41 | href: '#', 42 | icon: (props) => ( 43 | 44 | 45 | 46 | ), 47 | }, 48 | { 49 | name: 'GitHub', 50 | href: 'https://github.com/sandypockets', 51 | icon: (props) => ( 52 | 53 | 58 | 59 | ), 60 | }, 61 | { 62 | name: 'Dribbble', 63 | href: '#', 64 | icon: (props) => ( 65 | 66 | 71 | 72 | ), 73 | }, 74 | ], 75 | } 76 | 77 | export default function Footer() { 78 | return ( 79 |
80 | 81 |
82 | 96 |
97 | {navigation.social.map((item, index) => ( 98 | 103 | {item.name} 104 | 106 | ))} 107 |
108 |

109 | © {new Date().getFullYear()} {BLOG_NAME} 110 | , Inc. All rights reserved. 111 |

112 |
113 |
114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /components/Layout/Layout.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import Footer from './Footer' 3 | import Meta from './Meta' 4 | import Nav from './Nav' 5 | 6 | export default function Layout({ children }) { 7 | const [darkMode, setDarkMode] = useState() 8 | 9 | useEffect(() => { 10 | if ( 11 | localStorage.theme === 'dark' || 12 | (!('theme' in localStorage) && 13 | window.matchMedia('(prefers-color-scheme: dark)').matches) 14 | ) { 15 | document.documentElement.classList.add('dark') 16 | setDarkMode(true) 17 | } else { 18 | document.documentElement.classList.remove('dark') 19 | setDarkMode(false) 20 | } 21 | }, [darkMode]) 22 | 23 | return ( 24 | <> 25 | 26 |
27 |
31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /components/Layout/Meta.jsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { HOME_OG_IMAGE_URL } from '../../lib/constants' 3 | 4 | // Favicon should be recreated at various sizes for each link below. 5 | export default function Meta() { 6 | // const [theme, setTheme] = useState('okaidia'); 7 | const theme = 'okaidia' 8 | 9 | return ( 10 | 11 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /components/Layout/Nav.jsx: -------------------------------------------------------------------------------- 1 | import { Disclosure } from '@headlessui/react' 2 | import Link from 'next/link' 3 | import Toggle from '../Utils/Toggle' 4 | import Highlight from '../Utils/Highlight' 5 | import { useRouter } from 'next/router' 6 | import MenuIcon from '../Icons/MenuIcon' 7 | import XMarkIcon from '../Icons/XMarkIcon' 8 | 9 | const navigation = [ 10 | { 11 | name: 'About', 12 | href: '/about', 13 | }, 14 | { 15 | name: 'Contact', 16 | href: '/contact', 17 | }, 18 | ] 19 | 20 | export default function Nav({ darkMode, setDarkMode }) { 21 | const router = useRouter() 22 | const currentPath = router.pathname 23 | 24 | return ( 25 | 29 | {({ open }) => ( 30 | <> 31 |
32 |
33 |
34 |
35 | 36 | 37 | SANDYPOCKETS 38 | 39 | 40 |
41 |
42 | {navigation.map((item, index) => ( 43 | 44 | 51 | {item.name} 52 | 53 | 54 | ))} 55 |
56 |
57 |
58 | 59 |
60 |
61 | {/* Mobile menu button */} 62 | 63 | Open main menu 64 | {open ? ( 65 | 70 |
71 |
72 |
73 | 74 | 75 |
76 | {navigation.map((item, index) => ( 77 | 78 | 85 | {item.name} 86 | 87 | 88 | ))} 89 |
90 |
91 | 92 |
93 |
94 | 95 | )} 96 |
97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /components/PageHeading.jsx: -------------------------------------------------------------------------------- 1 | export default function PageHeading({ children }) { 2 | return ( 3 |
4 |

5 | {children} 6 |

7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /components/Post/Header.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function Header() { 4 | return ( 5 |

6 | 7 | Blog 8 | 9 | . 10 |

11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/Post/PostBody.jsx: -------------------------------------------------------------------------------- 1 | import markdownStyles from './markdown-styles.module.css' 2 | 3 | export default function PostBody({ content }) { 4 | return ( 5 |
6 |
10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/Post/PostHeader.jsx: -------------------------------------------------------------------------------- 1 | import Avatar from '../Image/Avatar' 2 | import CoverImage from '../Image/CoverImage' 3 | import DateFormatter from '../Utils/DateFormatter' 4 | import PostTitle from './PostTitle' 5 | 6 | export default function PostHeader({ title, coverImage, date, author }) { 7 | return ( 8 | <> 9 | {title} 10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /components/Post/PostTitle.jsx: -------------------------------------------------------------------------------- 1 | export default function PostTitle({ children }) { 2 | return ( 3 |

4 | {children} 5 |

6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /components/Post/markdown-styles.module.css: -------------------------------------------------------------------------------- 1 | /* Markdown CSS */ 2 | .markdown code[class*='language-'] { 3 | text-shadow: none; 4 | } 5 | 6 | .markdown { 7 | @apply text-lg leading-relaxed; 8 | } 9 | 10 | .markdown a { 11 | @apply text-blue-500 hover:text-blue-700; 12 | } 13 | 14 | .markdown code, 15 | .markdown pre, 16 | .markdown pre > code { 17 | @apply font-mono text-sm font-light; 18 | } 19 | 20 | .markdown code { 21 | @apply border border-gray-300 border-opacity-25 p-0.5; 22 | } 23 | 24 | .markdown pre > code { 25 | @apply p-0.5 border-0; 26 | } 27 | 28 | .markdown pre { 29 | @apply pl-5 px-1 py-5 overflow-scroll; 30 | @apply whitespace-pre bg-gray-100; 31 | } 32 | 33 | .markdown h2 > code { 34 | @apply text-3xl mt-12 mb-4 leading-snug font-mono; 35 | } 36 | 37 | .markdown h3 > code { 38 | @apply text-2xl mt-8 mb-4 leading-snug font-mono; 39 | } 40 | 41 | .markdown p { 42 | @apply font-light; 43 | } 44 | 45 | .markdown p strong { 46 | @apply font-semibold; 47 | } 48 | 49 | .markdown blockquote { 50 | @apply italic bg-gray-600 bg-opacity-20 py-1 px-5 border-l-4 border-gray-400; 51 | } 52 | 53 | .markdown p, 54 | .markdown ul, 55 | .markdown ol, 56 | .markdown blockquote { 57 | @apply my-6; 58 | } 59 | 60 | .markdown ul { 61 | @apply list-disc ml-10 font-light; 62 | } 63 | 64 | .markdown ul li { 65 | @apply px-0 py-2; 66 | } 67 | 68 | .markdown ol { 69 | @apply list-decimal mx-10 font-light; 70 | } 71 | 72 | .markdown ol li { 73 | @apply px-0 py-2; 74 | } 75 | 76 | .markdown hr { 77 | @apply my-10; 78 | } 79 | 80 | .markdown h1, 81 | .markdown h2, 82 | .markdown h3, 83 | .markdown h4, 84 | .markdown h5, 85 | .markdown h6 { 86 | @apply font-bold tracking-tighter leading-tight; 87 | } 88 | 89 | .markdown h1 { 90 | @apply text-4xl md:text-6xl lg:text-7xl mt-14 mb-4; 91 | } 92 | 93 | .markdown h2 { 94 | @apply text-4xl md:text-5xl lg:text-6xl mt-12 mb-4; 95 | } 96 | 97 | .markdown h3 { 98 | @apply text-3xl md:text-4xl lg:text-5xl mt-8 mb-4; 99 | } 100 | 101 | .markdown h4 { 102 | @apply text-2xl md:text-3xl lg:text-4xl mt-8 mb-4; 103 | } 104 | 105 | .markdown h5 { 106 | @apply text-xl md:text-2xl lg:text-3xl mt-8 mb-4; 107 | } 108 | 109 | .markdown h6 { 110 | @apply text-lg md:text-xl lg:text-2xl mt-6 mb-4; 111 | } 112 | 113 | .markdown table { 114 | @apply table-auto; 115 | } 116 | -------------------------------------------------------------------------------- /components/Utils/ContactForm/ContactDetails.jsx: -------------------------------------------------------------------------------- 1 | import MapPinIcon from '../../Icons/MapPinIcon' 2 | import MailIcon from '../../Icons/MailIcon' 3 | 4 | export default function ContactDetails() { 5 | return ( 6 |
7 |
8 |

9 | Reach me by email, or by submitting this contact form. 10 |

11 |
12 |
13 |
Postal address
14 |
15 |
21 |
22 |
23 |
Email
24 |
25 |
31 |
32 |
33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /components/Utils/ContactForm/ContactForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import * as gtag from '../../../lib/gtag' 3 | import axios from 'axios' 4 | 5 | export default function ContactForm() { 6 | const [formContents, setFormContents] = useState({ 7 | name: '', 8 | email: '', 9 | phone: '', 10 | message: '', 11 | }) 12 | 13 | const handleName = (e) => { 14 | setFormContents((prev) => ({ ...prev, name: e.target.value })) 15 | } 16 | const handleEmail = (e) => { 17 | setFormContents((prev) => ({ ...prev, email: e.target.value })) 18 | } 19 | const handlePhone = (e) => { 20 | setFormContents((prev) => ({ ...prev, phone: e.target.value })) 21 | } 22 | const handleMessage = (e) => { 23 | setFormContents((prev) => ({ ...prev, message: e.target.value })) 24 | } 25 | 26 | const handleSubmit = (e) => { 27 | console.log('19 - formContents: ', formContents) 28 | e.preventDefault() 29 | gtag.event({ 30 | action: 'submit_form', 31 | category: 'Contact', 32 | label: `Contact: ${formContents.email}`, 33 | value: formContents.message, 34 | }) 35 | 36 | const msg = { 37 | subject: `New message from ${formContents.name} 👋`, 38 | text: `${formContents.name}, ${formContents.email} - ${formContents.message}`, 39 | } 40 | 41 | axios 42 | .post('/api/email', { 43 | msg, 44 | }) 45 | .then(function (response) { 46 | console.log(response) 47 | }) 48 | .catch(function (error) { 49 | console.log(error) 50 | }) 51 | 52 | setFormContents({ 53 | name: '', 54 | email: '', 55 | phone: '', 56 | message: '', 57 | }) 58 | } 59 | 60 | return ( 61 |
62 |
63 |
64 |
65 | 68 | handleName(e)} 80 | /> 81 |
82 |
83 | 86 | handleEmail(e)} 98 | /> 99 |
100 |
101 | 104 | handlePhone(e)} 116 | /> 117 |
118 |
119 | 122 |