Read OSS

Extending Gatsby: The Plugin API, Themes, and Deployment Adapters

Intermediate

Prerequisites

  • Articles 1-4 of this series
  • Basic understanding of Gatsby plugin usage from the user perspective

Extending Gatsby: The Plugin API, Themes, and Deployment Adapters

Every Gatsby site is, at its core, a composition of plugins. Even the most basic site runs a dozen internal plugins that provide filesystem routing, error pages, Babel configuration, and webpack theme shadowing. Understanding the plugin system isn't just useful for plugin authors—it's essential for understanding how Gatsby itself works.

In this final article, we'll cover the full extensibility surface: how plugins are discovered and loaded, the three API surfaces they can implement, how themes compose via recursive config merging and component shadowing, the canonical patterns for source and transformer plugins, the SSG/DSG/SSR page mode system, and the deployment adapter abstraction that decouples Gatsby from any particular hosting platform.

Plugin Resolution and the Three API Surfaces

As we saw in Part 2, the plugin loader in packages/gatsby/src/bootstrap/load-plugins/index.ts runs during initialization. Its pipeline is: normalizevalidate optionsload internalsflattencollate APIsvalidate exports.

The collatePluginAPIs step checks each plugin's exports against three documented API surfaces:

Surface File Key APIs Environment
Node gatsby-node.js sourceNodes, createPages, onCreateNode, onCreateWebpackConfig, createSchemaCustomization Node.js (build time)
Browser gatsby-browser.js onClientEntry, wrapPageElement, wrapRootElement, onRouteUpdate Browser (client runtime)
SSR gatsby-ssr.js onRenderBody, onPreRenderHTML, wrapPageElement, wrapRootElement Node.js (HTML generation)

The validation at lines 64–76 collects "bad exports"—any exports from a plugin that don't match a known API name. This catches typos (e.g., exports.createPage instead of exports.createPages) and helps plugin authors stay aligned with the framework.

flowchart TD
    A["gatsby-config.js plugins array"] --> B["normalizeConfig()"]
    B --> C["validateConfigPluginsOptions()"]
    C --> D["loadInternalPlugins()"]
    D --> E["flattenPlugins()"]
    E --> F["collatePluginAPIs()"]
    F --> G["handleBadExports()"]
    G --> H["handleMultipleReplaceRenderers()"]
    H --> I["SET_SITE_FLATTENED_PLUGINS"]

After loading, each plugin entry in the flattened array includes metadata about which APIs it implements:

{
  resolve: "/path/to/plugin",
  name: "gatsby-source-filesystem",
  nodeAPIs: ["sourceNodes", "onCreateNode"],
  browserAPIs: [],
  ssrAPIs: [],
  pluginOptions: { path: "./src/data" },
}

Tip: The handleMultipleReplaceRenderers check (line 74) ensures that at most one plugin implements the replaceRenderer API. If multiple plugins try to replace the renderer (e.g., two different HTML rendering strategies), Gatsby surfaces a clear error at startup rather than failing at build time.

Internal Plugins as Architectural Examples

Gatsby uses its own plugin API extensively via internal plugins located in packages/gatsby/src/internal-plugins/. These are loaded automatically by loadInternalPlugins and serve as canonical examples of the plugin contract.

Internal Plugin Purpose
internal-data-bridge Creates Site and SitePage nodes from Redux state
dev-404-page Generates the development 404 page with route listing
prod-404-500 Generates production 404 and 500 error pages
load-babel-config Loads and merges Babel configuration
bundle-optimisations Configures webpack bundle splitting strategies
webpack-theme-component-shadowing Enables component shadowing for themes
functions Handles Gatsby Functions (serverless)
partytown Integrates Partytown for off-main-thread scripts

The webpack-theme-component-shadowing plugin is particularly instructive. Its gatsby-node.js at packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/gatsby-node.js is just 25 lines—it implements onCreateWebpackConfig to inject a custom webpack resolve plugin:

exports.onCreateWebpackConfig = ({ store, actions }) => {
  const { flattenedPlugins, program } = store.getState()
  actions.setWebpackConfig({
    resolve: {
      plugins: [
        new GatsbyThemeComponentShadowingResolverPlugin({
          extensions: program.extensions,
          themes: flattenedPlugins.map(plugin => ({
            themeDir: plugin.pluginFilepath,
            themeName: plugin.name,
          })),
          projectRoot: program.directory,
        }),
      ],
    },
  })
}

This is Gatsby eating its own dog food—the component shadowing feature is implemented as a plugin that hooks into the same onCreateWebpackConfig API available to any third-party plugin.

The Theme System: Recursive Config and Component Shadowing

Themes in Gatsby are plugins that have their own gatsby-config. This simple definition enables powerful composition.

Recursive Config Merging

When loadThemes encounters a plugin that has a gatsby-config, it recursively resolves that config's themes. Theme configs can be functions that receive theme options:

// In a theme's gatsby-config.js
module.exports = (themeOptions) => ({
  plugins: [
    { resolve: `gatsby-source-filesystem`, options: { path: themeOptions.contentPath } },
  ],
})

The resolution algorithm walks the theme tree depth-first, collects all configs, and merges them using mergeGatsbyConfig. This means a theme can depend on other themes, and those dependencies are resolved transitively.

flowchart TD
    A["User's gatsby-config"] --> B["resolveTheme('gatsby-theme-blog')"]
    B --> C["Theme's gatsby-config(options)"]
    C --> D["resolveTheme('gatsby-theme-core')"]
    D --> E["Core theme's gatsby-config"]
    E --> F["mergeGatsbyConfig(core, blog)"]
    F --> G["mergeGatsbyConfig(merged, user)"]
    G --> H["Final merged config"]

Component Shadowing

Component shadowing is the mechanism that makes themes customizable without forking. If a theme defines a component at gatsby-theme-blog/src/components/bio.js, a user site can override it by creating src/gatsby-theme-blog/components/bio.js.

This works through the webpack resolve plugin injected by the internal webpack-theme-component-shadowing plugin. When webpack resolves an import from a theme's src/ directory, the plugin checks if a shadow file exists in the user's src/{theme-name}/ directory and redirects the import if so.

Tip: Component shadowing follows a strict file path convention: src/{theme-name}/{path-within-theme-src}. The theme name must exactly match the plugin/theme package name. If you're not seeing your shadow applied, check for directory name mismatches.

Source and Transformer Plugin Patterns

Gatsby's data layer is populated by two categories of plugins that work in concert.

Source Plugins

Source plugins implement sourceNodes to create nodes from external data. The canonical example is gatsby-source-filesystem, which creates File nodes from the local filesystem.

Its implementation is interesting because it uses an XState machine internally (line 4) to manage chokidar's ready/not-ready states and flush queued file operations.

sequenceDiagram
    participant Core as Gatsby Core
    participant Source as gatsby-source-filesystem
    participant FS as File System
    participant Redux as Redux Store

    Core->>Source: sourceNodes({ actions, createNodeId })
    Source->>FS: chokidar.watch(path)
    loop For each file
        FS->>Source: file detected
        Source->>Source: createFileNode(path)
        Source->>Redux: actions.createNode(fileNode)
    end
    Note over Source: Machine transitions to "ready"
    Source-->>Core: Promise resolves

The pattern: use a file-watching library, create structured nodes with internal.type, internal.contentDigest, and metadata fields, then dispatch via createNode.

Transformer Plugins

Transformer plugins implement onCreateNode (or shouldOnCreateNode for filtering) to create child nodes from parent nodes. The canonical example is gatsby-transformer-remark:

const { onCreateNode, shouldOnCreateNode } = require(`./on-node-create`)
exports.onCreateNode = onCreateNode
exports.shouldOnCreateNode = shouldOnCreateNode
exports.createSchemaCustomization = require(`./create-schema-customization`)
exports.setFieldsOnGraphQLNodeType = require(`./extend-node-type`)

The shouldOnCreateNode export is a performance optimization—it lets Gatsby skip calling onCreateNode for nodes the plugin doesn't care about, avoiding the overhead of loading the full handler.

flowchart LR
    A["gatsby-source-filesystem"] -->|"Creates File nodes"| B["File node<br/>(mediaType: text/markdown)"]
    B -->|"onCreateNode"| C["gatsby-transformer-remark"]
    C -->|"Creates child"| D["MarkdownRemark node"]
    D -->|"setFieldsOnGraphQLNodeType"| E["html, excerpt,<br/>frontmatter fields"]

The parent-child relationship is key: the MarkdownRemark node references its parent File node, and Gatsby automatically adds childMarkdownRemark and childrenMarkdownRemark convenience fields to the File type via the @childOf schema extension.

Page Modes: SSG, DSG, and SSR

Gatsby supports three rendering strategies, determined by examining component exports. The resolution logic lives in packages/gatsby/src/utils/page-mode.ts:

flowchart TD
    A["Page component"] --> B{"Exports getServerData?"}
    B -->|Yes| C["SSR"]
    B -->|No| D{"Exports config?"}
    D -->|Yes| E{"config().defer === true?"}
    D -->|No| F{"page.defer === true?"}
    E -->|Yes| G["DSG"]
    E -->|No| H["SSG"]
    F -->|Yes| G
    F -->|No| H

The resolvePageMode function at lines 37–80 implements this decision tree:

Mode Trigger Build Behavior
SSG (Static Site Generation) Default Query runs at build time, HTML generated at build time
DSG (Deferred Static Generation) config export with defer: true Skipped at build time, generated on first request
SSR (Server-Side Rendering) getServerData export Query and HTML generated on every request

A crucial detail at lines 69–77: status pages (/404.html and /500.html) are force-set to SSG regardless of their component's exports, with a warning:

if (pageMode !== `SSG` && (page.path === `/404.html` || page.path === `/500.html`)) {
  reportOnce(`Status page "${page.path}" ignores page mode ("${pageMode}")...`)
  pageMode = `SSG`
}

This makes sense—error pages must be available immediately and can't depend on a running server.

As we saw in Part 2, the build pipeline uses page mode to filter queries: only SSG pages run their queries during gatsby build. DSG and SSR pages are handled by rendering engines bundled separately.

Deployment Adapters

The adapter system is Gatsby's answer to platform-agnostic deployment. Instead of hard-coding deployment targets, Gatsby builds abstract manifests that adapters transform into platform-specific configurations.

The type definitions in packages/gatsby/src/utils/adapter/types.ts define the contract:

export type Route = IStaticRoute | IFunctionRoute | IRedirectRoute
export type RoutesManifest = Array<Route>

The three route types map to how a URL should be handled:

Route Type Properties Maps To
IStaticRoute path, filePath, headers Static file serving
IFunctionRoute path, functionId, cache Serverless function invocation
IRedirectRoute path, toPath, status, headers HTTP redirect

The adapter manager in packages/gatsby/src/utils/adapter/manager.ts constructs these manifests from the build output. At line 49, the setAdapter function also validates compatibility:

flowchart TD
    A["Build Output"] --> B["Adapter Manager"]
    B --> C["RoutesManifest"]
    B --> D["FunctionsManifest"]
    B --> E["HeaderRoutes"]

    C --> F["Adapter.adapt()"]
    D --> F
    E --> F

    F --> G["Platform-specific config<br/>(e.g., _redirects, netlify.toml)"]

Adapters can declare feature support limitations. For example, an adapter might not support pathPrefix or might only support certain trailingSlash options. The manager checks these at lines 71–99 and warns about incompatibilities.

Adapters can also request that certain plugins be disabled (lines 111–119), which dispatches DISABLE_PLUGINS_BY_NAME to the Redux store. This is how platform adapters can gracefully replace plugins they supersede.

Tip: If you're building a custom adapter, start by examining gatsby-adapter-netlify in the monorepo. It's the reference implementation and demonstrates how to transform the abstract manifests into Netlify's _redirects and _headers files.

Series Conclusion

Over these five articles, we've traced Gatsby's architecture from the outermost shell (the global CLI) through the monorepo structure, the build pipeline, the XState develop server, the Redux/LMDB/GraphQL data layer, and finally the plugin and deployment systems.

A few themes emerge from this deep dive:

Separation of concerns is real. The CLI doesn't know about webpack. The state machine doesn't know about HTML generation. The plugin system doesn't know about LMDB. Each layer communicates through clean interfaces—Redux actions, service functions, manifest types.

The build/develop split is fundamental. These aren't two flavors of the same thing—they're different architectures for different problems. The sequential pipeline optimizes for throughput; the state machine optimizes for responsiveness.

Composition is the design philosophy. From Lerna packages to plugin APIs to theme shadowing to deployment adapters, every layer is designed to be composed, extended, and replaced. Even Gatsby's own features (error pages, filesystem routing, Babel configuration) are implemented as plugins.

Whether you're contributing to Gatsby core, building a plugin, or just trying to debug a build issue, knowing these architectural layers gives you a mental map for navigating one of the most complex open-source JavaScript projects ever built.