Concepts

You’ll learn how different concepts of Gustwind go together on this page. It’s self-similar in many ways and if you are familiar with technologies such as HTML, Tailwind, or React, likely you’ll get used to it fast.

At its core, the page engine is an interpolating template interpreter that allows data binding. That means there’s special syntax for connecting data with your pages and components. The data binding itself happens at routing level and here we focus on the component and layout level.

Components

To give you a simple example of a component, consider the following example for a link that is able to bold itself if it’s matching to the path of the current page:

components/Link.json

{
  "type": "a",
  "class": "underline",
  "classList": {
    "font-bold": [
      { "utility": "get", "parameters": ["props", "href"] },
      { "utility": "get", "parameters": ["context", "pathname"] }
    ]
  },
  "children": {
    "utility": "get",
    "parameters": ["props", "children"]
  },
  "attributes": {
    "href": {
      "utility": "get",
      "parameters": ["props", "href"]
    }
  }
}

The styling semantics are based on Tailwind but you can see there’s also data binding going on at classList.

A navigation component built on top of Link could look like this:

components/Navigation.json

[
  {
    "type": "Link",
    "props": {
      "children": "Modes",
      "href": "/modes/"
    }
  },
  {
    "type": "Link",
    "props": {
      "children": "Configuration",
      "href": "/configuration/"
    }
  },
  {
    "type": "Link",
    "props": {
      "children": "Routing",
      "href": "/routing/"
    }
  },
  {
    "type": "Link",
    "props": {
      "children": "Concepts",
      "href": "/concepts/"
    }
  },
  {
    "type": "Link",
    "props": {
      "children": "Deployment",
      "href": "/deployment/"
    }
  },
  {
    "type": "Link",
    "props": {
      "children": "Breezewind",
      "href": "/breezewind/"
    }
  }
]

Utilities

The following example illustrates the usage of utilities:

layouts/blogIndex.json

{
  "type": "BaseLayout",
  "props": {
    "content": {
      "type": "div",
      "class": "md:mx-auto my-8 px-4 md:px-0 w-full lg:max-w-3xl prose lg:prose-xl",
      "children": [
        {
          "type": "ul",
          "foreach": [{
            "utility": "get",
            "parameters": ["context", "blogPosts"]
          }, {
            "type": "li",
            "class": "inline",
            "children": [
              {
                "type": "Link",
                "bindToProps": {
                  "children": {
                    "utility": "get",
                    "parameters": ["props", "data.title"]
                  },
                  "href": {
                    "utility": "concat",
                    "parameters": [
                      {
                        "utility": "get",
                        "parameters": ["props", "data.slug"]
                      },
                      "/"
                    ]
                  }
                }
              }
            ]
          }]
        }
      ]
    }
  }
}

In this case we add / to each slug.

Data sources

In the examples above, data coming from data sources has been connected, or bound, to the visible structure. Data sources are defined as below:

dataSources.ts

import { parse } from "https://deno.land/x/frontmatter@v0.1.4/mod.ts";
import markdown from "./transforms/markdown.ts";
import { dir } from "../utils/fs.ts";
import type { MarkdownWithFrontmatter } from "../types.ts";

function blogPosts() {
  return indexMarkdown("./blogPosts");
}

function documentation() {
  return indexMarkdown("./documentation");
}

async function readme() {
  return markdown(await Deno.readTextFile("./README.md"));
}

async function parseHeadmatter() {
  return parse(await Deno.readTextFile("./breezewind/README.md"));
}

async function indexMarkdown(directory: string) {
  const files = await dir(directory, ".md");

  return Promise.all(
    files.map(({ path }) =>
      Deno.readTextFile(path).then((d) => parse(d) as MarkdownWithFrontmatter)
    ),
  );
}

export { blogPosts, documentation, parseHeadmatter, readme };

Data sources are asynchronous functions returning arrays of objects. Then, when bound, you can access the content. This would be a good spot to connect to a database, external API, or local data.

Transforms

Transforms accept data and then perform a manipulation on it. It could for example accept a Markdown string and convert it to HTML or reverse the input given to it as below:

transforms/reverse.ts

function reverse(arr: unknown[]) {
  return [...arr].reverse();
}

export default reverse;

Layouts

Gustwind layouts are comparable to components:

layouts/siteIndex.json

{
  "type": "BaseLayout",
  "props": {
    "content": [
      {
        "type": "header",
        "class": "bg-gradient-to-br from-purple-200 to-emerald-100 py-8",
        "children": [
          {
            "type": "div",
            "class": "sm:mx-auto px-4 py-4 sm:py-8 max-w-3xl flex",
            "children": [
              {
                "type": "div",
                "class": "flex flex-col gap-8",
                "children": [
                  {
                    "type": "h1",
                    "class": "text-4xl md:text-8xl",
                    "children": [
                      {
                        "type": "span",
                        "class": "whitespace-nowrap pr-4",
                        "children": "🐳💨"
                      },
                      {
                        "type": "span",
                        "children": "Gustwind"
                      }
                    ]
                  },
                  {
                    "type": "h2",
                    "class": "text-xl md:text-4xl font-extralight",
                    "children": "Deno powered JSON oriented site generator"
                  }
                ]
              }
            ]
          }
        ]
      },
      {
        "type": "div",
        "class": "md:mx-auto my-8 px-4 md:px-0 w-full lg:max-w-3xl prose lg:prose-xl",
        "children": {
          "utility": "get",
          "parameters": ["context", "readme.content"]
        }
      }
    ]
  }
}

For pages that are generated dynamically, i.e. blog pages, match is exposed.

layouts/blogPage.json

{
  "type": "BaseLayout",
  "props": {
    "content": {
      "type": "div",
      "class": "md:mx-auto my-8 px-4 md:px-0 w-full lg:max-w-3xl prose lg:prose-xl",
      "children": [
        {
          "type": "h1",
          "children": {
            "utility": "get",
            "parameters": ["context", "document.data.title"]
          }
        },
        {
          "type": "p",
          "children": {
            "utility": "markdown",
            "parameters": [{
              "utility": "get",
              "parameters": ["context", "document.content"]
            }]
          }
        }
      ]
    }
  }
}

The same idea can be used to implement an RSS feed.

layouts/rssPage.json

[
  {
    "type": "?xml",
    "attributes": {
      "version": "1.0",
      "encoding": "UTF-8"
    },
    "closingCharacter": "?"
  },
  {
    "type": "feed",
    "__reference": "https://kevincox.ca/2022/05/06/rss-feed-best-practices/",
    "attributes": {
      "xmlns": "http://www.w3.org/2005/Atom"
    },
    "children": [
      {
        "type": "title",
        "children": {
          "utility": "get",
          "parameters": ["context", "meta.siteName"]
        }
      },
      {
        "type": "id",
        "children": {
          "utility": "get",
          "parameters": ["context", "meta.url"]
        }
      },
      {
        "type": "link",
        "attributes": {
          "rel": "alternate",
          "href": {
            "utility": "get",
            "parameters": ["context", "meta.url"]
          }
        }
      },
      {
        "type": "link",
        "attributes": {
          "rel": "self",
          "children": {
            "utility": "concat",
            "parameters": [
              {
                "utility": "get",
                "parameters": ["context", "meta.url"]
              },
              "atom.xml"
            ]
          }
        }
      },
      {
        "type": "updated",
        "children": {
          "utility": "dateToISO",
          "parameters": [
            { "utility": "get", "parameters": ["context", "meta.built"] }
          ]
        }
      },
      {
        "foreach": [
          { "utility": "get", "parameters": ["context", "blogPosts"] },
          {
            "type": "entry",
            "children": [
              {
                "type": "title",
                "children": {
                  "utility": "get",
                  "parameters": ["props", "data.title"]
                }
              },
              {
                "type": "link",
                "attributes": {
                  "rel": "alternate",
                  "type": "text/html",
                  "href": {
                    "utility": "concat",
                    "parameters": [
                      {
                        "utility": "get",
                        "parameters": ["context", "meta.url"]
                      },
                      "blog",
                      "/",
                      {
                        "utility": "get",
                        "parameters": ["props", "data.slug"]
                      },
                      "/"
                    ]
                  }
                }
              },
              {
                "type": "id",
                "children": {
                  "utility": "get",
                  "parameters": ["props", "data.slug"]
                }
              },
              {
                "type": "published",
                "children": {
                  "utility": "dateToISO",
                  "parameters": [
                    { "utility": "get", "parameters": ["props", "data.date"] }
                  ]
                }
              },
              {
                "type": "content",
                "attributes": {
                  "type": "html"
                },
                "children": {
                  "utility": "get",
                  "parameters": ["props", "data.description"]
                }
              }
            ]
          }
        ]
      }
    ]
  }
]