diff --git a/greenwood.config.js b/greenwood.config.js index f0760558..00a865c7 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -1,5 +1,39 @@ import { greenwoodPluginCssModules } from "@greenwood/plugin-css-modules"; import { greenwoodPluginImportRaw } from "@greenwood/plugin-import-raw"; +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; + +// TODO would be nice to find a better way to solve this problem +// https://github.com/ProjectEvergreen/www.greenwoodjs.dev/issues/125 +class ActiveFrontmatterDocsTitleRestorerResource extends ResourceInterface { + constructor() { + super(); + this.extensions = ["html"]; + this.contentType = "text/html"; + this.matches = ["My Blog - Active Frontmatter", "My Site - Going Further"]; + this.replacer = "${globalThis.page.title}"; + } + + async shouldIntercept(url, request, response) { + return response.headers.get("Content-Type")?.indexOf(this.contentType) >= 0; + } + + async intercept(url, request, response) { + let body = await response.text(); + + this.matches.forEach((match) => { + if (body.indexOf(match) > 0) { + const titleParts = match.split("-"); + + body = body.replace( + new RegExp(String.raw`${match}`, "g"), + `${titleParts[0]}- ${this.replacer}`, + ); + } + }); + + return new Response(body); + } +} export default { activeContent: true, @@ -10,7 +44,15 @@ export default { plugins: ["@mapbox/rehype-prism", "rehype-slug", "rehype-autolink-headings", "remark-github"], }, - plugins: [greenwoodPluginCssModules(), greenwoodPluginImportRaw()], + plugins: [ + greenwoodPluginCssModules(), + greenwoodPluginImportRaw(), + { + name: "active-frontmatter-docs-title-restorer-resource", + type: "resource", + provider: (compilation) => new ActiveFrontmatterDocsTitleRestorerResource(compilation), + }, + ], polyfills: { importAttributes: ["css", "json"], diff --git a/src/assets/docs/graphql-playground.png b/src/assets/docs/graphql-playground.png new file mode 100644 index 00000000..825e9ab9 Binary files /dev/null and b/src/assets/docs/graphql-playground.png differ diff --git a/src/components/heading-box/heading-box.module.css b/src/components/heading-box/heading-box.module.css index 613b975f..ef296b77 100644 --- a/src/components/heading-box/heading-box.module.css +++ b/src/components/heading-box/heading-box.module.css @@ -17,4 +17,8 @@ .slotted { margin: var(--size-4) 0 var(--size-1); + + & a { + color: var(--color-accent); + } } diff --git a/src/components/side-nav/side-nav.js b/src/components/side-nav/side-nav.js index adc08b82..1c002e22 100644 --- a/src/components/side-nav/side-nav.js +++ b/src/components/side-nav/side-nav.js @@ -29,6 +29,10 @@ export default class SideNav extends HTMLElement { } }); + if (sections.length === 0) { + return; + } + this.innerHTML = `
${sections diff --git a/src/components/side-nav/side-nav.module.css b/src/components/side-nav/side-nav.module.css index 77b2c513..a5308b1b 100644 --- a/src/components/side-nav/side-nav.module.css +++ b/src/components/side-nav/side-nav.module.css @@ -18,6 +18,7 @@ padding: 0 12px; width: 100%; text-align: right; + color: var(--color-black); } .compactMenuPopoverTrigger { @@ -60,7 +61,7 @@ padding: 0 var(--size-2); min-width: 200px; display: inline-block; - box-shadow: var(--shadow-3); + box-shadow: var(--shadow-2); margin-left: -16px; } } diff --git a/src/components/table-of-contents/table-of-contents.module.css b/src/components/table-of-contents/table-of-contents.module.css index 68c9a560..fc28b01c 100644 --- a/src/components/table-of-contents/table-of-contents.module.css +++ b/src/components/table-of-contents/table-of-contents.module.css @@ -26,7 +26,7 @@ min-height: 150px; margin: 0 var(--size-2); border: 1px solid var(--color-gray); - box-shadow: var(--shadow-3); + box-shadow: var(--shadow-2); padding: var(--size-2); } @@ -75,6 +75,7 @@ padding: 0 12px; width: 100%; text-align: right; + color: var(--color-black); } @media (min-width: 1200px) { @@ -83,7 +84,7 @@ border-left: 1px solid var(--color-gray); border-top: 1px solid var(--color-gray); border-bottom: 1px solid var(--color-gray); - box-shadow: var(--shadow-3); + box-shadow: var(--shadow-2); padding: var(--size-2) 0 0 var(--size-7); & a, diff --git a/src/layouts/docs.html b/src/layouts/docs.html new file mode 100644 index 00000000..c3870105 --- /dev/null +++ b/src/layouts/docs.html @@ -0,0 +1,36 @@ + + + Greenwood - ${globalThis.page.title} + + + + + + + + + + + +
+ + +
+ + diff --git a/src/layouts/guides.html b/src/layouts/guides.html index 17a2c079..f797d4c5 100644 --- a/src/layouts/guides.html +++ b/src/layouts/guides.html @@ -17,158 +17,7 @@ src="../components/edit-on-github/edit-on-github.js" data-gwd-opt="static" > - + diff --git a/src/pages/docs/content-as-data/active-frontmatter.md b/src/pages/docs/content-as-data/active-frontmatter.md new file mode 100644 index 00000000..7c3ea8f7 --- /dev/null +++ b/src/pages/docs/content-as-data/active-frontmatter.md @@ -0,0 +1,101 @@ +--- +layout: docs +order: 4 +tocHeading: 2 +--- + +# Active Frontmatter + +Active Frontmatter enables the ability to apply static substitutions in your pages and layouts based on the frontmatter content of your pages, and inspired by JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) syntax. + +Really useful for passing page content or collections as attributes to a custom element. + +## Usage + +Say for instance we want to + +```html + + + + Home Page + + + + + + + +``` + +Or given some frontmatter in a markdown file: + +```md +--- +layout: post +title: Git Explorer +published: 04.07.2020 +description: Local git repository viewer +author: Owen Buckley +image: /assets/blog-post-images/git.png +--- +``` + +It can be accessed and substituted statically in either markdown: + +```md +# My Blog Post + +Published: ${globalThis.page.data.published} + +Lorum Ipsum. +``` + +Or HTML: + +```html + + + My Blog - ${globalThis.page.title} + + + + + + + + + + + +``` + +## Data Client + +You can also access this content using our data client. + +```js +import { getContentByCollection } from "@greenwood/cli/src/data/client.js"; + +export default class Navigation extends HTMLElement { + async connectedCallback() { + const items = await getContentByCollection("nav"); + + this.innerHTML = ` + + `; + } +} +``` diff --git a/src/pages/docs/content-as-data/collections.md b/src/pages/docs/content-as-data/collections.md new file mode 100644 index 00000000..321b9513 --- /dev/null +++ b/src/pages/docs/content-as-data/collections.md @@ -0,0 +1,72 @@ +--- +layout: docs +order: 3 +tocHeading: 2 +--- + +# Collections + +Collections are a feature in Greenwood by which you can use [frontmatter](/docs/resources/markdown#frontmatter) to group pages that can the be referenced through JavaScript or [`activeFrontmatter`](/docs/configuration/#active-frontmatter). + +This can be a useful way to group pages for things like navigation menus based on the content in your pages directory. + +## Usage + +To define a collections, just add a **collection** property to the frontmatter of any static file: + +```md +--- +collection: nav +order: 2 +--- + +# About Page +``` + +You can now a reference to that collection either in HTML using [`activeFrontmatter`](/docs/content-as-data/active-frontmatter/): + +```html + + + Home Page + + + + + + + +``` + +Or programmatically in your JavaScript using our [**Data Client**](/docs/content-as-data/data-client/): + +```js +import { getContentByCollection } from "@greenwood/cli/src/data/client.js"; + +export default class Nav extends HTMLElement { + async connectedCallback() { + // sort based on frontmatter order set in your markdown + const navItems = (await getContentByCollection("nav")).sort((a, b) => + a.data.order > b.data.order ? 1 : -1, + ); + + this.innerHTML = ` + + `; + } +} + +customElements.define("x-nav", Nav); +``` diff --git a/src/pages/docs/content-as-data/data-client.md b/src/pages/docs/content-as-data/data-client.md new file mode 100644 index 00000000..b3c6050a --- /dev/null +++ b/src/pages/docs/content-as-data/data-client.md @@ -0,0 +1,111 @@ +--- +layout: docs +order: 2 +tocHeading: 2 +--- + +# Data Client + +To access your content as data with Greenwood, there are three pre-made APIs you can use, based on your use case. These are isomorphic in that they will consume live data during development, and statically build out each query at build time to its own JSON file that can be fetched client side independently. + +This way, you can serialize and / or hydrate from this data as needed based on your application's needs. + +> These features works best when used for build time templating combining our [**prerender**](/docs/configuration/#prerender) and [**static** optimization](/docs/configuration/#optimization) configurations. + +## Content + +To get every page back in one array, simple call `getContent` + +```js +// get turn the entire set of pages as an array + +import { getContent } from "@greenwood/cli/src/data/client.js"; + +const pages = await getContent(); + +pages.forEach((page) => console.log(page.title)); +``` + +## Content By Route + +To narrow down a set of pages by an entire route, you can call `getContentByRoute` and pass the route as the first argument. + +Below is an example of generating a list of all pages starting with a route of _/blog/_: + +```js +import { getContentByRoute } from "@greenwood/cli/src/data/queries.js"; + +export default class BlogPostsList extends HTMLElement { + async connectedCallback() { + const posts = (await getContentByRoute("/blog/")) + // we sort in reverse chronologic order, e.g. last in, first out (LIFO) + .sort((a, b) => + new Date(a.data.published).getTime() > new Date(b.data.published).getTime() ? -1 : 1, + ); + + this.innerHTML = ` + + `; + } +} + +customElements.define("blog-posts-list", BlogPostsList); +``` + +## Content By Collection + +To get access to [**Collections**](/docs/content-as-data/collections/), you can use `getContentByCollection` and pass the collection name as the first argument. + +Below is an example of using a collection to generate the navigation items for a header menu, using a custom frontmatter to define the **order**: + +```js +import { getContentByCollection } from "@greenwood/cli/src/data/client.js"; + +export default class Header extends HTMLElement { + async connectedCallback() { + // sort based on frontmatter order set in your markdown + const navItems = (await getContentByCollection("nav")).sort((a, b) => + a.data.order > b.data.order ? 1 : -1, + ); + + this.innerHTML = ` +
+ +
+ `; + } +} + +customElements.define("x-header", Header); +``` + +## Integrations + +If you're using Greenwood's Data Client with additional component development and testing tooling, like [**Storybook**](/guides/ecosystem/storybook/) or [**Web Test Runner**](/guides/ecosystem/web-test-runner/), please see our ecosystem guides for more information and support. diff --git a/src/pages/docs/content-as-data/graph-ql.md b/src/pages/docs/content-as-data/graph-ql.md new file mode 100644 index 00000000..80d80a81 --- /dev/null +++ b/src/pages/docs/content-as-data/graph-ql.md @@ -0,0 +1,13 @@ +--- +title: GraphQL +label: GraphQL +layout: docs +order: 5 +tocHeading: 2 +--- + +# GraphQL + +For GraphQL support, please see our [**GraphQL plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-graphql) which in additional to exposing an [Apollo server and playground](https://www.apollographql.com/docs/apollo-server/) locally at `http://localhost:4000`, also provides GraphQL alternatives to our [Data Client](/docs/content-as-data/data-client/) through a customized (read only) Apollo client based wrapper. + +![graphql-playground](/assets/docs/graphql-playground.png) diff --git a/src/pages/docs/content-as-data/index.md b/src/pages/docs/content-as-data/index.md new file mode 100644 index 00000000..fc532ac3 --- /dev/null +++ b/src/pages/docs/content-as-data/index.md @@ -0,0 +1,25 @@ +--- +layout: docs +order: 5 +--- + + +

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 programmatically becomes incredibly useful for generating repetitive HTML.

+
+ +If you are developing a blog site, like in our [Getting Started](/guides/getting-started/) guide, having to manually list a couple of blog posts by hand isn't so bad. + +```html + +``` + +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? + +To assist with this, Greenwood provides a set of "content as data" capabilities on the left sidebar you can take advantage of. + +> First thing though, make sure you've set the [`activeContent`](/docs/configuration/#active-content) flag to `true` in your _greenwood.config.js_. +> +> These features works best when used for build time templating combining our [**prerender**](/docs/configuration/#prerender) and [**static** optimization](/docs/configuration/#optimization) configurations. diff --git a/src/pages/docs/content-as-data/pages-data.md b/src/pages/docs/content-as-data/pages-data.md new file mode 100644 index 00000000..d6f5a56a --- /dev/null +++ b/src/pages/docs/content-as-data/pages-data.md @@ -0,0 +1,101 @@ +--- +layout: docs +order: 1 +tocHeading: 2 +--- + +# Pages Data + +To get started with, let's review the kind of content you can get as data. + +## Schema + +Each page will return data in the following schema: + +- **id** - a unique kebabe-case transformation of the filename +- **title** (customizable) - inferred title based on the filename +- **label** (customizable) - inferred from the **title** if not configured +- **route** - the filename converted into a path as per file based routing +- **data** (customizable) - any custom frontmatter keys you've added to your page + +So for a page at _src/pages/blog/first-post.md_, this is the data you would get back: + +```md +--- +author: Project Evergreen +published: 2024-01-01 +--- + +# First Post + +This is my first post. +``` + +```json +{ + "id": "blog-first-post", + "title": "First Post", + "label": "First Post", + "route": "/blog/first-post/", + "data": { + "author": "Project Evergreen", + "published": "2024-01-01" + } +} +``` + +## Table of Contents + +Additionally for markdown pages, you can add a frontmatter property called `tocHeading` that will read all the HTML heading tags that match that number, and provide a subset of data, useful for generated a table of contents. + +Taking our previous example, if we were to configure this for `

` tags: + +```md +--- +author: Project Evergreen +published: 2024-01-01 +tocHeading: 2 +--- + +# First Post + +This is my first post. + +## Overview + +Lorum Ipsum + +## First Point + +Something something... +``` + +We would get this additional content as data out: + +```json +{ + "id": "blog-first-post", + "title": "First Post", + "label": "First Post", + "route": "/blog/first-post/", + "data": { + "author": "Project Evergreen", + "published": "2024-01-01", + "tocHeading": 2, + "tableOfContents": [ + { + "content": "Overview", + "slug": "overview" + }, + { + "content": "First Point", + "slug": "first-post" + } + ] + } +} +``` + +## External Content + +Using our [Source plugin](/docs/reference/plugins-api/#source), just as you can get your content as data _out_ of Greenwood, so can you provide your own sources of content (as data) _to_ Greenwood. This is great for pulling content from a headless CMS, database, or anything else you can imagine! diff --git a/src/pages/docs/index.md b/src/pages/docs/index.md new file mode 100644 index 00000000..8678c86b --- /dev/null +++ b/src/pages/docs/index.md @@ -0,0 +1,17 @@ +--- +title: Docs +layout: docs +--- + + +

Welcome to our docs, we're excited to help you get the most out of Greenwood and the web!

+
+ +The content is broken down across these sections: + +- [Introduction](/docs/introduction/) - An intro to Greenwood, its philosophy, and how it install it +- [Pages](/docs/hosting/) - Learn about file-based routing and how to leverage SSR pages, API routes, and more +- [Resources](/docs/resources/) - Scripts? Styles? Fonts and images? This section has you covered +- [Plugins](/docs/plugins/) - Check out all plugins created by the Greenwood team +- [Content as Data](/docs/content-as-data/) - See Greenwood's capabilities for leveraging your content programmatically +- [Reference](/docs/reference/) - Configuration options, Plugin API docs, and other useful content for help you to build your project diff --git a/src/pages/docs/introduction/about.md b/src/pages/docs/introduction/about.md new file mode 100644 index 00000000..51edc839 --- /dev/null +++ b/src/pages/docs/introduction/about.md @@ -0,0 +1,45 @@ +--- +layout: docs +order: 1 +tocHeading: 2 +--- + +# About + +Greenwood's goal is to bring the power of the modern web right to your fingertips while staying out of your way, all backed by useful and minimal conventions that help you get the most out of the web platform. For those who want the option to just start with an HTML file, Greenwood aims to be a faithful and authentic gateway to web friendly runtimes everywhere, allowing you to leverage your knowledge of the web across the stack. It's why we consider Greenwood your _**workbench**_ for the web, and not a framework per se; we don't mind going vanilla! 🍦 + +> Whether you are building a blog or a single page application, a hobby project or passion project, we encourage you to give Greenwood a try! + +## Features + +Some of Greenwood's feature include: + +- Unbundled local development workflow, using `E-Tags` headers for efficient caching and live reloads +- Out of the box support for ESM and Web APIs, on both the client and server +- Server Side Rendering of Web Components (Light and Shadow DOM) +- File-based routing, including API Routes +- Automatic code splitting through user defined ` + + + + + +``` + +## URL + +The [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) constructor provides an elegant way for referencing [static assets](/docs/resources/assets/) on the client and on the server, and it works great when combined with [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) for easily interacting with search params in a request. + +Here is an example of some of these APIs in action in an API Route handler: + +```js +export async function handler(request) { + const params = new URLSearchParams(request.url.slice(request.url.indexOf("?"))); + const name = params.has("name") ? params.get("name") : "World"; + + console.log({ name }); + + // ... +} +``` + +## FormData + +[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) is a very useful Web API that works great both on the client and the server, when dealing with forms. + +In the browser, it can be used to easily gather the inputs of a `
` tag for communicating with a backend API: + +```html + + + + + + +

Search Page

+ + + + +
+ + +``` + +On the server, we can use the same API to collect the inputs from that form request: + + + +```js +// src/pages/api/search.js +// we pull in WCC here to generate HTML fragments for us +import { getProductsBySearchTerm } from "../../db/client.js"; + +export async function handler(request) { + // use the web standard FormData to get the incoming form submission + const formData = await request.formData(); + const term = formData.has("term") ? formData.get("term") : ""; + const products = await getProductsBySearchTerm(term); + + // ... +} +``` + + diff --git a/src/pages/docs/pages/api-routes.md b/src/pages/docs/pages/api-routes.md new file mode 100644 index 00000000..2e8c30be --- /dev/null +++ b/src/pages/docs/pages/api-routes.md @@ -0,0 +1,122 @@ +--- +title: API Routes +label: API Routes +layout: docs +order: 3 +tocHeading: 2 +--- + +# API Routes + +Greenwood has support for API routes, which are just functions that run on the server, and take in a [**Request**](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [**Response**](https://developer.mozilla.org/en-US/docs/Web/API/Response). Each API route must export an `async` function called **handler**. + +## Usage + +API routes follow a file-based routing convention, within the [pages directory](/docs/pages/routing/). + +So this structure will yield an endpoint available at _/api/greeting_ in the browser: + +```shell +src/ + pages/ + api/ + greeting.js +``` + +Here is an example of that API Route, which reads a query parameter of **name** and returns a JSON response: + +```js +// src/pages/api/greeting.js +export async function handler(request) { + const params = new URLSearchParams(request.url.slice(request.url.indexOf("?"))); + const name = params.has("name") ? params.get("name") : "World"; + const body = { message: `Hello ${name}! 👋` }; + + return new Response(JSON.stringify(body), { + headers: new Headers({ + "Content-Type": "application/json", + }), + }); +} +``` + +## Hypermedia + +Inspired by [**Doug Parker's**](https://blog.dwac.dev/) blog post [_A Simpler HTML-over-the-Wire_](https://blog.dwac.dev/posts/html-fragments/) and tools like [**htmx**](/guides/ecosystem/htmx/), one useful pattern afforded by Greenwood is the ability to render the same custom element definition on the client _and_ the server. This "fragments" API approach can be used to server render Web Component definitions, such that as the HTML is added to the DOM from the response, these components will hydrate automatically and become instantly interactive if the same definition has also been loaded on the client via a ` +``` + +This is also supported for pages with an additional option": + +```js +import { greenwoodPluginTypeScript } from "@greenwood/plugin-typescript"; + +export default { + // ... + + plugins: [ + greenwoodPluginTypeScript({ + servePage: false, + }), + ], +}; +``` + +> For server and pre-rendering use cases, make sure to enable [custom imports](/docs/pages/server-rendering/#custom-imports). diff --git a/src/pages/docs/reference/appendix.md b/src/pages/docs/reference/appendix.md new file mode 100644 index 00000000..e9f2a4f2 --- /dev/null +++ b/src/pages/docs/reference/appendix.md @@ -0,0 +1,143 @@ +--- +layout: docs +order: 5 +tocHeading: 2 +--- + +# Appendix + +## Build Output + +Greenwood produces a consistent build output that typically mirrors the source directory as it persists all file naming, albeit typically with hashed filenames. For static content, this can be used by static hosting sites with no additional configuration, on serverless hosting with our adapters, or self-hosted with Greenwood's `serve` command on your own server or in a Docker container. + +The type of output you may see from Greenwood in the output directory, depending on what features you are using, includes: + +- **Static HTML** - For static and markdown pages, or SSR pages using the `prerender` option +- **Static Assets** - Images and fonts bundled from your CSS, or in the _assets/_ directory. This would also include JavaScript (` +``` + +For convenience, the value of **basePath** will also be made available as a global variable in the `` of your pages: + +```html + +``` + +> User content, like `` and `` tags will still require manually prefixing the `basePath` in your application code. + +## Dev Server + +Configuration for Greenwood's development server is available using the `devServer` option, including the following options: + +- **extensions**: Provide an array of extensions to watch for changes and reload the live server with. By default, Greenwood will already watch all "standard" web assets (HTML, CSS, JS, etc) it supports by default, as well as any extensions set by [resource plugins](/docs/reference/plugins-api/#resource) you are using in your _greenwood.config.js_. +- **hud**: The HUD option ([_head-up display_](https://en.wikipedia.org/wiki/Head-up_display)) is some additional HTML added to your site's page when Greenwood wants to help provide information to you in the browser. For example, if your HTML is detected as malformed, which could break the parser. Set this to `false` if you would like to turn it off. +- **port**: Pick a different port when starting the dev server +- **proxy**: A set of paths to match and re-route to other hosts. Highest specificity should go at the end. + +Below is an example configuration: + +```js +export default { + devServer: { + extensions: ["txt"], + port: 3000, + proxy: { + "/api": "https://stage.myapp.com", + "/api/foo": "https://foo.otherdomain.net", + }, + }, +}; +``` + +## Isolation Mode + +If running Greenwood as a server in production with the `greenwood serve` command, it may be desirable to isolate the server rendering of SSR pages and API routes from the global runtime process. This is a common assumption for many Web Component libraries that may aim to more faithfully honor the browser's native specification on the server. + +Examples include: + +- _Custom Elements Registry_ - Per the spec, a custom element can only be defined once using `customElements.define`. +- _DOM Shims_ - These often assume a globally unique runtime, and so issues can arise when these DOM globals are repeatedly loaded and initialized into the global space + +> See these discussions for more information +> +> - https://github.com/ProjectEvergreen/greenwood/discussions/1117 +> - https://github.com/ProjectEvergreen/wcc/discussions/145 + +As servers have to support multiple clients (as opposed to a browser tab only serving one client at a time), Greenwood offers an isolation mode that can be used to run SSR pages and API routes in their own context per request. + +To configure an entire project for this, simply set the flag in your _greenwood.config.js_: + +```js +export default { + isolation: true, // default value is false +}; +``` + +Optionally, you can opt-in on a per SSR page / API route basis by exporting an `isolation` option: + +```js +// src/pages/products.js + +export const isolation = true; +``` + +## Layouts Directory + +By default the directory Greenwood will use to look for your layouts is in _layouts/_. It is relative to your [user workspace](/docs/reference/configuration/#workspace) setting, e.g. `${userWorkspace}/${layoutsDirectory}`. + +```js +export default { + layoutsDirectory: "layouts", // Greenwood will look for layouts at src/layouts/ +}; +``` + +## Markdown + +You can install and provide custom **unifiedjs** [presets](https://github.com/unifiedjs/unified#preset) and [plugins](https://github.com/unifiedjs/unified#plugin) to further customize and process your markdown past what Greenwood does by default. + +For plugins, after installing their packages, you can provide their names to Greenwood: + +```js +export default { + markdown: { + settings: { commonmark: true }, + plugins: ["rehype-slug", "rehype-autolink-headings"], + }, +}; +``` + +## Optimization + +Greenwood provides a number of different ways to send hints to Greenwood as to how JavaScript and CSS tags in your HTML should get loaded by the browser. Greenwood supplements, and builds up on top of existing [resource "hints" like `preload` and `prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content). + +| Option | Description | Use Cases | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `default` | Will add a `` tag for every ` + + + + +``` + +> Just be mindful that style encapsulation provided by ShadowDOM (e.g. `:host`) for custom elements will now have their styles inlined in the `` and mixed with all other global styles, and thus may collide and [be susceptible to the cascade](https://github.com/ProjectEvergreen/greenwood/pull/645#issuecomment-873125192) depending on their degree of specificity. Increasing specificity of selectors or using only global styles will help resolve this. + +## Pages Directory + +By default the directory Greenwood will use to look for your local content is _pages/_. It is relative to your [user workspace](/docs/reference/configuration/#workspace) setting, e.g. `${userWorkspace}/${pagesDirectory}`. + +```js +export default { + pagesDirectory: "docs", // Greenwood will look for pages at src/docs/ +}; +``` + +## Plugins + +This takes an array of plugins either [created already](/docs/plugins/) by the Greenwood team, or one you [made yourself](/docs/reference/plugins-api/). + +```js +import { greenwoodCssModulesPlugin } from "@greenwood/plugin-css-modules"; + +export default { + plugins: [greenwoodCssModulesPlugin()], +}; +``` + +## Polyfills + +Greenwood provides polyfills for a few Web APIs out of the box. + +### Import Maps + +> Only applies to development mode. + +If you are developing with Greenwood in a browser that doesn't support [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap#browser_compatibility), with this flag enabled, Greenwood will add the [**ES Module Shims**](https://github.com/guybedford/es-module-shims) polyfill to provide support for import maps. + +```js +export default { + polyfills: { + importMaps: true, + }, +}; +``` + +### Import Attributes + +[Import Attributes](https://github.com/tc39/proposal-import-attributes), which are the underlying mechanism for supporting [CSS](https://web.dev/articles/css-module-scripts) and [JSON](https://github.com/tc39/proposal-json-modules) module scripts, are not widely supported in [all browsers yet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#browser_compatibility). Greenwood can enable this in a browser compatible why by specifying which attributes you want handled. In both cases, Greenwood bundles these as ES Modules and will strip the attributes syntax. + +```js +export default { + polyfills: { + importAttributes: ["css", "json"], + }, +}; +``` + +In the case of CSS, Greenwood will inline and export your CSS as a [Constructable Stylesheet](https://web.dev/articles/constructable-stylesheets) + +So given this usage: + + + +```js +import sheet from "./styles.css" with { type: "css" }; +``` + + + +The fallback will become this: + +```js +const sheet = new CSSStyleSheet(); +sheet.replaceSync(" /* ... */ "); +export default sheet; +``` + +For JSON, Greenwood will simply export an object. + +So this usage: + + + +```js +// this +import data from "./data.css" with { type: "json" }; +``` + + + +Will fallback to this: + +```js +export default { + /* ... */ +}; +``` + +## Port + +Unlike the port option for `devServer` configuration, this option allows you to configure the port that your production server will run on when running `greenwood serve`. + +```js +export default { + port: 8181, +}; +``` + +## Prerender + +When set to `true` [Greenwood will prerender](/docs/reference/rendering-strategies/) your application using [**WCC**](https://github.com/ProjectEvergreen/wcc) and generate HTML from any Web Components you include in your pages and layouts as part of the final static HTML build output. + +```js +export default { + prerender: true, +}; +``` + +> You can combine this with ["static" components](/docs/reference/configuration/#optimization) so that you can just do single pass rendering of your Web Components and get their output as static HTML and CSS at build time without having to ship any runtime JavaScript! + +## Static Router + +> ⚠️ _This feature is experimental. Please follow along with [our discussion](https://github.com/ProjectEvergreen/greenwood/discussions/1033) to learn more._ + +Setting the `staticRouter` option to `true` will add a small router runtime in production for static pages to prevent needing full page reloads when navigation between pages that share a layout. For example, the Greenwood website is entirely static, outputting an HTML file per page however, if you navigate from the _Docs_ page to the _Getting Started_ page, you will notice the site does not require a full page load. Instead, the router will just swap out the content of the page much like client-side SPA router would. This technique is similar to how projects like [**pjax**](https://github.com/defunkt/jquery-pjax) and [**Turbolinks**](https://github.com/turbolinks/turbolinks) work, and like what you can see on websites like GitHub. + +```js +export default { + staticRouter: true, +}; +``` + +## Workspace + +Path to where all your project files will be located, provided a valid `URL`. Default is `new URL('./src', import.meta.url)` relative to the project's root, where the _greenwood.config.js_ file is located. + +For example, to change the workspace to be _www/_: + +```js +export default { + workspace: new URL("./www/", import.meta.url), +}; +``` + +> Please note the trailing `/` here as for ESM, as paths must end in a `/` for directories. diff --git a/src/pages/docs/reference/index.md b/src/pages/docs/reference/index.md new file mode 100644 index 00000000..bafff109 --- /dev/null +++ b/src/pages/docs/reference/index.md @@ -0,0 +1,15 @@ +--- +layout: docs +order: 6 +--- + + +

These reference docs aim to provide additional information on how to configure, customize, and tweak Greenwood as you need. Details of Greenwood internals are also covered.

+
+ +The content is broken down across these sections: + +- [Configuration](/docs/reference/configuration/) - All of Greenwood's configuration options +- [Plugins API](/docs/reference/plugins-api/) - Learn how to create your own plugins +- [Rendering Strategies](/docs/reference/rendering-strategies/) - Techniques and tips for various rendering options like CSR, SSR, and prerendering +- [Appendix](/docs/reference/appendix/) - Supplemental topics like Greenwood's build output, internal build state, and DOM emulation handling diff --git a/src/pages/docs/reference/plugins-api.md b/src/pages/docs/reference/plugins-api.md new file mode 100644 index 00000000..c853c4ec --- /dev/null +++ b/src/pages/docs/reference/plugins-api.md @@ -0,0 +1,697 @@ +--- +title: Plugins API +label: Plugins API +layout: docs +order: 2 +tocHeading: 2 +--- + +# Plugins API + +Below are the various plugin types you can use to extend and further customize Greenwood. + +## Overview + +Each plugin must return a function that has the following three properties: + +- **name**: A string to give your plugin a name and used for error handling and logging output +- **type**: A string to specify to Greenwood the type of plugin. Right now the current supported plugin types are: + - **Adapter** + - **Context** + - **Copy** + - **Renderer** + - **Resource** + - **Rollup** + - **Server** + - **Source** +- **provider**: A function that will be invoked by Greenwood that can accept a [**compilation**](/docs/reference/appendix/#compilation) param to provide read-only access to Greenwood's state and configuration. + +Here is an example of creating a plugin in a _greenwood.config.js_: + + + +```javascript +export default { + // ... + + plugins: [ + (options) => { + return { + name: "my-plugin", + type: "resource", + provider: (compilation) => { + // do stuff here + }, + }; + }, + ], +}; +``` + + + +The **provider** function takes a Greenwood [**compilation** object](/docs/reference/appendix/#compilation) consisting of the following properties: + +- **config** - Current values for of Greenwood's [configuration](/docs/reference/configuration/) settings +- **graph** - All the pages in your project per Greenwood's [Content as Data page schema](/docs/content-as-data/page-data/) +- **context** - Access to relevant build directories like project workspace, output directory, etc + +## Adapter + +Adapter plugins are designed with the intent to be able to post-process the Greenwood standard build output. For example, moving [build output files](/docs/reference/appendix#build-output/) around into the desired location for a specific hosting provider, like Vercel or AWS. + +### Usage + +An adapter plugin is simply an `async` function that gets invoked by the Greenwood CLI after all assets, API routes, and SSR pages have been built and optimized. With access to the compilation, you can also process all these files to meet any additional format / output targets. + + + +```js +const greenwoodPluginMyPlatformAdapter = (options = {}) => { + return { + type: "adapter", + name: "plugin-adapter-my-platform", + provider: (compilation) => { + return async () => { + // run your code here.... + }; + }, + }; +}; + +export { greenwoodPluginMyPlatformAdapter }; +``` + +### Example + +The most common use case is to "shim" in a hosting platform handler function in front of Greenwood's, which is based on two parameters of `Request` / `Response`. In addition, producing any hosting provided specific metadata is also doable at this stage. + +Here is an example of the "generic adapter" created for Greenwood's own internal test suite. + +```js +import fs from "fs/promises"; +import { checkResourceExists } from "../../../../cli/src/lib/resource-utils.js"; + +function generateOutputFormat(id, type) { + const path = type === "page" ? `/${id}.route` : `/api/${id}`; + const ref = id.replace(/-/g, "").replace(/\//g, ""); + + return ` + import { handler as ${ref} } from '../public${path}.js'; + + export async function handler (request) { + const { url, headers } = request; + const req = new Request(new URL(url, \`http://\${headers.host}\`), { + headers: new Headers(headers) + }); + + return await ${ref}(req); + } + `; +} + +async function genericAdapter(compilation) { + const adapterOutputUrl = new URL("./adapter-output/", compilation.context.projectDirectory); + const ssrPages = compilation.graph.filter((page) => page.isSSR); + const apiRoutes = compilation.manifest.apis; + + if (!(await checkResourceExists(adapterOutputUrl))) { + await fs.mkdir(adapterOutputUrl); + } + + for (const page of ssrPages) { + const { id } = page; + const outputFormat = generateOutputFormat(id, "page"); + + await fs.writeFile(new URL(`./${id}.js`, adapterOutputUrl), outputFormat); + } + + for (const [key] of apiRoutes) { + const { id } = apiRoutes.get(key); + const outputFormat = generateOutputFormat(id, "api"); + + await fs.writeFile(new URL(`./api-${id}.js`, adapterOutputUrl), outputFormat); + } +} + +const greenwoodPluginAdapterGeneric = (options = {}) => [ + { + type: "adapter", + name: "plugin-adapter-generic", + provider: (compilation) => { + return async () => { + await genericAdapter(compilation, options); + }; + }, + }, +]; + +export { greenwoodPluginAdapterGeneric }; +``` + +> **Note**: Check out [Vercel adapter plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-adapter-vercel) for a more complete example. + +## Context + +Context plugins allow users to extend where Greenwood can look for certain files and folders, like [layouts and pages](/docs/pages/layouts/). This allows plugin authors to publish a full set of assets like HTML, CSS and images (a "theme pack") so that Greenwood users can simply "wrap up" their content in a nice custom layout and theme just by installing a package from npm! 💯 + +Similar in spirit to [**CSS Zen Garden**](http://www.csszengarden.com/) + +> 🔎 For more information on developing and publishing a Theme Pack, check out [our guide on theme packs](/guides/theme-packs/). + +### API + +At present, Greenwood allows for configuring the following locations as array of (absolute) paths + +- Layouts directory - where additional custom page layouts can be found + +> We plan to expand the scope of this as use cases are identified. + +#### Layouts + +By providing paths to directories of layouts, plugin authors can share complete pages, themes, and UI complete with JavaScript and CSS to Greenwood users, and all a user has to do (besides installing the plugin), is specify a layout filename in their frontmatter. + +```md +--- +layout: acme-theme-blog-layout +--- + +## Welcome to my blog! +``` + +Your plugin might look like this: + +```js +/* + * For context, when your plugin is installed via npm or Yarn, import.meta.url will be /path/to/node_modules// + * + * You can then choose how to organize and publish your files. In this case, we have published the layout under a _dist/_ folder, which was specified in the package.json `files` field. + * + * node_modules/ + * acme-theme-pack/ + * dist/ + * layouts/ + * acme-theme-blog-layout.html + * acme-theme-pack.js + * package.json + */ +export function myContextPlugin() { + return { + type: "context", + name: "acme-theme-pack:context", + provider: () => { + return { + layouts: [ + // when the plugin is installed import.meta.url will be /path/to/node_modules// + new URL("./dist/layouts/", import.meta.url), + ], + }; + }, + }; +} +``` + +> Additionally, you can provide the default _app.html_ and _page.html_ layouts this way as well! + +## Copy + +The copy plugin allows users to copy files around as part of the Greenwood `build` command. For example, Greenwood uses this feature to copy all files in the user's [_/assets/_](/docs/resources/assets/) directory to final output directory automatically. You can use this plugin to copy single files, or entire directories. + +### API + +This plugin supports providing an array of "paired" URL objects that can either be files or directories, by providing a `from` and `to` location as instances of `URL`s: + +```js +export function myCopyPlugin() { + return { + type: "copy", + name: "plugin-copy-some-files", + provider: (compilation) => { + const { context } = compilation; + + return [ + { + // copy a file + from: new URL("./robots.txt", context.userWorkspace), + to: new URL("./robots.txt", context.outputDir), + }, + { + // copy a directory (notice the trailing /) + from: new URL("./pdfs/", context.userWorkspace), + to: new URL("./pdfs/", context.outputDir), + }, + ]; + }, + }; +} +``` + +> You can see more examples in the [Greenwood repo](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/plugins/copy). + +## Renderer + +Renderer plugins allow users to customize how Greenwood server renders (and prerenders) your project. By default, Greenwood supports using [**WCC** or (template) strings](/docs/pages/server-rendering/) to return static HTML for the content and template of your server side routes. With this plugin for example, you can use [Lit's SSR](https://github.com/lit/lit/tree/main/packages/labs/ssr) to render your Lit Web Components on the server side instead. + +### API + +This plugin expects to be given a path to a module that exports a function to execute the SSR content of a page by being given its HTML and related scripts. For local development Greenwood will run this in a `Worker` thread for live reloading, and use it standalone for production bundling and serving. + +```js +const greenwoodPluginMyCustomRenderer = (options = {}) => { + return { + type: "renderer", + name: "plugin-renderer-custom", + provider: () => { + return { + executeModuleUrl: new URL("./execute-route-module.js", import.meta.url), + prerender: options.prerender, + }; + }, + }; +}; + +export { greenwoodPluginMyCustomRenderer }; +``` + +#### Options + +This plugin type supports the following options: + +- `executeModuleUrl` (recommended) - `URL` to the location of a file with the SSR rendering implementation +- `customUrl` - `URL` to a file that has a `default export` of a function for handling the _prerendering_ lifecyle of a Greenwood build, and running the provided `callback` function +- `prerender` (optional) - Flag can be used to indicate if this custom renderer should be used to statically [prerender](/docs/configuration/#prerender) pages too. + +### Examples + +#### Default + +The recommended Greenwood API for executing server rendered code is in a function that is expected to implement any combination of [these APIs](/docs/pages/server-rendering/#api); `default export`, `getBody`, `getLayout`, and `getFrontmatter`. + +You can follow the [WCC default implementation for Greenwood](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/cli/src/lib/execute-route-module.js) as a reference. + +#### Custom Implementation + +This option is useful for exerting full control over the rendering lifecycle, like running a headless browser. You can follow [Greenwood's implementation for Puppeteer](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-renderer-puppeteer/src/puppeteer-handler.js) as a reference. + +## Resource + +Resource plugins allow for the manipulation and transformation of files served and bundled by Greenwood. Whether you need to support a file with a custom extension or transform the contents of a file from one type to the other, resource plugins provide the lifecycle hooks into Greenwood to enable these customizations. Examples from Greenwood's own plugin system include: + +- Minifying and bundling CSS +- Compiling TypeScript into JavaScript +- Converting vanilla CSS into ESM +- Injecting site analytics or other third party snippets into your HTML + +It uses standard Web APIs for facilitating these transformations such as [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). + +### API + +A [resource "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/resource-interface.js) has been provided by Greenwood that you can use to start building your own resource plugins with. + +```javascript +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; + +class ExampleResource extends ResourceInterface { + constructor(compilation, options = {}) { + super(); + + this.compilation = compilation; // Greenwood's compilation object + this.options = options; // any optional configuration provided by the user of your plugin + this.extensions = ["foo", "bar"]; // add custom extensions for file watching + live reload here, ex. ts for TypeScript + this.servePage = `static|dynamic`; // optionally opt-in to Greenwood using the plugin's serve lifecycle for processing static pages ('static') or SSR pages and API routes ('dynamic') + } + + // lifecycles go here +} + +export function myResourcePlugin(options = {}) { + return { + type: "resource", + name: "my-resource-plugin", + provider: (compilation) => new ExampleResource(compilation, options), + }; +} +``` + +> Note: Using `servePage` with the `'dynamic'` setting requires enabling [custom imports](/docs/pages/server-rendering/#custom-imports). + +### Lifecycles + +A resource plugin in Greenwood has access to four lifecycles, in this order: + +1. `resolve` - Where the resource is located, e.g. on disk +1. `serve` - What are the contents of a resource +1. `preIntercept` - transforming the response of a _served_ resource before Greenwood can `intercept` it +1. `intercept` - transforming the response of a _served_ resource +1. `optimize` - transforming the response of resource after `intercept` lifecycle has run (only runs at build time) + +Each lifecycle also supports a corresponding predicate function, e.g. `shouldResolve` that should return a boolean of `true|false` if this plugin's lifecycle should be invoked for the given resource. + +#### Resolve + +When requesting a resource like a file, such as `/main.js`, Greenwood needs to know _where_ this resource is located. This is the first lifecycle that is run and takes in a `URL` and `Request` as parameters, and should return a `Request` object. Below is an example from [Greenwood's codebase](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/cli/src/plugins/resource/plugin-user-workspace.js). + + + +```js +import fs from "fs"; +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; + +class UserWorkspaceResource extends ResourceInterface { + async shouldResolve(url, request) { + const { pathname } = url; + const { userWorkspace } = this.compilation.context; + const hasExtension = !["", "/"].includes(pathname.split(".").pop()); + + return ( + hasExtension && + !pathname.startsWith("/node_modules") && + fs.existsSync(new URL(`.${pathname}`, userWorkspace).pathname) + ); + } + + async resolve(url, request) { + const { pathname } = url; + const { userWorkspace } = this.compilation.context; + const workspaceUrl = new URL(`.${pathname}`, userWorkspace); + + return new Request(workspaceUrl); + } +} +``` + + + +> For most cases, you will not need to use this lifecycle as by default Greenwood will first check if it can resolve a request to a file either in the current workspace or _/node_modules/_. If it finds a match, it will transform the request into a `file://` protocol with the full local path, otherwise the request will remain as the default of `http://`. + +#### Serve + +When requesting a file and after knowing where to resolve it, such as `/path/to/user-workspace/main/scripts/main.js`, Greenwood needs to return the contents of that resource so can be served to a browser or bundled appropriately. This is done by passing an instance of `URL` and `Request` and returning an instance of `Response`. For example, Greenwood uses this lifecycle extensively to serve all the standard web content types like HTML, JS, CSS, images, fonts, etc and also providing the appropriate `Content-Type` header. + +If you are supporting _non standard_ file formats, like TypeScript (`.ts`) or JSX (`.jsx`), this is where you would want to handle providing the contents of this file transformed into something a browser could understand; like compiling the TypeScript to JavaScript. + +Below is an example from [Greenwood's codebase](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/cli/src/plugins/resource/plugin-standard-javascript.js) for serving JavaScript files. + + + +```js +import fs from "fs"; +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; + +class StandardJavaScriptResource extends ResourceInterface { + async shouldServe(url, request) { + return url.protocol === "file:" && url.pathname.split(".").pop() === "js"; + } + + async serve(url, request) { + const body = await fs.promises.readFile(url, "utf-8"); + + return new Response(body, { + headers: { + "Content-Type": "text/javascript", + }, + }); + } +} +``` + + + +> If this was a TypeScript file, this would be the lifecycle where you would run `tsc`. + +#### Pre Intercept + +After the `serve` lifecycle comes the `preIntercept` lifecycle. This lifecycle is useful for transforming an already served resource _Greenwood_ or any other plugins try and `intercept` it the contents. It takes in as parameters an instance of `URL`, `Request`, and `Response`. + +This lifecycle is useful for augmenting _standard_ web formats prior to Greenwood operating on them. A good example of this is wanting to run pre-processors like Babel, ESBuild, or PostCSS to "downlevel" non-standard syntax _into_ standard syntax before other plugins can operate on it. + +Below is an example of Greenwood's PostCSS plugin using `preIntercept` on CSS files. + + + +```js +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; +import { normalizePathnameForWindows } from "@greenwood/cli/src/lib/resource-utils.js"; +import postcss from "postcss"; + +async function getConfig() { + // ... +} + +class PostCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ["css"]; + this.contentType = "text/css"; + } + + async shouldPreIntercept(url) { + return url.protocol === "file:" && url.pathname.split(".").pop() === this.extensions[0]; + } + + async preIntercept(url, request, response) { + const config = await getConfig(this.compilation, this.options.extendConfig); + const plugins = config.plugins || []; + const body = await response.text(); + const css = + plugins.length > 0 + ? (await postcss(plugins).process(body, { from: normalizePathnameForWindows(url) })).css + : body; + + return new Response(css, { headers: { "Content-Type": this.contentType } }); + } +} +``` + + + +#### Intercept + +After the `preIntercept` lifecycle comes the `intercept` lifecycle. This lifecycle is useful for transforming already served resources and returning an instance of a `Response` with the new transformation. It takes in as parameters an instance of `URL`, `Request`, and `Response`. + +This lifecycle is useful for augmenting _standard_ web formats, where Greenwood can handle resolving and serving the standard contents, allowing plugins to handle any one-off transformations. + +A good example of this is [Greenwood's "raw" plugin](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-import-raw/src/index.js) which can take a standard web format like CSS, and convert it onto a standard ES Module when a `?type=raw` is added to any `import`, which would be useful for CSS-in-JS use cases, for example: + + + +```js +import styles from "./hero.css?type=raw"; +``` + + + + + +```js +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; + +class ImportRawResource extends ResourceInterface { + async shouldIntercept(url) { + const { protocol, searchParams } = url; + const type = searchParams.get("type"); + + return protocol === "file:" && type === "raw"; + } + + async intercept(url, request, response) { + const body = await response.text(); + const contents = `const raw = \`${body.replace(/\r?\n|\r/g, " ").replace(/\\/g, "\\\\")}\`;\nexport default raw;`; + + return new Response(contents, { + headers: new Headers({ + "Content-Type": "text/javascript", + }), + }); + } +} +``` + + + +#### Optimize + +This lifecycle is only run during a build (`greenwood build`) and after the `intercept` lifecycle, and as the name implies is a way to do any final production ready optimizations or transformations. It takes as parameters an instance of `URL` and `Response` and should return an instance of `Response`. + +Below is an example from [Greenwood's codebase](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-import-css/src/index.js) for minifying CSS. (The actual function for minifying has been omitted for brevity) + + + +```js +import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js"; + +function bundleCss() { + // .. +} + +class StandardCssResource extends ResourceInterface { + async shouldOptimize(url, response) { + const { protocol, pathname } = url; + + return ( + this.compilation.config.optimization !== "none" && + protocol === "file:" && + pathname.split(".").pop() === "css" && + response.headers.get("Content-Type").indexOf("text/css") >= 0 + ); + } + + async optimize(url, response) { + const body = await response.text(); + const optimizedBody = bundleCss(body); + + return new Response(optimizedBody); + } +} +``` + + + +> You can see [more in-depth examples of resource plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/plugins/resource/) by reviewing the default plugins maintained in Greenwood's CLI package. + +## Rollup + +Though rare, there may be cases for tapping into the bundling process for Greenwood. If so, this plugin type allow users to tap into Greenwood's [**Rollup**](https://rollupjs.org/) configuration to provide any custom Rollup behaviors you may need. + +Simply use the `provider` method to return an array of Rollup plugins: + +```js +import bannerRollup from "rollup-plugin-banner"; +import fs from "fs"; + +const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8")); + +export function myRollupPlugin() { + const now = new Date().now(); + + return { + type: "rollup", + name: "plugin-something-something", + provider: () => [ + bannerRollup(`/* ${packageJson.name} v${packageJson.version} - built at ${now}. */`), + ], + }; +} +``` + +## Server + +Server plugins allow developers to start and stop custom servers as part of the _development_ lifecycle of Greenwood. + +These lifecycles provide the ability to do things like: + +- Start a live reload server (like Greenwood does by default) +- Starting a GraphQL server +- Reverse proxy to help route external requests + +### API + +Although JavaScript is loosely typed, a [server "interface"](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/lib/server-interface.js) has been provided by Greenwood that you can use to start building your own server plugins. Effectively you just have to provide two methods: + +- `start` - function to run to start your server +- `stop` - function to run to stop / teardown your server + +They can be used in a _greenwood.config.js_ just like any other plugin type. + +```javascript +import { myServerPlugin } from "./my-server-plugin.js"; + +export default { + // ... + + plugins: [myServerPlugin()], +}; +``` + +### Example + +The below is an excerpt of [Greenwood's internal LiveReload server](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/plugins/server/plugin-livereload.js) plugin. + +```javascript +import { ServerInterface } from "@greenwood/cli/src/lib/server-interface.js"; +import livereload from "livereload"; + +class LiveReloadServer extends ServerInterface { + constructor(compilation, options = {}) { + super(compilation, options); + + this.liveReloadServer = livereload.createServer({ + /* options */ + }); + } + + async start() { + const { userWorkspace } = this.compilation.context; + + return this.liveReloadServer.watch(userWorkspace, () => { + console.info(`Now watching directory "${userWorkspace}" for changes.`); + return Promise.resolve(true); + }); + } +} + +export function myServerPlugin(options = {}) { + return { + type: "server", + name: "plugin-livereload", + provider: (compilation) => new LiveReloadServer(compilation, options), + }; +} +``` + +## Source + +The source plugin allows users to include external content as pages that will be statically generated just like if they were a markdown or HTML in your _pages/_ directory. This would be the primary API to include content from a headless CMS, database, the filesystem, SaaS provider (Notion, AirTables) or wherever else you keep it. + +### API + +This plugin supports providing an array of "page" objects that will be added as nodes in [the graph](/docs/data/). + +```js +// my-source-plugin.js +export const customExternalSourcesPlugin = () => { + return { + type: "source", + name: "source-plugin-myapi", + provider: () => { + return async function () { + // this could just as easily come from an API, DB, Headless CMS, etc + const artists = await fetch("http://www.myapi.com/...").then((resp) => resp.json()); + + return artists.map((artist) => { + const { bio, id, imageUrl, name } = artist; + const route = `/artists/${name.toLowerCase().replace(/ /g, "-")}/`; + + return { + title: name, + body: ` +

${name}

+

${bio}

+ + `, + route, + id, + label: name, + }; + }); + }; + }, + }; +}; +``` + +In the above example, you would have the following statically generated in the output directory: + +```shell +public/ + artists/ + /index.html + /index.html + /index.html +``` + +And accessible at the following routes in the browser: + +- `/artists//` +- `/artists//` +- `/artists//` diff --git a/src/pages/docs/reference/rendering-strategies.md b/src/pages/docs/reference/rendering-strategies.md new file mode 100644 index 00000000..ea66df5b --- /dev/null +++ b/src/pages/docs/reference/rendering-strategies.md @@ -0,0 +1,104 @@ +--- +layout: docs +order: 3 +tocHeading: 2 +--- + +# Rendering Strategies + +Greenwood is very flexible in the types of rendering strategies you can use, based on your needs and use case. Below is a brief overview of each of them and some general guidelines and recommendations. + +As always, sometimes it makes sense to mix and match, so as with any generalized technology advice, [_**YMMY**_](https://en.wiktionary.org/wiki/your_mileage_may_vary). + +## CSR + +Client Side Rendering (CSR), like Single Page Applications (SPAs), generally favor client side fetches for data in the user's browser. Typically this is done by communication through API endpoints, which Greenwood [supports](/docs/pages/api-routes/). With this approach, your HTML will typically just be sent to the browser as an ["app shell"](https://developer.chrome.com/blog/app-shell) that loads in data by making `fetch` calls and updating the UI with that data. + +If you are building data or interaction heavy applications, or your data is very dynamic and / or does not benefit from SEO, then this approach would be a good choice. Start with an _index.html_ and off you go! + +## SSG + +Static Site Generation (SSG) is an approach characterized by building your pages and content ahead of time, typically deploying to just a CDN. If your project is very content heavy, but it doesn't change often, or you want to author in markdown, then this is a great choice. Static hosting like [**Netlify**](/guides/hosting/netlify/) or [**Vercel**](/guides/hosting/netlify/) just works out of the box with no configuration at all. + +If you are building a documentation site, portfolio, blog, or landing pages, SSG is a great option. + +> That said, this can often slow down a project over time as more and more content gets added overtime, but Greenwood is capable of building [thousands of pages in just a couple of minutes](https://github.com/ProjectEvergreen/greenwood/issues/970#issuecomment-1283194296). + +## SSR + +Server-Side Rendering (SSR) is a great choice when you might need to combine the needs of SEO with dynamic data that needs to be rendered out on each request. You can self-host or Dockerize your own Greenwood server, or use one our adapters to deploy to a serverless provider like Netlify or Vercel. + +If you are building an e-commerce site or a catalog-like site, SSR would be a good choice. + +## Prerendering + +Prerendering is a feature of Greenwood by which custom element definitions (be it with WCC, Lit, or a custom renderer) can be executed and run once at build time through a "single pass" render. It can be combined with any of the above strategies. This makes custom elements a powerful, JavaScript based templating system using just the web standards you already know. + +### Static Optimization + +For example, take a list of blog posts rendered based on the project's pages directory + + + + +1. Add the `prerender` config to _greenwood.config.js_ + + ```js + export default { + prerender: true, + }; + ``` + +1. Create a data fetching component + + ```js + import { getContentByRoute } from "@greenwood/cli/src/data/queries.js"; + + export default class BlogPostsList extends HTMLElement { + async connectedCallback() { + const posts = await getContentByRoute("/blog/"); + + this.innerHTML = ` + ${posts + .map((post) => { + return ` +
+ ${post.title} + + `; + }) + .join("")} + `; + } + } + + customElements.define("blog-posts-list", BlogPostsList); + ``` + +1. Add it to your HTML page with the [**static** optimization attribute](/docs/reference/configuration/#optimizations) + + ```html + + + + Blog + + + +

All Blog Posts

+ + + + ``` + + +### SSR + +You can emit SSR pages as pure HTML on an opt-in basis by setting the `prerender` flag in the file (or for all pages in your _greenwood.config.js_): + +```js +// src/pages/artists.js +export const prerender = true; +``` + +> If you need more robust support for executing JavaScript at build time, you can consider using our [Puppeteer plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-puppeteer), or [create your own custom implementation](http://localhost:1984/docs/reference/plugins-api/#custom-implementation), say for using JSDOM instead. diff --git a/src/pages/docs/resources/assets.md b/src/pages/docs/resources/assets.md new file mode 100644 index 00000000..1260730c --- /dev/null +++ b/src/pages/docs/resources/assets.md @@ -0,0 +1,87 @@ +--- +layout: docs +order: 3 +tocHeading: 2 +--- + +# Assets + +Greenwood provides handling and support for common web formats and conventions. This can include images, fonts, PDFs, whatever you need. + +## Directory + +For convenience, **Greenwood** supports an _assets/_ directory wherein anything included in that directory will be automatically copied into the build output directory as is. This can be useful if you have files that are not bundled through CSS or JavaScript (e.g `import`, `@import`, ` + +``` + +### Layouts + +When creating multiple [page layouts](/docs/pages/layouts/), you can use the **layout** frontmatter key to configure Greenwood to use that layout to wrap a given page. + +```md +--- +layout: blog +--- + +# My First Blog Post + +This is my first blog post, I hope you like it! +``` + +In this example, _src/layouts/blog.html_ will be used to wrap the content of this markdown page. + +> **Note:** By default, Greenwood will look for and use _src/layouts/page.html_ for all pages by default. + +### Custom Data + +You can also define any custom frontmatter property you want and that will be made available on the `data` property of [the page object](/docs/content-as-data/pages-data/). + +```md +--- +author: Jon Doe +date: 04/07/2020' +--- + +# First Post + +My first post +``` + +## Active Frontmatter + +With [`activeContent`](/docs/reference/configuration/#active-content) enabled, any of these properties would be available in your HTML or markdown through Greenwood's [content as data features](/docs/content-as-data/). + +```md +--- +author: Project Evergreen +--- + +## My Post + +Authored By: ${globalThis.page.data.author} +``` diff --git a/src/pages/docs/resources/scripts.md b/src/pages/docs/resources/scripts.md new file mode 100644 index 00000000..07347147 --- /dev/null +++ b/src/pages/docs/resources/scripts.md @@ -0,0 +1,113 @@ +--- +layout: docs +order: 1 +tocHeading: 2 +--- + +# Scripts + +The page covers usage of JavaScript in Greenwood using all the standard browser conventions like ` + + + + + + + + +``` + +## Modules (ESM) + +Greenwood also supports (and recommends) usage of ECMAScript Modules (ESM) with the `type="module"` attribute, like in the example below: + +```html + + + + + + + + + + +``` + +Keep in mind that the specification dictates the following conventions when referencing ESM files: + +1. It must be a relative path +1. It must have an extension + + + +```js +// happy panda +import { Foo } from "./foo.js"; +``` + + + +```js +// sad panda +import { Foo } from "foo"; +``` + +## Node Modules + +Packages from [**npm**](https://www.npmjs.com/) (and compatible registries) can be used by installing them with your favorite package manager. In the browser, Greenwood will automatically build up an import map from any packages defined in the `dependencies` property of your _package.json_. + +Below are some examples: + +```js +// after having installed Lit +// npm i lit +import { html, LitElement } from "lit"; + +export class SimpleGreeting extends LitElement { + static properties = { + name: { type: String }, + }; + + render() { + return html`

Hello, ${this.name ?? "World"}!

`; + } +} + +customElements.define("simple-greeting", SimpleGreeting); +``` + +You can reference **node_modules** directly from a ` + + + +
+ +
+ + +``` + +The rule of thumb is: + +- If it's a package from npm, you can use bare specifiers and no extension +- Otherwise, you will need to use a relative path and the extension diff --git a/src/pages/docs/resources/styles.md b/src/pages/docs/resources/styles.md new file mode 100644 index 00000000..33586b03 --- /dev/null +++ b/src/pages/docs/resources/styles.md @@ -0,0 +1,72 @@ +--- +layout: docs +order: 2 +tocHeading: 2 +--- + +# Styles + +The page covers usage of CSS in Greenwood using all the standard browser conventions like ` + + + + + + + + +``` + +## NPM + +Packages from [**npm**](https://www.npmjs.com/) can be used by installing them with your favorite package manager. + +In a CSS file, you can use relative paths to resolve to _node_modules_: + +```css +/* after having installed Open Props */ +/* npm i open-props */ +/* src/theme.css */ +@import "../../node_modules/open-props/src/props.borders.css"; +@import "../../node_modules/open-props/src/props.fonts.css"; +@import "../../node_modules/open-props/src/props.shadows.css"; +@import "../../node_modules/open-props/src/props.sizes.css"; +``` + +From an HTML file, you can reference **node_modules** by starting the path with _node_modules_: + +```html + + + + + + + + + + + +``` diff --git a/src/pages/guides/getting-started/going-further.md b/src/pages/guides/getting-started/going-further.md index 7ff49cd8..63ac26d8 100644 --- a/src/pages/guides/getting-started/going-further.md +++ b/src/pages/guides/getting-started/going-further.md @@ -161,7 +161,7 @@ And access these values through HTML, like in a layout file: diff --git a/src/pages/guides/tutorials/theme-packs.md b/src/pages/guides/tutorials/theme-packs.md index 9d40e914..c91b80b1 100644 --- a/src/pages/guides/tutorials/theme-packs.md +++ b/src/pages/guides/tutorials/theme-packs.md @@ -270,7 +270,7 @@ For users, they would just need to do the following: Success! 🥳 -> _Don't forget, user's can also [include additional CSS / JS files in their frontmatter](/docs/front-matter/#imports), to further extend, customize, and override your layouts!_ +> Don't forget, user's can also [include additional CSS / JS files in their frontmatter](/docs/front-matter/#imports), to further extend, customize, and override your layouts! ## FAQ diff --git a/src/styles/docs.css b/src/styles/docs.css new file mode 100644 index 00000000..e0ff8005 --- /dev/null +++ b/src/styles/docs.css @@ -0,0 +1,180 @@ +body:has(#compact-menu:popover-open) { + overflow-y: hidden; +} + +.page-content { + margin: var(--size-4) auto; + + & hr { + margin: 0 0 var(--size-4); + } +} + +.page-content .content-outlet { + padding: 0 var(--size-4); + + & h1 { + font-size: var(--font-size-5); + } + + & h2 { + font-size: var(--font-size-4); + font-family: var(--font-primary-bold); + font-weight: 100; + margin: var(--size-8) 0 0 !important; + } + + & h2, + & p { + margin: var(--size-4) 0; + } + + & h3, + & h4, + & h5, + & h6 { + font-size: var(--font-size-3); + margin: var(--size-4) 0 var(--size-1); + } + + & table a, + & table code, + & table strong, + & table th, + & table td { + font-size: 24px; + vertical-align: top; + } + + & table tr { + height: 20px; + } + + & table th { + text-decoration: underline; + background-color: var(--color-accent); + } + + & table { + border: 1px solid var(--color-prism-bg); + } + + & table td { + padding: 6px 12px; + } + + & ol, + & ul { + padding-left: var(--size-4); + } + + & img { + width: 100%; + } + + & h2 > a > span.icon, + & h3 > a > span.icon, + & h4 > a > span.icon { + vertical-align: text-top; + } + + & iframe.stackblitz { + display: none; + } +} + +app-edit-on-github { + display: block; + padding: var(--size-3) 0; + position: sticky; + bottom: 0; + text-align: right; + margin: var(--size-px-7) 0 var(--size-px-3) 0; +} + +app-heading-box { + & .spacer { + display: block; + margin: var(--size-1); + } + + & .question { + font-style: italic; + margin: 0 var(--size-2) 0 0; + } + + & .answer { + font-family: var(--font-primary-bold); + font-weight: 100; + text-decoration: underline; + margin-bottom: 20px; + display: inline-block; + } +} + +app-side-nav, +app-toc { + display: inline-block; + margin: 0 auto var(--size-4); +} + +app-side-nav { + padding: 0 0 0 var(--size-4); +} + +@media (min-width: 1200px) { + .content-outlet { + display: inline-block; + width: 45%; + font-size: var(--font-size-1); + } + + app-edit-on-github { + position: fixed; + bottom: var(--size-10); + right: var(--size-6); + } + + app-side-nav { + display: inline-block; + width: 20%; + min-width: 25%; + vertical-align: top; + } + + app-side-nav:has(ul) { + padding: var(--size-4) 0 0 var(--size-fluid-5); + background-color: var(--color-gray); + border: 1px dotted var(--color-prism-bg); + border-radius: 0 var(--radius-2) var(--radius-2) 0; + } + + app-toc { + float: right; + display: inline-block; + width: 20%; + position: sticky; + top: var(--size-4); + margin: 0 var(--size-1) 0 0; + } +} + +@media (min-width: 768px) { + .page-content { + & iframe.stackblitz { + width: 100%; + height: 800px; + display: block; + } + } +} + +@media (min-width: 1440px) { + .page-content .content-outlet { + width: 54%; + } + + app-side-nav:has(ul) { + padding: var(--size-4) var(--size-1) 0 var(--size-fluid-6); + } +}