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! 💯
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!
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
.
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:
The Graph query returns an array of all pages.
query {
graph {
filename,
id,
label,
outputPath,
path,
route,
template,
title
}
}
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;
}
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.
The Children query returns an array of all pages below a given top level route.
query {
children {
id,
filename,
label,
outputPath,
path,
route,
template,
title
}
}
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;
}
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"
}
]
The Config query returns the configuration values from your greenwood.config.js. Useful for populating tags like <title>
and <meta>
.
query {
config {
devServer {
port
},
meta {
name,
rel,
content,
property,
value,
href
},
optimization,
port,
staticRouter,
title,
workspace
}
}
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;
}
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))
}
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!
/* 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!
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.
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
}