iansinnott / react-static-webpack-plugin

Build full static sites using React, React Router and Webpack (Webpack 2 supported)
MIT License
155 stars 24 forks source link

Dynamic Routes #2

Open iansinnott opened 8 years ago

iansinnott commented 8 years ago

AKA routes that look like

<Route path='posts/:id' component={Post} />

The issue

We need dynamic routes to support creating an entire category of sties including blogs. However currently we don't have a way to turn something like posts/:id into a file directly. Normally we would have something like this:

posts/
  hellow-world.md
  second-post.md
  still-here.md

Which would generate the following HTML...

posts/
  hello-world.html
  second-post.html
  still-here.html

But in our case React Router has no way of knowing the contents of our posts/ directory so how would it know what files to generate?

Most static site generators would read the posts/ directory and then generate static sites from those files. However, that requires the user to either learn how to configure their generator or learn the conventions for creating dynamic content.

We could do the same thing:

fs.readdirSync('./posts').forEach(filename => {
  // 1. Read contents of file
  // 2. Generate a static HTML file for it
});

But then the user would have to know how to hook into this process. It is a first-class goal of this project to not force the user to learn some new technology. Just add this plugin to a webpack config and go. This is the issue I'd like to solve.

iansinnott commented 8 years ago

Update

Thinking out loud here. One possible approach is webpack contexts. For example, given a posts/ directory containing js files that export components to be rendered as blog posts we could do this:

// Import all necessary modules up here...

// Get a list of post components using webpack context
const context = require.context('./posts', false, /\.js$/);
const posts = context.keys().sort().map(context);

export const routes = (
  <Route path='/' title='App' component={App}>
    {/* Other router config here... */}
    {posts.map((module, i) => {
      const Component = module.__esModule ? module.default : module;
      return <Route key={i} path={String(i)} component={Component} />;
    })}
    <Route path='*' title='404: Post Not Found' component={NotFound} />
  </Route>
);

export default routes;
iansinnott commented 8 years ago

Another potential solution would be to expect directories to exist based on routes. For example, given the following router config:

// Import all necessary modules up here...

export const routes = (
  <Route path='/' title='App' component={App}>
    <Route path='posts' component={Posts}>
      <IndexRoute component={PostList} />
      <Route path=':id' component={Post} />
    </Route>
    <Route path='*' title='404: Post Not Found' component={NotFound} />
  </Route>
);

export default routes;

We would see the :id route and infer the that the user must have a directory on the filesystem containing the files necessary to generate these routes. In this case, we would look up the tree and see the parent route has a path of 'posts' so we would expect a posts/ directory at the project root.

Drawbacks:

scherler commented 8 years ago

Why not allow to pass path ([]) as option and then in https://github.com/iansinnott/react-static-webpack-plugin/blob/master/src/index.js#L83 merge the user one and the ones from the router.

That would remove any dependencies on file system or such and open it for other underlying logic to generate the paths.

---webpack.config.js
/*
* the variable could be generated from outside and then set dynamically.
* in my use case I fetch some json data (jenkins-plugins) and if that is fetched I want to invoke a static export of all the detail page of each plugin. So I would end up with an array of 1200+ and pass it to the plugin
*/
const paths = ['/', '/post/xxx', '...']; 

new StaticSitePlugin({
      paths: paths, 
      src: 'app',
      stylesheet: '/app.css',
      favicon: '/favicon.ico',
    }),

--index.js#L83
    const routerPaths = getAllPaths(Component);
    log('Parsed routes:', routerPaths);
    const { paths: userPaths } = this.options;
    const paths = Array.from(new Set(routerPaths.concat(userPaths)));

Or are there some pitfalls that I do not see?

iansinnott commented 8 years ago

Hm, where would you get the paths from?

I'm not entirely clear how this would work, but you might be on to something here. So let's assume you already had the array of paths, how would use use them in your routes.js file?

If you put together or a complete example of how the API might look I'm happy to check it out. I would also welcome a pull request if you think you have a solution :)

scherler commented 8 years ago

@iansinnott the path you can inject or write them as I did in the above example. I will have a look now since I am may need to change how the plugin resolves the paths.

As short explanation I am ATM working on https://github.com/scherler/react-static-boilerplate/tree/redux the redux branch to add it to the boilerplate if you like it. However there is a problem in the way we normally would invoke the store vs. the export all routes.

If you look into the above branch you will find

    <Provider store={store}>
      <Router routes={routes} history={browserHistory} />
    </Provider>

To inject the store you need to to this in the top parent or you will need to do it in each route. I am still playing around to see whether I can move it around somehow, will let you know.

f0rr0 commented 8 years ago

@iansinnott Gatsby does this by traversing the file-tree. https://github.com/gatsbyjs/gatsby/blob/master/lib/isomorphic/create-routes.js

It uses https://github.com/markdalgleish/static-site-generator-webpack-plugin/ underneath which provides a routes option. It does not use react-router, so all the routes need to be specified by the user. If I am not wrong, @scherler was suggesting something along that line (merging user provided routes into the already existing ones from react-router).

However, I like your require.context approach. Any progress on that?

iansinnott commented 8 years ago

Hey @sidjain26 thanks for the ping. I haven't looked at this issue in a while, because for whatever reason I haven't yet run into the need for dynamic routes. I've been using this plugin extensively but it's all been for sites without dynamic routing.

In my own minimal testing in the past the context approach actually worked quite well. it's currently the option I favor because it is explicit and doesn't require the user to know any special configuration—this is a first class goal of this project.

What's also nice is that the require.context approach should work out of the box right now, because it doesn't require any specific configuration within this plugin. It's simply leveraging a feature of webpack. So although I don't currently have a guide for how to do this it should be a viable option for anyone wanting to implement dynamic routes.

Once I rewrite my blog using this plugin I will definitely figure out how to do dynamic routing.

lifeiscontent commented 8 years ago

@iansinnott how would you do something like this for I18n routes? e.g. /about and /fr/about?

iansinnott commented 8 years ago

@lifeiscontent I'm not sure, but my guess is that Webpack has a i18n solution. Have you tried internationalizing a web app with webpack yet?