├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── _authors └── makerkit.json ├── _collections └── lorem-ipsum.json ├── _posts ├── dextera-sibi-orbes.mdx └── lorem-reddita.mdx ├── components ├── Author.tsx ├── ClientOnly.tsx ├── CollectionLink.tsx ├── DarkModeToggle.tsx ├── DateFormatter.tsx ├── Footer.tsx ├── Header.tsx ├── LayoutContainer.tsx ├── Logo.tsx ├── MDXComponents.tsx ├── MDXRenderer.tsx ├── Meta.tsx ├── Post.tsx ├── PostBody.module.css ├── PostBody.tsx ├── PostHeader.tsx ├── PostImage.tsx ├── PostMetadata.tsx ├── PostPreview.tsx ├── PostTitle.tsx ├── PostsList.tsx └── ThemeProvider.tsx ├── configuration.ts ├── global.d.ts ├── lib ├── blog │ ├── api.ts │ ├── author.ts │ ├── blog-post.ts │ ├── collection.ts │ ├── compile-mdx.ts │ └── rss-feed.ts ├── is-browser.ts └── theme.ts ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [collection] │ └── [slug].tsx ├── _app.tsx └── index.tsx ├── postcss.config.js ├── public └── favicon.ico ├── styles ├── Home.module.css ├── github-dark.css └── globals.css ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | .idea 37 | 38 | # generated 39 | public/rss.xml 40 | public/atom.xml 41 | public/rss.json 42 | public/robots.txt 43 | public/sitemap*.xml 44 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "arrowParens": "always", 6 | "parser": "typescript", 7 | "printWidth": 80, 8 | "singleQuote": true 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A free and open-source starter by MakerKit. 2 | 3 | # Blog Starter Template with Next.js, MDX and Tailwind CSS 4 | 5 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 6 | 7 | This blog starter is the perfect foundation for writing your next blog, portfolio or online publication using Next.js and Tailwind CSS. 8 | 9 | ## Features 10 | 11 | - ✅ **Fully responsive Blog/Portfolio Site** 12 | - 📄 **Write your articles with all the power of MDX** 13 | - ⚡ **Core Web Vitals = 100** 14 | - 🚀 **Search Engine Optimized (SEO) out-of-the-box** 15 | - 📂 **Sitemap and RSS generated automatically** 16 | - ✨ **Written with strict Typescript, validated with EsLint, formatted with Prettier** 17 | 18 | ## Getting Started 19 | 20 | Clone the repository: 21 | 22 | ``` 23 | git clone https://github.com/makerkit/next-blog-kit 24 | ``` 25 | 26 | Rename your project and jump into the folder. Then, run the development server: 27 | 28 | ```bash 29 | npm run dev 30 | # or 31 | yarn dev 32 | ``` 33 | 34 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 35 | 36 | ### Setting the upstream folder 37 | 38 | If you want, reinitialize the git repository and set this repository as your upstream, so you can continue getting updates: 39 | 40 | ``` 41 | rm -rf .git 42 | git init 43 | git remote add upstream https://github.com/makerkit/next-blog-kit 44 | ``` 45 | 46 | To keep your repository up-to-date with this, use `git pull`: 47 | 48 | ``` 49 | git pull upstream main 50 | ``` 51 | 52 | Solve the eventual conflicts and merge 😃 53 | 54 | ### Configuration 55 | 56 | Open the configuration file at `./configuration.ts`. It will have the following content: 57 | 58 | ```tsx 59 | const configuration = { 60 | site: { 61 | name: '', 62 | description: '', 63 | themeColor: '', 64 | siteUrl: '', 65 | siteName: '', 66 | twitterHandle: '', 67 | githubHandle: '', 68 | language: 'en', 69 | }, 70 | blog: { 71 | maxReadMorePosts: 6, 72 | }, 73 | production: process.env.NODE_ENV === 'production', 74 | }; 75 | ``` 76 | 77 | Update it with your own data, or leave as is to begin with. 78 | 79 | ### Add Articles, Collections and Authors 80 | 81 | Before creating a blog post, we define which collection it belongs to and the author of the post. 82 | 83 | To define a collection, create a JSON file at `_collctions/`: 84 | 85 | ```json 86 | { 87 | "name": "Tutorials", 88 | "emoji": "🖥️" 89 | } 90 | ``` 91 | 92 | Alternatively, you can choose to assign a picture to each collection (or neither): 93 | 94 | ```json 95 | { 96 | "name": "Tutorials", 97 | "picture": "/assets/images/tutorials.png" 98 | } 99 | ``` 100 | 101 | Next, we need to add the author of the article. Add a JSON file at `_authors`: 102 | 103 | ```json 104 | { 105 | "name": "MakerKit", 106 | "picture": "/assets/images/makerkit.png", 107 | "url": "https://twitter.com/makerkit_dev" 108 | } 109 | ``` 110 | 111 | We can now create a blog post. Add an MDX file at `_posts`: 112 | 113 | ```yaml 114 | --- 115 | title: 'Dextera Sibi Orbes' 116 | collection: '_collections/lorem-ipsum.json' 117 | author: '_authors/makerkit.json' 118 | date: 2022-03-30 119 | live: true 120 | image: '' 121 | description: "Lorem markdownum ictu; leti quae, paenituisse venere. Liquet praemia omne di 122 | amarunt dicta." 123 | --- 124 | ``` 125 | 126 | As you can see, the properties `collection` and `author` are references to the path of each.s 127 | 128 | ## Building this from scratch 129 | 130 | For mor information about building this codebase from scratch, take a look at the article [Create an MDX-powered Blog with Next.js](https://makerkit.dev/blog/tutorials/create-a-blog-mdx-nextjs). 131 | 132 | ## Deploy on Vercel 133 | 134 | The easiest way to deploy this app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 135 | 136 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 137 | -------------------------------------------------------------------------------- /_authors/makerkit.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MakerKit", 3 | "picture": "", 4 | "url": "https://twitter.com/makerkit_dev" 5 | } 6 | -------------------------------------------------------------------------------- /_collections/lorem-ipsum.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lorem Ipsum", 3 | "emoji": "🖥️" 4 | } 5 | -------------------------------------------------------------------------------- /_posts/dextera-sibi-orbes.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Dextera Sibi Orbes' 3 | collection: '_collections/lorem-ipsum.json' 4 | author: '_authors/makerkit.json' 5 | date: 2022-03-30 6 | live: true 7 | image: 'https://images.unsplash.com/photo-1604931668626-ab49cb27d952?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2370&q=80' 8 | description: "Lorem markdownum ictu; leti quae, paenituisse venere. Liquet praemia omne di 9 | amarunt dicta." 10 | --- 11 | 12 | ## Digna Plexippi argento dixerunt 13 | 14 | Lorem markdownum ictu; leti quae, paenituisse venere. Liquet praemia omne di 15 | amarunt dicta. 16 | 17 | Tarda tantorum magnum, **mea**. Dilecta requirenti florebat, nitenti potuit 18 | victa; sunt que corpusque, ausis, et esse *dubia*. Sedemque terque accede, 19 | caelum! Nec ferro quid donec et praebetque nympha, **ut adfata** si pressum 20 | precatur. Nymphae anxia relatus cumque, patrio, *cum* et, fraudem praemiaque 21 | ratis nam *duris praesagia addere* dies. 22 | 23 | ``` 24 | pcbOf = 530359; 25 | im_mbr(2); 26 | if (lagBacklink - 37 != 4) { 27 | processor = sdslCloneOn + hard_nui; 28 | } 29 | ``` 30 | 31 | Sulcis urbem inulta in **minata** en litus; solebat **depulerat at** quaedam 32 | fluctus aequoris. Flamma dilatant equos cum placuit nec, si capitis recusat de 33 | erat edere: Italiam frondere. Cervice maxima adporrectumque huc datis Euboicus: 34 | iactura pudibundaque danti? Fera ducebat; perpetuas veteris. Amat sanguine, 35 | luctus dextra. 36 | 37 | ## Solutum scelerique ignarus vincere Troasque mihi 38 | 39 | Poste [secutum pietas](http://prohibetedecipienda.org/parstrepidaeque) ne 40 | supplex nefas memorantur proceres duce exemplum animoque esset. Toro decuit, 41 | filia annos tura salicta, cognoscit dotem deterritus miraturus animi comitemque 42 | auxiliaribus. 43 | 44 | > Quodque freta, generis plurimus merito destrinxit, redunco sollicitive cruento 45 | > potentia, aera dum nectare **requirit si potes**. Tegendas **et violaverat 46 | > tempus**? 47 | 48 | Pete in captantur vulnere trabibus excipit Terram te cara arserunt pugnabo. 49 | Matura in Baccho fulvis tollere. Viaeque sua ultoresque saepe successisse 50 | nubibus orbe bello, aliis domestice omnia negare. Et pone posito defensore 51 | fuisset quos veneni illa? Moto parte forti patriosque postquam facit pennisque 52 | ventos, functi, mihi classem honores? 53 | 54 | Carne quae terra Sunt nec ex naides votisque finemque turpe, Hersilien neque 55 | volentem mentes trabes temptat oro sublimis. Quae hoc ait nos concretaque: illos 56 | Iphis pariterque coniugis flenda. Raptam adversaque, nec petit quo [speciem 57 | coepit et](http://www.ignes-in.org/munere) angulus [Caune fons 58 | adesse](http://www.animoque-hoc.net/orbae-aegre.html) spes, et dicitur de? 59 | 60 | Humus vertice parentis corrigit [ruricolae 61 | unus](http://auroraefonti.net/habet-nebulas)? Dum tellure hac: Iovique statione 62 | repressit gemmae inficit ductum. Dederunt mirabile non probat portus nullo, est 63 | ulta tenet, quoque currus, ponentem ora nomen lacrimis nostris. Gravis pecoris 64 | velut: haec sive *angulus* illi bella iuventus. 65 | -------------------------------------------------------------------------------- /_posts/lorem-reddita.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Lorem Reddita' 3 | collection: '_collections/lorem-ipsum.json' 4 | author: '_authors/makerkit.json' 5 | date: 2022-03-30 6 | live: true 7 | image: 'https://images.unsplash.com/photo-1519879110616-349b57f8cd11?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2370&q=80' 8 | description: "Emi non revolvor opem exitus vires, ad ante regno, unus. Iter 9 | meo venae finierat, nec neu ense lassaque Phoeboque probabit Iapygis" 10 | --- 11 | 12 | ## Tectis nectare tamen veni Pandrose 13 | 14 | Lorem [markdownum urbem iste](http://et.com/) reddita. Videt Iuppiter 15 | [inpavidus](http://www.adunco.net/omniauncis) temone versus. Aliquid **rupit** 16 | ita noctem Cipi: emi non revolvor opem exitus vires, ad ante regno, unus. Iter 17 | meo venae finierat, nec neu ense lassaque Phoeboque probabit Iapygis et humo 18 | penetrant. Venerem sistitur ingreditur te laesi vacet confer, prodit Ixion 19 | socios moriensque cornua, [et edidit eadem](http://www.caelovirga.org/). 20 | 21 | Rupes mora voce erant brevissimus aras, sed vocat de umero patriam quod. Non 22 | pecudes terraque in nostris fistula supposita neve inluxisse rapta, igitur, 23 | Iphis dei pendent pallebant tigres. Cum iter fallere lanient ferumque percepto 24 | restabat armis illa abesse tecta cum coniunx aut. Arma facundus, et eam volat et 25 | e canis montis sed suae caput isto Cecropidis senatum. 26 | 27 | ## Non o 28 | 29 | Crinem caduca externis extrema urbis illum nomen, non voce superare sacri 30 | *monstris Phoebus* vocesque. Sustinet talibus memorabile stat; **Phaethon ea 31 | rursus** proculcat tepere culpae. [Ait superis](http://divulsere.org/) nec illi 32 | cum demittere flumine petis subtexere mirantem manu luce facunde nec ille 33 | finita. Perque viri non [ipsa](http://inguinibus.org/iuris-bis), Iovis, positu 34 | pendebat *crimine*; victima culmine. 35 | 36 | ## Pia Lucifer nulla vota conferat quid parentis 37 | 38 | Morphea Boeotia annua prole Romana opibus senilis hastae Iunone Pittheia orantem 39 | ipsa certumque taurorum. Patrios subponunt defrenato Apis **fugis et** coniugis, 40 | contemptor quamque ait; sub pastores; male nostros. Quam dedit laborum, in domo 41 | sancta quo, artus virgineis aequoris Ceres. **Eadem haec**, toto aquae, erat 42 | Mycenae, vela capi sonitusque umbras; in. Vos erant corpore iam dixit stamina 43 | miremur virgamque coniugis festa nomine: fecerat. 44 | 45 | 1. Sic lapides io factoque Peneidas ignis pharetras 46 | 2. Et esse quaerit vinctum montibus qui vulnera 47 | 3. Fetus spumaque referam fuisset virgo palluit quotiens 48 | 4. Tam fui quem quoque 49 | 5. Et cremat tauri sit ferro bacchae quem 50 | 6. Diffudere plurima non haberet rediit praecipites nulla 51 | 52 | ## Quod mecum vestis commissas os ipsos pavido 53 | 54 | Laceraret se munera vestibus Achivi. Duros dicta, montis vidit. Graves dixit, 55 | sua dixit Desine Ilion catulus inerti: hac. Est triennia collum, preces terga. 56 | Impia [sine sic](http://www.me.io/ne-adligat.html) aeterna rigidi. 57 | 58 | Cum quo ubi requiemque vastat, altrice dexterior sum o. Athenas adnuit: media 59 | cum; Nixosque est vagantes profanam. 60 | -------------------------------------------------------------------------------- /components/Author.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import AuthorType from '~/lib/blog/author'; 3 | 4 | const Author: React.FCC<{ author: AuthorType }> = ({ author }) => { 5 | const alt = `${author.name}'s picture`; 6 | const imageSize = `45px`; 7 | 8 | return ( 9 |
10 | 11 | {author.picture ? ( 12 | {alt} 18 | ) : null} 19 | 20 | {author.name} 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default Author; 27 | -------------------------------------------------------------------------------- /components/ClientOnly.tsx: -------------------------------------------------------------------------------- 1 | import { isBrowser } from '~/lib/is-browser'; 2 | 3 | const ClientOnly: React.FCC = ({ children }) => { 4 | return isBrowser() ? <>{children} : null; 5 | }; 6 | 7 | export default ClientOnly; 8 | -------------------------------------------------------------------------------- /components/CollectionLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Collection from '~/lib/blog/collection'; 3 | 4 | const CollectionLink: React.FCC<{ 5 | collection: Collection; 6 | }> = ({ collection }) => { 7 | const href = `/${collection.slug}`; 8 | 9 | return ( 10 |
11 | 12 | {collection.name} 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default CollectionLink; 19 | -------------------------------------------------------------------------------- /components/DarkModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | 3 | import { 4 | loadThemeFromLocalStorage, 5 | setTheme, 6 | DARK_THEME_CLASSNAME, 7 | } from '~/lib/theme'; 8 | 9 | const DarkModeToggle = () => { 10 | const theme = useRef(loadThemeFromLocalStorage()); 11 | 12 | const toggleMode = useCallback(() => { 13 | const themeClass = theme.current ? null : DARK_THEME_CLASSNAME; 14 | 15 | theme.current = themeClass; 16 | setTheme(themeClass); 17 | }, []); 18 | 19 | return ( 20 | 23 | ); 24 | }; 25 | 26 | export default DarkModeToggle; 27 | 28 | function ThemeIcon() { 29 | return ( 30 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/DateFormatter.tsx: -------------------------------------------------------------------------------- 1 | import { parseISO, format } from 'date-fns'; 2 | 3 | const DateFormatter: React.FCC<{ 4 | dateString: string; 5 | }> = ({ dateString }) => { 6 | const date = parseISO(dateString); 7 | 8 | return ; 9 | }; 10 | 11 | export default DateFormatter; 12 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | const Footer: React.FCC = () => { 2 | return
Makerkit - {new Date().getFullYear()}
; 3 | }; 4 | 5 | export default Footer; 6 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import Logo from '~/components/Logo'; 3 | 4 | const DarkModeToggle = dynamic(() => import('~/components/DarkModeToggle'), { 5 | ssr: false, 6 | }); 7 | 8 | const Header: React.FCC = () => { 9 | return ( 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Header; 21 | -------------------------------------------------------------------------------- /components/LayoutContainer.tsx: -------------------------------------------------------------------------------- 1 | const LayoutContainer: React.FCC = ({ children }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default LayoutContainer; 6 | -------------------------------------------------------------------------------- /components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const Logo: React.FCC = () => { 4 | return ( 5 |
6 | 7 | Your Name 8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Logo; 14 | -------------------------------------------------------------------------------- /components/MDXComponents.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import configuration from '~/configuration'; 4 | import PostsList from './PostsList'; 5 | import ClientOnly from '~/components/ClientOnly'; 6 | 7 | type ImageLayout = 'fixed' | 'fill' | 'intrinsic' | 'responsive' | undefined; 8 | type StringObject = Record; 9 | 10 | const NextImage: React.FCC = (props: StringObject) => { 11 | const width = props.width ?? '4'; 12 | const height = props.height ?? '3'; 13 | 14 | return ( 15 | {props.alt} 24 | ); 25 | }; 26 | 27 | const ExternalLink: React.FCC<{ href: string }> = ({ href, children }) => { 28 | const siteUrl = configuration.site.siteUrl ?? ''; 29 | const isRoot = href[0] === '/'; 30 | const isInternalLink = href.startsWith(siteUrl) || isRoot; 31 | 32 | if (isInternalLink) { 33 | return {children}; 34 | } 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | }; 42 | 43 | const Video: React.FCC<{ 44 | src: string; 45 | width?: string; 46 | type?: string; 47 | }> = ({ src, type, width }) => { 48 | const useType = type ?? 'video/mp4'; 49 | 50 | return ( 51 | 52 | 63 | 64 | ); 65 | }; 66 | 67 | const MDXComponents = { 68 | img: NextImage, 69 | a: ExternalLink, 70 | PostsList, 71 | Video, 72 | Image: NextImage, 73 | }; 74 | 75 | export default MDXComponents; 76 | -------------------------------------------------------------------------------- /components/MDXRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as runtime from 'react/jsx-runtime'; 2 | import { runSync } from '@mdx-js/mdx'; 3 | import MDXComponents from '~/components/MDXComponents'; 4 | 5 | type MdxComponent = React.ExoticComponent<{ 6 | components: Record; 7 | }>; 8 | 9 | function MDXRenderer({ code }: { code: string }) { 10 | const { default: MdxModuleComponent } = runSync(code, runtime) as { 11 | default: MdxComponent; 12 | }; 13 | 14 | return ; 15 | } 16 | 17 | export default MDXRenderer; 18 | -------------------------------------------------------------------------------- /components/Meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import configuration from '~/configuration'; 3 | 4 | const Meta = () => { 5 | const siteUrl = configuration.site.siteUrl; 6 | 7 | if (!siteUrl) { 8 | throw new Error(`Please add the property siteUrl in the configuration`); 9 | } 10 | 11 | const structuredData = { 12 | name: configuration.site.name, 13 | url: configuration.site.siteUrl, 14 | logo: `${configuration.site.siteUrl}/assets/images/favicon/favicon-150x150.png`, 15 | '@context': 'https://schema.org', 16 | '@type': 'Organization', // change to person for Personal websites 17 | }; 18 | 19 | return ( 20 | 21 | 26 | 27 | 33 | 34 | 40 | 41 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 75 | 76 | {configuration.site.name} 77 | 78 | 83 | 84 | 85 | 86 | 91 | 92 | 97 | 98 | 99 | 100 | 105 | 106 | 110 | 111 |