toastdotdev/toast

page wrapper API changes

Closed this issue · 1 comments

As a developer using Toast, I want to be able to achieve the following:

  • no page wrapper on this one page
  • the default page wrapper on all pages (unless I say otherwise)
  • a different page wrapper on a subset of pages
    • specifically "this page gets a different wrapper"
    • maybe "this subset of glob matches gets X page-wrapper"
  • all of the above, but for pages that are in src/pages and not run through createPage

A notable point is that in other frameworks, I've had the frustration of working with different processing pipelines for createPage and src/pages analogues and I'd like to avoid that here.

If we consider src/pages to be "createPage", then it is sugar on top of createPage that leads us toward modularized apis (aka themes, or whatever you want to call them). By making src/pages feed into createPage people will need the ability to override the page wrapper for specific pages in src/pages. While you already have control over this if you use createPage directly, src/pages CreatePage structs will need to be overridden or shadowable through user-defined mechanisms.

This leads to a priority facade where src/pages is considered "less truthful" than user createPage calls. If there is a facade here, then we should be able to trace all changes to the data over time and show that to the user. "X was modified by Y at Z" effectively creating an event sourced system with a full audit log?


for createPage it makes sense to add a pageWrapper field I think, resulting in the following API:

createPage({
  slug: "/somewhere",
  component: <source-code-as-a-string>,
  data: {...},
  wrapper: <source-code-as-a-string>
})

This allows wrappers to come from remote sources, which is nice, but makes working with local files a bit more painful. Pulling in src/page-wrapper.js requires an fs.readFile and this is something users are likely to get wrong (ex: readFile on every createPage call).

To this end, there should likely be sugar on top of this API that accepts filepaths instead of component strings. Then we can get the logic for reading files in right in the framework, making it easier to build performant data pipelines for users.

createPage({
  slug: "/somewhere",
  component: <source-code-as-a-string> | <filepath>,
  data: {...},
  wrapper: <source-code-as-a-string> | <filepath> | false
})

I think this API is OK, but potentially has implications for determining whether something is a filepath or a component string. There are crates like Url that can assist with this, but if the URL is malformatted and it registers as a component, that results in confusing error messages for the user (because Toast won't know and has to default to something). It might be better to allow two fields or a discriminator so the user can indicate their intent, which results in better error messages specific to the intended behavior.

{
  value: <source-code-as-string> | <filepath-relative-to-src-dir>,
  mode: 'filepath' | 'source'
}
createPage({
  slug: "/somewhere",
  component: {
    value: 'page-wrapper.js',
    mode: 'filepath',
  },
  data: {...},
  wrapper:  {
    value: <some-component-source-code>,
    mode: 'source',
  },
})

This could theoretically also be extended in the future:

{
  value: <source-code-as-string> | <filepath-relative-to-src-dir>,
  mode: 'filepath' | 'source',
  type: 'default' | 'vue-sfc' | 'something-else'
}

src/pages

So now that the createPage API is worked out, the src/pages needs an "override"-ability. I think this works out if successive createPage calls merge over the top of existing values and user createPage calls "outrank" src/pages.

so src/pages/index.js is effectively:

createPage({
  slug: '/',
  component: {
    value: 'pages/index.js',
    type: 'filepath'
  }
})

and then users can do this to overlay on top.

createPage({
  slug: '/',
  data: {...},
  wrapper: <some-code> | false
})

That brings up a different issue, in that setData is now basically just an "extra function" because createPage can be used with this, which is exactly what setData does.

createPage({
  slug: '/',
  data: {...}
})

We have to handle data for pages anyway, so this doesn't introduce any extra complexity that wouldn't already be there, but it does mean that createPage is a weird name for an overloaded function that can do multiple things.

the new API is setDataForSlug and is designed as described above with the caveat that we moved slug out to the first positional argument as it is always required anyway.

setDataForSlug("/", {
  component: {},
  data: {},
  wrapper: {}
})