Extending Gatsby: The Plugin API, Themes, and Deployment Adapters
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: normalize → validate options → load internals → flatten → collate APIs → validate 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
handleMultipleReplaceRendererscheck (line 74) ensures that at most one plugin implements thereplaceRendererAPI. 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-netlifyin the monorepo. It's the reference implementation and demonstrates how to transform the abstract manifests into Netlify's_redirectsand_headersfiles.
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.