gatsbyjs/gatsby · error · Error

Pages can only be created by plugins. There wasn't a plugin

Error message

Pages can only be created by plugins. There wasn't a plugin set when creating this page.

What it means

The pages reducer's CREATE_PAGE case requires action.plugin?.name to be set, because every page must be attributable to the plugin (or the user's site) that created it - this drives ownership, telemetry, and later page-to-plugin reverse lookups. If the action reaches the reducer without a plugin (e.g. dispatched directly against the store bypassing the wrapped action creator), it logs the action JSON and throws.

Source

Thrown at packages/gatsby/src/redux/reducers/pages.ts:29

  state: IGatsbyState["pages"] = new Map<string, IGatsbyPage>(),
  action:
    | IDeleteCacheAction
    | ICreatePageAction
    | IDeletePageAction
    | IMaterializePageMode
): IGatsbyState["pages"] => {
  switch (action.type) {
    case `DELETE_CACHE`:
      return new Map()

    case `CREATE_PAGE`: {
      // throws an error if the page is not created by a plugin
      if (!action.plugin?.name) {
        console.log(``)
        console.error(JSON.stringify(action, null, 4))
        console.log(``)

        throw new Error(
          `Pages can only be created by plugins. There wasn't a plugin set when creating this page.`
        )
      }

      // Add page to the state with the path as key
      state.set(action.payload.path, action.payload)

      return state
    }

    case `DELETE_PAGE`: {
      state.delete(action.payload.path)

      return state
    }

    case `MATERIALIZE_PAGE_MODE`: {
      const page = state.get(action.payload.path)

View on GitHub (pinned to 8b06340921)

Solutions

  1. Always use the actions.createPage(...) helper passed into Gatsby Node APIs rather than dispatching CREATE_PAGE yourself.
  2. If dispatching manually in tests, include `plugin: { name: 'your-plugin' }` on the action.
  3. Make sure you are calling createPage from within a Gatsby-recognized hook (createPages, createPagesStatefully) so plugin context is bound.

Example fix

// before (test/internal)
store.dispatch({ type: `CREATE_PAGE`, payload: { path: `/`, component } })
// after
store.dispatch({ type: `CREATE_PAGE`, plugin: { name: `test` }, payload: { path: `/`, component } })
// or better - use the wrapped API in gatsby-node:
exports.createPages = ({ actions }) => actions.createPage({ path: `/`, component })
Defensive patterns

Strategy: validation

Validate before calling

// Always go through actions.createPage; if you must dispatch manually, set plugin.
function safeCreatePage(store, plugin, payload) {
  if (!plugin?.name) throw new Error('createPage requires plugin.name')
  store.dispatch({ type: `CREATE_PAGE`, plugin, payload })
}

Type guard

function hasPluginContext(action) {
  return !!action?.plugin?.name
}

Prevention

When it happens

Trigger: Dispatching a raw CREATE_PAGE action to the redux store without an action.plugin object (or with plugin.name falsy). The public actions.createPage wrapper injects the plugin automatically, so this typically means a plugin or test is bypassing the wrapper and dispatching manually.

Common situations: A plugin/test calling store.dispatch({ type: 'CREATE_PAGE', payload: {...} }) directly instead of using the provided `actions.createPage`; an internal code path that lost the plugin context; mocking/stubbing the redux store in tests without setting plugin.

Related errors


AI-assisted analysis of gatsbyjs/gatsby@8b06340921 (2026-08-13). Data as JSON: /api/errors/ec5cdfa513b51658. Report an issue: GitHub.