17

I am a bit confused by the documentation and not sure if it's possible what I am trying to do.

Goal:

  • Export NextJS app statically and host it on netlify
  • Allow users to create posts and have links to these posts which work

For example:

  • User creates a new post with the id: 2
  • This post should be publicly accessible under mysite.com/posts/2

I'd imagine that I can create a skeleton html file called posts.html and netlify redirects all posts/<id> requests to that posts.html file which will then show the skeleton and load the necessary data via an API on the fly.

I think without this netlify hack, my goal of static export + working links to dynamic routes is not possible with next.js according to their documentation since fallback: true is only possible when using SSR.

Question: How can I achieve my dream setup of static nextjs export + working links to dynamic routes?

EDIT: I just found out about Redirects. They could be the solution to my problem.

siva
  • 1,183
  • 3
  • 12
  • 28

1 Answers1

13

getStaticProps and getStaticPaths()

It looks like using getStaticProps and getStaticPaths() is the way to go.

I have something like this in my [post].js file:

const Post = ({ pageContent }) => {
  // ...
}

export default Post;

export async function getStaticProps({ params: { post } }) {
  const [pageContent] = await Promise.all([getBlogPostContent(post)]);
  return { props: { pageContent } };
}

export async function getStaticPaths() {
  const [posts] = await Promise.all([getAllBlogPostEntries()]);

  const paths = posts.entries.map((c) => {
    return { params: { post: c.route } }; // Route is something like "this-is-my-post"
  });

  return {
    paths,
    fallback: false,
  };
}

In my case, I query Contentful using my getAllBlogPostEntries for the blog entries. That creates the files, something like this-is-my-post.html. getBlogPostContent(post) will grab the content for the specific file.

export async function getAllBlogPostEntries() {
  const posts = await client.getEntries({
    content_type: 'blogPost',
    order: 'fields.date',
  });
  return posts;
}

export async function getBlogPostContent(route) {
  const post = await client.getEntries({
    content_type: 'blogPost',
    'fields.route': route,
  });
  return post;
}

When I do an npm run export it creates a file for each blog post...

info  - Collecting page data ...[
  {
    params: { post: 'my-first-post' }
  },
  {
    params: { post: 'another-post' }
  },

In your case the route would just be 1, 2, 3, etc.


Outdated Method - Run a Query in next.config.js

If you are looking to create a static site you would need to query the posts ahead of time, before the next export.

Here is an example using Contentful which you might have set up with blog posts:

First create a page under pages/blog/[post].js.

Next can use an exportMap inside next.config.js.

// next.config.js
const contentful = require('contentful');

// Connects to Contentful
const contentfulClient = async () => {
  const client = await contentful.createClient({
    space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
    accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
  });
  return client;
};

// Gets all of the blog posts
const getBlogPostEntries = async (client) => {
  const entries = await client.getEntries({
    content_type: 'blogPost',
    order: 'fields.date',
  });
  return entries;
};

module.exports = {
  async exportPathMap() {
    const routes = {
      '/': { page: '/' }, // Index page
      '/blog/index': { page: '/blog' }, // Blog page
    };

    const client = await contentfulClient();
    const posts = await getBlogPostEntries(client);

    // See explanation below
    posts.items.forEach((item) => {
      routes[`/blog/${item.fields.route}`] = { page: '/blog/[post]' };
    });

    return routes;
  },
};

Just above return routes; I'm connecting to Contentful, and grabbing all of the blog posts. In this case each post has a value I've defined called route. I've given every piece of content a route value, something like this-is-my-first-post and just-started-blogging. In the end, the route object looks something like this:

routes = {
  '/': { page: '/' }, // Index page
  '/blog/index': { page: '/blog' }, // Blog page
  '/blog/this-is-my-first-post': { page: '/blog/[post]' },
  '/blog/just-started-blogging': { page: '/blog/[post]' },
};

Your export in the out/ directory will be:

out/
   /index.html
   /blog/index.html
   /blog/this-is-my-first-post.html
   /blog/just-started-blogging.html

In your case, if you are using post id numbers, you would have to fetch the blog posts and do something like:

const posts = await getAllPosts();

posts.forEach((post) => {
  routes[`/blog/${post.id}`] = { page: '/blog/[post]' };
});

// Routes end up like
// routes = {
//   '/': { page: '/' }, // Index page
//   '/blog/index': { page: '/blog' }, // Blog page
//   '/blog/1': { page: '/blog/[post]' },
//   '/blog/2': { page: '/blog/[post]' },
// };

The next step would be to create some sort of hook on Netlify to trigger a static site build when the user creates content.

Also here is and idea of what your pages/blog/[post].js would look like.

import Head from 'next/head';

export async function getBlogPostContent(route) {
  const post = await client.getEntries({
    content_type: 'blogPost',
    'fields.route': route,
  });
  return post;
}

const Post = (props) => {
  const { title, content } = props;
  return (
    <>
      <Head>
        <title>{title}</title>
      </Head>
      {content}
    </>
  );
};

Post.getInitialProps = async ({ asPath }) => {
  // asPath is something like `/blog/this-is-my-first-post`
  const pageContent = await getBlogPostContent(asPath.replace('/blog/', ''));
  const { items } = pageContent;
  const { title, content } = items[0].fields;
  return { title, content };
};

export default Post;

Lee Han Kyeol
  • 2,371
  • 2
  • 29
  • 44
narmageddon
  • 1,117
  • 2
  • 10
  • 7
  • 5
    Does this work for blog posts created after npm run export is run? I'm trying to get something working where users create bookings separate to me exporting and deploying. – martinedwards Nov 02 '21 at 13:00
  • 3
    Based on the documentation, I understand that when using the static export feature from next, you're limited to dynamic routes using `getStaticPaths` which is determines the available pages at **build time** and not at runtime. Therefore, you wouldn't be able to route to a blog post that was created after you built your next app. You would have to re-build your next app so that `getStaticPaths` picks up the new post and generates the static site for it which seems like a major drawback of using the static export feature together with dynamic routing... – Ynnckth Jun 30 '22 at 08:49
  • 1
    As far as I know, @Ynnckth is right. But I don't understand why NextJS doesn't support loading data dynamically in a statically exported app, because HTML files can run JavaScript, which can call APIs etc. I'm trying to read stock tickers from a list in the browser's IndexedDB and route to a page to register transactions on a stock from that list. Then the Transactions page will need the ticker in it's dynamic route, which means reading a value from the URI. That should be possible with pure HTML and JavaScript. – Eivind Gussiås Løkseth May 14 '23 at 20:44