Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Truly static pages without react app #5054

Closed
EugeneKostrikov opened this issue Aug 29, 2018 · 45 comments
Closed

Truly static pages without react app #5054

EugeneKostrikov opened this issue Aug 29, 2018 · 45 comments

Comments

@EugeneKostrikov
Copy link

Feature request

Is your feature request related to a problem? Please describe.

We want some static pages to be as lightweight as possible and we're fine with full page reloads navigating out of them if we can chop off extra 200Kb.

Describe the solution you'd like

Add an option to path map in next.config.js to indicate this specific page doesn't need to include react app and can operate as plain HTML page.
React app is only removed in case the flag is set to true explicitly.

Looks like it should be enough to just make files content empty there if option is set. https://github.com/zeit/next.js/blob/canary/server/render.js#L100

For the simple example from tutorial setting up

// next.config.js
module.exports = {
  exportPathMap: function(defaultPathMap) {
    return {
      '/': { page: '/', withoutReact: true },
      '/about': { page: '/about' }
    }
  }
}

As a result /out would be populated with

<!--/out/index/index.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charSet="utf-8" class="next-head"/>
  </head>
<body>
   <a href="/about">About</a>
</body>
</html>


<!--/out/about/index.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charSet="utf-8" class="next-head"/>
    <link rel="preload" href="/_next/9d298d49-6fb5-4c6f-a992-31fdb0d3e01a/page/about.js" as="script"/>
    <!--other scripts-->
  </head>
<body>
   <!--page content -->
   <script>
     NEXT_DATA = {"props": "etc.."}
   </script>
  <script id="__NEXT_PAGE__/about" src="..."></script>
</body>
</html>

This way index page is going to be very lightweight, with a tradeoff of full page reload navigating to /about. While /about would load the app, so navigation back to / would be handled on the client-side.

Describe alternatives you've considered

Static site generators :) But we need way more than just static pages and next.js fits us best except for this tiny quirk.

Additional context

Happy to implement that, just wanted to make sure this is not something going against the roadmap for next.js. Or if it's something already available, but hidden. I'm new to the library, only playing around for the second day.

@danielr18
Copy link
Contributor

I think you can do that with a custom Document. Something like this:

// pages/_document.js
import Document, {  Main, NextScript } from 'next/document'

const pagesWithoutReact = ['/'];

export default class MyDocument extends Document {
  render() {
    const { __NEXT_DATA__ } = this.props;
    return (
      <html>
        <body>
          <Main />
          {!pagesWithoutReact.includes(__NEXT_DATA__.pathname) ? <NextScript /> : null}
        </body>
      </html>
    )
  }
}

@timneutkens
Copy link
Member

@danielr18 that's not a reliable way to remove the script tags as they're still preloaded.

@EugeneKostrikov
Copy link
Author

Technically looks like that would do the job. Discovered staticMarkup option in export renderOpts. It instructs NextScript not to include the scripts. Effectively same thing as custom Document above, but without one and can be managed on exportPathMap level. It was not 100% implemented but did some tweaks on the fork and got it working. It was not removing all scripts and preload links.

Now facing another issue not directly related to next.js itself. The static page i'm trying to render is a snapshot of quite complex page currently rendered by Wordpress. If i navigate to this page directly (with full page reload) everything is smooth. But navigation using Link breaks the scripts already present on the page (there's angular, jquery and whole galaxy of other trash). Looks like something to do with scripts load order when they're added to the body dynamically. Continue with this - would rather have some neat solution in place before creating PR.

@IanEdington
Copy link

This would be a great addition to this ecosystem. We are looking to generate a number of static landing pages as lightweight entry points into our SPA. This is the perfect solution.

@cesarvarela
Copy link

Whats the status of this? I'm needing something like this too.

@tnunes
Copy link

tnunes commented Feb 15, 2019

Curious about the state of this, as I'm also looking to implement something similar for statically exported error pages.

@Shankar1598
Copy link

Is there any other tool available to do this now? I too need something similar.

@mkaradeniz
Copy link

I did it as danielr18 described.

Create a custom _document and omit <Head /> and <NextScript />. I did switch to react-helmet to replace the other functionality of <Head />. See the with-react-helmet example to get that working. This worked fine for my use case (server-rendered React without any client-side javascript).

@bancek
Copy link

bancek commented Apr 20, 2019

I also did something similar to @danielr18. The problem with his example is that JavaScript files are still preloaded even though they are not needed.

I extended Head component and removed all link preload tags.

Screen Shot 2019-04-20 at 22 52 15

// pages/_document.js
import Document, { Main, Head } from 'next/document';

class CustomHead extends Head {
  render() {
    const res = super.render();

    function transform(node) {
      // remove all link preloads
      if (node && node.type === 'link' && node.props && node.props.rel === 'preload') {
        return null;
      }
      if (node && node.props && node.props.children) {
        return {
          ...node,
          props: {
            ...node.props,
            children: node.props.children.map(transform),
          },
        };
      }
      if (Array.isArray(node)) {
        return node.map(transform);
      }

      return node;
    }

    return transform(res);
  }
}

class StaticDocument extends Document {
  render() {
    return (
      <html>
        <CustomHead />
        <body>
          <Main />
        </body>
      </html>
    )
  }
}

export default process.env.NODE_ENV === 'production' ? StaticDocument : Document;

This generates a minimal build without any JavaScript when running next build && next export and still use hot reloading in development.

@TriPSs
Copy link

TriPSs commented Jun 17, 2019

@bancek had a small issue with your solution, had to change:

if (node && node.props && node.props.children) {
return {
    ...node,
    props: {
      ...node.props,
      children: node.props.children.map(transform)
    },
  }
}

To:

if (node && node.props && node.props.children) {
return {
    ...node,
    props: {
      ...node.props,
      children: Array.isArray(node.props.children)
	    ? node.props.children.map(transform)
	    : transform(node.props.children),
    },
  }
}

@askirmas
Copy link

askirmas commented Jun 21, 2019

Super applying is Email templates embed in ecosystem, not as a side feature

@iamstarkov
Copy link

@askirmas can you elaborate?

@askirmas
Copy link

@iamstarkov In autumn

@iamstarkov
Copy link

@askirmas i meant that i dont understand you

@askirmas
Copy link

Example

  • layouts/website.js
export default ({children}) => <><Header/>{children}<Footer/></>
  • layouts/email.js (or as separate project with pages reuse)
export default ({children}) => <>
  <EmailHeader/>
  <div>Dear {full_name}</div>
  {children}
  <EmailFooter/>
</>
  • components/?page/newSuperProposition.js
export default () =><b>New Super Proposition!</b> 

With exportPathMap and etc will be exported as

  • out/website/newSuperProposition.html
  • out/email/newSuperProposition.html

There are many limitations to send newSuperProposition.replace('{full_name}', 'Andrii Kirmas') as email without loosing styles or something else. And first step is not have scripts

@timothyjoh
Copy link

@bancek or @TriPSs have you guys completely cut off the re-hydration this way? Have you tried to do any partial-hydration so that <Link> would work on these pages? Just asking because I have been looking for a way to re-hydrate certain parts of the app (if not the whole app) after the initial draw and CPU idle, so that users are served the page fast, but subsequent pages can also be fast (using the client-side router) and possibly there would be components on these pages that could be lazily-hydrated.

@bancek
Copy link

bancek commented Aug 18, 2019

@timothyjoh yes, we've completely cut off the re-hydration. There is no JavaScript on our static pages. We use Next.js to reuse styles and React components between our React app and our static pages. On our static pages we keep the styles.

Example: https://app.koofr.net/legal/privacy

@timothyjoh
Copy link

I am going to look into a way to defer hydration, so that after page load (and CPU-idle) that we would then load react and re-hydrate the links to other pages. If the user starts to interact with the page (using the search box or scrolling) hopefully this will not mess up the experience. But if they click a link to navigate away before the page is hydrated, maybe the Link would take them to another SSR page with full page refresh (not using the client-side router) but as soon as the user is sufficiently paused, the react app would be loaded and client-side routing would re-engage and take over.

That's my panacea that I am shooting for anyway.

@BootprojectStore
Copy link

BootprojectStore commented Oct 15, 2019

I tried the solution to remove the preload links, they are removed server side, but gets injected clientside... Whats is going on here? How can it be injected when they are removed?

error

Anyhow, i solved it by adding

const props = this.props.__NEXT_DATA__.props.pageProps; const useReact = !!props.useReact; {useReact && <NextScript />}
And then in my index page.

` static getInitialProps = async ({ query, reduxStore }) => {
const store = reduxStore.getState();
const page = store.pages.find(p => p.path === query.path);

    /*
       Create data, meta for the application
     */
    const data = {
        meta: {
            lang: 'en',
            title: page.meta_title,
            description: page.meta_description
        },
        id: query.path,
        current: page,
        layout: page.layout,
        routes: store.pages,
        store: store,
        subRoutes: null,
        useReact: true //<!--------- Added this to page properties
    };
    return data;
}`

@jwhiting
Copy link

Does anyone have a suggestion for how to accomplish this while retaining the CSS imports?

I'm new to the platform but perusing the source for the Document component shows it's quite complex, I'm hard pressed to think that just subclassing the component to replace the render method entirely with a simple one, as proposed here by @danielr18 and others, is going to suffice. Not only for the CSS but other functionality captured there, as evidenced by others in the thread having to go further to preserve things like head tags but not include preload tags, etc.

Perhaps someone here is familiar or adventurous enough to modify the Document component to accept a flag to disable the scripting/preloading behaviors while retaining those still pertinent to static pages (CSS, various head meta tags, etc)?

@BootprojectStore
Copy link

Does anyone have a suggestion for how to accomplish this while retaining the CSS imports?

I'm new to the platform but perusing the source for the Document component shows it's quite complex, I'm hard pressed to think that just subclassing the component to replace the render method entirely with a simple one, as proposed here by @danielr18 and others, is going to suffice. Not only for the CSS but other functionality captured there, as evidenced by others in the thread having to go further to preserve things like head tags but not include preload tags, etc.

Perhaps someone here is familiar or adventurous enough to modify the Document component to accept a flag to disable the scripting/preloading behaviors while retaining those still pertinent to static pages (CSS, various head meta tags, etc)?

Hi.
Have a look here: https://bitbucket.org/rickardmagnusson/bootproject-on-next-js-and-redux/src/master/src/pages/_document.js
I modified the meta and other scripts to prevent those links,
You can turn on/off next script by controlling {useReact && } from the layout.

`export default class CustomHead extends Head {

render() {
    const res = super.render();

    function transform(node) {
        // remove all link preloads and styles

        if (node && node.type === 'style')
            return null;

        if (node && node.type === 'link' && node.props && node.props.rel === 'preload') {
            return null;
        }
        if (node && node.props && node.props.children) {
            return {
                ...node,
                props: {
                    ...node.props,
                    children: Array.isArray(node.props.children)
                        ? node.props.children.map(transform)
                        : transform(node.props.children)
                }
            }
        }
        if (Array.isArray(node)) {
            return node.map(transform);
        }

        return node;
    }

    return transform(res);
}

}`

@Luxiyalu
Copy link

Can be achieved with customized react-helmet setup, see:

@timneutkens
Copy link
Member

@Luxiyalu that approach breaks a lot of Next.js features as suddenly Next.js can't inject anything. eg HTML properties needed for AMP, styles from css-in-js solutions, next/head tags etc. Also you lose the guarantee that things will keep working between upgrades.

@Luxiyalu
Copy link

@Luxiyalu that approach breaks a lot of Next.js features as suddenly Next.js can't inject anything. eg HTML properties needed for AMP, styles from css-in-js solutions, next/head tags etc. Also you lose the guarantee that things will keep working between upgrades.

Fair enough, but in this case we truly don't want any file injected from head, js or css. All we want is static HTML, sans even js. The react aspect is only added for easier composition of pure HTML, nothing more, and nextjs is what makes it possible to remove js from a react page. Thanks for that, by the way!

The next/head was replaced with react-helmet for customizing head, html and body attributes, and as for upgrades, can't have everything in life :)

@jwhiting
Copy link

@Luxiyalu @timneutkens since I need the nextjs-generated CSS in my static site, I can't use any of the approaches here.

What I do right now is apply a post-processing stage after exporting and before deployment. For all html files in the export folder (out/), I strip the script tags, strip the preload link tags, and then also delete everything in out/_next except the out/_next/static/css folder. The resulting file hierarchy is pushed to an s3 bucket which then serves the site statically.

@jerrygreen
Copy link
Contributor

I believe what you want is Svelte. It's inspired by NextJS, and one of their main concerns is "No virtual DOM" and, overall, to move all the framework cycles into compile time, rather than runtime.

@jwhiting
Copy link

jwhiting commented Mar 16, 2020

@jerrygreen That's a cool project, but switching to a totally different framework and losing the leverage of react+webpack doesn't strike me as a good tradeoff just to get rid of unneeded script tags in the final page render. Also keep in mind I'm interested in an S3-bucket site, with no backend (not Svelte's main use case).

Granted javascript-less pages are not the intended use case for React either, but, there are still some major advantages to using React even for static sites, such as declarative/reusable componentry, webpack asset and css pipeline processing, and so forth. NextJS is very close to supporting this.

All NextJS needs to do is omit the script tags on export, preferably with a per-page flag (perhaps in the export path map?) In the meantime, stripping script tags in post-processing step after export is an OK workaround.

timneutkens added a commit to timneutkens/next.js that referenced this issue Apr 16, 2020
This allows a page to be fully static (no runtime JavaScript) on a per-page basis.

The initial implementation does not disable JS in development mode as we need to figure out a way to inject CSS from CSS imports / CSS modules without executing the component JS. This restriction is somewhat similar to https://www.gatsbyjs.org/packages/gatsby-plugin-no-javascript/. All things considered that plugin only has a usage of 600 downloads per week though, hence why I've made this option unstable/experimental initially as I'd like to see adoption patterns for it first.

Having a built-in way to do this makes sense however as the people that do want to adopt this pattern are overriding Next.js internals currently and that'll break between versions.

Related issue: vercel#5054 - Not adding `fixes` right now as this implementation needs more work. If anyone wants to work on this feel free to reach out on https://twitter.com/timneutkens
@timneutkens
Copy link
Member

I've opened up a PR (#11949) to add initial support for this feature on a per-page basis, it still needs quite a bit of work to get the DX right, if anyone wants to work on that please reach out to me on https://twitter.com/timneutkens

@timneutkens
Copy link
Member

Posting my reply on the PR here so that people looking for it can see:
#11949 (comment)

Landing this as experimental, I can't spend tons of time on adding all the needed changes to make it work in development right now so the DX won't be what you'd expect. e.g. if you use useEffect or useState they'll work in development but won't work in production as the JS bundles will be excluded.

If someone wants to work on this feature (with guidance from me) please let me know: https://twitter.com/timneutkens, dm's are open.

timneutkens added a commit that referenced this issue Apr 17, 2020
This allows a page to be fully static (no runtime JavaScript) on a per-page basis.

The initial implementation does not disable JS in development mode as we need to figure out a way to inject CSS from CSS imports / CSS modules without executing the component JS. This restriction is somewhat similar to https://www.gatsbyjs.org/packages/gatsby-plugin-no-javascript/. All things considered that plugin only has a usage of 600 downloads per week though, hence why I've made this option unstable/experimental initially as I'd like to see adoption patterns for it first.

Having a built-in way to do this makes sense however as the people that do want to adopt this pattern are overriding Next.js internals currently and that'll break between versions.

Related issue: #5054 - Not adding `fixes` right now as this implementation needs more work. If anyone wants to work on this feel free to reach out on https://twitter.com/timneutkens
@bobaaaaa
Copy link
Contributor

First of all: I like this feature a lot 👍. Maybe a good starting point is:

  • experimental flag
  • print a extra warning in dev and prod mode

I mean you should know what are you doing with this flag. It's userlands problem when they use features like useEffect and useState(for now). The react team is currently working on a better SSR solution. Hoping for better tooling 🤞.

@timneutkens
Copy link
Member

experimental flag

See above PR, it adds an unstable flag to opt-in.

print a extra warning in dev and prod mode

Feel free to add that, but it's tricky to only show it once, it would show on every render.

@amannn
Copy link
Contributor

amannn commented Aug 18, 2020

If somebody is interested, I've built a plugin that allows to add vanilla JS entry points for pages that have unstable_runtimeJS: false: https://github.com/amannn/next-client-script. It's useful if you still need a bit of interactivity but you don't want React on the client side for performance reasons.

Note that this plugin modifies webpack configuration of Next.js. Similar as with other Next.js plugins that do this, it's possible that this plugin will break when there are updates to Next.js. I'm keeping the plugin updated so that it continues to work with new versions of Next.js. I've just updated the plugin to work with Next.js 9.5 🙂 .

@artcg
Copy link

artcg commented Aug 25, 2020

A neater way of doing this (pruning out the script tags via a custom _document.js) is possibly not putting them in the first place:

Have some logic in a custom node server.js which renders your react components for pages you want to be truly static via ReactDOMServer.renderToStaticMarkup (of course, also including any HTML tags which are needed)

and serving this for paths as necessary

That also has the advantage of leaving your site completely untouched for normal paths which need javascript interactivity

I am doing this for a side project I am working on and it's working nicely

@adammockor
Copy link

@artcg do you example of that server.js logic?

@artcg
Copy link

artcg commented Aug 26, 2020

Sure, I won't include all the boilerplate etc that you would include but the pattern is simply

// server.js
const {createServer} = require('http');
const {parse} = require('url');
const next = require('next');

const app = next({dev: process.env.NODE_ENV !== 'production'});

const handle = app.getRequestHandler();

app.prepare().then(() => {
  createServer((req, res) => {

    const parsedUrl = parse(req.url, true);

    if (parsedUrl.pathname === '/static_example') {
      yourOwnCustomHandler(req, res);
      // render your React Component to static markup here,
      // add HTTP headers, and write to res, then call res.end()

    } else {
      handle(req, res, parsedUrl);
    }
  }).listen(8080, err => {
    if (err) throw err;
  });
});

The result is effectively your entire site is running with NextJS, except for whichever route you define which you can handle yourself

It's just node so it is fully controllable :)

@timneutkens
Copy link
Member

Sure, I won't include all the boilerplate etc that you would include but the pattern is simply

// server.js
const {createServer} = require('http');
const {parse} = require('url');
const next = require('next');

const app = next({dev: process.env.NODE_ENV !== 'production'});

const handle = app.getRequestHandler();

app.prepare().then(() => {
  createServer((req, res) => {

    const parsedUrl = parse(req.url, true);

    if (parsedUrl.pathname === '/static_example') {
      yourOwnCustomHandler(req, res);
      // render your React Component to static markup here,
      // add HTTP headers, and write to res, then call res.end()

    } else {
      handle(req, res, parsedUrl);
    }
  }).listen(8080, err => {
    if (err) throw err;
  });
});

The result is effectively your entire site is running with NextJS, except for whichever route you define which you can handle yourself

It's just node so it is fully controllable :)

This solutions makes you opt out of all Next.js features because you opt-out of using Next.js completely. Would recommend just using #11949.

@artcg
Copy link

artcg commented Aug 26, 2020

I tried that but it didn't work, and there is no documentation for it anywhere, for my case it was easier to opt out of next for that file

@naorye
Copy link

naorye commented Oct 9, 2020

Isn't the following solve this issue?

export const config = {
  unstable_runtimeJS: false
}

export default () => <h1>My page</h1>

(taken from #11949)

@timneutkens
Copy link
Member

Isn't the following solve this issue?

export const config = {
  unstable_runtimeJS: false
}

export default () => <h1>My page</h1>

(taken from #11949)

Only partially. See #11949 (comment)

@lambrospetrou
Copy link

lambrospetrou commented Mar 11, 2021

Having truly static pages is something I want as well.

I ended up doing it post-export since I already had some hooks there, but having it natively would be nicer.

I am looking forward to https://twitter.com/shaneOsbourne/status/1360708099552780293 being released :)

@hcharley
Copy link

hcharley commented Mar 24, 2021

My usecase is similar to those that have mentioned "email templates."

Email templates really shouldn't have any JS in them at all, so having this ability would really open doors for my team.

Right now I am planning on just injecting the template into an iframe to support this usecase. Here's what I'm doing:

import { EmailTemplateContextUiProvider } from '@ui/contexts/email-template-context';
import React, { FC, ReactChild, ReactChildren } from 'react';
import Frame from 'react-frame-component';

export interface EmailTemplateBaseProps {
  shouldRenderIframeWrapper?: boolean;
  shouldRenderHtmlAndBody?: boolean;
  shouldRenderNextPage?: boolean;
  title?: string;
  head?: ReactChildren | ReactChild;
}

export const EmailTemplateBase: FC<EmailTemplateBaseProps> = ({
  children,
  shouldRenderNextPage = true,
  shouldRenderIframeWrapper = shouldRenderNextPage,
  shouldRenderHtmlAndBody = shouldRenderIframeWrapper,
  title = 'My Email Template',
}) => {
  let newChildren = (
    <EmailTemplateContextUiProvider>
      {children}
    </EmailTemplateContextUiProvider>
  );

  // Optionally render template inside a full HTML document, otherwise
  // render the template's body only
  if (shouldRenderHtmlAndBody) {
    newChildren = (
      <html>
        <head>
          <title>{title}</title>
        </head>
        <body>{children}</body>
      </html>
    );
  }

  // Optionally render template inside an <iframe /> to avoid any JS from Next.
  // When NextJS supports fully static pages, we can rethink this: https://github.com/vercel/next.js/issues/5054#issuecomment-805829676
  if (shouldRenderIframeWrapper) {
    newChildren = (
      <Frame
        style={{
          width: '100%',
          height: '100%',
          border: 0,
          flexGrow: 1,
          padding: 0,
          margin: 0,
        }}
      >
        {children}
      </Frame>
    );
  }
  return newChildren;
};

@jerrygreen
Copy link
Contributor

Just figured I need some embeddable component for an iframe for one page, pretty much like Twitter embeddable card or something. I would like to have some js in there though, so react hooks etc, - I want that, but I wanna disable all the nextjs features like any kind of preloading, routing, etc. Is it possible to disable all that just for one page? Or should I create-next-app inside another nextjs app to isolate this little page and it's the only way currently?

@aralroca
Copy link
Contributor

aralroca commented Jun 1, 2021

In my case, I'm still using the workaround that @bancek proposed above because the unstable_runtimeJS flag doesn't solve my problem. I want to disable all the JavaScript based on the user-agent (for bots, as dynamic rendering), not based on-page.

The problem I recently encountered is when using the new Image component, that the loading="lazy" is managed with JavaScript and it does not load the images. As a solution, I made another workaround that I tell here... I hope a better solution to this will be found.

@alex-drocks
Copy link

I think this is worth the effort. I was tempted to use astro just because of this. But i prefer Next by far so im kinda stuck with either full React or only basic self made html css build tools with webpack. I even tried Hugo but it was way overcomplicated.

@lambrospetrou
Copy link

Now with Next.js 12 and the alpha integration with Server Components, will this issue have a proper solution?

unstable_runtimeJS gets somewhat there but if you want to add a small dynamic widget then you either have to go full runtime again, or add an extra JS bundle that you control outside Next js and will render those widgets. It works but it's a hassle.

I was waiting for the React Server Components (RSC) to land hoping that they would trivially solve this issue since they could run once during compile time, and then the runtime could just include the client component along with the static version of the server components embedded (if needed at all).

However, the Server Components alpha documentation, https://nextjs.org/docs/advanced-features/react-18#unsupported-nextjs-apis, mentions:

Server Components are executed per requests and aren't stateful.

No support for getInitialProps, getStaticProps and getStaticPaths

So this feels that they are only usable when you actually have a server running, and not integrated with next export.

Is this something due to being alpha integration yet, or is this how it's going to be in the final state as well?

Thanks for all the amazing work so far!

@vercel vercel locked and limited conversation to collaborators Dec 7, 2021
@balazsorban44 balazsorban44 converted this issue into discussion #32217 Dec 7, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests