├── .env.example ├── .github ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── NEWS.md ├── README.md ├── ROADMAP.md ├── TUTORIAL.md ├── TUTORIAL2.md ├── archived ├── TUTORIAL.md └── TUTORIAL2.md ├── assets └── images │ ├── require-auth-directive.excalidraw │ ├── require-auth-directive.png │ ├── welcome-directive.excalidraw │ └── welcome-directive.png ├── code ├── html │ ├── 404.html │ ├── _community_nav.html │ ├── _nav.html │ ├── _search.html │ ├── _top_level_nav.html │ ├── cookbook │ │ └── .keep │ ├── docs │ │ └── .keep │ ├── index.html │ ├── layouts │ │ └── application.html │ ├── logos.html │ ├── reference │ │ └── .keep │ ├── roadmap.html │ ├── security.html │ ├── stickers-thanks.html │ ├── stickers.html │ ├── tutorial │ │ └── .keep │ ├── tutorial2 │ │ └── .keep │ └── videos │ │ └── .keep ├── javascripts │ ├── application.js │ └── controllers │ │ ├── application_controller.js │ │ ├── aside_controller.js │ │ ├── nav_controller.js │ │ └── search_controller.js └── stylesheets │ ├── application.pcss │ ├── markdown.pcss │ └── tailwind.pcss ├── cookbook ├── Background_Worker.md ├── Custom_Function.md ├── File_Upload.md ├── GoTrue_Auth.md ├── Mocking_GraphQL_Storybook.md ├── No_API.md ├── Pagination.md ├── Role-based_Access_Control.md ├── Self-hosting_Redwood.md ├── Sending_Emails.md ├── Supabase_Auth.md ├── Third_Party_API.md └── windows_setup.md ├── docs ├── a11y.md ├── appConfiguration.md ├── assetsAndFiles.md ├── authentication.md ├── builds.md ├── cells.md ├── cliCommands.md ├── connectionPooling.md ├── contributing-walkthrough.md ├── contributing.md ├── cors.md ├── customIndex.md ├── dataMigrations.md ├── deploy.md ├── directives.md ├── environmentVariables.md ├── form.md ├── graphql.md ├── localPostgresSetup.md ├── logger.md ├── mockGraphQLRequests.md ├── prerender.md ├── projectConfiguration.md ├── quick-start.md ├── redwoodRecord.md ├── router.md ├── schemaRelations.md ├── security.md ├── seo.md ├── serverlessFunctions.md ├── services.md ├── storybook.md ├── testing.md ├── toastNotifications.md ├── typescript.md ├── webhooks.md └── webpackConfiguration.md ├── functions ├── .keep └── stickers.js ├── lib ├── build.js ├── docutron.js ├── middleware │ ├── use-extension.js │ ├── use-headers.js │ └── use-redirects.js ├── news.js ├── roadmap.js ├── search.js └── templates │ ├── nav_item.html.template │ ├── news.html.template │ ├── news_article.html.template │ └── page.html.template ├── netlify.toml ├── package.json ├── postcss.config.js ├── prettier.config.js ├── publish ├── downloads │ ├── redwoodjs-diecut_mark.zip │ ├── redwoodjs-logo.zip │ └── redwoodjs-mark.zip ├── favicon.png └── images │ ├── .keep │ ├── diecut.svg │ ├── logo.svg │ ├── mark-logo-cover.png │ ├── mark-logo-transparent.png │ ├── opengraph-256.png │ ├── stickers.png │ ├── structure.png │ └── type.svg ├── tailwind.config.js ├── videos ├── authentication.md ├── router.md └── tutorial.md ├── webpack.config.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env and replace with real API keys 2 | 3 | ALGOLIA_API_KEY= 4 | ALGOLIA_SEARCH_KEY= 5 | ALGOLIA_APP_ID= 6 | ALGOLIA_INDEX_NAME=docs-dev 7 | GITHUB_AUTH= 8 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue template 3 | about: Docs Migration 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 🚨 HEADS UP 10 | 11 | In preparation for the 1.0.0 release, the Redwood Docs have been integrated with the main Redwood Framework redwoodjs/redwood repo. We are in the final stages of the migration. Once complete, this repo will be archived. 12 | 13 | > Please do not open new Issues here. Instead, open them at https://github.com/redwoodjs/redwood/issues 14 | 15 | The new doc site uses Docusaurus. The Docs are located here: 16 | - https://github.com/redwoodjs/redwood/tree/main/docs 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 🚨 HEADS UP 2 | 3 | In preparation for the 1.0.0 release, the Redwood Docs have been integrated with the main Redwood Framework redwoodjs/redwood repo. We are in the final stages of the migration. Once complete, this repo will be archived. 4 | 5 | > Please do not open new PRs here. Instead, open them at https://github.com/redwoodjs/redwood/pulls 6 | 7 | The new doc site uses Docusaurus. The Docs are located here: 8 | - https://github.com/redwoodjs/redwood/tree/main/docs 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | node_modules 4 | publish/* 5 | !publish/downloads 6 | !publish/images 7 | !publish/favicon.* 8 | !publish/security.txt 9 | yarn-error.log 10 | code/html/cookbook/*.html 11 | !code/html/cookbook/index.html 12 | code/html/docs/*.html 13 | !code/html/docs/index.html 14 | code/html/tutorial/*.html 15 | !code/html/tutorial/index.html 16 | code/html/tutorial2/*.html 17 | !code/html/tutorial2/index.html 18 | code/html/videos/*.html 19 | !code/html/videos/index.html 20 | code/html/_*nav.html 21 | !code/html/_nav.html 22 | !code/html/_community_nav.html 23 | !code/html/_top_level_nav.html 24 | code/html/news.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redwoodjs.com 2 | 3 | This is the repo for https://redwoodjs.com 4 | 5 | The content for the tutorials are managed along with localization over at [learn.redwoodjs.com](https://github.com/redwoodjs/learn.redwoodjs.com). 6 | 7 | Other documentation is pulled from various READMEs in the main [redwoodjs/redwood](https://github.com/redwoodjs/redwood) repo (see `lib/build.js`, the `SECTIONS` constant). 8 | 9 | ## Local Development 10 | 11 | This codebase is built with https://cameronjs.com and relies on plain HTML pages and Javascript with a couple helpers built in to abstract things like partials and layouts. We use https://stimulusjs.org for the few sprinkles of interactivity throughout the site. 12 | 13 | First, make sure that you are running Node 14+. If you're not sure of how to manage your node versions, see [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows). 14 | 15 | Then build the tutorial and doc pages (after you've installed all dependencies with `yarn install`): 16 | 17 | yarn build 18 | 19 | And to develop locally (you'll need to run `yarn build` once first in order to generate some of the navigation menus): 20 | 21 | yarn dev 22 | 23 | If you are already running a `yarn dev` process, when you `yarn build`, you may need to stop and start `yarn dev` to pick up the new pages properly. 24 | 25 | ## Contributing 26 | 27 | Open a PR against the repo on GitHub. That will build and launch a copy of the site that you can get from the `netlify/redwoodjs/deploy-preview` check (click "Details" to open it): 28 | 29 | ![image](https://user-images.githubusercontent.com/300/76569613-c4421000-6470-11ea-8223-eb98504e6994.png) 30 | 31 | Double check that your changes look good! 32 | 33 | ## Contributors 34 | 35 | Redwood is amazing thanks to a wonderful [community of contributors](https://github.com/redwoodjs/redwood/blob/main/README.md#contributors). 36 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | ## Tutorial Contributions (and Translators) are Welcome! 2 | To support translations, the Redwood Tutorials have been moved to the `learn.redwoodjs.com` Repo: 3 | - **Tutorial Part 1:** [learn.redwoodjs.com/docs/tutorial/](https://github.com/redwoodjs/learn.redwoodjs.com/tree/main/docs/tutorial) 4 | - **Tutorial Part 2:** [learn.redwoodjs.com/docs/tutorial2/](https://github.com/redwoodjs/learn.redwoodjs.com/tree/main/docs/tutorial2) 5 | 6 | Instructions for the translation workflow and contributing are here: 7 | - [learn.redwoodjs.com/README.md](https://github.com/redwoodjs/learn.redwoodjs.com/blob/main/README.md) 8 | 9 | 10 | ## Translation Project Lead 11 | For more information and to learn how you can help contribute and translate, please contact Core Team member [@clairefro](https://github.com/clairefro) 12 | -------------------------------------------------------------------------------- /TUTORIAL2.md: -------------------------------------------------------------------------------- 1 | ## Tutorial Contributions (and Translators) are Welcome! 2 | To support translations, the Redwood Tutorials have been moved to the `learn.redwoodjs.com` Repo: 3 | - **Tutorial Part 1:** [learn.redwoodjs.com/docs/tutorial/](https://github.com/redwoodjs/learn.redwoodjs.com/tree/main/docs/tutorial) 4 | - **Tutorial Part 2:** [learn.redwoodjs.com/docs/tutorial2/](https://github.com/redwoodjs/learn.redwoodjs.com/tree/main/docs/tutorial2) 5 | 6 | Instructions for the translation workflow and contributing are here: 7 | - [learn.redwoodjs.com/README.md](https://github.com/redwoodjs/learn.redwoodjs.com/blob/main/README.md) 8 | 9 | 10 | ## Translation Project Lead 11 | For more information and to learn how you can help contribute and translate, please contact Core Team member [@clairefro](https://github.com/clairefro) 12 | -------------------------------------------------------------------------------- /assets/images/require-auth-directive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/assets/images/require-auth-directive.png -------------------------------------------------------------------------------- /assets/images/welcome-directive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/assets/images/welcome-directive.png -------------------------------------------------------------------------------- /code/html/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RedwoodJS: Not Found 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Page Not Found

15 |

Are you lost in the woods?

16 |

Go Home

17 |

Are you looking for the tutorials? They've moved here:

18 |

learn.redwoodjs.com

19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /code/html/_community_nav.html: -------------------------------------------------------------------------------- 1 |
  • 2 | Discourse Forum 3 |
  • 4 |
  • 5 | Discord Chat 6 |
  • 7 | -------------------------------------------------------------------------------- /code/html/_nav.html: -------------------------------------------------------------------------------- 1 | 138 | -------------------------------------------------------------------------------- /code/html/_search.html: -------------------------------------------------------------------------------- 1 |
    2 | 11 | 16 |
    17 | -------------------------------------------------------------------------------- /code/html/_top_level_nav.html: -------------------------------------------------------------------------------- 1 | 81 | -------------------------------------------------------------------------------- /code/html/cookbook/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/code/html/cookbook/.keep -------------------------------------------------------------------------------- /code/html/docs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/code/html/docs/.keep -------------------------------------------------------------------------------- /code/html/logos.html: -------------------------------------------------------------------------------- 1 | @@layout("application", { "title": "RedwoodJS | The App Framework for Startups", "version": "v1.0.0-rc.6" }) 2 | 3 |
    4 |
    5 |

    Redwood Logos and Usage

    6 |

    Here are some official assets for linking to the project.

    7 | 8 |
    9 |
    10 |
    11 | RedwoodJS logo 12 |
    13 |

    14 | Download Logo 15 |

    16 |
    17 |
    18 |
    19 | RedwoodJS diecut mark 20 |
    21 |

    22 | Download Diecut Mark 23 |

    24 |
    25 |
    26 |
    27 | RedwoodJS mark 28 |
    29 |

    30 | Download Mark 31 |

    32 |
    33 |
    34 | 35 |
    36 |
    37 |

    Okay

    38 |
      39 |
    • Use the Cone or RedwoodJS logo to link to RedwoodJS
    • 40 |
    • Use the Cone or RedwoodJS logo in a blog post or news article about Redwood
    • 41 |
    42 |
    43 |
    44 |

    Not Okay

    45 |
      46 |
    • Use the Cone or RedwoodJS logo for your application’s icon
    • 47 |
    • Create a modified version of the Cone or RedwoodJS logo
    • 48 |
    • Integrate the Cone or RedwoodJS logo into your logo
    • 49 |
    • Change the colors, dimensions or add your own text/images
    • 50 |
    51 |
    52 |
    53 | 54 |
    55 |
    56 |

    Contact Us

    57 |
      58 |
    • If you want to use artwork not included here
    • 59 |
    • If you want to use these images in a video/mainstream media
    • 60 |
    61 |
    62 |
    63 |

    Naming Projects and Products

    64 |

    65 | Please do not claim endorsement of your product or service by RedwoodJS without permission. 66 | 67 |

    68 |
    69 |
    70 |
    71 | -------------------------------------------------------------------------------- /code/html/reference/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/code/html/reference/.keep -------------------------------------------------------------------------------- /code/html/security.html: -------------------------------------------------------------------------------- 1 | @@layout("application", { "title": "RedwoodJS | The App Framework for Startups", "version": "v1.0.0-rc.6" }) 2 | 3 |
    4 |
    5 |

    Security Policy

    6 |
    7 |
    8 |

    Last updated 21 June 2021

    9 |

    To view the RedwoodJS Security policy, click here

    10 |
    11 |
    12 |

    Contact

    13 |

    If you discover a potential security issue, do let us know as soon as possible. We'll quickly work toward a resolution, so please provide us with a reasonable amount of time before disclosure to the public or a third-party.

    14 |
    15 |

    Contact us at security@redwoodjs.com

    16 |
    17 |

    Thank you for helping improve Redwood security!

    18 |
    19 | 20 |
    21 | -------------------------------------------------------------------------------- /code/html/stickers-thanks.html: -------------------------------------------------------------------------------- 1 | @@layout("application", { "title": "RedwoodJS - Get a sticker!", "version": "v1.0.0-rc.6" }) 2 | 3 | 24 | 25 |
    26 |
    27 |
    28 | Redwood stickers 29 |
    30 |

    We got your address!

    31 |
    32 |

    The day may come when we ask you for a favor...

    33 |

    And that day is today: star us on GitHub!

    34 | Star on GitHub 35 |
    36 | 37 |
    38 | 39 |
    40 | -------------------------------------------------------------------------------- /code/html/tutorial/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/code/html/tutorial/.keep -------------------------------------------------------------------------------- /code/html/tutorial2/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/code/html/tutorial2/.keep -------------------------------------------------------------------------------- /code/html/videos/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/code/html/videos/.keep -------------------------------------------------------------------------------- /code/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // See https://cameronjs.com/js for more info 2 | 3 | import { Application } from "stimulus"; 4 | import { definitionsFromContext } from "stimulus/webpack-helpers"; 5 | 6 | const application = Application.start(); 7 | const context = require.context("./controllers", true, /\.js$/); 8 | application.load(definitionsFromContext(context)); 9 | 10 | var Turbolinks = require('turbolinks') 11 | Turbolinks.start() 12 | -------------------------------------------------------------------------------- /code/javascripts/controllers/application_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'stimulus' 2 | import hljs from 'highlight.js' 3 | import ClipboardJS from 'clipboard' 4 | 5 | export default class extends Controller { 6 | static get targets() { 7 | return ['header','logo','search','stars','nav','innerNav','body','code','year','thanks', 'cone'] 8 | } 9 | 10 | connect() { 11 | // set the year in the footer 12 | this.yearTarget.textContent = new Date().getFullYear() 13 | 14 | // code highlighting 15 | this.codeTargets.forEach((target) => { 16 | hljs.highlightBlock(target) 17 | }) 18 | 19 | // show the header logo unless we're on the homepage 20 | if (!this.isHomePage) { 21 | this.logoTarget.classList.remove('lg:hidden') 22 | } 23 | 24 | // add copy buttons to code blocks 25 | this._enableCopy() 26 | 27 | // if there is a hash in the URL, open a collapsed sections that contains that target 28 | this._openCollapsedSectionForHash() 29 | 30 | // show the star count 31 | this._showStarCount() 32 | 33 | // show a rain of cones on the sticker thank you page 34 | if (this.hasThanksTarget) { 35 | this._spawnCones() 36 | } 37 | } 38 | 39 | focusSearch(event) { 40 | if (event.code === 'Slash' && !this.someInputHasFocus) { 41 | this.searchTarget.focus() 42 | event.preventDefault() 43 | } 44 | } 45 | 46 | toggleNav() { 47 | this.navTarget.classList.toggle('hidden') 48 | this.bodyTarget.classList.toggle('hidden') 49 | } 50 | 51 | closeNav() { 52 | this.navTarget.classList.add('hidden') 53 | this.bodyTarget.classList.remove('hidden') 54 | } 55 | 56 | saveScrollPosition() { 57 | window.navScrollPosition = this.innerNavTarget.scrollTop 58 | if (window.scrollY > this.headerTarget.offsetHeight) { 59 | window.windowScrollPosition = this.headerTarget.offsetHeight 60 | } else { 61 | window.windowScrollPosition = window.scrollY 62 | } 63 | } 64 | 65 | restoreScrollPosition() { 66 | if (window.navScrollPosition !== 0 || window.windowScrollPosition !== 0) { 67 | this.innerNavTarget.scrollTop = window.navScrollPosition || 0 68 | window.scrollTo(null, window.windowScrollPosition || 0) 69 | } 70 | } 71 | 72 | _enableCopy() { 73 | const COPY_BUTTON_CSS = [ 74 | 'copy-button', 75 | 'absolute', 76 | 'right-0', 77 | 'bottom-0', 78 | 'm-2', 79 | 'text-xs', 80 | 'text-gray-500', 81 | 'hover:text-gray-400', 82 | 'bg-gray-800', 83 | 'hover:bg-gray-700', 84 | 'px-1', 85 | 'rounded', 86 | 'focus:outline-none', 87 | 'transition', 88 | 'duration-100', 89 | 'focus:outline-none', 90 | 'focus:shadow-outline', 91 | ] 92 | const codeBlocks = document.getElementsByTagName('code') 93 | for (let block of codeBlocks) { 94 | const parent = block.parentElement 95 | 96 | // is this is a copyable code block
    ...
    97 | if (parent.tagName === 'PRE') { 98 | parent.classList.add('relative') 99 | var button = document.createElement('button') 100 | button.classList.add(...COPY_BUTTON_CSS) 101 | button.textContent = 'Copy' 102 | block.parentElement.appendChild(button) 103 | 104 | new ClipboardJS('.copy-button', { 105 | text: (trigger) => { 106 | this._copiedMessage(trigger) 107 | return this._stripComments(trigger.previousElementSibling.innerText) 108 | }, 109 | }) 110 | } 111 | } 112 | } 113 | 114 | _copiedMessage(trigger) { 115 | trigger.focus() 116 | trigger.textContent = 'Copied' 117 | setTimeout(() => { 118 | trigger.textContent = 'Copy' 119 | }, 750) 120 | } 121 | 122 | // strips any leading comments out of a chunk of text 123 | _stripComments(content) { 124 | let lines = content.split('\n') 125 | 126 | if (lines[0].match(/^\/\/|\*/)) { 127 | lines.shift() 128 | // remove empty lines after comments 129 | while (lines[0].trim() === '') { 130 | lines.shift() 131 | } 132 | } 133 | 134 | return lines.join('\n') 135 | } 136 | 137 | _openCollapsedSectionForHash() { 138 | let hash = location.hash 139 | 140 | if (hash) { 141 | hash = hash.substring(1) 142 | 143 | const element = document.getElementById(hash) 144 | const parent = element.parentNode 145 | 146 | if (parent.tagName === 'DETAILS') { 147 | parent.open = true 148 | window.scrollTo(0, element.offsetTop) 149 | } 150 | } 151 | } 152 | 153 | _spawnCones() { 154 | let count = 0 155 | 156 | while (count < 20) { 157 | const fallTime = Math.random() * 2 + 1.5 158 | const rotateStart = Math.random() * 360 - 180 159 | const rotateEnd = Math.random() * 360 - 180 160 | const wait = Math.random() * 1 161 | const size = Math.random() * 64 + 24 162 | const cone = this.coneTarget.cloneNode(true) 163 | 164 | this.element.appendChild(cone) 165 | cone.style.left = `${Math.random() * this.element.offsetWidth}px` 166 | cone.style.width = `${size}px` 167 | cone.style.setProperty('--rotateStart',`${rotateStart}deg`) 168 | cone.style.setProperty('--rotateEnd',`${rotateEnd}deg`) 169 | 170 | setTimeout(() => { 171 | cone.classList.remove('hidden') 172 | cone.style.animation = `falling ${fallTime}s ease-in forwards` 173 | }, wait * 1000) 174 | 175 | count++ 176 | } 177 | } 178 | 179 | async _showStarCount() { 180 | const stars = await this._getStarCount() 181 | this.starsTarget.textContent = stars 182 | } 183 | 184 | async _getStarCount() { 185 | const response = await fetch('https://api.github.com/repos/redwoodjs/redwood') 186 | const body = await response.json() 187 | return body.stargazers_count 188 | } 189 | 190 | get isHomePage() { 191 | return location.pathname === '/' 192 | } 193 | 194 | get someInputHasFocus() { 195 | return ['INPUT', 'TEXTAREA'].indexOf(document.activeElement.tagName) !== -1 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /code/javascripts/controllers/aside_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'stimulus' 2 | 3 | let scroll 4 | 5 | export default class extends Controller { 6 | connect() { 7 | this.element.scrollTop = scroll 8 | } 9 | 10 | saveScroll() { 11 | scroll = this.element.scrollTop 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /code/javascripts/controllers/nav_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'stimulus' 2 | 3 | export default class extends Controller { 4 | static get targets() { 5 | return ['link'] 6 | } 7 | 8 | connect() { 9 | this._highlightNav() 10 | document.dispatchEvent(new Event('navigated')) 11 | } 12 | 13 | // opens/closes a nav section 14 | toggle(event) { 15 | event.preventDefault() 16 | event.currentTarget.nextSibling.nextSibling.classList.toggle('hidden') 17 | } 18 | 19 | // Highlight nav items if the URL matches the `href` on the link. 20 | // 21 | // If no links matched, look at the data-match attribute on the first link in 22 | // a list and if one of those matches, highlight it 23 | _highlightNav() { 24 | let linkFound = false 25 | 26 | this.linkTargets.forEach((link) => { 27 | if (this._linkDoesMatch(link)) { 28 | this._activateLink(link) 29 | linkFound = true 30 | } else { 31 | this._deactivateLink(link) 32 | } 33 | return !linkFound 34 | }) 35 | 36 | if (!linkFound) { 37 | this._fallbackLink() 38 | } 39 | } 40 | 41 | _linkDoesMatch(link) { 42 | return location.href.indexOf(link.href) !== -1 43 | } 44 | 45 | _fallbackLink() { 46 | this.linkTargets.every((link) => { 47 | if (link.dataset.match && location.href.indexOf(link.dataset.match) !== -1) { 48 | this._activateLink(link) 49 | return false 50 | } else { 51 | return true 52 | } 53 | }) 54 | } 55 | 56 | _activateLink(link) { 57 | link.classList.add(...this.activeClasses) 58 | if (this.removeClasses.length) { 59 | link.classList.remove(...this.removeClassesClasses) 60 | } 61 | // make sure whole parent list is visible 62 | link.closest('ul').classList.remove('hidden') 63 | } 64 | 65 | _deactivateLink(link) { 66 | link.classList.remove(...this.activeClasses) 67 | } 68 | 69 | get removeClasses() { 70 | return this.data.get('remove') ? this.data.get('remove').split(' ') : [] 71 | } 72 | 73 | get activeClasses() { 74 | return this.data.get('active').split(' ') 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /code/javascripts/controllers/search_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'stimulus' 2 | import template from 'lodash.template' 3 | import escape from 'lodash.escape' 4 | import clone from 'lodash.clone' 5 | import algoliasearch from 'algoliasearch' 6 | 7 | export default class extends Controller { 8 | static get targets() { 9 | return ['input', 'results'] 10 | } 11 | 12 | initialize() { 13 | // create a handler bound to `this` that we can add and remove 14 | this.documentClickHandler = () => { 15 | this.close() 16 | } 17 | 18 | this.client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_SEARCH_KEY) 19 | this.index = this.client.initIndex(process.env.ALGOLIA_INDEX_NAME) 20 | this.searchOptions = { 21 | hitsPerPage: 10, 22 | attributesToRetrieve: '*', 23 | attributesToSnippet: 'text:20,section:20', 24 | attributesToHighlight: null, 25 | snippetEllipsisText: '…', 26 | analytics: true, 27 | } 28 | 29 | this.searchResultTemplate = template(` 30 | 31 |
    32 |

    \${chapter}

    33 |
    34 |

    \${section}

    35 |

    \${text}

    38 |
    39 |
    40 |
    `) 41 | } 42 | 43 | // Run search on keystrokes 44 | search(event) { 45 | event.stopPropagation() 46 | 47 | if (event.currentTarget.value.trim() !== '') { 48 | if (event.key === 'Escape') { 49 | this.close() 50 | return 51 | } else { 52 | this.index.search(event.currentTarget.value, this.searchOptions).then((data) => { 53 | this._parseResults(data) 54 | }) 55 | } 56 | } else { 57 | this._clear() 58 | } 59 | } 60 | 61 | close() { 62 | this.resultsTarget.classList.add('hidden') 63 | document.removeEventListener('click', this.documentClickHandler) 64 | } 65 | 66 | _clear() { 67 | this.resultsTarget.innerHTML = '' 68 | this.close() 69 | } 70 | 71 | _formatSection(text) { 72 | // return escape(text.replace(/`/g, '')) 73 | 74 | let output = text.replace(/`/g, '') 75 | 76 | // no idea why, but sometimes opening and closing HTML tags in results from Algolia 77 | // are already escaped properly, and if we don't do this check here then they'll 78 | // get double-escaped and show < and > 79 | if (!output.match(/</)) { 80 | output = escape(output) 81 | } 82 | return output 83 | } 84 | 85 | _formatText(text) { 86 | return escape(text.replace(/`/g, '')) 87 | } 88 | 89 | _parseResults(data) { 90 | if (data.hits.length === 0) { 91 | return this._show( 92 | `

    No docs found for ${data.query}

    ` 93 | ) 94 | } 95 | 96 | const sections = [] 97 | data.hits.map((hit) => { 98 | if (sections.indexOf(hit.book) === -1) { 99 | sections.push(hit.book) 100 | } 101 | }) 102 | 103 | const items = {} 104 | data.hits.forEach((hit) => { 105 | let attributes = Object.assign(clone(hit), { 106 | text: this._formatText(hit.text), 107 | section: this._formatSection(hit.section), 108 | }) 109 | let html = this.searchResultTemplate(attributes) 110 | 111 | if (items[hit.book]) { 112 | items[hit.book].push(html) 113 | } else { 114 | items[hit.book] = [html] 115 | } 116 | }) 117 | 118 | let output = '' 119 | for (let item in items) { 120 | output += `

    ${item}

    ` 121 | output += items[item].join('') 122 | } 123 | 124 | this._show(output) 125 | } 126 | 127 | _show(html) { 128 | this.resultsTarget.classList.remove('hidden') 129 | this.resultsTarget.innerHTML = html 130 | document.addEventListener('click', this.documentClickHandler) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /code/stylesheets/application.pcss: -------------------------------------------------------------------------------- 1 | /* See https://cameronjs.org/css for more info */ 2 | 3 | @import "tailwind.pcss"; 4 | @import "markdown.pcss"; 5 | -------------------------------------------------------------------------------- /code/stylesheets/markdown.pcss: -------------------------------------------------------------------------------- 1 | /* I'm used on the built-in CameronJS welcome page code/html/index.html */ 2 | /* Feel free to delete me once you start changing that page. */ 3 | 4 | #code { 5 | @apply bg-red-200 text-gray-900 text-sm px-1 py-half rounded; 6 | font-family: Fira Code, Fira Mono, Menlo, Monoco, monospace; 7 | } 8 | 9 | #link { 10 | @apply text-red-700 11 | } 12 | 13 | .markdown { 14 | h1, 15 | h2 { 16 | @apply text-xl font-semibold; 17 | } 18 | 19 | h3 { 20 | @apply text-lg font-semibold; 21 | } 22 | 23 | h2, 24 | h3, 25 | h4, 26 | h5 { 27 | @apply mt-8 mb-4; 28 | } 29 | 30 | h4 { 31 | @apply font-semibold; 32 | } 33 | 34 | h5 { 35 | @apply font-semibold text-sm text-gray-700; 36 | } 37 | 38 | /* Move headings over because the anchor link will be hidden */ 39 | @screen lg { 40 | h1, h2, h3, h4, h5, h6 { 41 | margin-left: -1.1rem; 42 | } 43 | 44 | h4 { 45 | margin-left: -0.9rem; 46 | } 47 | } 48 | 49 | p { 50 | @apply my-4; 51 | } 52 | 53 | ul, 54 | ol { 55 | @apply ml-8; 56 | } 57 | 58 | ul { 59 | @apply list-disc; 60 | } 61 | 62 | ol { 63 | @apply list-decimal; 64 | } 65 | 66 | pre { 67 | @apply mt-4 bg-gray-900 p-4 pb-8 text-sm text-white overflow-x-scroll rounded-lg; 68 | } 69 | 70 | pre + ul { 71 | @apply mt-4; 72 | } 73 | 74 | .highlighted-line { 75 | @apply block -mx-4 px-4 bg-gray-800 block 76 | } 77 | 78 | @screen lg { 79 | pre { 80 | @apply pb-4; 81 | } 82 | } 83 | 84 | p code, 85 | li code, 86 | dl code { 87 | @apply bg-red-200 text-gray-900 text-sm px-1 py-half rounded; 88 | } 89 | 90 | li pre code { 91 | @apply bg-transparent text-white px-0 py-0; 92 | } 93 | 94 | blockquote { 95 | @apply text-sm border border-red-300 bg-red-100 py-1 px-6 my-8 rounded-lg; 96 | } 97 | 98 | blockquote ul:last-child { 99 | @apply mb-6; 100 | } 101 | 102 | img { 103 | @apply my-8; 104 | } 105 | 106 | p a, 107 | li a, 108 | blockquote a { 109 | @apply text-red-700; 110 | } 111 | 112 | p a:hover, 113 | li a:hover, 114 | blockquote a:hover { 115 | @apply underline; 116 | } 117 | 118 | .markdownIt-Anchor { 119 | visibility: visible; 120 | } 121 | h1:hover .markdownIt-Anchor, 122 | h2:hover .markdownIt-Anchor, 123 | h3:hover .markdownIt-Anchor, 124 | h4:hover .markdownIt-Anchor, 125 | h5:hover .markdownIt-Anchor, 126 | h6:hover .markdownIt-Anchor { 127 | visibility: visible; 128 | } 129 | 130 | /* Hide anchor links when the screen is big enough (and probably not a touch-only device) */ 131 | @screen lg { 132 | .markdownIt-Anchor { 133 | visibility: hidden; 134 | } 135 | } 136 | 137 | table { 138 | @apply w-full text-left bg-red-100 rounded-t-lg; 139 | } 140 | 141 | th { 142 | @apply p-2 bg-red-200; 143 | } 144 | 145 | th:first-child { 146 | @apply rounded-tl-lg whitespace-no-wrap; 147 | } 148 | 149 | th:last-child { 150 | @apply rounded-tr-lg; 151 | } 152 | 153 | td { 154 | @apply p-2; 155 | } 156 | 157 | td code { 158 | @apply bg-red-200 text-red-700 text-sm px-1 py-half rounded whitespace-no-wrap; 159 | } 160 | 161 | td a { 162 | @apply text-red-700; 163 | } 164 | 165 | td a:hover { 166 | @apply underline; 167 | } 168 | 169 | details { 170 | @apply mb-2; 171 | } 172 | 173 | summary { 174 | @apply outline-none cursor-pointer; 175 | } 176 | 177 | dl { 178 | @apply my-8; 179 | } 180 | 181 | dt { 182 | @apply mt-4; 183 | } 184 | 185 | dd { 186 | @apply text-sm text-gray-800 mt-1 ml-4; 187 | code { 188 | @apply text-xs; 189 | } 190 | } 191 | .code-dl { 192 | @apply mt-8; 193 | p { 194 | @apply mt-1 mb-8; 195 | } 196 | } 197 | } 198 | 199 | #status-0 { 200 | @apply bg-red-200 text-red-700 text-sm px-1 py-half rounded; 201 | } 202 | 203 | #status-1 { 204 | @apply bg-orange-200 text-orange-700 text-sm px-1 py-half rounded; 205 | } 206 | 207 | #status-2 { 208 | @apply bg-purple-200 text-purple-700 text-sm px-1 py-half rounded; 209 | } 210 | 211 | #status-3 { 212 | @apply bg-blue-200 text-blue-700 text-sm px-1 py-half rounded; 213 | } 214 | 215 | #status-4 { 216 | @apply bg-green-200 text-green-700 text-sm px-1 py-half rounded; 217 | } 218 | -------------------------------------------------------------------------------- /code/stylesheets/tailwind.pcss: -------------------------------------------------------------------------------- 1 | /* See https://cameronjs.com/tailwindcss for more about Tailwind CSS */ 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | 6 | /* 7 | Overriding styles or extracted components go here, see: 8 | 9 | https://tailwindcss.com/docs/adding-base-styles 10 | https://tailwindcss.com/docs/extracting-components 11 | */ 12 | 13 | @tailwind utilities; 14 | 15 | .top-24 { 16 | top: 6rem 17 | } 18 | 19 | .searchresult em { 20 | @apply font-semibold underline not-italic 21 | } 22 | 23 | .bottom-full { 24 | bottom: 100% 25 | } 26 | 27 | .top-full { 28 | top: 100% 29 | } 30 | 31 | code.javascript, code.bash { 32 | @apply text-xs rounded-lg p-4 33 | } 34 | 35 | .tooltip-text { 36 | @apply hidden absolute w-48 mt-2 text-white bg-red-700 text-xs rounded py-2 px-3 right-0 37 | } 38 | 39 | .tooltip:hover .tooltip-text { 40 | @apply block 41 | } 42 | 43 | .arrow-right { 44 | width: 0; 45 | height: 0; 46 | border-top: 0.55rem solid transparent; 47 | border-bottom: 0.55rem solid transparent; 48 | border-left: 0.55rem solid #E38163; 49 | } 50 | 51 | /* 52 | Completely custom CSS goes here, or add a new stylesheet and include it in application.pcss: 53 | 54 | @import "tailwind.pcss"; 55 | @import "custom.css"; 56 | 57 | See https://tailwindcss.com/docs/adding-new-utilities for creating new Tailwind utilities 58 | */ 59 | -------------------------------------------------------------------------------- /cookbook/Background_Worker.md: -------------------------------------------------------------------------------- 1 | # Creating a Background Worker with Exec and Faktory 2 | 3 | In this cookbook, we'll use Redwood's [exec CLI command](/docs/cli-commands#exec) to create a background worker using [Faktory](https://contribsys.com/faktory/). 4 | 5 | At a high level, Faktory is a language-agnostic, persistent background-job server. 6 | You can run it [with Docker](https://github.com/contribsys/faktory/wiki/Docker). 7 | 8 | We'll have to have a way of communicating with the server from our Redwood app. 9 | We'll use this [node library](https://github.com/jbielick/faktory_worker_node) to send jobs from our Redwood app to our Faktory server. 10 | 11 | ## Creating the Faktory Worker 12 | 13 | Let's create our faktory worker. 14 | First, generate the worker script: 15 | 16 | ``` 17 | yarn rw g script faktoryWorker 18 | ``` 19 | 20 | We'll start by registering a task called `postSignupTask` in our worker: 21 | 22 | ```javascript 23 | // scripts/faktoryWorker.js 24 | 25 | const { postSignupTask } from '$api/src/lib/tasks' 26 | import { logger } from '$api/src/lib/logger' 27 | 28 | import faktory from 'faktory-worker' 29 | 30 | faktory.register('postSignupTask', async (taskArgs) => { 31 | logger.info("running postSignupTask in background worker") 32 | 33 | await postSignupTask(taskArgs) 34 | }) 35 | 36 | export default async ({ _args }) => { 37 | const worker = await faktory 38 | .work({ 39 | url: process.env.FAKTORY_URL, 40 | }) 41 | .catch((error) => { 42 | logger.error(`worker failed to start: ${error}`) 43 | process.exit(1) 44 | }) 45 | 46 | worker.on('fail', ({ _job, error }) => { 47 | logger.error(`worker failed to start: ${error}`) 48 | }) 49 | } 50 | ``` 51 | 52 | This won't work yet as we haven't made `postSignupTask` in `api/src/lib/tasks.js` or set `FAKTORY_URL`. 53 | Set `FAKTORY_URL` in `.env` to where your server's running. 54 | 55 | In `postSignupTask`, we may want to perform operations that need to contact external services, such as sending an email. 56 | For this type of work, we typically don't want to hold up the request/response cycle and can perform it in the background: 57 | 58 | ```javascript 59 | // api/src/lib/tasks.js 60 | 61 | export const postSignupTask = async ({ userId, emailPayload }) => { 62 | // Send a welcome email to new user. 63 | // You'll have to have an integration with an email service for this to work. 64 | await sendEmailWithTemplate({ 65 | ...emailPayload, 66 | TemplateModel: { 67 | ...emailPayload.TemplateModel, 68 | }, 69 | }) 70 | } 71 | ``` 72 | 73 | Once we've created our task, we need to call it in the right place. 74 | For this task, it makes sense to call it right after the user has completed their signup. 75 | This is an example of a Service that'll most likely be called via a GraphQL Mutation. 76 | 77 | ```javascript 78 | // src/services/auth/auth.js 79 | 80 | const faktory = require('faktory-worker') 81 | 82 | export const signUp = async ({ input }) => { 83 | // Perform all the signup operations, such as creating an entry in the DB and auth provider 84 | // ... 85 | 86 | // The, send our task to the Faktory server 87 | const client = await faktory.connect() 88 | await client.job('postSignupTask', { ...taskArgs, }).push() 89 | await client.close() 90 | } 91 | 92 | ``` 93 | 94 | That's it—we're done! 95 | Run your Faktory server using Docker and run the worker using `yarn rw exec faktoryWorker`. 96 | 97 | If your Faktory server in running and you have set `FAKTORY_URL` correctly, you'll see the server pick up the jobs and your worker process the job. -------------------------------------------------------------------------------- /cookbook/Mocking_GraphQL_Storybook.md: -------------------------------------------------------------------------------- 1 | # Mocking GraphQL in Storybook 2 | 3 | ## Pre-requisites 4 | 5 | 1. Storybook should be running, start it by running `yarn rw storybook` 6 | 2. Have a Cell, Query, or Mutation that you would like to mock 7 | 8 | ## Where to put mock-requests 9 | 10 | 1. Mock-requests placed in a file ending with `.mock.js|ts` are automatically imported and become globally scoped, which means that they will be available in all of your stories. 11 | 2. Mock-requests in a story will be locally scoped and will overwrite globally scoped mocks. 12 | 13 | ## Mocking a Cell's Query 14 | 15 | Locate the file ending with with `.mock.js` in your Cell's folder. This file exports a value named `standard`, which is the mock-data that will be returned for your Cell's `QUERY`. 16 | ```js{4,5,6,12,13,14} 17 | // UserProfileCell/UserProfileCell.js 18 | export const QUERY = gql` 19 | query UserProfileQuery { 20 | userProfile { 21 | id 22 | } 23 | } 24 | ` 25 | 26 | // UserProfileCell/UserProfileCell.mock.js 27 | export const standard = { 28 | userProfile: { 29 | id: 42 30 | } 31 | } 32 | ``` 33 | 34 | The value assigned to `standard` is the mock-data associated to the `QUERY`, so modifying the `QUERY` means you need to modify the mock-data. 35 | ```diff 36 | // UserProfileCell/UserProfileCell.js 37 | export const QUERY = gql` 38 | query UserProfileQuery { 39 | userProfile { 40 | id 41 | + name 42 | } 43 | } 44 | ` 45 | 46 | // UserProfileCell/UserProfileCell.mock.js 47 | export const standard = { 48 | userProfile: { 49 | id: 42, 50 | + name: 'peterp', 51 | } 52 | } 53 | ``` 54 | 55 | > Behind the scenes: Redwood uses the value associated to `standard` as the second argument to `mockGraphQLQuery`. 56 | 57 | ### GraphQL request variables 58 | 59 | If you want to dynamically modify mock-data based on a queries variables the `standard` export can also be a function, and the first parameter will be an object containing the variables: 60 | ```js{2,7} 61 | // UserProfileCell/UserProfileCell.mock.js 62 | export const standard = (variables) => { 63 | return { 64 | userProfile: { 65 | id: 42, 66 | name: 'peterp', 67 | profileImage: `https://example.com/profile.png?size=${variables.size}` 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ## Mocking a GraphQL Query 74 | 75 | If you're not using a Cell, or if you want to overwrite a globally scoped mock, you can use `mockGraphQLQuery`: 76 | 77 | ```jsx 78 | // Header/Header.stories.js 79 | export const withReallyLongName = () => { 80 | mockGraphQLQuery('UserProfileQuery', () => { 81 | return { 82 | userProfile: { 83 | id: 99, 84 | name: 'Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr.' 85 | } 86 | } 87 | }) 88 | return
    89 | } 90 | ``` 91 | 92 | ## Mocking a GraphQL Mutation 93 | 94 | Use `mockGraphQLMutation`: 95 | 96 | ```js 97 | // UserProfileCell/UserProfileCell.mock.js 98 | export const standard = /* ... */ 99 | 100 | mockGraphQLMutation('UpdateUserName', ({ name }) => { 101 | return { 102 | userProfile: { 103 | id: 99, 104 | name, 105 | } 106 | } 107 | }) 108 | ``` 109 | 110 | ## Mock-requests that intentionally produce errors 111 | 112 | `mockGraphQLQuery` and `mockGraphQLMutation` have access to `ctx` which allows you to modify the mock-response: 113 | 114 | ```js 115 | mockGraphQLQuery('UserProfileQuery', (_vars, { ctx }) => { 116 | // Forbidden 117 | ctx.status(403) 118 | }) 119 | ``` -------------------------------------------------------------------------------- /cookbook/Pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | This tutorial will show you one way to implement pagination in an app built using RedwoodJS. It builds on top of [the tutorial](https://redwoodjs.com/tutorial) and I'll assume you have a folder with the code from the tutorial that you can continue working on. (If you don't, you can clone this repo: https://github.com/thedavidprice/redwood-tutorial-test) 4 | 5 | ![redwoodjs-pagination](https://user-images.githubusercontent.com/30793/94778130-ec6d6e00-03c4-11eb-9fd0-97cbcdf68ec2.png) 6 | 7 | The screenshot above shows what we're building. See the pagination at the bottom? The styling is up to you to fix. 8 | 9 | So you have a blog, and probably only a few short posts. But as the blog grows bigger you'll soon need to paginate all your posts. So, go ahead and create a bunch of posts to make this pagination worthwhile. We'll display five posts per page, so begin with creating at least six posts, to get two pages. 10 | 11 | We'll begin by updating the SDL. To our `Query` type a new query is added to get just a single page of posts. We'll pass in the page we want, and when returning the result we'll also include the total number of posts as that'll be needed when building our pagination component. 12 | 13 | ```javascript 14 | // api/src/graphql/posts.sdl.js 15 | 16 | export const schema = gql` 17 | # ... 18 | 19 | type PostPage { 20 | posts: [Post!]! 21 | count: Int! 22 | } 23 | 24 | type Query { 25 | postPage(page: Int): PostPage 26 | posts: [Post!]! 27 | post(id: Int!): Post! 28 | } 29 | 30 | # ... 31 | ` 32 | ``` 33 | 34 | You might have noticed that we made the page optional. That's because we want to be able to default to the first page if no page is provided. 35 | 36 | Now we need to add a resolver for this new query to our posts service. 37 | ```javascript 38 | // api/src/services/posts/posts.js 39 | 40 | const POSTS_PER_PAGE = 5 41 | 42 | export const postPage = ({ page = 1 }) => { 43 | const offset = (page - 1) * POSTS_PER_PAGE 44 | 45 | return { 46 | posts: db.post.findMany({ 47 | take: POSTS_PER_PAGE, 48 | skip: offset, 49 | orderBy: { createdAt: 'desc' }, 50 | }), 51 | count: db.post.count(), 52 | } 53 | } 54 | ``` 55 | 56 | So now we can make a GraphQL request (using [Apollo](https://www.apollographql.com/)) for a specific page of our blog posts. And the resolver we just updated will use [Prisma](https://www.prisma.io/) to fetch the correct posts from our database. 57 | 58 | With these updates to the API side of things done, it's time to move over to the web side. It's the BlogPostsCell component that makes the gql query to display the list of blog posts on the HomePage of the blog, so let's update that query. 59 | 60 | ```javascript 61 | // web/src/components/BlogPostsCell/BlogPostsCell.js 62 | 63 | export const QUERY = gql` 64 | query BlogPostsQuery($page: Int) { 65 | postPage(page: $page) { 66 | posts { 67 | id 68 | title 69 | body 70 | createdAt 71 | } 72 | count 73 | } 74 | } 75 | ` 76 | ``` 77 | 78 | The `Success` component in the same file also needs a bit of an update to handle the new gql query result structure. 79 | 80 | ```javascript 81 | // web/src/components/BlogPostsCell/BlogPostsCell.js 82 | 83 | export const Success = ({ postPage }) => { 84 | return postPage.posts.map((post) => ) 85 | } 86 | ``` 87 | 88 | Now we need a way to pass a value for the `page` parameter to the query. To do that we'll take advantage of a little RedwoodJS magic. Remember from the tutorial how you made the post id part of the route path `()` and that id was then sent as a prop to the BlogPostPage component? We'll do something similar here for the page number, but instead of making it a part of the url path, we'll make it a url query string. These, too, are magically passed as a prop to the relevant page component. And you don't even have to update the route to make it work! Let's update `HomePage.js` to handle the prop. 89 | 90 | ```javascript 91 | // web/src/pages/HomePage/HomePage.js 92 | 93 | const HomePage = ({ page = 1 }) => { 94 | return ( 95 | 96 | 97 | 98 | ) 99 | } 100 | ``` 101 | 102 | So now if someone navigates to https://awesomeredwoodjsblog.com?page=2 (and the blog was actually hosted on awesomeredwoodjsblog.com), then `HomePage` would have its `page` prop set to `"2"`, and we then pass that value along to `BlogPostsCell`. If no `?page=` query parameter is provided `page` will default to `1` 103 | 104 | Going back to `BlogPostsCell` there is one me thing to add before the query parameter work. 105 | 106 | ```javascript 107 | // web/src/components/BlogPostsCell/BlogPostsCell.js 108 | 109 | export const beforeQuery = ({ page }) => { 110 | page = page ? parseInt(page, 10) : 1 111 | 112 | return { variables: { page } } 113 | } 114 | ``` 115 | 116 | The query parameter is passed to the component as a string, so we need to parse it into a number. 117 | 118 | If you run the project with `yarn rw dev` on the default port 8910 you can now go to http://localhost:8910 and you should only see the first five posts. Change the URL to http://localhost:8910?page=2 and you should see the next five posts (if you have that many, if you only have six posts total you should now see just one post). 119 | 120 | The final thing to add is a page selector, or pagination component, to the end of the list of posts to be able to click and jump between the different pages. 121 | 122 | Generate a new component with`yarn rw g component Pagination` 123 | 124 | ```javascript 125 | // web/src/components/Pagination/Pagination.js 126 | 127 | import { Link, routes } from '@redwoodjs/router' 128 | 129 | const POSTS_PER_PAGE = 5 130 | 131 | const Pagination = ({ count }) => { 132 | const items = [] 133 | 134 | for (let i = 0; i < Math.ceil(count / POSTS_PER_PAGE); i++) { 135 | items.push( 136 |
  • 137 | 138 | {i + 1} 139 | 140 |
  • 141 | ) 142 | } 143 | 144 | return ( 145 | <> 146 |

    Pagination

    147 |
      {items}
    148 | 149 | ) 150 | } 151 | 152 | export default Pagination 153 | ``` 154 | 155 | Keeping with the theme of the official RedwoodJS tutorial we're not adding any css, but if you wanted the pagination to look a little nicer it'd be easy to remove the bullets from that list, and make it horizontal instead of vertical. 156 | 157 | Finally let's add this new component to the end of `BlogPostsCell`. Don't forget to `import` it at the top as well. 158 | 159 | ```javascript 160 | // web/src/components/BlogPostsCell/BlogPostsCell.js 161 | 162 | import Pagination from 'src/components/Pagination' 163 | 164 | // ... 165 | 166 | export const Success = ({ postPage }) => { 167 | return ( 168 | <> 169 | {postPage.posts.map((post) => )} 170 | 171 | 172 | 173 | ) 174 | } 175 | ``` 176 | 177 | And there you have it! You have now added pagination to your redwood blog. One technical limitation to the current implementation is that it doesn't handle too many pages very gracefully. Just imagine what that list of pages would look like if you had 100 pages! It's left as an exercise to the reader to build a more fully featured Pagination component. 178 | 179 | Most of the code in this tutorial was copy/pasted from the ["Hammer Blog" RedwoodJS example](https://github.com/redwoodjs/example-blog) 180 | 181 | If you want to learn more about [pagination with Prisma](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/pagination) and [pagination with Apollo](https://www.apollographql.com/docs/react/data/pagination/) they both have excellent docs on the topic. 182 | -------------------------------------------------------------------------------- /cookbook/Self-hosting_Redwood.md: -------------------------------------------------------------------------------- 1 | # Self-hosting Redwood (Serverful) 2 | 3 | Do you prefer hosting Redwood on your own server, the traditional serverful way, instead of all this serverless magic? Well, you can! In this recipe we configure a Redwood app with PM2 and Nginx on a Linux server. 4 | 5 | > A code example can be found at https://github.com/njjkgeerts/redwood-pm2, and can be viewed live at http://redwood-pm2.nickgeerts.com. 6 | 7 | ## Requirements 8 | 9 | You should have some basic knowledge of the following tools: 10 | 11 | - [PM2](https://pm2.keymetrics.io/docs/usage/pm2-doc-single-page/) 12 | - [Nginx](https://nginx.org/en/docs/) 13 | - Linux 14 | - [Postgres](https://www.postgresql.org/docs/) 15 | 16 | ## Configuration 17 | 18 | To self-host, you'll have to do a bit of configuration both to your Redwood app and your Linux server. 19 | 20 | ### Adding Dependencies 21 | 22 | First add PM2 as a dev dependency to your project root: 23 | 24 | ```termninal 25 | yarn add -DW pm2 26 | ``` 27 | 28 | Then create a PM2 ecosystem configuration file. For clarity, it's recommended to rename `ecosystem.config.js` to something like `pm2.config.js`: 29 | 30 | ```terminal 31 | yarn pm2 init 32 | mv ecosystem.config.js pm2.config.js 33 | ``` 34 | 35 | Last but not least, change the API endpoint in `redwood.toml`: 36 | 37 | ```diff 38 | - apiUrl = "/.redwood/functions" 39 | + apiUrl = "/api" 40 | ``` 41 | 42 | Optionally, add some scripts to your top-level `package.json`: 43 | 44 | ```json 45 | "scripts": { 46 | "deploy:setup": "pm2 deploy pm2.config.js production setup", 47 | "deploy": "pm2 deploy pm2.config.js production deploy" 48 | } 49 | ``` 50 | 51 | We'll refer to these later, so even if you don't add them to your project, keep them in mind. 52 | 53 | ### Linux server 54 | 55 | Your Linux server should have a user for deployment, configured with an SSH key providing access to your production environment. In this example, the user is named `deploy`. 56 | 57 | ### Nginx 58 | 59 | Typically, you keep your Nginx configuration file at `/etc/nginx/sites-available/redwood-pm2` and symlink it to `/etc/nginx/sites-enabled/redwood-pm2`. It should look something like this: 60 | 61 | ```nginx{10} 62 | server { 63 | server_name redwood-pm2.example.com; 64 | listen 80; 65 | 66 | location / { 67 | root /home/deploy/redwood-pm2/current/web/dist; 68 | try_files $uri /index.html; 69 | } 70 | 71 | location /api/ { 72 | proxy_pass http://localhost:8911/; 73 | proxy_http_version 1.1; 74 | proxy_set_header Upgrade $http_upgrade; 75 | proxy_set_header Connection 'upgrade'; 76 | proxy_set_header Host $host; 77 | proxy_cache_bypass $http_upgrade; 78 | } 79 | } 80 | ``` 81 | 82 | Please note that the trailing slash in `proxy_pass` is essential to correctly map the API functions. 83 | 84 | ### PM2 85 | 86 | Let's configure PM2 with the `pm2.config.js` file we made earlier. The most important variables are at the top. Note that the port is only used locally on the server and should match the port in the Nginx config: 87 | 88 | ```javascript 89 | const name = 'redwood-pm2' // Name to use in PM2 90 | const repo = 'git@github.com:njjkgeerts/redwood-pm2.git' // Link to your repo 91 | const user = 'deploy' // Server user 92 | const path = `/home/${user}/${name}` // Path on the server to deploy to 93 | const host = 'example.com' // Server hostname 94 | const port = 8911 // Port to use locally on the server 95 | const build = `yarn install && yarn rw build && yarn rw prisma migrate deploy` 96 | 97 | module.exports = { 98 | apps: [ 99 | { 100 | name, 101 | node_args: '-r dotenv/config', 102 | cwd: `${path}/current/`, 103 | script: 'yarn rw serve api', 104 | args: `--port ${port}`, 105 | env: { 106 | NODE_ENV: 'development', 107 | }, 108 | env_production: { 109 | NODE_ENV: 'production', 110 | }, 111 | }, 112 | ], 113 | 114 | deploy: { 115 | production: { 116 | user, 117 | host, 118 | ref: 'origin/master', 119 | repo, 120 | path, 121 | ssh_options: 'ForwardAgent=yes', 122 | 'post-deploy': `${build} && pm2 reload pm2.config.js --env production && pm2 save`, 123 | }, 124 | }, 125 | } 126 | ``` 127 | 128 | If you need to seed your production database during your first deployment, `yarn redwood prisma migrate dev` will do that for you. 129 | 130 | > **Caveat:** the API seems to only work in fork mode in PM2, not [cluster mode](https://pm2.keymetrics.io/docs/usage/cluster-mode/). 131 | 132 | ## Deploying 133 | 134 | First, we need to create the PM2 directories: 135 | 136 | ```terminal 137 | yarn install 138 | yarn deploy:setup 139 | ``` 140 | 141 | Your server directories are now set, but we haven't configured the `.env` settings yet. SSH into your server and create an `.env` file in the `current` subdirectory of the deploy directory: 142 | 143 | ```terminal 144 | vim /home/deploy/redwood-pm2/current/.env 145 | ``` 146 | 147 | For example, add a `DATABASE_URL` variable: 148 | 149 | ```env 150 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/redwood-pm2 151 | ``` 152 | 153 | Now we can deploy the app! Just run the following; it should update the code, take care of database migrations, and restart the app in PM2: 154 | 155 | ```terminal 156 | yarn deploy 157 | ``` 158 | 159 | Enjoy! 😁 160 | -------------------------------------------------------------------------------- /cookbook/windows_setup.md: -------------------------------------------------------------------------------- 1 | # Windows Development Setup 2 | 3 | This guide provides a simple setup to start developing a RedwoodJS project on Windows. Many setup options exist, but this aims to make getting started as easy as possible. This is the recommended setup unless you have experience with some other shell, like PowerShell. 4 | 5 | > If you're interested in using the Windows Subsystem for Linux instead, there is a [community guide for that](https://community.redwoodjs.com/t/windows-subsystem-for-linux-setup/2439). 6 | 7 | ### Git Bash 8 | 9 | Download the latest release of [**Git for Windows**](https://git-scm.com/download/win) and install it. 10 | When installing Git, you can add the icon on the Desktop and add Git Bash profile to Windows Terminal if you use it, but it is optional. 11 | 12 | ![1-git_components.png](https://user-images.githubusercontent.com/18013532/146685298-b12ed1a5-fe99-4286-ab12-69cf0a7be139.png) 13 | 14 | Next, set VS Code as Git default editor (or pick any other editor you're comfortable with). 15 | 16 | ![2-git_editor.png](https://user-images.githubusercontent.com/18013532/146685299-0e067554-a5a8-46b9-91ac-ffcd6f738b80.png) 17 | 18 | For all other steps, we recommended keeping the default choices. 19 | 20 | ### Node.js environment (and npm) 21 | 22 | We recommend you install the latest `nvm-setup.zip` of [**nvm-windows**](https://github.com/coreybutler/nvm-windows/releases) to manage multiple version installations of Node.js. When the installation of nvm is complete, run Git Bash as administrator to install Node with npm. 23 | 24 | ![3-git_run_as_admin.png](https://user-images.githubusercontent.com/18013532/146685300-1762a00a-26cb-4f8b-b480-c6aba4e26b89.png) 25 | 26 | Redwood uses the LTS version of Node. To install, run the following commands inside the terminal: 27 | 28 | ```bash 29 | $ nvm install lts --latest-npm 30 | // installs latest LTS and npm; e.g. 16.13.1 for the following examples 31 | $ nvm use 16.13.1 32 | ``` 33 | 34 | ### Yarn 35 | 36 | Now you have both Node and npm installed! Redwood also uses yarn, which you can now install using npm: 37 | 38 | ```bash 39 | npm install -g yarn 40 | ``` 41 | 42 | *Example of Node.js, npm, and Yarn installation steps* 43 | 44 | ![4-install_yarn.png](https://user-images.githubusercontent.com/18013532/146685297-b361ebea-7229-4d8c-bc90-472773d06816.png) 45 | 46 | ## Congrats! 47 | 48 | You now have everything ready to build your Redwood app. 49 | 50 | Next, you should start the amazing [**Redwood Tutorial**](https://learn.redwoodjs.com/docs/tutorial/installation-starting-development) to learn how to use the framework. 51 | 52 | Or run `yarn create redwood-app myApp` to get started with a new project. 53 | 54 | 55 | >⚠️ Heads Up 56 | On Windows Git Bash, `cd myapp` and `cd myApp` will select the same directory because Windows is case-insensitive. But make sure you type the original capitalization to avoid strange errors in your Redwood project. 57 | -------------------------------------------------------------------------------- /docs/a11y.md: -------------------------------------------------------------------------------- 1 | # Accessibility (aka a11y) 2 | 3 | We built Redwood to make building websites more accessible (we write all the config so you don't have to), but Redwood's also built to help you make more accessible websites. Accessibility shouldn't be a nice-to-have. It should be a given from the start, a core feature that's built-in and well-supported. 4 | 5 | There's a lot of great tooling out there that'll not only help you build accessible websites, but also help you learn exactly what that means. 6 | 7 | > **With all this tooling, do I still have to manually test my application?** 8 | > 9 | > Unequivocally, yes. Even with all the tooling in the world, manual testing's still important, especially for accessibility. 10 | > The GDS Accessibility team found that [automated testing only catches ~30% of all the issues](https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage). 11 | > 12 | > But just because the tools don't catch 'em all doesn't mean they're not valuable. It'd be much harder to learn what to look for without them. 13 | 14 | ## Accessible Routing with Redwood Router 15 | 16 | For single-page applications (SPAs), accessibility starts with the Router. Without a full-page refresh, you just can't be sure that things like announcements and focus are being taken care of the way they're supposed to be. Here's a great example of [how disorienting SPAs can be to screen-reader users](https://www.youtube.com/watch?v=NKTdNv8JpuM). On navigation, nothing's announced. It's important not to understate the severity of this; the lack of an announcement isn't just buggy behavior, it's broken. 17 | 18 | Normally the onus would be on you as a developer to announce to screen-reader users that they've navigated somewhere new. That's a lot to ask, and hard to get right, especially when you're just trying to build your app. Luckily, if you're writing good content and marking it up semantically, there's nothing you have to do! Redwood automatically and always announces pages on navigation. Redwood looks for announcements in this order: 19 | 20 | 1. `RouteAnnouncement` 21 | 2. `h1` 22 | 3. `document.title` 23 | 4. `location.pathname` 24 | 25 | The reason for this is that announcements should be as specific as possible; more specific usually means more descriptive, and more descriptive usually means that users can not only orient themselves and navigate through the content, but also find it again. 26 | If you're not sure if your content is descriptive enough, see the [W3 guidelines](https://www.w3.org/WAI/WCAG21/Techniques/general/G88.html). 27 | 28 | Even though Redwood looks for a `RouteAnnouncement` first, you don't have to have one on every page—it's more than ok for the h1 to be what's announced. `RouteAnnouncement` is there for when the situation calls for a custom announcement. 29 | 30 | The API is simple: `RouteAnnouncement`'s children will be announced; note that this can be something on the page, or can be visually hidden using the `visuallyHidden` prop: 31 | 32 | ```js 33 | // web/src/pages/HomePage.js 34 | 35 | import { RouteAnnouncement } from '@redwoodjs/router' 36 | 37 | const HomePage = () => { 38 | return ( 39 | // this will still be visible 40 | 41 |

    Welcome to my site!

    42 |
    43 | ) 44 | } 45 | 46 | export default HomePage 47 | ``` 48 | 49 | ```js 50 | // web/src/pages/AboutPage.js 51 | 52 | import { RouteAnnouncement } from '@redwoodjs/router' 53 | 54 | const AboutPage = () => { 55 | return ( 56 |

    Welcome to my site!

    57 | // this won't be visible 58 | 59 | All about me 60 | 61 | ) 62 | } 63 | 64 | export default AboutPage 65 | ``` 66 | 67 | Whenever possible, it's good to maintain parity between the visual and audible experience of your app. That's just to say that `visuallyHidden` shouldn't be the first thing you reach for. But it's there if you need it! 68 | 69 | 70 | 71 | ## Focus 72 | 73 | On page change, Redwood Router resets focus to the top of the DOM so that users can navigate through the new page. While this is the expected behavior (and the behavior you usually want), for some apps, especially those with a lot of navigation, it can be cumbersome for users to have tab through all that nav before getting to the main point. (And that goes for every page change!) 74 | 75 | Right now, there's two ways to alleviate this in Redwood: with skip links and/or the `RouteFocus` component. 76 | 77 | ### Skip links 78 | 79 | Since the main content isn't usually the first thing on the page, it's a good practice to provide a shortcut for keyboard and screen-reader users to skip to it. Skip links do just that, and if you generate a layout (`yarn rw g layout`) with the `--skipLink` flag, you'll get a layout with a skip link: 80 | 81 | ```terminal 82 | yarn rw g layout main --skipLink 83 | ``` 84 | 85 | ```js 86 | import { SkipNavLink, SkipNavContent } from '@redwoodjs/router' 87 | import '@reach/skip-nav/styles.css' 88 | 89 | const MainLayout = ({ children }) => { 90 | return ( 91 | <> 92 | 93 | 94 | 95 |
    {children}
    96 | 97 | ) 98 | } 99 | 100 | export default MainLayout 101 | ``` 102 | 103 | `SkipNavLink` renders a link that remains hidden till focused; `SkipNavContent` renders a div as the target for the link. For more on these components, see the [Reach UI](https://reach.tech/skip-nav/#reach-skip-nav) docs. 104 | 105 | Making sure your navs have skip links is a great practice that goes a long way. And it really doesn't cost you much! 106 | One thing you'll probably want to do is change the URL the skip link sends the user to when activated. You can do that by changing the `contentId` and `id` props of `SkipNavLink` and `SkipNavContent` respectively: 107 | 108 | ```js 109 | 110 | 111 | // ... 112 | 113 | 114 | ``` 115 | 116 | If you'd prefer to implement your own skip link, [Ben Myers' blog](https://benmyers.dev/blog/skip-links/) is a great resource, and a great place to read about accessibility in general. 117 | 118 | ### RouteFocus 119 | 120 | Sometimes you don't want to just skip the nav, but send a user somewhere. In this situation, you of course have the foresight that that place is where the user wants to be! So please use this at your discretion—sending a user to an unexpected location can be worse than sending them back the top. 121 | 122 | Having said that, if you know that on a particular page change a user's focus is better off being directed to a particular element, the `RouteFocus` component is what you want: 123 | 124 | ```js 125 | import { RouteFocus } from '@redwoodjs/router' 126 | 127 | const ContactPage = () => ( 128 | 131 | 132 | // the contact form the user actually wants to interact with 133 | 134 | 135 | 136 | ) 137 | 138 | export default ContactPage 139 | ``` 140 | 141 | `RouteFocus` tells the router to send focus to it's child on page change. In the example above, when the user navigates to the contact page, the name text field on the form gets focus—the first field of the form they're here to fill out. 142 | 143 | For a video example of using `RouteFocus`, see our [meetup on Redwood's accessibility features](https://youtu.be/T1zs77LU68w?t=3240). 144 | -------------------------------------------------------------------------------- /docs/assetsAndFiles.md: -------------------------------------------------------------------------------- 1 | # Assets and Files 2 | 3 | > ⚠ **Work in Progress** ⚠️ 4 | > 5 | > There's more to document here. In the meantime, you can check our [community forum](https://community.redwoodjs.com/search?q=assets%20and%20files) for answers. 6 | > 7 | > Want to contribute? Redwood welcomes contributions and loves helping people become contributors. 8 | > You can edit this doc [here](https://github.com/redwoodjs/redwoodjs.com/blob/main/docs/assetsAndFiles.md). 9 | > If you have any questions, just ask for help! We're active on the [forums](https://community.redwoodjs.com/c/contributing/9) and on [discord](https://discord.com/channels/679514959968993311/747258086569541703). 10 | 11 | There are two methods for adding assets to a Redwood app: 12 | 13 | i) Webpack imports and 14 | ii) directly adding to the `/public` folder. 15 | 16 | ## Importing Assets 17 | 18 | In general, it's best to import files directly into a template, page or component. This allows Webpack to include that file in the bundle, ensuring correct processing for the distribution folder while providing error checks and correct paths along the way. 19 | 20 | ### Example Asset Import with Webpack 21 | 22 | Using `import`, we can do the following: 23 | 24 | ```javascript 25 | import React from 'react' 26 | import logo from './my-logo.jpg' 27 | 28 | function Header() { 29 | return Logo 30 | } 31 | 32 | export default Header 33 | ``` 34 | 35 | Webpack will correctly handle the file path and add the file to the distribution folder within `/dist/media` (created when Webpack builds for production). 36 | 37 | > Note: In this example, the file `my-logo.jpg` is located in the same directory as the component. This is recommended practice to keep all files related to a component in a single directory. 38 | 39 | Behind the scenes, we are using Webpack's ["file-loader"](https://webpack.js.org/loaders/file-loader/) and ["url-loader"](https://webpack.js.org/loaders/url-loader/) (which transforms images less than 10kb into data URIs for improved performance). 40 | 41 | ## Directly Adding Assets using the "Public" Folder 42 | 43 | Alternately, you can add files directly to the folder "web/public", effectively adding static files to your app. All included files and folders will be copied into the production build `web/dist` folder. They will also be available during development when you run `yarn rw dev`. 44 | 45 | Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Webpack, etc. 46 | 47 | > Note: files will _not_ hot reload while the development server is running. You'll need to manually stop/start to access file changes. 48 | 49 | Behind the scenes, Redwood is using Webpack's ["copy-webpack-plugin"](https://github.com/webpack-contrib/copy-webpack-plugin). 50 | 51 | ### Example Use 52 | 53 | Assuming `public/` includes the following: 54 | 55 | - `favicon.png` 56 | - `static-files/my-logo.jpg` 57 | 58 | Running `yarn build` will copy the file `favicon.png` to `/dist/favicon.png`. The new directory with file `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g. 59 | 60 | ```html 61 | 62 | ``` 63 | 64 | and 65 | 66 | ```html 67 | Logo 68 | ``` 69 | 70 | > Note: because the directory `dist/` becomes your production root, it should not be included in the path. 71 | -------------------------------------------------------------------------------- /docs/builds.md: -------------------------------------------------------------------------------- 1 | # Builds 2 | 3 | > ⚠ **Work in Progress** ⚠️ 4 | > 5 | > There's more to document here. In the meantime, you can check our [community forum](https://community.redwoodjs.com/search?q=yarn%20rw%20build) for answers. 6 | > 7 | > Want to contribute? Redwood welcomes contributions and loves helping people become contributors. 8 | > You can edit this doc [here](https://github.com/redwoodjs/redwoodjs.com/blob/main/docs/builds.md). 9 | > If you have any questions, just ask for help! We're active on the [forums](https://community.redwoodjs.com/c/contributing/9) and on [discord](https://discord.com/channels/679514959968993311/747258086569541703). 10 | 11 | 12 | ## API 13 | 14 | The api side of Redwood is transpiled by Babel into the `./api/dist` folder. 15 | 16 | ### steps on Netlify 17 | 18 | To emulate Netlify's build steps locally: 19 | 20 | ```bash 21 | yarn rw build api 22 | cd api 23 | yarn zip-it-and-ship-it dist/functions/ zipballs/ 24 | ``` 25 | 26 | Each lambda function in `./api/dist/functions` is parsed by zip-it-and-ship-it resulting in a zip file per lambda function that contains all the dependencies required for that lambda function. 27 | 28 | >Note: The `@netlify/zip-it-and-ship-it` package needs to be installed as a dev dependency in `api/`. Use the command `yarn workspace api add -D @netlify/zip-it-and-ship-it`. 29 | >- You can learn more about the package [here](https://www.npmjs.com/package/@netlify/zip-it-and-ship-it). 30 | >- For more information on AWS Serverless Deploy see these [docs](https://redwoodjs.com/docs/deploy#aws-serverless-deploy). 31 | 32 | ## Web 33 | 34 | The web side of Redwood is packaged by Webpack into the `./web/dist` folder. 35 | -------------------------------------------------------------------------------- /docs/connectionPooling.md: -------------------------------------------------------------------------------- 1 | # Connection Pooling 2 | 3 | > ⚠ **Work in Progress** ⚠️ 4 | > 5 | > There's more to document here. In the meantime, you can check our [community forum](https://community.redwoodjs.com/search?q=connection%20pooling) for answers. 6 | > 7 | > Want to contribute? Redwood welcomes contributions and loves helping people become contributors. 8 | > You can edit this doc [here](https://github.com/redwoodjs/redwoodjs.com/blob/main/docs/connectionPooling.md). 9 | > If you have any questions, just ask for help! We're active on the [forums](https://community.redwoodjs.com/c/contributing/9) and on [discord](https://discord.com/channels/679514959968993311/747258086569541703). 10 | 11 | Production Redwood apps should enable connection pooling in order to properly scale with your Serverless functions. 12 | ## Prisma Pooling with PgBouncer 13 | 14 | PgBouncer holds a connection pool to the database and proxies incoming client connections by sitting between Prisma Client and the database. This reduces the number of processes a database has to handle at any given time. PgBouncer passes on a limited number of connections to the database and queues additional connections for delivery when space becomes available. 15 | 16 | 17 | To use Prisma Client with PgBouncer from a serverless function, add the `?pgbouncer=true` flag to the PostgreSQL connection URL: 18 | 19 | ``` 20 | postgresql://USER:PASSWORD@HOST:PORT/DATABASE?pgbouncer=true 21 | ``` 22 | 23 | Typically, your PgBouncer port will be 6543 which is different than the Postgres default of 5432. 24 | 25 | > Note that since Prisma Migrate uses database transactions to check out the current state of the database and the migrations table, if you attempt to run Prisma Migrate commands in any environment that uses PgBouncer for connection pooling, you might see an error. 26 | > 27 | > To work around this issue, you must connect directly to the database rather than going through PgBouncer when migrating. 28 | 29 | For more information on Prisma and PgBouncer, please refer to Prisma's Guide on [Configuring Prisma Client with PgBouncer](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management/configure-pg-bouncer). 30 | 31 | ## Supabase 32 | 33 | For Postgres running on [Supabase](https://supabase.io) see: [PgBouncer is now available in Supabase](https://supabase.io/blog/2021/04/02/supabase-pgbouncer#using-connection-pooling-in-supabase). 34 | 35 | All new Supabase projects include connection pooling using [PgBouncer](https://www.pgbouncer.org/). 36 | 37 | We recommend that you connect to your Supabase Postgres instance using SSL which you can do by setting `sslmode` to `require` on the connection string: 38 | 39 | ``` 40 | // not pooled typically uses port 5432 41 | postgresql://postgres:mydb.supabase.co:5432/postgres?sslmode=require 42 | // pooled typically uses port 6543 43 | postgresql://postgres:mydb.supabase.co:6543/postgres?sslmode=require&pgbouncer=true 44 | ``` 45 | 46 | ## Heroku 47 | For Postgres, see [Postgres Connection Pooling](https://devcenter.heroku.com/articles/postgres-connection-pooling). 48 | 49 | Heroku does not officially support MySQL. 50 | 51 | 52 | ## Digital Ocean 53 | For Postgres, see [How to Manage Connection Pools](https://www.digitalocean.com/docs/databases/postgresql/how-to/manage-connection-pools) 54 | 55 | Connection Pooling for MySQL is not yet supported. 56 | 57 | ## AWS 58 | Use [Amazon RDS Proxy](https://aws.amazon.com/rds/proxy) for MySQL or PostgreSQL. 59 | 60 | From the [AWS Docs](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy.limitations): 61 | >Your RDS Proxy must be in the same VPC as the database. The proxy can't be publicly accessible. 62 | 63 | Because of this limitation, with out-of-the-box configuration, you can only use RDS Proxy if you're deploying your Lambda Functions to the same AWS account. Alternatively, you can use RDS directly, but you might require larger instances to handle your production traffic and the number of concurrent connections. 64 | 65 | 66 | ## Why Connection Pooling? 67 | 68 | Relational databases have a maximum number of concurrent client connections. 69 | 70 | * Postgres allows 100 by default 71 | * MySQL allows 151 by default 72 | 73 | In a traditional server environment, you would need a large amount of traffic (and therefore web servers) to exhaust these connections, since each web server instance typically leverages a single connection. 74 | 75 | In a Serverless environment, each function connects directly to the database, which can exhaust limits quickly. To prevent connection errors, you should add a connection pooling service in front of your database. Think of it as a load balancer. 76 | -------------------------------------------------------------------------------- /docs/customIndex.md: -------------------------------------------------------------------------------- 1 | # Custom Web Index 2 | 3 | You might've noticed that there's no call to `ReactDOM.render` anywhere in your Redwood App (`v0.26` and greater). That's because Redwood automatically mounts your `` in `web/src/App.js` to the DOM. But if you need to customize how this happens, you can provide a file called `index.js` in `web/src` and Redwood will use that instead. 4 | 5 | To make this easy to do, there's a setup command that'll give you the file you need where you need it: 6 | 7 | ``` 8 | yarn rw setup custom-web-index 9 | ``` 10 | 11 | This generates a file named `index.js` in `web/src` that looks like this: 12 | 13 | ```js 14 | // web/src/index.js 15 | 16 | import ReactDOM from 'react-dom' 17 | 18 | import App from './App' 19 | /** 20 | * When `#redwood-app` isn't empty then it's very likely that you're using 21 | * prerendering. So React attaches event listeners to the existing markup 22 | * rather than replacing it. 23 | * https://reactjs.org/docs/react-dom.html#hydrate 24 | */ 25 | const rootElement = document.getElementById('redwood-app') 26 | 27 | if (rootElement.hasChildNodes()) { 28 | ReactDOM.hydrate(, rootElement) 29 | } else { 30 | ReactDOM.render(, rootElement) 31 | ``` 32 | 33 | 34 | This is actually the same file Redwood uses [internally](https://github.com/redwoodjs/redwood/blob/main/packages/web/src/entry/index.js). So even if you don't customize anything any further than this, things will still work the way the should! 35 | -------------------------------------------------------------------------------- /docs/dataMigrations.md: -------------------------------------------------------------------------------- 1 | # Data Migrations 2 | 3 | > Data Migrations are available as of RedwoodJS v0.15 4 | 5 | There are two kinds of changes you can make to your database: 6 | 7 | * Changes to structure 8 | * Changes to content 9 | 10 | In Redwood, [Prisma Migrate](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-migrate) takes care of codifying changes to your database *structure* in code by creating a snapshot of changes to your database that can be reliably repeated to end up in some known state. 11 | 12 | To track changes to your database *content*, Redwood includes a feature we call **Data Migration**. As your app evolves and you move data around, you need a way to consistently declare how that data should move. 13 | 14 | Imagine a `User` model that contains several columns for user preferences. Over time, you may end up with more and more preferences to the point that you have more preference-related columns in the table than you do data unique to the user! This is a common occurrence as applications grow. You decide that the app should have a new model, `Preference`, to keep track of them all (and `Preference` will have a foreign key `userId` to reference it back to its `User`). You'll use Prisma Migrate to create the new `Preference` model, but how do you copy the preference data to the new table? Data migrations to the rescue! 15 | 16 | ## Installing 17 | 18 | Just like Prisma, we will store which data migrations have run in the database itself. We'll create a new database table `DataMigration` to keep track of which ones have run already. 19 | 20 | Rather than create this model by hand, Redwood includes a CLI tool to add the model to `schema.prisma` and create the DB migration that adds the table to the database: 21 | 22 | yarn rw data-migrate install 23 | 24 | You'll see a new directory created at `api/db/dataMigrations` which will store our individual migration tasks. 25 | 26 | Take a look at `schema.prisma` to see the new model definition: 27 | 28 | ```javascript 29 | // api/db/schema.prisma 30 | 31 | model RW_DataMigration { 32 | version String @id 33 | name String 34 | startedAt DateTime 35 | finishedAt DateTime 36 | } 37 | ``` 38 | 39 | The install script also ran `yarn rw prisma migrate dev --create-only` automatically so you have a DB migration ready to go. You just need to run the `prisma migrate dev` command to apply it: 40 | 41 | yarn rw prisma migrate dev 42 | 43 | ## Creating a New Data Migration 44 | 45 | Data migrations are just plain Typescript or Javascript files which export a single anonymous function that is given a single argument—an instance of `PrismaClient` called `db` that you can use to access your database. The files have a simple naming convention: 46 | 47 | {version}-{name}.js 48 | 49 | Where `version` is a timestamp, like `20200721123456` (an ISO8601 datetime without any special characters or zone identifier), and `name` is a param-case human readable name for the migration, like `copy-preferences`. 50 | 51 | To create a data migration we have a generator: 52 | 53 | yarn rw generate dataMigration copyPreferences 54 | 55 | This will create `api/db/dataMigrations/20200721123456-copy-preferences.js`: 56 | 57 | ```javascript 58 | // api/db/dataMigrations/20200721123456-copy-preferences.js 59 | 60 | export default async ({ db }) => { 61 | // Migration here... 62 | } 63 | ``` 64 | 65 | > **Why such a long name?** 66 | > 67 | > So that if multiple developers are creating data migrations, the chances of them creating one with the exact same filename is essentially zero, and they will all run in a predictable order—oldest to newest. 68 | 69 | Now it's up to you to define your data migration. In our user/preference example, it may look something like: 70 | 71 | ```javascript 72 | // api/db/dataMigrations/20200721123456-copy-preferences.js 73 | 74 | const asyncForEach = async (array, callback) => { 75 | for (let index = 0; index < array.length; index++) { 76 | await callback(array[index], index, array) 77 | } 78 | } 79 | 80 | export default async ({ db }) => { 81 | const users = await db.user.findMany() 82 | 83 | asyncForEach(users, async (user) => { 84 | await db.preference.create({ 85 | data: { 86 | newsletter: user.newsletter, 87 | frequency: user.frequency, 88 | theme: user.theme, 89 | user: { connect: { id: user.id } } 90 | } 91 | }) 92 | }) 93 | } 94 | ``` 95 | 96 | This loops through each existing `User` and creates a new `Preference` record containing each of the preference-related fields from `User`. 97 | 98 | > Note that in a case like this where you're copying data to a new table, you would probably delete the columns from `User` afterwards. This needs to be a two step process! 99 | > 100 | > 1. Create the new table (db migration) and then move the data over (data migration) 101 | > 2. Remove the unneeded columns from `User` 102 | > 103 | > When going to production, you would need to run this as two separate deploys to ensure no data is lost. 104 | > 105 | > The reason is that all DB migrations are run and *then* all data migrations. So if you had two DB migrations (one to create `Preference` and one to drop the unneeded columns from `User`) they would both run before the Data Migration, so the columns containing the preferences are gone before the data migration gets a chance to copy them over! 106 | > 107 | > **Remember**: Any destructive action on the database (removing a table or column especially) needs to be a two step process to avoid data loss. 108 | 109 | ## Running a Data Migration 110 | 111 | When you're ready, you can execute your data migration with `data-migrate`'s `up` command: 112 | 113 | yarn rw data-migrate up 114 | 115 | This goes through each file in `api/db/dataMigrations`, compares it against the list of migrations that have already run according to the `DataMigration` table in the database, and executes any that aren't present in that table, sorted oldest to newest based on the timestamp in the filename. 116 | 117 | Any logging statements (like `console.info()`) you include in your data migration script will be output to the console as the script is running. 118 | 119 | If the script encounters an error, the process will abort, skipping any following data migrations. 120 | 121 | > The example data migration above didn't include this for brevity, but you should always run your data migration [inside a transaction](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/transactions#bulk-operations-experimental) so that if any errors occur during execution the database will not be left in an inconsistent state where only *some* of your changes were performed. 122 | 123 | ## Long-term Maintainability 124 | 125 | Ideally you can run all database migrations and data migrations from scratch (like when a new developer joins the team) and have them execute correctly. Unfortunately you don't get that ideal scenario by default. 126 | 127 | Take our example above—what happens when a new developer comes long and attempts to setup their database? All DB migrations will run first (including the one that drops the preference-related columns from `User`) before the data migrations run. They will get an error when they try to read something like `user.newsletter` and that column doesn't exist! 128 | 129 | One technique to combat this is to check for the existence of these columns before the data migration does anything. If `user.newsletter` doesn't exist, then don't bother running the data migration at all and assume that your [seed data](https://redwoodjs.com/docs/cli-commands.html#seed) is already in the correct format: 130 | 131 | ```javascript{4,15} 132 | export default async ({ db }) => { 133 | const users = await db.user.findMany() 134 | 135 | if (typeof user.newsletter !== undefined) { 136 | asyncForEach(users, async (user) => { 137 | await db.preference.create({ 138 | data: { 139 | newsletter: user.newsletter, 140 | frequency: user.frequency, 141 | theme: user.theme, 142 | user: { connect: { id: user.id } } 143 | } 144 | }) 145 | }) 146 | } 147 | } 148 | ``` 149 | 150 | ## Lifecycle Summary 151 | 152 | Run once: 153 | 154 | yarn rw data-migrate install 155 | yarn rw prisma migrate dev 156 | 157 | Run every time you need a new data migration: 158 | 159 | yarn rw generate dataMigration migrationName 160 | yarn rw data-migrate up 161 | -------------------------------------------------------------------------------- /docs/environmentVariables.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | You can provide environment variables to each side of your Redwood app in different ways, depending on each Side's target, and whether you're in development or production. 4 | 5 | > Right now, Redwood apps have two fixed Sides, API and Web, that have each have a single target, nodejs and browser respectively. 6 | 7 | ## Generally 8 | 9 | Redwood apps use [dotenv](https://github.com/motdotla/dotenv) to load vars from your `.env` file into `process.env`. 10 | For a reference on dotenv syntax, see the dotenv README's [Rules](https://github.com/motdotla/dotenv#rules) section. 11 | 12 | > Technically, we use [dotenv-defaults](https://github.com/mrsteele/dotenv-defaults), which is how we also supply and load `.env.defaults`. 13 | 14 | 15 | 16 | Redwood also configures Webpack with `dotenv-webpack`, so that all references to `process.env` vars on the Web side will be replaced with the variable's actual value at built-time. More on this in [Web](#Web). 17 | 18 | ## Web 19 | 20 | ### Including environment variables 21 | > **Heads Up:** for Web to access environment variables in production, you _must_ configure one of the options below. 22 | > 23 | > Redwood recommends **Option 1: `redwood.toml`** as it is the most robust. 24 | 25 | In production, you can get environment variables to the Web Side either by 26 | 27 | 1. adding to `redwood.toml` via the `includeEnvironmentVariables` array, or 28 | 2. prefixing with `REDWOOD_ENV_` 29 | 30 | Just like for the API Side, you'll also have to set them up with your provider. 31 | 32 | #### Option 1: includeEnvironmentVariables in redwood.toml 33 | 34 | For Example: 35 | 36 | ```toml 37 | // redwood.toml 38 | 39 | [web] 40 | includeEnvironmentVariables = ['SECRET_API_KEY', 'ANOTHER_ONE'] 41 | ``` 42 | 43 | By adding environment variables to this array, they'll be available to Web in production via `process.env.SECRET_API_KEY`. This means that if you have an environment variable like `process.env.SECRET_API_KEY` Redwood removes and replaces it with its _actual_ value. 44 | 45 | Note: if someone inspects your site's source, _they could see your `REDWOOD_ENV_SECRET_API_KEY` in plain text._ This is a limitation of delivering static JS and HTML to the browser. 46 | 47 | #### Option 2: Prefixing with REDWOOD*ENV* 48 | 49 | In `.env`, if you prefix your environment variables with `REDWOOD_ENV_`, they'll be available via `process.env.REDWOOD_ENV_MY_VAR_NAME`, and will be dynamically replaced at built-time. 50 | 51 | Like the option above, these are also removed and replaced with the _actual value_ during build in order to be available in production. 52 | 53 | 54 | ### Accessing API URLs 55 | 56 | Redwood automatically makes your API URL configurations from the web section of your `redwood.toml` available globally. 57 | They're accessible via the `window` or `global` objects. 58 | For example, `global.RWJS_API_GRAPHQL_URL` gives you the URL for your graphql endpoint. 59 | 60 | The toml values are mapped as follows: 61 | 62 | | `redwood.toml` key | Available globally as | Description | 63 | | ------------------ | ----------------------------- | ---------------------------------------- | 64 | | `apiUrl` | `global.RWJS_API_URL` | URL or absolute path to your api-server | 65 | | `apiGraphQLUrl` | `global.RWJS_API_GRAPHQL_URL` | URL or absolute path to GraphQL function | 66 | | `apiDbAuthUrl` | `global.RWJS_API_DBAUTH_URL` | URL or absolute path to DbAuth function | 67 | 68 | See the [redwood.toml reference](https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths) for more details. 69 | 70 | ## API 71 | 72 | ### Development 73 | 74 | You can access environment variables defined in `.env` and `.env.defaults` as `process.env.VAR_NAME`. For example, if we define the environment variable `HELLO_ENV` in `.env`: 75 | 76 | ``` 77 | HELLO_ENV=hello world 78 | ``` 79 | 80 | and make a hello Function (`yarn rw generate function hello`) and reference `HELLO_ENV` in the body of our response: 81 | 82 | ```javascript{6} 83 | // ./api/src/functions/hello.js 84 | 85 | export const handler = async (event, context) => { 86 | return { 87 | statusCode: 200, 88 | body: `${process.env.HELLO_ENV}`, 89 | } 90 | } 91 | ``` 92 | 93 | Navigating to http://localhost:8911/hello shows that the Function successfully accesses the environment variable: 94 | 95 | 96 | 97 | 98 | ![rw-envVars-api](https://user-images.githubusercontent.com/32992335/86520528-47112100-bdfa-11ea-8d7e-1c0d502805b2.png) 99 | 100 | ### Production 101 | 102 | 103 | 104 | 105 | Whichever platform you deploy to, they'll have some specific way of making environment variables available to the serverless environment where your Functions run. For example, if you deploy to Netlify, you set your environment variables in **Settings** > **Build & Deploy** > **Environment**. You'll just have to read your provider's documentation. 106 | 107 | ## Keeping Sensitive Information Safe 108 | 109 | Since it usually contains sensitive information, you should [never commit your `.env` file](https://github.com/motdotla/dotenv#should-i-commit-my-env-file). Note that you'd actually have to go out of your way to do this as, by default, a Redwood app's `.gitignore` explicitly ignores `.env`: 110 | 111 | ```plaintext{2} 112 | .DS_Store 113 | .env 114 | .netlify 115 | dev.db 116 | dist 117 | dist-babel 118 | node_modules 119 | yarn-error.log 120 | ``` 121 | 122 | ## Where Does Redwood Load My Environment Variables? 123 | 124 | For all the variables in your `.env` and `.env.defaults` files to make their way to `process.env`, there has to be a call to `dotenv`'s `config` function somewhere. So where is it? 125 | 126 | It's in [the CLI](https://github.com/redwoodjs/redwood/blob/main/packages/cli/src/index.js#L6-L12)—every time you run a `yarn rw` command: 127 | 128 | ```javascript 129 | // packages/cli/src/index.js 130 | 131 | import { config } from 'dotenv-defaults' 132 | 133 | config({ 134 | path: path.join(getPaths().base, '.env'), 135 | encoding: 'utf8', 136 | defaults: path.join(getPaths().base, '.env.defaults'), 137 | }) 138 | ``` 139 | 140 | Remember, if `yarn rw dev` is already running, your local app won't reflect any changes you make to your `.env` file until you stop and re-run `yarn rw dev`. 141 | -------------------------------------------------------------------------------- /docs/localPostgresSetup.md: -------------------------------------------------------------------------------- 1 | # Local Postgres Setup 2 | 3 | RedwoodJS uses a SQLite database by default. While SQLite makes local development easy, you're 4 | likely going to want to run the same database you use in production locally at some point. And since the odds of that database being Postgres are high, here's how to set up Postgres. 5 | 6 | ## Install Postgres 7 | ### Mac 8 | If you're on a Mac, we recommend using Homebrew: 9 | 10 | ```bash 11 | brew install postgres 12 | ``` 13 | 14 | > **Install Postgres? I've messed up my Postgres installation so many times, I wish I could just uninstall everything and start over!** 15 | > 16 | > We've been there before. For those of you on a Mac, [this video](https://www.youtube.com/watch?v=1aybOgni7lI) is a great resource on how to wipe the various Postgres installs off your machine so you can get back to a blank slate. 17 | > Obviously, warning! This resource will teach you how to wipe the various Postgres installs off your machine. Please only do it if you know you can! 18 | 19 | ### Windows and Other Platforms 20 | If you're using another platform, see Prisma's [Data Guide](https://www.prisma.io/docs/guides/database-workflows/setting-up-a-database/postgresql) for detailed instructions on how to get up and running. 21 | 22 | ## Creating a database 23 | 24 | If everything went well, then Postgres should be running and you should have a few commands at your disposal (namely, `psql`, `createdb`, and `dropdb`). 25 | 26 | Check that Postgres is running with `brew services` (the `$(whoami)` bit in the code block below is just where your username should appear): 27 | 28 | ```bash 29 | $ brew services 30 | Name Status User Plist 31 | postgresql started $(whoami) /Users/$(whoami)/Library/LaunchAgents/homebrew.mxcl.postgresql.plist 32 | ``` 33 | 34 | If it's not started, start it with: 35 | 36 | ```bash 37 | brew services start postgresql 38 | ``` 39 | 40 | Great. Now let's try running the PostgresQL interactive terminal, `psql`: 41 | 42 | ```bash 43 | $ psql 44 | ``` 45 | 46 | You'll probably get an error like: 47 | 48 | ```bash 49 | psql: error: FATAL: database $(whoami) does not exist 50 | ``` 51 | 52 | This is because `psql` tries to log you into a database of the same name as your user. But if you just installed Postgres, odds are that database doesn't exist. 53 | 54 | Luckily it's super easy to create one using another of the commands you got, `createdb`: 55 | 56 | ```bash 57 | $ createdb $(whoami) 58 | ``` 59 | 60 | Now try: 61 | 62 | ``` 63 | $ psql 64 | psql (13.1) 65 | Type "help" for help. 66 | 67 | $(whoami)=# 68 | ``` 69 | 70 | If it worked, you should see a prompt like the one above—your username followed by `=#`. You're in the PostgreSQL interactive terminal! While we won't get into `psql`, here's a few the commands you should know: 71 | 72 | - `\q` — quit (super important!) 73 | - `\l` — list databases 74 | - `\?` — get a list of commands 75 | 76 | If you'd rather not follow any of the advice here and create another Postgres user instead of a Postgres database, follow [this](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04#step-3-%E2%80%94-creating-a-new-role). 77 | 78 | ## Update the Prisma Schema 79 | 80 | Tell Prisma to use a Postgres database instead of SQLite by updating the `provider` attribute in your 81 | `schema.prisma` file: 82 | 83 | ```prisma 84 | // prisma/schema.prisma 85 | datasource db { 86 | provider = "postgresql" 87 | url = env("DATABASE_URL") 88 | } 89 | ``` 90 | 91 | ## Connect to Postgres 92 | 93 | Add a `DATABASE_URL` to your `.env` file with the URL of the database you'd like to use locally. The 94 | following example uses `redwoodblog_dev` for the database. It also has `postgres` setup as a 95 | superuser for ease of use. 96 | ```env 97 | DATABASE_URL="postgresql://postgres@localhost:5432/redwoodblog_dev?connection_limit=1" 98 | ``` 99 | 100 | Note the `connection_limit` parameter. This is [recommended by Prisma](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/deployment#recommended-connection-limit) when working with 101 | relational databases in a Serverless context. You should also append this parameter to your production 102 | `DATABASE_URL` when configuring your deployments. 103 | 104 | ### Local Test DB 105 | You should also set up a test database similarly by adding `TEST_DATABASE_URL` to your `.env` file. 106 | ```env 107 | TEST_DATABASE_URL="postgresql://postgres@localhost:5432/redwoodblog_test?connection_limit=1" 108 | ``` 109 | 110 | > Note: local postgres server will need manual start/stop -- this is not handled automatically by RW CLI in a manner similar to sqlite 111 | 112 | ### Base URL and path 113 | 114 | Here is an example of the structure of the base URL and the path using placeholder values in uppercase letters: 115 | ```bash 116 | postgresql://USER:PASSWORD@HOST:PORT/DATABASE 117 | ``` 118 | The following components make up the base URL of your database, they are always required: 119 | | Name | Placeholder | Description | 120 | | ------ | ------ | ------| 121 | | Host | `HOST`| IP address/domain of your database server, e.g. `localhost` | 122 | | Port | `PORT` | Port on which your database server is running, e.g. `5432` | 123 | | User | `USER` | Name of your database user, e.g. `postgres` | 124 | | Password | `PASSWORD` | password of your database user | 125 | | Database | `DATABASE` | Name of the database you want to use, e.g. `redwoodblog_dev` | 126 | 127 | ## Migrations 128 | Migrations are snapshots of your DB structure, which, when applied, manage the structure of both your local development DB and your production DB. 129 | 130 | To create and apply a migration to the Postgres database specified in your `.env`, run the _migrate_ command. (Did this return an error? If so, see "Migrate from SQLite..." below.): 131 | ```bash 132 | yarn redwood prisma migrate dev 133 | ``` 134 | 135 | ### Migrate from SQLite to Postgres 136 | If you've already created migrations using SQLite, e.g. you have a migrations directory at `api/db/migrations`, follow this two-step process. 137 | 138 | #### 1. Remove existing migrations 139 | **For Linux and Mac OS** 140 | From your project root directory, run either command corresponding to your OS. 141 | ```bash 142 | rm -rf api/db/migrations 143 | ``` 144 | 145 | **For Windows OS** 146 | ```bash 147 | rmdir /s api\db\migrations 148 | ``` 149 | 150 | > Note: depending on your project configuration, your migrations may instead be located in `api/prisma/migrations` 151 | 152 | #### 2. Create a new migration 153 | Run this command to create and apply a new migration to your local Postgres DB: 154 | ```bash 155 | yarn redwood prisma migrate dev 156 | ``` 157 | 158 | ## DB Management Tools 159 | Here are our recommendations in case you need a tool to manage your databases: 160 | - [TablePlus](https://tableplus.com/) (Mac, Windows) 161 | - [Beekeeper Studio](https://www.beekeeperstudio.io/) (Linux, Mac, Windows - Open Source) 162 | -------------------------------------------------------------------------------- /docs/mockGraphQLRequests.md: -------------------------------------------------------------------------------- 1 | # Mocking GraphQL requests 2 | 3 | Testing and building components without having to rely on the API is a good best practice. Redwood makes this possible via `mockGraphQLQuery` and `mockGraphQLMutation`. 4 | 5 | The argument signatures of these functions are identical. Internally, they target different operation types based on their suffix. 6 | 7 | ```js 8 | mockGraphQLQuery('OperationName', (variables, { ctx, req }) => { 9 | ctx.delay(1500) // pause for 1.5 seconds 10 | return { 11 | userProfile: { 12 | id: 42, 13 | name: 'peterp', 14 | } 15 | } 16 | }) 17 | ``` 18 | 19 | ## The operation name 20 | 21 | The first argument is the [operation name](https://graphql.org/learn/queries/#operation-name); it's used to associate mock-data with a query or a mutation: 22 | 23 | ```js 24 | query UserProfileQuery { /*...*/ } 25 | mockGraphQLQuery('UserProfileQuery', { /*... */ }) 26 | ``` 27 | 28 | ```js 29 | mutation SetUserProfile { /*...*/ } 30 | mockGraphQLMutation('SetUserProfile', { /*... */ }) 31 | ``` 32 | 33 | Operation names should be unique. 34 | 35 | ## The mock-data 36 | 37 | The second argument can be an object or a function: 38 | 39 | ```js{1} 40 | mockGraphQLQuery('OperationName', (variables, { ctx }) => { 41 | ctx.delay(1500) // pause for 1.5 seconds 42 | return { 43 | userProfile: { 44 | id: 42, 45 | name: 'peterp', 46 | } 47 | } 48 | }) 49 | ``` 50 | 51 | If it's a function, it'll receive two arguments: `variables` and `{ ctx }`. The `ctx` object allows you to make adjustments to the response with the following functions: 52 | 53 | - `ctx.status(code: number, text?: string)`: set a http response code: 54 | 55 | ```js{2} 56 | mockGraphQLQuery('OperationName', (_variables, { ctx }) => { 57 | ctx.status(404) 58 | }) 59 | ``` 60 | 61 |
    62 | 63 | - `ctx.delay(numOfMS)`: delay the response 64 | 65 | ```js{2} 66 | mockGraphQLQuery('OperationName', (_variables, { ctx }) => { 67 | ctx.delay(1500) // pause for 1.5 seconds 68 | return { id: 42 } 69 | }) 70 | ``` 71 | 72 |
    73 | 74 | - `ctx.errors(e: GraphQLError[])`: return an error object in the response: 75 | 76 | ```js{2} 77 | mockGraphQLQuery('OperationName', (_variables, { ctx }) => { 78 | ctx.errors([{ message: 'Uh, oh!' }]) 79 | }) 80 | ``` 81 | 82 | ## Global mock-requests vs local mock-requests 83 | 84 | Placing your mock-requests in `".mock.js"` will cause them to be globally scoped in Storybook, making them available to all stories. 85 | 86 | > **All stories?** 87 | > 88 | > In React, it's often the case that a single component will have a deeply nested component that perform a GraphQL query or mutation. Having to mock those requests for every story can be painful and tedious. 89 | 90 | Using `mockGraphQLQuery` or `mockGraphQLMutation` inside a story is locally scoped and will overwrite a globally-scoped mock-request. 91 | 92 | We suggest always starting with globally-scoped mocks. 93 | 94 | ## Mocking a Cell's `QUERY` 95 | 96 | To mock a Cell's `QUERY`, find the file ending with with `.mock.js` in your Cell's directory. This file exports a value named `standard`, which is the mock-data that will be returned for your Cell's `QUERY`. 97 | 98 | ```js{4,5,6,12,13,14} 99 | // UserProfileCell/UserProfileCell.js 100 | export const QUERY = gql` 101 | query UserProfileQuery { 102 | userProfile { 103 | id 104 | } 105 | } 106 | ` 107 | 108 | // UserProfileCell/UserProfileCell.mock.js 109 | export const standard = { 110 | userProfile: { 111 | id: 42 112 | } 113 | } 114 | ``` 115 | 116 | Since the value assigned to `standard` is the mock-data associated with the `QUERY`, modifying the `QUERY` means you also need to modify the mock-data. 117 | 118 | ```diff 119 | // UserProfileCell/UserProfileCell.js 120 | export const QUERY = gql` 121 | query UserProfileQuery { 122 | userProfile { 123 | id 124 | + name 125 | } 126 | } 127 | ` 128 | 129 | // UserProfileCell/UserProfileCell.mock.js 130 | export const standard = { 131 | userProfile: { 132 | id: 42, 133 | + name: 'peterp', 134 | } 135 | } 136 | ``` 137 | 138 | > **Behind the scenes** 139 | > 140 | > Redwood uses the value associated with `standard` as the second argument to `mockGraphQLQuery`. 141 | -------------------------------------------------------------------------------- /docs/prerender.md: -------------------------------------------------------------------------------- 1 | # Prerender 2 | 3 | Some of your pages don't have dynamic content; it'd be great if you could render them ahead of time, making for a faster experience for your end users. 4 | 5 | We thought a lot about what the developer experience should be for route-based prerendering. The result is one of the smallest APIs imaginable! 6 | 7 | > **How's Prerendering different from SSR/SSG/SWR/ISSG/...?** 8 | > 9 | > As Danny said in his [Prerender demo](https://www.youtube.com/watch?v=iorKyMlASZc&t=2844s) at our Community Meetup, the thing all of these have in common is that they render your markup in a Node.js context to produce HTML. The difference is when (build or runtime) and how often. 10 | 11 | 12 | 13 | ## Prerendering a Page 14 | 15 | Prerendering a page is as easy as it gets. Just add the `prerender` prop to the Route that you want to prerender: 16 | 17 | ```js{3} 18 | // Routes.js 19 | 20 | 21 | ``` 22 | 23 | Then run `yarn rw build` and enjoy the performance boost! 24 | 25 | 26 | 27 | 28 | ### Prerendering all pages in a Set 29 | 30 | Just add the `prerender` prop to the Set that wraps all Pages you want to prerender: 31 | 32 | ```js{3} 33 | // Routes.js 34 | 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | ### Not found page 42 | 43 | You can also prerender your not found page (a.k.a your 404 page). Just add—you guessed it—the `prerender` prop: 44 | 45 | ```diff 46 | - 47 | + 48 | ``` 49 | 50 | This will prerender your NotFoundPage to `404.html` in your dist folder. Note that there's no need to specify a path. 51 | 52 | ## Cells, Private Routes, and Dynamic URLs 53 | 54 | How does Prerendering handle dynamic data? For Cells, Redwood prerenders your Cells' `` component. Similarly, for Private Routes, Redwood prerenders your Private Routes' `whileLoadingAuth` prop: 55 | 56 | ```js{1,2} 57 | 58 | // Loading is shown while we're checking to see if the user's logged in 59 | } prerender/> 60 | 61 | ``` 62 | 63 | Right now prerendering won't work for dynamic URLs. We're working on this. If you try to prerender one of them, nothing will break, but nothing happens. 64 | 65 | ```js 66 | // web/src/Routes.js 67 | 68 | 69 | ``` 70 | 71 | ## Prerender Utils 72 | 73 | Sometimes you need more fine-grained control over whether something gets prerendered. This may be because the component or library you're using needs access to browser APIs like `window` or `localStorage`. Redwood has three utils to help you handle these situations: 74 | 75 | - `` 76 | - `useIsBrowser` 77 | - `isBrowser` 78 | 79 | > **Heads-up!** 80 | > 81 | > If you're prerendering a page that uses a third-party library, make sure it's "universal". If it's not, try calling the library after doing a browser check using one of the utils above. 82 | > 83 | > Look for these key words when choosing a library: _universal module, SSR compatible, server compatible_—all these indicate that the library also works in Node.js. 84 | 85 | ### `` component 86 | 87 | This higher-order component is great for JSX: 88 | 89 | ```jsx 90 | import { BrowserOnly } from '@redwoodjs/prerender/browserUtils' 91 | 92 | const MyFancyComponent = () => { 93 |

    👋🏾 I render on both the server and the browser

    94 | 95 |

    🙋‍♀️ I only render on the browser

    96 |
    97 | } 98 | ``` 99 | 100 | ### `useIsBrowser` hook 101 | 102 | If you prefer hooks, you can use the `useIsBrowser` hook: 103 | 104 | ```jsx 105 | import { useIsBrowser } from '@redwoodjs/prerender/browserUtils' 106 | 107 | const MySpecialComponent = () => { 108 | const browser = useIsBrowser() 109 | 110 | return ( 111 |
    112 |

    Render info:

    113 | 114 | {browser ?

    Browser

    :

    Prerendered

    } 115 |
    116 | ) 117 | } 118 | ``` 119 | 120 | ### `isBrowser` boolean 121 | 122 | If you need to guard against prerendering outside React, you can use the `isBrowser` boolean. This is especially handy when running initializing code that only works in the browser: 123 | 124 | ```js 125 | import { isBrowser } from '@redwoodjs/prerender/browserUtils' 126 | 127 | if (isBrowser) { 128 | netlifyIdentity.init() 129 | } 130 | ``` 131 | 132 | ### Optimization Tip 133 | 134 | If you dynamically load third-party libraries that aren't part of your JS bundle, using these prerendering utils can help you avoid loading them at build time: 135 | 136 | ```js 137 | import { useIsBrowser } from '@redwoodjs/prerender/browserUtils' 138 | 139 | const ComponentUsingAnExternalLibrary = () => { 140 | const browser = useIsBrowser() 141 | 142 | // if `browser` evaluates to false, this won't be included 143 | if (browser) { 144 | loadMyLargeExternalLibrary() 145 | } 146 | 147 | return ( 148 | // ... 149 | ) 150 | ``` 151 | 152 | ### Debugging 153 | 154 | If you just want to debug your app, or check for possible prerendering errors, after you've built it, you can run this command: 155 | 156 | ```terminal 157 | yarn rw prerender --dry-run 158 | ``` 159 | 160 | Since we just shipped this in v0.26, we're actively looking for feedback! Do let us know if: everything built ok? you encountered specific libraries that you were using that didn’t work? 161 | 162 | ## Images and Assets 163 | 164 | 165 | 166 | Images and assets continue to work the way they used to. For more, see [this doc](https://redwoodjs.com/docs/assets-and-files). 167 | 168 | Note that there's a subtlety in how SVGs are handled. Importing an SVG and using it in a component works great: 169 | 170 | ```js{1} 171 | import logo from './my-logo.svg' 172 | 173 | function Header() { 174 | return 175 | } 176 | ``` 177 | 178 | But re-exporting the SVG as a component requires a small change: 179 | 180 | ```js 181 | // ❌ due to how Redwood handles SVGs, this syntax isn't supported. 182 | import Logo from './Logo.svg' 183 | export default Logo 184 | ``` 185 | 186 | ```js 187 | // ✅ use this instead. 188 | import Logo from './Logo.svg' 189 | 190 | const LogoComponent = () => 191 | 192 | export default LogoComponent 193 | ``` 194 | 195 | ## Configuring redirects 196 | 197 | Depending on what pages you're prerendering, you may want to change your redirect settings. Using Netlify as an example: 198 | 199 |
    200 | If you prerender your `notFoundPage` 201 | 202 | 203 | You can remove the default redirect to index in your `netlify.toml`. This means the browser will accurately receive 404 statuses when navigating to a route that doesn't exist: 204 | 205 | ```diff 206 | [[redirects]] 207 | - from = "/*" 208 | - to = "/index.html" 209 | - status = 200 210 | ``` 211 | 212 |
    213 | 214 |
    215 | 216 | If you don't prerender your 404s, but prerender all your other pages 217 | You can add a 404 redirect if you want: 218 | 219 | ```diff 220 | [[redirects]] 221 | from = "/*" 222 | to = "/index.html" 223 | - status = 200 224 | + status = 404 225 | ``` 226 | 227 |
    228 | 229 | ## Flash after page load 230 | 231 | > We're actively working preventing these flashes with upcoming changes to the Router. 232 | 233 | You might notice a flash after page load. A quick workaround for this is to make sure whatever page you're seeing the flash on isn't code split. You can do this by explicitly importing the page in `Routes.js`: 234 | 235 | ```js 236 | import { Router, Route } from '@redwoodjs/router' 237 | import HomePage from 'src/pages/HomePage' 238 | 239 | const Routes = () => { 240 | return ( 241 | 242 | 243 | 244 | 245 | 246 | ) 247 | } 248 | 249 | export default Routes 250 | ``` 251 | -------------------------------------------------------------------------------- /docs/projectConfiguration.md: -------------------------------------------------------------------------------- 1 | # Project Configuration: Dev, Test, Build 2 | 3 | ## Babel 4 | 5 | Out of the box Redwood configures [Babel](https://babeljs.io/) so that you can write modern JavaScript and TypeScript without needing to worry about transpilation at all. 6 | GraphQL tags, JSX, SVG imports—all of it's handled for you. 7 | 8 | For those well-versed in Babel config, you can find Redwood's in [@redwoodjs/internal](https://github.com/redwoodjs/redwood/tree/main/packages/internal/src/build/babel). 9 | 10 | ### Configuring Babel 11 | 12 | For most projects, you won't need to configure Babel at all, but if you need to you can configure each side (web, api) individually using side-specific `babel.config.js` files. 13 | 14 | > **Heads up** 15 | > 16 | > `.babelrc{.js}` files are ignored. 17 | > You have to put your custom config in the appropriate side's `babel.config.js`: `web/babel.config.js` for web and `api/babel.config.js` for api. 18 | 19 | Let's go over an example. 20 | 21 | #### Example: Adding Emotion 22 | 23 | Let's say we want to add the styling library [emotion](https://emotion.sh), which requires adding a Babel plugin. 24 | 25 | 1. Create a `babel.config.js` file in `web`: 26 | ```shell 27 | touch web/babel.config.js 28 | ``` 29 |
    30 | 31 | 2. Add the `@emotion/babel-plugin` as a dependency: 32 | ```shell 33 | yarn workspace web add --dev @emotion/babel-plugin 34 | ``` 35 |
    36 | 37 | 3. Add the plugin to `web/babel.config.js`: 38 | ```js 39 | // web/babel.config.js 40 | 41 | module.exports = { 42 | plugins: ["@emotion"] // 👈 add the emotion plugin 43 | } 44 | 45 | // ℹ️ Notice how we don't need the `extends` property 46 | ``` 47 | 48 | That's it! 49 | Now your custom web-side Babel config will be merged with Redwood's. 50 | 51 | ## Jest 52 | 53 | Redwood uses [Jest](https://jestjs.io/) for testing. 54 | Let's take a peek at how it's all configured. 55 | 56 | At the root of your project is `jest.config.js`. 57 | It should look like this: 58 | 59 | ```js 60 | // jest.config.js 61 | 62 | module.exports = { 63 | rootDir: '.', 64 | projects: ['/{*,!(node_modules)/**/}/jest.config.js'], 65 | } 66 | ``` 67 | 68 | This just tells Jest that the actual config files sit in each side, allowing Jest to pick up the individual settings for each. 69 | `rootDir` also makes sure that if you're running Jest with the `--collectCoverage` flag, it'll produce the report in the root directory. 70 | 71 | #### Web Jest Config 72 | 73 | The web side's configuration sits in `./web/jest.config.js` 74 | 75 | ```js 76 | const config = { 77 | rootDir: '../', 78 | preset: '@redwoodjs/testing/config/jest/web', 79 | // ☝️ load the built-in Redwood Jest configuration 80 | } 81 | 82 | module.exports = config 83 | ``` 84 | 85 | > You can always see Redwood's latest configuration templates in the [create-redwood-app package](https://github.com/redwoodjs/redwood/blob/main/packages/create-redwood-app/template/web/jest.config.js). 86 | 87 | The preset includes all the setup required to test everything that's going on in web: rendering React components and transforming JSX, automatically mocking Cells, transpiling with Babel, mocking the Router and the GraphQL client—the list goes on! 88 | You can find all the details in the [source](https://github.com/redwoodjs/redwood/blob/main/packages/testing/config/jest/web/jest-preset.js). 89 | 90 | #### Api Side Config 91 | 92 | The api side is configured similarly, with the configuration sitting in `./api/jest.config.js`. 93 | But the api preset is slightly different in that: 94 | 95 | - it's configured to run tests serially (because Scenarios seed your test database) 96 | - it has setup code to make sure your database is 1) seeded before running tests 2) reset between Scenarios 97 | 98 | You can find all the details in the [source](https://github.com/redwoodjs/redwood/blob/main/packages/testing/config/jest/api/jest-preset.js). 99 | 100 | ## GraphQL Codegen 101 | 102 | Redwood uses [GraphQL Code Generator](https://www.graphql-code-generator.com) to generate types for your GraphQL queries and mutations. 103 | 104 | While the defaults are configured so that things JustWork™️, you can customize them by adding a `./codegen.yml` file to the root of your project. 105 | Your custom settings will be merged with the built-in ones. 106 | 107 | > If you're curious about the built-in settings, they can be found [here](https://github.com/redwoodjs/redwood/blob/main/packages/internal/src/generate/typeDefinitions.ts) in the Redwood source. Look for the `generateTypeDefGraphQLWeb` and `generateTypeDefGraphQLApi` functions. 108 | 109 | For example, adding this `codegen.yml` to the root of your project will transform all the generated types to UPPERCASE: 110 | 111 | ```yml 112 | # ./codegen.yml 113 | 114 | config: 115 | namingConvention: 116 | typeNames: change-case-all#upperCase 117 | ``` 118 | 119 | For completeness, [here's the docs](https://www.graphql-code-generator.com/docs/config-reference/config-field) on configuring GraphQL Code Generator. 120 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | >RedwoodJS requires [Node.js](https://nodejs.org/en/) (>=14.x <=16.x) and [Yarn](https://classic.yarnpkg.com/en/docs/install/) (>=1.15). 4 | 5 | Run the following command to create a new Redwood project in a "my-redwood-app" project directory. 6 | ``` 7 | yarn create redwood-app my-redwood-app 8 | ``` 9 | Start the development server: 10 | ``` 11 | cd my-redwood-app 12 | yarn redwood dev 13 | ``` 14 | A browser should automatically open to http://localhost:8910 and you will see the Redwood welcome page. 15 | 16 | ## The Redwood CLI 17 | 18 | The Redwood developer experience relies heavily on the Redwood CLI. It's installed as a dependency when you create a new redwood-app, and is run locally in your app. 19 | 20 | The following will show all the available commands in the Redwood CLI (note: rw is alias of redwood): 21 | ``` 22 | yarn rw --help 23 | ``` 24 | 25 | Some commands, like [prisma](https://redwoodjs.com/docs/cli-commands#db), have a lot of options. You can dig further into a specific command by adding `--help` to the command like so: 26 | ``` 27 | yarn rw prisma --help 28 | ``` 29 | 30 | Take a visit to the [CLI Doc](https://redwoodjs.com/docs/cli-commands.html) to see detailed information on all commands and options. 31 | 32 | ## Generators 33 | 34 | Redwood generators make monotonous developer tasks a breeze. Creating all the boilerplate code required for CRUD operations on a model can be accomplished with a few commands. Three to be exact. 35 | 36 | Every new Redwood project comes with a default Model called UserExample in `api/db/schema.prisma` (ignore the rest of the file for now, it's for more advanced configuration data). 37 | 38 | ``` 39 | model UserExample { 40 | id Int @id @default(autoincrement()) 41 | email String @unique 42 | name String? 43 | } 44 | ``` 45 | 46 | With only two commands, Redwood will create everything we need for our CRUD operations: 47 | ``` 48 | yarn rw prisma migrate dev 49 | yarn rw generate scaffold UserExample 50 | ``` 51 | 52 | What exactly just happened? Glad you asked. 53 | 54 | - `yarn rw prisma migrate dev` creates and applies a snapshot of our UserExample model for our migration, creating a new table in our database called `UserExample` 55 | - `yarn rw generate scaffold UserExample` tells Redwood to create the necessary Pages, SDL, and Services for the given Model 56 | 57 | Just like that, we are done. No seriously. Visit http://localhost:8910/user-examples to see for yourself. 58 | 59 | Screen Shot 2020-10-21 at 6 28 08 PM 60 | 61 | Redwood has created everything we need to create, edit, delete and view a User. And you didn't have to write one line of boilerplate code. 62 | 63 | We have some other [generators](https://redwoodjs.com/docs/cli-commands#generate-alias-g) that are just as awesome, don't forget to check them out as well. 64 | 65 | ## Next Steps 66 | 67 | Need more? The best way to get to know Redwood is by going through the extensive [Redwood Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood). 68 | 69 | - Join our [Discord Server](https://discord.gg/redwoodjs) 70 | - Join our [Discourse Community](https://community.redwoodjs.com) 71 | -------------------------------------------------------------------------------- /docs/schemaRelations.md: -------------------------------------------------------------------------------- 1 | # Prisma Relations and Redwood's Generators 2 | 3 | These docs apply to Redwood v0.25 and greater. Previous versions of Redwood had limitations when creating scaffolds for any one-to-many or many-to-many relationships. Most of those have been resolved so you should definitely [upgrade to 0.25](https://community.redwoodjs.com/t/upgrading-to-redwoodjs-v0-25-and-prisma-v2-16-db-upgrades-and-project-code-mods/1811) if at all possible! 4 | 5 | ## Many-to-many Relationships 6 | 7 | [Here](https://www.prisma.io/docs/concepts/components/prisma-schema/relations#many-to-many-relations) 8 | are Prisma's docs for creating many-to-many relationships - A many-to-many 9 | relationship is accomplished by creating a "join" or "lookup" table between two 10 | other tables. For example, if a **Product** can have many **Tag**s, any given 11 | **Tag** can also have many **Product**s that it is attached to. A database 12 | diagram for this relationship could look like: 13 | 14 | ``` 15 | ┌───────────┐ ┌─────────────────┐ ┌───────────┐ 16 | │ Product │ │ ProductsOnTag │ │ Tag │ 17 | ├───────────┤ ├─────────────────┤ ├───────────┤ 18 | │ id │────<│ productId │ ┌──│ id │ 19 | │ title │ │ tagId │>──┘ │ name │ 20 | │ desc │ └─────────────────┘ └───────────┘ 21 | └───────────┘ 22 | ``` 23 | 24 | The `schema.prisma` syntax to create this relationship looks like: 25 | 26 | ```javascript 27 | model Product { 28 | id Int @id @default(autoincrement()) 29 | title String 30 | desc String 31 | tags Tag[] 32 | } 33 | 34 | model Tag { 35 | id Int @id @default(autoincrement()) 36 | name String 37 | products Product[] 38 | } 39 | ``` 40 | 41 | These relationships can be [implicit](https://www.prisma.io/docs/concepts/components/prisma-schema/relations#implicit-many-to-many-relations) (as this diagram shows) or [explicit](https://www.prisma.io/docs/concepts/components/prisma-schema/relations#explicit-many-to-many-relations) (explained below). Redwood's SDL generator (which is also used by the scaffold generator) only supports an **explicit** many-to-many relationship when generating with the `--crud` flag. What's up with that? 42 | 43 | ## CRUD Requires an `@id` 44 | 45 | CRUD (Create, Retrieve, Update, Delete) actions in Redwood currently require a single, unique field in order to retrieve, update or delete a record. This field must be denoted with Prisma's [`@id`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#id) attribute, marking it as the tables's primary key. This field is guaranteed to be unique and so can be used to find a specific record. 46 | 47 | Prisma's implicit many-to-many relationships create a table _without_ a single field marked with the `@id` attribute. Instead, it uses a similar attribute: [`@@id`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#id-1) to define a *multi-field ID*. This multi-field ID will become the tables's primary key. The diagram above shows the result of letting Prisma create an implicit relationship. 48 | 49 | Since there's no single `@id` field in implicit many-to-many relationships, you can't use the SDL generator with the `--crud` flag. Likewise, you can't use the scaffold generator, which uses the SDL generator (with `--crud`) behind the scenes. 50 | 51 | ## Supported Table Structure 52 | 53 | To support both CRUD actions and to remain consistent with Prisma's many-to-many relationships, a combination of the `@id` and [`@@unique`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#unique-1) attributes can be used. With this, `@id` is used to create a primary key on the lookup-table; and `@@unique` is used to maintain the table's unique index, which was previously accomplished by the primary key created with `@@id`. 54 | 55 | > Removing `@@unique` would let a specific **Product** reference a particular **Tag** more than a single time. 56 | 57 | You can get this working by creating an explicit relationship—defining the table structure yourself: 58 | 59 | ```javascript 60 | model Product { 61 | id Int @id @default(autoincrement()) 62 | title String 63 | desc String 64 | tags ProductsOnTag[] 65 | } 66 | 67 | model Tag { 68 | id Int @id @default(autoincrement()) 69 | name String 70 | products ProductsOnTag[] 71 | } 72 | 73 | model ProductsOnTag { 74 | id Int @id @default(autoincrement()) 75 | tagId Int 76 | tag Tag @relation(fields: [tagId], references: [id]) 77 | productId Int 78 | product Product @relation(fields: [productId], references: [id]) 79 | 80 | @@unique([tagId, productId]) 81 | } 82 | ``` 83 | 84 | Which creates a table structure like: 85 | 86 | ``` 87 | ┌───────────┐ ┌──────────────────┐ ┌───────────┐ 88 | │ Product │ │ ProductsOnTags │ │ Tag │ 89 | ├───────────┤ ├──────────────────┤ ├───────────┤ 90 | │ id │──┐ │ id │ ┌──│ id │ 91 | │ title │ └──<│ productId │ │ │ name │ 92 | │ desc │ │ tagId │>─┘ └───────────┘ 93 | └───────────┘ └──────────────────┘ 94 | 95 | ``` 96 | 97 | Almost identical! But now there's an `id` and the SDL/scaffold generators will work as expected. The explicit syntax gives you a couple additional benefits—you can customize the table name and even add more fields. Maybe you want to track which user tagged a product—add a `userId` column to `ProductsOnTags` and now you know. 98 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | RedwoodJS wants you to be able build and deploy secure applications and takes the topic of security seriously. 4 | 5 | * [RedwoodJS Security](https://github.com/redwoodjs/redwood/security) on GitHub 6 | * [CodeQL code scanning](https://github.com/features/security) 7 | * [Authentication](/docs/authentication) 8 | * [Webhook signature verification](/docs/webhooks) 9 | * [Ways to keep your serverless functions secure](/docs/serverless-functions#security-considerations) 10 | * [Environment variables for secure keys and tokens](/docs/environment-variables) 11 | 12 | > ⚠️ **Security is Your Responsibility** 13 | > While Redwood offers the tools, practices, and information to keep your application secure, it remains your responsibility to put these in place. Proper password, token, and key protection using disciplined communication, password management systems, and environment management services like [Doppler](https://www.doppler.com) are strongly encouraged. 14 | 15 | > **Security Policy and Contact Information** 16 | > The RedwoodJS Security Policy is located [in the codebase repository on GitHub](https://github.com/redwoodjs/redwood/security/policy) 17 | > 18 | > To report a potential security vulnerability, contact us at [security@redwoodjs.com](mailto:security@redwoodjs.com) 19 | ## Authentication 20 | 21 | `@redwoodjs/auth` is a lightweight wrapper around popular SPA authentication libraries. We [currently support](https://redwoodjs.com/docs/authentication) the following authentication providers: 22 | 23 | * Netlify Identity Widget 24 | * Auth0 25 | * Azure Active Directory 26 | * Netlify GoTrue-JS 27 | * Magic Links - Magic.js 28 | * Firebase's GoogleAuthProvider 29 | * Ethereum 30 | * Supabase 31 | * Nhost 32 | 33 | For example implementations, please see [Authentication](https://github.com/redwoodjs/redwood/tree/main/packages/auth) and the use of the `getCurrentUser` and `requireAuth` helpers. 34 | 35 | For a demonstration, check out the [Auth Playground](https://redwood-playground-auth.netlify.app). 36 | 37 | ## GraphQL 38 | 39 | GraphQL is a fundamental part of Redwood. For details on how Redwood uses GraphQL and handles important security considerations, please see the [GraphQL Security](/docs/graphql.html#security) section and the [Secure Services](/docs/services.html#secure-services) section. 40 | 41 | ### Depth Limits 42 | 43 | The RedwoodJS GraphQL handler sets [reasonable defaults](/docs/graphql.html##query-depth-limit) to prevent deep, cyclical nested queries that attackers often use to exploit systems. 44 | ### Disable Introspection and Playground 45 | 46 | Because both introspection and the playground share possibly sensitive information about your data model, your data, your queries and mutations, best practices for deploying a GraphQL Server call to [disable these in production](/docs/graphql.html#introspection-and-playground-disabled-in-production), RedwoodJS **only enables introspection and the playground when running in development**. 47 | ## Functions 48 | 49 | When deployed, a [serverless function](/docs/serverless-functions) is an open API endpoint. That means anyone can access it and perform any tasks it's asked to do. In many cases, this is completely appropriate and desired behavior. But there are often times you need to restrict access to a function, and Redwood can help you do that using a [variety of methods and approaches](/docs/serverless-functions#security-considerations). 50 | 51 | For details on how to keep your functions secure, please see the [Serverless functions & Security considerations](/docs/serverless-functions#security-considerations) section in the RedwoodJS documentation. 52 | 53 | ## Webhooks 54 | 55 | [Webhooks](/docs/webhooks) are a common way that third-party services notify your RedwoodJS application when an event of interest happens. 56 | 57 | They are a form of messaging or automation and allows web applications to communicate with each other and send real-time data from one application to another whenever a given event occurs. 58 | 59 | Since each of these webhooks will call a function endpoint in your RedwoodJS api, you need to ensure that these run **only when they should**. That means you need to: 60 | 61 | * Verify it comes from the place you expect 62 | * Trust the party 63 | * Know the payload sent in the hook hasn't been tampered with 64 | * Ensure that the hook isn't reprocessed or replayed 65 | 66 | For details on how to keep your incoming webhooks secure and how to sign your outgoing webhooks, please see [Webhooks](/docs/webhooks). 67 | -------------------------------------------------------------------------------- /docs/seo.md: -------------------------------------------------------------------------------- 1 | # SEO & Meta tags 2 | 3 | ## Add app title 4 | You certainly want to change the title of your Redwood app. 5 | You can start by adding or modify `title` inside `redwood.toml` 6 | 7 | ```diff 8 | [web] 9 | - title = "Redwood App" 10 | + title = "My Cool App" 11 | port = 8910 12 | apiUrl = "/.redwood/functions" 13 | ``` 14 | This title (the app title) is used by default for all your pages if you don't define another one. 15 | It will also be use for the title template ! 16 | ### Title template 17 | Now that you have the app title set, you probably want some consistence with the page title, that's what the title template is for. 18 | 19 | Add `titleTemplate` as a prop for `RedwoodProvider` to have a title template for every pages 20 | 21 | In _web/src/App.{tsx,js}_ 22 | ```diff 23 | - 24 | + 25 | /* ... */ 26 | 27 | ``` 28 | 29 | You can write the format you like. 30 | 31 | _Examples :_ 32 | ```js 33 | "%PageTitle | %AppTitle" => "Home Page | Redwood App" 34 | 35 | "%AppTitle · %PageTitle" => "Redwood App · Home Page" 36 | 37 | "%PageTitle : %AppTitle" => "Home Page : Redwood App" 38 | ``` 39 | 40 | So now in your page you only need to write the title of the page. 41 | 42 | ## Adding to page `` 43 | So you want to change the title of your page, or add elements to the `` of the page? We've got you! 44 | 45 | 46 | Let's say you want to change the title of your About page, 47 | Redwood provides a built in `` component, which you can use like this 48 | 49 | 50 | In _AboutPage/AboutPage.{tsx,js}_ 51 | ```diff 52 | +import { Head } from '@redwoodjs/web' 53 | 54 | const AboutPage = () => { 55 | return ( 56 |
    57 |

    AboutPage

    58 | + 59 | + About the team 60 | + 61 | ``` 62 | 63 | You can include any valid `` tag in here that you like, but just to make things easier we also have a utility component [MetaTags](#setting-meta-tags-open-graph-directives). 64 | 65 | ### What about nested tags? 66 | Redwood uses [react-helmet-async](https://github.com/staylor/react-helmet-async) underneath, which will use the tags furthest down your component tree. 67 | 68 | For example, if you set title in your Layout, and a title in your Page, it'll render the one in Page - this way you can override the tags you wish, while sharing the tags defined in Layout. 69 | 70 | 71 | > **Side note** 72 | > for these headers to appear to bots and scrapers e.g. for twitter to show your title, you have to make sure your page is prerendered 73 | > If your content is static you can use Redwood's built in [Prerender](/docs/prerender). For dynamic tags, check the [Dynamic head tags](#dynamic-tags) 74 | 75 | ## Setting meta tags / open graph directives 76 | Often we want to set more than just the title - most commonly to set "og" headers. Og standing for 77 | [open graph](https://ogp.me/) of course. 78 | 79 | Redwood provides a convenience component `` to help you get all the relevant tags with one go (but you can totally choose to do them yourself) 80 | 81 | Here's an example setting some common headers, including how to set an `og:image` 82 | ```js 83 | import { MetaTags } from '@redwoodjs/web' 84 | 85 | const AboutPage = () => { 86 | return ( 87 |
    88 |

    AboutPage

    89 | 97 |

    This is the about page!

    98 |
    99 | ) 100 | } 101 | 102 | export default AboutPage 103 | ``` 104 | 105 | This is great not just for link unfurling on say Facebook or Slack, but also for SEO. Take a look at the [source](https://github.com/redwoodjs/redwood/blob/main/packages/web/src/components/MetaTags.tsx#L83) if you're curious what tags get set here. 106 | 107 | 108 | ## Dynamic tags 109 | Great - so far we can see the changes, and bots will pick up our tags if we've prerendered the page, but what if I want to set the header based on the output of the Cell? 110 | 111 | _Just keep in mind, that Cells are currently not prerendered_ - so it'll be visible to your users, but not to link scrapers and bots. 112 | 113 | > **\s up**
    114 | > For dynamic tags to appear to bots and link scrapers you have to setup an external prerendering service. If you're on Netlify you can use their [built-in one](https://docs.netlify.com/site-deploys/post-processing/prerendering/). Otherwise you can follow [this great cookbook](https://community.redwoodjs.com/t/cookbook-getting-og-and-meta-tags-working-with-nginx-pre-render-io-and-docker/2014) from the Redwood community 115 | 116 | 117 | Let's say in our PostCell, we want to set the title to match the Post. 118 | ```js 119 | import Post from 'src/components/Post/Post' 120 | 121 | export const QUERY = gql` 122 | query FindPostById($id: Int!) { 123 | post: post(id: $id) { 124 | title 125 | snippet 126 | author { 127 | name 128 | } 129 | } 130 | } 131 | ` 132 | 133 | export const Loading = /* ... */ 134 | 135 | export const Empty = /* ... */ 136 | 137 | export const Success = ({ post }) => { 138 | return ( 139 | <> 140 | 145 | 146 | 147 | ) 148 | } 149 | ``` 150 | Once the success component renders, it'll update your page's title and set the relevant meta tags for you! 151 | -------------------------------------------------------------------------------- /docs/storybook.md: -------------------------------------------------------------------------------- 1 | # Storybook 2 | 3 | Storybook enables a kind of frontend-first, component-driven development workflow that we've always wanted. 4 | By developing your UI components in isolation, you get to focus exclusively on your UI's needs, 5 | saving you from getting too caught up in the details of your API too early. 6 | 7 | Storybook also makes debugging a lot easier. 8 | You don't have to start the dev server, login as a user, tab through dropdowns, and click buttons just for that one bug to show up. 9 | Or render a whole page and make six GraphQL calls just to change the color of a modal. 10 | You can set it all up as a story, tweak it there as you see fit, and even test it for good measure. 11 | 12 | ## Getting Started with Storybook 13 | 14 | You can start Storybook with `yarn rw storybook`: 15 | 16 | ``` 17 | yarn rw storybook 18 | ``` 19 | 20 | This spins up Storybook on port `7910`. 21 | 22 | ## Configuring Storybook 23 | 24 | You only have to configure Storybook if you want to extend Redwood's default configuration, which handles things like how to find stories, configuring Webpack, starting Mock Service Worker, etc. 25 | 26 | There are three files you can add to your project's `web/config` directory to configure Storybook: `storybook.config.js`, `storybook.manager.js`, and `storybook.preview.js`. Note that you may have to create the `web/config` directory: 27 | 28 | ``` 29 | cd redwood-project/web 30 | mkdir config 31 | touch config/storybook.config.js config/storybook.manager.js config/storybook.preview.js 32 | ``` 33 | 34 | `storybook.config.js` configures Storybook's server, `storybook.manager.js` configures Storybook's UI, and `storybook.preview.js` configures the way stories render. 35 | All of these files get merged with Redwood's default configurations, which you can find in the `@redwoodjs/testing` package: 36 | 37 | - [main.js](https://github.com/redwoodjs/redwood/blob/main/packages/testing/config/storybook/main.js)—gets merged with your project's `storybook.config.js` 38 | - [manager.js](https://github.com/redwoodjs/redwood/blob/main/packages/testing/config/storybook/manager.js)—gets merged with your project's `storybook.manager.js` 39 | - [preview.js](https://github.com/redwoodjs/redwood/blob/main/packages/testing/config/storybook/preview.js)—gets merged with your project's `storybook.preview.js` 40 | 41 | ### Configuring the Server with `storybook.config.js` 42 | 43 | > Since `storybook.config.js` configures Storybook's server, note that any changes you make require restarting Storybook. 44 | 45 | While you can configure [any of Storybook server's available options](https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project) in `storybook.config.js`, you'll probably only want to configure `addons`: 46 | 47 | ```js 48 | // web/config/storybook.config.js 49 | 50 | module.exports = { 51 | /** 52 | * This line adds all of Storybook's essential addons. 53 | * 54 | * @see {@link https://storybook.js.org/addons/tag/essentials} 55 | */ 56 | addons: ['@storybook/addon-essentials'], 57 | } 58 | ``` 59 | 60 | ### Configuring Rendering with `storybook.preview.js` 61 | 62 | Sometimes you want to change the way all your stories render. 63 | It'd be mixing concerns to add that logic to your actual components, and it'd get old fast to add it to every single `.stories.js` file. 64 | Instead decorate all your stories with any custom rendering logic you want in `storybook.preview.js`. 65 | 66 | For example, something you may want to do is add some margin to all your stories so that they're not glued to the top left corner: 67 | 68 | ```js 69 | // web/config/storybook.preview.js 70 | 71 | export const decorators = [ 72 | (Story) => ( 73 |
    74 | 75 |
    76 | ), 77 | ] 78 | ``` 79 | 80 | For more, see the Storybook docs on [configuring how stories render](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering). 81 | 82 | ### Configuring the UI with `storybook.manager.js` 83 | 84 | > Note that some of the changes you make to Storybook's UI require refreshing its cache. 85 | > The easiest way to do so is when starting Storybook: 86 | > 87 | > ``` 88 | > yarn rw storybook --no-manager-cache 89 | > ``` 90 | 91 | You can [theme Storybook's UI](https://storybook.js.org/docs/react/configure/theming) by installing two packages and making a few changes to Redwood's initial configuration. 92 | 93 | From the root of your RedwoodJS project: 94 | 95 | ``` 96 | yarn workspace web add -D @storybook/addons @storybook/theming 97 | ``` 98 | 99 | Then, we'll configure our theme by creating a `storybook.manager.js` file. Below we're enabling Storybook's dark theme. 100 | 101 | ```js 102 | // web/config/storybook.manager.js 103 | 104 | import { addons } from '@storybook/addons' 105 | import { themes } from '@storybook/theming' 106 | 107 | addons.setConfig({ 108 | theme: themes.dark, 109 | }) 110 | ``` 111 | 112 | Check out [Storybook's theming quickstart](https://storybook.js.org/docs/react/configure/theming#create-a-theme-quickstart) for a guide on creating your own theme. You may also want to export your theme to [re-use it with Storybook Docs](https://storybook.js.org/docs/react/configure/theming#theming-docs). 113 | -------------------------------------------------------------------------------- /docs/toastNotifications.md: -------------------------------------------------------------------------------- 1 | # Toast Notifications 2 | 3 | > Deprecation Warning: In RedwoodJS v0.27, the custom Flash Messaging was replaced with React Hot Toast. Flash, implemented with `import { useFlash } from '@redwoodjs/web'` will be deprecated in Redwood v1. If you are currently using `` and `useFlash`, you can update your app [via these instructions](https://community.redwoodjs.com/t/redwood-flash-is-being-replaced-with-react-hot-toast-how-to-update-your-project-v0-27-0/1921). 4 | 5 | Did you know that those little popup notifications that you sometimes see at the top of pages after you've performed an action are affectionately known as "toast" notifications? Because they pop up like a piece of toast from a toaster! 6 | 7 | ![Example Toast Animation](https://user-images.githubusercontent.com/300/110032806-71024680-7ced-11eb-8d69-7f462929815e.gif) 8 | 9 | Redwood supports these notifications out of the box thanks to the [react-hot-toast](https://react-hot-toast.com/) package. 10 | 11 | ## Usage 12 | 13 | This doc will not cover everything you can do with toasts, and the [react-hot-toast docs](https://react-hot-toast.com/docs) are very thorough. But here are a couple of common use cases. 14 | 15 | ### Displaying a Toast 16 | 17 | Wherever you want your notifications to be output, include the **<Toaster>** component: 18 | 19 | ```javascript 20 | import { Toaster } from '@redwoodjs/web/toast' 21 | 22 | const HomePage = () => { 23 | return ( 24 |
    25 | 26 | 27 |
    28 | ) 29 | } 30 | 31 | export default HomePage 32 | ``` 33 | 34 | **<Toaster>** accepts several options, including placement options: 35 | 36 | * top-left 37 | * top-center 38 | * top-right 39 | * bottom-left 40 | * bottom-center 41 | * bottom-right 42 | 43 | and a delay for how long to show each type of notification: 44 | 45 | ```javascript 46 | 50 | ``` 51 | 52 | See the [official Toaster docs](https://react-hot-toast.com/docs/toaster) for more options. There's also a [dedicated doc for styling](https://react-hot-toast.com/docs/styling). 53 | 54 | ### Triggering a Toast 55 | 56 | To show a toast message, just include a call to the `toast` object: 57 | 58 | ```javascript 59 | import { toast } from '@redwoodjs/web/toast' 60 | 61 | const UserForm = () => { 62 | onSubmit: () => { 63 | // code to save a record 64 | toast.success('User created!') 65 | } 66 | 67 | return ( 68 | // Component JSX 69 | ) 70 | }) 71 | ``` 72 | 73 | There are different "types" of toasts, by default each shows a different icon to indicate that type: 74 | 75 | * `toast()` - Text only, no icon shown 76 | * `toast.success()` - Checkmark icon with text 77 | * `toast.error()` - X icon with text 78 | * `toast.loading()` - Spinner icon, will show for 30 seconds by default, or until dismissed via `toast.dismiss(toastId)` 79 | * `toast.promise()` - Spinner icon, displays until the Promise resolves 80 | 81 | Check out the [full docs on `toast()`](https://react-hot-toast.com/docs/toast) for more options and usage examples. 82 | 83 | ## Generators 84 | 85 | If you generate a scaffold, you will get toast notifications automatically for the following actions: 86 | 87 | * Creating a new record 88 | * Editing an existing record 89 | * Deleting a record 90 | -------------------------------------------------------------------------------- /docs/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | Redwood comes with full TypeScript support out of the box, and you don't have to give up any of the conveniences that Redwood offers to enjoy all the benefits of a type-safe codebase. You can use TypeScript and have great DX too. 3 | 4 | ## Starting a TypeScript Redwood project 5 | You can use the `--typescript` flag on create-redwood-app to generate a project with TypeScript configured: 6 | ```shell 7 | yarn create redwood-app --typescript my-redwood-app 8 | ``` 9 | 10 | ## Converting an existing JS project to TypeScript 11 | If you already have a Redwood app, but want to configure it for TypeScript, you can use our setup command: 12 | 13 | ``` 14 | yarn rw setup tsconfig 15 | ``` 16 | Remember you don't _need_ to convert all your files to TypeScript, you can always do it incrementally. Start by renaming your files from `.js` to `.ts` or `.tsx` 17 | 18 | Having issues with automatic setup? See instructions for manual setup below: 19 | 20 |
    21 | Manually setup TypeScript 22 | 23 | This is what the setup command does for you step by step: 24 | 25 | **API** 26 | 27 | 1. Create a `./api/tsconfig.json` file: 28 | 29 | ```shell 30 | touch api/tsconfig.json 31 | ``` 32 | 33 |
    34 | 35 | 2. Now copy and paste the latest config from the Redwood template [api/tsconfig.json](https://github.com/redwoodjs/redwood/blob/main/packages/create-redwood-app/template/api/tsconfig.json) file here 36 | 37 | **WEB** 38 | 39 | 1. Create a `./api/tsconfig.json` file: 40 | 41 | ```shell 42 | touch web/tsconfig.json 43 | ``` 44 | 45 |
    46 | 47 | 2. Now copy and paste the latest config from the Redwood template [web/tsconfig.json](https://github.com/redwoodjs/redwood/blob/main/packages/create-redwood-app/template/web/tsconfig.json) file here 48 | 49 | 50 | You should now have type definitions—you can rename your files from `.js` to `.ts`, and the files that contain JSX to `.tsx`. 51 |
    52 | 53 | ## Sharing Types between sides 54 | For your shared types, we need to do a few things: 55 | 56 | 1. Put your shared types at the root of the project (makes sense right?), in a folder called `types` at the root 57 | 2. Run 'Restart TS Server' in vscode via the command palette. And your new types should now be available on both web and api sides! 58 | 59 | Redwood's tsconfig already contains the config for picking up types from `web/types`, `api/types` and `types` folders in your project. 60 | 61 | If you have an outdated tsconfig, and would like to replace it (i.e. overwrite it), use the setup command with the force flag 62 | 63 | ``` 64 | yarn rw setup tsconfig --force 65 | ``` 66 | 67 | ## Running type checks 68 | 69 | Redwood uses Babel to transpile your TypeScript - which is why you are able to incrementally convert your project from JS to TS. However, it also means that just doing a build won't show you errors that the TypeScript compiler finds in your project

    That's why we have the handy `redwood type-check` command! 70 | 71 | To check your TypeScript project for errors, run 72 | ``` 73 | yarn rw type-check 74 | ``` 75 | This will run `tsc` on all the sides in your project, and make sure all the generated types are generated first, including Prisma. 76 | 77 | 78 | ### Check then build, on CI 79 | > **Tip!**
    80 | > You don't need to build your project to run `rw type-check` 81 | 82 | If your project is fully TypeScript, it might be useful to add typechecks before you run the deploy command in your CI. 83 | 84 | For example, if you're deploying to Vercel, you could add a `build:ci` script to your `package.json` 85 | ```diff 86 | "scripts": { 87 | . 88 | . 89 | + "build:ci": "yarn rw type-check && yarn rw test --no-watch && yarn rw deploy vercel", 90 | } 91 | ``` 92 | Configure your project's build command to be `yarn build:ci` - and you even have your type checks and tests run whenever a pull request is opened! 93 | 94 | 95 | 96 | 97 | 98 | ## Auto generated types 99 | Redwood's CLI automatically generates types for you, which not only includes types for your GraphQL queries, but also for your named routes, Cells, scenarios and tests. 100 | 101 | When you run `yarn rw dev`, the CLI watches for file changes and automatically triggers the type generator. 102 | 103 | To trigger type generation, you can run: 104 | ```shell 105 | yarn rw g types 106 | ``` 107 | 108 | 109 | If you're curious, you can see the generated types in the `.redwood/types` folder, and in `./api/types/graphql.d.ts` and `./web/types/graphql.d.ts` in your project 110 | -------------------------------------------------------------------------------- /docs/webpackConfiguration.md: -------------------------------------------------------------------------------- 1 | # Webpack Configuration 2 | 3 | Redwood uses webpack. And with webpack comes configuration. 4 | 5 | One of Redwood's tenets is convention over configuration, so it's worth repeating that you don't have to do any of this! 6 | Take the golden path and everything will work just fine. 7 | 8 | But another of Redwood's tenets is to make the hard stuff possible. 9 | Whether configuring webpack counts as hard-stuff or not is up for debate, but one thing we know for sure is that it can be an epic time sink. 10 | We hope that documenting it well makes it fast and easy. 11 | 12 | ## Configuring Webpack 13 | 14 | The best way to start configuring webpack is with the webpack setup command: 15 | 16 | ```bash 17 | yarn rw setup webpack 18 | ``` 19 | 20 | This command adds a file called `webpack.config.js` to your project's `web/config` directory, creating `web/config` if it doesn't exist: 21 | 22 | ```js 23 | // web/config/webpack.config.js 24 | 25 | module.exports = (config, { mode }) => { 26 | if (mode === 'development') { 27 | /** 28 | * Add a development-only plugin 29 | */ 30 | } 31 | 32 | /** 33 | * Add custom rules and plugins: 34 | * 35 | * ``` 36 | * config.module.rules.push(YOUR_RULE) 37 | * config.plugins.push(YOUR_PLUGIN) 38 | * ``` 39 | */ 40 | 41 | /** 42 | * And make sure to return the config! 43 | */ 44 | return config 45 | } 46 | ``` 47 | 48 | This file exports a function that gets passed two arguments: `config`, which is Redwood's webpack configuration, and an object with the property `mode`, which is either `'development'` or `'production'`. 49 | 50 | In this function, you can add custom rules and plugins or modify Redwood's webpack configuration, which you can find in `@redwoodjs/core`. 51 | Redwood has a common webpack configuration that gets merged with others depending on your project's environment (i.e. development or production): 52 | - [webpack.common.js](https://github.com/redwoodjs/redwood/blob/main/packages/core/config/webpack.common.js)—the common configuration; does most of the leg work 53 | - [webpack.development.js](https://github.com/redwoodjs/redwood/blob/main/packages/core/config/webpack.development.js)—used when you start the dev server (`yarn rw dev`) 54 | - [webpack.production.js](https://github.com/redwoodjs/redwood/blob/main/packages/core/config/webpack.production.js)—used when you build the web side (`yarn rw build web`) 55 | 56 | ### Sass 57 | 58 | Redwood comes configured with support for Sass—all you have to do is install dependencies: 59 | 60 | ```bash 61 | yarn workspace web add -D sass sass-loader 62 | ``` 63 | 64 | ### Tailwind CSS 65 | 66 | Configuring webpack just to use Tailwind CSS? Don't! Use the setup command instead: 67 | 68 | ``` 69 | yarn rw setup ui tailwindcss 70 | ``` 71 | 72 | ## Webpack Dev Server 73 | 74 | Redwood uses [Webpack Dev Server](https://webpack.js.org/configuration/dev-server/) for local development. 75 | When you run `yarn rw dev`, TOML keys in your `redwood.toml`'s `[web]` table, like `port` and `apiUrl`, are used as Webpack Dev Server options (in this case, [devServer.port](https://webpack.js.org/configuration/dev-server/#devserverport) and [devServer.proxy](https://webpack.js.org/configuration/dev-server/#devserverproxy) respectively). 76 | 77 | ### Passing options with `--forward` 78 | 79 | While you can configure Webpack Dev Server in `web/config/webpack.config.js`, it's often simpler to just pass options straight to `yarn rw dev` using the `--forward` flag. 80 | 81 | > For the full list of Webpack Dev Server options, see https://webpack.js.org/configuration/dev-server/. 82 | 83 | #### Example: Setting the Port and Disabling Browser Opening 84 | 85 | In addition to passing new options, you can override those in your `redwood.toml`: 86 | 87 | ```bash 88 | yarn rw dev --forward="--port 1234 --no-open" 89 | ``` 90 | 91 | This starts your project on port `1234` and disables automatic browser opening. 92 | 93 | #### Example: Allow External Host Access 94 | 95 | If you're running Redwood in dev mode and trying to test your application from an external source (i.e. outside your network), you'll get an “Invalid Host Header”. To enable this workflow, run the following: 96 | 97 | ```bash 98 | yarn rw dev --forward="--allowed-hosts example.company.com --host 0.0.0.0" 99 | ``` 100 | 101 | This starts your project and forwards it to `example.company.com`. 102 | -------------------------------------------------------------------------------- /functions/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/functions/.keep -------------------------------------------------------------------------------- /functions/stickers.js: -------------------------------------------------------------------------------- 1 | // 2020-10-27 Rob Cameron 2 | // 3 | // This function takes every sticker request and adds it to an 4 | // AirTable at https://airtable.com/tblX3TAoUUW7afyso/viw0gZLG7DYkc5Vn0?blocks=hide 5 | // All PWV employees should have access. 6 | // 7 | // When a new sticker request comes into Netlify they will call a Webhook 8 | // endpoint. In this case the endpoint is this function. It accepts the incoming 9 | // data and then makes an API call out to AirTable to add the record. 10 | const fetch = require('node-fetch'); 11 | 12 | exports.handler = async (event,context) => { 13 | const fields = JSON.parse(event.body).data 14 | delete fields.ip 15 | delete fields.user_agent 16 | delete fields.referrer 17 | fields.status = 'New' 18 | fields.created_at = new Date().toISOString() 19 | 20 | const response = await fetch(process.env.AIRTABLE_ENDPOINT, 21 | { 22 | method: 'POST', 23 | headers: { 24 | 'Authorization': `Bearer ${process.env.AIRTABLE_API_KEY}`, 25 | 'Content-Type': 'application/json' 26 | }, 27 | body: JSON.stringify({ 28 | records: [{ fields }] 29 | }) 30 | } 31 | ) 32 | 33 | console.info(response) 34 | 35 | return { 36 | statusCode: 200, 37 | body: JSON.stringify(response.body) 38 | }; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /lib/docutron.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const template = require('lodash.template') 6 | const { paramCase } = require('param-case') 7 | const { titleCase } = require('title-case') 8 | const highlightLines = require('markdown-it-highlight-lines') 9 | const markdownItTocAndAnchor = require('markdown-it-toc-and-anchor').default 10 | const markdownItCollapsible = require('markdown-it-collapsible') 11 | const markdownItDeflist = require('markdown-it-deflist') 12 | const hljs = require('highlight.js') 13 | const hljsDefineGraphQL = require("highlightjs-graphql"); 14 | hljsDefineGraphQL(hljs); 15 | const md = require('markdown-it')({ 16 | html: true, 17 | linkify: true, 18 | highlight: function (str, language) { 19 | if (language && hljs.getLanguage(language)) { 20 | try { 21 | return hljs.highlight(str, { language }).value 22 | } catch (__) {} 23 | } 24 | 25 | return '' // use external default escaping 26 | }, 27 | }) 28 | md.use(highlightLines) 29 | md.use(markdownItTocAndAnchor, { 30 | toc: false, 31 | anchorLink: true, 32 | }) 33 | md.use(markdownItCollapsible) 34 | md.use(markdownItDeflist) 35 | 36 | const HTML_ROOT = path.join('code', 'html') 37 | const TEMPLATE_ROOT = path.join('lib', 'templates') 38 | 39 | const PAGE_TEMPLATE_PATH = path.join(TEMPLATE_ROOT, 'page.html.template') 40 | const NAV_ITEM_TEMPLATE_PATH = path.join(TEMPLATE_ROOT, 'nav_item.html.template') 41 | const TEMPLATES = { 42 | page: template(fs.readFileSync(PAGE_TEMPLATE_PATH).toString()), 43 | navItem: template(fs.readFileSync(NAV_ITEM_TEMPLATE_PATH).toString()), 44 | } 45 | 46 | // Converts an array of pages in markdown to HTML 47 | const convertToHtml = (pages, book, version, styles) => { 48 | return pages.map((page, index) => { 49 | let content = md.render(page.text) 50 | // Disable Turbolinks for links to sections of same page 51 | content = content.replace(/( header.level > 1), 57 | nextPage: pages[index + 1], 58 | pageTitle: book ? `${titleCase(book)} - ${page.title}` : page.title, 59 | version: version, 60 | }) 61 | 62 | return Object.assign(page, { html: output }) 63 | }) 64 | } 65 | 66 | // splits a markdown document by h1 and h2 into "pages" 67 | const splitToPages = (markdown, book, options = {}) => { 68 | const sections = [] 69 | let buffer = [] 70 | 71 | const shouldPageBreak = (line) => { 72 | const matches = line.match(/^(#+) /m) 73 | 74 | if (matches && options.pageBreakAtHeadingDepth.indexOf(matches[1].length) !== -1) { 75 | return true 76 | } else { 77 | return false 78 | } 79 | } 80 | 81 | let isCodeBlock = false 82 | 83 | markdown.split('\n').forEach((line, index) => { 84 | if (options.skipLines && index < options.skipLines) { 85 | return 86 | } 87 | 88 | if (line.match(/^```/)) { 89 | isCodeBlock = !isCodeBlock 90 | } 91 | 92 | if (!isCodeBlock && shouldPageBreak(line) && buffer.length) { 93 | sections.push(buffer.join('\n')) 94 | buffer = [] 95 | } 96 | buffer.push(line) 97 | }) 98 | sections.push(buffer.join('\n')) 99 | 100 | const groups = sections.map((section, index) => { 101 | let title = section.match(/^#+ (.*)$/m)[1] 102 | 103 | if (index === 0 && options.title) { 104 | title = options.title 105 | } 106 | 107 | return { href: `/${book}/${paramCase(title.toLowerCase())}.html`, title, text: section } 108 | }) 109 | 110 | return groups 111 | } 112 | 113 | // give an array of pages, builds a nav link for each 114 | const buildNav = (pages, book, index) => { 115 | return pages.map((page) => { 116 | const vars = Object.assign(page, { book, first: index === 0 }) 117 | 118 | return TEMPLATES.navItem(vars) 119 | }) 120 | } 121 | 122 | // creates the "on this page" nav links 123 | const subNav = (markdown, styles = {}) => { 124 | return markdown.match(/^#{1,4} (.*?)$/gm).map((header) => { 125 | const headerLevelIndex = header.indexOf(' ') 126 | const title = header 127 | .substring(headerLevelIndex + 1) 128 | .replace(/`/g, '') 129 | .replace('<', '<') 130 | .replace('>', '>') 131 | 132 | return { 133 | href: `#${paramCase(title.toLowerCase().replace(/([`'";]|&.*?;)/g, ''))}`, 134 | level: headerLevelIndex, 135 | title: title, 136 | style: title in styles ? styles[title] : '', 137 | // To indent headers according to the number of # they have. 138 | // But since we remove #, which equal 1, we want to consider ## as 1 and indent from there. 139 | ml: (headerLevelIndex - 2) * 0.75, 140 | } 141 | }) 142 | } 143 | 144 | const create = (markdown, book, options = {}) => { 145 | console.info('create book', book) 146 | const markdownPages = splitToPages(markdown, book, options) 147 | const htmlPages = convertToHtml(markdownPages, book, options.version, options.styles) 148 | // buildNav(markdownPages, book) 149 | 150 | htmlPages.forEach((page) => { 151 | fs.writeFileSync(path.join(HTML_ROOT, page.href), page.html) 152 | console.info(`+ Wrote ${book}:${page.title}`) 153 | }) 154 | 155 | return markdownPages 156 | } 157 | 158 | module.exports = { create, buildNav } 159 | -------------------------------------------------------------------------------- /lib/middleware/use-extension.js: -------------------------------------------------------------------------------- 1 | // https://github.com/tapio/live-server/issues/244 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const rootDirectory = './publish/' 6 | const extensions = ['html'] 7 | 8 | module.exports = function (req, res, next) { 9 | if (req.method !== 'GET' && req.method !== 'HEAD') { 10 | return next() 11 | } 12 | 13 | if (req.url !== '/' && path.extname(req.url) === '') { 14 | const requestedPath = req.url.replace('/', '') 15 | let i = 0 16 | const check = () => { 17 | const path = rootDirectory + requestedPath + '.' + extensions[i] 18 | 19 | fs.access(path, (err) => { 20 | if (!err) { 21 | req.url += '.' + extensions[i] 22 | next() 23 | } else { 24 | if (++i >= extensions.length) { 25 | next() 26 | } else { 27 | check() 28 | } 29 | } 30 | }) 31 | } 32 | 33 | check() 34 | } else { 35 | next() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/middleware/use-headers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises') 2 | const toml = require('toml') 3 | 4 | const headers = {} 5 | 6 | fs.readFile('./netlify.toml').then((data) => { 7 | toml.parse(data).headers.forEach((rule) => { 8 | headers[rule.for] = Object 9 | .entries(rule.values) 10 | .map(([name, value]) => ({ name, value })) 11 | }) 12 | }) 13 | 14 | module.exports = function (req, res, next) { 15 | if (req.method === 'GET') { 16 | const resHeaders = headers[req.url] 17 | 18 | if (resHeaders) { 19 | resHeaders.forEach((header) => { 20 | res.setHeader(header.name, header.value) 21 | }) 22 | } 23 | } 24 | 25 | next() 26 | } 27 | -------------------------------------------------------------------------------- /lib/middleware/use-redirects.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises') 2 | const toml = require('toml') 3 | 4 | const redirects = {} 5 | 6 | fs.readFile('./netlify.toml').then((data) => { 7 | toml.parse(data).redirects.forEach((rule) => { 8 | const from = rule.from 9 | redirects[from] = { 10 | to: rule.to, 11 | status: rule.status, 12 | } 13 | }) 14 | }) 15 | 16 | module.exports = function (req, res, next) { 17 | if (req.method === 'GET') { 18 | const rule = redirects[req.url] 19 | 20 | if (rule) { 21 | res.writeHead(rule.status || 301, { Location: rule.to }) 22 | res.end() 23 | return; 24 | } 25 | } 26 | 27 | next() 28 | } 29 | -------------------------------------------------------------------------------- /lib/news.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const marked = require('marked') 4 | marked.setOptions({ 5 | renderer: new marked.Renderer(), 6 | pedantic: false, 7 | gfm: true, 8 | breaks: false, 9 | sanitize: false, 10 | smartLists: true, 11 | smartypants: false, 12 | xhtml: false, 13 | }) 14 | const template = require('lodash.template') 15 | 16 | const HTML_ROOT = path.join('code', 'html') 17 | const TEMPLATE_ROOT = path.join('lib', 'templates') 18 | const NEWS_PATH = path.join(HTML_ROOT, 'news.html') 19 | const TAG_COLORS = { 20 | Podcast: 'blue-500', 21 | Article: 'orange-500', 22 | Video: 'purple-500', 23 | 'Message Thread': 'yellow-400', 24 | Newsletter: 'orange-700', 25 | Meetup: 'green-500', 26 | } 27 | 28 | const articleTemplate = template(fs.readFileSync(path.join(TEMPLATE_ROOT, 'news_article.html.template')).toString()) 29 | const newsTemplate = template(fs.readFileSync(path.join(TEMPLATE_ROOT, 'news.html.template')).toString()) 30 | 31 | const parseMarkdown = () => { 32 | const articles = [{}] 33 | const markdown = fs.readFileSync(path.join('.', 'NEWS.md')).toString() 34 | const tokens = marked.lexer(markdown) 35 | let index = 0 36 | 37 | tokens.forEach((token) => { 38 | switch (token.type) { 39 | case 'hr': 40 | index++ 41 | articles[index] = {} 42 | return 43 | case 'heading': 44 | articles[index] = Object.assign(articles[index], parseHeading(token)) 45 | return 46 | case 'paragraph': 47 | articles[index] = Object.assign(articles[index], parseImage(token)) 48 | return 49 | default: 50 | return 51 | } 52 | }) 53 | 54 | return sortArticles(articles) 55 | } 56 | 57 | const sortArticles = (articles) => { 58 | return articles.sort((a, b) => { 59 | const aDate = new Date(a.date) 60 | const bDate = new Date(b.date) 61 | 62 | return bDate - aDate 63 | }) 64 | } 65 | 66 | const generateAll = (articles) => { 67 | return articles 68 | .map((article) => { 69 | return articleTemplate(Object.assign(article, { colors: TAG_COLORS })) 70 | }) 71 | .join('\n') 72 | } 73 | 74 | const generateColumns = (articles) => { 75 | const columns = [[], [], []] 76 | let index = 0 77 | 78 | articles.forEach((article) => { 79 | columns[index].push(articleTemplate(Object.assign(article, { colors: TAG_COLORS }))) 80 | index = (index + 1) % 3 81 | }) 82 | 83 | return { 84 | column1: columns[0].join('\n'), 85 | column2: columns[1].join('\n'), 86 | column3: columns[2].join('\n'), 87 | } 88 | } 89 | 90 | const parseHeading = (token) => { 91 | let output = {} 92 | 93 | switch (token.depth) { 94 | case 1: 95 | output.link = token.text.match(/\((.*?)\)/)[1] 96 | output.title = token.text.match(/\[(.*?)\]/)[1] 97 | case 2: 98 | output.date = token.text 99 | case 3: 100 | output.description = token.text 101 | case 4: 102 | output.tags = token.text.split(',').map((tag) => tag.trim()) 103 | } 104 | 105 | return output 106 | } 107 | 108 | const parseImage = (token) => { 109 | return { 110 | image: token.text.match(/\((.*?)\)/)[1], 111 | alt: token.text.match(/\[(.*?)\]/)[1], 112 | } 113 | } 114 | 115 | const run = () => { 116 | const articles = parseMarkdown() 117 | const { column1, column2, column3 } = generateColumns(articles) 118 | const vars = { 119 | allArticles: generateAll(articles), 120 | column1, 121 | column2, 122 | column3, 123 | } 124 | 125 | fs.writeFileSync(NEWS_PATH, newsTemplate(vars)) 126 | } 127 | 128 | module.exports = { run } 129 | -------------------------------------------------------------------------------- /lib/roadmap.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const { create: createDocs } = require('./docutron.js') 5 | 6 | const HTML_ROOT = path.join('code', 'html') 7 | 8 | const run = () => { 9 | console.info(`\nROADMAP...`) 10 | 11 | const markdown = fs.readFileSync('./ROADMAP.md').toString() 12 | const styles = { 13 | Accessibility: 4, 14 | Auth: 4, 15 | Core: 4, 16 | Deployment: 4, 17 | Docs: 3, 18 | Generators: 4, 19 | Logging: 4, 20 | Performance: 3, 21 | Prerender: 4, 22 | Router: 4, 23 | Storybook: 4, 24 | Structure: 3, 25 | ['Testing (App)']: 4, 26 | TypeScript: 4, 27 | } 28 | const [page] = createDocs( 29 | markdown, 30 | '', 31 | { pageBreakAtHeadingDepth: [1], file: './ROADMAP.md', styles } 32 | ) 33 | fs.writeFileSync(path.join(HTML_ROOT, page.href), page.html) 34 | } 35 | 36 | module.exports = { run } -------------------------------------------------------------------------------- /lib/search.js: -------------------------------------------------------------------------------- 1 | const { paramCase } = require('param-case') 2 | const { titleCase } = require('title-case') 3 | const md5 = require('blueimp-md5') 4 | const algoliasearch = require('algoliasearch') 5 | const marked = require('marked') 6 | 7 | let publish 8 | let getObjectIDs 9 | 10 | if (process.env['ALGOLIA_APP_ID']) { 11 | marked.setOptions({ 12 | renderer: new marked.Renderer(), 13 | highlight: function (code, language) { 14 | const hljs = require('highlight.js') 15 | const validLanguage = hljs.getLanguage(language) ? language : 'plaintext' 16 | return hljs.highlight(code, { language: validLanguage }).value 17 | }, 18 | pedantic: false, 19 | gfm: true, 20 | breaks: false, 21 | sanitize: false, 22 | smartLists: true, 23 | smartypants: false, 24 | xhtml: false, 25 | }) 26 | 27 | let indexName = process.env['ALGOLIA_INDEX_NAME'] 28 | if (process.env['CONTEXT'] && process.env['CONTEXT'] !== 'production') { 29 | indexName = process.env['ALGOLIA_BRANCH_INDEX_NAME'] 30 | } 31 | const searchClient = algoliasearch(process.env['ALGOLIA_APP_ID'], process.env['ALGOLIA_API_KEY']) 32 | const searchIndex = searchClient.initIndex(indexName) 33 | 34 | const IGNORE_TOKENS = [ 35 | 'blockquote_start', 36 | 'blockquote_end', 37 | 'hr', 38 | 'html', 39 | 'list_start', 40 | 'list_end', 41 | 'list_item_start', 42 | 'list_item_end', 43 | 'loose_item_start', 44 | 'space', 45 | ] 46 | 47 | const tokenToSearchRecord = (book, chapter, section, token) => { 48 | const id = md5(`${book}:${chapter}:${section}:${token.type}:${token.text}`) 49 | const href = `/${book}/${paramCase(chapter.toLowerCase())}.html#${paramCase(section.toLowerCase())}` 50 | 51 | return { 52 | objectID: id, 53 | href, 54 | book: titleCase(book), 55 | chapter, 56 | section, 57 | type: token.type, 58 | text: token.text, 59 | } 60 | } 61 | 62 | publish = async (markdown, book, options = {}) => { 63 | const tokens = marked.lexer(markdown) 64 | const recordsToPublish = [] // records to be published to search 65 | const newRecordIDs = [] // IDs that we create during this process 66 | let existingRecordIDs = [] // IDs that are already in search 67 | let chapter = null 68 | let section = null 69 | 70 | console.info(`Publishing to search index "${indexName}"...`) 71 | 72 | const shouldPageBreak = (depth) => { 73 | return options.pageBreakAtHeadingDepth.indexOf(depth) !== -1 74 | } 75 | 76 | const shouldIgnoreToken = (type) => { 77 | return IGNORE_TOKENS.indexOf(type) !== -1 78 | } 79 | 80 | const isHeader = (type) => { 81 | return type === 'heading' 82 | } 83 | 84 | const isNewRecord = (record) => { 85 | const ids = options.objectIDs[record.book] && options.objectIDs[record.book][record.chapter] 86 | if (ids) { 87 | return ids.indexOf(record.objectID) === -1 88 | } else { 89 | return true 90 | } 91 | } 92 | 93 | tokens.forEach((token) => { 94 | if (shouldIgnoreToken(token.type)) { 95 | return 96 | } 97 | 98 | if (isHeader(token.type)) { 99 | if (shouldPageBreak(token.depth)) { 100 | // start a new page 101 | chapter = options.title || token.text 102 | section = options.title || token.text 103 | } else { 104 | // keep the same page, but change the section's name 105 | section = token.text 106 | } 107 | } else { 108 | if (options.title && chapter === null && section === null) { 109 | chapter = options.title 110 | section = options.title 111 | } 112 | const record = tokenToSearchRecord(book, chapter, section, token) 113 | newRecordIDs.push(record.objectID) 114 | 115 | if (isNewRecord(record)) { 116 | recordsToPublish.push(record) 117 | } else { 118 | // collect existing IDs from this book/chapter to be sure we have them all when we're done 119 | // `new Set` makes sure that we have a unique union of two arrays 120 | existingRecordIDs = [...new Set([...existingRecordIDs, ...options.objectIDs[record.book][record.chapter]])] 121 | } 122 | } 123 | }) 124 | 125 | // push new records 126 | console.info(`-> Sending ${recordsToPublish.length} record(s) to search`) 127 | searchIndex.saveObjects(recordsToPublish) 128 | 129 | // figure out which records need to be deleted by doing a difference 130 | const idsToDelete = existingRecordIDs.filter((id) => !newRecordIDs.includes(id)) 131 | console.info(`<- Deleting ${idsToDelete.length} record(s)`) 132 | searchIndex.deleteObjects(idsToDelete) 133 | } 134 | 135 | getObjectIDs = async () => { 136 | let objectIDs = {} 137 | 138 | await searchIndex.browseObjects({ 139 | query: '', 140 | attributesToRetrieve: ['objectID', 'book', 'chapter'], 141 | batch: (batch) => { 142 | batch.forEach((b) => { 143 | if (!objectIDs[b.book]) objectIDs[b.book] = {} 144 | if (!objectIDs[b.book][b.chapter]) objectIDs[b.book][b.chapter] = [] 145 | objectIDs[b.book][b.chapter].push(b.objectID) 146 | }) 147 | }, 148 | }) 149 | 150 | return objectIDs 151 | } 152 | } else { 153 | publish = () => {} 154 | getObjectIDs = () => [] 155 | } 156 | 157 | module.exports = { publish, getObjectIDs } 158 | -------------------------------------------------------------------------------- /lib/templates/nav_item.html.template: -------------------------------------------------------------------------------- 1 |
  • 2 | data-match="/${book}"<% } %>>${title} 3 |
  • 4 | -------------------------------------------------------------------------------- /lib/templates/news.html.template: -------------------------------------------------------------------------------- 1 | @@layout("application", { "title": "RedwoodJS - News, Articles, Podcasts", "version": "" }) 2 | 3 |
    4 |
    5 |

    News, Articles and Podcasts

    6 |

    All the news that's fit to print about RedwoodJS. If you've got news we should feature open a PR. 7 |

    8 | 19 | 20 |
    21 | ${allArticles} 22 |
    23 |
    24 | -------------------------------------------------------------------------------- /lib/templates/news_article.html.template: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /lib/templates/page.html.template: -------------------------------------------------------------------------------- 1 | @@layout("application", { "title": "${pageTitle} : RedwoodJS Docs", "version": "${version}" }) 2 | 3 | @@contentFor("aside", 4 | 22 | ) 23 | 24 |
    25 | ${content} 26 | <% if (nextPage) { %> 27 |
    28 | Next » 29 |
    30 | <% } %> 31 |
    32 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [dev] 2 | command = "cameronjs dev" 3 | publish = "publish" 4 | port = 8080 5 | 6 | [build] 7 | command = "yarn build" 8 | publish = "publish" 9 | functions = "functions" 10 | 11 | # For more settings see https://www.netlify.com/docs/netlify-toml-reference/#post-processing 12 | [build.processing] 13 | skip_processing = false 14 | [build.processing.css] 15 | minify = true 16 | [build.processing.js] 17 | minify = true 18 | [build.processing.html] 19 | pretty_urls = true 20 | [build.processing.images] 21 | compress = true 22 | 23 | [context.production] 24 | environment = { NODE_ENV = "production"} 25 | 26 | [context.branch-deploy] 27 | environment = { NODE_ENV = "production" } 28 | 29 | [[redirects]] 30 | from = "/docs/*" 31 | to = "https://redwoodjs-docs.netlify.app/docs/:splat" 32 | status = 200 33 | force = true 34 | 35 | [[redirects]] 36 | from = "/cookbook" 37 | to = "/docs/how-to/index" 38 | status = 301 39 | force = true 40 | 41 | [[redirects]] 42 | from = "/cookbook/*" 43 | to = "/docs/how-to/:splat" 44 | status = 301 45 | force = true 46 | 47 | [[redirects]] 48 | from = "/tutorial" 49 | to = "/docs/tutorial/welcome-to-redwood" 50 | status = 301 51 | force = true 52 | 53 | [[redirects]] 54 | from = "/tutorial/*" 55 | to = "/docs/tutorial/:splat" 56 | status = 301 57 | force = true 58 | 59 | [[redirects]] 60 | from = "/assets/*" 61 | to = "https://redwoodjs-docs.netlify.app/assets/:splat" 62 | status = 200 63 | force = true 64 | 65 | [[redirects]] 66 | from = "/img/*" 67 | to = "https://redwoodjs-docs.netlify.app/img/:splat" 68 | status = 200 69 | force = true 70 | 71 | [[redirects]] 72 | from = "/newsletter" 73 | to = "https://redwoodjs.us19.list-manage.com/subscribe/post?u=0c27354a06a7fdf4d83ce07fc&id=09f634eea4" 74 | force = true 75 | 76 | [[redirects]] 77 | from ="/community" 78 | to ="https://community.redwoodjs.com/t/welcome-to-the-redwoodjs-community/2416" 79 | 80 | [[redirects]] 81 | from ="/v1launchweek" 82 | to ="https://v1launchweek.redwoodjs.com/" 83 | 84 | [[redirects]] 85 | from ="/v1-launch-week" 86 | to ="https://v1launchweek.redwoodjs.com/" 87 | 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redwoodjs.com", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "node lib/build.js && webpack && postcss --verbose code/stylesheets/application.pcss -o publish/stylesheets/application.css", 6 | "clean": "del 'code/html/(cookbook|docs|tutorial|tutorial2)/*.html' 'code/html/_(cookbook|docs|tutorial|tutorial2)_nav.html' 'publish/!(downloads|images|favicon.*)'", 7 | "dev": "yarn serve & yarn watch", 8 | "netlify": "yarn watch & netlify dev", 9 | "rebuild": "yarn clean && yarn build", 10 | "serve": "live-server --watch=./publish --mount=/:./publish --entry-file='publish/404.html' --middleware=../../../lib/middleware/use-extension --middleware=../../../lib/middleware/use-redirects", 11 | "watch": "webpack --watch & postcss --verbose code/stylesheets/application.pcss -o publish/stylesheets/application.css --watch" 12 | }, 13 | "private": true, 14 | "devDependencies": { 15 | "del-cli": "3.0.1", 16 | "live-server": "1.2.1", 17 | "toml": "3.0.0" 18 | }, 19 | "dependencies": { 20 | "@fullhuman/postcss-purgecss": "2.3.0", 21 | "@octokit/rest": "16.43.2", 22 | "algoliasearch": "4.11.0", 23 | "autoprefixer": "9.8.8", 24 | "blueimp-md5": "2.19.0", 25 | "cameronjs-html-webpack-plugin": "0.5.1", 26 | "clipboard": "2.0.8", 27 | "dotenv": "8.6.0", 28 | "highlight.js": "10.7.3", 29 | "highlightjs-graphql": "1.0.2", 30 | "install": "0.13.0", 31 | "lodash.clone": "4.5.0", 32 | "lodash.escape": "4.0.1", 33 | "lodash.template": "4.5.0", 34 | "markdown-it": "12.3.2", 35 | "markdown-it-collapsible": "1.0.0", 36 | "markdown-it-deflist": "2.1.0", 37 | "markdown-it-highlight-lines": "1.0.2", 38 | "markdown-it-toc-and-anchor": "4.2.0", 39 | "marked": "4.0.10", 40 | "node-fetch": "2.6.7", 41 | "param-case": "3.0.4", 42 | "postcss-cli": "7.1.2", 43 | "postcss-import": "12.0.1", 44 | "postcss-nested": "4.2.3", 45 | "stimulus": "1.1.1", 46 | "tailwindcss": "1.9.6", 47 | "title-case": "3.0.3", 48 | "turbolinks": "5.2.0", 49 | "webpack": "4.46.0", 50 | "webpack-cli": "3.3.12" 51 | }, 52 | "peerDependencies": { 53 | "cameronjs": "^0.5.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('tailwindcss'), 5 | require('postcss-nested'), 6 | require('autoprefixer'), 7 | process.env.NODE_ENV === 'production' && 8 | require('@fullhuman/postcss-purgecss')({ 9 | content: ['./publish/**/*.html'], 10 | defaultExtractor: (content) => content.match(/[\w-/:]+(? 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /publish/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /publish/images/mark-logo-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/publish/images/mark-logo-cover.png -------------------------------------------------------------------------------- /publish/images/mark-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/publish/images/mark-logo-transparent.png -------------------------------------------------------------------------------- /publish/images/opengraph-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/publish/images/opengraph-256.png -------------------------------------------------------------------------------- /publish/images/stickers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/publish/images/stickers.png -------------------------------------------------------------------------------- /publish/images/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redwoodjs/redwoodjs-com-archive/f6681f5c5d65d04b7bb85a9c0d679b71685ed3bc/publish/images/structure.png -------------------------------------------------------------------------------- /publish/images/type.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* See https://tailwindcss.com/docs/configuration for more options */ 2 | 3 | module.exports = { 4 | theme: { 5 | extend: { 6 | colors: { 7 | red: { 8 | 'hacktoberfest-purple': { 9 | 400: '#BC7E97', 10 | 500: '#9F466B', 11 | }, 12 | 'hacktoberfest-blue': { 13 | 500: '#072540' 14 | }, 15 | '100': '#FDF8F6', 16 | '200': '#FAEAE5', 17 | '300': '#F3C7BA', 18 | '400': '#EBA48E', 19 | '500': '#E38163', 20 | '600': '#DC5E38', 21 | '700': '#BF4722', 22 | '800': '#682712', 23 | '900': '#341309', 24 | }, 25 | }, 26 | fontFamily: { 27 | sans: ['Open Sans', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], 28 | mono: ['Fira Code', 'Fira Mono', 'Menlo', 'Monoco', 'monospace'], 29 | }, 30 | spacing: { 31 | half: '0.125rem', 32 | '22': '5.5rem', 33 | '9/16': '56.25%', 34 | }, 35 | }, 36 | }, 37 | variants: {}, 38 | plugins: [], 39 | } 40 | -------------------------------------------------------------------------------- /videos/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | ## RedwoodJS Authentication in 5 Minutes 4 | 5 | Add authentication to an existing Redwood app at the speed of light! 6 | 7 | -------------------------------------------------------------------------------- /videos/router.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | ## Configurable PageLoadingContext 4 | 5 | Add a loading indicator to any page once a time threshold is reached. 6 | 7 |
    8 | 18 | 19 |
    -------------------------------------------------------------------------------- /videos/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | Learn about RedwoodJS the easy way—no typing, more snacks. 4 | 5 | ## Introduction to Creating a New App, Generators, Pages, and Layouts 6 | 7 | 15 | 16 | ## Using Scaffolds and Cells 17 | 18 | 26 | 27 | ## Creating a Form and Saving Records to the Database 28 | 29 | 37 | 38 | ## Deploying to Netlify and Adding Authentication 39 | 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CameronJSHtmlWebpackPlugin = require('cameronjs-html-webpack-plugin') 3 | const webpack = require('webpack') 4 | require('dotenv').config() 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | entry: './code/javascripts/application.js', 9 | mode: process.env.NODE_ENV || 'development', 10 | output: { 11 | filename: 'javascripts/application.js', 12 | path: path.resolve(__dirname, 'publish'), 13 | }, 14 | plugins: [ 15 | new CameronJSHtmlWebpackPlugin({ 16 | source: './code/html', 17 | layouts: 'layouts', 18 | partials: 'partials', 19 | }), 20 | new webpack.DefinePlugin({ 21 | 'process.env.ALGOLIA_APP_ID': JSON.stringify(process.env['ALGOLIA_APP_ID']), 22 | 'process.env.ALGOLIA_API_KEY': JSON.stringify(process.env['ALGOLIA_API_KEY']), 23 | 'process.env.ALGOLIA_SEARCH_KEY': JSON.stringify(process.env['ALGOLIA_SEARCH_KEY']), 24 | 'process.env.ALGOLIA_INDEX_NAME': JSON.stringify( 25 | process.env['CONTEXT'] && process.env['CONTEXT'] !== 'production' 26 | ? process.env['ALGOLIA_BRANCH_INDEX_NAME'] 27 | : process.env['ALGOLIA_INDEX_NAME'] 28 | ), 29 | }), 30 | ], 31 | watchOptions: { 32 | ignored: /node_modules/, 33 | }, 34 | } 35 | --------------------------------------------------------------------------------