- Draft pages
- Injecting data
- Routing within a page
- Minimal builds for single-page apps
- Markdown within JS
- Route change listeners
- Analyzing bundles
- Page-specific CSS
- Generating tables of contents for Markdown pages
Any page with the front matter published: false
will be considered a draft page.
In development, draft pages are built and visible.
However, in production
builds these pages are not included and should be handled with a 404 by the server.
Most of the time, you should store data as JSON or JS and import
or require
it as needed.
Nothing special.
If, however, you are dealing with lots of data; that data is used across a number of pages; and each of those pages does not need all of the data — then you may not want to write all that data into your JS bundles. You may want to control which parts of it get written to which bundles.
You can do this with the dataSelectors
configuration option.
Store data in JSON or JS, anywhere in your project, then specify which data to inject into any given page with dataSelectors
in your configuration.
dataSelectors
also have access to build-time data, like the front matter of all the pages being compiled.
Each data selector creates a module that can be import
ed to inject the return value into a component or page.
The return value of each data selector is the default export of the module available at @mapbox/batfish/data/[selector-name-kebab-cased]
.
Example:
// batfish.config.js
const myBigData = require('path/to/my/big-data.json');
module.exports = () => {
return {
/* ... */
dataSelectors: {
posts: data => {
return data.pages.filter(pagesData => /\/posts\//.test(pagesData.path));
},
fancyDesserts: () => {
return myBigData.recipes.desserts;
}
}
};
};
// Page
import React from 'react';
import { DessertDisplay } from 'path/to/dessert-display';
import posts from '@mapbox/batfish/data/posts';
import fancyDesserts from '@mapbox/batfish/data/fancy-desserts';
export default class MyPage extends React.PureComponent {
render() {
return (
<div>
<h1>Page!</h1>
<h2>Posts</h2>
{posts.map(post => {
return (
<div key={post.path}>
<a href={post.path}>{post.frontMatter.title}</a>
</div>
);
})}
<h2>Desserts</h2>
{fancyDesserts.map(dessert => {
return (
<DessertDisplay key={dessert.id} {...dessert} />
);
})}
</div>
);
}
}
If you'd like to use a client-side routing library within a Batfish page, like React Router or nanorouter, add internalRouting: true
to the page's front matter.
By specifying that the page has internal routes, any URLs that start with the page's path will be considered matches.
If the page is pages/animals.js
, for example, then /animals/
will match as usual, but /animals/tiger/
and /animals/zebra/
will also match.
The client-side router you use within the page can determine what to do with the rest of the URL.
Look at examples/internal-routing
to see how this works.
If your app includes only one page or else all the client-side routing is handled with some other client-side routing library, like React Router or nanorouter, you can turn off all of Batfish's routing.
To do this, set the spa
configuration option to true
.
Read more about the effects of spa
in the option's documentation.
You may want to minimize the amount of code that gets parsed and executed doing the static build.
One reason is so the static build runs as quickly as possible: instead of passing all of your code through Webpack, you can only pass the code that's needed to build your minimal static HTML.
Another reason is to allow you to write code, or import dependencies, that will rely completely on a browser environment — that global window
object — without bumping up against errors during the static build.
For production apps, you probably want to think about what gets rendered before the JS downloads and executes; so you can do the following:
- Include an app shell and loading state in your single page.
- Dynamically
import(/* webpackMode: "eager" */ '../path/to/app')
your main app component in the page'scomponentDidMount
hook. (/* webpackMode: "eager" */
tells Webpack not to create a separate async chunk with this file, but to include it in the main client-side bundle.) - Use
webpackStaticIgnore
to block '../path/to/app' from being included in the static build. - Set
staticHtmlInlineDeferCss
tofalse
to avoid a flash of unstyled content.
For example:
// Page component, which will be statically rendered.
import React from 'react';
import { Helmet } from 'react-helmet';
import InitialLoadingState from '../initial-loading-state';
export default Page extends React.Component {
constructor() {
super();
this.state = { body: <InitialLoadingState /> };
}
componentDidMount() {
import(/* webpackMode: "eager" */ '../app').then(AppModule => {
this.setState({ body: <AppModule.default /> });
});
}
render() {
return (
<div>
<Helmet>
<title>Your title</title>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
{/* ... other <head> things ... */}
</Helmet>
{this.state.body}
</div>
);
}
}
// batfish.config.js
const path = require('path');
module.exports = () => {
return {
webpackStaticIgnore: path.join(__dirname, 'src/app.js')
// ... other config
};
}
Sometimes you don't care at all about the static HTML that gets served, and just want an HTML shell with some things in the <head>
and a completely empty <body>
that will be populated when the JS downloads and executes.
This is the kind of app you build with create-react-app, which you might use for prototyping, internal tooling, etc.
To accomplish this:
- Use
webpackStaticStubReactComponent
to stub your main app component. - Set
staticHtmlInlineDeferCss
tofalse
to avoid a flash of unstyled content.
For example:
// Page component, which will be statically rendered.
import React from 'react';
import { Helmet } from 'react-helmet';
import App from '../app';
export default Page extends React.Component {
render() {
return (
<div>
<Helmet>
<title>Your title</title>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
{/* ... other <head> things ... */}
</Helmet>
<App />
</div>
);
}
}
// batfish.config.js
const path = require('path');
module.exports = () => {
return {
webpackStaticStubReactComponent: [path.join(__dirname, 'src/app.js')]
// ... other config
};
}
You can use jsxtreme-markdown within JS, as well as in .md
page files.
It is compiled by Babel, so your browser bundle will not need to include a Markdown parser!
Batfish exposes babel-plugin-transform-jsxtreme-markdown as @mapbox/batfish/modules/md
.
The value of this (fake) module is a template literal tag.
Any template literal with this tag will be compiled as Markdown (jsxtreme-markdown, with interpolated JS expression and JSX elements) at compile time.
const React = require('react');
const md = require('@mapbox/batfish/modules/md');
class MyPage extends React.Component {
render() {
const text = md`
# A title
This is a paragraph. Receives interpolated props, like this one:
{{this.props.location}}.
You can use interpolated {{<span className="foo">JSX elements</span>}},
also.
`;
return (
<div>
{/* some fancy stuff */}
{text}
{/* some more fancy stuff */}
</div>
);
}
}
To attach listeners to route change events (e.g. add a page-loading animation), use the route-change-listeners
module.
Batfish's --stats
flag, when used with the build
command, will output Webpack's stats.json
so you can use it to analyze the composition of your bundles.
webpack-bundle-analyzer and webpack.github.io/analyse are two great tools that you can feed your stats.json
to.
There are also others out there in the Webpack ecosystem.
Most of the time, you should add CSS to your site with the stylesheets
configuration option.
However, if you are adding a lot of CSS that is not widely used, you might choose to add it to one page at a time, instead of adding it to the full site's stylesheet.
Batfish includes a way to to this.
If you import
a .css
file within your pagesDirectory
, you will get a React component (with no props) that you can render within the page.
When the component mounts, the stylesheet's content (processed through PostCSS, using your postcssPlugins
) will be inserted into a <style>
tag in the <head>
of the document.
When the component unmounts, that <style>
tag will be removed.
Like other React components, this one will only be added to the JS bundle of the page that uses it (unless you use it in a number of pages); and it will be rendered into the page's HTML during static rendering. So that's how you can page-specific CSS, when the fancy strikes.
Example:
import React from 'react';
import SpecialStyles from './special-styles.css';
export default class SomePage extends React.Component {
render() {
return (
<div>
<SpecialStyles />
<h1>Some page</h1>
{/* ... */}
</div>
);
}
}
You can turn this behavior off if you have your own preferences about what to do with imported .css
files.
Set the pageSpecificCss
option to false
.
The front matter of Markdown pages is automatically augmented with a headings
property that includes data about the headings in the Markdown file.
(This is a feature of jsxtreme-markdown.)
You can use this data in your Markdown wrapper to automatically generate a table of contents!
The value headings
is an an array of objects; each object has the following properties:
text
: the text of the heading.slug
: the slugified heading text, which corresponds to anid
attribute automatically added to the heading elements. Use this to create hash fragment links, e.g.<a href={
#${item.slug}}>
.level
: the level of the heading (1-6).
Use regular JS methods like filter
, map
, etc., to transform the provided data structure into the table of contents of your dreams.
For example, examples/table-of-contents/
includes a Markdown wrapper that creates a table of contents that only displays headings of levels 2 and 3 and indents level 3 headings.