Sådan opbygges din kodningsblog fra bunden ved hjælp af Gatsby og MDX

Jeg har været Gatsby-bruger siden omkring version 0 tilbage i maj 2017.

Dengang brugte jeg en skabelon kaldet Lumen. Det var lige hvad jeg havde brug for på det tidspunkt. Siden da er jeg gået fra at bruge en skabelon til at oprette min blog.

I årenes løb har jeg bygget min egen Progressive Disclosure of Complexity med Gatsby til hvor jeg er nu.

Hvad betyder det?

Det betyder, at selvom der er en utrolig mængde Gatsby-startere og temaer derude for at komme i gang på få minutter, vil dette indlæg fokusere på, hvad du skal gøre for at opbygge din egen blog. Startende med den mest basale "Hello World!" til implementering af din kode til produktion.

Hvad du skal bygge

Du skal opbygge en udviklerblog med MDX-understøttelse (for nogle React-komponenter i Markdown-godhed), så du vil være i stand til at tilføje dine egne React-komponenter til dine Markdown-indlæg.

Der vil være:

  • Tilføjelse af et layout
  • Grundlæggende styling med stylede komponenter
  • Kodeblokke med syntaksfremhævning
  • Kopier kodestykke til udklipsholder
  • Forsidebilleder til stillingerne
  • Konfiguration af en SEO-komponent
  • Implementering af det til Netlify

Hvem er denne vejledning til?

Folk, der muligvis har brugt Gatsby før som en skabelon, og som nu ønsker at blive mere involveret i, hvordan man foretager ændringer.

Hvis du vil have fremhævning af kodesyntaks.

Hvis du vil bruge stylede komponenter i en app.

Jeg vil virkelig undgå dette!

Hver kodningsvejledning, der nogensinde er skrevet pic.twitter.com/Y641Cd9DMr

- Quincy Larson (@ossia) 15. april 2015

Krav

Du får brug for en grundlæggende webudviklingsopsætning: node, terminal (bash, zsh eller fish) og en teksteditor.

Jeg kan godt lide at bruge codesandbox.io til denne slags guider for at reducere adgangsbarrieren, men i dette tilfælde har jeg fundet ud af, at der er nogle begrænsninger ved at starte fra bunden på codesandbox.io, hvilket ikke gør dette muligt.

Jeg har lavet en guide til at blive klar til webudvikling med Windows Web-Dev Bootstrap og også dækket den samme proces i Ubuntu.

Okay? Tid til at komme i gang!

Hej Verden

For at starte dette med Gatsby 'hej verden' skal du initialisere projektet med:

npm init -y git init

Jeg foreslår, at du forpligter denne kode til et git-arkiv, så du skal starte med en .gitignorefil.

touch .gitignore echo "# Project dependencies .cache node_modules # Build directory public # Other .DS_Store yarn-error.log" > .gitignore

Ok nu er det et godt tidspunkt at lave en, git initog hvis du bruger VSCode, kan du se ændringerne afspejlet i sidepanelet.

grundlæggende hej verden

Ok en Gatsby hej verden, kom i gang med det absolutte minimum! Installer følgende:

yarn add gatsby react react-dom

Du bliver nødt til at oprette et sidekatalog og tilføje en indeksfil. Du kan gøre det i terminalen ved at skrive følgende:

# -p is to create parent directories too if needed mkdir -p src/pages touch src/pages/index.js

Nu kan du begynde hello word-besværgelsen! index.jsIndtast følgende i det nyoprettede :

import React from 'react'; export default () => { return 

Hello World!

; };

Nu skal du tilføje Gatsby-udviklingsskriptet til package.jsonfilen, angive -phvilken port du vil køre projektet på og -oåbner en ny fane i din standardbrowser, så i dette tilfælde localhost:9988:

"dev": "gatsby develop -p 9988 -o"

Og nu er det tid til at køre koden! Fra terminalen skriver du npm-scriptkommandoen, du lige har oprettet:

yarn dev
Bemærk, at jeg bruger garn til installation af alle mine afhængigheder og kørende scripts. Hvis du foretrækker, kan du bruge npm, skal du bare huske på, at indholdet her bruger garn, så skift kommandoer ud, hvor det er nødvendigt.

Og med det er "Hello World" besværgelsen komplet?!

Tilføj indhold

Nu har du basen til din blog, du vil ønske at tilføje noget indhold. Først og fremmest skal vi fjerne konventionen. For denne vejledning vil datoformatet være logisk - den mest logiske måde for et datoformat er ÅÅÅÅMMDD , kæmp mig!

Så du vil strukturere dit indlægs indhold om år. I hver af dem vil du have en anden mappe, der relaterer til indlægget med det (korrekte) datoformat til begyndelsen af ​​filen efterfulgt af titlen på indlægget.

Du kan gå nærmere ind på dette, hvis du vil, ved at adskille måneder og dage. Afhængigt af antallet af indlæg, du har startet, kan dette være en god tilgang. I dette tilfælde og i de medfølgende eksempler vil den detaljerede konvention blive brugt.

# create multiple directories using curly braces mkdir -p posts/2019/{2019-06-01-hello-world,2019-06-10-second-post,2019-06-20-third-post} touch posts/2019/2019-06-01-hello-world/index.mdx touch posts/2019/2019-06-10-second-post/index.mdx touch posts/2019/2019-06-20-third-post/index.mdx

Ok, sådan konfigureres dine indlæg. Nu skal du tilføje noget indhold til dem, så hver fil, du har herinde, skal have forsiden. Frontmatter er en måde at tildele egenskaber til indholdet, i dette tilfælde en title, offentliggjort dateog et publishedflag ( trueeller false).

--- title: Hello World - from mdx! date: 2019-06-01 published: true --- # h1 Heading My first post!! ## h2 Heading ### h3 Heading
--- title: Second Post! date: 2019-06-10 published: true --- This is my second post! #### h4 Heading ##### h5 Heading ###### h6 Heading
--- title: Third Post! date: 2019-06-20 published: true --- This is my third post! > with a block quote!

Gatsby konfigurations-API

Dernæst skal du konfigurere Gatsby, så den kan læse dit super fantastiske indhold, du lige har oprettet. Først skal du oprette en gatsby-config.jsfil. Opret filen i terminalen:

touch gatsby-config.js

Plugins

Og nu kan du tilføje de plugins, som Gatsby skal bruge til sourcing og visning af de filer, du lige har oprettet.

Gatsby kildefilsystem

Gatsby-source-filsystemet samler filerne på det lokale filsystem til brug i Gatsby, når de er konfigureret.

Gatsby-plugin MDX

The gatsby-plugin-mdx is what will be allowing us to write JSX in our Markdown documents and the heart of how the content is displayed in the blog.

Now is a good time to also add in dependent packages for the Gatsby plugin MDX which are @mdx-js/mdx and @mdx-js/react.

In the terminal install the dependencies:

yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react gatsby-source-filesystem

Now it's time to configure gatsby-config.js:

module.exports = { siteMetadata: { title: `The Localhost Blog`, description: `This is my coding blog where I write about my coding journey.`, }, plugins: [ { resolve: `gatsby-plugin-mdx`, options: { extensions: [`.mdx`, `.md`], }, }, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/posts`, name: `posts`, }, }, ], };

Query data from GraphQL

Now you can see what the gatsby-source-filesystem and gatsby-plugin-mdx have done for us. You can now go to the Gatsby GraphQL GraphiQL explorer and check out the data:

{ allMdx { nodes { frontmatter { title date } } } }

Site Metadata

When you want to reuse common pieces of data across the site (for example, your site title), you can store that data in siteMetadata. You touched on this when defining the gatsby-config.js, and now you’re going to separate this out from the module.exports. Why? It will be nicer to reason about once the config is filled with plugins.

At the top of gatsby-config.js add a new object variable for the site metadata:

const siteMetadata = { title: `The Localhost Blog`, description: `This is my coding blog where I write about my coding journey.`, };

Now query the Site Metadata with GraphQL.

{ site { siteMetadata { title description } } }

Site metadata hook

Ok, so, that’s cool n’ all but how am I meant to use it? We'll do some of the code stuff and make a React hook so you can get your site data in any component you need it.

Create a folder to keep all your hooks in and create a file for our hook. In the terminal do:

mkdir src/hooks touch src/hooks/useSiteMetadata.js

Ok, and in your newly created file were going to use the Gatsby useStaticQuery hook to make your own hook:

import { graphql, useStaticQuery } from 'gatsby'; export const useSiteMetadata = () => { const { site } = useStaticQuery( graphql` query SITE_METADATA_QUERY { site { siteMetadata { title description } } } ` ); return site.siteMetadata; };

Now you can use this hook anywhere in your site, so do that now in src/pages/index.js:

import React from 'react'; import { useSiteMetadata } from '../hooks/useSiteMetadata'; export default () => { const { title, description } = useSiteMetadata(); return (  

{title}

{description}

); };

Styling

You’re going to use styled-components for styling. Styled-components (for me) help with scoping styles in your components. Time to go over the basics now.

install styled-components

yarn add gatsby-plugin-styled-components styled-components babel-plugin-styled-components

So, what was all that I just installed?

The babel plugin is for automatic naming of components to help with debugging.

The Gatsby plugin is for built-in server-side rendering support.

Configure

Ok, with that detailed explanation out of the way, configure them in gatsby-config.js:

const siteMetadata = { title: `The Localhost Blog`, description: `This is my coding blog where I write about my coding journey.`, }; module.exports = { siteMetadata: siteMetadata, plugins: [ `gatsby-plugin-styled-components`, { resolve: `gatsby-plugin-mdx`, options: { extensions: [`.mdx`, `.md`], }, }, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/posts`, name: `posts` }, }, ], };

Time to go over a styled component. In  index.js you’re going to import styled from 'styled-components' and create a StyledH1 variable.

So, you’re using the variable to wrap your {title} that you’re destructuring from the useSiteMetadata hook you made previously.

For this example make it the now iconic Gatsby rebeccapurple.

import React from 'react'; import styled from 'styled-components'; import { useSiteMetadata } from '../hooks/useSiteMetadata'; const StyledH1 = styled.h1` color: rebeccapurple; `; export default () => { const { title, description } = useSiteMetadata(); return (  {title} 

{description}

); };

That is styled-components on a very basic level. Basically create the styling you want for your page elements you’re creating in the JSX.

Layout

Gatsby doesn’t apply any layouts by default but instead uses the way you can compose React components for the layout. This means it’s up to you how you want to layout what you're building with Gatsby.

In this guide we're going to initially create a basic layout component that you’ll add to as you go along. For more detail on layout components take a look at the Gatsby layout components page.

Now you’re going to refactor the home page (src/pages/index.js) a little and make some components for your blog layout and header. In the terminal create a components directory and a Header and Layout component:

mkdir src/components touch src/components/Header.js src/components/Layout.js

Now to move the title and description from src/pages/index.js to the newly created src/components/Header.js component, destructuring props for the siteTitle and siteDescription, you’ll pass these from the Layout component to here. You’re going to add Gatsby Link to this so users can click on the header to go back to the home page.

import { Link } from 'gatsby'; import React from 'react'; export const Header = ({ siteTitle, siteDescription }) => (  

{siteTitle}

{siteDescription}

);

Now to the Layout component: this is going to be a basic wrapper component for now. You’re going to use your site metadata hook for the title and description and pass them to the header component and return the children of the wrapper (Layout).

import React from 'react'; import { useSiteMetadata } from '../hooks/useSiteMetadata'; import { Header } from './Header'; export const Layout = ({ children }) => { const { title, description } = useSiteMetadata(); return (   {children}  ); };

Now to add the slightest of styles for some alignment for src/components/Layout.js, create an AppStyles styled component and make it the main wrapper of your Layout.

import React from 'react'; import styled from 'styled-components'; import { useSiteMetadata } from '../hooks/useSiteMetadata'; import { Header } from './Header'; const AppStyles = styled.main` width: 800px; margin: 0 auto; `; export const Layout = ({ children }) => { const { title, description } = useSiteMetadata(); return (   {children}  ); };

Ok, now refactor your homepage (src/pages/index.js) with Layout.

import React from 'react'; import { Layout } from '../components/Layout'; export default () => { return (    ); };

Index page posts query

Now you can take a look at getting some of the posts you’ve created added to the index page of your blog. You’re going to do that by creating a GraphQL query to list out the posts by title, order by date, and add an excerpt of the post.

The query will look something like this:

{ allMdx { nodes { id excerpt(pruneLength: 250) frontmatter { title date } } } }

If you put that into the GraphiQL GUI, though, you’ll notice that the posts aren’t in any given order. So now add a sort to this - and you’ll also add in a filter for posts that are marked as published or not.

{ allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { id excerpt(pruneLength: 250) frontmatter { title date } } } }

On the homepage (src/pages/index.js) you’re going to use the query we just put together to get a list of published posts in date order; add the following to the index.js file:

import { graphql } from 'gatsby'; import React from 'react'; import { Layout } from '../components/Layout'; export default ({ data }) => { return (   {data.allMdx.nodes.map(({ excerpt, frontmatter }) => (  

{frontmatter.title}

{frontmatter.date}

{excerpt}

))} ); }; export const query = graphql` query SITE_INDEX_QUERY { allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { id excerpt(pruneLength: 250) frontmatter { title date } } } } `;

Woah! WTF was all that yo!?

Ok, you’re looping through the data passed into the component via the GraphQL query. Gatsby graphql runs the query (SITE_INDEX_QUERY) at runtime and gives us the results as props to your component via the data prop.

Slugs and Paths

Gatsby source filesystem will help with the creation of slugs (URL paths for the posts you’re creating). In Gatsby node you’re going to create the slugs for your posts.

First up you’re going to need to create a gatsby-node.js file:

touch gatsby-node.js

This will create the file path (URL) for each of the blog posts.

You’re going to be using the Gatsby Node API onCreateNode and destructuring out node, actions and getNode for use in creating the file locations and associated value.

const { createFilePath } = require(`gatsby-source-filesystem`); exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions; if (node.internal.type === `Mdx`) { const value = createFilePath({ node, getNode }); createNodeField({ name: `slug`, node, value, }); } };

Now to help visualise some of the data being passed into the components you’re going to use Dump.js for debugging the data. Thanks to Wes Bos for the super handy Dump.js component.

To get the component set up, create a Dump.js file in your src\components folder and copypasta the code from the linked GitHub page.

touch /src/components/Dump.js
import React from 'react'; const Dump = props => ( {Object.entries(props).map(([key, val]) => ( 
 {key} ?  {JSON.stringify(val, '', ' ')} 
))} ); export default Dump;

Now you can use the Dump component anywhere in your project. To demonstrate, use it with the index page data to see the output.

So in the src/pages/index.js you’re going to import the Dump component and pass in the data prop and see what the output looks like.

import { graphql } from 'gatsby'; import React from 'react'; import Dump from '../components/Dump'; import { Layout } from '../components/Layout'; export default ({ data }) => { return (    {data.allMdx.nodes.map(({ excerpt, frontmatter }) => (  

{frontmatter.title}

{frontmatter.date}

{excerpt}

))} ); }; export const query = graphql` query SITE_INDEX_QUERY { allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { id excerpt(pruneLength: 250) frontmatter { title date } } } } `;

Link Paths

Now you’ve created the paths you can link to them with Gatsby Link. First you’ll need to add the slug to your SITE_INDEX_QUERY Then you can add gatsby Link to src/pages/index.js.

You’re also going to create some styled-components for wrapping the list of posts and each individual post as well.

import { graphql, Link } from 'gatsby'; import React from 'react'; import styled from 'styled-components'; import { Layout } from '../components/Layout'; const IndexWrapper = styled.main``; const PostWrapper = styled.div``; export default ({ data }) => { return (   {data.allMdx.nodes.map( ({ id, excerpt, frontmatter, fields }) => (   

{frontmatter.title}

{frontmatter.date}

{excerpt}

) )} ); }; export const query = graphql` query SITE_INDEX_QUERY { allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { id excerpt(pruneLength: 250) frontmatter { title date } fields { slug } } } } `;

Adding a Blog Post Template

Now you have the links pointing to the blog posts you currently have no file associated with the path. This means that clicking a link will give you a 404 and the built-in gatsby 404 will list all the pages available in the project, currently only the / index/homepage.

So, for each one of your blog posts you’re going to use a template that will contain the information you need to make up your blog post. To start, create a templates directory and template file for that with:

mkdir -p src/templates touch src/templates/blogPostTemplate.js

For now you’re going to scaffold out a basic template (you’ll be adding data to this shortly):

import React from 'react'; export default () => { return (  

post here

); };

To populate the template you’ll need to use Gatsby node to create your pages.

Gatsby Node has many internal APIs available to us. For this example you’re going to be using the createPages API.

More info on Gatsby createPages API can be found on the Gatsby docs, details here: //www.gatsbyjs.org/docs/node-apis/#createPages

In your gatsby-node.js file you’re going to add in the following in addition to the onCreateNode export you did earlier.

const { createFilePath } = require(`gatsby-source-filesystem`); const path = require(`path`); exports.createPages = ({ actions, graphql }) => { const { createPage } = actions; const blogPostTemplate = path.resolve( 'src/templates/blogPostTemplate.js' ); return graphql(` { allMdx { nodes { fields { slug } frontmatter { title } } } } `).then(result => { if (result.errors) { throw result.errors; } const posts = result.data.allMdx.nodes; // create page for each mdx file posts.forEach(post => { createPage({ path: post.fields.slug, component: blogPostTemplate, context: { slug: post.fields.slug, }, }); }); }); }; exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions; if (node.internal.type === `Mdx`) { const value = createFilePath({ node, getNode }); createNodeField({ name: `slug`, node, value, }); } };

The part that you need to pay particular attention to right now is the .forEach loop where you’re using the createPage function we destructured from the actions object.

This is where you pass the data needed by blogPostTemplate you defined earlier. You’re going to be adding more to the context for post navigation soon.

// create page for each mdx node posts.forEach(post => { createPage({ path: post.fields.slug, component: blogPostTemplate, context: { slug: post.fields.slug, }, }); });

Build out Blog Post Template

Now you’re going to take the context information passed to the blogPostTemplate.js to make the blog post page.

This is similar to the index.js homepage, whereas there’s GraphQL data used to create the page. But in this instance the template uses a variable (also known as a parameter or an identifier) so you can query data specific to that given variable.

Now quickly dig into that with a demo. In the GraphiQL GUI, create a named query and define the variable you’re going to pass in:

query PostBySlug($slug: String!) { mdx(fields: { slug: { eq: $slug } }) { frontmatter { title date(formatString: "YYYY MMMM Do") } } }

Here you’re defining the variable as slug with the $ denoting that it’s a variable. You also need to define the variable type as (in this case) String!. The exclamation after the type means that it has to be a string being passed into the query.

Using mdx you’re going to filter on fields where the slug matches the variable being passed into the query.

Running the query now will show an error as there’s no variable being fed into the query. If you look to the bottom of the query pane you should notice QUERY VARIABLES. Click on that to bring up the variables pane.

This is where you can add in one of the post paths you created earlier. If you have your dev server up and running, go to one of the posts and take the path and paste it into the quotes "" and try running the query again.

{ "slug": "/2019/2019-06-20-third-post/" }

Time to use that data to make the post. You’re going to add body to the query and have that at the bottom of your page file.

Right now you’re going to create a simple react component that will display the data you have queried.

Destructuring the frontmatter and body from the GraphQL query, you’ll get the Title and the Data from the frontmatter object and wrap the body in the MDXRenderer.

import { graphql } from 'gatsby'; import { MDXRenderer } from 'gatsby-plugin-mdx'; import React from 'react'; import { Layout } from '../components/Layout'; export default ({ data }) => { const { frontmatter, body } = data.mdx; return (  

{frontmatter.title}

{frontmatter.date}

{body} ); }; export const query = graphql` query PostsBySlug($slug: String!) { mdx(fields: { slug: { eq: $slug } }) { body frontmatter { title date(formatString: "YYYY MMMM Do") } } } `;

If you haven’t done so already now would be a good time to restart your dev server.

Now you can click on one of the post links and see your blog post template in all it’s basic glory!

Previous and Next navigation

Coolio! Now you have your basic blog where you can list available posts and click a link to see the full post in a predefined template. Once you’re in a post you have to navigate back to the home page to pick out a new post to read. In this section you’re going to work on adding in some previous and next navigation.

Remember the .forEach snippet you looked at earlier? That’s where you’re going to pass some additional context to the page by selecting out the previous and next posts.

// create page for each mdx node posts.forEach((post, index) => { const previous = index === posts.length - 1 ? null : posts[index + 1]; const next = index === 0 ? null : posts[index - 1]; createPage({ path: post.fields.slug, component: blogPostTemplate, context: { slug: post.fields.slug, previous, next, }, }); });

So this should now match up with the query you have on the homepage (src/pages/index.js) except you currently have no filter or sort applied here. So do that now in gatsby-node.js and apply the same filters as on the homepage query:

const { createFilePath } = require(`gatsby-source-filesystem`); const path = require(`path`); exports.createPages = ({ actions, graphql }) => { const { createPage } = actions; const blogPostTemplate = path.resolve( 'src/templates/blogPostTemplate.js' ); return graphql(` { allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { fields { slug } frontmatter { title } } } } `).then(result => { if (result.errors) { throw result.errors; } const posts = result.data.allMdx.nodes; // create page for each mdx node posts.forEach((post, index) => { const previous = index === posts.length - 1 ? null : posts[index + 1]; const next = index === 0 ? null : posts[index - 1]; createPage({ path: post.fields.slug, component: blogPostTemplate, context: { slug: post.fields.slug, previous, next, }, }); }); }); }; exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions; if (node.internal.type === `Mdx`) { const value = createFilePath({ node, getNode }); createNodeField({ name: `slug`, node, value, }); } };

Now you will be able to expose the previous and next objects passed in as context from Gatsby node.

You can destructure previous and next from pageContext and for now pop them into your super handy Dump component to take a look at their contents.

import { graphql } from 'gatsby'; import { MDXRenderer } from 'gatsby-plugin-mdx'; import React from 'react'; import Dump from '../components/Dump'; import { Layout } from '../components/Layout'; export default ({ data, pageContext }) => { const { frontmatter, body } = data.mdx; const { previous, next } = pageContext; return (    

{frontmatter.title}

{frontmatter.date}

{body} ); }; export const query = graphql` query PostsBySlug($slug: String!) { mdx(fields: { slug: { eq: $slug } }) { body frontmatter { title date(formatString: "YYYY MMMM Do") } } } `;

Add in previous and next navigation, this is a couple of ternary operations. If the variable is empty then return null else render a Gatsby Link component with the page slug and the frontmatter title:

import { graphql, Link } from 'gatsby'; import { MDXRenderer } from 'gatsby-plugin-mdx'; import React from 'react'; import Dump from '../components/Dump'; import { Layout } from '../components/Layout'; export default ({ data, pageContext }) => { const { frontmatter, body } = data.mdx; const { previous, next } = pageContext; return (    

{frontmatter.title}

{frontmatter.date}

{body} {previous === false ? null : ( {previous && (

{previous.frontmatter.title}

)} )} {next === false ? null : ( {next && (

{next.frontmatter.title}

)} )} ); }; export const query = graphql` query PostsBySlug($slug: String!) { mdx(fields: { slug: { eq: $slug } }) { body frontmatter { title date(formatString: "YYYY MMMM Do") } } } `;

Code Blocks

Now to add some syntax highlighting for adding code blocks to your blog pages. To do that you’re going to add dependencies for prism-react-renderer and react-live. You’ll also create the files you’re going to need to use:

yarn add prism-react-renderer react-live touch root-wrapper.js gatsby-ssr.js gatsby-browser.js

You’ll come onto react-live soon. For now, you’re going to get prism-react-render up and running for syntax highlighting for any code you’re going to add to the blog. But before that you’re going to go over the root wrapper concept.

So, to change the rendering of a page element, such as a heading or a code block, you’re going to need to use the MDXProvider. The MDXProvider is a component you can use anywhere higher in the React component tree than the MDX content you want to render.

Gatsby browser and a Gatsby SSR both have wrapRootElement available to them and that is as high up the tree as you can get. So you’re going to create the root-wrapper.js file and add elements you want to override there and import it into both gatsby-browser.js and gatsby-ssr.js so you’re not duplicating code.

Before you go any further I want to add that there is a top quality egghead.io playlist resource for using MDX with Gatsby by Chris Chris Biscardi. There’s a ton of useful information in there on MDX in Gatsby.

Ok, first up you’re going to import the root-wrapper.js file into both gatsby-browser.js and gatsby-ssr.js. Into both code modules paste the following:

import { wrapRootElement as wrap } from './root-wrapper'; export const wrapRootElement = wrap;

Ok, now you can work on the code that will be used in both modules. MDX allows you to control the rendering of page elements in your markdown. MDXProvider is used to give React components to override the markdown page elements.

Quick demonstration, in root-wrapper.js add the following:

import { MDXProvider } from '@mdx-js/react'; import React from 'react'; const components = { h2: ({ children }) => ( 

{children}

), 'p.inlineCode': props => ( ), }; export const wrapRootElement = ({ element }) => ( {element} );

You’re now overriding any h2 in your rendered markdown along with any code blocks (that’s words wrapped in `backticks`).

Ok, now for the syntax highlighting, create a post with a block of code in it:

mkdir posts/2019-07-01-code-blocks touch posts/2019-07-01-code-blocks/index.mdx

Paste in some content:

--- title: Code Blocks date: 2019-07-01 published: true --- ## Yes! Some code! Here is the `Dump` component! ```jsx import React from 'react'; const Dump = props => ( {Object.entries(props).map(([key, val]) => ( 
 {key} ?  {JSON.stringify(val, '', ' ')} 
))} ); export default Dump; ```

Go to the prism-react-renderer GitHub page and copy the example code into root-wrapper.js for the pre element.

You’re going to copy the provided code for highlighting to validate that it works.

import { MDXProvider } from '@mdx-js/react'; import Highlight, { defaultProps } from 'prism-react-renderer'; import React from 'react'; const components = { h2: ({ children }) => ( 

{children}

), 'p.inlineCode': props => ( ), pre: props => ( ; `} language="jsx"> {({ className, style, tokens, getLineProps, getTokenProps, }) => (
 {tokens.map((line, i) => ( 

{line.map((token, key) => ( ))} ))}

)} ), }; export const wrapRootElement = ({ element }) => ( {element} );

Cool, cool! Now you want to replace the pasted in code example with the props of the child component of the pre component. You can do that with props.children.props.children.trim() ?.

import { MDXProvider } from '@mdx-js/react'; import Highlight, { defaultProps } from 'prism-react-renderer'; import React from 'react'; const components = { pre: props => (  {({ className, style, tokens, getLineProps, getTokenProps, }) => ( 
 {tokens.map((line, i) => ( 

{line.map((token, key) => ( ))} ))}

)} ), }; export const wrapRootElement = ({ element }) => ( {element} );

Then to match the language, for now you’re going to add in a matches function to match the language class assigned to the code block.

import { MDXProvider } from '@mdx-js/react'; import Highlight, { defaultProps } from 'prism-react-renderer'; import React from 'react'; const components = { h2: ({ children }) => ( 

{children}

), 'p.inlineCode': props => ( ), pre: props => { const className = props.children.props.className || ''; const matches = className.match(/language-(?.*)/); return ( {({ className, style, tokens, getLineProps, getTokenProps, }) => (
 {tokens.map((line, i) => ( 

{line.map((token, key) => ( ))} ))}

)} ); }, }; export const wrapRootElement = ({ element }) => ( {element} );

prism-react-renderer comes with additional themes over the default theme which is duotoneDark. You’re going to use nightOwl in this example, but feel free to take a look at the other examples if you like.

Importer themeog brug det derefter i rekvisitterne til Highlightkomponenten.

import { MDXProvider } from '@mdx-js/react'; import Highlight, { defaultProps } from 'prism-react-renderer'; import theme from 'prism-react-renderer/themes/nightOwl'; import React from 'react'; const components = { pre: props => { const className = props.children.props.className || ''; const matches = className.match(/language-(?.*)/); return (  {({ className, style, tokens, getLineProps, getTokenProps, }) => ( 
 {tokens.map((line, i) => ( 

{line.map((token, key) => ( ))} ))}

)} ); }, }; export const wrapRootElement = ({ element }) => ( {element} );

Ok, nu er det tid til at abstrahere dette ud i sin egen komponent, så din root-wrapper.jsikke er så overfyldt.

Lav en Code.jskomponent, og flyt koden root-wrapper.jsderfra:

touch src/components/Code.js

Husk dette?

Sejt sejt! Nu vil du erstatte den indsatte i kodeeksempel med rekvisitterne til underkomponenten i prækomponenten. Du kan gøre det med props.children.props.children.trim()?

Hvis det ☝ ikke giver nogen reel mening for dig (jeg har været nødt til at læse det mange, mange gange selv), skal du ikke bekymre dig: nu skal du grave i det lidt mere for oprettelsen af ​​kodeblokken komponent.

Så for nu i det, componentsdu tilføjer i MDXProvider, skal du kigge på det, der propskommer ind i preelementet.

Comment out the code you added earlier and add in a console.log:

pre: props => { console.log('====================='); console.log(props); console.log('====================='); return 
; };

Now if you pop open the developer tools of your browser you can see the output.

{children: {…}} children: $$typeof: Symbol(react.element) key: null props: {parentName: "pre", className: "language-jsx", originalType: "code", mdxType: "code", children: "import React from 'react'↵↵const Dump = props => (… 
↵ ))}↵ ↵)↵↵export default Dump↵"} ref: null type: ƒ (re....

If you drill into the props of that output you can see the children of those props. If you take a look at the contents of that you will see that it is the code string for your code block. This is what you’re going to be passing into the Code component you’re about to create. Other properties to note here are the className and mdxType.

So, take the code you used earlier for Highlight, everything inside and including the return statement, and paste it into the Code.js module you created earlier.

Highlight requires several props:

The Code module should look something like this now:

import Highlight, { defaultProps } from 'prism-react-renderer'; import theme from 'prism-react-renderer/themes/nightOwl'; import React from 'react'; const Code = ({ codeString, language }) => { return (  {({ className, style, tokens, getLineProps, getTokenProps, }) => ( 
 {tokens.map((line, i) => ( 

{line.map((token, key) => ( ))} ))}

)} ); }; export default Code;

Back to the root-wrapper where you’re going to pass the props needed to the Code component.

The first check you’re going to do is if the mdxType is code then you can get the additional props you need to pass to your Code component.

You’re going to get defaultProps and the theme from prism-react-renderer so all that’s needed is the code and language.

The codeString you can get from the props, and the children by destructuring from the props being passed into the pre element. The language can either be the tag assigned to the meta property of the backticks, like js, jsx or equally empty. So you check for that with some JavaScript and also remove the language- prefix, then pass in the elements {...props}:

pre: ({ children: { props } }) => { if (props.mdxType === 'code') { return (  ); } };

Ok, now you’re back to where you were before abstracting out the Highlight component to it’s own module. Add some additional styles with styled-components and replace the pre with a styled Pre. You can also add in some line numbers with a styled span and style that as well.

import Highlight, { defaultProps } from 'prism-react-renderer'; import theme from 'prism-react-renderer/themes/nightOwl'; import React from 'react'; import styled from 'styled-components'; export const Pre = styled.pre` text-align: left; margin: 1em 0; padding: 0.5em; overflow-x: auto; border-radius: 3px; & .token-line { line-height: 1.3em; height: 1.3em; } font-family: 'Courier New', Courier, monospace; `; export const LineNo = styled.span` display: inline-block; width: 2em; user-select: none; opacity: 0.3; `; const Code = ({ codeString, language, ...props }) => { return (  {({ className, style, tokens, getLineProps, getTokenProps, }) => ( 
 {tokens.map((line, i) => ( 

{i + 1} {line.map((token, key) => ( ))} ))}

)} ); }; export default Code;

Copy code to clipboard

What if you had some way of getting that props code string into the clipboard?

I had a look around and found the majority of the components available for this sort of thing expected an input until this in the Gatsby source code. Which is creating the input for you ?

So, create a utils directory and the copy-to-clipboard.js file and add in the code from the Gatsby source code.

mkdir src/utils touch src/utils/copy-to-clipboard.js
// //github.com/gatsbyjs/gatsby/blob/master/www/src/utils/copy-to-clipboard.js export const copyToClipboard = str => { const clipboard = window.navigator.clipboard; /* * fallback to older browsers (including Safari) * if clipboard API not supported */ if (!clipboard || typeof clipboard.writeText !== `function`) { const textarea = document.createElement(`textarea`); textarea.value = str; textarea.setAttribute(`readonly`, true); textarea.setAttribute(`contenteditable`, true); textarea.style.position = `absolute`; textarea.style.left = `-9999px`; document.body.appendChild(textarea); textarea.select(); const range = document.createRange(); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); textarea.setSelectionRange(0, textarea.value.length); document.execCommand(`copy`); document.body.removeChild(textarea); return Promise.resolve(true); } return clipboard.writeText(str); };

Now you’re going to want a way to trigger copying the code to the clipboard.

Let's create a styled button. But first add a position: relative; to the Pre component which will let us position the styled button:

const CopyCode = styled.button` position: absolute; right: 0.25rem; border: 0; border-radius: 3px; margin: 0.25em; opacity: 0.3; &:hover { opacity: 1; } `;

And now you need to use the copyToClipboard function in the onClick of the button:

import Highlight, { defaultProps } from 'prism-react-renderer'; import theme from 'prism-react-renderer/themes/nightOwl'; import React from 'react'; import styled from 'styled-components'; import { copyToClipboard } from '../utils/copy-to-clipboard'; export const Pre = styled.pre` text-align: left; margin: 1rem 0; padding: 0.5rem; overflow-x: auto; border-radius: 3px; & .token-line { line-height: 1.3rem; height: 1.3rem; } font-family: 'Courier New', Courier, monospace; position: relative; `; export const LineNo = styled.span` display: inline-block; width: 2rem; user-select: none; opacity: 0.3; `; const CopyCode = styled.button` position: absolute; right: 0.25rem; border: 0; border-radius: 3px; margin: 0.25em; opacity: 0.3; &:hover { opacity: 1; } `; const Code = ({ codeString, language }) => { const handleClick = () => { copyToClipboard(codeString); }; return (  {({ className, style, tokens, getLineProps, getTokenProps, }) => ( 
 Copy {tokens.map((line, i) => ( 

{i + 1} {line.map((token, key) => ( ))} ))}

)} ); }; export default Code;

React live

So with React Live you need to add two snippets to your Code.js component.

You’re going to import the components:

import { LiveEditor, LiveError, LivePreview, LiveProvider, } from 'react-live';

Then you’re going to check if react-live has been added to the language tag on your mdx file via the props:

if (props['react-live']) { return (      ); }

Here’s the full component:

import Highlight, { defaultProps } from 'prism-react-renderer'; import theme from 'prism-react-renderer/themes/nightOwl'; import React from 'react'; import { LiveEditor, LiveError, LivePreview, LiveProvider, } from 'react-live'; import styled from 'styled-components'; import { copyToClipboard } from '../../utils/copy-to-clipboard'; const Pre = styled.pre` position: relative; text-align: left; margin: 1em 0; padding: 0.5em; overflow-x: auto; border-radius: 3px; & .token-lline { line-height: 1.3em; height: 1.3em; } font-family: 'Courier New', Courier, monospace; `; const LineNo = styled.span` display: inline-block; width: 2em; user-select: none; opacity: 0.3; `; const CopyCode = styled.button` position: absolute; right: 0.25rem; border: 0; border-radius: 3px; margin: 0.25em; opacity: 0.3; &:hover { opacity: 1; } `; export const Code = ({ codeString, language, ...props }) => { if (props['react-live']) { return (      ); } const handleClick = () => { copyToClipboard(codeString); }; return (  {({ className, style, tokens, getLineProps, getTokenProps, }) => ( 
 Copy {tokens.map((line, i) => ( 

{i + 1} {line.map((token, key) => ( ))} ))}

)} ); };

To test this, add react-live next to the language on your Dump component, so you have added to the blog post you made:

```jsx react-live

Now you can edit the code directly. Try changing a few things like this:

const Dump = props => ( {Object.entries(props).map(([key, val]) => ( 
 {key} ?  {JSON.stringify(val, '', ' ')} 
))} ); render();

Cover Image

Now to add a cover image to go with each post, you’ll need to install a couple of packages to manage images in Gatsby.

install:

yarn add gatsby-transformer-sharp gatsby-plugin-sharp gatsby-remark-images gatsby-image

Now you should config gatsby-config.js to include the newly added packages. Remember to add gatsby-remark-images to gatsby-plugin-mdx as both a gatsbyRemarkPlugins option and as a plugins option.

config:

module.exports = { siteMetadata: siteMetadata, plugins: [ `gatsby-plugin-styled-components`, `gatsby-transformer-sharp`, `gatsby-plugin-sharp`, { resolve: `gatsby-plugin-mdx`, options: { extensions: [`.mdx`, `.md`], gatsbyRemarkPlugins: [ { resolve: `gatsby-remark-images`, options: { maxWidth: 590, }, }, ], plugins: [ { resolve: `gatsby-remark-images`, options: { maxWidth: 590, }, }, ], }, }, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/posts`, name: `posts` }, }, ], };

Add image to index query in src/pages.index.js:

cover { publicURL childImageSharp { sizes( maxWidth: 2000 traceSVG: { color: "#639" } ) { ...GatsbyImageSharpSizes_tracedSVG } } }

Fix up the date in the query too:

date(formatString: "YYYY MMMM Do")

This will show the date as full year, full month and the day as a ‘st’, ‘nd’, ‘rd’ and ‘th’. So if today’s date were 1970/01/01 it would read 1970 January 1st.

Add gatsby-image use that in a styled component:

const Image = styled(Img)` border-radius: 5px; `;

Add some JavaScript to determine if there’s anything to render:

{ !!frontmatter.cover ? (  ) : null; }

Here’s what the full module should look like now:

import { Link } from 'gatsby'; import Img from 'gatsby-image'; import React from 'react'; import styled from 'styled-components'; import { Layout } from '../components/Layout'; const IndexWrapper = styled.main``; const PostWrapper = styled.div``; const Image = styled(Img)` border-radius: 5px; `; export default ({ data }) => { return (   {/*  */} {data.allMdx.nodes.map( ({ id, excerpt, frontmatter, fields }) => (   {!!frontmatter.cover ? (  ) : null} 

{frontmatter.title}

{frontmatter.date}

{excerpt}

) )} ); }; export const query = graphql` query SITE_INDEX_QUERY { allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { id excerpt(pruneLength: 250) frontmatter { title date(formatString: "YYYY MMMM Do") cover { publicURL childImageSharp { sizes(maxWidth: 2000, traceSVG: { color: "#639" }) { ...GatsbyImageSharpSizes_tracedSVG } } } } fields { slug } } } } `;

Additional resources:

  • this helped me for my own blog: //juliangaramendy.dev/custom-open-graph-images-in-gatsby-blog/
  • and the Gatsby docs: //www.gatsbyjs.org/docs/working-with-images/

Creating an SEO component with React Helmet

There’s a Gatsby github PR on seo with some great notes from Andrew Welch on SEO and a link to a presentation he did back in 2017.

Crafting Modern SEO with Andrew Welch:

Crafting Modern SEO with Andrew Welch:

In the following comments of that PR, Gatsby’s LekoArts details his own implementation which I have implemented as a React component. You’re going to be configuring that now in this how-to.

First up, install and configure gatsby-plugin-react-helmet. This is used for server rendering data added with React Helmet.

yarn add gatsby-plugin-react-helmet

You’ll need to add the plugin to your gatsby-config.js. If you haven’t done so already now is a good time to also configure the gatsby-plugin-styled-components as well.

Configure SEO Component for Homepage

To visualise the data you’re going to need to get into the SEO component use the Dump component to begin with to validate the data.

The majority of the information needed for src/pages/index.js can be first added to the gatsby-config.js, siteMetadata object then queried with the useSiteMetadata hook. Some of the data added here can then be used in src/templates/blogPostTemplate.js – more on that in the next section.

For now add the following:

const siteMetadata = { title: `The Localhost Blog`, description: `This is my coding blog where I write about my coding journey.`, image: `/default-site-image.jpg`, siteUrl: `//thelocalhost.blog`, siteLanguage: `en-GB`, siteLocale: `en_gb`, twitterUsername: `@spences10`, authorName: `Scott Spence`, } module.exports = { siteMetadata: siteMetadata, plugins: [ ...

You don’t have to abstract out the siteMetadata into its own component here. It’s only a suggestion on how to manage it.

The image is going to be the default image for your site. You should create a static folder at the root of the project and add in an image you want to be shown when the homepage of your site is shared on social media.

For siteUrl at this stage it doesn’t necessarily have to be valid. You can add a dummy url for now and change it later.

The siteLanguage is your language of choice for the site. Take a look at w3 language tags for more info.

Facebook OpenGraph is the only place the siteLocale is used and it is different from language tags.

Add your twitterUsername and your authorName.

Update the useSiteMetadata hook now to reflect the newly added properties:

import { graphql, useStaticQuery } from 'gatsby'; export const useSiteMetadata = () => { const { site } = useStaticQuery( graphql` query SITE_METADATA_QUERY { site { siteMetadata { description title image siteUrl siteLanguage siteLocale twitterUsername authorName } } } ` ); return site.siteMetadata; };

Begin with importing the Dump component in src/pages/index.js then plug in the props as they are detailed in the docs of the react-seo-component.

import Dump from '../components/Dump' import { useSiteMetadata } from '../hooks/useSiteMetadata' export default ({ data }) => { const { description, title, image, siteUrl, siteLanguage, siteLocale, twitterUsername, } = useSiteMetadata() return (    {data.allMdx.nodes.map( ...

Check that all the props are displaying valid values. Then you can swap out the Dump component with the SEO component.

The complete src/pages/index.js should look like this now:

import { graphql, Link } from 'gatsby'; import Img from 'gatsby-image'; import React from 'react'; import SEO from 'react-seo-component'; import styled from 'styled-components'; import { Layout } from '../components/Layout'; import { useSiteMetadata } from '../hooks/useSiteMetadata'; const IndexWrapper = styled.main``; const PostWrapper = styled.div``; const Image = styled(Img)` border-radius: 5px; `; export default ({ data }) => { const { description, title, image, siteUrl, siteLanguage, siteLocale, twitterUsername, } = useSiteMetadata(); return (    {/*  */} {data.allMdx.nodes.map( ({ id, excerpt, frontmatter, fields }) => (   {!!frontmatter.cover ? (  ) : null} 

{frontmatter.title}

{frontmatter.date}

{excerpt}

) )} ); }; export const query = graphql` query SITE_INDEX_QUERY { allMdx( sort: { fields: [frontmatter___date], order: DESC } filter: { frontmatter: { published: { eq: true } } } ) { nodes { id excerpt(pruneLength: 250) frontmatter { title date(formatString: "YYYY MMMM Do") cover { publicURL childImageSharp { sizes(maxWidth: 2000, traceSVG: { color: "#639" }) { ...GatsbyImageSharpSizes_tracedSVG } } } } fields { slug } } } } `;

Configure SEO Component for Blog Posts

This will be the same approach as with the homepage. Import the Dump component and validate the props before swapping out the Dump component with the SEO component.

import Dump from '../components/Dump' import { useSiteMetadata } from '../hooks/useSiteMetadata' export default ({ data, pageContext }) => { const { image, siteUrl, siteLanguage, siteLocale, twitterUsername, authorName, } = useSiteMetadata() const { frontmatter, body, fields, excerpt } = data.mdx const { title, date, cover } = frontmatter const { previous, next } = pageContext return (   

{frontmatter.title}

...

Add fields.slug, excerpt and cover.publicURL to the PostsBySlug query and destructure them from data.mdx and frontmatter, respectively.

For the image you’ll need to do some logic as to whether the cover exists and default to the default site image if it doesn’t.

The complete src/templates/blogPostTemplate.js should look like this now:

import { graphql, Link } from 'gatsby'; import { MDXRenderer } from 'gatsby-plugin-mdx'; import React from 'react'; import SEO from 'react-seo-component'; import { Layout } from '../components/Layout'; import { useSiteMetadata } from '../hooks/useSiteMetadata'; export default ({ data, pageContext }) => { const { image, siteUrl, siteLanguage, siteLocale, twitterUsername, authorName, } = useSiteMetadata(); const { frontmatter, body, fields, excerpt } = data.mdx; const { title, date, cover } = frontmatter; const { previous, next } = pageContext; return (   

{frontmatter.title}

{frontmatter.date}

{body} {previous === false ? null : ( {previous && (

{previous.frontmatter.title}

)} )} {next === false ? null : ( {next && (

{next.frontmatter.title}

)} )} ); }; export const query = graphql` query PostBySlug($slug: String!) { mdx(fields: { slug: { eq: $slug } }) { frontmatter { title date(formatString: "YYYY MMMM Do") cover { publicURL } } body excerpt fields { slug } } } `;

Build Site and Validate Meta Tags

Add in the build script to package.json and also a script for serving the built site locally.

"scripts": { "dev": "gatsby develop -p 9988 -o", "build": "gatsby build", "serve": "gatsby serve -p 9500 -o" },

Now it’s time to run:

yarn build && yarn serve

This will build the site and open a browser tab so you can see the site as it will appear when it is on the internet. Validate meta tags have been added to the build by selecting “View page source” (Crtl+u in Windows and Linux) on the page. You can do a Ctrl+f to find them.

Adding the Project to GitHub

Add your code to GitHub by either selecting the plus (+) icon next to your avatar on GitHub or by going directly to //github.com/new

Name your repository and click create repository. Then you will be given the instructions to link your local code to the repository you created via the command line.

How you authenticate with GitHub will depend on what the command looks like.

Some good resources for authenticating with GitHub via SSH are Kent Dodds Egghead.io video and also a how-to on CheatSheets.xyz.

Deploy to Netlify

To deploy your site to Netlify, if you haven’t done so already, you’ll need to add the GitHub integration to your GitHub profile. If you got to app.netlify.com the wizard will walk you through the process.

From here you can add your built site’s public folder drag ‘n drop style directly to the Netlify global CDNs.

You, however, are going to load your site via the Netlify CLI! In your terminal, if you haven’t already got the CLI installed, run:

yarn global add netlify-cli

Then once the CLI is installed:

# authenticate via the CLI netlify login # initialise the site netlify init

Enter the details for your team: the site name is optional, the build command will be yarn build, and directory to deploy is public.

You will be prompted to commit the changes and push them to GitHub (with git push). Once you have done that your site will be published and ready for all to see!

Validate Metadata with Heymeta

Last up is validating the metadata for the OpenGraph fields. To do that you’ll need to make sure that the siteUrl reflects what you have in your Netlify dashboard.

If you needed to change the url you’ll need to commit and push the changes to GitHub again.

Once your site is built with a valid url you can then test the homepage and a blog page for the correct meta tags with heymeta.com.

OpenGraph checking tools:

  • //www.heymeta.com/
  • //opengraphcheck.com/
  • //cards-dev.twitter.com/validator
  • //developers.facebook.com/tools/debug/sharing
  • //www.linkedin.com/post-inspector/

Additional resources:

  • The Essential Meta Tags for Social Media

Example Code for this Blog can be found here:

Or here.

Thanks for reading ?

That’s all folks! If there is anything I have missed, or if there is a better way to do something, then please let me know.

Følg mig på Twitter eller spørg mig om noget på GitHub.

Du kan læse andre artikler som denne i min digitale have.