UNPKG

32.5 kBMarkdownView Raw
1# Canon
2Reusable React environment and components for creating visualization engines.
3
4![](https://github.com/datawheel/canon/raw/master/docs/bang.png)
5
6#### Contents
7* [Setup and Installation](#setup-and-installation)
8* [Deployment](#deployment)
9* [Header/Meta Information](#header-meta-information)
10* [Page Routing](#page-routing)
11 * [Window Location](#window-location)
12* [Hot Module Reloading](#hot-module-reloading)
13* [Redux Store](#redux-store)
14* [Localization](#localization)
15 * [Language Detection](#language-detection)
16 * [Changing Languages](#changing-languages)
17* [User Management](#user-management)
18 * [Loading User Information](#loading-user-information)
19 * [Privacy Policy and Terms of Service](#privacy-policy-and-terms-of-service)
20 * [Password Reset](#password-reset)
21 * [E-mail Verification](#e-mail-verification)
22 * [Roles](#roles)
23 * [Social Logins](#social-logins)
24 * [Facebook](#facebook)
25 * [Github](#github)
26 * [Google](#google)
27 * [Instagram](#instagram)
28 * [LinkedIn](#linkedin)
29 * [Twitter](#twitter)
30* [Custom API Routes](#custom-api-routes)
31* [Custom Database Models](#custom-database-models)
32* [Server-Side Caching](#server-side-caching)
33* [Opbeat Error Tracking](#opbeat-error-tracking)
34* [Additional Environment Variables](#additional-environment-variables)
35* [Custom Environment Variables](#custom-environment-variables)
36
37---
38
39## Setup and Installation
40
41Canon is published on NPM, and should be installed just like any other node package. After creating a package.json file (try `npm init`), install Canon like this:
42
43```bash
44npm i @datawheel/canon-core
45```
46
47Once installed, run the following command to create some initial scaffolding:
48
49```bash
50npx canon-setup
51```
52
53Now that the necessary files are in place, simply run `npm run dev` to spin up the development server. Once the process finished "Bundling Client Webpack", visit `https://localhost:3300` in the browser and view your beautiful Hello World!
54
55All React components are stored in the `app/` directory, with the main entry component being `app/App.jsx`. Here is the initial scaffolding you should see in your project folder:
56* `.vscode/` - VSCode editor settings for code linting
57* `app/` - majority of the front-end site code
58 * `components/` - components that are used by multiple pages
59 * `pages/` - page-specific components (like the homepage and profiles)
60 * > `reducers/` - any redux reducers needed for the react-redux store (required to exist, but unused initially)
61 * `App.jsx` & `App.css` - the main parent component that all pages extend
62 * `d3plus.js` - global d3plus visualization styles
63 * `helmet.js` - default meta information for all pages to be displayed between the `<head>` tags
64 * `routes.jsx` - hook ups for all of the page routes
65 * > `store.js` - default redux store (required to exist, but unused initially)
66 * `style.yml` - global color and style variables
67* `static/` - static files used by the site like images and PDFs
68* `.eslintrc` - javascript style rules used for consistent coding
69* `.gitignore` - development files to exclude from the git repository
70* `canon.js` - contains any canon settings/modifications (empty by default)
71
72---
73
74## Deployment
75
76Deploying a site with canon is as easy as these 2 steps:
77
78* `npm run build` to compile the necessary production server and client bundles
79* `npm run start` to start an Express server on the default port
80
81---
82
83## Header/Meta Information
84
85All tags inside of the `<head>` of the rendered page are configured using [Helmet](https://github.com/helmetjs/helmet). If a file is present at `app/helmet.js`, the Object it exports will be used as the configuration. This file can use either ES6 or node style exports, but if you import any other dependencies into that file you must use node's `require` syntax.
86
87Here is an example configuration, as seen in this repo's sample app:
88
89```js
90export default {
91 link: [
92 {rel: "icon", href: "/images/favicon.ico?v=2"},
93 {rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Work+Sans:300,400,500,600,700,900"}
94 ],
95 meta: [
96 {charset: "utf-8"},
97 {"http-equiv": "X-UA-Compatible", "content": "IE=edge"},
98 {name: "description", content: "Reusable React environment and components for creating visualization engines."},
99 {name: "viewport", content: "width=device-width, initial-scale=1"},
100 {name: "mobile-web-app-capable", content: "yes"},
101 {name: "apple-mobile-web-app-capable", content: "yes"},
102 {name: "apple-mobile-web-app-status-bar-style", content: "black"},
103 {name: "apple-mobile-web-app-title", content: "Datawheel Canon"}
104 ],
105 title: "Datawheel Canonical Design"
106};
107```
108---
109
110## Page Routing
111
112All page routes need to be hooked up in `app/routes.jsx`. This filename and location cannot be changed, as the internals of Canon rely on it's presence. For linking between pages, use the [react-router](https://github.com/ReactTraining/react-router) `<Link>` component:
113
114```jsx
115import {Link} from "react-router";
116...
117<Link to="/about">About Page</Link>
118```
119
120As a fallback (mainly related to CMS content), Canon also intercepts all `<a>` tags and identifies whether or not they are client-side react friendly links or if they are external links that require a full window location change. If you need to trigger a browser reload (like when [changing languages](#changing-languages)), just add the `data-refresh` attribute to your HTML tag:
121
122```jsx
123<a data-refresh="true" href="/?locale=es">Spanish</a>
124```
125
126When linking to an anchor ID on the current page, use the `<AnchorLink>` component exported by canon to enable a silky smooth scrollto animation:
127
128```jsx
129import {AnchorLink} from "@datawheel/canon-core";
130...
131<AnchorLink to="economy">Jump to Economy</AnchorLink>
132...
133<a id="economy" href="#economy">Economy Section</a>
134```
135
136If needing to modify the page location with JavaScript, you must use the current active router passed down to any component registered in `app/routes.jsx`. In the past, it was possible to use the `browserHistory` object exported from [react-router](https://github.com/ReactTraining/react-router), but as more advanced routing features have been added to canon, it is now necessary to use the inherited live instance. This live instance is available as `this.props.router`, and can be passed down to any children needing it (either through props or context). Here is an example usage:
137
138```jsx
139import React, {Component} from "react";
140
141class Tile extends Component {
142
143 onChangePage() {
144 const {router} = this.props;
145 router.push("new-route");
146 }
147
148 onChangeQuery() {
149 const {router} = this.props;
150 router.replace("current-route?stuff=here");
151 }
152
153}
154```
155
156Notice the different usage of `push` and `replace`. Pushing a new URL to the router effects the push/pop history of the browser (so back and forward buttons work), which replacing the URL simply updates the value without effecting the browser history.
157
158### Window Location
159
160There are 3 preferred ways (each with their use cases) to determine the current page the user is viewing:
161
1621. **redux `state.location`** - for server-side rendering, like if you need the current page in a `render` function when a component mounts. This object is created manually on the server-side to mimic `window.location`, but _does NOT get updated on subsequent react-router page views_.
1632. **`this.props.router.location`** - every top-level component that is connected to a route in `routes.jsx` has access to the main react-router instance, which should be relied on to always contain the currently viewed page.
1643. **`this.context.router.location`** - the current react-router instance is also passed down to every component via context.
165
166---
167
168## Hot Module Reloading
169
170To enable hot module reloading, the component being used on a Route (like `Home.jsx` or `Profile.jsx`) needs to be wrapped with the `hot` wrapper when exporting. Import it like this:
171
172```jsx
173import {hot} from "react-hot-loader/root";
174```
175
176And export it like this:
177
178```jsx
179export default hot(Home);
180```
181
182---
183
184## Redux Store
185
186Default values can be added to the Redux Store by creating a file located at `app/store.js`. This file should export an Object, whose values will be merged with the defaul store. This file can use either ES6 or node style exports, but if you import any other dependencies into that file you must use node's `require` syntax.
187
188Here is an example:
189
190```js
191export default {
192 countries: ["nausa", "sabra", "aschn"]
193};
194```
195
196---
197
198## Localization
199
200In order to enable localization for a Canon site, you must first define the available languages as an environment variable:
201
202```sh
203export CANON_LANGUAGES="en,es"
204```
205
206Next, any component that needs access to localized text needs to be wrapped in the react-i18next `withNamespaces` function:
207
208```jsx
209import React, {Component} from "react";
210import {Link} from "react-router";
211import {withNamespaces} from "react-i18next";
212
213class Nav extends Component {
214
215 render() {
216
217 const {t} = this.props;
218
219 return (
220 <nav>
221 <Link href="/about">{ t("nav.about") }</Link>
222 { t("nav.welcome", {name: "Dave"}) }
223 </nav>
224 );
225
226 }
227}
228
229export default withNamespaces()(Nav);
230```
231
232When a component is wrapped with `withNamespaces`, it will have access to a function named `t` inside it's props. This function is what handles fetching the appropriate translation, and also allows us to scrape an entire project to locate every string that needs translation. When you are ready to start populating translations, simply run `npm run locales`.
233
234Canon will search your entire codebase for any component using the `t( )` function. Translations are stored in JSON files in a `locales/` folder in the root directory. In this example, running the script would produce the following file structure:
235
236```
237locales/
238├── en/
239│ ├── [project-name]_old.json
240│ └── [project-name].json
241└── es/
242 ├── [project-name]_old.json
243 └── [project-name].json
244```
245
246Translations that are in use are stored in a JSON file, while translations that were previously in use (from the last time the script was run) but are no longer in use are stored in the file suffixed `_old`. While running the script, any existing translations will be kept as is, so there is no need to worry about overwriting previous translations.
247
248Since this is our first time running the scraper, both language's translation files will look like this:
249
250```json
251{
252 "nav": {
253 "about": "",
254 "welcome": ""
255 }
256}
257```
258
259If you look at your site in this state, now strings will be displayed in the components because their translation values are empty! A filled in translation file would look like this:
260
261```json
262{
263 "nav": {
264 "about": "About",
265 "welcome": "Welcome back {{name}}!"
266 }
267}
268```
269
270Notice the second string contains a variable surrounded by two sets of curly brackets. This is the notation for passing variable to translated strings, and is crucial in creating mad-libs style text.
271
272Additionally, to set the default language used in the site on first visit, set the following environment variable (default is `"en"`):
273
274```sh
275export CANON_LANGUAGE_DEFAULT="es"
276```
277
278### Language Detection
279
280A user's language can be determined in multiple ways. Here is the order of the cascading detection. Once a valid language (one that contains translations in a JSON file) has been detected, the process exits:
281
2821. sub-domain (ie. `https://pt.codelife.com`)
2832. `lang` query argument (ie. `https://www.codelife.com?lang=pt`)
2843. `language` query argument (ie. `https://www.codelife.com?language=pt`)
2854. `locale` query argument (ie. `https://www.codelife.com?locale=pt`)
2865. `lng` query argument (ie. `https://www.codelife.com?lng=pt`)
287
288### Changing Languages
289
290In order to change the language of the current page, you must trigger a full browser reload with the new URL. This can be done one of two ways. By using an anchor tag with the `data-refresh` attribute:
291
292```jsx
293<a data-refresh="true" href="/?locale=es">Spanish</a>
294```
295
296Or in a JavaScript event handler:
297
298```jsx
299window.location = "/?locale=es";
300```
301
302---
303
304
305## User Management
306
307By setting the following environment variables:
308
309```sh
310export CANON_DB_NAME="XXX"
311export CANON_DB_USER="XXX"
312export CANON_DB_HOST="XXX"
313export CANON_DB_PW="XXX"
314export CANON_LOGINS=true
315```
316
317Canon will automatically instantiate a "users" table in the specified database to enable full user management. At this point, all that is needed in your application is to use the Login and Signup components exported by Canon:
318
319```jsx
320import {Login, SignUp} from "@datawheel/canon-core";
321```
322
323These two components can either be used directly with a Route, or as children of other components. They are simple forms that handle all of the authentication and errors. If you would like to change the page the user is redirected to after logging in, you can override the default "redirect" prop:
324
325```jsx
326<Login redirect="/profile" />
327```
328
329If a `false` value is provided as a redirect, the redirect will be disabled and you must provide you own detection of the `state.auth.user` object in the redux store.
330
331*NOTE*: If also using [social logins](#social-logins), the `CANON_SOCIAL_REDIRECT` environment variable needs to be set in order to change those redirects.
332
333### Loading User Information
334
335Once login/signup forms have been set up, any component that needs access to the currently logged in user needs to dispatch an action to request the information. Ideally, this logic happens in `app/App.jsx` so that anyone can access the user from the redux store:
336
337```jsx
338import React, {Component} from "react";
339import {connect} from "react-redux";
340import {Canon, isAuthenticated} from "@datawheel/canon-core";
341
342class App extends Component {
343
344 componentWillMount() {
345 this.props.isAuthenticated();
346 }
347
348 render() {
349
350 // use this auth object (auth.user) to selectively show/hide components
351 // based on whether user is logged in or not
352 const auth = this.props.auth;
353 console.log(auth);
354
355 return (
356 <Canon>
357 { auth.user ? `Welcome back ${auth.uesr.username}!` : "Who are you!?" }
358 { auth.loading ? "Loading..." : this.props.children }
359 </Canon>
360 );
361
362 }
363
364}
365
366const mapStateToProps = state => ({
367 auth: state.auth
368});
369
370const mapDispatchToProps = dispatch => ({
371 isAuthenticated: () => {
372 dispatch(isAuthenticated());
373 }
374});
375
376export default connect(mapStateToProps, mapDispatchToProps)(App);
377```
378
379### Privacy Policy and Terms of Service
380
381In order to force new users to agree to a Privacy Policy and/or Terms of Service when signing up for a new account, you must specify the valid routes as environment variables. If one or both of these routes are set, then a check box will appear in the `<SignUp>` component with the corresponding page links.
382
383```sh
384export CANON_LEGAL_PRIVACY="/privacy"
385export CANON_LEGAL_TERMS="/terms"
386```
387
388### Password Reset
389
390If a user forgets their password, it is common practice to allow sending their e-mail on file a link to reset it. Canon has built-in [Mailgun](https://www.mailgun.com) support, so once you have set up an account for your project through their website, you can enable this ability with the following environment variables (taken from the [Mailgun](https://www.mailgun.com) developer interface):
391
392```sh
393export CANON_MAILGUN_API="key-################################"
394export CANON_MAILGUN_DOMAIN="###.###.###"
395export CANON_MAILGUN_EMAIL="###@###.###"
396```
397
398With those variables set, if a user is trying to log in and types an incorrect password, the alert message will contain a link to reset their password. They will receive an e-mail containing a link that directs them to a page at the route `/reset`. This route needs to be hooked up as part of the **app/routes.jsx** file, and needs to contain the `<Reset />` component exported by Canon. For example:
399
400```jsx
401import React from "react";
402import {Route} from "react-router";
403import {Reset} from "@datawheel/canon-core";
404
405const App = () => "Hello World";
406
407export default () => <Route path="/" component={App}>
408 <Route path="reset" component={Reset} />
409</Route>;
410```
411
412If you would like to change the default path of the reset link, use the following environment variable:
413
414```sh
415export CANON_RESET_LINK="/my-reset-route"
416```
417
418The `<Reset />` component relies on detecting a unique token in the URL (which is sent in the e-mail to the user). If you would like to embed the component into an existing page, you must pass the Router object to the component on render:
419
420```jsx
421<Reset router={ this.props.router } />
422```
423
424By default, users are redirected to `/login` after a successful password reset. This can also be changed with a prop:
425
426```jsx
427<Reset redirect="/en/login" router={ this.props.router } />
428```
429
430When sending e-mails, datahweel-canon will use the "name" field of your **package.json** file as the site name in e-mail correspondence (ex. "Sincerely, the [name] team"). If you'd like to use a more human-readable site name, it can be set with the following environment variable:
431
432```sh
433export CANON_MAILGUN_NAME="Datawheel Canon"
434```
435
436The default contents of the e-mail to be sent is stored [here](https://github.com/Datawheel/canon/blob/master/src/auth/emails/resetPassword.html), and can be overridden using any local HTML file using the following environment variable:
437
438```sh
439export CANON_RESET_HTML="path/to/file.html"
440```
441
442The path to this file is relative to the current working directory (`process.cwd()`), and the text inside of the file is run through the i18n parser like all of the front-end client facing components. The associated translation tags can be located under the `mailgun` key inside of the `Reset` key.
443
444### E-mail Verification
445
446If you would like your site to require e-mail verification, you can utilize [Mailgun](https://www.mailgun.com) in a way very similar to the [Password Reset](#password-reset) workflow. Set the appropriate [Mailgun](https://www.mailgun.com) environment variables:
447
448```sh
449export CANON_MAILGUN_API="key-################################"
450export CANON_MAILGUN_DOMAIN="###.###.###"
451export CANON_MAILGUN_EMAIL="###@###.###"
452```
453
454And then hook up an `/activate` route with the `<Activate />` component:
455
456```jsx
457import React from "react";
458import {Route} from "react-router";
459import {Activate} from "@datawheel/canon-core";
460
461const App = () => "Hello World";
462
463export default () => <Route path="/" component={App}>
464 <Route path="activate" component={Activate} />
465</Route>;
466```
467
468If you would like to change the default path of the activation link, use the following environment variable:
469
470```sh
471export CANON_ACTIVATION_LINK="/my-activation-route"
472```
473
474This component needs to be viewed while logged in, and contains a button to resend a verification e-mail with a new token. Similar to the `<Reset />` component, if you would like to use the `<Activate />` component inside of a pre-existing route (such as an account profile page), you must pass the Router location to the component:
475
476```jsx
477<Activate location={ this.props.location } />
478```
479
480Additionally, the component has an optional property to allow it to be hidden on a page. The verification will still register, but the component itself will render `null`:
481
482```jsx
483<Activate hidden={ true } location={ this.props.location } />
484```
485
486By default, activation e-mails are only sent when clicking the button in the `<Activate />` component. If you would like to send a verification e-mail when a user first signs up, enable the following environment variable:
487
488```sh
489export CANON_SIGNUP_ACTIVATION=true
490```
491
492When sending e-mails, datahweel-canon will use the "name" field of your **package.json** file as the site name in e-mail correspondence (ex. "Sincerely, the [name] team"). If you'd like to use a more human-readable site name, it can be set with the following environment variable:
493
494```sh
495export CANON_MAILGUN_NAME="Datawheel Canon"
496```
497
498The default contents of the e-mail to be sent is stored [here](https://github.com/Datawheel/canon/blob/master/src/auth/emails/activation.html), and can be overridden using any local HTML file using the following environment variable:
499
500```sh
501export CANON_ACTIVATION_HTML="path/to/file.html"
502```
503
504The path to this file is relative to the current working directory (`process.cwd()`), and the text inside of the file is run through the i18n parser like all of the front-end client facing components. The associated translation tags can be located under the `mailgun` key inside of the `Activation` key.
505
506### Roles
507
508Every new user of a Canon site has a default "role" value of `0`. This value is accessible via the user object in the "auth" redux store object. The default roles are as follows:
509
510* `0` User
511* `1` Contributor
512* `2` Admin
513
514Canon exports a `<UserAdmin />` component that allows for changing these roles. It is a simple table that displays all users and their current role assignments.
515
516### Social Logins
517
518Once the respective social network application has been set up in their developer interface, Canon looks for a corresponding API and SECRET environment variables to enable that login.
519
520*NOTE*: If deploying using Supervisor, environment variables cannot be wrapped in quotation marks.
521
522If you would like to change the page the user is redirected to after logging in using a social network, an environment variable is needed:
523
524```sh
525export CANON_SOCIAL_REDIRECT="/profile"
526```
527
528#### Facebook
5291. [https://developers.facebook.com](https://developers.facebook.com)
5302. Once logged in, hover over "My Apps" in the top right of the page and click "Add a New App"
5313. Set up "Facebook Login" as the product.
5324. Choose "Web" as the Platform.
5335. Skip the Quickstart guide and go directly to "Settings" in the sidebar. Your settings should look like the following image, with at the very least `http://localhost:3300/auth/facebook/callback` in the Valid OAuth redirect URIs. Once there is a production URL, you will need to add that callback URL here along with localhost. ![](https://github.com/datawheel/canon/raw/master/docs/facebook-oauth.png)
5346. Go to "Settings" > "Advanced" and turn on "Allow API Access to App Settings" (at the time of writing, it was the last toggle in the "Security" panel)
5357. Go to "Settings" > "Basic" and copy the App ID and App Secret to your environment as the following variables:
536```sh
537export CANON_FACEBOOK_API="###############"
538export CANON_FACEBOOK_SECRET="##############################"
539```
540
541#### Github
5421. [https://github.com/settings/applications/new](https://github.com/settings/applications/new)
5432. Fill out the form and set "Authorization callback URL" to `https://localhost/auth/github/callback`
5443. Click register application
5454. From the next screen copy the Client ID and Client Secret values to:
546```
547export CANON_GITHUB_API="###############"
548export CANON_GITHUB_SECRET="##############################"
549```
550
551#### Google
5521. [https://console.developers.google.com/](https://console.developers.google.com/)
5532. Once logged in, enable the "Google+ API"
5543. Go to the "Credentials" tab inside the "Google+ API" settings view and click "Create Credentials" and create OAuth client credentials
5554. Click the name of the credentials you created in the previous step
5565. For "Authorized JavaScript origins" add `https://localhost`
5576. For "Authorized Redirect URIs" add `https://localhost/auth/google/callback`
5587. Set the Client ID (CANON_GOOGLE_API) and Client Secret (CANON_GOOGLE_SECRET) values in your environment:
559```sh
560export CANON_GOOGLE_API="###############"
561export CANON_GOOGLE_SECRET="##############################"
562```
563
564#### Instagram
5651. [https://www.instagram.com/developer/](https://www.instagram.com/developer/)
5662. Once logged in, click the "Manage Clients" button in the top navigation, then click the green "Register a New Client" button.
5673. Fill out the meta information about your project, but specifically set the "Valid redirect URIs" to `http://localhost:3300/auth/instagram/callback`. Once there is a production URL, you will need to add that callback URL here along with localhost.
5684. Click the green "Register" button when done.
5695. You should be returned to the page listing all of your projects. Click "Manage" on the current project and copy the Client ID and Client Secret to your environment as the following variables:
570```sh
571export CANON_INSTAGRAM_API="###############"
572export CANON_INSTAGRAM_SECRET="##############################"
573```
574
575#### LinkedIn
5761. [https://www.linkedin.com/developer/apps/new](https://www.linkedin.com/developer/apps/new)
5772. Fill out the form (LinkedIn requires that you add a square image of at least 80x80 px)
5783. Click "Submit"
5794. Under the OAuth 2.0 section for "Authorized Redirect URLs" enter `https://localhost/auth/linkedin/callback`
5805. Click "Add" then click "Update"
5816. From the same application settings screen, copy the Client ID and Client Secret values to:
582```
583export CANON_LINKEDIN_API="###############"
584export CANON_LINKEDIN_SECRET="##############################"
585```
586
587#### Twitter
5881. [https://apps.twitter.com](https://apps.twitter.com)
5892. Once logged in, click the "Create New App" button on the top right of the page.
5903. Fill out the meta information about your project, but specifically set the "Callback URL" to `http://localhost:3300/auth/twitter/callback`.
5914. Go to the "Key and Access Tokens" tab and copy the Consumer Key (API Key) and Consumer Secret (API Secret) to your environment as the following variables:
592```sh
593export CANON_TWITTER_API="###############"
594export CANON_TWITTER_SECRET="##############################"
595```
5965. Click the "Permissions" tab then at the bottom under "Additional Permissions" check the box that reads "Request email addresses from users" (if you would like to request e-mail addresses from users).
597---
598
599## Custom API Routes
600
601If you app requires custom API routes, Canon will import any files located in a `api/` directory and attach them to the current Express instance. For example, a file located at `api/simpleRoute.js`:
602
603```js
604module.exports = function(app) {
605
606 app.get("/api/simple", (req, res) => {
607
608 res.json({simple: true}).end();
609
610 });
611
612};
613```
614
615*NOTE*: Custom API routes are written using Node module syntax, not ES6/JSX.
616
617If you'd like to interact with the database in a route, the Express app contains the Sequelize instance as part of it's settings:
618
619```js
620module.exports = function(app) {
621
622 const {db} = app.settings;
623
624 app.get("/api/user", (req, res) => {
625
626 db.users.findAll({where: req.query}).then(u => res.json(u).end());
627
628 });
629
630};
631```
632
633Additionally, if you would like certain routes to only be reachable if a user is logged in, you can use this simple middleware to reject users that are not logged in:
634
635```js
636const authRoute = (req, res, next) => {
637 if (req.isAuthenticated()) return next();
638 return res.status(401).send("you are not logged in...");
639};
640
641module.exports = function(app) {
642
643 app.get("/api/authenticated", authRoute, (req, res) => {
644
645 res.status(202).send("you are logged in!").end();
646
647 });
648
649};
650```
651
652---
653
654## Custom Database Models
655
656If you have custom database models that you would like to interact with in API routes, Canon will import any file in a `db/` folder and set up all the associations Sequelize requires. For example, a `db/testTable.js` would look like this:
657
658```js
659module.exports = function(sequelize, db) {
660
661 return sequelize.define("testTable",
662 {
663 id: {
664 type: db.INTEGER,
665 primaryKey: true
666 },
667 title: db.STRING,
668 favorite: {
669 type: db.BOOLEAN,
670 allowNull: false,
671 defaultValue: false
672 }
673 }
674 );
675
676};
677```
678
679*NOTE*: Custom database models are written using Node module syntax, not ES6/JSX.
680
681---
682
683## Server-Side Caching
684
685Some projects benefit by creating a server-side data cache to be used in API routes (for example, metadata about cube dimensions). Canon imports all files present in the top level `cache/` directory, and stores their return contents in `app.settings.cache` based on their filename. For example, to store the results of an API request in the cache, you could create the following file at `cache/majors.js`:
686
687```js
688const axios = require("axios");
689
690module.exports = function() {
691
692 return axios.get("https://api.datausa.io/attrs/cip/")
693 .then(d => d.data);
694
695};
696```
697
698The results of this promise can then be used in an API route:
699
700```js
701module.exports = function(app) {
702
703 const {cache} = app.settings;
704
705 app.get("/api/cache/majors", (req, res) => {
706
707 res.json(cache.majors).end();
708
709 });
710
711};
712```
713
714---
715
716## Opbeat Error Tracking
717
718If you would like to enable error tracking using Opbeat, add these 3 environment variables after initializing the app in the Opbeat online interface:
719
720```sh
721export CANON_OPBEAT_APP=your-opbeat-app-id
722export CANON_OPBEAT_ORG=your-opbeat-organization-id
723export CANON_OPBEAT_TOKEN=your-opbeat-secret-token
724```
725
726*NOTE*: Opbeat runs as express middleware, and will only track in production environments.
727
728---
729
730## Additional Environment Variables
731
732Interacting with the internals of canon is done by specifying environment variables. The recommended way to set environment variables is to use `direnv` (installed with `brew install direnv`), which will detect any file named `.envrc` located in a project folder. This file should not be pushed to the repository, as it usually contains variables specific to the current environment (testing locally, running on a server etc).
733
734Here is an example `.envrc` file which turns off the default redux messages seen in the browser console and changes the default localization language:
735
736```sh
737export CANON_LOGREDUX=false
738export CANON_LANGUAGE_DEFAULT="es"
739```
740
741|variable|description|default|
742|---|---|---|
743|`CANON_API`|Used as a prefix with the fetchData action and the attribute types returned from the `ATTRS` url.|`undefined`|
744|`CANON_BASE_URL`|If hosting assets or running the server from a different location that the project folder, this variable can be used to define the base URL for all static assets. A `<base>` tag will be added to the start of the `<head>` tag.|`undefined`|
745|`CANON_GOOGLE_ANALYTICS`|The unique Google Analytics ID for the project (ex. `"UA-########-#"`).|`undefined`|
746|`CANON_FACEBOOK_PIXEL`|The unique Facebook Pixel ID for the project (ex. `"################"`).|`undefined`|
747|`CANON_GOOGLE_TAG_MANAGER`|The unique Google Tag Manager ID for the project (ex. `"GTM-#######"`).|`undefined`|
748|`CANON_HELMET_FRAMEGUARD`|Pass-through option for the "frameguard" property of the [helmet](https://github.com/helmetjs/helmet#how-it-works) initialization.|`false`|
749|`CANON_HOTJAR`|The unique Hotjar ID for the project (ex. `"#######"`).|`undefined`|
750|`CANON_LOGREDUX`|Whether or not to display the (rather verbose) Redux store events in the browser console.|`true`|
751|`CANON_LOGLOCALE`|Whether or not to display the (rather verbose) i18n locale events in the browser console.|`false`|
752|`CANON_PORT`|The port to use for the server.|`3300`|
753|`CANON_SESSION_SECRET`|A unique secret key to use for cookies.|The "name" field from package.json|
754|`CANON_SESSION_TIMEOUT`|The timeout, in milliseconds, for user authentication cookies.|`60 * 60 * 1000` (one hour)|
755|`CANON_STATIC_FOLDER`|Changes the default folder name for static assets.|`"static"`|
756|`NODE_ENV`|The current environment. Setting to `production` will result in the removal of browser development tools and return smaller package sizes.|`development`|
757
758Additional environment variables can also be set (and may be required) for canon plugins:
759
760* [cms](https://github.com/Datawheel/canon/tree/master/packages/cms#environment-variables)
761* [logiclayer](https://github.com/Datawheel/canon/tree/master/packages/logiclayer#canon-logic-layer)
762
763## Custom Environment Variables
764
765In addition to the predefined environment variabels, you can also pass any variable to the front-end using the `CANON_CONST_*` wildcard naming convention. Any environment variable that begins with `CANON_CONST_` will be passed through to the redux store to be available in the front-end. For example, given the following environment variable:
766
767```sh
768export CANON_CONST_API2=https://api.datausa.io/
769```
770
771This variable can now be referenced as `API2` in a front-end React component:
772
773```jsx
774import React, {Component} from "react";
775import {connect} from "react-redux";
776
777class Viz extends Component {
778
779 render() {
780
781 const {API2} = this.props;
782
783 }
784
785}
786
787const mapStateToProps = state => ({
788 API2: state.env.API2
789});
790
791export default connect(mapStateToProps)(Viz);
792```