UNPKG

12.9 kBMarkdownView Raw
1# Advanced Usage
2
3![](../batfish-ultra.png)
4
5## Table of contents
6
7- [Draft pages](#draft-pages)
8- [Injecting data](#injecting-data)
9- [Routing within a page](#routing-within-a-page)
10- [Minimal builds for single-page apps](#minimal-builds-for-single-page-apps)
11 - [Turn off Batfish's routing](#turn-off-batfishs-routing)
12 - [Minimize the static HTML build](#minimize-the-static-html-build)
13- [Markdown within JS](#markdown-within-js)
14- [Route change listeners](#route-change-listeners)
15- [Analyzing bundles](#analyzing-bundles)
16- [Page-specific CSS](#page-specific-css)
17- [Generating tables of contents for Markdown pages](#generating-tables-of-contents-for-markdown-pages)
18
19## Draft pages
20
21Any page with the front matter `published: false` will be considered a draft page.
22
23In development, draft pages are built and visible.
24However, in [`production`] builds these pages are **not** included and should be handled with a 404 by the server.
25
26## Injecting data
27
28Most of the time, you should store data as JSON or JS and `import` or `require` it as needed.
29Nothing special.
30
31If, 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.
32You may want to control which parts of it get written to which bundles.
33
34You can do this with the [`dataSelectors`] configuration option.
35Store data in JSON or JS, anywhere in your project, then specify which data to inject into any given page with [`dataSelectors`] in your configuration.
36[`dataSelectors`] also have access to build-time data, like the front matter of all the pages being compiled.
37
38Each data selector creates a module that can be `import`ed to inject the return value into a component or page.
39**The return value of each data selector is the default export of the module available at `@mapbox/batfish/data/[selector-name-kebab-cased]`.**
40
41Example:
42
43```js
44// batfish.config.js
45const myBigData = require('path/to/my/big-data.json');
46
47module.exports = () => {
48 return {
49 /* ... */
50 dataSelectors: {
51 posts: data => {
52 return data.pages.filter(pagesData => /\/posts\//.test(pagesData.path));
53 },
54 fancyDesserts: () => {
55 return myBigData.recipes.desserts;
56 }
57 }
58 };
59};
60
61// Page
62import React from 'react';
63import { DessertDisplay } from 'path/to/dessert-display';
64import posts from '@mapbox/batfish/data/posts';
65import fancyDesserts from '@mapbox/batfish/data/fancy-desserts';
66
67export default class MyPage extends React.PureComponent {
68 render() {
69 return (
70 <div>
71 <h1>Page!</h1>
72 <h2>Posts</h2>
73 {posts.map(post => {
74 return (
75 <div key={post.path}>
76 <a href={post.path}>{post.frontMatter.title}</a>
77 </div>
78 );
79 })}
80 <h2>Desserts</h2>
81 {fancyDesserts.map(dessert => {
82 return (
83 <DessertDisplay key={dessert.id} {...dessert} />
84 );
85 })}
86 </div>
87 );
88 }
89}
90```
91
92## Routing within a page
93
94If 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.
95
96By specifying that the page has internal routes, any URLs that *start with* the page's path will be considered matches.
97If the page is `pages/animals.js`, for example, then `/animals/` will match as usual, but `/animals/tiger/` and `/animals/zebra/` will *also* match.
98The client-side router you use within the page can determine what to do with the rest of the URL.
99
100Look at [`examples/internal-routing`](../examples/internal-routing) to see how this works.
101
102## Minimal builds for single-page apps
103
104### Turn off Batfish's routing
105
106If 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.
107
108To do this, set the [`spa`] configuration option to `true`.
109Read more about the effects of [`spa`] in the option's documentation.
110
111### Minimize the static HTML build
112
113You may want to minimize the amount of code that gets parsed and executed doing the static build.
114
115One 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.
116
117Another 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.
118
119For production apps, you probably want to think about what gets rendered *before* the JS downloads and executes; so you can do the following:
120
121- Include an app shell and loading state in your single page.
122- Dynamically `import(/* webpackMode: "eager" */ '../path/to/app')` your main app component in the page's `componentDidMount` hook.
123 (`/* webpackMode: "eager" */` tells Webpack not to create a separate async chunk with this file, but to include it in the main client-side bundle.)
124- Use [`webpackStaticIgnore`] to block '../path/to/app' from being included in the static build.
125- Set [`staticHtmlInlineDeferCss`] to `false` to avoid a flash of unstyled content.
126
127For example:
128
129```jsx
130// Page component, which will be statically rendered.
131import React from 'react';
132import Helmet from 'react-helmet';
133import InitialLoadingState from '../initial-loading-state';
134
135export default Page extends React.Component {
136 constructor() {
137 super();
138 this.state = { body: <InitialLoadingState /> };
139 }
140
141 componentDidMount() {
142 import(/* webpackMode: "eager" */ '../app').then(AppModule => {
143 this.setState({ body: <AppModule.default> });
144 });
145 }
146
147 render() {
148 return (
149 <div>
150 <Helmet>
151 <title>Your title</title>
152 <meta charset='utf-8' />
153 <meta name='viewport' content='width=device-width, initial-scale=1' />
154 {/* ... other <head> things ... */}
155 </Helmet>
156 {this.state.body}
157 </div>
158 );
159 }
160}
161```
162
163```js
164// batfish.config.js
165const path = require('path');
166
167module.exports = () => {
168 return {
169 webpackStaticIgnore: path.join(__dirname, 'src/app.js')
170 // ... other config
171 };
172}
173```
174
175Sometimes 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.
176This is the kind of app you build with create-react-app, which you might use for prototyping, internal tooling, etc.
177
178To accomplish this:
179
180- Use [`webpackStaticStubReactComponent`] to stub your main app component.
181- Set [`staticHtmlInlineDeferCss`] to `false` to avoid a flash of unstyled content.
182
183For example:
184
185```jsx
186// Page component, which will be statically rendered.
187import React from 'react';
188import Helmet from 'react-helmet';
189import App from '../app';
190
191export default Page extends React.Component {
192 render() {
193 return (
194 <div>
195 <Helmet>
196 <title>Your title</title>
197 <meta charset='utf-8' />
198 <meta name='viewport' content='width=device-width, initial-scale=1' />
199 {/* ... other <head> things ... */}
200 </Helmet>
201 <App />
202 </div>
203 );
204 }
205}
206```
207
208```js
209// batfish.config.js
210const path = require('path');
211
212module.exports = () => {
213 return {
214 webpackStaticStubReactComponent: [path.join(__dirname, 'src/app.js')]
215 // ... other config
216 };
217}
218```
219
220## Markdown within JS
221
222You can use [jsxtreme-markdown](https://github.com/mapbox/jsxtreme-markdown) within JS, as well as in `.md` page files.
223It is compiled by Babel, so your browser bundle will not need to include a Markdown parser!
224
225Batfish exposes [babel-plugin-transform-jsxtreme-markdown] as `@mapbox/batfish/modules/md`.
226The value of this (fake) module is a [template literal tag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals).
227Any template literal with this tag will be compiled as Markdown (jsxtreme-markdown, with interpolated JS expression and JSX elements) at compile time.
228
229```js
230const React = require('react');
231const md = require('@mapbox/batfish/modules/md');
232
233class MyPage extends React.Component {
234 render() {
235 const text = md`
236 # A title
237
238 This is a paragraph. Receives interpolated props, like this one:
239 {{this.props.location}}.
240
241 You can use interpolated {{<span className="foo">JSX elements</span>}},
242 also.
243 `;
244
245 return (
246 <div>
247 {/* some fancy stuff */}
248 {text}
249 {/* some more fancy stuff */}
250 </div>
251 );
252 }
253}
254```
255
256## Route change listeners
257
258To attach listeners to route change events (e.g. add a page-loading animation), use the [`route-change-listeners`] module.
259
260## Analyzing bundles
261
262Batfish's `--stats` flag, when used with the `build` command, will output [Webpack's `stats.json`](https://webpack.js.org/api/stats/) so you can use it to analyze the composition of your bundles.
263
264[webpack-bundle-analyzer](https://github.com/th0r/webpack-bundle-analyzer) and [webpack.github.io/analyse](https://webpack.github.io/analyse/) are two great tools that you can feed your `stats.json` to.
265There are also others out there in the Webpack ecosystem.
266
267## Page-specific CSS
268
269Most of the time, you should add CSS to your site with the [`stylesheets`] configuration option.
270However, 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.
271Batfish includes a way to to this.
272
273If you `import` a `.css` file within your [`pagesDirectory`], you will get a React component (with no props) that you can render within the page.
274When 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.
275When the component unmounts, that `<style>` tag will be removed.
276
277Like 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.
278So that's how you can page-specific CSS, when the fancy strikes.
279
280Example:
281
282```js
283import React from 'react';
284import SpecialStyles from './special-styles.css';
285
286export default class SomePage extends React.Component {
287 render() {
288 return (
289 <div>
290 <SpecialStyles />
291 <h1>Some page</h1>
292 {/* ... */}
293 </div>
294 );
295 }
296}
297```
298
299**You can turn this behavior off if you have your own preferences about what to do with imported `.css` files.**
300Set the [`pageSpecificCss`] option to `false`.
301
302## Generating tables of contents for Markdown pages
303
304The front matter of Markdown pages is automatically augmented with a `headings` property that includes data about the headings in the Markdown file.
305(This is [a feature of jsxtreme-markdown](https://github.com/mapbox/jsxtreme-markdown/tree/master/packages/jsxtreme-markdown#headings).)
306
307You can use this data in your Markdown wrapper to automatically generate a table of contents!
308The value `headings` is an an array of objects; each object has the following properties:
309
310- `text`: the text of the heading.
311- `slug`: the slugified heading text, which corresponds to an `id` attribute automatically added to the heading elements.
312 Use this to create hash fragment links, e.g. `<a href={`#${item.slug}`}>`.
313- `level`: the level of the heading (1-6).
314
315Use regular JS methods like `filter`, `map`, etc., to transform the provided data structure into the table of contents of your dreams.
316For example, [`examples/table-of-contents/`](../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.
317
318[`route-change-listeners`]: ./batfish-modules.md#route-change-listeners
319
320[babel-plugin-transform-jsxtreme-markdown]: https://github.com/mapbox/babel-plugin-transform-jsxtreme-markdown
321
322[`stylesheets`]: ./configuration.md#stylesheets
323
324[`pagesdirectory`]: ./configuration.md#pagesdirectory
325
326[`postcssplugins`]: ./configuration.md#postcssplugins
327
328[`production`]: ./configuration.md#production
329
330[`dataselectors`]: ./configuration.md#dataselectors
331
332[`spa`]: ./configuration.md#spa
333
334[react router]: https://reacttraining.com/react-router/
335
336[nanorouter]: https://github.com/yoshuawuyts/nanorouter
337
338[`webpackstaticignore`]: ./configuration.md#webpackstaticignore
339
340[`webpackstaticstubreactcomponent`]: ./configuration.md#webpackstaticstubreactcomponent
341
342[`statichtmlinlinedefercss`]: ./configuration.md#statichtmlinlinedefercss
343
344[`pagespecificcss`]: ./configuration.md#pagespecificcss