Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content as Data #1167

Closed
thescientist13 opened this issue Oct 17, 2023 · 4 comments · Fixed by #1266 or #1287
Closed

Content as Data #1167

thescientist13 opened this issue Oct 17, 2023 · 4 comments · Fixed by #1266 or #1287
Assignees
Labels
alpha.7 Content as Data documentation Greenwood specific docs feature New feature or request RFC Proposal and changes to workflows, architecture, APIs, etc v0.30.0
Milestone

Comments

@thescientist13
Copy link
Member

thescientist13 commented Oct 17, 2023

Type of Change

RFC

Summary

Coming out of #952 (and now while working on the new Greenwood website), wanted to track some specific thoughts and ideas around accessing content as data with Greenwood and making it easier to work in HTML / markdown for happy path cases, and mostly to provide an alternative to using GraphQL as built into Greenwood.

In addition, the "graph", if that's even a good name for it, is showing its age by now and in need of some TLC.

Details

Collections

Although Greenwood has the concept of menus, which is a way to group content in the context of a navigation and whatnot, perhaps a more general purpose name going forward may a "Collection", as inspired by Astro's (Content Collections](https://docs.astro.build/en/guides/content-collections/). Ultimately it should be whatever meaning the user wants to give, be it navigation, a bunch of blog posts, or something else entirely.

Things like:

  • exposing the menu and children query as JavaScript as standalone functions / helpers
  • making it possible to do next / prev links (like for sequential steps for tutorials, blogs, etc)
  • listing out a table of contents

(I think Menu's may have added extra functionality on top of that like ordering and what not, but maybe that should just be considered a "type" of collection?)

For example, say listing out the table of contents for a blog using constructor props instead of the Children GraphQL query

// notice this is not a .gql file, but perhaps the logic could be shared?
import { getChildren } from '@greenwood/cli/src/queries/menu.js';

export default class BlogPostsPage extends HTMLElement {
  constructor(request, graph) {
    super();

    this.graph = graph;
  }

  async connectedCallback() {
    const { graph } = this;
    const blogPosts = getChildren(graph, { parent: 'blog' });

    this.innerHTML = `
      ${
         blogPosts.map((post) => {
           return `<h2>${post.title}</h2>
         }).join('');
        }
    `;
  }
}

Frontmatter

We may also want to rename it from index to order since these aren't arrays, and the starting index thing may be confusing / misleading.

---
title: 'Docs'
collection: 'nav'
order: 1
---

## Docs

Welcome to the docs page

HTML

We already have frontmatter support for markdown and JS

export async function getFrontmatter(compilation) {
 return { /* ... */
}

But might be good to see if we can extend frontmatter support to HTML as well, assuming it doesn't break all the HTML parsers

---
collection: 'nav'
order: 2
---
<html>
  <head>
    <title>Guides</title>
  </head>
  <body>
    <h2>Guides Page
  </body>
</html

Rich Frontmatter

As reported in #1229 , would be good to see how we can support "rich" frontmatter data, and to the use case presenting point, what more could we do with frontmatter in this use case, as it would be really nice to leverage this data somehow combined with a custom element

---
name: 'My Playlist'
songs:
  - title: You Can Close Your Eyes
    url: http://archive.org/download/james-taylor-montreux-jazz-fest-1988-rsr/01 You Can Close Your Eyes.mp3
  - title: Country Road
    url:  http://archive.org/download/james-taylor-montreux-jazz-fest-1988-rsr/09 Country Road.mp3
---

## My Playlist

<x-playlist
  title="${globalThis.page.name}"
  songs="${JSON.stringify(globalThis.page.songs)}"
></x-playlist>

Might also be worth revisiting this pattern as well - https://www.greenwoodjs.io/docs/configuration/#interpolate-frontmatter

Rendering Strategies / Contexts

Somewhat related to #951 , one caveat around content as data is what to about CSR use cases, like when authoring HTML. How can you get content as data into a custom element using only HTML as you will be coupled to API calls and have to handle them somehow for production builds, since the plan isn't to support an actual Greenwood GraphQL server for production, and so all data is pre-generated at build time, this making it pretty much read-only.

<!doctype html>
<html lang="en" prefix="og:http://ogp.me/ns#">
  <head>
    <title>Greenwood</title>
    <script type="module" src="../components/footer/footer.js" data-gwd-opt="static"></script>
    <script type="module" src="../components/header/header.js"></script>

    <script type="application/json">
      /* graph JSON.stringified here??? */
    </script>
    
    <script type="module">
      const graph = await fetch('./graph.json').then(resp => resp.json());
      const nav = graph
        .filter(page => page.data.collection === 'nav')
        .sort((a, b) => a.data.index > b.data.index ? 1 : -1);

      console.log({ nav });
    </script>
  </head>

  <body>
    <!-- header component my also have a nav component to share the data with too --> 
    <app-header nav="???"></app-header>
    <h1>Welcome to my website!</h1>
    <app-footer></app-footer>
  </body>
</html>

Graph Structure

There are definitely some breaking changes to be made to the graph, to better align on terminology and overall usefulness to Greenwood and users. So given a current item in the graph

{
  page: {
    data: { menu: 'side', index: 7, linkheadings: 0, tableOfContents: [] },
    filename: 'source.md',
    id: 'source',
    label: 'Source',
    imports: [],
    resources: [],
    outputPath: '/plugins/source/index.html',
    path: 'www/pages/plugins/source.md',
    route: '/my-subpath/plugins/source/',
    template: 'page',
    title: 'Source',
    isSSR: false
  }
}

I wonder if there is a case to rename this to pages instead? A graph implies nodes, some sort of linking structure, etc which it is definitely not in its current state.


Would be good to go through and evaluate all currently open and related issues

@thescientist13
Copy link
Member Author

thescientist13 commented May 26, 2024

@jstockdi
Had some time to play around with some of the ideas here and have a demo you can checkout on the new Greenwood website, demonstrating rich frontmatter and using it in markdown / HTML to pass data to Web Components.

So taking your playlist example

---
title: Playlist
name: "My Cool Playlist"
songs:
  - title: You Can Close Your Eyes
    url: http://archive.org/download/james-taylor-montreux-jazz-fest-1988-rsr/01 You Can Close Your Eyes.mp3
  - title: Country Road
    url: http://archive.org/download/james-taylor-montreux-jazz-fest-1988-rsr/09 Country Road.mp3
---

<!doctype html>
<html lang="en" prefix="og:http://ogp.me/ns#">
  <head>
    <title>Playlist</title>
    <script type="module" src="../components/player.js"></script>
  </head>

  <body>
    <app-header></app-header>
    <h1>Welcome to the Player!</h1>
    <app-player name="${globalThis.page.name}" playlist='${globalThis.page.songs}'></app-player>
    <app-footer></app-footer>
  </body>
</html>

And here's the Player component

export default class Player extends HTMLElement {
  constructor() {
    super();

    this.title = '';
    this.playlist = [];
  }

  async connectedCallback() {
    this.playlist = JSON.parse(this.getAttribute("playlist") || "[]");
    this.title = this.getAttribute("name") || "";

    if (!this.shadowRoot) {
      const list = this.playlist.map((item) => `<li>${item.title}</li>`).join("\n");
      const template = document.createElement('template');
  
      template.innerHTML = `
        <div style="text-align: left; width: 30%; margin:0 auto;">
          <h1>${this.title}</h1>
          <ul>
            ${list}
          </ul>
          <button>Start Playlist</button>
        </div>
      `;

      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    this.shadowRoot.querySelector('button')?.addEventListener('click', this.startPlaylist.bind(this));
  }

  startPlaylist() {
    const { title, playlist } = this;
    console.log('starting playlist =>', { title, playlist });
    alert(`starting playlist => ${title}`);
  }
}

You can pass frontmatter right into your component, almost thinking of it like an ESI

You can see the demo running here
https://deploy-preview-49--super-tapioca-5987ce.netlify.app/playlist/
Screenshot 2024-05-25 at 8 39 21 PM


The patches are pretty straightforward, just like you has posed in #1229 if you want to run with those, while I get this into Greenwood. I plan to include this in the next release line (v0.30.0) which is in progress (#1208) and something I hope to have wrapped up in the next couple months, but this will likely be available earlier via an alpha release, so will keep you posted.

@jstockdi
Copy link
Contributor

    <app-player name="${globalThis.page.name}" playlist='${globalThis.page.songs}'></app-player>

I dig the syntax and usage here, the patch makes sense. Thinking broadly...

Rich Frontmatter should include:

  • Ability to render non-primitives (JSON) in templates (the patch shows this)
  • Ability to have complex data in the graph (the comments above seem to think about it...)

Should there be a patch to graph.js? Possibly adding the rich front-matter in...

    data: { menu: 'side', index: 7, linkheadings: 0, tableOfContents: [] },

In the playlist example... the graph.json could get pretty large, but in other cases, making menus or supporting tags, it could be quite useful. Currently, tags have to be implemented using CSV and parsing... w/ rich front-matter, I am guessing GQL queries could pre-render then...

@thescientist13
Copy link
Member Author

thescientist13 commented May 30, 2024

Should there be a patch to graph.js? Possibly adding the rich front-matter in...

Yeah, that should be handled automatically by the frontmatter package we're using, which is what is allowing us to JSON.stringify it into the page atm. By default anything in fronmatter that isn't one of these "reserved" keys should end up in the generic data property on a page in the graph, which should include GraphQL as well.

  • label
  • title
  • template
  • imports

In the playlist example... the graph.json could get pretty large, but in other cases, making menus or supporting tags, it could be quite useful.

Yeah, the one downside to the active frontmatter approach, in particular when combined with collections, is that a lot of that data would get serialized into the page, as seen in my demo

<app-player name="My Cool Playlist" playlist="[{&quot;title&quot;:&quot;You Can Close Your Eyes&quot;,&quot;url&quot;:&quot;http://archive.org/download/james-taylor-montreux-jazz-fest-1988-rsr/01 You Can Close Your Eyes.mp3&quot;},{&quot;title&quot;:&quot;Country Road&quot;,&quot;url&quot;:&quot;http://archive.org/download/james-taylor-montreux-jazz-fest-1988-rsr/09 Country Road.mp3&quot;}]">
  <template shadowrootmode="open">
    <div style="text-align: left; width: 30%; margin:0 auto;">
      <h1>My Cool Playlist</h1>
      <ol>
        <li>You Can Close Your Eyes</li>
        <li>Country Road</li>
      </ol>
      <button>Start Playlist</button>
    </div>
  </template>
</app-player>

Which could definitely add to the HTML payload size a bit for collections, which would be graph data, but hopefully narrowed down significantly since would just be a collection (subset) of pages. And at least it would be just HTML, so would be the least "punishing" to the browser / usage. 😅

I am guessing GQL queries could pre-render then...

Yeah, there is always the ability to do this all in client-side JavaScript either using fetch against /graph.json or the GraphQL plugin. My hope as part of this issue is to also make a more ergonomic option between raw graph.json and having to use the GraphQL plugin, e.g.

// notice this is not a .gql file, but perhaps the logic could be shared?
import { getChildren } from '@greenwood/cli/src/queries/menu.js';

export default class BlogPostsPage extends HTMLElement {
  // we can pass in the graph to component, or figure out a way to get it into `getChildren`
  constructor(request, graph) {
    super();

    this.graph = graph;
  }

  async connectedCallback() {
    const { graph } = this;
    const blogPosts = getChildren(graph, { parent: 'blog' });

    this.innerHTML = `
      ${
         blogPosts.map((post) => {
           return `<h2>${post.title}</h2>
         }).join('');
        }
    `;
  }

So that way users will now have multiple options, which I think provide a good set of pros / cons and authoring preferences:

  1. Active Rich Frontmatter + Collections in static markdown / HTML (new feature)
  2. Client-Side using fetch + graph.json (already supported)
  3. Client-Side using content "functions" (new feature)
  4. GraphQL plugin (already supported)

@jstockdi
Copy link
Contributor

Whoa... Gotta stop by tonight to check this out

@thescientist13 thescientist13 unpinned this issue Nov 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
alpha.7 Content as Data documentation Greenwood specific docs feature New feature or request RFC Proposal and changes to workflows, architecture, APIs, etc v0.30.0
Projects
No open projects
2 participants