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 |
|
21 | Any page with the front matter `published: false` will be considered a draft page.
|
22 |
|
23 | In development, draft pages are built and visible.
|
24 | However, in [`production`] builds these pages are **not** included and should be handled with a 404 by the server.
|
25 |
|
26 | ## Injecting data
|
27 |
|
28 | Most of the time, you should store data as JSON or JS and `import` or `require` it as needed.
|
29 | Nothing special.
|
30 |
|
31 | 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.
|
32 | You may want to control which parts of it get written to which bundles.
|
33 |
|
34 | You can do this with the [`dataSelectors`] configuration option.
|
35 | 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.
|
36 | [`dataSelectors`] also have access to build-time data, like the front matter of all the pages being compiled.
|
37 |
|
38 | Each 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 |
|
41 | Example:
|
42 |
|
43 | ```js
|
44 | // batfish.config.js
|
45 | const myBigData = require('path/to/my/big-data.json');
|
46 |
|
47 | module.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
|
62 | import React from 'react';
|
63 | import { DessertDisplay } from 'path/to/dessert-display';
|
64 | import posts from '@mapbox/batfish/data/posts';
|
65 | import fancyDesserts from '@mapbox/batfish/data/fancy-desserts';
|
66 |
|
67 | export 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 |
|
94 | 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.
|
95 |
|
96 | By specifying that the page has internal routes, any URLs that *start with* the page's path will be considered matches.
|
97 | If the page is `pages/animals.js`, for example, then `/animals/` will match as usual, but `/animals/tiger/` and `/animals/zebra/` will *also* match.
|
98 | The client-side router you use within the page can determine what to do with the rest of the URL.
|
99 |
|
100 | Look 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 |
|
106 | 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.
|
107 |
|
108 | To do this, set the [`spa`] configuration option to `true`.
|
109 | Read more about the effects of [`spa`] in the option's documentation.
|
110 |
|
111 | ### Minimize the static HTML build
|
112 |
|
113 | You may want to minimize the amount of code that gets parsed and executed doing the static build.
|
114 |
|
115 | 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.
|
116 |
|
117 | 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.
|
118 |
|
119 | For 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 |
|
127 | For example:
|
128 |
|
129 | ```jsx
|
130 | // Page component, which will be statically rendered.
|
131 | import React from 'react';
|
132 | import Helmet from 'react-helmet';
|
133 | import InitialLoadingState from '../initial-loading-state';
|
134 |
|
135 | export 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
|
165 | const path = require('path');
|
166 |
|
167 | module.exports = () => {
|
168 | return {
|
169 | webpackStaticIgnore: path.join(__dirname, 'src/app.js')
|
170 | // ... other config
|
171 | };
|
172 | }
|
173 | ```
|
174 |
|
175 | 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.
|
176 | This is the kind of app you build with create-react-app, which you might use for prototyping, internal tooling, etc.
|
177 |
|
178 | To 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 |
|
183 | For example:
|
184 |
|
185 | ```jsx
|
186 | // Page component, which will be statically rendered.
|
187 | import React from 'react';
|
188 | import Helmet from 'react-helmet';
|
189 | import App from '../app';
|
190 |
|
191 | export 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
|
210 | const path = require('path');
|
211 |
|
212 | module.exports = () => {
|
213 | return {
|
214 | webpackStaticStubReactComponent: [path.join(__dirname, 'src/app.js')]
|
215 | // ... other config
|
216 | };
|
217 | }
|
218 | ```
|
219 |
|
220 | ## Markdown within JS
|
221 |
|
222 | You can use [jsxtreme-markdown](https://github.com/mapbox/jsxtreme-markdown) within JS, as well as in `.md` page files.
|
223 | It is compiled by Babel, so your browser bundle will not need to include a Markdown parser!
|
224 |
|
225 | Batfish exposes [babel-plugin-transform-jsxtreme-markdown] as `@mapbox/batfish/modules/md`.
|
226 | The 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).
|
227 | Any 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
|
230 | const React = require('react');
|
231 | const md = require('@mapbox/batfish/modules/md');
|
232 |
|
233 | class 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 |
|
258 | To attach listeners to route change events (e.g. add a page-loading animation), use the [`route-change-listeners`] module.
|
259 |
|
260 | ## Analyzing bundles
|
261 |
|
262 | Batfish'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.
|
265 | There are also others out there in the Webpack ecosystem.
|
266 |
|
267 | ## Page-specific CSS
|
268 |
|
269 | Most of the time, you should add CSS to your site with the [`stylesheets`] configuration option.
|
270 | 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.
|
271 | Batfish includes a way to to this.
|
272 |
|
273 | 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.
|
274 | 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.
|
275 | When the component unmounts, that `<style>` tag will be removed.
|
276 |
|
277 | 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.
|
278 | So that's how you can page-specific CSS, when the fancy strikes.
|
279 |
|
280 | Example:
|
281 |
|
282 | ```js
|
283 | import React from 'react';
|
284 | import SpecialStyles from './special-styles.css';
|
285 |
|
286 | export 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.**
|
300 | Set the [`pageSpecificCss`] option to `false`.
|
301 |
|
302 | ## Generating tables of contents for Markdown pages
|
303 |
|
304 | The 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 |
|
307 | You can use this data in your Markdown wrapper to automatically generate a table of contents!
|
308 | The 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 |
|
315 | Use regular JS methods like `filter`, `map`, etc., to transform the provided data structure into the table of contents of your dreams.
|
316 | For 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
|