Multiple parrot pages for the same node type in Gatsby  📄 3️⃣

How can you achieve different page layouts for content with the same content node type but varying content types such as posts, projects, and pages?

The approach covered in yesterday's email used multiple page components; the day before was multiple page templates and today we are covering my current favorite: multiple parrot pages.

If you are not familiar with the term parrot pages, it's what we call files system routes around the Gatsby islands 🤪

Multiple Parrot Pages 🦜

The title is a little misleading as we cannot make multiple parrot pages for the same node type. But we'll use a nice little trick of creating new nodes named after the content type, resulting in the opportunity to make multiple parrot pages:

├─ pages/
│  ├─ post/
│  │  ├─ {Post.slug}.js
│  ├─ project/
│  │  ├─ {Project.slug}.js

We create new nodes by hooking into the onCreateNode extension point:

// gatsby-node.js
const { createFilePath } = require("gatsby-source-filesystem");

exports.onCreateNode = (gatsbyUtils) => {
  const { actions, node, getNode, reporter, createNodeId } = gatsbyUtils;
  const { createNode } = actions;

  if (node.internal.type === "MarkdownRemark") {
    const parentFileNode = getNode(node.parent);
    const contentType = parentFileNode?.sourceInstanceName || "";

    const contentNodeId = createNodeId(`${} >>> ${contentType}`);
    const slug = createFilePath({ node: parentFileNode, getNode });

      id: contentNodeId,
      slug: slug,
      childMarkdownRemark:, // 👈
      internal: {
        contentDigest: node.internal.contentDigest,
        type: contentType,
    });`${contentType} created for ${slug}`);

Take note of the childMarkdownRemark key added to the new node! It's what connects the new node to its content. However, if we query the new nodes at this point, we'll only get the node id.

How can we get the full content? By modifying their schema using @link:

// gatsby-node.js

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;

  const typeDefs = `
    type Post implements Node {
      slug: String
      childMarkdownRemark: MarkdownRemark @link
    type Project implements Node {
      slug: String
      childMarkdownRemark: MarkdownRemark @link


Now inside our parrot pages, we can query for the content on childMarkdownRemark:

// {Post.slug}.js

import React from "react";
import { graphql } from "gatsby";

const PostPage = ({ data }) => {
  const { html, frontmatter } =;
  const { title } = frontmatter;
  return (
        <section dangerouslySetInnerHTML={{ __html: html }} />

export default PostPage;

export const query = graphql`
  query ($id: String!) {
    post(id: { eq: $id }) {
      childMarkdownRemark {
        frontmatter {

Same as the other days, the sourceInstanceName used comes from configuring your gatsby-source-filesystem with the name option.

// gatsby-config.js
module.exports = {
  plugins: [
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/content/posts`,
        name: "Post",
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/content/projects`,
        name: "Project",

How do you solve different page layouts for content with the same content node type but varying content types?

All the best,
Queen Raae

Stuck on a reef in the sharky waters around the Gatsby islands?

Book a 1-on-1 Gatsby Call with Queen Raae. Friendly advice you can put into action immediately, guaranteed!

Serious about Gatsby? Sign up for emails sent every weekday to help you get the most out of Gatsby!