├── .babelrc ├── .gitignore ├── README.md ├── _content ├── _templates │ └── frontmatter.md ├── notes │ └── a-note.mdx └── posts │ ├── code-generation-with-hygen.mdx │ ├── next-styled-components.mdx │ └── relative-image-paths-in-mdx-with-remark.mdx ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── heart.png └── ogImage.png └── src ├── components ├── Example.js ├── Footer │ ├── index.js │ └── styles.js ├── GitHub.js ├── Header.js ├── HomePage │ ├── index.js │ └── styles.js ├── MDXOverrideComponents.js ├── PostPage │ ├── index.js │ └── styles.js ├── PostSnippet │ ├── index.js │ └── styles.js ├── SEO.js ├── Twitter.js ├── Width.js └── index.js ├── pages ├── [slug].js ├── _app.js ├── _document.js ├── about.mdx ├── garden.js └── index.js ├── site.config.js ├── styles ├── Global.js └── Theme.js └── utils ├── contentGlob.js ├── getPathsForContent.js ├── getSinglePost.js ├── index.js └── renderWithReact.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "styled-components", 6 | { 7 | "ssr": true 8 | } 9 | ], 10 | "@babel/plugin-proposal-export-default-from", 11 | [ 12 | "module-resolver", 13 | { 14 | "root": [ 15 | "./src" 16 | ] 17 | } 18 | ] 19 | ] 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | *.code-workspace 33 | 34 | .obsidian -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌱 Next + MDX Digital Garden Starter 2 | 3 | An opinionated starting point for Digital Garden content authoring. 4 | 5 | Note: This setup does not handle code-splitting. I tried switching to `mdx-bundler` but could not get it to work. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?s=https%3A%2F%2Fgithub.com%2FRyanWarner%2Fnext-mdx-digital-garden-starter) 8 | 9 | ## Getting started 10 | 11 | ``` 12 | npm install 13 | npm run dev 14 | ``` 15 | 16 | ## Use 17 | 18 | - Write MDX in the `content` directory. 19 | - Put components in the `components` directory. 20 | - Use components in MDX (without imports). 21 | 22 | ## Goals 23 | 24 | Statically generated pages from MDX files that are not tied to the filesystem path 25 | 26 | ## How it works 27 | 28 | - `[slug].js` generates static paths using `getStaticPaths` from all `.mdx` files located in the specified directory (`content/` by default). 29 | - The `getStaticProps` NextJS method passes the MDX content including front-matter (parsed with gray-matter) to the `` component to be rendered. 30 | - The index or home page uses similar logic to generate a list of posts sorted by date. 31 | 32 | ## TODO 33 | 34 | - [ ] Support filesystem-based routing as an option 35 | - [ ] Add some batteries-included examples 36 | - [ ] Write a tutorial 37 | -------------------------------------------------------------------------------- /_content/_templates/frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | description: 4 | date: {{date:MMMM Do YYYY}} 5 | timestamp: {{date}}T{{time}} 6 | isPublished: false 7 | --- -------------------------------------------------------------------------------- /_content/notes/a-note.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'A note title' 3 | description: 'This MDX file lives in a different directory, exemplifying the ability to organize your content your way.' 4 | date: 'July 20, 2020' 5 | timestamp: 2021-01-15T22:37 6 | --- 7 | 8 | This is a note with front-matter. -------------------------------------------------------------------------------- /_content/posts/code-generation-with-hygen.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Code generation with Hygen' 3 | description: 'Automate the creation of new components, MDX content, or any other commonly repeated boilerplate.' 4 | date: 'July 20, 2020' 5 | timestamp: 2021-08-15T22:37 6 | --- 7 | 8 | End goal: 2-3 letter command to scaffold a new React component exactly the way you want it. -------------------------------------------------------------------------------- /_content/posts/next-styled-components.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Next & Styled Components' 3 | description: 'Step by step instructions for installing Styled Components and using it with this starter.' 4 | date: 'July 21, 2020' 5 | timestamp: 2021-09-15T22:37 6 | --- 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm i styled-components babel-plugin-styled-components 12 | ``` 13 | 14 | ## Configuration 15 | 16 | 1. Add the [Babel plugin](https://styled-components.com/docs/tooling#babel-plugin) to your `.babelrc`. Make sure `ssr` is set to `true` because our static page generation happens server side. 17 | 18 | ``` 19 | "plugins": [ 20 | [ 21 | "styled-components", 22 | { 23 | "ssr": true 24 | } 25 | ], 26 | ... 27 | ``` 28 | 29 | 2. Configure Styled Components for Server Side Rendering. 30 | 31 | To learn more about how and why we need to do this, see the [Styled Components advanced docs](https://styled-components.com/docs/advanced#server-side-rendering). 32 | We'll be using the [official Next example](https://github.com/vercel/next.js/tree/canary/examples/with-styled-components) as a guide. 33 | 34 | Create a new file, `pages/_document.js` 35 | 36 | ```js 37 | import Document from 'next/document' 38 | import { ServerStyleSheet } from 'styled-components' 39 | 40 | export default class MyDocument extends Document { 41 | static async getInitialProps(ctx) { 42 | const sheet = new ServerStyleSheet() 43 | const originalRenderPage = ctx.renderPage 44 | 45 | try { 46 | ctx.renderPage = () => 47 | originalRenderPage({ 48 | enhanceApp: (App) => (props) => 49 | sheet.collectStyles(), 50 | }) 51 | 52 | const initialProps = await Document.getInitialProps(ctx) 53 | return { 54 | ...initialProps, 55 | styles: ( 56 | <> 57 | {initialProps.styles} 58 | {sheet.getStyleElement()} 59 | 60 | ), 61 | } 62 | } finally { 63 | sheet.seal() 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | 3. Start styling 🎨 70 | 71 | You're all set to start using Styled Components in your NextJS app! -------------------------------------------------------------------------------- /_content/posts/relative-image-paths-in-mdx-with-remark.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Relative image paths in MDX with Remark' 3 | description: 'Tame your images in MDX with the help of Remark.' 4 | date: 'July 22, 2020' 5 | timestamp: 2021-02-15T22:37 6 | --- 7 | 8 | 9 | 10 | This image lives inside the public/ dir. It could be nice to use next/image for this. 11 | ![heart](./heart.png) 12 | 13 | It indicates a synchronic distortion in the areas emanating triolic waves. The cerebellum, the cerebral cortex, the brain stem, the entire nervous system has been depleted of electrochemical energy. Any device like that would produce high levels of triolic waves. These walls have undergone some kind of selective molecular polarization. I haven't determined if our phaser energy can generate a stable field. We could alter the photons with phase discriminators. 14 | 15 | I have reset the sensors to scan for frequencies outside the usual range. By emitting harmonic vibrations to shatter the lattices. We will monitor and adjust the frequency of the resonators. He has this ability of instantly interpreting and extrapolating any verbal communication he hears. It may be due to the envelope over the structure, causing hydrogen-carbon helix patterns throughout. I'm comparing the molecular integrity of that bubble against our phasers. 16 | 17 | Communication is not possible. The shuttle has no power. Using the gravitational pull of a star to slingshot back in time? We are going to Starbase Montgomery for Engineering consultations prompted by minor read-out anomalies. Probes have recorded unusual levels of geological activity in all five planetary systems. Assemble a team. Look at records of the Drema quadrant. Would these scans detect artificial transmissions as well as natural signals? 18 | 19 | Sensors indicate human life forms 30 meters below the planet's surface. Stellar flares are increasing in magnitude and frequency. Set course for Rhomboid Dronegar 006, warp seven. There's no evidence of an advanced communication network. Total guidance system failure, with less than 24 hours' reserve power. Shield effectiveness has been reduced 12 percent. We have covered the area in a spherical pattern which a ship without warp drive could cross in the given time. -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require('@next/mdx')({ 2 | extension: /\.(md|mdx)$/ 3 | }) 4 | 5 | module.exports = (nextConfig = {}) => { 6 | return Object.assign({}, nextConfig, withMDX({ 7 | pageExtensions: ['js', 'jsx', 'mdx'], 8 | })) 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-mdx-digital-garden-starter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next-remote-watch _content", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "prettier-standard --lint 'src/**/*.{js,jsx,json,md}'" 10 | }, 11 | "dependencies": { 12 | "@mdx-js/loader": "^2.0.0-next.9", 13 | "@mdx-js/runtime": "^2.0.0-next.9", 14 | "@mdx-js/tag": "^0.20.3", 15 | "@next/mdx": "11.0.1", 16 | "fast-glob": "3.2.7", 17 | "gray-matter": "4.0.3", 18 | "next": "11.0.1", 19 | "prop-types": "15.7.2", 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2", 22 | "styled-components": "5.3.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.14.6", 26 | "@babel/plugin-proposal-export-default-from": "7.14.5", 27 | "@babel/plugin-proposal-object-rest-spread": "^7.14.7", 28 | "@babel/plugin-transform-react-jsx": "^7.14.5", 29 | "babel-plugin-module-resolver": "4.1.0", 30 | "babel-plugin-remove-export-keywords": "1.6.22", 31 | "babel-plugin-styled-components": "1.13.2", 32 | "next-remote-watch": "^1.0.0", 33 | "prettier-standard": "16.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanWarner/next-mdx-digital-garden-starter/01d5dad4aaf9e926238d3c62b5eb7c73b753a7dd/public/favicon.png -------------------------------------------------------------------------------- /public/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanWarner/next-mdx-digital-garden-starter/01d5dad4aaf9e926238d3c62b5eb7c73b753a7dd/public/heart.png -------------------------------------------------------------------------------- /public/ogImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanWarner/next-mdx-digital-garden-starter/01d5dad4aaf9e926238d3c62b5eb7c73b753a7dd/public/ogImage.png -------------------------------------------------------------------------------- /src/components/Example.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const ExampleComponent = styled.div` 4 | color: green; 5 | ` 6 | 7 | const Example = props => ( 8 | This is an example component 9 | ) 10 | 11 | export default Example 12 | -------------------------------------------------------------------------------- /src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import * as S from './styles' 2 | import { Width } from 'components' 3 | import siteConfig from 'site.config' 4 | 5 | const Footer = props => ( 6 | 7 | 8 | 9 | © {new Date().getFullYear()} {siteConfig.author} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | 23 | export default Footer 24 | -------------------------------------------------------------------------------- /src/components/Footer/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | import Twitter from '../Twitter' 4 | import GitHub from '../GitHub' 5 | 6 | export const Footer = styled.footer` 7 | height: 70px; 8 | display: flex; 9 | align-items: center; 10 | width: 100%; 11 | border-top: 1px solid ${props => props.theme.rule}; 12 | ` 13 | 14 | export const Copyright = styled.p` 15 | display: flex; 16 | align-items: center; 17 | ` 18 | 19 | export const Social = styled.div` 20 | display: flex; 21 | align-items: center; 22 | margin-left: auto; 23 | ` 24 | 25 | export const StyledGitHub = styled(GitHub)` 26 | margin-left: 12px; 27 | ` 28 | 29 | export const StyledTwitter = styled(Twitter)`` 30 | 31 | export const A = styled.a` 32 | &:hover { 33 | path { 34 | fill: ${props => props.theme.green10}; 35 | } 36 | } 37 | ` 38 | -------------------------------------------------------------------------------- /src/components/GitHub.js: -------------------------------------------------------------------------------- 1 | const GitHub = props => ( 2 | 11 | GitHub Logo 12 | 16 | 17 | ) 18 | 19 | export default GitHub 20 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import siteConfig from 'site.config' 3 | import styled from 'styled-components' 4 | 5 | import { Width } from 'components' 6 | 7 | const Nav = styled.nav` 8 | height: 70px; 9 | display: flex; 10 | align-items: center; 11 | width: 100%; 12 | ` 13 | 14 | const Wordmark = styled.div` 15 | display: flex; 16 | align-items: center; 17 | ` 18 | 19 | const Logo = styled.span` 20 | margin-right: 8px; 21 | font-size: 20px; 22 | ` 23 | 24 | const Name = styled.span` 25 | font-weight: bold; 26 | ` 27 | 28 | const NavItems = styled.ul` 29 | margin-left: auto; 30 | display: flex; 31 | ` 32 | 33 | export const A = styled.a` 34 | text-decoration: none; 35 | color: ${props => props.theme.text10}; 36 | 37 | &:hover { 38 | color: ${props => props.theme.green10}; 39 | } 40 | ` 41 | 42 | export const NavItem = styled.li` 43 | margin-left: 24px; 44 | list-style-type: none; 45 | ` 46 | 47 | const Header = props => ( 48 | 49 | 71 | 72 | ) 73 | 74 | export default Header 75 | -------------------------------------------------------------------------------- /src/components/HomePage/index.js: -------------------------------------------------------------------------------- 1 | import { SEO, PostSnippet, GitHub, Header } from 'components' 2 | import * as S from './styles' 3 | 4 | const githubUrl = 5 | 'https://github.com/RyanWarner/next-mdx-digital-garden-starter' 6 | 7 | export default function HomePage ({ allMdx }) { 8 | return ( 9 | 10 | 11 |
12 | 13 | 14 | NextJS + MDX 15 |
16 | Digital Garden Starter 17 |
18 | 19 | 20 | 21 | Create top level routes from .mdx files organized however you want. 22 | 23 | 24 | Statically generated routes using Next’s `getStaticPaths`. 25 | 26 | Supports frontmatter (thanks to gray-matter). 27 | 28 | 29 | 30 | 31 | View source on GitHub 32 | 33 | 34 | Featured posts 35 | 36 | {allMdx.map(item => ( 37 | 38 | 39 | 40 | ))} 41 | 42 |
43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/HomePage/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Footer } from 'components' 3 | 4 | export const Wrap = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | padding: 0 30px; 9 | min-height: 100vh; 10 | box-sizing: border-box; 11 | ` 12 | 13 | export const StyledFooter = styled(Footer)` 14 | margin-top: auto; 15 | padding-top: 40px; 16 | ` 17 | 18 | export const Main = styled.main` 19 | max-width: 700px; 20 | padding: 0 30px; 21 | width: 100%; 22 | margin-top: 60px; 23 | position: relative; 24 | display: flex; 25 | flex-direction: column; 26 | ` 27 | 28 | export const H1 = styled.h1` 29 | font-size: 50px; 30 | margin-bottom: 20px; 31 | ` 32 | 33 | export const GitHubButton = styled.a` 34 | border-radius: 4px; 35 | border: 1px solid black; 36 | padding: 10px 12px; 37 | color: black; 38 | text-decoration: none; 39 | margin-top: 30px; 40 | display: grid; 41 | gap: 14px; 42 | grid-template-columns: auto auto; 43 | align-self: flex-start; 44 | ` 45 | 46 | export const H2 = styled.h2` 47 | letter-spacing: 0.1em; 48 | text-transform: uppercase; 49 | font-size: 14px; 50 | margin-top: 100px; 51 | color: ${props => props.theme.text20}; 52 | ` 53 | 54 | export const FeatureList = styled.ul` 55 | margin: 8px 0 0 0; 56 | padding: 0 0 0 30px; 57 | ` 58 | 59 | export const ListItem = styled.li` 60 | font-size: 21px; 61 | margin-bottom: 16px; 62 | color: ${props => props.theme.text20}; 63 | ` 64 | 65 | export const PostList = styled.ul` 66 | margin: 22px 0 0 0; 67 | padding: 0; 68 | list-style-type: none; 69 | ` 70 | 71 | export const PostListItem = styled.li` 72 | margin-bottom: 30px; 73 | ` 74 | -------------------------------------------------------------------------------- /src/components/MDXOverrideComponents.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const p = styled.p` 4 | font-size: 18px; 5 | line-height: 180%; 6 | margin-bottom: 3rem; 7 | ` 8 | -------------------------------------------------------------------------------- /src/components/PostPage/index.js: -------------------------------------------------------------------------------- 1 | import * as S from './styles' 2 | import { SEO, Header } from 'components' 3 | 4 | const PostPage = ({ frontmatter, mdxHtml }) => { 5 | return ( 6 | <> 7 | 8 | 9 |
10 | 11 | {frontmatter.title} 12 |

{frontmatter.description}

13 |
14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default PostPage 21 | -------------------------------------------------------------------------------- /src/components/PostPage/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrap = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | padding: 0 30px; 8 | ` 9 | 10 | export const Main = styled.main` 11 | max-width: 700px; 12 | width: 100%; 13 | ` 14 | 15 | export const H1 = styled.h1` 16 | color: green; 17 | ` 18 | -------------------------------------------------------------------------------- /src/components/PostSnippet/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import * as S from './styles' 4 | 5 | export default function PostSnippet ({ slug, frontmatter }) { 6 | const href = `/${slug}` 7 | 8 | return ( 9 | <> 10 | 11 | 18 | {frontmatter.title} 19 | 20 | 21 | {frontmatter.description} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/PostSnippet/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Description = styled.p` 4 | font-size: 14px; 5 | margin: 6px 0 0 0; 6 | color: ${props => props.theme.text20}; 7 | max-width: 400px; 8 | line-height: 160%; 9 | ` 10 | -------------------------------------------------------------------------------- /src/components/SEO.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | 3 | import siteConfig from 'site.config' 4 | 5 | export default function SEO ({ 6 | title, 7 | description, 8 | twitterHandle, 9 | favicon, 10 | ogImage, 11 | url 12 | }) { 13 | return ( 14 | 15 | {title} 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | SEO.defaultProps = { 38 | ...siteConfig 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Twitter.js: -------------------------------------------------------------------------------- 1 | const Twitter = props => ( 2 | 10 | 14 | 15 | ) 16 | 17 | export default Twitter 18 | -------------------------------------------------------------------------------- /src/components/Width.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const MaxWidth = styled.div` 4 | max-width: 700px; 5 | width: 100%; 6 | ` 7 | 8 | const CenterContainer = styled.div` 9 | display: flex; 10 | justify-content: center; 11 | width: 100%; 12 | ` 13 | 14 | const Width = ({ children, ...rest }) => ( 15 | 16 | {children} 17 | 18 | ) 19 | 20 | export default Width 21 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export SEO from './SEO' 2 | export Width from './Width' 3 | export Header from './Header' 4 | export Footer from './Footer' 5 | export Example from './Example' 6 | export PostSnippet from './PostSnippet' 7 | export GitHub from './GitHub' 8 | export Twitter from './Twitter' 9 | export HomePage from './HomePage' 10 | export PostPage from './PostPage' 11 | export * from './MDXOverrideComponents' 12 | -------------------------------------------------------------------------------- /src/pages/[slug].js: -------------------------------------------------------------------------------- 1 | import * as components from 'components' 2 | import getSinglePost from 'utils/getSinglePost' 3 | import getPathsForContent from 'utils/getPathsForContent' 4 | 5 | export async function getStaticPaths() { 6 | const paths = getPathsForContent() 7 | 8 | return { 9 | paths, 10 | fallback: false 11 | } 12 | } 13 | 14 | export async function getStaticProps({ params: { slug } }) { 15 | const { mdxHtml, frontmatter } = await getSinglePost(slug) 16 | 17 | return { 18 | props: { 19 | mdxHtml, 20 | frontmatter 21 | } 22 | } 23 | } 24 | 25 | export default components.PostPage 26 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import App from 'next/app' 2 | import { ThemeProvider } from 'styled-components' 3 | 4 | import { GlobalStyle } from '../styles/Global' 5 | import Theme from 'styles/Theme' 6 | 7 | export default class MyApp extends App { 8 | render () { 9 | const { Component, pageProps } = this.props 10 | return ( 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document from 'next/document' 2 | import { ServerStyleSheet } from 'styled-components' 3 | import { MDXProvider } from '@mdx-js/react' 4 | import glob from 'fast-glob' 5 | import fs from 'fs' 6 | import MDXRuntime from '@mdx-js/runtime' 7 | 8 | import * as components from 'components' 9 | import contentGlob from 'utils/contentGlob' 10 | 11 | /** 12 | * We have to include all of our MDX documents in the 13 | * collectStyles call for Styled Components to work properly. 14 | * 15 | * This could potentially create performance issues on large 16 | * sites with lots of bespoke components. Is there a better way? 17 | */ 18 | export const getAllMdx = async () => { 19 | const files = glob.sync(contentGlob) 20 | const mdxDocs = [] 21 | 22 | for (const file of files) { 23 | const mdxSource = fs.readFileSync(file) 24 | 25 | mdxDocs.push() 26 | } 27 | 28 | return () => ( 29 | 30 | {mdxDocs.map(MdxDoc => { 31 | return MdxDoc 32 | })} 33 | 34 | ) 35 | } 36 | 37 | export default class MyDocument extends Document { 38 | static async getInitialProps (ctx) { 39 | const sheet = new ServerStyleSheet() 40 | const originalRenderPage = ctx.renderPage 41 | 42 | const MDX = await getAllMdx() 43 | 44 | try { 45 | ctx.renderPage = () => 46 | originalRenderPage({ 47 | enhanceApp: App => props => 48 | sheet.collectStyles( 49 | <> 50 | 51 | {MDX && } 52 | 53 | ) 54 | }) 55 | 56 | const initialProps = await Document.getInitialProps(ctx) 57 | return { 58 | ...initialProps, 59 | styles: ( 60 | <> 61 | {initialProps.styles} 62 | {sheet.getStyleElement()} 63 | 64 | ) 65 | } 66 | } finally { 67 | sheet.seal() 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/about.mdx: -------------------------------------------------------------------------------- 1 | # MDX powered About page! 2 | 3 | Here lies some MDX. 4 | 5 | ``` 6 | const code = {} 7 | ``` -------------------------------------------------------------------------------- /src/pages/garden.js: -------------------------------------------------------------------------------- 1 | import { SEO, Header } from 'components' 2 | 3 | const Garden = () => { 4 | return ( 5 |
6 | 7 |
8 |

Garden

9 |

This is where we should put a list of our garden posts.

10 |
11 | ) 12 | } 13 | 14 | export default Garden -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import matter from 'gray-matter' 3 | import glob from 'fast-glob' 4 | 5 | import { HomePage } from 'components' 6 | import contentGlob from 'utils/contentGlob' 7 | 8 | export default HomePage 9 | 10 | export async function getStaticProps () { 11 | const files = glob.sync(contentGlob) 12 | 13 | const allMdx = files.map(file => { 14 | const split = file.split('/') 15 | const filename = split[split.length - 1] 16 | const slug = filename.replace('.mdx', '').replace('.md', '') 17 | 18 | const mdxSource = fs.readFileSync(file) 19 | const { data } = matter(mdxSource) 20 | 21 | return { 22 | slug, 23 | frontmatter: data 24 | } 25 | }) 26 | 27 | const orderedByDate = allMdx.sort((a, b) => { 28 | return new Date(b.frontmatter.timestamp) - new Date(a.frontmatter.timestamp) 29 | }) 30 | 31 | return { 32 | props: { 33 | allMdx: orderedByDate 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/site.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'NextJS MDX Digital Garden Starter', 3 | description: 4 | 'An opinionated starting point for authoring interactive content using MDX and NextJS', 5 | twitterHandle: '@RyanWarnerCodes', 6 | githubUsername: 'RyanWarner', 7 | author: 'Your Name', 8 | favicon: '/favicon.png', 9 | ogImage: '/ogImage.png', 10 | url: 'https://next-mdx.warner.codes' 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/Global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | html { 5 | margin: 0; 6 | padding: 0; 7 | max-width: 100vw; 8 | background-color: ${props => props.theme.background}; 9 | color: ${props => props.theme.text10}; 10 | font-family: ${props => props.theme.fontFamily}; 11 | } 12 | 13 | body { 14 | min-height: 100vh; 15 | margin: 0; 16 | padding: 0; 17 | max-width: 100vw; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | div { 23 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 24 | } 25 | 26 | ::selection { 27 | background: ${props => props.theme.highlightBackground}; 28 | color: ${props => props.theme.text10}; 29 | } 30 | ` 31 | -------------------------------------------------------------------------------- /src/styles/Theme.js: -------------------------------------------------------------------------------- 1 | const typeTokens = { 2 | fontFamily: 'Lato, sans-serif' 3 | } 4 | 5 | export default { 6 | light: { 7 | ...typeTokens, 8 | background: '#FBFBF9', 9 | text10: '#1E1E1B', 10 | text20: '#696966', 11 | rule: 'rgba(0, 0, 0, 0.1)', 12 | green10: '#79BB40', 13 | highlightBackground: '#ABEF70' 14 | }, 15 | dark: { 16 | ...typeTokens, 17 | background: '#171716', 18 | text10: '#FBFBF9', 19 | text20: '#9C9C96', 20 | rule: 'rgba(255, 255, 255, 0.1)', 21 | green10: '#79BB40', 22 | highlightBackground: '#ABEF70' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/contentGlob.js: -------------------------------------------------------------------------------- 1 | const contentGlob = ['_content/**/*.{md,mdx}', '!_content/_templates'] 2 | export default contentGlob -------------------------------------------------------------------------------- /src/utils/getPathsForContent.js: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob' 2 | 3 | import contentGlob from './contentGlob' 4 | 5 | const getSinglePost = () => { 6 | const files = glob.sync(contentGlob) 7 | 8 | const paths = files.map(file => { 9 | const split = file.split('/') 10 | const filename = split[split.length - 1] 11 | const slug = filename.replace('.mdx', '').replace('.md', '') 12 | 13 | return { 14 | params: { 15 | slug 16 | } 17 | } 18 | }) 19 | 20 | return paths 21 | } 22 | 23 | export default getSinglePost -------------------------------------------------------------------------------- /src/utils/getSinglePost.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import matter from 'gray-matter' 3 | import glob from 'fast-glob' 4 | 5 | import * as components from 'components' 6 | import { renderWithReact } from 'utils/renderWithReact' 7 | import contentGlob from './contentGlob' 8 | 9 | const getSinglePost = async (slug) => { 10 | const files = glob.sync(contentGlob) 11 | 12 | const fullPath = files.filter(item => { 13 | const split = item.split('/') 14 | const filename = split[split.length - 1] 15 | return filename.replace('.mdx', '').replace('.md', '') === slug 16 | })[0] 17 | 18 | const mdxSource = fs.readFileSync(fullPath) 19 | const { content, data } = matter(mdxSource) 20 | 21 | if (!fullPath) { 22 | console.warn('No MDX file found for slug') 23 | } 24 | 25 | const mdxHtml = await renderWithReact(content, { components }) 26 | 27 | return { 28 | mdxHtml, 29 | frontmatter: data || {} 30 | } 31 | } 32 | 33 | export default getSinglePost -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob' 2 | 3 | import contentGlob from './contentGlob' 4 | 5 | export const getSlugForFilePath = path => { 6 | const split = path.split('/') 7 | const filename = split[split.length - 1] 8 | const slug = filename.replace('.mdx', '').replace('.md', '') 9 | return slug 10 | } 11 | 12 | export const getContent = () => { 13 | const files = glob.sync(contentGlob) 14 | 15 | return files.map(item => ({ 16 | filepath: item, 17 | slug: getSlugForFilePath(item) 18 | })) 19 | } 20 | 21 | const content = getContent() 22 | 23 | export const getFilePathForSlug = slug => { 24 | return content.filter(item => item.slug === slug)[0].filepath 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/renderWithReact.js: -------------------------------------------------------------------------------- 1 | /* Created from: https://github.com/mdx-js/mdx/blob/master/packages/test-util/index.js */ 2 | const babel = require('@babel/core') 3 | const { MDXProvider, mdx: createElement } = require('@mdx-js/react') 4 | const React = require('react') 5 | const { renderToStaticMarkup } = require('react-dom/server') 6 | 7 | const mdx = require('@mdx-js/mdx') 8 | 9 | const transform = code => 10 | babel.transform(code, { 11 | plugins: [ 12 | '@babel/plugin-transform-react-jsx', 13 | '@babel/plugin-proposal-object-rest-spread', 14 | 'babel-plugin-remove-export-keywords' 15 | ] 16 | }).code 17 | 18 | const renderWithReact = async (mdxCode, { components } = {}) => { 19 | const jsx = await mdx(mdxCode, { skipExport: true }) 20 | const code = transform(jsx) 21 | const scope = { mdx: createElement } 22 | 23 | const fn = new Function( // eslint-disable-line no-new-func 24 | 'React', 25 | ...Object.keys(scope), 26 | `${code}; return React.createElement(MDXContent)` 27 | ) 28 | 29 | const element = fn(React, ...Object.values(scope)) 30 | 31 | const elementWithProvider = React.createElement( 32 | MDXProvider, 33 | { components }, 34 | element 35 | ) 36 | 37 | return renderToStaticMarkup(elementWithProvider) 38 | } 39 | 40 | module.exports.renderWithReact = renderWithReact 41 | --------------------------------------------------------------------------------