Copying The Plone Site

Now we have seen how the plugin and starter work together.

The end goal of the plugin along with GatsbyJS is to generate a static site which is an exact copy of the Plone site it sourced data from.

Once gatsby-source-plugin retrieves all the required data for us, the gatsby-starter-plone processes and uses this data to generate the static site.

Internally what gatsby-starter-plone does in steps is:

  • Create pages for each content object and ensure tree structure by retaining parent and items relationships.
  • Display HTML, images and files in content objects correctly.
  • Handle navigation in the site with a Navbar and Breadcrumbs.

Page Creation

exports.createPages is the API used in gatsby-node.js for creating pages from nodes.

To create pages for all nodes, first we query the list of all different types of nodes and use this list to create individual pages.

To illustrate how this process works, let us create pages for all nodes of type PloneFolder.

Later we can move on to include all the other types.

In gatsby-node.js:

const path = require('path');

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;

  // Get data via GraphQL
  const result = await graphql(`
    {
      allPloneFolder {
        edges {
          node {
            _path
          }
        }
      }
    }
  `);

  // Create pages for each PloneFolder item
  result.data.allPloneFolder.edges.forEach(({ node }) => {
    createPage({
      path: node._path,
      component: path.resolve('./src/templates/Folder.js'),
    });
  });
};

The _path property is helpful to create pages. It is unique to all nodes. Files and images are linked to the nodes in which they are present via the _path value.

This backlinking will be explained in the later sections.

Backlinking can be used to set the relative path of a node in the generated static site. It can also get the required images and files.

Handling Different Data Types

For generating pages for each content object, the same process as illustrated by the code above can be followed. The only major change would be that we would be using a default.js template instead and separate components for each type. This would internally check what type of node is being used for page creation and return the matching component.

In gatsby-node.js query for all data types:

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  const result = await graphql(`
    {
      allPloneDocument {
        edges {
          node {
            _path
          }
        }
      }
      allPloneEvent {
        edges {
          node {
            _path
          }
        }
      }
      allPloneFolder {
        edges {
          node {
            _path
          }
        }
      }
      allPloneNewsItem {
        edges {
          node {
            _path
          }
        }
      }
    }
  `);
  []
    .concat(
      result.data.allPloneDocument.edges,
      result.data.allPloneEvent.edges,
      result.data.allPloneFolder.edges,
      result.data.allPloneNewsItem.edges
    )
    .forEach(({ node }) => {
      createPage({
        path: node._path,
        component: path.resolve('./src/templates/default.js'),
      });
    });
  }

The default.js template:

const componentFor = data => {
  if (data) {
    if (data.ploneCollection) {
      return (
        <Folder
          data={data.ploneCollection}
        />
      );
    } else if (data.ploneDocument) {
      return (
        <Document
          data={data.ploneDocument}
        />
      );
    } else if (data.ploneEvent) {
      return (
        <Event
          data={data.ploneEvent}
        />
      );
    } else if (data.ploneFolder) {
      return (
        <Folder
          data={data.ploneFolder}
        />
      );
    } else if (data.ploneNewsItem) {
      return (
        <NewsItem
          data={data.ploneNewsItem}
        />
      );
    } else {
      return null;
    }
  } else {
    return null;
  }
};

const DefaultLayout = ({ data }) => <Layout>{componentFor(data)}</Layout>;

// Query for all the different types from GraphQL
// Fragments for each type are defined in their relevant components
export const query = graphql`
  query DefaultTemplateQuery($path: String!) {
    ploneCollection(_path: { eq: $path }) {
      ...Collection
    }
    ploneDocument(_path: { eq: $path }) {
      ...Document
    }
    ploneEvent(_path: { eq: $path }) {
      ...Event
    }
    ploneFolder(_path: { eq: $path }) {
      ...Folder
    }
    ploneNewsItem(_path: { eq: $path }) {
      ...NewsItem
    }
  }
`;

To understand what happens in the components, let us take the example of the Folder component:

import React from 'react';
import { graphql, Link } from 'gatsby';

const Folder = ({ data, title }) => (
  <nav key={data._id}>
    <h1>{title ? title : data.title}</h1>
    <p>
      <strong>{data.description}</strong>
    </p>
    <ul>
      {data.items.filter(item => item.title).map(item => (
        <li key={item._path}>
          <Link to={item._path}>{item.title}</Link>
        </li>
      ))}
    </ul>
  </nav>
);

export default Folder;

export const query = graphql`
  fragment Folder on PloneFolder {
    _id
    title
    description
    items {
      _path
    }
    _path
  }
`;

Here, the fragment is used by default.js to get the relevant data of the Folder content object and is passed in to the Folder component as data.

Note

Fragments are reusable GraphQL queries. It also allows you to split up complex queries into smaller, easier to understand components.

In our case, even though all data is queried in default.js template, we split up the queries by type and place them in the relevant component. These fragments are included back in the template as required. This helps in maintainability as all the parts of a component, including the query, are placed together.

The Folder component now displays the title and description of the Folder itself and a list of child items.

Note

With the Link component and _path we can directly link between GatsbyJS pages.