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

support custom pages through theme packs #681

Open
1 of 5 tasks
thescientist13 opened this issue Aug 3, 2021 · 1 comment
Open
1 of 5 tasks

support custom pages through theme packs #681

thescientist13 opened this issue Aug 3, 2021 · 1 comment
Assignees
Labels
documentation Greenwood specific docs feature New feature or request Plugins Greenwood Plugins
Milestone

Comments

@thescientist13
Copy link
Member

thescientist13 commented Aug 3, 2021

Type of Change

  • New Feature Request
  • Documentation / Website
  • Improvement / Suggestion
  • Bug
  • Other (please clarify below)

Summary

Coming out of #570 / #669 was that currently Context plugins (or more specifically theme packs) are limited to just providing additional templates/ directories. Although Greenwood can effectively resolve anything during development, it can only build pages that are part of its graph, so being able to ship an actual page as part of a plugin is not possible and is already being tracked as part of #21 . (so this may be more of a reminder to update the docs / examples is all, but may require some work as well, we shall see)

Details

For example, like in greenwood-starter-presentation I would like to provide the actual home / landing / index.html page for users, to act as the entire interface to all their content (slides).

<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="/components/presenter-mode.js"></script>
    <script type="module" src="/components/slide-list.js"></script>
    <script type="module" src="/components/slide-viewer.js"></script>
    <script type="module">
      import client from '@greenwood/plugin-graphql/core/client';
      import GraphQuery from '@greenwood/plugin-graphql/queries/graph';
      
      client.query({
        query: GraphQuery
      }).then((response) => {
        const urlParams = new URLSearchParams(window.location.search);
        const selectedSlideId = urlParams.get('selectedSlideId');
        const slides = response.data.graph.filter(slide => slide.id !== 'index');
        const currentSlideIndex = selectedSlideId ? slides.findIndex(slide => slide.id === selectedSlideId) : 0;

        document.querySelector('presenter-mode').setAttribute('slides', JSON.stringify(slides));
        document.querySelector('slide-list').setAttribute('slides', JSON.stringify(slides));
        document.querySelector('slide-viewer').setAttribute('slide', JSON.stringify(slides[currentSlideIndex]));
      });
    </script>

    <script>
      document.addEventListener('slide-selected', (slide) => {
        document.querySelector('slide-viewer').setAttribute('slide', JSON.stringify(slide.detail));
      })
    </script>

    <style>
      body {
        background-color: #e8dcd2;
      }

      main {
        min-width: 1024px;
      }

      header {
        width: 90%;
      }

      header > * {
        float: right;
      }

      .column {
        display: flex;
        flex-direction: column;
        flex-basis: 100%;
        flex: 1;
        min-height: 300px;
      }

      .left {
        float: left;
        min-width: 23%;

      }

      .right {
        min-width: 67%;
      }

      footer {
        margin-top: 20px;
      }

      footer a {
        text-decoration: none;
      }

      footer a:visited {
        color: #020202;
      }

      footer h4, header h1 {
        width: 90%;
        margin: 0 auto;
        padding: 0;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <main>
      <section>
        <header>
          <presenter-mode></presenter-mode>
        </header>
      </section>
      
      <section>
        <div class="column left">
          <h1>Slides</h1>
          <slide-list></slide-list>
        </div>

        <div class="column right">
          <h1>Current Slide</h1>
          <slide-viewer></slide-viewer>
        </div>
      </section>

      <section>
        <footer>
          <h4>
            <a href="https://www.greenwoodjs.io/">GWD/PPT &#9672 Built with GreenwoodJS</a>
          </h4>
        </footer>
      </section>
    </main>
  </body>
</html>

So from a user perspective, they would literally only have this for their directory layout, with everything else coming from the plugin (note: you can pull in overrides on a per page already! 💥 )

Literally a user would only actually have this in their repo

src/
  assets/
    background.png 
  pages/
    slides/
      1.md
      2.md
      .
      .
   styles/
     overrides.css
@thescientist13 thescientist13 self-assigned this Aug 3, 2021
@thescientist13 thescientist13 added documentation Greenwood specific docs Plugins Greenwood Plugins labels Aug 3, 2021
@thescientist13 thescientist13 added this to the 1.0 milestone Aug 3, 2021
@thescientist13 thescientist13 changed the title support custom pages through support custom pages through configuration and / or plugins Aug 3, 2021
@thescientist13 thescientist13 changed the title support custom pages through configuration and / or plugins support custom pages through theme packs Aug 3, 2021
@thescientist13
Copy link
Member Author

thescientist13 commented Aug 10, 2021

A couple options I attempted at the moment, as we don't have support for #21 yet.

Using a Resource

One option I tried was using Resource in a couple ways, as the primary advantage here is that the user of the theme pack doesn't have to do anything, the plugin author can handle this entirely from their end.

Resolve

One way could be to just "hijack" the path to / and override by providing a path to index.html

class ThemePackPageOverrideResource extends ResourceInterface {
  constructor(compilation, options) {
    super(compilation, options);
    this.extensions = ['.html'];
  }

  async shouldResolve(url) {
    console.debug('ThemePackPageOverrideResource shouldResolve', url)
    return Promise.resolve(url.replace(this.compilation.context.userWorkspace, '') === '/');
  }

  async resolve(url) {
    return Promise.resolve(path.join(__dirname, 'dist/pages/index.html'));
  }
}

However, it looks like Greenwood heavily filters what counts as a page so maybe we have to add some sort of "simple" check like

path.extname(url) === 'html' && fs.existsSync(url)

After getting it to work

% git diff
diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js
index ed181169..39b0ae81 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-html.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-html.js
@@ -30,16 +30,21 @@ const getPageTemplate = (barePath, templatesDir, template, contextPlugins = [])
   const customPluginDefaultPageTemplates = getCustomPageTemplates(contextPlugins, 'page');
   const customPluginPageTemplates = getCustomPageTemplates(contextPlugins, template);

+  console.debug('pageIsHtmlPath', pageIsHtmlPath);
+  console.debug('barePath', barePath);
   if (template && customPluginPageTemplates.length > 0 || fs.existsSync(`${templatesDir}/${template}.html`)) {
     // use a custom template, usually from markdown frontmatter
     contents = customPluginPageTemplates.length > 0
       ? fs.readFileSync(`${customPluginPageTemplates[0]}/${template}.html`, 'utf-8')
       : fs.readFileSync(`${templatesDir}/${template}.html`, 'utf-8');
-  } else if (fs.existsSync(`${barePath}.html`) || fs.existsSync(pageIsHtmlPath)) {
+  } else if (fs.existsSync(barePath) || fs.existsSync(`${barePath}.html`) || fs.existsSync(pageIsHtmlPath)) {
     // if the page is already HTML, use that as the template
+    console.debug('// if the page is already HTML, use that as the template');
     const indexPath = fs.existsSync(pageIsHtmlPath)
       ? pageIsHtmlPath
-      : `${barePath}.html`;
+      : fs.existsSync(barePath)
+        ? barePath
+        :`${barePath}.html`;

     contents = fs.readFileSync(indexPath, 'utf-8');
   } else if (customPluginDefaultPageTemplates.length > 0 || fs.existsSync(`${templatesDir}/page.html`)) {
@@ -269,12 +274,16 @@ class StandardHtmlResource extends ResourceInterface {
       ? `${pagesDir}${relativeUrl}index`
       : `${pagesDir}${relativeUrl.replace('.html', '')}`;

-    return Promise.resolve(this.extensions.indexOf(path.extname(relativeUrl)) >= 0 || path.extname(relative
Url) === '') &&
-    (fs.existsSync(`${barePath}.html`) || barePath.substring(barePath.length - 5, barePath.length) === 'ind
ex')
-    || fs.existsSync(`${barePath}.md`) || fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf(`${pa
th.sep}index`))}.md`);
+    return Promise.resolve((fs.existsSync(url) && path.extname(url)) === '.html' ||
+      this.extensions.indexOf(path.extname(relativeUrl)) >= 0 || path.extname(relativeUrl) === '') &&
+      (fs.existsSync(`${barePath}.html`) || barePath.substring(barePath.length - 5, barePath.length) === 'i
ndex')
+      || fs.existsSync(`${barePath}.md`) || fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf(`${
path.sep}index`))}.md`);
   }

   async serve(url) {
+    console.debug('serve HTML url@@@@@@@@@@@@@@@', url);
+    console.debug('text', path.extname(url));
+    console.debug('true', fs.existsSync(url) && path.extname(url) === '.html');
     return new Promise(async (resolve, reject) => {
       try {
         const config = Object.assign({}, this.compilation.config);
@@ -286,14 +295,17 @@ class StandardHtmlResource extends ResourceInterface {
         let body = '';
         let template = null;
         let processedMarkdown = null;
-        const barePath = normalizedUrl.endsWith(path.sep)
-          ? `${pagesDir}${normalizedUrl}index`
-          : `${pagesDir}${normalizedUrl.replace('.html', '')}`;
+        const barePath = fs.existsSync(url) && path.extname(url) === '.html'
+          ? url
+          : normalizedUrl.endsWith(path.sep)
+            ? `${pagesDir}${normalizedUrl}index`
+            : `${pagesDir}${normalizedUrl.replace('.html', '')}`;
         const isMarkdownContent = fs.existsSync(`${barePath}.md`)
           || fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf(`${path.sep}index`))}.md`)
           || fs.existsSync(`${barePath.replace(`${path.sep}index`, '.md')}`);

-        if (isMarkdownContent) {
+        console.debug('barePath??????', barePath);
+        if (!(fs.existsSync(url) && path.extname(url) === '.html') && isMarkdownContent) {
           const markdownPath = fs.existsSync(`${barePath}.md`)
             ? `${barePath}.md`
             : fs.existsSync(`${barePath.substring(0, barePath.lastIndexOf(`${path.sep}index`))}.md`)

seeing this issue now running yarn serve though 😫

done prerendering all pages
copying assets/ directory...
copying graph.json...
Initializing project config
Initializing project workspace contexts
Generating graph of workspace files...
Started production test server at localhost:8080

  Error: ENOENT: no such file or directory, open '/Users/owenbuckley/Workspace/github/repos/knowing-your-tco/public/index.html'

Intercept

Trying to intercept on the default HTML response from plugin-standard-html so as to re-write it on the fly. Unfortunately, this takes the page out of the flow of the templating process that's done in the earlier serve phase.

class ThemePackPageOverrideResource extends ResourceInterface {
  constructor(compilation, options) {
    super(compilation, options);
    this.extensions = ['*'];
  }

  async shouldIntercept(url) {
    console.debug('ThemePackPageOverrideResource shouldIntercept', url)
    return Promise.resolve(url.replace(this.compilation.context.userWorkspace, '') === '/');
  }

  async intercept(url) {
    console.debug('ThemePackPageOverrideResource intercept', url)
    return Promise.resolve({ body: 'hello world!'});
  }
}

This means that you get the content of index.html but without

  • import map
  • app template merged to join a <head> and <body> tags
  • whatever else Greenwood does to assemble a page

So this just means that the plugin author just has to merge the content of their own index.html into the default returned by Greenwood. I suppose since Greenwood bring along with it an html parser, plugins could piggy back off this and do it just like Greenwood does it?

Templates

Another quick option, the would require user "intervention" is by publishing index.html as a template and then having the user specify that in their project.

src/
  assets/
    background.png 
  pages/
    index.md
    slides/
      1.md
      2.md
      .
      .
   styles/
     overrides.css

index.md

---
template: 'index'
---

Naturally the downside here is the user has to do something on their side, but it's a pretty low effort / impact step, and is explicit, as opposed to the above solution, which is very much implicit. However, in this case, this isn't really a template, it's the home / landing page so... 🤷‍♂️

I suppose the better alternative will be when we can allow direct users of Greenwood to push content directly into the graph, but I think it doesn't necessarily hurts if Greenwood is robust enough to allow for a large degree of flexibility in how its APIs are used. As long as they are clear and stable and consistent, it should be a net positive for everyone.


Either way, here are something we can document for now at least.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Greenwood specific docs feature New feature or request Plugins Greenwood Plugins
Projects
Status: 📋 Backlog
Development

No branches or pull requests

1 participant