Thanks for contributing to the GreenwoodJS website! This document aims to help guide contributions into this project.
The layout of the project is as follows:
- /assets/ - Public assets like images and fonts used throughout the site
- /components/ - Custom Element web components to be used throughout the project
- /pages/ - File based routing as provided by Greenwood
- /stories/ - General developer documentation about the project for developers
- /styles/ - Global theme and styles
Note
Please review the documentation contained in this project's Storybook by running npm run story:dev
and going through the content in the Overview section.
Documentation changes specific to an in progress / unreleased feature in Greenwood should be made to the corresponding feature branch in this repository aligning with that Greenwood release. This can be determined at the time of submitting your PR to Greenwood in coordination with the maintainers.
For example, if the next release your feature is targeting for Greenwood is 1.1.0, the git workflow would be as follows:
$ git checkout release/1.1.0
$ git pull origin release/1.1.0
$ git checkout -b content/issue-xxx-the-feature
Where issue-xxx
is the corresponding issue in the GreenwoodJS project.
General changes to the website can be made by submitting a PR directly to the main branch. This includes typos, style changes, and general enhancements to the website as a whole.
There is a npm script that you can run that will check all relative links and hashes (except for blog pages) to check that links aren't broken. Running the command will build the site for production automatically and generate a report.
$ npm run lint:links
#...
✅ all links checked successfully and no broken links found
All global theming and general styles should go in src/styles/theme.css, like font family and CSS custom properties to be used throughout the site.
For anything that may not be easily "componentized" or is very general like for markdown based content, it should go in src/styles/main.css.
Note
Open Props are used in this project to provide a set of consistent and re-usable design system tokens. Please review these first before creating any new custom values or variables.
This project leverage Web Components (custom elements) as the mechanism for all templating and / or interactivity, with pre-rendering of the content occurring during the build (SSG).
export default class Greeting extends HTMLElement {
connectedCallback() {
// ...
}
}
// we use app- as the tag name prefix
customElements.define("app-greeting", Greeting);
Since most of the content for this project is static content, Light DOM based HTML is preferred, rendering directly into innerHTML
. For styling these components, a Greenwood based implementation of CSS Modules is used, that will link the class names at build time yet still emit static CSS in the <head>
of the page.
/* greeting.module.css */
.wrapper {
color: var(--color-primary);
box-shadow: var(--shadow-3);
}
import styles from "./greeting.module.css";
export default class Greeting extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<p class="${styles.wrapper}">Hello from the greeting component!</p>
`;
}
}
customElements.define("app-greeting", Greeting);
This would emit the following generated HTML
<app-greeting>
<p class="greeting-1234321-wrapper">Hello from the greeting component!</p>
</app-greeting>
Important
When adding these components to a page, we would want to optimize them as static; data-gwd-opt="static"
<script src="../components/greeting/greeting.js" type="module" data-gwd-opt="static">
For interactive components that would require client side interactivity, like event handlers, these components should be authored rendering into a Shadow Root using Declarative Shadow DOM and with Greenwood's raw plugin.
/* card.css */
.card {
color: var(--color-primary);
box-shadow: var(--shadow-3);
}
import sheet from "./card.css" with { type: "css" };
export default class Card extends HTMLElement {
selectItem() {
// do the thing
}
connectedCallback() {
if (!this.shadowRoot) {
const thumbnail = this.getAttribute("thumbnail");
const title = this.getAttribute("title");
const template = document.createElement("template");
template.innerHTML = `
<div class="card">
<h3>${title}</h3>
<img src="${thumbnail}" alt="${title}" loading="lazy" width="100%">
<button>View Item Details</button>
</div>
`;
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
this.shadowRoot.adoptedStyleSheets = [sheet];
this.shadowRoot?.querySelector("button").addEventListener("click", this.selectItem.bind(this));
}
}
customElements.define("app-card", Card);
This would emit the following generated HTML
<app-card title="My Title" thumbnail="/image.png">
<template shadowrootmode="open">
<div>
<h3>My Title</h3>
<img src="/image.png" alt="My Title" loading="lazy" width="100%" />
<button>View Item Details</button>
</div>
</template>
</app-card>
Tip
If the component does not need client side JavaScript, use a Light DOM component. If it will need client side JavaScript, use a Shadow DOM component.
For each component, a testing file ("spec") should be included to test basic functionality, living alongside the component in its directory.
components/
my-component/
my-component.js
my-component.spec.js
Each component will require a DOM-like environment in order to be queried for the custom element and, thus assert on the component details.
The following is an example of a spec file, with imports and a set of describe
/it
blocks. In this case, we import the add
method from the math.js
file, and assert that it provides an expected result.
import { expect } from "@esm-bundle/chai";
import { add } from "./math.js";
describe("Add Function", () => {
it("should do something expected", () => {
expect(add(1, 2)).equal(3);
});
});
With the spec file foundation, the custom element can now be created and applied to the mock DOM environment. To do this, we use the Web Test Runner (WTR) framework which provides the scaffolding to test Web Componets in a browser environment.
The WTR and the full setup is covered in greater detail within the GreenwoodJS test runner docs. In an attempt to not duplicate those docs, an outline has been provided here, but please refer to those docs for a full user guide.
-
Install dependencies and configure WTR. This project has that covered already! ✅ (Let's chat if there becomes a need to reconfigure the current setup.)
-
Within a traditional
before
block, create the custom element, and append to the DOM. See the example below. 👇 -
If experiencing a 404 for a missing asset, a simple middleware can help mock that request. Otherwise, you can skip this section.
-
If using a Greenwood resource plugin, you'll need to provide that info to stub out the signiture.
-
If using one of Greenwood's Content as Data Client APIs, mocking
fetch
with mock data is necessary.
With the test setup complete, we can now create the block necessary to put the custom element under test.
Populating the previous example, we include the before
block and create, customtize, and append the element using the Browser API.
import { expect } from "@esm-bundle/chai";
import "./my-custom-element.js";
// ...mocks truncated....
describe("Components/My Custom Element", () => {
let myElement;
before(async () => {
// create.
myElement = document.createElement("app-my-element"); // the tagname as it appears in the browser
// customize.
myElement.addAttribute("route", "/some/path/to/a/page");
// append.
document.body.appendChild(myElement);
// all ready.
await myElement.updateComplete;
});
after(() => {
// cleanup.
myElement.remove();
myElement = null;
});
it("should do something expected", () => {
expect("something").equal("some" + "thing");
});
// ...it()...
});
For each component, a Storybook file should be included to demonstrate basic functionality, living alongside the component in its directory. Generally a default story should be sufficient.
components/
my-component/
my-component.js
my-component.stories.js
Below is an example of a basic Storybook file:
import "./header.js";
export default {
title: "Components/Header",
};
const Template = () => "<app-header></app-header>";
export const Primary = Template.bind({});
When a component implements one of Greenwood's Content as Data Client APIs, the story will need to mock this request before being able to render within the Storybook.
See the Greenwood Storybook docs for more information and the blog-posts-list.stories.js story for an example in this project.
This project is hosted on Netlify and automatically deploys on each merge into main. Release branches will be curated over the course of a Greenwood release cycle and then merged at the time the new Greenwood release is published to NPM.