Data Sources

Overview

Having to repeat things when programming is no fun, and that's why (web) component based development is so useful! As websites start to grow, there comes a point where being able to have access to the content and structure of your site's layout and configuration as part of the development process becomes essential towards maintainability, performance, and scalability.

As an example, if you are developing a blog site, like in our Getting Started guide, having to manually list a couple of blog posts by hand isn't so bad.

<ul>
  <li><a href="/blog/2019/first-post.md">First Post</li></a>
  <li><a href="/blog/2019/second-post.md">Second Post</li></a>
</ul>

But what happens over time, when that list grows to 10, 50, 100+ posts? Imagine maintaining that list each time, over and over again? Or just remembering to update that list each time you publish a new post? Not only that, but wouldn't it also be great to sort, search, filter, and organize those posts to make them easier for users to navigate and find?

So instead of a static list, you can do something like this!

render() {
  return html`
    <ul>
      ${pages.map((page) => {
        return html`
          <li><a href="${page.path}">${page.title}</a></li>
        `;
      })}
    </ul>
  `;
}

To assist with this, Greenwood provides all your content as data, accessible from a single graph.json file that you can simply fetch RESTfully or, if you install our plugin for GraphQL, you can use a GraphQL from your client side code instead! 💯

Internal Sources

Greenwood (via plugin-graphql) exposes an Apollo server locally available at localhost:4000 as well as a custom Apollo client that can both be used when developing to get information about your local content like path, "slug", title and other useful information that will be dynamic to the content you create. Programmatic access to this data can provide you the opportunity to share your content with your users in a way that supports sorting, filter, organizing, and more!

graphql-playground

Schema

To kick things off, let's review what is available to you. Currently, the main "API" is just a list of all pages in your pages/ directory, represented as a Page type definition. This is called Greenwood's graph.

This is what the schema looks like:

graph {
  filename, // (string) base filename

  id, // (string) filename without the extension

  label, // (string) best guess pretty text / display based on filename

  outputPath, // (string) the relative path to write to when generating static HTML

  relativeWorkspacePagePath, // the file path relative to the user's workspace directory

  path, // (string) path to the file

  route,  // (string) A URL, typically derived from the filesystem path, e.g. /blog/2019/first-post/

  template, // (string) page template used for the page

  title,  // (string) Useful for a page's <title> tag or the title attribute for an <a> tag, inferred from the filesystem path, e.g. "First Post" or provided through front matter.
}

All queries return subsets and / or derivatives of the graph.

Queries

To help facilitate development, Greenwood provides a couple queries out of the box that you can use to get access to the graph and start using it in your components, which we'll get to next.

Below are the queries available:

Graph

The Graph query returns an array of all pages.

Definition
query {
  graph {
    filename,
    id,
    label,
    outputPath,
    path,
    route,
    template,
    title
  }
}
Usage

import the query in your component

import client from '@greenwood/plugin-graphql/src/core/client.js';
import GraphQuery from '@greenwood/plugin-graphql/src/queries/menu.gql';

.
.
.

async connectedCallback() {
  super.connectedCallback();
  const response = await client.query({
    query: GraphQuery
  });

  this.posts = response.data.graph;
}
Response

This will return the full graph of all pages as an array

[
  {
    filename: "index.md",
    id: "index",
    label: "Index",
    outputPath: "index.html",
    path: "./index.md",
    route: "/",
    template: "page",
    title: "Home Page"
  }, {
    filename: "first-post.md",
    id: "first-post",
    label: "First Post",
    outputPath: "/blog/2019/first-post/index.html",
    path: "./blog/2019/first-post.md",
    route: "/blog/2019/first-post",
    template: "blog",
    title: "My First Blog Post"
  },
  {
    filename: "second-post.md",
    id: "second-post",
    label: "Second Post",
    outputPath: "/blog/2019/second-post/index.html",
    path: "./blog/2019/second-post.md",
    route: "/blog/2019/second-post",
    template: "blog",
    title: "My Second Blog Post"
  }
]

See Menus for documentation on querying for custom menus.

Children

The Children query returns an array of all pages below a given top level route.

Definition
query {
  children {
    id,
    filename,
    label,
    outputPath,
    path,
    route,
    template,
    title
  }
}
Usage

import the query in your component

import client from '@greenwood/plugin-graphql/src/core/client.js';
import ChildrenQuery from '@greenwood/plugin-graphql/src/queries/menu.gql';

.
.
.

async connectedCallback() {
  super.connectedCallback();
  const response = await client.query({
    query: ChildrenQuery,
    variables: {
      parent: '/blog'
    }
  });

  this.posts = response.data.children;
}
Response

This will return the full graph of all pages as an array that are under a given root, e.g. /blog.

[
  {
    filename: "first-post.md",
    id: "first-post",
    label: "First Post",
    outputPath: "/blog/2019/first-post/index.html",
    path: "./blog/2019/first-post.md",
    route: "/blog/2019/first-post",
    template: "blog",
    title: "My First Blog Post"
  },
  {
    filename: "second-post.md",
    id: "second-post",
    label: "Second Post",
    outputPath: "/blog/2019/second-post/index.html",
    path: "./blog/2019/second-post.md",
    route: "/blog/2019/second-post",
    template: "blog",
    title: "My Second Blog Post"
  }
]
Config

The Config query returns the configuration values from your greenwood.config.js. Useful for populating tags like <title> and <meta>.

Definition
query {
  config {
  	devServer {
      port
    },
    meta {
      name,
      rel,
      content,
      property,
      value,
      href
    },
    optimization,
    port,
    staticRouter,
    title,
    workspace
  }
}
Usage

import the query in your component

import client from '@greenwood/plugin-graphql/src/core/client.js';
import ConfigQuery from '@greenwood/plugin-graphql/src/queries/menu.gql';
.
.
.

async connectedCallback() {
  super.connectedCallback();
  const response = await client.query({
    query: ConfigQuery
  });

  this.meta = response.data.config.meta;
}
Response

This will return an object of your greenwood.config.js as an object. Example:

{
  devServer: {
    port: 1984
  },
  meta: [
    { name: 'twitter:site', content: '@PrjEvergreen' },
    { rel: 'icon', href: '/assets/favicon.ico' }
  ],
  title: 'My App',
  workspace: 'src' // equivalent to => fileURLToPath(new URL('./www', import.meta.url))
}
Custom

You can of course come up with your own as needed! Greenwood provides the gql-tag module and will also resolve .gql or .graphql file extensions!

example:
/* src/data/my-query.gql */
query {
  graph {
    /* whatever you are looking for */
  }
}

Or within your component

import gql from 'graphql-tag';  // comes with Greenwood

const query = gql`
  {
    user(id: 5) {
      firstName
      lastName
    }
  }
`

Then you can use import anywhere in your components!

Complete Example

Now of course comes the fun part, actually seeing it all come together. Here is an example from the Greenwood website's own header component.

import { LitElement, html } from 'lit';
import client from '@greenwood/plugin-graphql/src/core/client.js';
import MenuQuery from '@greenwood/plugin-graphql/src/queries/menu.gql';

class HeaderComponent extends LitElement {

  static get properties() {
    return {
      navigation: {
        type: Array
      }
    };
  }

  constructor() {
    super();
    this.navigation = [];
  }

  async connectedCallback() {
    super.connectedCallback();

    const response = await client.query({
      query: MenuQuery,
      variables: {
        name: 'navigation'
      }
    });

    this.navigation = response.data.menu.children.map(item => item.item);
  }

  render() {
    const { navigation } = this;

    return html`
      <header class="header">

        <nav>
          <ul>
            ${navigation.map(({ item }) => {
              return html`
                <li><a href="${item.route}" title="Click to visit the ${item.label} page">${item.label}</a></li>
              `;
            })}
          </ul>
        </nav>

      </header>
    `;
  }
}

customElements.define('app-header', HeaderComponent);

For more information on using GraphQL with Greenwood, please see our GraphQL plugin's README.

External Sources

Using our Source plugin, just as you can get your content as data out of Greenwood, so can you provide your own sources of data to Greenwood. This is great for pulling content from a headless CMS, database, or anything else you can imagine!

The supported fields from Greenwood's schema are:

graph {
  body, // REQUIRED (string of your content)
  id,
  label,
  route,  // REQUIRED and MUST end in a forward slash
  template,
  title,
  data
}