Gatsby themes introduce a new model for building Gatsby sites and apps. However, due to the recency of themes being available, the majority of the ecosystem is still in starters that could better serve their users as themes. This series of posts will take you through converting the gatsby-starter-blog to a theme. We'll start by getting the starter to run as a theme.
If you're unfamiliar with themes, you may want to read my introduction on the official gatsby blog or watch my talk at the first ever Gatsby Days -- Tweet this
In this series of posts, we'll use Yarn Workspaces to work
on our theme while also keeping an example usage of said
theme close by. It is not required to use Yarn Workspaces
and you can develop in any way you feel comfortable, such as
using yarn link
, local filepaths for package.json
dependencies, or anything else. I also wrote an
Introduction to Yarn Workspaces
if you are unfamiliar and also interested in learning more.
First create a directory to hold our packages.
mkdir converting-a-gatsby-starter-to-themescd converting-a-gatsby-starter-to-themesyarn init -y
Then we can use Gatsby's starter functionality to pull down
the blog starter into a gatsby-theme-awesome-blog
folder.
npx gatsby new gatsby-theme-awesome-blog https://github.com/gatsbyjs/gatsby-starter-blog
We'll also remove the package-lock.json
because we're
using yarn
and change the name in the
~/gatsby-theme-awesome-blog/package.json
from
gatsby-starter-blog
to gatsby-theme-awesome-blog
.
rm gatsby-theme-awesome-blog/package-lock.json
{"name": "gatsby-theme-awesome-blog",...}
Next, we'll enable workspaces by making our root
package.json
private (required for workspaces to work),
and also add a workspaces
config. Here's my package.json
before changes.
{"name": "converting-a-gatsby-starter-to-themes","version": "1.0.0","main": "index.js","author": "christopherbiscardi <chris@christopherbiscardi.com> (@chrisbiscardi)","license": "MIT"}
and after we configure two workspaces. One for our theme and
another called my-blog
for our example app. We'll create
the example app itself in a bit.
{"name": "converting-a-gatsby-starter-to-themes","private": true,"version": "1.0.0","main": "index.js","author": "christopherbiscardi <chris@christopherbiscardi.com> (@chrisbiscardi)","license": "MIT","workspaces": ["gatsby-theme-awesome-blog","my-blog"]}
We've set up enough to run the starter as it originally exists. This will ensure that you've set up workspaces correctly.
yarn workspace gatsby-theme-awesome-blog develop
Now that we have the starter running, we can start using it
as a theme and see what breaks. Let's create the my-blog
example app to use the theme.
mkdir my-blogcd my-blogyarn init -y
We need to add gatsby-theme-awesome-blog
as a dependency
of our blog and create the important gatsby-config.js
where we'll configure our theme.
in package.json we add the dependency and some useful scripts.
{"name": "my-blog","version": "1.0.0","main": "index.js","author": "christopherbiscardi <chris@christopherbiscardi.com> (@chrisbiscardi)","license": "MIT","dependencies": {"gatsby": "2.0.111","gatsby-theme-awesome-blog": "*"},"scripts": {"build": "gatsby build","develop": "gatsby develop","serve": "gatsby serve","test": "echo \"Write tests! -> https://gatsby.app/unit-testing\""}}
in gatsby-config.js
module.exports = {__experimentalThemes: [`gatsby-theme-awesome-blog`]};
One point I would like to note here is that using a theme
in your new project is a total of two files. The
package.json
, to declare the dependency on gatsby
and
your theme, as well as the gatsby-config.js
to use the
theme.
Run yarn
in the root of the project to install and link
our dependencies (including the theme into our blog) and now
we can run develop
to run our starter as a theme and see
what breaks.
➜ yarn workspace my-blog developyarn workspace v1.13.0yarn run v1.13.0$ gatsby developerror Cannot find module 'gatsby-theme-awesome-blog'Error: Cannot find module 'gatsby-theme-awesome-blog'- loader.js:581 Function.Module._resolveFilenameinternal/modules/cjs/loader.js:581:15- v8-compile-cache.js:162 Function.require.resolve[converting-a-gatsby-starter-to-themes]/[v8-compile-cache]/v8-compile-cache.js:162:23- index.js:27[converting-a-gatsby-starter-to-themes]/[gatsby]/dist/bootstrap/load-themes/index.js:27:43- Generator.next- debuggability.js:313 Promise._execute[converting-a-gatsby-starter-to-themes]/[bluebird]/js/release/debuggability.js:313:9- promise.js:483 Promise._resolveFromExecutor[converting-a-gatsby-starter-to-themes]/[bluebird]/js/release/promise.js:483:18- promise.js:79 new Promise[converting-a-gatsby-starter-to-themes]/[bluebird]/js/release/promise.js:79:10- index.js:44 resolveTheme[converting-a-gatsby-starter-to-themes]/[gatsby]/dist/bootstrap/load-themes/index.js:44:17- index.js:100[converting-a-gatsby-starter-to-themes]/[gatsby]/dist/bootstrap/load-themes/index.js:100:32- Generator.next
This first problem can be a little confusing if you're not
used to how node packages get resolved. What it means is
that it can't find an index.js
or a main
field in the
package.json
for our theme. We can solve this by creating
an empty index.js
file in our theme. You'll also notice
this pattern in a lot of gatsby plugins if you check out
their source.
touch gatsby-theme-awesome-blog/index.js
Now we get a new error
➜ yarn workspace my-blog developyarn workspace v1.13.0yarn run v1.13.0$ gatsby developsuccess open and validate gatsby-configs — 0.017 ssuccess load plugins — 0.351 ssuccess onPreInit — 1.036 ssuccess delete html and css files from previous builds — 0.009 ssuccess initialize cache — 0.031 ssuccess copy gatsby files — 0.099 ssuccess onPreBootstrap — 0.007 ssuccess source and transform nodes — 0.118 ssuccess building schema — 0.384 s⠁The plugin "gatsby-theme-awesome-blog" created a page with a component that doesn't exist{ path: '/hi-folks/',component:'/converting-a-gatsby-starter-to-themes/my-blog/src/templates/blog-post.js',context:{ slug: '/hi-folks/',previous: { fields: [Object], frontmatter: [Object] },next: null } }error See the documentation for createPage https://www.gatsbyjs.org/docs/actions/#createPageerror Command failed with exit code 1.info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.error Command failed.Exit code: 1Command: /Users/chris/.nvm/versions/node/v10.6.0/bin/nodeArguments: /usr/local/Cellar/yarn/1.13.0/libexec/lib/cli.js developDirectory: /converting-a-gatsby-starter-to-themes/my-blogOutput:info Visit https://yarnpkg.com/en/docs/cli/workspace for documentation about this command.
This error is telling us that the createPage
calls in our
theme are creating pages that don't point to React
components. If we go to
gatsby-theme-awesome-blog/gatsby-node.js
we can see why.
The blog post template is using
path.resolve
to create the path to the
React component. In a node module,
path.resolve
will default to the current
working directory if you only give it a relative path, like
our theme is doing here.
If after processing all given path segments an absolute path has not yet been generated, the current working directory is used. -- node docs
const blogPost = path.resolve(`./src/templates/blog-post.js`);
For starters this is fine because we always run scripts from
the root of our project and our code isn't tucked away in a
node module. When our theme is in a node module, this
doesn't work because the current working directory is the
directory that our blog is in and our theme is going to be
somewhere else (node_modules, a workspace package, etc).
Since we're using JavaScript, we can piggy-back on node's
require algorithm itself and replace path
with require
.
An additional benefit of require.resolve
is that it will
error out if it can't find the file. Failing earlier at the
source of the problem makes it easier to detect where the
actual problem is in our code.
const blogPost = require.resolve(`./src/templates/blog-post.js`);
Now we can run again
➜ yarn workspace my-blog developyarn workspace v1.13.0yarn run v1.13.0$ gatsby developsuccess open and validate gatsby-configs — 0.019 ssuccess load plugins — 0.310 ssuccess onPreInit — 0.650 ssuccess delete html and css files from previous builds — 0.012 ssuccess initialize cache — 0.036 ssuccess copy gatsby files — 0.094 ssuccess onPreBootstrap — 0.007 ssuccess source and transform nodes — 0.117 ssuccess building schema — 0.340 ssuccess createPages — 0.053 ssuccess createPagesStatefully — 0.011 ssuccess onPreExtractQueries — 0.005 ssuccess update schema — 0.203 swarning The GraphQL query in the non-page component "/converting-a-gatsby-starter-to-themes/gatsby-theme-awesome-blog/src/pages/404.js" will not be run.warning The GraphQL query in the non-page component "/converting-a-gatsby-starter-to-themes/gatsby-theme-awesome-blog/src/pages/index.js" will not be run.Exported queries are only executed for Page components. It's possible you'retrying to create pages in your gatsby-node.js and that's failing for somereason.If the failing component(s) is a regular component and not intended to be a pagecomponent, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)instead of exporting a page query.If you're more experienced with GraphQL, you can also export GraphQLfragments from components and compose the fragments in the Page componentquery and pass data down into the child component — http://graphql.org/learn/queries/#fragmentssuccess extract queries from components — 0.168 sGenerating image thumbnails [==============================] 4/4 0.0 secs 100%success run graphql queries — 0.353 s — 6/6 17.07 queries/secondsuccess write out page data — 0.004 ssuccess write out redirect data — 0.002 sGenerating image thumbnails [==============================] 11/11 0.4 secs 100%info bootstrap finished - 4.907 serror Plugin gatsby-plugin-manifest returned an errorError: icon (content/assets/gatsby-icon.png) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.success onPostBootstrap — 0.016 s(node:34557) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` insteaderror Input file is missingError: Input file is missingerror UNHANDLED REJECTIONError: Input file is missing
This time we're getting an issue from
gatsby-plugin-manifest
about an icon that can't be found.
If we dive into
the doesNotExist function
that is used
right before generating icons
we can see it's using a statSync
, which needs a full file
path because it performs a similar resolution to
path.resolve
.
exports.doesIconExist = function doesIconExist(srcIcon) {try {return fs.statSync(srcIcon).isFile();} catch (e) {if (e.code === `ENOENT`) {return false;} else {throw e;}}};
In this case we'll use path.resolve
since we're trying to
generate a path for a .png
file. In
gatsby-theme-awesome-blog/gatsby-config.js
we'll replace
the icon
key with path.resolve
and a __dirname
as the
first path to resolve with.
{resolve: `gatsby-plugin-manifest`,options: {name: `Gatsby Starter Blog`,short_name: `GatsbyJS`,start_url: `/`,background_color: `#ffffff`,theme_color: `#663399`,display: `minimal-ui`,icon: path.resolve(__dirname, `content/assets/gatsby-icon.png`),},},
If we run again, we'll see that we're past the other two path issues, and onto a third :) Path changes are the major change you'll need to make when converting a starter to a theme because it's likely that (due to documentation and other factors) the paths most starters are using include assumptions about running in the same working directory as the site and not an NPM package.
The third is for gatsby-plugin-typography
➜ yarn workspace my-blog developyarn workspace v1.13.0yarn run v1.13.0$ gatsby developsuccess open and validate gatsby-configs — 0.017 ssuccess load plugins — 0.309 ssuccess onPreInit — 0.633 ssuccess delete html and css files from previous builds — 0.023 ssuccess initialize cache — 0.015 ssuccess copy gatsby files — 0.111 ssuccess onPreBootstrap — 0.007 ssuccess source and transform nodes — 0.081 ssuccess building schema — 0.360 ssuccess createPages — 0.056 ssuccess createPagesStatefully — 0.011 ssuccess onPreExtractQueries — 0.005 ssuccess update schema — 0.220 swarning The GraphQL query in the non-page component "/converting-a-gatsby-starter-to-themes/gatsby-theme-awesome-blog/src/pages/404.js" will not be run.warning The GraphQL query in the non-page component "/converting-a-gatsby-starter-to-themes/gatsby-theme-awesome-blog/src/pages/index.js" will not be run.Exported queries are only executed for Page components. It's possible you'retrying to create pages in your gatsby-node.js and that's failing for somereason.If the failing component(s) is a regular component and not intended to be a pagecomponent, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)instead of exporting a page query.If you're more experienced with GraphQL, you can also export GraphQLfragments from components and compose the fragments in the Page componentquery and pass data down into the child component — http://graphql.org/learn/queries/#fragmentssuccess extract queries from components — 0.160 ssuccess run graphql queries — 0.081 s — 6/6 75.96 queries/secondsuccess write out page data — 0.004 ssuccess write out redirect data — 0.001 s⡀ onPostBootstrapdone generating icons for manifestsuccess onPostBootstrap — 0.221 sinfo bootstrap finished - 4.51 s(node:35229) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` insteadhere ../node_modules/gatsby-plugin-typography/.cache/typography.jsModule not found: Error: Can't resolve '/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography' in '/converting-a-gatsby-starter-to-themes/node_modules/gatsby-plugin-typography/.cache'resolve '/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography' in '/converting-a-gatsby-starter-to-themes/node_modules/gatsby-plugin-typography/.cache'using description file: /converting-a-gatsby-starter-to-themes/node_modules/gatsby-plugin-typography/package.json (relative path: ./.cache)using description file: /converting-a-gatsby-starter-to-themes/my-blog/package.json (relative path: ./src/utils/typography)no extension/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography doesn't exist.mjs/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.mjs doesn't exist.js/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.js doesn't exist.jsx/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.jsx doesn't exist.wasm/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.wasm doesn't exist.json/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.json doesn't existas directory/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography doesn't exist[/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography][/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.mjs][/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.js][/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.jsx][/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.wasm][/converting-a-gatsby-starter-to-themes/my-blog/src/utils/typography.json]@ ../node_modules/gatsby-plugin-typography/.cache/typography.js 1:17-134@ ../node_modules/gatsby-plugin-typography/gatsby-ssr.js@ ./.cache/api-runner-ssr.js@ ./.cache/develop-static-entry.jserror There was an error compiling the html.js component for the development server.See our docs page on debugging HTML builds for help https://gatsby.app/debug-html
gatsby-plugin-typography
has a few different ways to
resolve the utils file it looks for
based on if the path is an absolute path or not.
This means we can change our path to an absolute path like
before and it will work again. In this case, we're looking
for a JavaScript file again, so we'll use require.resolve
.
{resolve: `gatsby-plugin-typography`,options: {pathToConfigModule: require.resolve(`./src/utils/typography`),},},
Ok so now our site is compiling and running and we still have one warning to deal with.
➜ yarn workspace my-blog developyarn workspace v1.13.0yarn run v1.13.0$ gatsby developsuccess open and validate gatsby-configs — 0.021 ssuccess load plugins — 0.304 ssuccess onPreInit — 0.627 ssuccess delete html and css files from previous builds — 0.025 ssuccess initialize cache — 0.029 ssuccess copy gatsby files — 0.093 ssuccess onPreBootstrap — 0.007 ssuccess source and transform nodes — 0.118 ssuccess building schema — 0.362 ssuccess createPages — 0.056 ssuccess createPagesStatefully — 0.012 ssuccess onPreExtractQueries — 0.005 ssuccess update schema — 0.207 swarning The GraphQL query in the non-page component "/converting-a-gatsby-starter-to-themes/gatsby-theme-awesome-blog/src/pages/404.js" will not be run.warning The GraphQL query in the non-page component "/converting-a-gatsby-starter-to-themes/gatsby-theme-awesome-blog/src/pages/index.js" will not be run.Exported queries are only executed for Page components. It's possible you'retrying to create pages in your gatsby-node.js and that's failing for somereason.If the failing component(s) is a regular component and not intended to be a pagecomponent, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)instead of exporting a page query.If you're more experienced with GraphQL, you can also export GraphQLfragments from components and compose the fragments in the Page componentquery and pass data down into the child component — http://graphql.org/learn/queries/#fragmentssuccess extract queries from components — 0.162 ssuccess run graphql queries — 0.216 s — 6/6 27.97 queries/secondsuccess write out page data — 0.004 ssuccess write out redirect data — 0.001 s⠄ onPostBootstrapdone generating icons for manifestsuccess onPostBootstrap — 0.183 sinfo bootstrap finished - 4.627 s(node:36031) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead(node:36031) DeprecationWarning: Resolver#doResolve: The type arguments (string) is now a hook argument (Hook). Pass a reference to the hook instead.Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration .DONE Compiled successfully in 3985msYou can now view my-blog in the browser.http://localhost:8000/View GraphiQL, an in-browser IDE, to explore your site's data and schemahttp://localhost:8000/___graphqlNote that the development build is not optimized.To create a production build, use gatsby buildℹ 「wdm」:ℹ 「wdm」: Compiled successfully.
If we visit the working site we'll see the standard Gatsby
development page showing each of the URLs for our blog
posts. What we won't see is the pages that are in
gatsby-theme-awesome-blog/src/pages
, including the
index.js
or /
page. This is because src/pages
is
handled by default with every Gatsby site by
gatsby-plugin-page-creator
which is automatically instantiated. While Gatsby themes
will eventually add this to our themes by default as well,
it isn't implemented yet so we'll do it ourselves for now.
In gatsby-config.js
, add the following plugin.
{resolve: `gatsby-plugin-page-creator`,options: {path: path.resolve(__dirname, `src/pages`),},},
In total, we created an empty index.js
, changed a few of
our paths, and added a plugin to our starter to change it
into a theme. In the near future we won't have to add the
plugin either. Creating Gatsby themes is very much the
same as creating a normal Gatsby site. In the next post
we'll start making our theme a bit more "theme-like" by
moving the markdown files into our blog instead of letting
them live in the theme itself.