toastdotdev/toast

Extensions to SetDataForSlug: mime types and ssr/client restrictions

Opened this issue · 4 comments

SetDataForSlug

Currently the Rust types for setDataForSlug() as used in the JS environment look like this

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(tag = "mode")]
pub enum ModuleSpec {
    // users should see this as `component: null`
    #[serde(alias = "no-module")]
    NoModule,
    #[serde(alias = "filepath")]
    File {
        #[serde(alias = "value")]
        path: PathBuf,
    },
    #[serde(alias = "source")]
    Source {
        #[serde(alias = "value")]
        code: String,
    },
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SetDataForSlug {
    /// /some/url or some/url
    pub slug: String,
    pub component: Option<ModuleSpec>,
    pub data: Option<serde_json::Value>,
    pub wrapper: Option<ModuleSpec>,
}

Module Spec

I'm going to ignore the slug for now, which gives us a JSON object for ModuleSpec that looks like one of these three options.

{
  mode: "no-module"
}
{
  mode: "filepath",
  value: "./some/file.js"
}
{
  mode: "source",
  value: "import { h } from 'preact'; export default props => <div>hi</div>"
}

This covers many needs. For example it allows:

  • MDX from a remote CMS using a source component.
    {
      component: {
        mode: "source",
        value: mdxAsJsx
      }
    }
  • The ability to render HTML from markdown through a simple source component. (The markdown could also be injected into the component itself, or converted into js and used directly as a component).
    {
      component: {
        mode: "source",
        value: `import { h } from 'preact';
    export default props => <div dangerouslySetInnerHTML={{ __html: markdownFromRemark }}/>`
      },
      data: {
        markdownFromRemark: "some html string"
      }
    }

So we cover "ways of acquiring content":

  • no content
  • from a file
  • from a string

and currently assume that you're passing in a JavaScript type (a .js file or a js source string).

What it doesn't cover

SSR vs client-side

  • server-side only components
    • components (or wrappers) that are rendered on the server and not shipped to the client.
  • client-side only components?
    • I don't know what this would be used for. It's here for completeness at the moment
  • different server and client-side components
    • seems complicated but maybe valuable in the way of "use react-helmet on the server but omit it on the client" sort of way.

Possible extension

{
  mode: "filepath",
  value: "./some/file.js",
  server: true, // default
  client: true, // default
}
{
  mode: "filepath",
  value: "./some/file.js",
  client: false,
}
{
  mode: "filepath",
  value: "./some/file.js",
  server: false,
}

different content types

We also don't support other content types when used directly.

  • Vue SFC
    {
      component: {
        mode: "source",
        value: `
    <template lang="jade">
    div
      p {{ greeting }} World!
      OtherComponent
    </template>
    
    <script>
    import OtherComponent from './OtherComponent.vue'
    export default {
      components: {
        OtherComponent
      },
      data () {
        return {
          greeting: 'Hello'
        }
      }
    }
    </script>
    
    <style lang="stylus" scoped>
    p
      font-size 2em
      text-align center
    </style>`
      }
    }
  • Markdown
    {
      component: {
        mode: "source",
        value: `# hello
    some stuff
    with <span>html</span>`
      }
    }
  • MDX
    {
      component: {
        mode: "source",
        value: `# hello
    
    <AThing/>`
      }
    }
  • HTML
    {
      component: {
        mode: "source",
        value: `<div>I got this from somewhere else... wordpress maybe</div>`
      }
    }
  • latex
    I only mention latex to think about "what would external plugins look like?"

Possible extension

maybe we use mime types? This would require custom types for mdx and vue sfc but it would also ensure we don't conflict with existing future additions and accidentally create our own format

{
  mode: "filepath",
  value: "./some/file.mdx",
  mediaType: "text/mdx",
}
{
  mode: "filepath",
  value: "./some/file.html",
  mediaType: "text/html",
}
{
  mode: "filepath",
  value: "./some/file.js",
  mediaType: "application/js", // default
}
{
  mode: "filepath",
  value: "./some/file.md",
  mediaType: "text/markdown",
}

I like this 💜

RE: different client/server rendering:

seems complicated but maybe valuable in the way of "use react-helmet on the server but omit it on the client" sort of way.

my use case: for a lot of my blog posts, I use MDX to enhance the posts with extra markup, but I don't need any interactivity at all — it would be great if I could tell Toast to only generate the HTML and skip the rehydration since I don't need it on those pages

"different server and client-side components" in this description is meant to be

{
  server: {
      mode: "filepath",
      value: "./some/file.js",
  },
  client: {
      mode: "filepath",
      value: "./another/different/file.js",
  },
}

Which I'm not sure has much value. I guess it could if you wanted to run some arbitrary script on the client, but generate the html on the server.

it would be great if I could tell Toast to only generate the HTML and skip the rehydration

Would be:

{
  mode: "filepath",
  value: "./some/file.js",
  server: true, // default
  client: false
}

in cases like I ran into with CJS/ESM compat problems, I can see this being a not-great-but-maybe-more-approachable-than-patch-package option?

client: false is exactly what I was hoping for! in that scenario, does it skip everything? page-wrapper, etc. to end up being equivalent to serving the page with JS disabled?

Another note here: setDataForSlug currently focuses on pre-render-able content. That is, if you have .mdx content that is not page-level content (maybe about.mdx for an author's bio), it should not be pre-rendered.

setDataForSlug("/fragments/about", {
  prerender: false,
  component: {...},
  data: {...},
  wrapper: {...}
});

prerender precludes wrapper though (in the current model, if it's not prerendered, it wouldn't get wrapped), so it's awkward to have them both at the same level.


So it feels like setDataForSlug is getting pretty heavy. I still feel like learning "one API" is better than "five APIs" for users (one could imagine setData, setComponent, setPage, etc), but only if we can keep the options coherent (that is, so that two mutually exclusive options are not usable at the same time) as incoherent options would be confusing for users, on top of needing to understand the API itself.

A no-prerender API would also replace webpack loaders to an extent.