├── .eslintrc.yml ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── generated-website-bug-report.md │ └── medium-import-bug-report.md ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin ├── import-medium-article.js └── medium-to-own-blog.js ├── docs ├── README.md ├── local-workflow.md ├── online-workflow.md └── screencast.gif ├── gatsby-template ├── .dockerignore ├── Dockerfile ├── README.md ├── config.js ├── gatsby-config.js ├── gatsby-node.js ├── netlify.toml └── package.json ├── gatsby-theme ├── .gitignore ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── index.js ├── package.json ├── src │ ├── components │ │ ├── bio.js │ │ ├── code-highlighting.js │ │ ├── embed.js │ │ ├── layout.js │ │ ├── main-bio.js │ │ ├── pills.js │ │ ├── responses.js │ │ ├── section.js │ │ ├── seo.js │ │ └── wrap-root-element.js │ ├── pages │ │ ├── 404.js │ │ └── index.js │ ├── templates │ │ └── blog-post.js │ ├── theme.js │ └── utils │ │ ├── dates.js │ │ └── string.js ├── static │ ├── admin │ │ └── config.yml │ ├── prism-theme.css │ └── robot.txt └── utils │ └── default-options.js ├── lib ├── add-gatsby-files.js ├── default-icon.png ├── generate-md.js ├── get-profile.js ├── import-article-from-medium.js └── utils.js ├── package-lock.json └── package.json /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | parser: babel-eslint 3 | extends: 4 | - airbnb 5 | - prettier 6 | plugins: 7 | - prettier 8 | 9 | rules: 10 | ########### 11 | # PLUGINS # 12 | ########### 13 | import/prefer-default-export: 0 14 | react/jsx-filename-extension: 0 15 | react/jsx-one-expression-per-line: 0 16 | react/prop-types: 0 17 | react/jsx-props-no-spreading: 0 18 | react/jsx-indent: 0 19 | react/no-unescaped-entities: 0 20 | 21 | ########### 22 | # BUILTIN # 23 | ########### 24 | no-param-reassign: 0 25 | no-underscore-dangle: 0 26 | no-plusplus: 0 27 | 28 | ########### 29 | # SPECIAL # 30 | ########### 31 | prettier/prettier: 32 | - error 33 | - singleQuote: true 34 | trailingComma: es5 35 | semi: false 36 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /lib @mathieudutour 2 | /bin @mathieudutour 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mathieu@dutour.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Any idea on how to make the process easier or how to improve the generated blog? [Open a new issue](https://github.com/mathieudutour/medium-to-own-blog/issues/new)! We need all the help we can get to make this project awesome! 2 | 3 | ## Getting Started 4 | 5 | 1. Clone the project 6 | 2. Run `npm install` 7 | 3. Run `npm test` 8 | 9 | Any file outsite `gatsby-template` is related to the import script. To verify changes, use any `medium-export.zip file. 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: mathieudutour 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/generated-website-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generated website bug report 3 | about: Report an issue of the generated website 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/medium-import-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Medium import bug report 3 | about: Report an issue when importing your Medium content 4 | title: '' 5 | labels: 'bug: medium import' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | - What's the URL of the Medium article it is failing to parse? 16 | - What part of the article isn't parsed correctly? 17 | 18 | **Screenshots** 19 | If applicable, add screenshots to help explain your problem. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | mathieudutour-blog 3 | gatsby-template/package-lock.json 4 | gatsby-theme/package-lock.json 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mathieu Dutour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Medium to own blog 2 | 3 | _Switch from Medium to your own blog in a few minutes._ 4 | 5 | [![demo](./docs/screencast.gif)](https://twitter.com/MathieuDutour/status/1134448154793914368) 6 | 7 | ## :rocket: QuickStart 8 | 9 | _Requires [Node.js](https://nodejs.org/en/)_ 10 | 11 | ```bash 12 | npx medium-to-own-blog 13 | ``` 14 | 15 | ## :link: Live Demo 16 | 17 | Here's a [live demo](https://mathieudutour-blog.netlify.com). 18 | 19 | ## :muscle: Motivation 20 | 21 | There is no shortage of explanations behind exiting Medium. Here is a few selection of articles: 22 | 23 | - [https://m.signalvnoise.com/signal-v-noise-exits-medium/](https://m.signalvnoise.com/signal-v-noise-exits-medium/) 24 | - [https://www.gautamdhameja.com/why-i-migrated-my-blog-from-medium/](https://www.gautamdhameja.com/why-i-migrated-my-blog-from-medium/) 25 | - [https://baremetrics.com/blog/medium-back-to-blog](https://baremetrics.com/blog/medium-back-to-blog) 26 | 27 | ## :fire: Features 28 | 29 | - Own your content 30 | - Write using Markdown / [MDX](https://github.com/mdx-js/mdx) 31 | - Syntax Highlighting using Prism 32 | - Edit on Github 33 | - Fully customizable 34 | - Rich embeds using MDX 35 | - Easy deployment: Deploy on Netlify / Now.sh / Docker 36 | - SEO friendly 37 | - :100: on the Performance, Accessibility, Best Practices, and SEO's [LightHouse tests](https://developers.google.com/web/tools/lighthouse/) 38 | 39 | ## :book: Documentation 40 | 41 | Head over [here](./docs/README.md) to find a few guides to help you editing the content of your newly created blog. 42 | 43 | ## :pencil2: Contributing 44 | 45 | Any idea on how to make the process easier or how to improve the generated blog? [Open a new issue](https://github.com/mathieudutour/medium-to-own-blog/issues/new)! We need all the help we can get to make this project awesome! 46 | 47 | ## :shell: Technical stack 48 | 49 | This project is only possible thanks to the awesomeness of the following projects: 50 | 51 | - [Gatsby](https://www.gatsbyjs.org/) 52 | - [Markdown / MDX](https://github.com/mdx-js/mdx) 53 | - [GitHub](https://github.com) 54 | - [Netlify](https://netlify.com) 55 | 56 | ## :tm: License 57 | 58 | MIT 59 | 60 | ## Migration Troubleshooting 61 | 62 | Since everyone has different content in their Medium blogs, you might encounter some issues that can't be fixed in a standardized way or aren't worth trying. These issues and potential workarounds will be posted below: 63 | 64 | - **JSX closing tag parsing error** - [Issue #56](https://github.com/mathieudutour/medium-to-own-blog/issues/56). You may have some self-closing, void tags in your blog posts. JSX requires all tags to be self-closed so even though the HTML break tag can be written as `
`, you will need to change the syntax to read `
` or go back later after running the migration and place the tags in a code block. 65 | - **GitHub authentication errors** - [Issue #54](https://github.com/mathieudutour/medium-to-own-blog/issues/54). GitHub allows users to set up authentication several different ways. For instance, if you have two-factor authentication enabled, you have to provide a token in certain cases when cloning down repositories. Please check your authentication settings if you experience any issues related to authentication failures. 66 | -------------------------------------------------------------------------------- /bin/import-medium-article.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | const inquirer = require('inquirer') 5 | const ora = require('ora') 6 | const { 7 | getMarkdownFromOnlinePost, 8 | } = require('../lib/import-article-from-medium') 9 | 10 | // eslint-disable-next-line no-console 11 | console.log(` ------------------------- 12 | 13 | Hello there! 14 | 15 | Let's import one of your Medium article here. 16 | `) 17 | 18 | let spinner 19 | 20 | inquirer 21 | .prompt([ 22 | { 23 | name: 'canonicalLink', 24 | message: 'URL of the Medium article', 25 | }, 26 | ]) 27 | .then(({ canonicalLink }) => { 28 | canonicalLink = canonicalLink.trim() 29 | if (!canonicalLink) { 30 | throw new Error(` 31 | We do need the URL to import the article...`) 32 | } 33 | spinner = ora('Parsing Medium article').start() 34 | 35 | return getMarkdownFromOnlinePost( 36 | path.join(process.cwd(), './content'), 37 | canonicalLink 38 | ) 39 | }) 40 | .then(slug => { 41 | if (!slug) { 42 | throw new Error( 43 | 'Looks like the URL points to a draft or a response to an article. We cannot import that.' 44 | ) 45 | } 46 | // eslint-disable-next-line no-console 47 | console.log(` 48 | ------------------------- 49 | 50 | Your article is ready to go! 🙌 51 | 52 | You can find it here: ${path.join(process.cwd(), './content', slug)} 53 | 54 | Happy blogging! 55 | ... 56 | 57 | 58 | `) 59 | }) 60 | .then(() => process.exit(0)) 61 | .catch(err => { 62 | if (spinner) { 63 | spinner.fail() 64 | } 65 | // eslint-disable-next-line no-console 66 | console.log() 67 | // eslint-disable-next-line no-console 68 | console.error(err) 69 | process.exit(1) 70 | }) 71 | -------------------------------------------------------------------------------- /bin/medium-to-own-blog.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require('child_process') 4 | const inquirer = require('inquirer') 5 | const ora = require('ora') 6 | const JSZip = require('jszip') 7 | const fs = require('fs-extra') 8 | const { getProfile } = require('../lib/get-profile') 9 | const { getMarkdownFromPost } = require('../lib/generate-md') 10 | const { addGatsbyFiles } = require('../lib/add-gatsby-files') 11 | const { exec, withOutputPath } = require('../lib/utils') 12 | 13 | // eslint-disable-next-line no-console 14 | console.log(` ------------------------- 15 | 16 | Hello there! 17 | 18 | Let's move your Medium content to your very own website. 19 | First of all, you need to download your medium content. 20 | 21 | 1. Head over https://medium.com/me/export 22 | 2. Click on the "Download your information" button 23 | 24 | In a bit, you will receive an email from Medium with a link to "Download your archive". 25 | Download it and drag-and-drop the resulting zip file here. 26 | `) 27 | 28 | let spinner 29 | let profile = {} 30 | 31 | inquirer 32 | .prompt([ 33 | { 34 | name: 'archivePath', 35 | message: 'Path to the Medium archive', 36 | }, 37 | ]) 38 | .then(({ archivePath }) => { 39 | spinner = ora('Parsing Medium content').start() 40 | return fs.readFile(archivePath.trim()) 41 | }) 42 | .then(body => { 43 | return JSZip.loadAsync(body) 44 | }) 45 | .then(zip => { 46 | if (!zip.file('profile/profile.html')) { 47 | throw new Error( 48 | 'It seems that we cannot find your profile in the Medium archive. Are you sure you gave the right path?\nIf so, please open an issue here: https://github.com/mathieudutour/medium-to-own-blog/issues/new.' 49 | ) 50 | } 51 | 52 | spinner.succeed('Parsed Medium content') 53 | spinner.start('Parsing the Medium profile') 54 | 55 | return getProfile(zip) 56 | .then(_profile => { 57 | profile = _profile 58 | }) 59 | .then(() => zip) 60 | }) 61 | .then(zip => { 62 | spinner.succeed('Parsed the Medium profile') 63 | spinner.start('Parsing the Medium posts') 64 | return zip.file(/^posts/).reduce((p, zippedPost, index, array) => { 65 | return p 66 | .then(() => { 67 | spinner.start( 68 | `Parsing the Medium posts [${index + 1}/${array.length}]` 69 | ) 70 | }) 71 | .then(() => zippedPost.async('text')) 72 | .then(content => getMarkdownFromPost(profile, content, zippedPost.name)) 73 | }, Promise.resolve()) 74 | }) 75 | .then(() => { 76 | console.warn(` 77 | 78 | ⚠️ Due to recent changes in Medium, we cannot access the posts' categories. You might want to touch them up manually. 79 | `) 80 | spinner.succeed('Parsed the Medium posts') 81 | spinner.start('Preparing the Gatsby project') 82 | 83 | return addGatsbyFiles(profile) 84 | }) 85 | .then(() => { 86 | spinner.succeed('Prepared the Gatsby project') 87 | spinner.stop() 88 | 89 | // eslint-disable-next-line no-console 90 | console.log(` 91 | ------------------------- 92 | 93 | Yay, your blog is ready! 94 | Now let's work on putting it online. 95 | 96 | GitHub is a free platform to host code online. 97 | 98 | 1. Head over https://github.com/new and create a new repository. 99 | Do NOT create it with a license or readme - otherwise issues will arise later. 100 | 2. Copy paste the URL of your new repository here 101 | `) 102 | return inquirer.prompt([ 103 | { 104 | name: 'repoURL', 105 | message: 'URL of the repository', 106 | }, 107 | ]) 108 | }) 109 | .then(({ repoURL }) => { 110 | repoURL = repoURL.trim() 111 | if (!repoURL) { 112 | throw new Error(` 113 | Looks like the repository's URL is empty. 114 | You can still use the generated project as a normal Gatsby project. 115 | 116 | You will find some information about the generated project in its README. 117 | 118 | Happy writing!`) 119 | } 120 | 121 | spinner.start('Updating the project to use the repository URL') 122 | 123 | const remoteURL = repoURL 124 | 125 | // handle if passed a git url 126 | if (repoURL.match(/^git@github.com:.*/i)) { 127 | repoURL = repoURL.replace(/^git@github.com:.*/i, 'https://github.com/') 128 | } 129 | // handle if passed url ending with .git (GitHub handle both seamlessly) 130 | repoURL = repoURL.replace(/\.git$/i, '') 131 | 132 | return Promise.all([ 133 | fs 134 | .readFile(withOutputPath(profile, './package.json'), 'utf8') 135 | .then(content => 136 | fs.writeFile( 137 | withOutputPath(profile, './package.json'), 138 | content.replace(/{{ githubURL }}/g, repoURL), 139 | 'utf8' 140 | ) 141 | ), 142 | fs 143 | .readFile(withOutputPath(profile, './config.js'), 'utf8') 144 | .then(content => 145 | fs.writeFile( 146 | withOutputPath(profile, './config.js'), 147 | content.replace(/{{ githubURL }}/g, repoURL), 148 | 'utf8' 149 | ) 150 | ), 151 | ]).then(() => 152 | exec( 153 | `git init && git remote add origin ${remoteURL} && git add . && git commit -m "first commit :tada:"`, 154 | { 155 | cwd: withOutputPath(profile), 156 | } 157 | ) 158 | ) 159 | }) 160 | .then(() => { 161 | spinner.succeed('Updated the project to use the repository URL') 162 | spinner.stop() 163 | 164 | // eslint-disable-next-line no-console 165 | console.log( 166 | 'Pushing the code for your blog to GitHub (you might be prompted for your GitHub identifiers)...\n' 167 | ) 168 | return new Promise((resolve, reject) => { 169 | const child = spawn('git', ['push', 'origin', 'master'], { 170 | cwd: withOutputPath(profile), 171 | stdio: 'inherit', 172 | }) 173 | 174 | child.on('error', () => reject()) 175 | child.on('close', () => resolve()) 176 | child.on('exit', () => resolve()) 177 | }) 178 | }) 179 | .then(() => { 180 | // eslint-disable-next-line no-console 181 | console.log(` 182 | 183 | ------------------------- 184 | 185 | Now that the code for your blog is online, 186 | we need to configure how it will be deployed. 187 | 188 | Netlify is hosting your personal project for free. 189 | 190 | 1. Head over https://app.netlify.com/start 191 | 2. Connect with GitHub 192 | 3. Pick the repository you just created 193 | 4. Click on the big green "Deploy Site" button 194 | 195 | Netlify probably gave a funny name to your project, 196 | like friendly-keller-0b06be or something, 197 | but you can choose one by going to the "Site Settings" 198 | and clicking on "Change site name". 199 | 200 | 5. Copy paste the URL of the Netlify deployment here (not the URL of Netlify dashboard) 201 | `) 202 | return inquirer.prompt([ 203 | { 204 | name: 'publicURL', 205 | message: 'URL of the blog', 206 | }, 207 | ]) 208 | }) 209 | .then(({ publicURL }) => { 210 | publicURL = publicURL.trim() 211 | if (!publicURL) { 212 | throw new Error(` 213 | Looks like the blog's URL is empty. 214 | You can still use the generated project as a normal Gatsby project. 215 | 216 | You will find some information about the generated project in its README. 217 | 218 | Happy writing!`) 219 | } 220 | 221 | spinner.start("Updating the project to use the blog's URL") 222 | 223 | return fs 224 | .readFile(withOutputPath(profile, './config.js'), 'utf8') 225 | .then(content => 226 | fs.writeFile( 227 | withOutputPath(profile, './config.js'), 228 | content.replace(/http:\/\/localhost:8000/g, publicURL), 229 | 'utf8' 230 | ) 231 | ) 232 | .then(() => 233 | exec(`git add . && git commit -m "update config to use blog's URL"`, { 234 | cwd: withOutputPath(profile), 235 | }) 236 | ) 237 | }) 238 | .then(() => { 239 | spinner.succeed("Updated the project to use the blog's URL") 240 | spinner.stop() 241 | 242 | // eslint-disable-next-line no-console 243 | console.log( 244 | 'Pushing the code for your blog to GitHub (you might be prompted for your GitHub identifiers)...\n' 245 | ) 246 | return new Promise((resolve, reject) => { 247 | const child = spawn('git', ['push', 'origin', 'master'], { 248 | cwd: withOutputPath(profile), 249 | stdio: 'inherit', 250 | }) 251 | 252 | child.on('error', () => reject()) 253 | child.on('close', () => resolve()) 254 | child.on('exit', () => resolve()) 255 | }) 256 | }) 257 | .then(() => { 258 | // eslint-disable-next-line no-console 259 | console.log(` 260 | ------------------------- 261 | 262 | Your blog is ready to go! 🙌 263 | 264 | We created a folder called "${profile.mediumUsername}-blog" with 265 | everything needed inside. 266 | 267 | Your blog posts are in the "content" sub-folder but the first thing 268 | you will want to edit is the "config.js" file. It contains 269 | a few values like your bio or your social media links that you should edit. 270 | 271 | For more information, you can check the guides on 272 | https://github.com/mathieudutour/medium-to-own-blog/master/tree/master/docs. 273 | 274 | Happy blogging! 275 | ... 276 | 277 | 278 | `) 279 | }) 280 | .then(() => process.exit(0)) 281 | .catch(err => { 282 | if (spinner) { 283 | spinner.fail() 284 | } 285 | // eslint-disable-next-line no-console 286 | console.log() 287 | // eslint-disable-next-line no-console 288 | console.error(err) 289 | process.exit(1) 290 | }) 291 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | ## Working on your blog 4 | 5 | You have different ways to update your blog. If you are familiar with a terminal and git, you might want to [work locally on your blog](./local-workflow.md). Otherwise you can enable the [online editor](./online-workflow.md) and use it. 6 | 7 | ## Using your own domain name 8 | 9 | Check out [this article](https://css-tricks.com/using-your-domain-with-a-netlify-hosted-site/) which explains how to use your own domain name with a Netlify hosted site. 10 | 11 | ## Analytics, tracking page views, etc. 12 | 13 | Once you have your site live you may start wanting to get an idea of how many visitors are coming to your site along with other metrics such as: 14 | 15 | - What articles are most popular? 16 | - Where do my visitors come from? 17 | - When do people visit my site? 18 | 19 | Out of the box, the site generated by `medium-to-own-blog` does not track readers. If you want to add analytics to your blog, you have different solutions: 20 | 21 | - use the recommend service: [GoatCount](https://www.goatcounter.com/). It is a privacy-aware, free, and open source analytics service. Simply sign up to GoatCounter and copy your code to your `config.js` file. 22 | - use any other services: [https://www.gatsbyjs.org/docs/adding-analytics/](https://www.gatsbyjs.org/docs/adding-analytics/). 23 | 24 | ## Comments, likes, and webmentions 25 | 26 | In the spirit of the decentralization of the web, you can support comments and likes on your blog by leveraging webmentions. 27 | 28 | ## Changing styles 29 | 30 | `medium-to-own-blog` allows you to change the default theme styling by updating the theme values. 31 | 32 | First, you must create a theme file and then you can override theme values. See all [theme values](../gatsby-theme/src/theme.js) 33 | 34 | ```js 35 | // src/gatsby-theme-medium-to-own-blog/theme.js 36 | 37 | import defaultTheme from 'gatsby-theme-medium-to-own-blog/src/theme' 38 | 39 | export default { 40 | ...defaultTheme, 41 | colors: { 42 | ...defaultTheme.colors, 43 | text: '#000', 44 | primary: '#6166DC', 45 | background: '#fff', 46 | }, 47 | } 48 | ``` 49 | 50 | ## Component Shadowing 51 | 52 | > This feature allows users to override a component in order to customize its rendering. 53 | > 54 | > Component Shadowing lets you replace the theme’s original file, `gatsby-theme-medium-to-own-blog/src/components/bio.js` for example, with your own to implement any changes you need. 55 | 56 | Any component or section is able to be replaced with your own custom component. 57 | 58 | This opens up a full customization of the blog to your designed needs. You can copy any component directly from `medium-to-own-blog` and alter it how you like, or you can create your own component to replace `medium-to-own-blog`'s entirely. 59 | 60 | Check out [the Gatsby documentation](https://www.gatsbyjs.org/docs/themes/shadowing/). 61 | -------------------------------------------------------------------------------- /docs/local-workflow.md: -------------------------------------------------------------------------------- 1 | # Working locally on your blog 2 | 3 | To work on your blog locally, run the following commands: 4 | 5 | ```bash 6 | npm install 7 | npm start 8 | ``` 9 | 10 | You can then visit [http://localhost:8000/](http://localhost:8000/) to view the blog. Any edit made to the content of the blog will automatically be reflected there. 11 | 12 | ## Creating an article 13 | 14 | 1. Create a new folder in the `content` folder. The name of the folder will be the url of the post. 15 | 2. Inside the newly created folder, create a `index.md` file. 16 | 3. Inside the file, you can specify some metadata for the post using the frontmatter syntax: 17 | 18 | ```md 19 | --- 20 | title: 'Title of the article' 21 | description: 'a short description that will show up in the front page of the blog and in the google description' 22 | date: 'date of the publication, you can leave empty until you publish it' 23 | categories: 24 | - tag 25 | - of 26 | - the 27 | - article 28 | published: `true` or `false` (if false, you can access the article via its url but it won't show up in the front page) 29 | --- 30 | ``` 31 | 32 | 4. All content after the `---` will be treated as the article body. You can edit the content in different ways: 33 | 1. Use any text editor to write markdown. 34 | 2. Use this medium-like editor [tool](https://ionicabizau.github.io/medium-editor-markdown/example/) to generate the markdown for you and then copy/paste the markdown into the file. 35 | 36 | _Alternatively, you can add this file using the editor on GitHub.com which also has a Preview tab._ 37 | 38 | ## Updating an article 39 | 40 | To update an article, update the content of `content/url-of-the-article/index.md` file. 41 | 42 | ## Deleting an article 43 | 44 | To delete an article, delete the `content/url-of-the-article` folder. 45 | 46 | ## Publishing the changes 47 | 48 | To publish the local changes, you need to use git. 49 | 50 | Open a terminal and run the following commands 51 | 52 | ```bash 53 | git add . 54 | git commit -m 'publish changes' 55 | git push 56 | ``` 57 | 58 | Netlify will automatically pick up the changes and deploy them. Wait a couple of minutes and the changes will be live! 59 | -------------------------------------------------------------------------------- /docs/online-workflow.md: -------------------------------------------------------------------------------- 1 | # Working on your blog online 2 | 3 | `medium-to-own-blog` provides a CMS to create/update/delete articles online, with a similar experience as Medium. 4 | 5 | There are a couple of steps to set it up. 6 | 7 | From your site dashboard on Netlify: 8 | 9 | 1. Go to Settings > Identity, and select Enable Identity service. 10 | 2. Under Registration preferences, select Invite only. 11 | 3. If you'd like to allow one-click login with services like Google and GitHub, check the boxes next to the services you'd like to use, under External providers. 12 | 4. Scroll down to Services > Git Gateway, and click Enable Git Gateway. 13 | 14 | You can now head to your site and go to the `/admin` page. Once logged in, you will be able to edit your blog. 15 | -------------------------------------------------------------------------------- /docs/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/medium-to-own-blog/c9e88263d592926c8fc0acda9ebecd940c43d3d7/docs/screencast.gif -------------------------------------------------------------------------------- /gatsby-template/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | .cache 4 | -------------------------------------------------------------------------------- /gatsby-template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:carbon 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | # Install app dependencies 7 | RUN npm -g install gatsby-cli 8 | 9 | COPY package*.json ./ 10 | 11 | RUN npm ci 12 | 13 | # Bundle app source 14 | COPY . . 15 | 16 | # Build static files 17 | RUN npm run build 18 | 19 | # serve on port 8080 20 | CMD ["gatsby", "serve", "--verbose", "--prefix-paths", "-p", "8080", "--host", "0.0.0.0"] 21 | -------------------------------------------------------------------------------- /gatsby-template/README.md: -------------------------------------------------------------------------------- 1 | # {{ authorName }}'s blog 2 | -------------------------------------------------------------------------------- /gatsby-template/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // the name of your website 3 | title: '{{ authorName }}', 4 | // the description of the website (eg. what shows on Google) 5 | description: "{{ authorName }}'s blog", 6 | // a short bio shown at the bottom of your blog posts 7 | // It should complete the sentence: Written by {{ authorName }} ... 8 | shortBio: '', 9 | // a longer bio showing on the landing page of the blog 10 | bio: `{{ bio }}`, 11 | author: '{{ authorName }}', 12 | githubUrl: '{{ githubURL }}', 13 | // replace this by the url where your website will be published 14 | siteUrl: 'http://localhost:8000', 15 | social: { 16 | // leave the social media you do not want to appear as empty strings 17 | twitter: '{{ twitterUsername }}', 18 | medium: '@{{ mediumUsername }}', 19 | facebook: '{{ facebookUsername }}', 20 | github: '', 21 | linkedin: '', 22 | instagram: '', 23 | }, 24 | // GoatCounter code to enable analytics. See https://github.com/mathieudutour/medium-to-own-blog/tree/master/docs#analytics-tracking-page-views-etc 25 | goatCounterCode: null, 26 | } 27 | -------------------------------------------------------------------------------- /gatsby-template/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | 3 | module.exports = { 4 | siteMetadata: config, 5 | plugins: [ 6 | { 7 | resolve: `gatsby-theme-medium-to-own-blog`, 8 | options: { 9 | config, 10 | webmentionsToken: process.env.WEBMENTIONS_TOKEN, 11 | }, 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /gatsby-template/gatsby-node.js: -------------------------------------------------------------------------------- 1 | exports.createSchemaCustomization = ({ actions }) => { 2 | const { createTypes } = actions 3 | const typeDefs = ` 4 | type Mdx implements Node { 5 | frontmatter: Frontmatter 6 | fields: MediumMdxFields 7 | } 8 | type Frontmatter { 9 | title: String 10 | description: String 11 | date: Date @dateformat 12 | published: Boolean 13 | canonical_link: String 14 | categories: [String] 15 | redirect_from: [String] 16 | redirect_to: String 17 | } 18 | type MediumMdxFields { 19 | slug: String 20 | published: Boolean 21 | } 22 | 23 | type WebMentionEntry implements Node { 24 | type: String 25 | author: WebMentionAuthor 26 | content: WebMentionContent 27 | url: String 28 | published: Date @dateformat 29 | wmReceived: Date @dateformat 30 | wmId: Int 31 | wmPrivate: Boolean 32 | wmTarget: String 33 | wmSource: String 34 | wmProperty: String 35 | likeOf: String 36 | mentionOf: String 37 | inReplyTo: String 38 | repostOf: String 39 | bookmarkOf: String 40 | rsvp: String 41 | video: [String] 42 | } 43 | type WebMentionAuthor { 44 | type: String 45 | name: String 46 | url: String 47 | photo: String 48 | } 49 | type WebMentionContent { 50 | text: String 51 | html: String 52 | } 53 | ` 54 | createTypes(typeDefs) 55 | } 56 | -------------------------------------------------------------------------------- /gatsby-template/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | command = "gatsby build" 4 | -------------------------------------------------------------------------------- /gatsby-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ mediumUsername }}-blog", 3 | "private": true, 4 | "description": "", 5 | "version": "0.1.0", 6 | "author": "{{ authorName }} <{{ authorEmail }}>", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+{{ githubURL }}.git" 10 | }, 11 | "dependencies": { 12 | "gatsby": "^2.13.13", 13 | "gatsby-theme-medium-to-own-blog": "^0.2.14", 14 | "react": "^16.8.6", 15 | "react-dom": "^16.8.6" 16 | }, 17 | "devDependencies": { 18 | "lighthousebot": "git+https://github.com/GoogleChromeLabs/lighthousebot.git", 19 | "wait-for-netlify-preview": "^1.2.1" 20 | }, 21 | "keywords": [ 22 | "gatsby", 23 | "medium", 24 | "blog" 25 | ], 26 | "license": "MIT", 27 | "scripts": { 28 | "build": "gatsby build", 29 | "dev": "gatsby develop", 30 | "start": "npm run dev", 31 | "serve": "gatsby serve", 32 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\"", 33 | "import-medium-article": "npx --package=medium-to-own-blog parse-medium-article" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gatsby-theme/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /gatsby-theme/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | // prism theme 2 | import './static/prism-theme.css' 3 | import { wrapRootElement as wrap } from './src/components/wrap-root-element' 4 | 5 | export const wrapRootElement = wrap 6 | -------------------------------------------------------------------------------- /gatsby-theme/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const withDefaults = require(`./utils/default-options`) 2 | const theme = require(`./src/theme`) 3 | 4 | module.exports = themeOptions => { 5 | const options = withDefaults(themeOptions) 6 | const siteUrl = (options.config.siteUrl || '').replace(/^https?:\/\//i, '') 7 | 8 | const plugins = [ 9 | 'gatsby-plugin-react-helmet', 10 | { 11 | resolve: 'gatsby-plugin-mdx', 12 | options: { 13 | extensions: ['.md', '.mdx'], 14 | gatsbyRemarkPlugins: [ 15 | { 16 | resolve: 'gatsby-remark-images', 17 | options: { 18 | maxWidth: 700, 19 | backgroundColor: 'transparent', 20 | showCaptions: true, 21 | }, 22 | }, 23 | 'gatsby-remark-copy-linked-files', 24 | 'gatsby-remark-embed-video', 25 | { 26 | resolve: 'gatsby-remark-responsive-iframe', 27 | options: { 28 | wrapperStyle: 'margin-bottom: 1.0725rem', 29 | }, 30 | }, 31 | 'gatsby-remark-autolink-headers', 32 | 'gatsby-remark-smartypants', 33 | { 34 | resolve: '@weknow/gatsby-remark-twitter', 35 | options: { 36 | align: 'center', 37 | }, 38 | }, 39 | 'gatsby-remark-external-links', 40 | ], 41 | plugins: [ 42 | { 43 | resolve: 'gatsby-remark-images', 44 | options: { 45 | maxWidth: 700, 46 | backgroundColor: 'transparent', 47 | showCaptions: true, 48 | }, 49 | }, 50 | ], 51 | }, 52 | }, 53 | { 54 | resolve: 'gatsby-source-filesystem', 55 | options: { 56 | name: 'images', 57 | path: options.imagesPath, 58 | }, 59 | }, 60 | { 61 | resolve: 'gatsby-source-filesystem', 62 | options: { 63 | name: 'blog', 64 | path: options.contentPath, 65 | }, 66 | }, 67 | 'gatsby-plugin-netlify', 68 | 'gatsby-transformer-sharp', 69 | 'gatsby-plugin-sharp', 70 | { 71 | resolve: 'gatsby-plugin-manifest', 72 | options: { 73 | name: options.config.title, 74 | short_name: options.config.title, 75 | start_url: '/', 76 | background_color: theme.colors.background, 77 | theme_color: theme.colors.background, 78 | display: 'minimal-ui', 79 | icon: `${options.imagesPath}/icon.png`, // This path is relative to the root of the site. 80 | }, 81 | }, 82 | { 83 | resolve: `gatsby-plugin-feed`, 84 | options: { 85 | feeds: [ 86 | { 87 | title: options.config.title, 88 | serialize: ({ query: { site, allMdx } }) => { 89 | return allMdx.edges.map(edge => { 90 | return { 91 | ...edge.node.frontmatter, 92 | url: site.siteMetadata.siteUrl + edge.node.fields.slug, 93 | guid: site.siteMetadata.siteUrl + edge.node.fields.slug, 94 | custom_elements: [{ 'content:encoded': edge.node.html }], 95 | } 96 | }) 97 | }, 98 | query: ` 99 | { 100 | allMdx( 101 | filter: { fields: { published: { eq: true } } } 102 | limit: 1000, 103 | sort: { 104 | order: DESC, 105 | fields: [frontmatter___date] 106 | } 107 | ) { 108 | edges { 109 | node { 110 | frontmatter { 111 | title 112 | description 113 | date 114 | } 115 | fields { 116 | slug 117 | } 118 | html 119 | } 120 | } 121 | } 122 | } 123 | `, 124 | output: `rss.xml`, 125 | }, 126 | ], 127 | }, 128 | }, 129 | { 130 | resolve: `gatsby-plugin-webmention`, 131 | options: { 132 | username: siteUrl, 133 | identity: { 134 | github: (options.config.social || {}).github, 135 | twitter: ((options.config.social || {}).twitter || '').replace( 136 | /^@/, 137 | '' 138 | ), 139 | }, 140 | mentions: true, 141 | pingbacks: true, 142 | domain: siteUrl, 143 | token: options.webmentionsToken, 144 | }, 145 | }, 146 | `gatsby-plugin-netlify-cms`, 147 | ] 148 | 149 | if (options.config.goatCounterCode) { 150 | plugins.push({ 151 | resolve: `gatsby-plugin-goatcounter`, 152 | optiona: { 153 | code: options.config.goatCounterCode, 154 | urlCleanup: true, 155 | }, 156 | }) 157 | } 158 | 159 | return { plugins } 160 | } 161 | -------------------------------------------------------------------------------- /gatsby-theme/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { createFilePath } = require('gatsby-source-filesystem') 3 | 4 | exports.onCreateNode = ({ node, actions, getNode }, themeOptions) => { 5 | const { createNodeField } = actions 6 | 7 | if (node.internal.type === 'Mdx') { 8 | let value = `${ 9 | themeOptions.pathPrefix ? themeOptions.pathPrefix : '' 10 | }${createFilePath({ node, getNode })}` 11 | if (!value.startsWith('/')) { 12 | value = `/${value}` 13 | } 14 | createNodeField({ 15 | name: 'slug', 16 | node, 17 | value, 18 | }) 19 | 20 | createNodeField({ 21 | name: 'published', 22 | node, 23 | value: node.frontmatter.published, 24 | }) 25 | } 26 | } 27 | 28 | exports.createPages = ( 29 | { graphql, actions, reporter, pathPrefix }, 30 | themeOptions 31 | ) => { 32 | const { createPage, createRedirect } = actions 33 | return graphql( 34 | ` 35 | { 36 | site { 37 | siteMetadata { 38 | siteUrl 39 | } 40 | } 41 | allMdx(sort: { fields: [frontmatter___date], order: DESC }) { 42 | edges { 43 | node { 44 | id 45 | fields { 46 | slug 47 | published 48 | } 49 | frontmatter { 50 | redirect_from 51 | redirect_to 52 | title 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ` 59 | ).then(result => { 60 | if (result.errors && result.errors.length) { 61 | if (result.errors.length === 1) { 62 | throw new Error(result.errors[0]) 63 | } 64 | 65 | result.errors.forEach(error => { 66 | reporter.error('Error while querying the mdx', error) 67 | }) 68 | 69 | throw new Error('See errors above') 70 | } 71 | 72 | const posts = result.data.allMdx.edges 73 | // We'll call `createPage` for each result 74 | posts.forEach(({ node }, index) => { 75 | let previous = index === posts.length - 1 ? null : posts[index + 1].node 76 | let next = index === 0 ? null : posts[index - 1].node 77 | 78 | if (previous && !previous.fields.published) { 79 | previous = null 80 | } 81 | if (next && !next.fields.published) { 82 | next = null 83 | } 84 | 85 | const pagePath = `${pathPrefix}${node.fields.slug}` 86 | const permalink = `${result.data.site.siteMetadata.siteUrl}${node.fields.slug}` 87 | 88 | createPage({ 89 | path: pagePath, 90 | component: path.resolve( 91 | path.join(__dirname, `./src/templates/blog-post.js`) 92 | ), 93 | context: { id: node.id, previous, next, permalink, themeOptions }, 94 | }) 95 | 96 | if ( 97 | node.frontmatter && 98 | node.frontmatter.redirect_from && 99 | Array.isArray(node.frontmatter.redirect_from) && 100 | node.frontmatter.redirect_from.length 101 | ) { 102 | node.frontmatter.redirect_from.forEach(fromPath => { 103 | createRedirect({ 104 | fromPath, 105 | toPath: pagePath, 106 | isPermanent: true, 107 | }) 108 | }) 109 | } 110 | 111 | if ( 112 | node.frontmatter && 113 | node.frontmatter.redirect_to && 114 | node.frontmatter.redirect_to.length 115 | ) { 116 | createRedirect({ 117 | fromPath: pagePath, 118 | toPath: node.frontmatter.redirect_to, 119 | isPermanent: true, 120 | redirectInBrowser: true, 121 | }) 122 | } 123 | }) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /gatsby-theme/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import { wrapRootElement as wrap } from './src/components/wrap-root-element' 2 | 3 | export const wrapRootElement = wrap 4 | -------------------------------------------------------------------------------- /gatsby-theme/index.js: -------------------------------------------------------------------------------- 1 | // no-op 2 | -------------------------------------------------------------------------------- /gatsby-theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-theme-medium-to-own-blog", 3 | "description": "Gatsby theme for Medium-to-own-blog", 4 | "version": "0.2.15", 5 | "author": "Mathieu Dutour ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mathieudutour/medium-to-own-blog.git", 9 | "directory": "gatsby-theme" 10 | }, 11 | "peerDependencies": { 12 | "gatsby": "^2.13.13", 13 | "react": "^16.8.6", 14 | "react-dom": "^16.8.6" 15 | }, 16 | "dependencies": { 17 | "@emotion/core": "^10.0.28", 18 | "@emotion/styled": "^10.0.27", 19 | "@mdx-js/mdx": "^1.0.16", 20 | "@mdx-js/react": "^1.0.16", 21 | "@weknow/gatsby-remark-twitter": "^0.2.1", 22 | "gatsby-image": "^2.1.1", 23 | "gatsby-plugin-emotion": "^4.1.24", 24 | "gatsby-plugin-feed": "^2.2.1", 25 | "gatsby-plugin-goatcounter": "^0.3.1", 26 | "gatsby-plugin-manifest": "^2.1.1", 27 | "gatsby-plugin-mdx": "^1.0.19", 28 | "gatsby-plugin-netlify": "^2.0.17", 29 | "gatsby-plugin-netlify-cms": "^4.1.41", 30 | "gatsby-plugin-offline": "^3.0.40", 31 | "gatsby-plugin-react-helmet": "^3.0.12", 32 | "gatsby-plugin-sharp": "^2.0.36", 33 | "gatsby-plugin-webmention": "^0.2.0", 34 | "gatsby-remark-autolink-headers": "^2.0.16", 35 | "gatsby-remark-copy-linked-files": "^2.0.12", 36 | "gatsby-remark-embed-video": "^2.0.1", 37 | "gatsby-remark-external-links": "^0.0.4", 38 | "gatsby-remark-images": "^3.0.11", 39 | "gatsby-remark-responsive-iframe": "^2.1.1", 40 | "gatsby-remark-smartypants": "^2.0.9", 41 | "gatsby-source-filesystem": "^2.0.33", 42 | "gatsby-transformer-sharp": "^2.1.19", 43 | "netlify-cms-app": "^2.11.29", 44 | "prism-react-renderer": "^1.0.2", 45 | "react-helmet": "^5.2.1" 46 | }, 47 | "devDependencies": {}, 48 | "keywords": [ 49 | "gatsby", 50 | "medium", 51 | "blog" 52 | ], 53 | "license": "MIT", 54 | "scripts": { 55 | "build": "gatsby build", 56 | "dev": "gatsby develop", 57 | "start": "npm run dev", 58 | "serve": "gatsby serve" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gatsby-theme/src/components/bio.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStaticQuery, graphql } from 'gatsby' 3 | import Image from 'gatsby-image' 4 | import Styled from '@emotion/styled' 5 | 6 | const Wrapper = Styled.div` 7 | display: flex; 8 | margin-bottom: 4.375rem; 9 | ` 10 | 11 | const StyledImage = Styled(Image)` 12 | margin-right: 0.875rem; 13 | margin-bottom: 0; 14 | min-width: 50px; 15 | border-radius: 100%; 16 | ` 17 | 18 | function Bio() { 19 | const { site, avatar } = useStaticQuery( 20 | graphql` 21 | query BioQuery { 22 | avatar: file(absolutePath: { regex: "/avatar.png/" }) { 23 | childImageSharp { 24 | fixed(width: 50, height: 50, quality: 80) { 25 | base64 26 | width 27 | height 28 | src 29 | srcSet 30 | } 31 | } 32 | } 33 | site { 34 | siteMetadata { 35 | author 36 | shortBio 37 | social { 38 | twitter 39 | } 40 | } 41 | } 42 | } 43 | ` 44 | ) 45 | 46 | const { author, social, shortBio } = site.siteMetadata 47 | 48 | return ( 49 | 50 | 57 |

58 | Written by {author} 59 | {shortBio ? ` ${shortBio}` : ''}.{` `} 60 | {social.twitter ? ( 61 | 62 | You should follow them on Twitter. 63 | 64 | ) : null} 65 |

66 |
67 | ) 68 | } 69 | 70 | export default Bio 71 | -------------------------------------------------------------------------------- /gatsby-theme/src/components/code-highlighting.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Highlight, { defaultProps } from 'prism-react-renderer' 3 | import theme from '../theme' 4 | 5 | if (!theme.highlighting.plain) { 6 | theme.highlighting.plain = {} 7 | } 8 | if (!theme.highlighting.plain.color) { 9 | theme.highlighting.plain.color = theme.colors.primary 10 | } 11 | if (!theme.highlighting.plain.backgroundColor) { 12 | theme.highlighting.plain.backgroundColor = theme.colors.codeBackground 13 | } 14 | 15 | const preToCodeBlock = preProps => { 16 | if ( 17 | // children is MDXTag 18 | preProps.children && 19 | // MDXTag props 20 | preProps.children.props && 21 | // if MDXTag is going to render a 22 | preProps.children.props.name === 'code' 23 | ) { 24 | // we have a
 situation
 25 |     const {
 26 |       children: codeString,
 27 |       props: { className, ...props },
 28 |     } = preProps.children.props
 29 | 
 30 |     return {
 31 |       codeString: codeString.trim(),
 32 |       language: className && className.split('-')[1],
 33 |       ...props,
 34 |     }
 35 |   }
 36 |   if (preProps.children && typeof preProps.children === 'string') {
 37 |     const { children, className, ...props } = preProps
 38 |     return {
 39 |       codeString: children.trim(),
 40 |       language: className && className.split('-')[1],
 41 |       ...props,
 42 |     }
 43 |   }
 44 |   return undefined
 45 | }
 46 | 
 47 | const InlineCode = ({ codeString, language /* , ...props */ }) => {
 48 |   return (
 49 |     
 55 |       {({ className, style, tokens, getLineProps, getTokenProps }) => (
 56 |         
 57 |           {tokens.map((line, i) => (
 58 |             
 59 |               {line.map((token, key) => (
 60 |                 
 61 |               ))}
 62 |             
 63 |           ))}
 64 |         
 65 |       )}
 66 |     
 67 |   )
 68 | }
 69 | 
 70 | const Code = ({ codeString, language /* , ...props */ }) => {
 71 |   return (
 72 |     
 78 |       {({ className, style, tokens, getLineProps, getTokenProps }) => (
 79 |         
80 | {tokens.map((line, i) => ( 81 |
82 | {line.map((token, key) => ( 83 | 84 | ))} 85 |
86 | ))} 87 |
88 | )} 89 |
90 | ) 91 | } 92 | 93 | export const PrismjsReplacement = preProps => { 94 | const props = preToCodeBlock(preProps) 95 | // if there's a codeString and some props, we passed the test 96 | if (props) { 97 | return 98 | } 99 | // it's possible to have a pre without a code in it 100 | return
101 | }
102 | 
103 | export const PrismjsReplacementInline = preProps => {
104 |   const props = preToCodeBlock(preProps)
105 |   // if there's a codeString and some props, we passed the test
106 |   if (props) {
107 |     return 
108 |   }
109 |   // it's possible to have a pre without a code in it
110 |   return 
111 | }
112 | 


--------------------------------------------------------------------------------
/gatsby-theme/src/components/embed.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable jsx-a11y/iframe-has-title */
 2 | import React, { useRef, useEffect } from 'react'
 3 | import Styled from '@emotion/styled'
 4 | 
 5 | const Container = Styled.div`
 6 |   position: relative;
 7 | `
 8 | 
 9 | const ImageForRatio = Styled.img`
10 |   display: block;
11 |   height: auto;
12 |   width: 100%;
13 | `
14 | 
15 | const IframeWithRatio = Styled.iframe`
16 |   height: 100%;
17 |   left: 0;
18 |   position: absolute;
19 |   top: 0;
20 |   width: 100%;
21 | `
22 | 
23 | function Embed({ aspectRatio, src, caption }) {
24 |   const iframeRef = useRef(null)
25 | 
26 |   useEffect(() => {
27 |     const iframe = iframeRef.current
28 |     if (!iframe) {
29 |       return
30 |     }
31 | 
32 |     let doc = iframe.document
33 |     if (iframe.contentDocument) doc = iframe.contentDocument
34 |     else if (iframe.contentWindow) doc = iframe.contentWindow.document
35 | 
36 |     const gistScript = ``
37 |     const styles = ''
38 |     const elementId = src.replace('https://gist.github.com/', '')
39 |     const resizeScript = `onload="parent.document.getElementById('${elementId}').style.height=document.body.scrollHeight + 'px'"`
40 |     const iframeHtml = `${styles}${gistScript}`
41 | 
42 |     doc.open()
43 |     doc.writeln(iframeHtml)
44 |     doc.close()
45 |   }, [iframeRef, src])
46 | 
47 |   if (src && src.match(/^https:\/\/gist.github.com/)) {
48 |     return (
49 |       
50 |