dadi / publish

Publish provides beautiful editorial interfaces for the management of content within API
https://dadi.cloud/en/publish
64 stars 13 forks source link

Article preview #509

Open paulrgn opened 5 years ago

paulrgn commented 5 years ago

As an editor I would like to be able to preview articles before publishing so that I can check content presentation. This does not need to be a literal interpretation of the page, just enough to see how the copy is presented with all markdown styling/elements applied. It would also be useful to see at least an approximation of presentation on different devices (perhaps by adjusting the browser window, for example).

Should this be a built-in feature of Publish (like a split screen markdown editor) or should Publish offer the easy plug-in of such a system for developers building a site using our tech?

Discuss.

abovedave commented 5 years ago

In the same vein of these tight coupling with Web + API, would be cool to get a link to the document in the front end

abovedave commented 5 years ago

Wondering if the answer is some kind of internal publish fields in API? Similar to Craft and Wordpress

screenshot 2018-09-25 at 15 49 05 screenshot 2018-09-25 at 15 49 30
eduardoboucas commented 5 years ago

I'm personally not a fan of the preview link route because I think it creates dangerous precedents in terms of coupling and, most importantly, it makes a lot of bad assumptions.

  1. For Publish to generate a preview URL, it needs to know where the final content is published, so it needs to learn about where Web (or similar) lives. And that assumes that the content will be published to one single location, but the reality is that a single API entry can end up in various different channels. Ultimately, I don't think Publish should know or care where the content will end up.

  2. It assumes that it's even possible to generate a preview URL. What if you're not publishing for the Web and your article will be rendered on a native app or similar? And what if you want to protect the preview URLs behind some sort of authentication system, so that the people can't access content that isn't meant to be published? We're talking about using an external source, so your session with Publish won't be of any use.

  3. Relying on an external service to generate the previews doesn't feel compatible with the level of responsiveness we've added to the UI. I would expect a real-time visualisation of the content as I'm creating it – if I need to wait for a round-trip to an externa server and for the resulting page to be rendered, we sort of lose that effect.

  4. Finally, using an external service means that only content that exists in API can be previewed. If I'm working on content that is only saved locally on my machine, it will be impossible to preview. Sure, we can introduce a "published state" field that flags documents as not ready for public consumption, but again that's assuming a lot. Not only that concept doesn't exist in API, but also I might not want to have that in my API installation at all.

My preference would be for Publish to have the concept of preview templates, whereby users have the ability to supply, in their workspace directories, one or more templates for each of their collections, defining how they are rendered for preview purposes. This could be a faithful representation of the layout used in the live website, or it could be some sort of iPhone simulator chrome that shows how the article will be rendered on a native app.

danwdart commented 5 years ago

So presumably that's what we're going for - there'll be a user-themed file and a placeholder for where the content gets rendered. A tutorial on how to do that for the specific collection could be shown in a sidebar if one does not exist, otherwise the sidebar could show the information in a resizable component in a sort of "here's what it'll look like when you're done" similar to a live-view markdown preview in a text editor. Is that the plan?

anaumovets commented 5 years ago

Template files I guess, single file won't be enough for the template, we'll need at least the main .html file, style.css file, probably some header/footer images, probably some more. I suggest these are uploaded via FieldFile, which should be like our current FieldImage, but allowing arbitrary file uploads, showing filenames instead of thumbnails and not allowing to select existing files. Perhaps FieldImage should be replaced with FieldFile with the relevant props.

Uploading templates I suppose uploading templates separately for each article is a bad idea. So they should be made into a separate collection and selected by reference in the article's Meta (or Details?).

Previewing A "Preview" button at the bottom of the article editing interface will link to /articles/article_id/preview, where the article preview with the chosen template will be shown.

Conventions When there are multiple files uploaded, we need to figure out which file to use as the main template file. If there's only one .html -- use it, or template.html if there's more than one, show error otherwise. The article will be injected into the element with id='article'. We can make it customizable later if necessary.

If this makes sense, I'll break it up into smaller parts and proceed.

eduardoboucas commented 5 years ago

Thanks for your input, guys.

I'm not sure that uploading these templates via Publish is the best approach. Where would these files be uploaded to? Currently, Publish will only upload files to API and in the case of document previews we're trying to make Publish as independent as possible from other applications.

Also, bear in mind that the application ships with a compiled bundle. It's not trivial to make it import user-generated files in order to process/compile them. For this reason, I suggest a templating layer that is fully decoupled from the application core.

I envision this as factory functions attached to the global window object, capable of taking a candidate document for a given collection and returning the HTML for a fully-rendered preview. The Publish core is not responsible for rendering these templates, it's just responsible for looking for them on the global scope, calling them and adding the generated output to the preview window.

// Given a candidate document `testDocument` and a collection with path `collectionPath`:
if (window.previewTemplates[collectionPath] {
  let previewHTML = window.previewTemplates[collectionPath](document)

  renderInAPreactComponent(previewHTML)
}

This approach means that Publish is not prescriptive as to what templating language is used. If users are into React, they can build React components (and a separate transpilation/build pipeline); if they're into something like Pug or Dust.js, they can have that too. As long as templates export the right functions, Publish is fine with it.

As a side note, I would like to use a very similar approach for #299.

anaumovets commented 5 years ago

I get the point that we need to allow usage of any framework/template language the user likes. That means that the article document can contain arbitrary markup language rather than just HTML - is that right? But how does user provide the previewTemplates function and the templating layer in general? Do we have a mechanism for that?

eduardoboucas commented 5 years ago

I started putting a prototype together, but since it involves passing down information from the host application (i.e. the application on user land that includes the @dadi/publish npm module and calls app.run()) it's a bit of a pain. Instead, I'm describing the various steps involved.

Part 1: Store template files

Users would be asked to place their preview template files inside workspace/preview in the host application. They should follow the same path as the API collections they relate to – for example, the preview template for /1.0/cloud/articles should be placed on workspace/preview/1.0/cloud/articles/index.js.

The entry point for a template is always a JavaScript file called index.js that calls a global registerPreviewTemplate function with the name of the collection and the templating function as arguments.

workspace/preview/1.0/cloud/articles/index.js

window.registerPreviewTemplate('1.0/cloud/articles', document => {
  return `<html><body><h1>${document.title}</h1></body></html>`
})

The function above could be as complex as needed, including markup for CSS files, additional JavaScript files or even require calls for templating engine modules (note that any bundling required would be handled by the user, not by Publish).

Part 2: Load template files

Here we are augmenting the index.html file with some global variables that the Preact application will read, such as routes and configuration parameters. In here, we would need to add our global registerPreviewTemplate function, which in an initial release could be as simple as taking calls and adding them to a global hash map for preview templates.

window.registerPreviewTemplate = function (collection, factoryFn) {
  window.previewTemplates[collection] = factoryFn
}

We could also add a function for rendering a preview template, if available. Yes, we could directly access the hash map both for reading and writing, but having getter/setter functions makes it more future-proof.

window.renderPreviewTemplate = function (collection, document) {
  return window.previewTemplates[collection] && window.previewTemplates[collection](document)
}

Part 3: Use template files

Whenever we need to render a preview for a collection in our Preact components, we can call the globally-available window.renderPreviewTemplate method, passing it the name of the collection we're trying to preview and the candidate document.

containers/DocumentEdit.jsx

render () {
  return (
    <div ref={el => this.previewWrapper} />
  )
}

renderPreview (document) {
  let previewHTML = window.renderPreviewTemplate(document) || 'No preview available'

  this.previewWrapper.innerHTML = previewHTML
}

Does that make sense?

/cc @jimlambie @abovedave

danwdart commented 5 years ago

The above makes sense to me. There's not going to be a case of not having access to the app which require()s Publish (so any hopes of just passing plain Publish a config file via npx or something are dashed), so that's where local Publish files should go. This might also make it easier to pipeline build.

anaumovets commented 5 years ago

I'd like to make sure I understand how things operate overall. Correct me if I'm wrong:

  1. There's Publish app used for creating the articles.
  2. There's an API instance to which the articles are written.
  3. App that takes care of templating and serves the articles from the API (most probably dadi Web, but can be native or something else)

There are two issues I can see, though they are not critical and probably acceptable:

  1. To see the previews correctly, user will need to make sure that templating is identical to the one made by the web app.

Edit: I've had a look at Dust.js and pug, and it looks like using synchronous functions should be ok in most cases. So the issue below is not relevant. [2. Very likely, there will be some bundling involved, so I suppose the template preview functions should be asynchronous. Thus handling the preview rendering will be trickier than let previewHTML = window.renderPreviewTemplate(document). On the positive side this means that if we aren't too concerned about responsiveness, we can even simply query the web app to get the generated html.]

I'll build a prototype for this.