prismicio / prismic-gatsby

Gatsby plugins for building websites using Prismic
https://prismic.io/docs/technologies/gatsby
Apache License 2.0
313 stars 96 forks source link

Preview feature? #22

Closed tremby closed 4 years ago

tremby commented 6 years ago

Is there any way to get Prismic's in-situ preview feature working with a (deployed) Gatsby site?

MathRivest commented 6 years ago

Did you figure it out?

tremby commented 6 years ago

No. I haven't used gatsby-source-prismic, and won't unless I hear that the preview is possible.

angeloashmore commented 6 years ago

Hey @tremby, sorry for the late reply. From what I can tell, I don't see how Prismic's preview system can work with Gatsby's build system. If you have an idea of how this could work, please feel free to post some ideas in here.

Prismic sends a preview token via HTTP that should be used as the ref when fetching the document data. This assumes a server exists that can retrieve that token, make a request to Prismic to fetch the document, and render the page using that document. Since Gatsby gathers all document data at build time, it does not have a way to grab the document preview.

As an alternative, it would be possible to create a custom page in Gatsby that retrieves the ref in the URL query and makes the appropriate Prismic API calls to render the page. In other words, the page would not be static and would require making network calls directly to Prismic to fill in content areas.

Ideally Prismic would allow querying for documents at the "Draft" level through the API without needing to receive the correct ref to do so. I.e. if we could request a preview ref on demand, creating a separate "staging" build of your site would be possible. This is similar to what Contentful does with its preview.contentful.com API.

tremby commented 6 years ago

I'm aware of how Prismic's preview feature works -- I've used it front-end with the Prismic Javascript SDK and back-end with the PHP SDK in the past.

As an alternative, it would be possible to create a custom page in Gatsby that retrieves the ref in the URL query and makes the appropriate Prismic API calls to render the page. In other words, the page would not be static and would require making network calls directly to Prismic to fill in content areas.

Yes, this is the sort of thing I had in mind. I imagine the Gatsby site handling the preview signal from Prismic and rendering the page all client-side. I don't know what sort of challenges this might entail -- perhaps none of the logic to turn a Prismic API response into data the Gatsby pages can render isn't bundled to the client side, for example. But this is what I'm asking about.

Ideally Prismic would allow querying for documents at the "Draft" level through the API without needing to receive the correct ref to do so. I.e. if we could request a preview ref on demand, creating a separate "staging" build of your site would be possible. This is similar to what Contentful does with it's preview.contentful.com API.

This isn't really valuable to me. The way I use Prismic (and the way I believe my clients use it) is to prepare changes and hit the "preview" button from the Prismic UI to view the preview, or to stage a bunch of changes as a release then hit the "preview" button to preview the entire release. Both ways use pass the ref to the website the normal "preview" way, and don't require a different endpoint to be set up.

angeloashmore commented 6 years ago

I am not 100% clear on how the preview feature works, so please correct me if anything I say is wrong.

I see two ways this could be done:

Provide a guide for a separate "Preview"/Drafts build

Gatsby only pulls data at build time, so the only way to render the preview document data is to trigger a new build with that ref. This would require a server that can listen to the request from Prismic, grab the ref, and provide that ref to the source plugin via gatsby-config.js using some clever CLI args or temporary file. Doing this means the preview would take about a minute or two to be available depending on how long the build takes to process. This isn't terrible, but not great either.

This should require minimal changes to the source plugin, but more documentation on how to set it up.

Create a "dynamic" endpoint to display preview data using existing templates

Since Prismic will send a request to /preview, one would need to create a preview page that does the following:

  1. Parses the URL query for the ref and document slug/URL
  2. Fetches the document data from Prismic
  3. Stores the data in some app-wide store (e.g. Redux)
  4. Redirects to the final destination

If you changed the reference to this.props.data in your pages' render function to check the store for content before using this.props.data, you could alter the content displayed. This would mean changing every page/template that uses this.props.data – not too hard, but something to think about. This also would not work for previewing new pages that weren't built in Gatsby before deploying.

I think this method gets pretty messy and makes your site highly dependent on a single service, but the benefit of having a live preview is nice.

Any thoughts on either approach?

tremby commented 6 years ago

The former probably isn't valuable to me, unless there was a really easy-to-deploy script for this I could put on AWS Lambda or something -- I wouldn't want to run a server -- and as long as it would be very fast to run. The latter approach seems more valuable and is more like what I had in mind, but it sounds like you have some reservations, and yes, it does look like it'd make building the site more complicated.

MadsMadsDk commented 6 years ago

This is also functionality I'm looking for. There's actually a somewhat comprehensive guide for doing preview buttons on a Prismic site here: https://prismic.io/docs/reactjs/beyond-the-api/in-website-preview

Maybe we could do some kind of reverse engineering, to trigger preview builds? I'm experimenting with this plugin, along with netlify, which allows custom deployment contexts.

I'm imagining something like this:

  1. We hit the preview button in Prismic, and enter our preview site.
  2. Our preview component fetches the ref, and calls a function (e.g. AWS Lambda, which Netlify can also handle).
  3. Our Lambda calls a new webhook on Netlify, passing the ref to the build context, which in turn calls a gatsby build command, with the ref as an argument.
  4. gatsby-prismic-source uses the passed ref argument, to build the site.

This might be a cumbersome work around, but you eliminate the need to update a state with your new data. I'll look in to this tomorrow, and report on any findings :)

AdamPflug commented 6 years ago

I've run into this issue as well. When you get Prismic previews working the way they're intended, it's really awesome for everyone (editors, approvers, devs, etc). I've done it either all on the front-end with a create-react-app SPA, and on the back-end as well. It's not too challenging in those environments because your code already knows how to talk to the API, so it just needs to do it with an extra ref parameter (and maybe disable some caching for the current session). The challenge for getting it to work with Gatsby is that you're being fed data from the GraphQL store and not directly requesting it from the API - so it's frozen in the state it was at during the last build. This is what makes it a static site and not just an SPA.

Right now it seems like @angeloashmore is right. You either need to write separate logic to fetch the data for previews (this isn't really a great solution, too much duplicated code) or you need to rebuild the site static for each new preview ref (this might be possible with something like deploy previews on Netlify, but it's slow because Gastby needs to fetch & process all the data and render all the pages before the preview is available.)

I don't think either solution is really all that great frankly. You either more than double your work for fetching prismic data (plus, add a bunch more opportunities for bugs to show up), or you make editors wait a few minutes before they can preview any changes (while simultaneously making your staging/continuous integration setups more complicated).

This isn't really a unique problem to Gatsby - it's an issue with previews for static site generators in general (the fact that Gastby serves the logic and data separately makes it seem like it's closer than other static site generators... but it's really not because so much processing needs to happen to the data to enable the GraphQL stuff). Prismic also makes the problem more obvious than something like Contentful because they have such a dynamic preview system.

The question I guess is: how can we improve on this situation. The Prismic team seems like they'd be willing to do work to support a solution if we could come up with one. Similarly, there has been some talk about preview functionality over in the Gatsby repository: https://github.com/gatsbyjs/gatsby/issues/2271#issuecomment-333407277 https://github.com/gatsbyjs/gatsby/issues/2847

If we look at other repositories for guidance we get some potential steps. Contentful basically uses a simplified preview model that serves all draft changes with a single preview key. Prismic could provide something similar by giving us an access key to pull content in from master+all releases (this is actually a complicated problem because releases can have conflicting changes - particularly if you're using experiments). That would let you set up a single preview environment, but you lose per-environment and per-document previews - which are a really nice features of Prismic. It also doesn't solve the problem of making editors wait a few minutes before the preview is ready to view (but at least they might not need to ask for it before the build kicks off?).

The other thing Contentful provides is a sync API, which essentially lets to you diff the local data you have against server's copy, so you can pull down only what's changed since your last sync. This combined with some of the work that's been happening on the Gatsby side to enable new data to be loaded while running in develop mode might help improve the problem with preview speed - you don't need to re-build the javascript/css/image assets and you can only pull what's changed when re-building the data, so that should speed things up significantly. Of course that still requires a completely different hosting architecture for your preview environment (one that keeps the service running) but at least you don't have to write a bunch of new code.

It seems like the ideal solution would be if Gatsby provided some kind of support for this kind of thing more directly. Perhaps some way for data at run time to be fetched directly from the appropriate APIs (potentially requiring those APIs to support GraphQL? or for the source plugins to support the translation from GraphQL queries into efficient API calls and reformat the responses) rather than needing the pre-processed static versions of the data that are built from a full content download. This would let you load a static placeholder "/preview" route that then switches to loading the preview data in dynamically and redirecting you to the final route client-side. This seems like a lot of work though and I wouldn't know where to start. It also gets rid of the benefits of being able to cache the page data you usually need at CDN edges, so it probably would need to be an option.

Does anyone have better ideas?

madeleineostoja commented 6 years ago

Also very keen for a preview feature, but (as @AdamPflug summed up well) I don't think it's worth doing until Gatsby has some way to refresh data without a full rebuild. That way you could run a dev/staging server and have Prismic call a webhook on save to refresh the data and do a partial rebuild to view previews. Not ideal, since it'd mean keeping a warm server running, but it'd just be for internal editors, and better than alternatives. Doing a full build of a site of any real size to preview every change isn't feasible IMO, and writing your own data layer sounds like an engineering headache that doesn't make sense in terms of effort/reward.

Second issue linked above seems to be the one to follow for Gatsby refreshing data.

angeloashmore commented 6 years ago

Check out the __refresh endpoint talked about here: https://github.com/angeloashmore/gatsby-source-prismic/issues/31

I wonder if this could help at all. The endpoint is only available when using the built-in dev server, so I wonder how feasible it is to have it constantly running as a preview server.

madeleineostoja commented 6 years ago

I think that's the best compromise - running the dev server as a staging env for previews. Not ideal, but it'd only be for internal editors, so better than nothing IMO.

angeloashmore commented 6 years ago

I came across this example site from Prismic that handles previews using the pre-v1 version of the source plugin: https://github.com/levimykel/gatsby-website/blob/87f21ddde60958b13893b41cc4aa41ea877d7141/src/templates/page.js

  1. If the URL contains a preview token in the query string, it fetches the preview token and saves it as a cookie
  2. It then redirects to the correct page URL where it checks for the cookie
  3. If the cookie is present, it fetches the document data from the Prismic API and sets the document in the template's state. If the cookie isn't present, the document data from props/Gatsby is set in state.
  4. The render function uses this.state.data rather than this.props.data, since the state can be modified with the preview data if needed.

This works well because the API response from the preview token mirrors the data schema of the pre-v1 plugin. v1 changes the schema to add some additional processing, so it is not a 1-to-1 mapping anymore.

But, we could still use the above method by running the API response through the same transformers the source plugin uses. We would just need to export/import the normalize function. The functions are pretty light, so it wouldn't add much to the overall bundle.

There are still some things to think about, like how does it handle different content types, and how would it map directly to the same accessors as through this.props.data from the GraphQL response.

AdamPflug commented 6 years ago

@angeloashmore yeah that's basically what I was suggesting above as the best we can do for now. I'd be awesome if support for something like that was built into gatsby, so source plugins could support both build time and run-time queries (not just useful for previews, but also for things like advanced filtering), and it would also probably play nicer with pages that need data from multiple sources. That way we could avoid writing duplicate query logic to handle those two cases.

In the meantime, it sounds like the Prismic team is getting close to launching a beta of native GraphQL support for their API, which may help reduce some of the overhead of maintaining two copies of your query logic somewhat.

simplesessions commented 6 years ago

Right now I have a sloppier solution that takes advantage of our Netlify account to only load preview pages in a password-protected version of our site. However, I'm handling data mapping to a common structure by having two different functions that get called depending on the source.

AdamPflug commented 6 years ago

@simplesessions how did you handle getting your previewed content out of the API at build time? I love deploying to Netlify but haven't figured out a good solution for previewing prismic stuff there because of the per-release refs that change all the time. Also, how are you triggering new builds? I thought he web-hooks were only for publish/archive events

In other news

The first stage of Prismic's native GraphQL support is in beta: Documentation: https://prismic.link/2tSa4AT Discussion/Roadmap: https://youtu.be/qu1KKrhR8wI

With this in mind, I had a thought: would it be possible to write a higher order component that wraps page components to fetch the preview content for them dynamically when you have a preview cookie set? It would need to:

If we can extract all the complexities of having 2 code paths into a single HOC, then as long as that's solid it eliminates a lot of the drawbacks discussed above, and it probably doesn't require any additional support from the Gatsby core team to implement. To me this feels like an interesting approach, but I could be missing something (or it may end up being very difficult to implement in practice). I see the API something like this (the key is basically just he last line):

import React from "react";
import prismicPreview from `gatsby-source-prismic`;

let ProductListPage= ({ data }) => {
  const products = data.PrismicProductQuery.allPrismicProduct.edges;
  return (
    <div>
      {products.map(({product}) => 
        <div>
          <h1>{product.data.name}</h1>
          <div dangerouslySetInnerHTML={{ __html: product.data.description.html }} />
        </div>
      )}
    </div>
  );
};

export const query = graphql`
  query PrismicProductQuery {
    allPrismicProduct {
      edges {
        node {
          id
          data {
            name
            price
            description {
              html
            }
          }
        }
      }
    }
  }
`;

export default prismicPreview(ProductListPage, query);

We'd still need to solve the problem of what to do with 404s (for net new pages as an example), but if feels like a step in the right direction?

simplesessions commented 6 years ago

@AdamPflug Sorry, I somehow missed that you replied to this! I just used it in a hacky way by taking advantage of Netlify and deploying an instance of the site that's password-protected, includes a template that loads and parses the data, and exposes the API key.

It works quickly and effectively, and am hesitant to lean towards any other options that trigger full on builds since it'll make the preview process for the content guys we work with kind of a slog.

So yeah - for now, I'm exposing the access token and simply doing a fetch and parsing all the info :)

birkir commented 6 years ago

Hey @simplesessions can you share your config/hack with the access token exposed for previews.

Or are you saying that you are skipping the gatsby-source-prismic plugin all together?

AdamPflug commented 6 years ago

Stage 2 of native graphQL coming soon: https://www.youtube.com/watch?v=W-QWGw7Zb1c

simplesessions commented 6 years ago

@birkir In my case I'm skipping gatsby-source-prismic altogether and just straight up using PrismicDOM. I avoid any sort of recompiling and the load time is super quick. Right now, I have it just straight up loading behind a password-protected Netlify instance, but looking to incorporate some lambda function to load everything.

birkir commented 5 years ago

Thanks @simplesessions

Here is my solution:

withPrismicPreview.tsx

import React from 'react';
import traverse from 'traverse';
import { get, isArray } from 'lodash';
import Prismic from 'prismic-javascript';

export function withPrismicPreview(Component) {
  return class WrappedComponent extends React.Component {

   state = {
      data: this.props.data,
    }

    componentDidMount() {
      this.setup();
    }

    async setup() {
      const url = new URL(window.location.toString());
      const data = { ...this.props.data };
      const previewFlag = url.searchParams.get('preview');
      if (previewFlag) {

        // Get all prismicId's (unique)
        const prismicIds = traverse(data).reduce(function(acc, x) {
          if (this.key === 'prismicId' && acc.indexOf(this.parent.node.prismicId) === -1) {
            acc.push(this.parent.node.prismicId);
          }
          return acc;
        }, []);

        // Warn possible missing prismicId's
        if (prismicIds.length === 0) {
          // tslint:disable-next-line no-console
          console.warn('Preview did not find any `prismicId` keys in your graphql schema. '
            + 'Please add them to allow proper mapping.');
        }

        const api = await Prismic.getApi(`https://${process.env.GATSBY_PRISMIC_ENDPOINT}.cdn.prismic.io/api/v2`);
        const previews = await Promise.all(prismicIds.map((prismicId) => api.getByID(prismicId)));

        // Update the object with a match from preview (if available)
        const updateWithPreview = (obj) => {
          const preview = previews.find((p) => p.id === obj.prismicId);
          return traverse(obj).map(function(x) {

            if (this.key === 'dataString') {
              this.update(JSON.stringify(preview.data));
            }

            if (this.key === 'raw' && isArray(x)) {
              const paths = this.path.slice(0);
              paths.pop();
              this.update(get(preview, paths.join('.')), true);
            }

            if (this.isLeaf) {
              const previewValue = get(preview, this.path.join('.'));
              if (previewValue) {
                this.update(previewValue);
              }
            }
          });
        };

        // Traverse all objects that have `prismicId` in them
        traverse(data).forEach(function(x) {
          if (get(x, 'prismicId')) {
            this.update(updateWithPreview(x));
          }
        });

        // Set newly mapped data
        this.setState({ data });
      }
    }

    public render() {
      const { data, ...passProps } = this.props;
      return <Component data={this.state.data} {...passProps} />;
    }
  };
}

pages/preview.jsx

import React from 'react';
import Prismic from 'prismic-javascript';
import linkResolver from 'utils/linkResolver';

export default class PreviewPage extends React.Component {

  componentDidMount() {
    const url = new URL(window.location as any);
    const token = url.searchParams.get('token');

    if (token) {
      Prismic.getApi(`https://${process.env.GATSBY_PRISMIC_ENDPOINT}.cdn.prismic.io/api/v2`)
      .then((api) =>
        api.previewSession(token, linkResolver, '/')
        .then((pathname: string) => {
          const now = new Date();
          now.setHours(now.getHours() + 1);
          document.cookie = `${Prismic.previewCookie}=${token}; expires=${now.toUTCString()}; path=/`;
          url.searchParams.append('preview', 'true');
          url.pathname = pathname;
          window.location = url;
        }),
      );
    }
  }

 render() {
    return null;
  }
}

Usage

import React from 'react';
import { RichText } from 'prismic-reactjs';
import { withPrismicPreview } from 'utils/withPrismicPreview';

@withPrismicPreview
export default class HomePage extends React.PureComponent {
  render() {
    const content = JSON.parse(this.props.data.prismicPage.dataString);
    return <h1>{RichText.asText(content.title)}</h1>;

    // or using raw
    return <h1>{RichText.asText(this.props.data.prismicPage.data.title.raw)}</h1>;
  }
}

export const query = graphql`
    prismicPage {
      id
      prismicId # necessery for previews
      dataString
      data {
        title {
          raw {
            text
            type
         }
       }
     }
   }
`

(I removed typescript type annotations so pardon me if there is anything missing from the code snippets)

krabbypattified commented 5 years ago

Hello! It's Gabe from Prismic. We are currently working on another implementation of gatsby-source-prismic with our new GraphQL API. We considered a solution similar to @birkir's idea at first. However, we had two issues. First, how do you preview an unpublished document? It will go to the 404 page, and you wouldn't know which React template to render for that URL. Second, we only have access to the data, not the original query. Sometimes it is possible to reconstruct the query from the shape of the data, but what if you decide to rename your fields in the GraphQL query?

post {
  mytitle:title
  mysummary:summary 
}

We decided that in order for Prismic previews to work with Gatsby, users need a slightly more structured workflow, and plugin developers need to have access to requests in the browser APIs. Our proposed solution is a plugin (gatsby-apollo) that enables apollo-style querying in any Gatsby site. There is no configuration necessary. It just works with your existing site. If you decide to add a query using the apollo-style gql tag, plugin developers such as gatsby-source-prismic can intercept requests in the browser and route them where necessary.

The plugin works by surrounding every page in an ApolloProvider with a special ApolloLink (developers can hook into this) that will look for results in a JSON file where the name matches a (deterministic) hash of the query itself. Query variables are automatically injected from the URL (/posts/:lang/:uid) by default, but the user may specify their own variables (apollo-style).

This is an example of how gatsby-source-prismic can provide special functionality to queries made in the browser:

import { extendApolloLink } from 'gatsby-apollo'

exports.onClientEntry = () => {
  extendApolloLink(link => new ApolloLink(async (operation, forward) => {
    if (condition) return forward(operation)
    return liveQuery(operation)
  })).concat(link)
}

A typical developer's workflow:

A page for loading the list of posts using the Query component:

const Posts = () => (
<Query query={postsQuery}>
  // Loading would be `true` while a live preview is loading,
  // and the `Posts` template can handle it however it chooses.
  {({ data, loading, error }) => data.prismic.allBlogPosts.map(post =>
    <div>{ post.title }</div>
  )}
</Query>
)

// `gql`, not `graphql`
const postsQuery = gql`{
  prismic {
    allBlogPosts {
      title
      summary
    }
  }
}`

export default Posts

A page for loading a single post using the graphql HOC:

const Post = ({ data, loading, error }) => (
  <div>{ data.prismic.blogPost.title }</div>
)

// This query need not be in this file. It can be decoupled for re-use.
// Normally Gatsby would need it here to connect this query to the props.
// Queries with variables (i.e. $uid) now must be compiled explicity.
const blogPostQuery = gql`
  query BlogPost($lang: String, $uid: String) {
    prismic {
      blogPost(uid: $uid, lang: $lang) {
        title
        summary
      }
    }
  }
`

// `graphql` HOC
export default graphql(blogPostQuery)(Post)

In the browser, the plugin automatically adds the lang and uid URL components to the query variables. If the query returns a 404, Post.js can handle it accordingly.

Queries with variable definitions no longer compiled via createPage. They are explicitly compiled like so:

// Compile the `BlogPost` query with these 3 variations
compileQueryVariations({
  query: 'BlogPost',
  variations: [
    { lang: 'en-us', uid: 'post-1' },
    { lang: 'fr-fr', uid: 'post-1' },
    { lang: 'en-us', uid: 'post-2' },
  ],
})

// We create ONLY ONE PAGE to render any blog post.
// In production, infer the `lang` and `uid` variables from the URL.
createPage({
  template: './src/components/Post.js',
  matchPath: '/posts/:lang/:uid',
})

createPage tells Reach Router to render Post.js for any URL matching a specific pattern (/posts/:lang/:uid).

We could generate the variations array above by making a query for the lang and uid of all Prismic blog-post types:

graphql(`{
  prismic {
    allBlogPosts {
      lang
      uid
    }
  }
}`).map(result => result.data.prismic.allBlogPosts)

This system allows queries to be decoupled from pages.

The configuration for this plugin is one line:

// In gatsby-config.js
{ resolve: 'gatsby-apollo' }

I am planning to implement gatsby-apollo as soon as possible, but in the meantime, let me know your thoughts on this solution. Or, if you'd like to speed up the process, make a proof of concept!

birkir commented 5 years ago

Yes, the points you mentioned are pain points but can be solved with restrictions and extra work.

a) To overcome unpublished documents, that Gatsby would have to generate on build time, we can just have placeholder pages that can render the specific document type dynamically (/_preview/blog, /_preview/custom-type), through a custom linkResolver for preview.

b) Not having a single source of truth is a very bad problem. For things like filtering and paging (where we don't want to create static pages for each possible scenario), we have to have two sources of truth. Same for preview, but renaming the fields that have prismicId as a child is fine, but other deeply nested fields will fail the mapping.


That being said, the solution you proposed is a great addition to the Gatsby ecosystem and will enable a single source of truth for all source plugins that support the system.

The only issue is exposing of secrets and stuff needed to fetch the data on the client. But there is no way to overcome that problem with Gatsby.

Thanks Gabe.

krabbypattified commented 5 years ago

@birkir Thank you for your feedback.

The "secrets needed to fetch the data" problem seems solvable in the case of Prismic previews. For normal fetching, data is retreived from the static file server. If a live query is required (for a blog post keyword search perhaps), you would have to change your Prismic settings to allow anyone access to the Master Ref. For preview fetching, prismic.js (or the gatsby plugin) will communicate with your repository "example.prismic.io" and will automatically authenticate your request for preview content if you are currently logged into that repository. No secrets spilled :)

To perhaps clarify to people how our unpublished preview would work:

When you navigate to /posts/gibberish/post-1 where the url pattern from createPage is /posts/:lang/:uid and gibberish is a new language you're previewing, Apollo would run the query with those :lang and :uid variables. It would not find a document and therefore return a 404. However, gatsby-source-prismic would extend the ApolloLink and attempt a live-query to Prismic in this scenario, this time with a io.prismic.preview cookie. A few moments later, your content would load normally.

And for pagination and filtering:

query BlogPosts($page: Number, $keyword: String) {
  prismic {
    blogPosts(page: $page, keyword: $keyword) {
      title
      summary
    }
  }
}

We would not want to use compileQueryVariations on this query in order to force gatsby-source-prismic to fetch these documents from the live server (because there are too many variations to handle). The beauty of gatsby-apollo is that you can optionally compile some of the variations like $page: 1 and keyword: '' because this might be the default view for the homepage and you wouldn't want a live query for every homepage load. (Note: for this case you would want to allow anyone open access to the Master Ref in your Prismic settings so that they can search for documents with any keyword)

angeloashmore commented 5 years ago

@krabbypattified Glad to see official support for Gatsby! Re: the Apollo method, it sounds like every page would be rendered client-side. Would the method you describe above be implemented in addition to statically rendering pages?

krabbypattified commented 5 years ago

@angeloashmore I am not 100% sure what you mean, but I will try my best to answer. I'll assume by "rendered client-side" you mean being able to dynamically respond to the URL and render live data. And by "statically rendering pages" you mean the normal Gatsby process where we fetch from the JSON files or inline the results of queries.

To answer your question, the plugin doesn't interfere with anything. It simply adds a querying method (gql) and a way to compile gql queries with variables (compileQueryVariations). The gatsby-apollo plugin does not interfere with the process of creating/rendering pages, creating the GraphQL schema, etc.

Perhaps the example I provided was confusing? The example shows how you can create a page that will render your Post.js template for any URL with a /posts/:lang/:uid pattern by using Gatsby's matchPath:

createPage({
  template: './src/components/Post.js',
  matchPath: '/posts/:lang/:uid',
})

And the :uid syntax would allow the Apollo plugin to autofill query variables via Reach Router context.

Here is an example of another workflow for a list of posts (you could use the Query component or the graphql HOC here):

// In ./src/pages/posts.js
const Posts = () => (
  <Query query={postsQuery}>
    {({ data, loading, error }) => data.prismic.allBlogPosts.map(post =>
      <div>{ prismic.allBlogPosts.map(p => <div>{p.title}</div>) }</div>
    )}
  </Query>
)

const postsQuery = gql`{
  prismic {
    allBlogPosts {
      title
      summary
    }
  }
}`

export default Posts

I hope that answered your question, but please let me know if not.

AdamPflug commented 5 years ago

@krabbypattified I think the question is: do we get a fully pre-rendered HTML version of the page for the initial page load of /posts/en-us/1234. Usually in Gatsby you would, but it looks like the approach you're suggesting bypasses that and would basically generate a /posts/ page that would then make an HTTP request to the pre-built .json file with the data needed for the specific uid/language combination (or a live request to prismic if needed), then render the HTML client-side. Correct me if I'm wrong.

This seems like a bummer because that pre-rendered HTML you usually get is one of Gatbsy's key advantages (optimized time to first contentful paint). By requiring 2 HTTP requests in series before you can even begin to generate the HTML client side and then query for all the required resources you really slow down that first page load. I agree with @birkir that there's probably another way to work around the "unpublished documents" problem (perhaps having some code on the 404 page that, when in preview mode, does a react-router redirect to the correct route, but this time renders client-side in live query mode).

madeleineostoja commented 5 years ago

Haven't been following this too closely, but just wanted to quickly chime in to agree with @AdamPflug – anything that works against Gatsby's buildtime rendering of all pages would be a dealbreaker for most I imagine. That's really one of the key selling points of Gatsby over a more conventional React/SPA setup.

birkir commented 5 years ago

Wanted to share my experience with prismic previews for the past couple of weeks.

Limitations and constraints

My idea is based around piggybacking onto existing graphql query responses and patching them. We have to add prismicId to the graphql queries to replace documents with preview data.

Field aliasing

Because of the implementation, we need to know the field's name to match them correctly. So it's fine to alias documents, but not fields within documents.

# Good
blog: prismicBlog {
  prismicId
  data {
    title
  }
}

# Bad
prismicBlog {
  prismicId
  data {
    title: heading
  }
}

gatsby-image

When using image sharper, include the actual Prismic image url and make sure the UI can fallback to that one if no localFile was found.

  myimage {
    url # this one
    localFile {
      childImageSharp {
        fixed(width:100, height:100) {
          ...GatsbyImageSharpFixed
        }
      }
    }
  }
{myimage.localFile ? <Img fixed={myimage.localFile} /> : <img src={myimage.url} />}

Because previews happen dynamically, gatsby hasn't processed the updated image with image-sharper, so the solution is to remove the localFile property and rely on the actual image url.

Unpublished documents

I solved this problem by finding another published document of the same type, resolve its page and then swap the document id with the previewing document id. This only happens when the previewable document isn't already published

So the only case when this fails is when no published type of the document exists. And guess what, Gatsby also crashes when this is the case, so not really an issue with my solution per se.


so tldr;

If anyone has anything to add, let me know. I'll be creating an npm package with the updated solution and would to like to know more edge cases to support.

Dean177 commented 5 years ago

@birkir I ended up tackling the image and aliasing in a slightly different way: I parse the graphql query and specifically look for alias and image sharp transforms, then attempt to apply those to the response from prismic (e.g filling in the values you would get from childImageSharp based on the url).

birkir commented 5 years ago

@Dean177 Ok, that's a good idea. I just didn't bother to research if that was consistent, thanks for letting me know, will add!

krabbypattified commented 5 years ago

@AdamPflug Thank you for clarifying, you are absolutely right. That was my mistake to overlook pre-rendering HTML. The method I described (Apollo) is different than Gatsby's. It would require the website to have JavaScript enabled, and it would have to make 2 HTTP requests before the first meaningful paint. That is, unless Gatby's HTML pre-renderer is smart enough to compile a React component that uses Apollo queries. That would be so cool.

That being said, pre-rendering HTML for each URL is only a great idea while the URL is the single, deterministic factor in the first meaningful paint. If there are other factors at play such as cookies, local storage, API requests, etc., this solution quickly falls apart. At best, the browser renders a flash of inaccurate content.

On the surface, Gatsby's solution seems great. But to achieve this improved performance, Gatsby pulls a few hacks. It implements two routing systems: the static file server and the Reach router. The file server doubles as an HTML pre-renderer. graphql queries look like JavaScript, but they are not. This is why a graphql query must be in the same file as its page component. In fact, these queries are completely stripped away at buildtime (for security).

These hacks are excusable for a website that fits the 100% static architecture, or "works without JavaScript," if you will. As soon as people begin to ask for features that aren't very static (live data, dynamic routes, preview content, etc.), the 100% static architecture falls apart. Any plugin designed to work around this design limitation is more or less a hack on top of a hack.

On the other hand, we have systems like NextJS and Netlify that do not sacrifice architecture for performance. They do not force the model to fit the concept of a "works without JavaScript" file server. Plugins built on top of these systems are not hacks. In fact, there is no Prismic plugin required for these systems. Simply add prismic.js and everything works.

Improving performance almost always adds complexity. Developers have to decide where that complexity makes sense. In the case of Gatsby, this complexity hurts me dearly. gatsby-apollo is an attempt to refactor Gatsby's architecture give plugin developers and website developers more freedom. For example, one could make a Gatsby plugin that allows a developer to pick and choose which URLs he wants to pre-render, realizing that the content may be inaccurate and React will have to re-render it. One could also make a Gatsby plugin that predicts the next URL to be loaded and prefetches the content. One could even add a service worker that caches or actively prefetches resources for offline use.

Perhaps I should also mention the elephant in the room - that an API request to Prismic is pretty darn fast. Since the requests are deterministic, they are permanently cached at our servers, our CDN, and your browser. (If you want to avoid the extra request to get the correct ref, you can keep the ref in local storage, or even hardcode it.) That means requests are as fast as (or faster than) requesting a file via GitHub Pages, Netlify, or some other file server.

I want to help you all as best as I can. If gatsby-apollo is a

dealbreaker for most

where should I focus my efforts instead? What is the best solution?

What is the complexity of the /_preview/custom-type solution?

Is the

perhaps having some code on the 404 page that, when in preview mode, does a react-router redirect to the correct route, but this time renders client-side in live query mode

solution comprehensive enough?

I sincerely appreciate all of your contributions to this conversation, so please let me know if I have not addressed anything. I am only one person, so I may not have time to explore every idea myself. Your help is always appreciated.

Gabe

Dean177 commented 5 years ago

@krabbypattified It feels like you are making a lot of assumptions about the use cases that gatsby meets.

That being said, pre-rendering HTML for each URL is only a great idea while the URL is the single, deterministic factor in the first meaningful paint. If there are other factors at play such as cookies, local storage, API requests, etc., this solution quickly falls apart. At best, the browser renders a flash of inaccurate content.

Having the url be the single deterministic factor for the first meaningful paint is not a severe restriction.

Take a look at some of the sites from the showcase: https://www.gatsbyjs.org/showcase/

Marketing sites, blogs, landing pages.

Those pages can still have content content that relies on cookies, localstorage or api requests, they can have placeholders or spinners whilst dynamic content is resolved.

As soon as people begin to ask for features that aren't very static (live data, dynamic routes, preview > content, etc.), the 100% static architecture architecture falls apart. Any plugin designed to work around this design limitation is more or less a hack on top of a hack.

There is a perfectly serviceible way to add completely dynamic content to gatsby apps: https://www.gatsbyjs.org/docs/building-apps-with-gatsby/?no-cache=1

Improving performance almost always adds complexity. Developers have to decide where that complexity makes sense.

I am perfectly ok with complexity living in the frameworks and plugins that I use.

Perhaps I should also mention the elephant in the room - that an API request to Prismic is pretty darn fast. Since the requests are deterministic, they are permanently cached at our servers, our CDN, and your browser. (If you want to avoid the extra request to get the correct ref, you can keep the ref in local storage, or even hardcode it.) That means requests are as fast as (or faster than) requesting a file via GitHub Pages, Netlify, or some other file server.

It doesn't matter that your api is as fast as github pages. Its a request that each individual client doesn't need to make. One of the valuable parts of gatsby is that this requests happens once on our CI server. Its one fewer component in the live pipeline, a reduction in complexity, one less thing that can be slow or fail completely.

Where should I focus my efforts instead? What is the best solution?

Initially, try to understand why people chose gatsby over the similar frameworks and why they are happy with those tradeoffs. Calling them hacks is not productive, people clearly get a lot of value from Gatsby.

joseph-cloudthing commented 5 years ago

It doesn't matter that your api is as fast as github pages. Its a request that each individual client doesn't need to make.

To clarify, I assumed this work was targeting the development server. After gatsby build all would be static?

krabbypattified commented 5 years ago

Seeing the example projects, I agree with you here:

Having the url be the single deterministic factor for the first meaningful paint is not a severe restriction ... Those pages can still have content content that relies on cookies, localstorage or api requests, they can have placeholders or spinners whilst dynamic content is resolved.

I also agree with you here:

It doesn't matter that your api is as fast as github pages. Its a request that each individual client doesn't need to make.

And I confess I may have confused "100% static" with Gatsby's implementation of it. Static is very powerful if done right. I trust it is the best solution for you and many others. Additionally, Gatsby is a great tool. However, I stand by my critique of Gatsby. It has the potential to become tremendously more valuable to developers. I call specific features "hacks," but perhaps I should elaborate on that. What I mean is that they add unneccessary restrictions and complexity to their framework by having, for example, a query mechanism that is not properly designed (described in my post above). Gatsby sees the problem, and v3 is attempting to solve it.

For the moment, we do not have a v3. But Gatsby offers Node APIs and Browser APIs that make it possible to add features without having to submit a pull request. For example:

Initially, try to understand why people chose gatsby over the similar frameworks and why they are happy with those tradeoffs. Calling them hacks is not productive, people clearly get a lot of value from Gatsby.

Thank you for the example projects. I saw them. The tradeoffs make sense. But I think calling Gatsby out on bad design is a necessary step for progress to be made (unless you were only criticizing my word choice).

@Dean177 Thank you for your reply. From your other comment above, it seems you lean toward the prismicId solution. If that is the case, I invite you to make an argument for why an Apollo plugin for Gatsby with an HTML pre-renderer is not a better idea.

krabbypattified commented 5 years ago

@joseph-cloudthing Yes, all would be static after the build. He is referring to something like Jenkins.

Dean177 commented 5 years ago

@krabbypattified thanks for your considered response, I admit I was partly critisizing the word choice.

In this case I think I do lean towards the prismicId solution.

Honestly Apollo with HTML pre-rendering sounds fantastic, but it does come with some drawbacks. One of the features gatsby provides is its transformer plugins, particularly image sharp to facilitate build-time image resizing and dynamic cropping which wouldn't be possible with the Apollo + Prerender (??).

krabbypattified commented 5 years ago

@Dean177 Interesting point about image sharp. The tricky part is making a live Prismic query with an embedded image sharp query. I have an idea, but I am not 100% sure about it.

  1. Run makeRemoteExecutableSchema to get the the Prismic GraphQL API schema.
  2. Merge the Prismic schema with image sharp using mergeSchemas. Use schema transforms to transform the request to Prismic by removing the image sharp part of the query. Transform the result by performing image sharp functions on the image returned.
  3. Merge the Prismic + image sharp schema with the Gatsby schema.

If the image sharp is run in the browser, it would make the request a bit slower. But since queries are compiled at build time, this would only affect a live (Preview) query.

simplesessions commented 5 years ago

Leaving image rendering with sharp for just a sec, what's really working well for me is the "sloppier" solution I mentioned earlier, which involves loading PrismicDOM and just having it render relevant parts. Like mentioned earlier in the thread, the Prismic API is pretty fast, so the content replacement is super fast. In my case, especially in my case where I'm using it for long-form articles.

What really helps is that the data that the GraphQL queries output is super close in shape to the data that PrismicDOM outputs, though there are some small differences that could be worked out.

My ideal solution would be to basically have a small script that's hosted on Prismic that basically just verifies that the URL params hint or indicate that the page that I'm rendering is a preview. It can then contact Prismic and, if I'm logged in, load up PrismicDOM remotely, grab the preview data, and replace the content. Perhaps the pages that are previewable are armed with some callback that PrismicDOM or whatever library expects so that all it does is pass the new content into it, then it's up to us to tell it how to render. I do something similar where I pass in either the GraphQL data or PrismicDOM data to a renderContent(data) function that does minor changes to shape the data.

In this solution, I'm looking to avoid loading any other Prismic-related libs explicitly so it's not bundled with the site while enabling previews to still load and maintain the static HTML nature.

Any thoughts on something like that?

angeloashmore commented 5 years ago

@simplesessions I believe with dynamic imports, loading the Prismic libraries only when necessary is possible. Though maybe that won't be possible until Suspense is released? Not sure…

I'm leaning toward @simplesessions's solution to previewing, with the caveat of somehow overcoming the need to run a separate preview instance with custom code. This allows statically rendering all known pages, but falling back to a dynamic/client-side renderer for new/unknown pages.

As @simplesessions mentions, this requires modifying or supplementing the gatsby-source-prismic GraphQL API to closer match PrismicDOM's input schema. We passed around the idea of providing the current API (with things like text and html) along with a "preview-able" API with some helpful fragments to cut down on the query size.

birkir commented 5 years ago

I have created a gatsby plugin to track this feature in a separate package.

https://github.com/birkir/gatsby-plugin-prismic-preview

birkir commented 5 years ago

New plugin that will help to get us live previews for more GraphQL powered CMS's

https://github.com/birkir/gatsby-source-graphql-universal

angeloashmore commented 4 years ago

Closing this out as v3 has been in open beta for a while now and provides preview functionality. Please give it a try if you haven't yet!

https://github.com/angeloashmore/gatsby-source-prismic/blob/v3-beta/docs/previews-guide.md