├── .gitignore ├── LICENSE ├── README.md ├── config.json ├── content └── posts │ ├── license.mdx │ ├── lorem-ipsum.mdx │ ├── markdown-syntax.mdx │ ├── references.mdx │ ├── rich-content-with-mdx.mdx │ └── welcome.mdx ├── global.d.ts ├── jest.config.js ├── meta ├── authors.yml └── tags.yml ├── netlify.toml ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── admin │ ├── config.yml │ └── index.html ├── favicon.ico ├── icon.png ├── images │ └── 600x300.png ├── og_image.png ├── site.webmanifest └── styles │ ├── content.module.css │ └── global.css ├── src ├── __tests__ │ └── pagination.test.ts ├── assets │ ├── github-alt.svg │ └── twitter-alt.svg ├── components │ ├── Author.tsx │ ├── Burger.tsx │ ├── Copyright.tsx │ ├── Date.tsx │ ├── Layout.tsx │ ├── Navigation.tsx │ ├── Pagination.tsx │ ├── PostItem.tsx │ ├── PostLayout.tsx │ ├── PostList.tsx │ ├── SocialList.tsx │ ├── TagButton.tsx │ ├── TagLink.tsx │ ├── TagPostList.tsx │ └── meta │ │ ├── BasicMeta.tsx │ │ ├── JsonLdMeta.tsx │ │ ├── OpenGraphMeta.tsx │ │ └── TwitterCardMeta.tsx ├── lib │ ├── authors.ts │ ├── config.ts │ ├── pagination.ts │ ├── posts.ts │ └── tags.ts └── pages │ ├── _app.tsx │ ├── index.tsx │ └── posts │ ├── [post].tsx │ ├── index.tsx │ ├── page │ └── [page].tsx │ └── tags │ └── [[...slug]].tsx ├── tsconfig.json └── 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 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # dist 28 | /dist 29 | 30 | # Local Netlify folder 31 | .netlify 32 | 33 | # mdx 34 | .mdx-data 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Takahiro Fujiwara 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Next.js blogging template for Netlify](https://repository-images.githubusercontent.com/284910441/d8efc300-e2ae-11ea-9596-b01e3844e39d) 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/c6f44d34-0570-4ca0-9d3d-cabdaa2b3afb/deploy-status)](https://app.netlify.com/sites/nextjs-netlify-blog-template/deploys) 4 | [![MADE BY Next.js](https://img.shields.io/badge/MADE%20BY%20Next.js-000000.svg?style=flat&logo=Next.js&labelColor=000)](https://nextjs.org/) 5 | 6 | Next.js blogging template for Netlify is a boilerplate for building blogs with only Netlify stacks. 7 | 8 | There are some boilerplate or tutorials for the combination of Next.js and Netlify on GitHub. These resources have documentation and good tutorial to get started Next.js and Netlify quickly, but they are too simple to build blogs with standard features like tagging. 9 | 10 | Next.js blogging template for Netlify has already implemented these standard features for building blogs with only using Next.js and Netlify stacks. 11 | 12 | ## Demo 13 | 14 | Deploy on your environment by clicking here: 15 | 16 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/wutali/nextjs-netlify-blog-template&stack=cms) 17 | 18 | Or access the following demo site: 19 | 20 | [Next.js blog template for Netlify](https://nextjs-netlify-blog-template.netlify.app/) 21 | 22 | ## Features 23 | 24 | - **Tagging**: organizes content by tags 25 | - **Author**: displays author names who write a post 26 | - **Pagination**: limits the number of posts per page 27 | - **CMS**: built with CMS to allow editors modifying content with the quickest way 28 | - **SEO optimized**: built-in metadata like JSON-LD 29 | - **Shortcode**: extends content writing with React component like WordPress shortcodes 30 | 31 | ## Dependencies 32 | 33 | - [TypeScript](https://www.typescriptlang.org/) 34 | - [Next.js](https://nextjs.org/) 35 | - [Netlify](https://www.netlify.com/) 36 | - [MDX](https://mdxjs.com/) 37 | 38 | ## Getting started 39 | 40 | To create your blog using the template, open your terminal, `cd` into the directory you'd like to create the app in, 41 | and run the following command: 42 | 43 | ``` 44 | npx create-next-app your-blog --example "https://github.com/wutali/nextjs-netlify-blog-template" 45 | ``` 46 | 47 | After that, set up your project as following the Netlify blog: 48 | 49 | [A Step-by-Step Guide: Deploying on Netlify](https://www.netlify.com/blog/2016/09/29/a-step-by-step-guide-deploying-on-netlify/) 50 | 51 | ## Customization 52 | 53 | This template is just a template and a boilerplate in which users can customize anything after the project was cloned and started. 54 | The following instructions introduce common customization points like adding new metadata or applying a new design theme. 55 | 56 | ### Styling pages by a customized theme 57 | 58 | All source codes related to the blog are under [components](/src/components) and [pages](/src/pages) directory. 59 | You can modify it freely if you want to apply your design theme. 60 | All components use [styled-jsx](https://github.com/vercel/styled-jsx) and [css-modules](https://github.com/css-modules/css-modules) to define their styles, but you can choose any styling libraries for designing your theme. 61 | 62 | The directory tree containing the blog source code are described below: 63 | 64 | ``` 65 | meta: yaml files defining metadata like authors or tags 66 | public: images, favicons and other static assets 67 | src 68 | ├── assets: other assets using inside of components 69 | ├── components: pieces of components consisting of pages 70 | ├── content: mdx files for each post page 71 | ├── lib: project libraries like data fetching or pagination 72 | └── pages: page components managing by Next.js 73 | ``` 74 | 75 | ### Organizing content by categories 76 | 77 | The category metadata that associates with content have the same relationship with the authors' one. 78 | Then reference these implementations for adding new metadata: 79 | 80 | - [public/admin/config.yml](/public/admin/config.yml#L51): author metadata definition for Netlify CMS 81 | - [src/lib/authors.tsx](/src/lib/authors.ts): fetches metadata and defines utility functions for components 82 | - [meta/authors.yml](/src/meta/authors.yml): author content managed by Netlify CMS 83 | - [src/components/PostLayout.tsx](/src/components/PostLayout.tsx): displays author content for each page 84 | 85 | You understood they have four steps to add the category metadata on your project after you read the above source codes: 86 | 87 | 1. Define the category metadata on the above Netlify config file 88 | 2. Create an empty file named with `categories.yml` under [meta](/src/meta/) directory 89 | 3. Create a new module for fetching category metadata 90 | 4. Display the category metadata on [src/components/PostLayout.tsx](/src/components/PostLayout.tsx#L75) or other components you want 91 | 92 | It is all you have to do. After that, you can access Netlify CMS and create new categories at any time. 93 | 94 | ### Locale settings for Netlify CMS 95 | 96 | Modify [config.yml](/public/admin/config.yml) and 97 | [index.html](/public/admin/index.html) under [public/admin](/public/admin/) directory 98 | as following instructions: 99 | 100 | [Netlify CMS - Configuration Options #Locale](https://www.netlifycms.org/docs/configuration-options/#locale) 101 | 102 | ## References 103 | 104 | - [Netlify CMS Documentation](https://www.netlifycms.org/docs/intro/) 105 | - [Building a Markdown blog with Next 9.4 and Netlify](https://www.netlify.com/blog/2020/05/04/building-a-markdown-blog-with-next-9.4-and-netlify/) 106 | - [Hugo Theme - Codex](https://github.com/jakewies/hugo-theme-codex) 107 | - [Next.js Starter Template for TypeScript](https://github.com/vercel/next-learn-starter/tree/master/typescript-final) 108 | - [Building Blog with NextJS and Netlify CMS](https://dev.to/mefaba/building-blog-with-nextjs-and-netlify-cms-fom) 109 | - [Unicons](https://github.com/Iconscout/unicons) 110 | 111 | ## License 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "https://wutali-nextjs-netlify-blog.netlify.app", 3 | "site_title": "Next.js Netlify Blog", 4 | "site_description": "Next.js blog template for Netlify", 5 | "site_keywords": [ 6 | { 7 | "keyword": "Next.js" 8 | }, 9 | { 10 | "keyword": "Netlify" 11 | }, 12 | { 13 | "keyword": "React" 14 | } 15 | ], 16 | "posts_per_page": 5, 17 | "twitter_account": "@my-account", 18 | "github_account": "wutali" 19 | } -------------------------------------------------------------------------------- /content/posts/license.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: license 3 | title: License 4 | date: 2020-06-08 5 | author: wutali 6 | tags: 7 | - license 8 | --- 9 | 10 | ## Next.js and netlify blog template is managed under the MIT license 11 | 12 | Copyright 2020 Takahiro Fujiwara 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /content/posts/lorem-ipsum.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: lorem-ipsum 3 | title: Lorem Ipsum 4 | date: 2020-06-03 5 | author: wutali 6 | tags: 7 | - mock 8 | --- 9 | 10 | ## The standard Lorem Ipsum passage 11 | 12 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 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." 13 | 14 | ## written by Cicero in 45 BC 15 | 16 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" 17 | 18 | ## 1914 translation by H. Rackham 19 | 20 | "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?" 21 | -------------------------------------------------------------------------------- /content/posts/markdown-syntax.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: "markdown-syntax" 3 | title: "Markdown Syntax Guide" 4 | date: 2020-06-09 5 | author: wutali 6 | tags: 7 | - style 8 | - markdown 9 | --- 10 | 11 | This article offers a sample of basic Markdown syntax that can be used in Hugo content files, also it shows whether basic HTML elements are decorated with CSS in MDX. 12 | 13 | ## Headings 14 | 15 | The following HTML `

`—`

` elements represent six levels of section headings. `

` is the highest section level while `

` is the lowest. 16 | 17 | # H1 18 | 19 | ## H2 20 | 21 | ### H3 22 | 23 | #### H4 24 | 25 | ##### H5 26 | 27 | ###### H6 28 | 29 | ## Paragraph 30 | 31 | Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat. 32 | 33 | Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat. 34 | 35 | ## Images 36 | 37 | ![Sample Image](/images/600x300.png) 38 | 39 | ## Blockquotes 40 | 41 | The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations. 42 | 43 | #### Blockquote without attribution 44 | 45 | > Tiam, ad mint andaepu dandae nostion secatur sequo quae. 46 | > **Note** that you can use _Markdown syntax_ within a blockquote. 47 | 48 | #### Blockquote with attribution 49 | 50 | > Simplicity is the ultimate sophistication. 51 | > 52 | > Leonardo da Vinci 53 | 54 | ## Tables 55 | 56 | Tables aren't part of the core Markdown spec, but Hugo supports supports them out-of-the-box. 57 | 58 | | Name | Age | 59 | | ----- | --- | 60 | | Bob | 27 | 61 | | Alice | 23 | 62 | 63 | #### Inline Markdown within tables 64 | 65 |
66 | 67 | | Inline    | Markdown    | In    | Table | 68 | | ------------------------ | -------------------------- | ----------------------------------- | ------ | 69 | | _italics_ | **bold** | ~~strikethrough~~    | `code` | 70 | 71 |
72 | 73 | ## Code Blocks 74 | 75 | #### Code block with backticks 76 | 77 | ```html 78 | 79 | 80 | 81 | 82 | Example HTML5 Document 83 | 84 | 85 |

Test

86 | 87 | 88 | ``` 89 | 90 | #### Code block indented with four spaces 91 | 92 | 93 | 94 | 95 | 96 | Example HTML5 Document 97 | 98 | 99 |

Test

100 | 101 | 102 | 103 | #### Code block with Hugo's internal highlight shortcode 104 | 105 | ```html 106 | 107 | 108 | 109 | 110 | Example HTML5 Document 111 | 112 | 113 |

Test

114 | 115 | 116 | ``` 117 | 118 | #### Wide code block 119 | 120 | ```html 121 | 122 | 123 | 124 | 125 | Example HTML5 Document 126 | 127 | 128 |

129 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 130 | tempor incididunt ut labore et dolore magna aliqua. 131 |

132 | 133 | 134 | ``` 135 | 136 | ## List Types 137 | 138 | #### Ordered List 139 | 140 | 1. First item 141 | 2. Second item 142 | 3. Third item 143 | 144 | #### Unordered List 145 | 146 | - List item 147 | - Another item 148 | - And another item 149 | 150 | #### Nested list 151 | 152 | - Item 153 | 1. First Sub-item 154 | 2. Second Sub-item 155 | 156 | ## Other Elements — abbr, sub, sup, kbd, mark 157 | 158 |

159 | GIF is a bitmap image format. 160 |

161 | 162 | H2O 163 | 164 | Xn + Yn = Zn 165 | 166 | Press CTRL+ALT+Delete to end the session. 167 | 168 | Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. 169 | -------------------------------------------------------------------------------- /content/posts/references.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: references 3 | title: References 4 | date: 2020-06-08 5 | author: wutali 6 | tags: 7 | - document 8 | --- 9 | 10 | - [Netlify CMS Documentation](https://www.netlifycms.org/docs/intro/) 11 | - [Building a Markdown blog with Next 9.4 and Netlify](https://www.netlify.com/blog/2020/05/04/building-a-markdown-blog-with-next-9.4-and-netlify/) 12 | - [Hugo Theme - Codex](https://github.com/jakewies/hugo-theme-codex) 13 | - [Next.js Starter Template for TypeScript](https://github.com/vercel/next-learn-starter/tree/master/typescript-final) 14 | - [Building Blog with NextJS and Netlify CMS](https://dev.to/mefaba/building-blog-with-nextjs-and-netlify-cms-fom) 15 | - [Unicons](https://github.com/Iconscout/unicons) 16 | -------------------------------------------------------------------------------- /content/posts/rich-content-with-mdx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: rich-content-with-mdx 3 | title: Rich Content with MDX 4 | date: 2020-06-08 5 | author: wutali 6 | tags: 7 | - style 8 | - mdx 9 | --- 10 | 11 | MDX supports to import React component and embed it directly as markdown content. 12 | 13 | ## Instagram with react-instagram-embed 14 | 15 |

16 | 21 |

22 | 23 | ## YouTube with react-youtube 24 | 25 |

26 | 27 |

28 | 29 | ## Twitter with react-twitter-embed 30 | 31 |

32 | 33 |

34 | -------------------------------------------------------------------------------- /content/posts/welcome.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome To Next.js and Netlify Blog Template 4 | date: 2020-06-10 5 | author: wutali 6 | tags: 7 | - document 8 | --- 9 | 10 | ## The standard Lorem Ipsum passage 11 | 12 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 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." 13 | 14 | ## written by Cicero in 45 BC 15 | 16 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" 17 | 18 | ## 1914 translation by H. Rackham 19 | 20 | "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?" 21 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import * as React from "react"; 3 | 4 | export const ReactComponent: React.FunctionComponent>; 7 | 8 | export default ReactComponent; 9 | } 10 | 11 | declare module "*.json" { 12 | const value: any; 13 | export default value; 14 | } 15 | 16 | declare module "*.yml" { 17 | const value: any; 18 | export default value; 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "./src" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /meta/authors.yml: -------------------------------------------------------------------------------- 1 | authors: 2 | - slug: wutali 3 | name: Takahiro Fujiwara 4 | introduction: 5 | Co-founder and CTO at Fuller, Inc. / Software & Data Engineer / 6 | Python / Golang / React 7 | -------------------------------------------------------------------------------- /meta/tags.yml: -------------------------------------------------------------------------------- 1 | tags: 2 | - slug: document 3 | name: document 4 | - slug: mock 5 | name: mock 6 | - slug: style 7 | name: style 8 | - slug: license 9 | name: license 10 | - slug: mdx 11 | name: mdx 12 | - slug: markdown 13 | name: markdown 14 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm run export" 4 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ 2 | pageExtensions: ["tsx"], 3 | webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { 4 | config.module.rules.push( 5 | ...[ 6 | { 7 | test: /\.yml$/, 8 | type: "json", 9 | use: "yaml-loader", 10 | }, 11 | { 12 | test: /\.svg$/, 13 | use: "@svgr/webpack", 14 | }, 15 | ] 16 | ); 17 | return config; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "export": "next build && next export -o ./dist", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@mapbox/rehype-prism": "^0.6.0", 14 | "@svgr/webpack": "^5.5.0", 15 | "@types/js-yaml": "^4.0.0", 16 | "date-fns": "^2.19.0", 17 | "gray-matter": "^4.0.2", 18 | "js-yaml": "^4.0.0", 19 | "next": "10.1.2", 20 | "next-mdx-remote": "^2.1.3", 21 | "normalize.css": "^8.0.1", 22 | "react": "17.0.2", 23 | "react-dom": "17.0.2", 24 | "react-instagram-embed": "^2.0.0", 25 | "react-schemaorg": "^1.3.1", 26 | "react-twitter-embed": "^3.0.3", 27 | "react-youtube": "^7.13.1", 28 | "schema-dts": "^0.8.2" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^26.0.22", 32 | "@types/node": "^14.14.37", 33 | "@types/react": "^17.0.3", 34 | "jest": "^26.6.3", 35 | "ts-jest": "^26.5.4", 36 | "typescript": "^4.2.3", 37 | "yaml-loader": "^0.6.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/admin/config.yml: -------------------------------------------------------------------------------- 1 | backend: 2 | name: git-gateway 3 | branch: master 4 | media_folder: public/images 5 | public_folder: /images 6 | publish_mode: editorial_workflow 7 | 8 | collections: 9 | - name: "config" 10 | label: "Config" 11 | delete: false 12 | editor: 13 | preview: false 14 | files: 15 | - name: "general" 16 | label: "Site Config" 17 | file: "config.json" 18 | description: "General site settings" 19 | fields: 20 | - label: "URL" 21 | name: "base_url" 22 | widget: "string" 23 | hint: "Do not enter the trailing slash of the URL" 24 | - label: "Site title" 25 | name: "site_title" 26 | widget: "string" 27 | - label: "Site description" 28 | name: "site_description" 29 | widget: "string" 30 | - label: "Site keywords" 31 | name: "site_keywords" 32 | widget: "list" 33 | summary: "{{fields.keyword.keyword}}" 34 | field: 35 | label: Keyword 36 | name: keyword 37 | widget: "string" 38 | - label: "Twitter account" 39 | name: "twitter_account" 40 | widget: "string" 41 | - label: "GitHub account" 42 | name: "github_account" 43 | widget: "string" 44 | 45 | - name: "meta" 46 | label: "Meta" 47 | delete: false 48 | editor: 49 | preview: false 50 | files: 51 | - name: "authors" 52 | label: "Authors" 53 | file: "meta/authors.yml" 54 | description: "Author descriptions" 55 | fields: 56 | - name: authors 57 | label: Authors 58 | label_singular: "Author" 59 | widget: list 60 | fields: 61 | - label: "Slug" 62 | name: "slug" 63 | widget: "string" 64 | hint: "The part of a URL identifies the author" 65 | - label: "Name" 66 | name: "name" 67 | widget: "string" 68 | hint: "First and Last" 69 | - label: "Introduction" 70 | name: "introduction" 71 | widget: "text" 72 | - name: "tags" 73 | label: "Tags" 74 | file: "meta/tags.yml" 75 | description: "List of tags" 76 | fields: 77 | - name: tags 78 | label: Tags 79 | label_singular: "Tag" 80 | widget: list 81 | fields: 82 | - label: "Slug" 83 | name: "slug" 84 | widget: "string" 85 | hint: "The part of a URL identifies the tag" 86 | - label: "Display Name" 87 | name: "name" 88 | widget: "string" 89 | hint: "Tag name for displaying on the site" 90 | 91 | - name: "posts" 92 | label: "Posts" 93 | folder: "content/posts/" 94 | extension: "mdx" 95 | format: "frontmatter" 96 | create: true 97 | slug: "{{slug}}" 98 | identifier_field: slug 99 | summary: "{{title}}" 100 | fields: 101 | - label: "Slug" 102 | name: "slug" 103 | widget: "string" 104 | - label: "Title" 105 | name: "title" 106 | widget: "string" 107 | - label: "Publish Date" 108 | name: "date" 109 | widget: "datetime" 110 | format: "YYYY-MM-DD" 111 | dateFormat: "YYYY-MM-DD" 112 | timeFormat: false 113 | - label: Author 114 | name: "author" 115 | widget: relation 116 | collection: "meta" 117 | file: "authors" 118 | searchFields: 119 | - "authors.*.name" 120 | displayFields: 121 | - "authors.*.name" 122 | valueField: "authors.*.slug" 123 | - label: Tags 124 | label_singular: "Tag" 125 | name: "tags" 126 | widget: list 127 | summary: "{{fields.tag}}" 128 | field: 129 | label: Tag 130 | name: tag 131 | widget: relation 132 | collection: "meta" 133 | file: "tags" 134 | searchFields: 135 | - "tags.*.name" 136 | displayFields: 137 | - "tags.*.name" 138 | valueField: "tags.*.slug" 139 | - label: "Body" 140 | name: "body" 141 | widget: "markdown" 142 | -------------------------------------------------------------------------------- /public/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Content Manager 7 | 8 | 9 | 10 | 11 | 12 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutali/nextjs-netlify-blog-template/147bcb103831795a9f275a6c3fc25b03a1ec69db/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutali/nextjs-netlify-blog-template/147bcb103831795a9f275a6c3fc25b03a1ec69db/public/icon.png -------------------------------------------------------------------------------- /public/images/600x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutali/nextjs-netlify-blog-template/147bcb103831795a9f275a6c3fc25b03a1ec69db/public/images/600x300.png -------------------------------------------------------------------------------- /public/og_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wutali/nextjs-netlify-blog-template/147bcb103831795a9f275a6c3fc25b03a1ec69db/public/og_image.png -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "", 3 | "name": "", 4 | "icons": [ 5 | { 6 | "src": "icon.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | } 10 | ], 11 | "start_url": "/?utm_source=homescreen", 12 | "background_color": "#000000", 13 | "theme_color": "#000000" 14 | } 15 | -------------------------------------------------------------------------------- /public/styles/content.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | font-family: "Ubuntu", sans-serif; 3 | color: #222; 4 | font-weight: 200; 5 | } 6 | 7 | .content time { 8 | color: #9b9b9b; 9 | } 10 | 11 | .content p { 12 | line-height: 1.5rem; 13 | margin: 1.5rem 0 0 0; 14 | } 15 | 16 | .content a { 17 | color: blue; 18 | text-decoration: underline; 19 | } 20 | 21 | .content h1 { 22 | margin: 2.5rem 0 0 0; 23 | } 24 | 25 | .content h2, 26 | .content h3, 27 | .content h4, 28 | .content h5 { 29 | margin: 2rem 0 0 0; 30 | line-height: 1.25em; 31 | } 32 | 33 | .content h2::before { 34 | position: absolute; 35 | margin-left: -1em; 36 | font-weight: 300; 37 | font-size: 1.5rem; 38 | color: #9b9b9b; 39 | display: none; 40 | content: "#"; 41 | } 42 | 43 | .content pre { 44 | display: block; 45 | background-color: rgba(27, 31, 35, 0.05); 46 | line-height: 1.25rem; 47 | padding: 1rem; 48 | overflow: auto; 49 | margin: 1.75rem 0 0 0; 50 | } 51 | 52 | .content pre code { 53 | background-color: transparent; 54 | font-size: 100%; 55 | padding: 0; 56 | } 57 | 58 | .content code { 59 | font-family: "Ubuntu Mono", monospace; 60 | font-size: 85%; 61 | padding: 0.2em 0.4em; 62 | margin: 0; 63 | background-color: rgba(27, 31, 35, 0.05); 64 | border-radius: 3px; 65 | } 66 | 67 | .content blockquote { 68 | margin: 0 1rem; 69 | } 70 | 71 | .content blockquote::before { 72 | position: absolute; 73 | content: "\201C"; 74 | font-size: 6em; 75 | font-family: roboto, serif; 76 | line-height: 1.5rem; 77 | margin-top: 0.1em; 78 | margin-left: -0.2em; 79 | z-index: -1; 80 | color: #ededed; 81 | } 82 | 83 | .content table { 84 | max-width: 100%; 85 | border-spacing: 0; 86 | margin-top: 1.5rem; 87 | } 88 | 89 | .content table thead { 90 | background: #f7f7f7; 91 | } 92 | 93 | .content table th { 94 | font-weight: 500; 95 | } 96 | 97 | .content table th, 98 | .content table td { 99 | padding: 0.5em 1em; 100 | border: 1px double #eee; 101 | } 102 | 103 | .content ol, 104 | .content ul { 105 | padding: 0 0 0 1.5rem; 106 | margin: 1.5rem 0 0 0; 107 | } 108 | 109 | .content ol li, 110 | .content ul li { 111 | line-height: 1.5rem; 112 | } 113 | 114 | .content li ol, 115 | .content li ul { 116 | margin: 0; 117 | } 118 | 119 | .content abbr[title] { 120 | text-decoration: underline double; 121 | } 122 | 123 | .content kbd { 124 | font-family: "Ubuntu Mono", monospace; 125 | } 126 | 127 | .content img { 128 | width: 100%; 129 | } 130 | 131 | @media (min-width: 769px) { 132 | .content h2, 133 | .content h3, 134 | .content h4, 135 | .content h5 { 136 | position: relative; 137 | } 138 | 139 | .content h2::before { 140 | display: block; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /public/styles/global.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Ubuntu+Mono&display=swap"); 3 | 4 | html, 5 | body { 6 | font-family: "Ubuntu", sans-serif; 7 | background-color: #fff; 8 | color: #222; 9 | font-weight: 200; 10 | height: 100%; 11 | } 12 | 13 | #__next { 14 | height: 100%; 15 | } 16 | 17 | a { 18 | color: #9b9b9b; 19 | text-decoration: none; 20 | transition: color 0.3s ease; 21 | } 22 | 23 | a:active, 24 | a:hover { 25 | color: #000; 26 | } 27 | 28 | .youtube-container { 29 | position: relative; 30 | width: 100%; 31 | height: 0; 32 | padding-bottom: 56.25%; 33 | overflow: hidden; 34 | margin-bottom: 50px; 35 | } 36 | 37 | .youtube-container iframe { 38 | width: 100%; 39 | height: 100%; 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | } 44 | -------------------------------------------------------------------------------- /src/__tests__/pagination.test.ts: -------------------------------------------------------------------------------- 1 | import { generatePagination } from "../lib/pagination"; 2 | 3 | test("excerpt starting and ending page numbers", () => { 4 | const pagination = generatePagination(5, 8); 5 | expect(pagination).toStrictEqual([ 6 | { page: 1, current: false, excerpt: false }, 7 | { page: null, current: false, excerpt: true }, 8 | { page: 4, current: false, excerpt: false }, 9 | { page: 5, current: true, excerpt: false }, 10 | { page: 6, current: false, excerpt: false }, 11 | { page: null, current: false, excerpt: true }, 12 | { page: 8, current: false, excerpt: false }, 13 | ]); 14 | }); 15 | 16 | test("excerpt ending page numbers", () => { 17 | const pagination = generatePagination(2, 8); 18 | expect(pagination).toStrictEqual([ 19 | { page: 1, current: false, excerpt: false }, 20 | { page: 2, current: true, excerpt: false }, 21 | { page: 3, current: false, excerpt: false }, 22 | { page: null, current: false, excerpt: true }, 23 | { page: 8, current: false, excerpt: false }, 24 | ]); 25 | }); 26 | 27 | test("excerpt ending page numbers at 1st page", () => { 28 | const pagination = generatePagination(1, 8); 29 | expect(pagination).toStrictEqual([ 30 | { page: 1, current: true, excerpt: false }, 31 | { page: 2, current: false, excerpt: false }, 32 | { page: null, current: false, excerpt: true }, 33 | { page: 8, current: false, excerpt: false }, 34 | ]); 35 | }); 36 | 37 | test("excerpt starting page numbers", () => { 38 | const pagination = generatePagination(7, 8); 39 | expect(pagination).toStrictEqual([ 40 | { page: 1, current: false, excerpt: false }, 41 | { page: null, current: false, excerpt: true }, 42 | { page: 6, current: false, excerpt: false }, 43 | { page: 7, current: true, excerpt: false }, 44 | { page: 8, current: false, excerpt: false }, 45 | ]); 46 | }); 47 | 48 | test("excerpt starting page numbers at last page", () => { 49 | const pagination = generatePagination(8, 8); 50 | expect(pagination).toStrictEqual([ 51 | { page: 1, current: false, excerpt: false }, 52 | { page: null, current: false, excerpt: true }, 53 | { page: 7, current: false, excerpt: false }, 54 | { page: 8, current: true, excerpt: false }, 55 | ]); 56 | }); 57 | 58 | export {}; 59 | -------------------------------------------------------------------------------- /src/assets/github-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/twitter-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Author.tsx: -------------------------------------------------------------------------------- 1 | import { AuthorContent } from "../lib/authors"; 2 | 3 | type Props = { 4 | author: AuthorContent; 5 | }; 6 | export default function Author({ author }: Props) { 7 | return ( 8 | <> 9 | {author.name} 10 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Burger.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | active: boolean; 3 | onClick: () => void; 4 | }; 5 | export default function Burger({ active, onClick }: Props) { 6 | return ( 7 |
8 |
9 |
10 |
11 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Copyright.tsx: -------------------------------------------------------------------------------- 1 | export default function Copyright() { 2 | return ( 3 | <> 4 |

© 2020

5 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Date.tsx: -------------------------------------------------------------------------------- 1 | import { format, formatISO } from "date-fns"; 2 | 3 | type Props = { 4 | date: Date; 5 | }; 6 | export default function Date({ date }: Props) { 7 | return ( 8 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Navigation from "./Navigation"; 3 | 4 | type Props = { 5 | children: React.ReactNode; 6 | }; 7 | export default function Layout({ children }: Props) { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 |
{children}
21 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useRouter } from "next/router"; 3 | import Burger from "./Burger"; 4 | import { useState } from "react"; 5 | 6 | export default function Navigation() { 7 | const router = useRouter(); 8 | const [active, setActive] = useState(false); 9 | return ( 10 | <> 11 | setActive(!active)} /> 12 |
13 | 31 | 89 |
90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { generatePagination } from "../lib/pagination"; 2 | import Link from "next/link"; 3 | 4 | type Props = { 5 | current: number; 6 | pages: number; 7 | link: { 8 | href: (page: number) => string; 9 | as: (page: number) => string; 10 | }; 11 | }; 12 | export default function Pagination({ current, pages, link }: Props) { 13 | const pagination = generatePagination(current, pages); 14 | return ( 15 |
    16 | {pagination.map((it, i) => ( 17 |
  • 18 | {it.excerpt ? ( 19 | "..." 20 | ) : ( 21 | 22 | {it.page} 23 | 24 | )} 25 |
  • 26 | ))} 27 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/PostItem.tsx: -------------------------------------------------------------------------------- 1 | import { PostContent } from "../lib/posts"; 2 | import Date from "./Date"; 3 | import Link from "next/link"; 4 | import { parseISO } from "date-fns"; 5 | 6 | type Props = { 7 | post: PostContent; 8 | }; 9 | export default function PostItem({ post }: Props) { 10 | return ( 11 | 12 | 13 | 14 |

{post.title}

15 | 27 |
28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/PostLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "../../public/styles/content.module.css"; 3 | import Author from "./Author"; 4 | import Copyright from "./Copyright"; 5 | import Date from "./Date"; 6 | import Layout from "./Layout"; 7 | import BasicMeta from "./meta/BasicMeta"; 8 | import JsonLdMeta from "./meta/JsonLdMeta"; 9 | import OpenGraphMeta from "./meta/OpenGraphMeta"; 10 | import TwitterCardMeta from "./meta/TwitterCardMeta"; 11 | import { SocialList } from "./SocialList"; 12 | import TagButton from "./TagButton"; 13 | import { getAuthor } from "../lib/authors"; 14 | import { getTag } from "../lib/tags"; 15 | 16 | type Props = { 17 | title: string; 18 | date: Date; 19 | slug: string; 20 | tags: string[]; 21 | author: string; 22 | description?: string; 23 | children: React.ReactNode; 24 | }; 25 | export default function PostLayout({ 26 | title, 27 | date, 28 | slug, 29 | author, 30 | tags, 31 | description = "", 32 | children, 33 | }: Props) { 34 | const keywords = tags.map(it => getTag(it).name); 35 | const authorName = getAuthor(author).name; 36 | return ( 37 | 38 | 44 | 49 | 54 | 62 |
63 |
64 |
65 |

{title}

66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 |
{children}
76 |
    77 | {tags.map((it, i) => ( 78 |
  • 79 | 80 |
  • 81 | ))} 82 |
83 |
84 |
85 |
86 | 87 |
88 | 89 |
90 |
91 | 136 | 236 |
237 | ); 238 | } 239 | -------------------------------------------------------------------------------- /src/components/PostList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PostContent } from "../lib/posts"; 3 | import PostItem from "./PostItem"; 4 | import TagLink from "./TagLink"; 5 | import Pagination from "./Pagination"; 6 | import { TagContent } from "../lib/tags"; 7 | 8 | type Props = { 9 | posts: PostContent[]; 10 | tags: TagContent[]; 11 | pagination: { 12 | current: number; 13 | pages: number; 14 | }; 15 | }; 16 | export default function PostList({ posts, tags, pagination }: Props) { 17 | return ( 18 |
19 |
20 |
    21 | {posts.map((it, i) => ( 22 |
  • 23 | 24 |
  • 25 | ))} 26 |
27 | (page === 1 ? "/posts" : "/posts/page/[page]"), 32 | as: (page) => (page === 1 ? null : "/posts/page/" + page), 33 | }} 34 | /> 35 |
36 |
    37 | {tags.map((it, i) => ( 38 |
  • 39 | 40 |
  • 41 | ))} 42 |
43 | 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/SocialList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Twitter from "../assets/twitter-alt.svg"; 3 | import GitHub from "../assets/github-alt.svg"; 4 | import config from "../lib/config"; 5 | 6 | export function SocialList({}) { 7 | return ( 8 |
9 | 15 | 16 | 17 | 23 | 24 | 25 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/TagButton.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { TagContent } from "../lib/tags"; 3 | 4 | type Props = { 5 | tag: TagContent; 6 | }; 7 | export default function TagButton({ tag }: Props) { 8 | return ( 9 | <> 10 | 11 | {tag.name} 12 | 13 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/TagLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { TagContent } from "../lib/tags"; 3 | 4 | type Props = { 5 | tag: TagContent; 6 | }; 7 | export default function Tag({ tag }: Props) { 8 | return ( 9 | 10 | {"#" + tag.name} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/TagPostList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PostContent } from "../lib/posts"; 3 | import { TagContent } from "../lib/tags"; 4 | import PostItem from "./PostItem"; 5 | import Pagination from "./Pagination"; 6 | 7 | type Props = { 8 | posts: PostContent[]; 9 | tag: TagContent; 10 | pagination: { 11 | current: number; 12 | pages: number; 13 | }; 14 | }; 15 | export default function TagPostList({ posts, tag, pagination }: Props) { 16 | return ( 17 |
18 |

19 | All posts / {tag.name} 20 |

21 |
    22 | {posts.map((it, i) => ( 23 |
  • 24 | 25 |
  • 26 | ))} 27 |
28 | "/posts/tags/[[...slug]]", 33 | as: (page) => 34 | page === 1 35 | ? "/posts/tags/" + tag.slug 36 | : `/posts/tags/${tag.slug}/${page}`, 37 | }} 38 | /> 39 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/components/meta/BasicMeta.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import config from "../../lib/config"; 3 | 4 | type Props = { 5 | title?: string; 6 | description?: string; 7 | keywords?: string[]; 8 | author?: string; 9 | url: string; 10 | }; 11 | export default function BasicMeta({ 12 | title, 13 | description, 14 | keywords, 15 | author, 16 | url, 17 | }: Props) { 18 | return ( 19 | 20 | 21 | {title ? [title, config.site_title].join(" | ") : config.site_title} 22 | 23 | 27 | it.keyword).join(",") 33 | } 34 | /> 35 | {author ? : null} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/meta/JsonLdMeta.tsx: -------------------------------------------------------------------------------- 1 | import { BlogPosting } from "schema-dts"; 2 | import { jsonLdScriptProps } from "react-schemaorg"; 3 | import config from "../../lib/config"; 4 | import { formatISO } from "date-fns"; 5 | import Head from "next/head"; 6 | 7 | type Props = { 8 | url: string; 9 | title: string; 10 | keywords?: string[]; 11 | date: Date; 12 | author?: string; 13 | image?: string; 14 | description?: string; 15 | }; 16 | export default function JsonLdMeta({ 17 | url, 18 | title, 19 | keywords, 20 | date, 21 | author, 22 | image, 23 | description, 24 | }: Props) { 25 | return ( 26 | 27 |