├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── _config.yml ├── assets └── theme-colors.less ├── circle.yml ├── package.json ├── side-contents.md ├── source ├── CNAME ├── admin.md ├── api-schemas.md ├── architecture_old.md ├── callbacks.md ├── card.md ├── cheatsheet.md ├── cloudinary.md ├── common-problems.md ├── components.md ├── connecting-remotely.md ├── database.md ├── datatable.md ├── debug.md ├── decorators.md ├── deployment.md ├── email.md ├── embed.md ├── error-tracking.md ├── errors.md ├── events.md ├── example-customization.md ├── example-forum.md ├── example-instagram.md ├── example-movies.md ├── example-permissions.md ├── example-simple.md ├── examples.md ├── features-packages.md ├── features.md ├── field-resolvers.md ├── file-architecture.md ├── filtering.md ├── forms-components.md ├── forms-custom.md ├── forms-upload.md ├── forms.md ├── forum-packages.md ├── fragments.md ├── graphql-schema.md ├── groups-permissions.md ├── head-tags.md ├── images │ ├── how-vulcan-works.png │ ├── how-vulcan-works.svg │ ├── terms-parameters.svg │ └── vulcan-schemas.svg ├── index.md ├── internationalization.md ├── learn.md ├── logo │ ├── favicon.png │ ├── icon-apollo-white-200x200.png │ ├── logo-apollo-space-left.svg │ ├── logo-apollo-space.svg │ └── logo-apollo-subbrands-developers-space.svg ├── material-ui.md ├── migration.md ├── mutations.md ├── newsletter.md ├── notifications.md ├── nutshell.md ├── other-components.md ├── packages.md ├── payments.md ├── performance.md ├── plugins.md ├── queries.md ├── reactive-state.md ├── recipes.md ├── redux.md ├── relations.md ├── resolvers.md ├── routing.md ├── schema-properties.md ├── schemas.md ├── security_old.md ├── server-queries.md ├── settings.md ├── sponsorship-tutorial.md ├── storybook.md ├── terms-parameters.md ├── themes.md ├── toc.md ├── tutorials.md ├── ui-components.md ├── unit-testing.md ├── users.md ├── videos.md ├── voting.md ├── vulcan.md └── why-vulcan.md ├── talk-outline.md ├── themes └── meteor │ ├── .gitignore │ ├── _config.yaml │ ├── layout │ ├── layout.ejs │ ├── page.ejs │ └── partials │ │ ├── illustration-compass.ejs │ │ ├── illustration-github.ejs │ │ ├── illustration-guide.ejs │ │ ├── illustration-help.ejs │ │ ├── illustration-logs.ejs │ │ ├── illustration-support.ejs │ │ └── sidebar.ejs │ └── source │ ├── fonts │ ├── percolate.eot │ ├── percolate.svg │ ├── percolate.ttf │ └── percolate.woff │ ├── images │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── safari-pinned-tab.svg │ ├── vulcan-logo-border-2.svg │ ├── vulcan-logo-border.png │ ├── vulcan-logo-color.svg │ └── vulcan-logo.svg │ ├── script │ ├── hotkey.js │ ├── main.js │ └── smooth-scroll.min.js │ └── style │ ├── _global │ ├── Checkbox.less │ ├── Input.less │ ├── Radio.less │ ├── Select.less │ ├── Textarea.less │ ├── animation.less │ ├── base.less │ ├── button.less │ ├── docsearch.less │ ├── form.less │ ├── formatting.less │ ├── icon.less │ ├── link.less │ ├── mobile.less │ ├── nav.less │ ├── panel.less │ ├── syntax.less │ └── tables.less │ ├── _theme │ ├── content.less │ ├── layout.less │ ├── nav.less │ ├── panel.less │ └── sidebar.less │ ├── _util │ ├── clearfix.import.less │ ├── color.import.less │ ├── easing.import.less │ ├── helper.import.less │ ├── index.import.less │ ├── lesshat.import.less │ ├── link.import.less │ ├── normalize.import.less │ ├── scrollbar.import.less │ ├── text.import.less │ ├── typography.import.less │ └── ui.import.less │ └── style.less └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | Thumbs.db 4 | db.json 5 | *.log 6 | node_modules/ 7 | public/ 8 | .deploy*/ 9 | docs.json 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-docs/3cb0cd142609b9efc00ead23c30de424a304a605/.gitmodules -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@vulcanjs.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Etiquette 2 | 3 | - **All PRs should be made to the `devel` branch, not `master`.** 4 | 5 | - Come check-in in the [Vulcan Slack channel](http://slack.telescopeapp.org/). 👋 6 | 7 | - Don't hesitate to issue PRs for any mistakes you find in the docs. 👩🏼‍💻 8 | 9 | - When you contribute new feature or just changes to existing ones, please also update these docs. 📖 10 | 11 | - Be nice 😉 12 | 13 | ## Branches 14 | 15 | - `master` branch reflects the contents of [Vulcan Docs](https://docs.vulcanjs.org/) and the documentation matches the latest version of the Vulcan packages published on Atmosphere 16 | - `devel` branch is the bleeding edge, documenting the `devel` branch of the Vulcan codebase 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vulcan Docs 2 | 3 | To run: 4 | 5 | ``` 6 | git clone --recursive https://github.com/VulcanJS/vulcan-docs.git 7 | cd vulcan-docs 8 | npm install 9 | npm start 10 | ``` 11 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Hexo Configuration 2 | ## Docs: http://hexo.io/docs/configuration.html 3 | ## Source: https://github.com/hexojs/hexo/ 4 | 5 | # Site 6 | title: Vulcan Docs 7 | propertytitle: Vulcan Docs 8 | subtitle: Vulcan Docs 9 | description: Vulcan Docs 10 | logo: "/images/vulcan-logo-border-2.svg" 11 | author: 12 | language: 13 | timezone: 14 | versions: 15 | - "1" 16 | sidebar_categories: 17 | null: 18 | - index 19 | - nutshell 20 | - cheatsheet 21 | - file-architecture 22 | - toc 23 | # - features 24 | # - architecture 25 | # - vulcan 26 | # - features-packages 27 | # - forum-packages 28 | Schemas: 29 | - schemas 30 | - schema-properties 31 | # - field-resolvers 32 | - relations 33 | GraphQL API: 34 | - graphql-schema 35 | - api-schemas 36 | - queries 37 | - filtering 38 | - mutations 39 | - fragments 40 | - server-queries 41 | - connecting-remotely 42 | Features: 43 | - routing 44 | - settings 45 | - groups-permissions 46 | - callbacks 47 | - users 48 | - internationalization 49 | - head-tags 50 | - errors 51 | - reactive-state 52 | Components: 53 | - components 54 | - ui-components 55 | - datatable 56 | - card 57 | - forms-components 58 | - other-components 59 | Forms: 60 | - forms 61 | - decorators 62 | - forms-custom 63 | Server: 64 | # - resolvers 65 | # - mutations 66 | # - redux 67 | # - security 68 | - performance 69 | - database 70 | Other Packages: 71 | - packages 72 | - material-ui 73 | - email 74 | # - newsletter 75 | # - payments 76 | # - admin 77 | # - embed 78 | # - cloudinary 79 | # - voting 80 | - events 81 | - error-tracking 82 | # - forms-upload 83 | - debug 84 | Testing: 85 | - unit-testing 86 | - storybook 87 | # Tutorials: 88 | # - example-simple 89 | # - example-movies 90 | # - example-instagram 91 | # - example-forum 92 | # - example-customization 93 | # - example-permissions 94 | # - sponsorship-tutorial 95 | # - deployment 96 | # - recipes 97 | Resources: 98 | - learn 99 | - examples 100 | - common-problems 101 | - plugins 102 | - themes 103 | - tutorials 104 | - videos 105 | github_repo: vulcanjs/vulcan-docs 106 | content_root: source 107 | 108 | typescript_api_box: 109 | data_file: docs.json 110 | 111 | social_links: 112 | github: "https://github.com/VulcanJS/vulcan-docs" 113 | twitter: "@vulcanjs" 114 | 115 | # API keys 116 | apis: 117 | 118 | # URL 119 | ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/' 120 | url: https://docs.vulcanjs.org/ 121 | root: / 122 | permalink: :year/:month/:day/:title/ 123 | permalink_defaults: 124 | 125 | # Directory 126 | source_dir: source 127 | public_dir: public 128 | tag_dir: tags 129 | archive_dir: archives 130 | category_dir: categories 131 | code_dir: downloads/code 132 | i18n_dir: :lang 133 | skip_render: 134 | 135 | # Writing 136 | new_post_name: :title.md # File name of new posts 137 | default_layout: post 138 | titlecase: false # Transform title into titlecase 139 | external_link: true # Open external links in new tab 140 | filename_case: 0 141 | render_drafts: false 142 | post_asset_folder: false 143 | relative_link: false 144 | future: true 145 | highlight: 146 | enable: true 147 | line_number: true 148 | auto_detect: true 149 | tab_replace: 150 | 151 | # Category & Tag 152 | default_category: uncategorized 153 | category_map: 154 | tag_map: 155 | 156 | # Date / Time format 157 | ## Hexo uses Moment.js to parse and display date 158 | ## You can customize the date format as defined in 159 | ## http://momentjs.com/docs/#/displaying/format/ 160 | date_format: YYYY-MM-DD 161 | time_format: HH:mm:ss 162 | 163 | # Pagination 164 | ## Set per_page to 0 to disable pagination 165 | per_page: 10 166 | pagination_dir: page 167 | 168 | # Extensions 169 | ## Plugins: http://hexo.io/plugins/ 170 | plugins: 171 | # - hexo-generator-cname 172 | 173 | ## Themes: http://hexo.io/themes/ 174 | theme: meteor 175 | 176 | # Deployment 177 | ## Docs: http://hexo.io/docs/deployment.html 178 | deploy: 179 | type: git 180 | repository: https://github.com/VulcanJS/vulcan-docs/ 181 | branch: gh-pages 182 | -------------------------------------------------------------------------------- /assets/theme-colors.less: -------------------------------------------------------------------------------- 1 | @color-primary: #F44A06; 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.12 4 | 5 | checkout: 6 | post: 7 | - git submodule update --init 8 | 9 | dependencies: 10 | cache_directories: 11 | - "site/node_modules" 12 | override: 13 | - npm install -g hexo-cli 14 | - npm install 15 | - cd code; npm install 16 | 17 | test: 18 | override: 19 | # maybe we will need tests in the future 20 | - echo 'ok!' 21 | 22 | deployment: 23 | s3: 24 | branch: /^(master|version-.*)/ 25 | commands: 26 | - npm run deploy 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "hexo": { 6 | "version": "3.8.0" 7 | }, 8 | "dependencies": { 9 | "global": "^4.3.2", 10 | "handlebars": "^4.5.3", 11 | "hexo": "^3.8.0", 12 | "hexo-deployer-git": "^1.0.0", 13 | "hexo-generator-archive": "^0.1.2", 14 | "hexo-generator-category": "^0.1.2", 15 | "hexo-generator-cname": "^0.3.0", 16 | "hexo-generator-index": "^0.2.1", 17 | "hexo-generator-tag": "^0.2.0", 18 | "hexo-renderer-ejs": "^0.3.1", 19 | "hexo-renderer-less": "^0.2.0", 20 | "hexo-renderer-marked": "^1.0.1", 21 | "hexo-server": "^0.3.3", 22 | "lodash": "^4.17.19", 23 | "showdown": "^1.9.1" 24 | }, 25 | "devDependencies": { 26 | "hexo-browsersync": "^0.3.0" 27 | }, 28 | "scripts": { 29 | "start": "hexo serve", 30 | "deploy": "hexo deploy --generate", 31 | "develop-theme": "nodemon -x 'rm db.json; hexo serve' -w assets/ -w code/ -w source/ -w themes/ -w scripts/" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /side-contents.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | Vulcan is a new kind of toolkit that lets you build single-page web apps a lot faster. You'll get ready-made components to take care of common tasks such as data loading, user accounts, form handling, and much more. 4 | 5 | ## 3 principles 6 | 7 | ### No Dumb Work 8 | 9 | Vulcan's main goal is to take care of all the repetitive tasks involved in building a web app: hooking up user accounts, coding up forms, setting up CRUD operations… if it's something you've done before, Vulcan should do it for you! 10 | 11 | ### Flexible 12 | 13 | Typically, the more features a framework has, the more rigid it gets. But Vulcan was built with extensibility and flexibility in mind from the start, meaning you can override and extend its features without having to ever touch the core. 14 | 15 | ### Eject Anytime 16 | 17 | There's nothing worse than realizing that the technology you've sunk hundreds of hours into isn't right for your needs. To avoid this, Vulcan is architectured to make it easy to take your React components or GraphQL resolvers, and migrate to a different stack altogether. Outgrowing Vulcan shouldn't be a big deal. 18 | 19 | ## Technology 20 | 21 | Vulcan is based on three open-source technologies that work hand-in-hand: 22 | 23 | ### Front-End: React 24 | 25 | React has rapidly become one of the best ways to build robuts web app UIs. Its component-based approach and vast ecosystem make it ideal for rapid development. 26 | 27 | ### Data Layer: GraphQL 28 | 29 | GraphQL (using the [Apollo](http://apollostack.com) implementation) is an ideal way of getting your data from the server to the client. It's extremely flexible, used by some of the biggest companies in the world, and best of all lets you request exactly the data you need. 30 | 31 | ### Server & Build Tool: Meteor 32 | 33 | Meteor is the glue that binds everything else together. Its ready-made built tool, database, and user accounts make creating new projects a snap. 34 | 35 | ## Features 36 | 37 | ### Data Layer 38 | 39 | Vulcan makes leveraging GraphQL easy by letting you skip all the boilerplate. Just specify the data you need, and let Vulcan's data layer do the rest. 40 | 41 | ### Forms 42 | 43 | What if you never had to code a form ever again? Vulcan generates forms based on your collections schemas, and also handles the submission and validation process for you. 44 | 45 | ### Users 46 | 47 | Every app needs user accounts, and Vulcan gives you easy sign-up and log-in forms, as well as a simple groups and permissions system. 48 | 49 | ### Theming 50 | 51 | Vulcan's theming system lets you take a third-party theme built with React components, and then pick and choose which components you want to replace or extend. 52 | 53 | ### Callback Hooks 54 | 55 | Every server-side operation exposes synchronous and asynchronous hooks so you can extend or modify the server's behavior without having to modify any core files. 56 | 57 | ### Internationalization 58 | 59 | Vulcan comes internationalized out of the box using [react-i18n](#). Add or change strings with a few lines of code. 60 | 61 | ## Packages 62 | 63 | In addition to its core features, Vulcan also comes with a few optional packages that cover a web app's most common requirements. 64 | 65 | ### Posts & Comments 66 | 67 | Many apps are organized around a central item feed, and the Posts package gives you just that. Additionally, the Comments package lets users react to each post. 68 | 69 | ### Categories 70 | 71 | The Categories package enables regular or nested categories to help organize your content. 72 | 73 | ### Newsletter 74 | 75 | The Newsletter package works in combination with the Posts and Commments packages to automatically compile and send daily or weekly recaps of your app's activity. 76 | 77 | ### Notifications 78 | 79 | You can keep users notified of any activity in your app by using the Notifications package to send out email notifications whenever something important happens. 80 | 81 | ### RSS & API 82 | 83 | Automatically generate RSS feeds and a JSON API for your posts and comments. 84 | 85 | ### Voting 86 | 87 | Add upvotes and downvotes to posts and comments. 88 | 89 | ## Team 90 | 91 | ### Sacha Greif 92 | 93 | I learned a lot about making development more approachable by writing [Discover Meteor](http://discovermeteor.com). But you can only do so much with a book, so creating something like Vulcan was a natural next step. 94 | 95 | ### Xavier Cazalot 96 | 97 | *** 98 | 99 | ### You? 100 | 101 | Vulcan is open source and we're always looking for new contributors, no matter if you're a JavaScript guru or coding newbie. Come say hello in [our Slack channel](http://slack.vulcanjs.org)! 102 | 103 | ## Future Goals 104 | 105 | ### Migrate from Meteor (if needed) 106 | 107 | ### Extract tools 108 | 109 | ### become db-agnostic? 110 | 111 | ### 112 | 113 | 114 | 115 | 116 | 117 | ### "Hack Learn Make" 118 | 119 | -------------------------------------------------------------------------------- /source/CNAME: -------------------------------------------------------------------------------- 1 | docs.vulcanjs.org -------------------------------------------------------------------------------- /source/admin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Admin Dashboard 3 | --- 4 | 5 | The `vulcan:admin` package provides a user moderation dashboard accessible to admin users at `/admin`. 6 | 7 | ### Adding Columns 8 | 9 | You can extend the admin dashboard with your own custom columns. First, you'll need to make the relevent data available to the client by adding it to the `UsersAdmin` fragment: 10 | 11 | ```js 12 | import { extendFragment } from 'meteor/vulcan:core'; 13 | 14 | extendFragment('UsersAdmin', ` 15 | posts{ 16 | ...PostsPage 17 | } 18 | `); 19 | ``` 20 | 21 | Then, you can create a component for the dashboard cell item: 22 | 23 | ```js 24 | import React from 'react'; 25 | import Posts from 'meteor/vulcan:posts'; 26 | import { Link } from 'react-router'; 27 | 28 | const AdminUsersPosts = ({ user }) => 29 | 34 | 35 | export default AdminUsersPosts; 36 | ``` 37 | 38 | And finally add the component to the dashboard as a new column using the `addAdminColumn` function: 39 | 40 | ```js 41 | import { addAdminColumn } from 'meteor/vulcan:core'; 42 | import AdminUsersPosts from './components/AdminUsersPosts'; 43 | 44 | addAdminColumn({ 45 | name: 'users.posts', 46 | order: 50, 47 | component: AdminUsersPosts 48 | }); 49 | ``` -------------------------------------------------------------------------------- /source/api-schemas.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Schemas 3 | --- 4 | 5 | API schemas lets you define schema fields that only exist in your GraphQL API, meaning these "virtual" fields don't appear in your forms and don't get saved to your database. 6 | 7 | ### Defining API Schemas 8 | 9 | These fields support the following properties:  10 | 11 | - `canRead`: same as main schema canRead, with one notable exception: for apiSchema fields, canRead will default to [guests] (e.g. the field is public). 12 | - `description`: this is used as help text for the GraphQL API.  13 | - `typeName`: this is the GraphQL type (and not the JavaScript type, unlike "normal" fields!) the field should get resolved to. 14 | - `arguments`: the field's arguments.  15 | - `resolver`: the field's resolver function. 16 | 17 | Note that canCreate and canUpdate are not supported on apiSchema fields, since these fields by definition can not be stored in the database. 18 | 19 | For example: 20 | 21 | ```js 22 | const apiSchema = { 23 | latestPost: { 24 | canRead: ['members'], 25 | description, 26 | typeName: 'Post', 27 | resolve: () => { //... } 28 | } 29 | } 30 | ``` 31 | 32 | ### Adding API Schemas 33 | 34 | You can add an API schema to a collection with: 35 | 36 | ```js 37 | extendCollection(Posts, { 38 | apiSchema, 39 | }); 40 | ``` -------------------------------------------------------------------------------- /source/architecture_old.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Architecture 3 | --- 4 | 5 | Here's what you need to know about Vulcan's architecture. 6 | 7 | 8 | ## Package-Based Architecture 9 | 10 | In keeping with this idea of flexibility and modularity, Vulcan's application structure is a bit different than most other Meteor apps. 11 | 12 | In Vulcan, each feature has its own distinct **Meteor package** which can be freely added or removed. [Learn more about packages here](packages.html). 13 | 14 | ## Extend, Don't Edit 15 | 16 | Another key point of Vulcan's philosophy is to never edit the core codebase (i.e. what you get from the Vulcan repo) directly. 17 | 18 | Editing Vulcan's code makes it harder to keep it up to date when things change in the main Vulcan repo, as your modifications might conflict with a new updated version. 19 | 20 | Instead, Vulcan includes many hooks and patterns that enable you to tweak and extend core features from your *own* packages without having to actually modify their code. 21 | 22 | ## Register & Execute 23 | 24 | Many Vulcan objects (such as fragments, routes, components, etc.) follow a “register and execute” pattern, in which: 25 | 26 | 1. All items are first registered in a centralized array. 27 | 2. The items are then all executed at runtime. 28 | 29 | This two-tiered approach has two benefits. First, it means items can be overriden in between registration and execution. For example if a theme registers a component, your custom code can then extend it and the final component object that is created at runtime will be the extended version. 30 | 31 | Second, things like React Router or the app's GraphQL schema can only be initialized once. By having you register items first, Vulcan is able to centralize objects from various sources, and then combine them all in a single initialization call. 32 | 33 | ## The Vulcan Core Package 34 | 35 | Unless mentioned otherwise, all Vulcan utilities function are imported from the `vulcan:core` Meteor package: 36 | 37 | ```js 38 | import { createCollection } from 'meteor/vulcan:core'; 39 | 40 | const Posts = createCollection({...}); 41 | ``` 42 | 43 | Note that a lot of these utilities actually live in the `vulcan:lib` package, but they're re-exported from `vulcan:core` for convenience's sake. 44 | 45 | ## Alternative Approaches 46 | 47 | Throughout the documentation, you'll find “Alternative Approach” sections that explain what a feature does, and how to achieve the same results without using Vulcan (typically, with standard React and/or Apollo code). This is useful if you've hit the limits of what Vulcan offers, and want to refactor parts of your app to use lower-level APIs. 48 | -------------------------------------------------------------------------------- /source/card.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Card 3 | --- 4 | 5 | Coming soon… 6 | -------------------------------------------------------------------------------- /source/cheatsheet.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cheatsheet 3 | --- 4 | 5 | 6 | ## Queries 7 | 8 | ### Input 9 | 10 | ```js 11 | const singleInput = { 12 | filter, 13 | sort, 14 | search, 15 | id, 16 | enableCache, 17 | enableTotal, 18 | contextName, 19 | } 20 | 21 | const multiInput = { 22 | filter, 23 | sort, 24 | search, 25 | offset, 26 | limit, 27 | enableCache, 28 | enableTotal, 29 | contextName, 30 | } 31 | ``` 32 | 33 | ### Hooks 34 | 35 | ```js 36 | const { document, loading, error, networkStatus, refetch } = useSingle2({ 37 | collection, 38 | fragmentName, 39 | input, 40 | pollInterval, 41 | queryOptions, // passed to Apollo hook 42 | }); 43 | ``` 44 | 45 | ```js 46 | const { results, loading, error, networkStatus, refetch } = useMulti2({ 47 | collection, 48 | fragmentName, 49 | input, 50 | pollInterval, 51 | queryOptions, // passed to Apollo hook 52 | }); 53 | ``` 54 | 55 | ## Mutations 56 | 57 | ### Hooks 58 | 59 | ```js 60 | const [ createFunction, { data, loading, error }] = useCreate2({ 61 | collection, 62 | fragmentName, 63 | mutationOptions, // passed to Apollo hook 64 | }); 65 | 66 | // to use the function: 67 | createFunction({ input: { data } }); 68 | 69 | ``` 70 | 71 | ```js 72 | const [ updateFunction, { data, loading, error }] = useUpdate2({ 73 | collection, 74 | fragmentName, 75 | mutationOptions, // passed to Apollo hook 76 | }); 77 | 78 | // to use the function: 79 | updateFunction({ input: { id, data } }); 80 | ``` 81 | 82 | ```js 83 | const [ deleteFunction, { data, loading, error }] = useDelete2({ 84 | collection, 85 | fragmentName, 86 | mutationOptions, // passed to Apollo hook 87 | }); 88 | 89 | // to use the function: 90 | deleteFunction({ input: { id } }); 91 | ``` 92 | 93 | ### Mutators 94 | 95 | ```js 96 | await createMutator({ 97 | collection, // collection containing document to mutate 98 | data, // data received from client 99 | currentUser, // current user 100 | validate, // boolean, whether to validate the operation 101 | context, // GraphQL context 102 | }); 103 | 104 | await updateMutator({ 105 | documentId // id of document to mutate 106 | collection, 107 | data, 108 | currentUser, 109 | validate, 110 | context, 111 | }); 112 | 113 | await deleteMutator({ 114 | documentId, 115 | collection, 116 | currentUser, 117 | validate, 118 | context, 119 | }); 120 | ``` 121 | 122 | ### Callbacks 123 | 124 | #### Properties 125 | 126 | ```js 127 | // movies.create.* 128 | const properties = { 129 | data, 130 | originalData: clone(data), 131 | currentUser, 132 | collection, 133 | context, 134 | schema, 135 | }; 136 | 137 | // movies.update.* 138 | const properties = { 139 | data, 140 | originalData: clone(data), 141 | document, 142 | originalDocument: oldDocument, 143 | currentUser, 144 | collection, 145 | context, 146 | schema, 147 | }; 148 | 149 | // movies.delete.* 150 | const properties = { 151 | document, 152 | currentUser, 153 | collection, 154 | context, 155 | schema 156 | }; 157 | ``` 158 | 159 | #### Mutation Callbacks 160 | 161 | ```js 162 | createCollection({ 163 | callbacks { 164 | create: { 165 | validate: [(validationErrors, properties) => { return validationErrors; }], 166 | before: [(document, properties) => { return document; }], 167 | after: [(document, properties) => { return document; }], 168 | async: [(properties) => { /* no return value */ }], 169 | } 170 | update: { 171 | validate: [(validationErrors, properties) => { return validationErrors; }], 172 | before: [(data, properties) => { return data; }], 173 | after: [(document, properties) => { return document; }], 174 | async: [(properties) => { /* no return value */ }], 175 | } 176 | delete: { 177 | validate: [(validationErrors, properties) => { return validationErrors; }], 178 | before: [(document, properties) => { return document; }], 179 | after: [(document, properties) => { return document; }], 180 | async: [(properties) => { /* no return value */ }], 181 | } 182 | } 183 | }); 184 | ``` 185 | 186 | #### Field Callbacks 187 | 188 | ```js 189 | const schema = { 190 | field: { 191 | onCreate: (properties) => { return fieldValue }, 192 | onUpdate: (properties) => { return fieldValue }, 193 | onDelete: (properties) => { // return nothing } 194 | } 195 | } 196 | 197 | 198 | -------------------------------------------------------------------------------- /source/cloudinary.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cloudinary 3 | --- 4 | 5 | This package is used to **cache** a image already hosted online using [Cloudinary](http://cloudinary.com). To *upload* a local image using Cloudinary check out the [Forms Upload](/forms-upload.html) package instead. 6 | 7 | ## Config 8 | 9 | In your **private** settings: 10 | 11 | ``` 12 | "cloudinary": { 13 | "apiKey": "123foo", 14 | "apiSecret": "456bar", 15 | "formats": [ 16 | { 17 | "name": "small", 18 | "width": 180, 19 | "height": 100} 20 | , { 21 | "name": "medium", 22 | "width": 400, 23 | "height": 250 } 24 | ] 25 | }, 26 | ``` 27 | 28 | ## makeCloudinary 29 | 30 | You can enable Cloudinary caching on a collection using the `makeCloudinary` function: 31 | 32 | ```js 33 | import { Posts } from 'meteor/example-forum'; 34 | import { makeCloudinary } from 'meteor/vulcan:cloudinary'; 35 | 36 | makeCloudinary({collection: Posts, fieldName: 'thumbnailUrl'}); 37 | ``` 38 | 39 | The `collection` option indicates which collection you want to use Cloudinary with, while the `fieldName` option indicates which field you want to cache (which should contain an image URL). 40 | 41 | Note that at this time, you can only cache a single image field per collection. 42 | 43 | ## Custom Fields 44 | 45 | `makeCloudinary` will add the following custom fields to store image data: 46 | 47 | ```js 48 | [ 49 | { 50 | fieldName: 'cloudinaryId', 51 | fieldSchema: { 52 | type: String, 53 | optional: true, 54 | canRead: ['guests'], 55 | } 56 | }, 57 | { 58 | fieldName: 'cloudinaryUrls', 59 | fieldSchema: { 60 | type: Array, 61 | optional: true, 62 | canRead: ['guests'], 63 | } 64 | }, 65 | { 66 | fieldName: 'cloudinaryUrls.$', 67 | fieldSchema: { 68 | type: Object, 69 | blackbox: true, 70 | optional: true 71 | } 72 | } 73 | ] 74 | ``` 75 | 76 | In addition, the `cloudinaryUrl` GraphQL-only field is also available as a shortcut to get a specific image format's URL. 77 | 78 | It optionally takes a `format` argument when used inside a GraphQL query or fragment, for example: 79 | 80 | ``` 81 | cloudinaryUrl(format: "small") 82 | ``` 83 | 84 | ## Callbacks 85 | 86 | `makeCloudinary` will also add two callbacks on `collection.new.sync` and `collection.edit.sync` to cache your images. 87 | -------------------------------------------------------------------------------- /source/common-problems.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Common Problems 3 | --- 4 | 5 | Sometimes, things just go wrong. Here are a few tips to debug your Vulcan app. 6 | 7 | ## Common Problems 8 | 9 | ### “Load More” Not Working 10 | 11 | If your app initially loads fine but “Load More” doesn't work, this most likely indicates that while server-side rendering is working, data loading isn't. 12 | 13 | The most common cause is a mismatch between your domain and the URL currently being used as your GraphQL endpoint, which is based on the `ROOT_URL` environment variable. 14 | 15 | To double-check which GraphQL endpoint URL is being used, just use your Devtools Network tab in your browser and inspect any failed requests. 16 | 17 | ## Debugging 18 | 19 | ### Finding Errors 20 | 21 | Locating the source of an issue can be trickier than you think. 22 | 23 | First, you'll find most client-side errors through the browser console (`cmd/ctrl + opt + j` in Chrome). 24 | 25 | If nothing is showing up in the browser console or maybe your app isn't loading at all, make sure you also check the server logs. You'll find them in the terminal window currently running your app locally, or with `mup logs -f` if you've deployed using Meteor Up. 26 | 27 | It's also possible that you've encountered a GraphQL error, which won't be shown in either places. One way to track this down is to open your browser's Devtools Network tab and look for any red `graphql` requests. If you see any failed requests, check their Response tab to view the error message. 28 | 29 | You can also access a query's error message through the Redux or Apollo devtools. In the Redux devtools, click the State tab, drill down to the affected query, and check its `networkError` and `graphQLErrors` properties. 30 | 31 | ### GraphQL Schema Issues 32 | 33 | Although Vulcan includes features for generating your GraphQL schema automatically from your JSON schema, sometimes you'll run into issues and need to take a look under the hood. 34 | 35 | You can review your final generated schema by opening the Meteor shell with command `meteor shell` and then type: 36 | 37 | ``` 38 | Vulcan.getGraphQLSchema() 39 | ``` 40 | 41 | You'll probably want to paste the output in a text editor and replace `\n` with actual line breaks for better formatting. 42 | 43 | ### SSR Warnings 44 | 45 | Another common type of problem is server-side rendering (SSR) warnings: 46 | 47 | > React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server: 48 | 49 | This means that the HTML content you've rendered on the server doesn't match up with the markup React has generated on the client. For example, maybe the server thinks the current user is logged out, while the client knows they're logged in. Or maybe the server thinks it's Tuesday, while the client thinks it's Wednesday. 50 | 51 | Whatever the cause, these warnings should not break your app, but they might slow it down a bit. 52 | 53 | ### Data Loading 54 | 55 | If your data doesn't seem to be loading, this could be due to a few reasons: 56 | 57 | * **Component issue:** the client is loading your data, but your component is not displaying it. Check that the data is available in the Redux store, and check the value of your component's `props` using the React devtools. 58 | * **Resolver issue:** if your client isn't loading any data at all, it might be because the resolver isn't returning any. In this scenario, `console.log` will be your best friend. 59 | * **Arguments issue:** maybe the resolver itself is fine, but it's just not receiving the right arguments from the client. Again, adding `console.log` statements to the resolver will usually point you in the right direction. 60 | * **Database issue:** sometimes your code works, but the data you expect just isn't present in your database. You can check this by taking the query being run by the resolver and running it manually in Mongo directly (using `meteor mongo` command). 61 | 62 | ### Mutations 63 | 64 | Mutations can fail for a number of reasons: 65 | 66 | * **Component issue:** is your mutation even getting called? Make sure you double-check those event handlers. 67 | * **Resolver issue:** just like queries, mutations can also have resolver problems. 68 | * **Permissions issue:** it's also possible that the mutation is failing because you don't have the proper rights. 69 | 70 | ### Data Updating 71 | 72 | If your data isn't updating properly after a mutation, this can be due to a fragment mismatch. Make sure that `create` mutations use the same fragment as the query they're updating (`update` mutations on the other hand can safely return partial fragments). 73 | -------------------------------------------------------------------------------- /source/connecting-remotely.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Connecting Remotely 3 | --- 4 | 5 | You might sometimes need to connect to your GraphQL endpoint (typically accessible at `http://yourapp.com/graphql`) from outside your app. 6 | 7 | ### API Key 8 | 9 | You can make authorized requests to your GraphQL endpoint by defining an arbitrary `vulcan.apiKey` setting in your `settings.json` file (make sure to keep it private by keeping it out of the `public` block), and then including an `apikey` header whose value matches that API key: 10 | 11 | ``` 12 | { 13 | "vulcan": { 14 | "apiKey": "123foo" 15 | } 16 | } 17 | ``` 18 | 19 | Any request made with that header will automatically be granted admin privileges. 20 | 21 | ### Authenticating as a User 22 | 23 | Another way to perform remote API queries and mutations with special privilages is to emulate one of your app's users. You can do so by reusing their authorization token. 24 | 25 | To figure out the token, log in as the user you want to emulate and inspect any `graphql` request using your devtools' Network tab. In the Headers tab, locate the `Authorization` field of the Request Headers group and make a note of its value. 26 | 27 | ![https://d3vv6lp55qjaqc.cloudfront.net/items/0i102x2t29322u0Y2H0L/%5B9c658a6cab34c2b07a48359f5003bc0f%5D_Screen%2520Shot%25202018-02-20%2520at%252017.58.06.png?X-CloudApp-Visitor-Id=f25ee64a8f9b32be400086060540ffac&v=221c3706](https://d3vv6lp55qjaqc.cloudfront.net/items/0i102x2t29322u0Y2H0L/%5B9c658a6cab34c2b07a48359f5003bc0f%5D_Screen%2520Shot%25202018-02-20%2520at%252017.58.06.png?X-CloudApp-Visitor-Id=f25ee64a8f9b32be400086060540ffac&v=221c3706) 28 | 29 | This is the header and token you'll need to set when making your API call to the GraphQL endpoint. You can test it using a tool such as [GraphQL Playground](https://github.com/graphcool/graphql-playground) 30 | . 31 | 32 | In GraphQL Playground, select the "HTTP Headers" tab and type in (replacing the value with your own token): 33 | 34 | ``` 35 | { "Authorization": "JKdsfahyvfHvsdf234s59pNbHGH7Z2fadsdrfCmR8WimItX" } 36 | ``` 37 | 38 | ### GraphQL Clients 39 | 40 | You can send your GraphQL request as a plain text string (inspect the Request Payload object in your devtools' Network tab to see what that looks like) or you can use a GraphQL client to perform the request. 41 | 42 | Here are a few options: 43 | 44 | - Ruby: [graphql-client](https://github.com/github/graphql-client) 45 | - Node: [graphql-request](https://github.com/graphcool/graphql-request) 46 | - iOS: [apollo-ios](https://github.com/apollographql/apollo-ios) 47 | - Android: [apollo-android](https://github.com/apollographql/apollo-android) -------------------------------------------------------------------------------- /source/database.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Database Layer 3 | --- 4 | 5 | Vulcan includes its own full-featured data layer (consiting of default resolvers and default mutations) that will let you avoid writing any database code most of the time. 6 | 7 | That being said, there will still be cases where you'll need to access your database directly. 8 | 9 | ## Non-Database Methods 10 | 11 | Before addressing how to make database calls, it's worth asking if you should be calling the database directly in the first place. Vulcan features two indirect ways to query for data from the server that, while they still call the database behind the scenes, let you make your app more "database-agnostic" by helping you limit any database-specific code to a few specific places. 12 | 13 | ### Server Queries 14 | 15 | Before discussing databases, it's important to remember that a document extracted from your database will often be different from the same document as provided by the GraphQL API; since the latter can feature [special GraphQL-only fields](/field-resolvers.html#GraphQL-Only-Fields). 16 | 17 | For that reason, Vulcan also offers [server-side GraphQL queries](/server-queries.html) you can use to fetch the “GraphQL version” of a document, even on the server. This will in turn call the same GraphQL resolvers that would be normally called when receiving a query from the client. 18 | 19 | As server-side queries rely on the same resolvers as normal GraphQL queries, you can't use them inside any resolver (or you'd risk an infinite loop). 20 | 21 | ### Using Dataloader 22 | 23 | Instead, inside GraphQL resolvers you'll usually want to go through Vulcan's [Dataloader layer](/performance.html#Caching-amp-Batching) instead of calling the database directly, to make sure you don't make extra database calls. 24 | 25 | ## Connectors 26 | 27 | In Vulcan you usually don't write any database code directly, even when you do want to perform a database operation. Instead, you call a **connector**, which is the function that actually specifies how to perform the operation. 28 | 29 | For example, if you need to create a new `Movies` document you can call the `create` connector: 30 | 31 | ```js 32 | const database = 'mongo'; 33 | 34 | Connectors[database].create(Movies, { name: 'Titanic'}); 35 | ``` 36 | 37 | Which itself will call Mongo's `insert`: 38 | 39 | ```js 40 | Connectors.mongo = { 41 | create: async (collection, document, options) => { 42 | return await collection.insert(document); 43 | } 44 | } 45 | ``` 46 | 47 | The advantage of this approach is that if you one day want to use MySQL instead of Mongo, you can simply change the value of `database` in `Connectors[database]` and let the SQL connector do the rest. 48 | 49 | The following connectors are available: 50 | 51 | - `await get(collection, selector, options)`: return a single document. 52 | - `await find(collection, selector, options)`: return a list of documents. 53 | - `await count(collection, selector, options)`: return a count of documents matching the selector. 54 | - `await create(collection, document, options)`: create a new document. 55 | - `await update(collection, selector, modifier, options)`: update a document. 56 | - `await delete(collection, selector, options)`: delete a document. 57 | 58 | Note that selectors can either be objects or `_id` strings. 59 | 60 | Of course, if you don't foresee the need to support multiple databases in your own code, making direct databases call is also perfectly valid. 61 | 62 | ## Databases 63 | 64 | ### MongoDB 65 | 66 | Vulcan is powered by Meteor, which itself comes bundled with MongoDB. 67 | 68 | Meteor collections (and by extension Vulcan collections) support all common MongoDB operations, such as: 69 | 70 | - `collection.find()` 71 | - `collection.findOne()` 72 | - `collection.update()` 73 | - `collection.remove()` 74 | 75 | ### Other Databases 76 | 77 | Vulcan does not currently include out-of-the-box support for other databases. That being said, just like you can make a Mongo call in your resolver, you can also call SQL or any other data source as you normally would in any Node app. 78 | 79 | In that regard, Vulcan is fairly database-agnostic, although it's true that the default resolvers and mutations only currently work with MongoDB. 80 | -------------------------------------------------------------------------------- /source/debug.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debugging 3 | --- 4 | 5 | ## Install 6 | 7 | ``` 8 | meteor add vulcan:debug 9 | ``` 10 | 11 | ## Dashboards 12 | 13 | The package provides a few debugging dashboards: 14 | 15 | - [http://0.0.0.0:3000/debug/groups](http://0.0.0.0:3000/debug/groups): view your currently defined [groups](/groups-permissions.html). 16 | - [http://0.0.0.0:3000/debug/settings](http://0.0.0.0:3000/debug/settings): view your currently defined [settings](/settings.html). 17 | - [http://0.0.0.0:3000/debug/callbacks](http://0.0.0.0:3000/debug/callbacks): view your currently defined [callbacks](/callbacks.html). 18 | - [http://0.0.0.0:3000/debug/emails](http://0.0.0.0:3000/debug/emails): view your currently defined [emails](/emails.html). 19 | - [http://0.0.0.0:3000/debug/routes](http://0.0.0.0:3000/debug/routes): view your currently defined [routes](/routing.html) 20 | - [http://0.0.0.0:3000/debug/components](http://0.0.0.0:3000/debug/components): view your currently defined [components](/theming.html) 21 | - [http://0.0.0.0:3000/debug/i18n](http://0.0.0.0:3000/debug/i18n): view your currently defined [I18n Strings](/internationalization.html) 22 | 23 | You can find a link to all these dashboards from the parent page : [http://0.0.0.0:3000/debug](http://0.0.0.0:3000/debug) 24 | -------------------------------------------------------------------------------- /source/decorators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Field Decorators 3 | --- 4 | 5 | (Available in Vulcan 1.15+) 6 | 7 | Decorators are functions you can call on your schema fields to “augment” them and make them work with special inputs such as checkboxes, radio buttons, autocomplete inputs, and more. 8 | 9 | ## Autocomplete 10 | 11 | You can make a schema field use an autocomplete input in your forms by calling the `makeAutocomplete` decorator. 12 | 13 | It takes the field as first argument, and an `options` object as second argument that takes the following properties: 14 | 15 | - `autocompletePropertyName`: the name of the property used to serve as a label on which to autocomplete. 16 | - `queryResolverName` (optional): the name of the GraphQL resolver to query to load the list of autocomplete suggestions. 17 | 18 | For example, if you have a `Post` schema with a `categoriesIds` field, you would want to query the `categories` query resolver to load a list of categories as suggested items as the user types into the autocomplete field; and use a category's `name` as a label. 19 | 20 | Note that for relation fields (where `resolveAs.relation` exists), Vulcan will try to guess the appropriate resolver name if you don't specify it explicitly. 21 | 22 | The autocomplete input will default as a single-value autocomplete, but will act as a multi-value autocomplete for any `Array` field. Note that even for single-value autocompletes, for consistency you should pass a `queryResolverName` pointing to a *multi* resolver, even if only one item is only ever loaded. 23 | 24 | ### Example 25 | 26 | ```js 27 | import { makeAutocomplete } from 'meteor/vulcan:core'; 28 | 29 | const postSchema = { 30 | categoriesIds: makeAutocomplete({ 31 | type: Array, 32 | arrayItem: { 33 | type: String, 34 | optional: true 35 | }, 36 | label: 'Categories', 37 | optional: true, 38 | canCreate: ['members'], 39 | canUpdate: ['owners'], 40 | canRead: ['guests'], 41 | resolveAs: { 42 | fieldName: 'categories', 43 | type: '[Category]', 44 | relation: 'hasMany' 45 | } 46 | }, 47 | { queryResolverName: 'categories', autocompletePropertyName: 'name' }) 48 | } 49 | ``` 50 | 51 | ## Checkboxgroup 52 | 53 | The `makeCheckboxgroup` decorator will add a checkbox group input, and automatically add an `allowedValues` property to your schema based on the field's `options` property to restrict the field to allowed values. 54 | 55 | ```js 56 | import { makeCheckboxgroup } from 'meteor/vulcan:core'; 57 | 58 | const schema = { 59 | checkboxGroupField: makeCheckboxgroup({ 60 | type: Array, 61 | arrayItem: { 62 | type: String 63 | }, 64 | optional: true, 65 | canRead: ['guests'], 66 | canCreate: ['members'], 67 | canUpdate: ['owners'], 68 | options: fieldOptions 69 | }) 70 | } 71 | ``` 72 | 73 | ## Radiogroup 74 | 75 | The `makeRadiogroup` decorator will add a radio buttin group input, and automatically add an `allowedValues` property to your schema based on the field's `options` property to restrict the field to allowed values. 76 | 77 | ```js 78 | import { makeRadiogroup } from 'meteor/vulcan:core'; 79 | 80 | const schema = { 81 | radioGroupField: makeRadiogroup({ 82 | type: Array, 83 | arrayItem: { 84 | type: String 85 | }, 86 | optional: true, 87 | canRead: ['guests'], 88 | canCreate: ['members'], 89 | canUpdate: ['owners'], 90 | options: fieldOptions 91 | }) 92 | } 93 | ``` 94 | 95 | ## Likert 96 | 97 | The `makeLikert` decorator will add a Likert scale input, as well as automatically set the proper `Int` type for the field's array items. 98 | 99 | ```js 100 | import { makeLikert } from 'meteor/vulcan:core'; 101 | 102 | const schema = { 103 | likertField: makeLikert({ 104 | optional: true, 105 | canRead: ['guests'], 106 | canCreate: ['members'], 107 | canUpdate: ['owners'], 108 | options: [ 109 | { value: 'reservation', label: 'Reservation process' }, 110 | { value: 'ease_of_use', label: 'Ease of use' }, 111 | { value: 'design', label: 'Visual design' }, 112 | { value: 'support', label: 'Customer support' }, 113 | { value: 'properties', label: 'Number of properties' }, 114 | { value: 'accomodations', label: 'Quality of accomodations' }, 115 | { value: 'pricing', label: 'Price of accomodations' } 116 | ], 117 | }) 118 | } 119 | ``` -------------------------------------------------------------------------------- /source/error-tracking.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Error Tracking 3 | --- 4 | 5 | TODO -------------------------------------------------------------------------------- /source/errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Errors & Messages 3 | --- 4 | 5 | ## Error Properties 6 | 7 | An error object can have the following properties (all optional): 8 | 9 | - `id`: a string identifier, for example `app.not_found`. Can be used as an internationalization key. 10 | - `path`: for field-specific errors inside forms, the path of the field with the issue. 11 | - `properties`: additional data. Will be passed to vulcan-i18n as values. 12 | - `message`: if `id` cannot be used as an i81n key, `message` will be used instead. 13 | - `type`: one of `error`, `warning`, `success`. Can be used to visually differentiate between message types. 14 | - `break`: a Boolean, whether to interrupt the current operation or try to keep going. 15 | - `errors`: if the error object itself contains an `errors` array, it will be interpreted as a "multi-error" that contains multiple related sub-errors. 16 | 17 | ## GraphQL Errors 18 | 19 | When an error is thrown during a GraphQL query or mutation, it can be a good idea to use [Apollo-Errors](https://github.com/thebigredgeek/apollo-errors) instead of "regular" Node errors. This lets you build more complex errors and transmit more data back to the client. 20 | 21 | ```js 22 | import { createError } from 'apollo-errors'; 23 | 24 | const FooError = createError('FooError', { message: 'A foo error has occurred' }); 25 | 26 | throw new FooError({ data: { something: 'important' } }); 27 | ``` 28 | 29 | For GraphQL errors, all the error properties listed above should be assigned on the `data` object (including `message`). 30 | 31 | For example, here is how you could throw an error when trying to geocode an address inside a mutation callback: 32 | 33 | ```js 34 | const message = `Could not geocode address “${address}”`; 35 | const GeoDataError = createError('app.geoData_error', { message }); 36 | throw new GeoDataError({ 37 | data: { 38 | break: false, 39 | id: 'app.geoData_error', 40 | path: 'address', 41 | properties: { address }, 42 | message, 43 | }, 44 | }); 45 | ``` 46 | 47 | Note that if `app.geoData_error` has a corresponding internationalization string, the string will be used (and `{ address }` passed as `values` usable inside the string); and if it does not `message` will be used instead. 48 | 49 | ## Parsing Errors 50 | 51 | When catching a server error as a result of an operation, you can call `getErrors()` to retrieve a properly formatted array of GraphQL (or regular) errors. 52 | 53 | ## Displaying Errors 54 | 55 | Inside your Vulcan app, errors can be displayed using the `withMessages` HoC, which gives a component access to the `flash()` function to show a message. 56 | 57 | ## Form Errors 58 | 59 | Form errors are a special use case, and are thrown from inside a form component using `this.props.throwError()`. 60 | 61 | Note that when throwing a GraphQL error meant to be displayed inside a form, you can give it a `path` property to relate it to a specific form field. -------------------------------------------------------------------------------- /source/example-forum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Forum Example 3 | --- 4 | 5 | This packages enables the original Telescope forum features and components. 6 | 7 | Note that it doesn't actually contain any code by itself, but instead activates the [forum packages](packages.html) package set. 8 | 9 | ## Settings 10 | 11 | | Setting | Example | Description | 12 | | --- | --- | --- | 13 | | enableNotifications | true | Whether to enable email notifications | 14 | | postInterval | 20 | How long, in seconds, to make users wait between submitting posts | 15 | | commentInterval | 20 | How long, in seconds, to make users wait between commenting | 16 | | maxPostsPerDay | 10 | How many posts a user can submit per day | 17 | | RSSLinksPointTo | "link"/"page" | Whether to point RSS links to the linked site or back to your own site | 18 | | postsPerPage | 10 | How many posts to display per page | 19 | | thumbnailWidth | 800 | Posts thumbnails width | 20 | | thumbnailHeight | 600 | Posts thumbnails height | 21 | | scoreUpdateInterval | 30 | How often, in seconds, to update posts scores. Set to `0` to disable score updating | 22 | 23 | ## Categories 24 | 25 | You can prefill categories from your `settings.json` file: 26 | 27 | ```js 28 | "categories": [ 29 | { 30 | "name": "Test Category 1", 31 | "description": "The first test category", 32 | "order": 4, 33 | "slug": "testcat1" 34 | }, 35 | { 36 | "name": "Test Category 2", 37 | "description": "The second test category", 38 | "order": 7, 39 | "slug": "testcat2" 40 | }, 41 | { 42 | "name": "Test Category 3", 43 | "description": "The third test category", 44 | "order": 10, 45 | "slug": "testcat3" 46 | } 47 | ], 48 | ``` -------------------------------------------------------------------------------- /source/example-instagram.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Instagram Example 3 | --- 4 | 5 | Here's a [video walkthrough](https://www.youtube.com/watch?v=qibyA_ReqEQ) of [the code](https://github.com/VulcanJS/Vulcan-Starter/tree/master/packages/example-instagram) for the `example-instagram` package. 6 | 7 | ### Note: Code Changes 8 | 9 | Unlike the video, the actual package now relies on [default resolvers](/resolvers.html#Default-Resolvers) and [default mutations](/mutations.html#Default-Mutations) to simplify the code, meaning the `resolvers.js` and `mutations.js` files were removed. 10 | 11 | Also, resolvers for the `userId` and `commentsCount` fields are now specified directly in the schema instead of in the `resolvers.js` file. 12 | 13 | ### Image Upload 14 | 15 | This example uses [the `vulcan:forms-upload` package](forms-upload.html), which uploads images to [Cloudinary](http://cloudinary.com). 16 | -------------------------------------------------------------------------------- /source/example-permissions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Permissions Example 3 | --- 4 | 5 | These videos cover the [Permissions example](https://github.com/VulcanJS/Vulcan/tree/devel/packages/example-permissions/). 6 | 7 | ## Basic Permissions 8 | 9 | 10 | 11 | Learn how to define custom user groups, and then give them custom permissions on a site-wide basis. 12 | 13 | ## Advanced Permissions 14 | 15 | 16 | 17 | Learn how to define permissions related to specific documents. -------------------------------------------------------------------------------- /source/example-simple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Simple Example 3 | --- 4 | 5 | The simplest possible VulcanJS app. 6 | 7 | ## Video Tutorial 8 | 9 | 10 | -------------------------------------------------------------------------------- /source/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | --- 4 | 5 | - [ZensHome](http://zenshome.jp/) 6 | - [LessWrong](http://lesswrong.com/) 7 | - [Huttle](http://huttle.co/) 8 | - [Sidebar](http://sidebar.io) 9 | - [SmartHosts](http://smarthosts.org) 10 | - [Blogpost: The Vulcan.js challenge - 15 days for an app](https://hackernoon.com/the-vulcan-js-challenge-15-days-for-an-app-e3735d1e3d4c) 11 | 12 | If you know about something based on Vulcan.js which not this list and worth mentioning, you can [edit this page](https://github.com/VulcanJS/vulcan-docs/edit/master/source/examples.md) and submit a PR to the docs. 13 | -------------------------------------------------------------------------------- /source/features-packages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Forum Packages 3 | --- 4 | 5 | Here's an overview of the various features packages included with Vulcan. 6 | 7 | ## Forms 8 | 9 | - Dependencies: `vulcan:core` 10 | 11 | ## Accounts 12 | 13 | - Dependencies: `vulcan:core` 14 | 15 | ## Email 16 | 17 | - Dependencies: `vulcan:core` 18 | - NPM Dependencies: `handlebars` 19 | 20 | Email package. 21 | 22 | ## Events 23 | 24 | - Dependencies: `vulcan:core` 25 | - NPM Dependencies: `analytics-node` 26 | 27 | Simple internal event tracking. 28 | 29 | #### `sendGoogleAnalyticsRequest()` 30 | 31 | Log a Google Analytics request for the current page. 32 | 33 | #### `requestAnalyticsAsync(hook, document, user)` 34 | 35 | Log a Segment request. 36 | 37 | ## Debug 38 | 39 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments`, `vulcan:email` 40 | 41 | Add some special debugging routes. 42 | 43 | ## Forms-Tags 44 | 45 | ## Forms-Upload 46 | 47 | ## Kadira 48 | -------------------------------------------------------------------------------- /source/file-architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: File Architecture 3 | --- 4 | 5 | ## Package-Based Architecture 6 | 7 | Vulcan's core code follows a **file-based architecture**, which means every feature lives inside its own package which can be enabled or disabled to add or remove said feature; or even over-ridden with your own custom package of the same name. 8 | 9 | ## App Packages 10 | 11 | When building your own Vulcan app, you will typically create a new package that contains the code for your app. Although you are of course free to follow your own structure, we recommend the following file architecture. 12 | 13 | ### Root Level 14 | 15 | First, a Meteor package will contain a `package.js` package manifest that list its contents. On the same level, you will have a `lib` directory that contains the entirety of the package's code. This `lib` directory is then divided into three directories: 16 | 17 | - `/client`: code that will only run inside the browser. 18 | - `/server`: code that will only run on the server. 19 | - `/modules`: code that will run on both. 20 | 21 | Both `client` and `server` should contain a `main.js` entry point that will load client- and server- specific code as well a shared `modules` code. 22 | 23 | ### Client Directory 24 | 25 | Because Vulcan enables server-side rendering, very little code actually runs *only* on the client. This directory will thus be empty save for the `main.js` entry point that should point back to `modules/index.js`. 26 | 27 | ### Modules Directory 28 | 29 | We recommend splitting your code around your collection (a.k.a. models). For example, a forum app could have the following directories: 30 | 31 | - `/modules/posts` 32 | - `/modules/users` 33 | - `/modules/comments` 34 | 35 | Each directory will then contain all or some of the following files: 36 | 37 | - `collection.js`: the main collection/model declaration 38 | - `schema.js`: the schema definition 39 | - `fragments.js`: GraphQL fragments related to the collection 40 | - `helpers.js`: any other code related to the collection 41 | 42 | Additionally, we also recommend always including an `index.js` file to centralize all imports/exports in every directory. 43 | 44 | #### Other Code 45 | 46 | You will usually also end up with some code that is not directly related to any collection, such as components imports, route declarations, or internationalization definitions. You can leave those files in `/modules`. 47 | 48 | ### Server Directory 49 | 50 | The server directory should more or less mirror the `modules` directory in terms of being organized by collection, except it will contain code that only runs on the server, such as mutation callbacks. -------------------------------------------------------------------------------- /source/forms-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Form Components 3 | --- 4 | 5 | ## General Components 6 | 7 | The following components are used to display Vulcan forms. 8 | 9 | #### `SmartForm` 10 | 11 | This component (`FormWrapper.jsx`) wraps the entire form and handles all data loading. 12 | 13 | #### `Form` 14 | 15 | This is the main component responsible for generating and displaying the form. 16 | 17 | #### `FormErrors` 18 | 19 | Used to display errors at the top of the form. 20 | 21 | #### `FormGroup` 22 | 23 | Used to display form groups. 24 | 25 | #### `FormSubmit` 26 | 27 | Used to display the form's submit and cancel buttons. 28 | 29 | #### `FieldErrors` 30 | 31 | Used to display errors beneath a form field. 32 | 33 | #### `FormComponent` 34 | 35 | Used to display an individual form field. 36 | 37 | #### `FormNested` & `FormNestedItem` 38 | 39 | Used to display nested form items along with add/remove controls. 40 | 41 | ## Field Components 42 | 43 | In addition to the components used to display the form's structure, an additional set of components is used to display each individual field, according to its type. 44 | 45 | To select a field, use its lowercase name as the `control` property on your schema (e.g. `control: 'select'`). 46 | 47 | #### `Default` 48 | 49 | Default form input, for text strings. 50 | 51 | #### `Textarea` 52 | 53 | Textarea form input. 54 | 55 | #### `Email` 56 | 57 | Email form input. 58 | 59 | #### `Number` 60 | 61 | Number form input. Will be used automatically for `Number` fields. 62 | 63 | #### `Url` 64 | 65 | URL form input. 66 | 67 | #### `Checkbox` 68 | 69 | Checkbox form input. Will be used automatically for `Boolean` fields. 70 | 71 | #### `Checkboxgroup` 72 | 73 | Checkbox form input. 74 | 75 | #### `Select` 76 | 77 | Select form input. 78 | 79 | #### `Radiogroup` 80 | 81 | Radio group form input 82 | 83 | #### `Date`, `Time`, `Datetime` 84 | 85 | Used to select a date, a time, or both. `Date` will be used automatically for `Date` fields. -------------------------------------------------------------------------------- /source/forms-custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Form Components 3 | --- 4 | 5 | ## Custom Components 6 | 7 | You'll often need to write your own form components. Fortunately, VulcanJS makes it relatively easy. The main thing you need to figure out is how your custom component will pass its value to the form's data in order to submit it back to the server. 8 | 9 | ### Handling Values 10 | 11 | Vulcan form components are **controlled components**, meaning that they get their value from their parent component. In other words, you can't change their value directly but must instead call the `onChange` event handler, which will itself call `this.context.updateCurrentValues()`. 12 | 13 | You can also call `this.context.updateCurrentValues()` directly. This function takes an object and will update the parent `Form` component's state with its properties: 14 | 15 | ```js 16 | import React, { PureComponent } from 'react'; 17 | import PropTypes from 'prop-types'; 18 | import { Input } from 'formsy-react-components'; 19 | 20 | class MyCustomFormComponent extends PureComponent { 21 | 22 | constructor() { 23 | super(); 24 | this.toggleMessage = this.toggleMessage.bind(this); 25 | this.state = { 26 | message: 'foo' 27 | }; 28 | } 29 | 30 | toggleMessage() { 31 | this.setState({ 32 | message: this.state.message === 'foo' ? 'bar' : 'foo' 33 | }); 34 | this.context.updateCurrentValues({message: this.state.message}); 35 | } 36 | 37 | render() { 38 | 39 | return ( 40 |
41 | 42 |
43 |

Message is: {this.state.message}

44 | Toggle Message 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | MyCustomFormComponent.contextTypes = { 52 | updateCurrentValues: PropTypes.func, 53 | }; 54 | 55 | export default MyCustomFormComponent; 56 | ``` 57 | 58 | Note that you'll need to define your custom component's `contextTypes` property to make `updateCurrentValues` available on the component's `context`. 59 | 60 | ### SmartForms API 61 | 62 | Here's an overview of the API methods available on a custom component's `context`: 63 | 64 | #### `updateCurrentValues(object)` 65 | 66 | This takes an object with `name: value` pairs, and will update the form state accordingly. Note that `name` can also be a dotted path, for nested components (e.g. `{ 'addresses.2.zipCode': 12345 }`). 67 | 68 | #### `addToAutofilledValues(object)` 69 | 70 | This adds a set of `name: value` pairs to the form's *autofilled* values. Autofilled values have a lower priority than "current" values. In other words, if someone fills in the `foo` field with `bar` but you then call `addToAutofilledValues({foo: 'baz'})`, the contents of `foo` will not change. 71 | 72 | This is useful when you want the value of one field to affect the contents or another, except when that other field has already been filled out. 73 | 74 | #### `getAutofilledValues()` 75 | 76 | Get just the autofilled values. 77 | 78 | #### `addToDeletedValues(string)` 79 | 80 | This takes a single **field name** and adds it to the list of document properties to be deleted on the server once the form is submitted. 81 | 82 | Note that once a field is added to the form state's `deletedValues`, it will be deleted even if said field contains a value. 83 | 84 | #### `addToSubmitForm(function)` 85 | 86 | Adds a callback that will be called on the `data` object containing all form values when the form is submitted. 87 | 88 | #### `addToFailureForm(function)` 89 | 90 | Adds a callback that will run with the `error` as argument if the form submission fails. 91 | 92 | #### `addToSuccessForm(function)` 93 | 94 | Adds a callback that will run with the `result` as argument if the form submission succeeds. 95 | 96 | #### `throwError(object)` 97 | 98 | Throws an error. 99 | 100 | #### `clearForm()` 101 | 102 | Clears the form. 103 | 104 | #### `getDocument()` 105 | 106 | Gets the entire document currently being inserted or edited. 107 | 108 | #### `setFormState(object)` 109 | 110 | Calls `setState` inside the `Form` component directly. Can be used as a more general method to affect form state whenever the previous API methods are not sufficient. For example, to set the form as disabled: 111 | 112 | ``` 113 | this.context.setFormState({disabled: true}); 114 | ``` 115 | -------------------------------------------------------------------------------- /source/forms-upload.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Upload Package 3 | --- 4 | 5 | This is a Vulcan package extending `vulcan:forms` to support form components for uploading one or more images. 6 | 7 | ![Screenshot](https://res.cloudinary.com/xavcz/image/upload/v1471534203/Capture_d_e%CC%81cran_2016-08-17_14.22.14_ehwv0d.png) 8 | 9 | Note: although this package only supports Cloudinary currently, PRs to support additional providers (S3, etc.) are warmly encouraged. 10 | 11 | ## Dependencies 12 | 13 | This package depends on the awesome `react-dropzone` ([repo](https://github.com/okonet/react-dropzone)), you need to install the dependency: 14 | ``` 15 | npm install react-dropzone isomorphic-fetch 16 | ``` 17 | 18 | ## With Cloudinary 19 | 20 | ### Setup 21 | 22 | Create a [Cloudinary account](https://cloudinary.com) if you don't have one. 23 | 24 | The upload to Cloudinary relies on **unsigned upload**: 25 | 26 | > Unsigned upload is an option for performing upload directly from a browser or mobile application with no authentication signature, and without going through your servers at all. However, for security reasons, not all upload parameters can be specified directly when performing unsigned upload calls. 27 | 28 | Unsigned upload options are controlled by [an upload preset](http://cloudinary.com/documentation/upload_images#upload_presets), so in order to use this feature you first need to enable unsigned uploading for your Cloudinary account from the [Upload Settings](https://cloudinary.com/console/settings/upload) page. 29 | 30 | When creating your **preset**, you can define image transformations. I recommend to set something like 200px width & height, fill mode and auto quality. Once created, you will get a preset id. 31 | 32 | It may look like this: 33 | 34 | ![Screenshot-Cloudinary](https://res.cloudinary.com/xavcz/image/upload/v1471534183/Capture_d_e%CC%81cran_2016-08-18_17.07.52_tr9uoh.png) 35 | 36 | Make sure that your **preset** is named the same between Cloudinary and the one you define in the schema. Otherwise, the upload will request will fail. 37 | 38 | ### Settings 39 | 40 | Edit your `settings.json` and add inside the `public: { ... }` block the following entries with your own credentials: 41 | 42 | ```json 43 | "public": { 44 | "cloudinary": { 45 | "cloudName": "YOUR_APP_NAME", 46 | } 47 | } 48 | ``` 49 | 50 | ### Specifying a Preset 51 | 52 | If you'd like to specify a Cloudinary preset to be used to resize, convert, etc. your images, you can do so in the properties of the **form field**: 53 | 54 | ```js 55 | photos: { 56 | label: 'Photos', 57 | type: Array, 58 | optional: false, 59 | canRead: ['guests'], 60 | canCreate: ['members'], 61 | canUpdate: ['members'], 62 | control: FormsUpload, // use the FormsUpload form component 63 | form: { 64 | options: { 65 | preset: 'myCloudinaryPreset' 66 | }, 67 | }, 68 | }, 69 | ``` 70 | 71 | ### Adding images to Posts (as an example) 72 | If you want to add images to a certain collection. Add the custom field, make sure to add this in your fragments.js file: 73 | 74 | ``` extendFragment('PostsList', ` 75 | image 76 | `); 77 | 78 | extendFragment('PostsPage', ` 79 | image 80 | `);``` 81 | 82 | You can now use the uploaded image anywhere in your Vulcan app, using this: 83 | `````` 84 | which will look for the image field from your post. 85 | -------------------------------------------------------------------------------- /source/forum-packages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Forum Packages 3 | --- 4 | 5 | Here's an overview of the various forum packages included with Vulcan. 6 | 7 | Note that most of these packages currently require the `vulcan:posts` package. Eventually, the goal is to generalize them so they can work with any Vulcan collection. 8 | 9 | ## Posts 10 | 11 | - Dependencies: `vulcan:core`, `vulcan:events` 12 | 13 | The Vulcan Posts package. 14 | 15 | ## Comments 16 | 17 | - Dependencies: `vulcan:core`, `vulcan:posts` 18 | 19 | The Vulcan Comments package. 20 | 21 | ## Notifications 22 | 23 | - Dependencies: `vulcan:core`, `vulcan:email` 24 | 25 | Email notifications. 26 | 27 | ## Voting 28 | 29 | - Dependencies: `vulcan:core`, `vulcan:posts` 30 | 31 | Enables upvoting and downvoting for the Posts and Comments collections. 32 | 33 | ## Embedly 34 | 35 | - Dependencies: `vulcan:core`, `vulcan:posts` 36 | 37 | Use [Embedly](http://embed.ly) to load extra metadata for posts. 38 | 39 | ## Categories 40 | 41 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments` 42 | 43 | Posts categories. 44 | 45 | ## API 46 | 47 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments` 48 | 49 | Create a JSON API for posts and comments. 50 | 51 | ## RSS 52 | 53 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments` 54 | 55 | Create RSS feeds for posts and comments. 56 | 57 | ## Newsletter 58 | 59 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments`, `vulcan:categories`, `vulcan:email` 60 | 61 | Generate and schedule a MailChimp newsletter for posts and comments. 62 | 63 | ## Email Templates 64 | 65 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments`, `vulcan:email` 66 | 67 | Templates for email notifications and the newsletter. 68 | 69 | ## Getting Started 70 | 71 | - Dependencies: `vulcan:core`, `vulcan:posts`, `vulcan:comments`, `vulcan:events` 72 | 73 | Dummy content for posts, comments, and users. -------------------------------------------------------------------------------- /source/fragments.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fragments 3 | --- 4 | 5 | A fragment is a piece of schema, usually used to define what data you want to query for. 6 | 7 | ## Registering Fragments 8 | 9 | You can register fragments by passing the fragment string to `registerFragment`: 10 | 11 | ```js 12 | import { registerFragment } from 'meteor/vulcan:core'; 13 | 14 | registerFragment(` 15 | fragment PostsList on Post { 16 | _id 17 | title 18 | url 19 | slug 20 | } 21 | `); 22 | ``` 23 | 24 | ## Using Fragments 25 | 26 | You can get a fragment with: 27 | 28 | ```js 29 | import { getFragment } from 'meteor/vulcan:lib'; 30 | 31 | getFragment('PostsList'); 32 | ``` 33 | 34 | ## Sub-Fragments 35 | 36 | When registering a fragment, you'll often want to use sub-fragments to avoid repeating frequently used properties. For example, the `PostsList` fragment uses the `UserMinimumInfo` fragment: 37 | 38 | ```js 39 | registerFragment(` 40 | fragment PostsList on Post { 41 | # vulcan:posts 42 | _id 43 | title 44 | url 45 | slug 46 | user { 47 | ...UsersMinimumInfo 48 | } 49 | } 50 | `); 51 | ``` 52 | 53 | Note that in “regular” Apollo code, you need to include any sub-fragment used by a fragment [as tagged template literal](http://dev.apollodata.com/react/fragments.html#reusing-fragments), but Vulcan takes care of this for you (provided you've previously registered any sub-fragment using `registerFragment`). 54 | 55 | ## Extending Fragments 56 | 57 | You often need to add one or more properties to a fragment without modifying its existing properties. You can do this with `extendFragment`: 58 | 59 | ```js 60 | import { extendFragment } from 'meteor/vulcan:lib'; 61 | 62 | extendFragment( 63 | 'PostsList', 64 | ` 65 | color # new custom property! 66 | ` 67 | ); 68 | ``` 69 | 70 | This is the same as registering the entire `PostsList` fragment with `color` tacked on at the end. 71 | 72 | ## Replacing Fragments 73 | 74 | To replace a fragment completely, you can just register it again under the same name. 75 | 76 | ```js 77 | import { registerFragment } from 'meteor/vulcan:lib'; 78 | 79 | registerFragment(` 80 | fragment UsersMinimumInfo on User { 81 | _id 82 | slug 83 | username 84 | # displayName # remove this 85 | # emailHash # and this 86 | mySuperCustomProperty # but add this 87 | } 88 | `); 89 | ``` 90 | 91 | Note that you can replace both “regular” fragments and sub-fragments. 92 | 93 | ## Default Fragments 94 | 95 | Every collection automatically gets a default fragment associated with it called `FooDefaultFragment` (for example `PostsDefaultFragment`). 96 | 97 | This default fragment simply contain all fields where `canRead` is defined (in other words, all public fields). Note that it **does not** follow field resolvers, meaning that the default fragment will e.g. include `userId` but not `user`. 98 | 99 | #### Alternative Approach 100 | 101 | You can use standard Apollo fragments at any point in your Vulcan app (passing them as `fragment` instead of `fragmentName`), but be aware that you will lose the ability to extend and replace fragments. You will also need to [manually specify](http://dev.apollodata.com/react/fragments.html#reusing-fragments) sub-fragments. 102 | -------------------------------------------------------------------------------- /source/graphql-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GraphQL Schema 3 | --- 4 | 5 | The first piece of any GraphQL API is the **schema**, which defines what data is made available to the client. 6 | 7 | In Vulcan, this GraphQL schema can be generated automatically from your collection's JSON schema, so you don't have to type things twice. Just pass a [SimpleSchema](https://github.com/aldeed/node-simple-schema)-compatible JSON schema as `createCollection`'s `schema` property. 8 | 9 | ## Viewing the Schema 10 | 11 | You can view your GraphQL schema using the Meteor shell. First, launch the shell from your terminal with: 12 | 13 | ``` 14 | meteor shell 15 | ``` 16 | 17 | Then, type: 18 | 19 | ``` 20 | Vulcan.getGraphQLSchema() 21 | ``` 22 | 23 | Note: if `Vulcan` is not defined you can import it first with `import {Vulcan} from 'meteor/vulcan:lib'`. 24 | 25 | ## Custom Schemas 26 | 27 | If you need to manually add a schema, you can also do this using the `addGraphQLSchema` function which will add one or more [GraphQL schemas](http://graphql.org/learn/schema/) to your GraphQL API: 28 | 29 | ```js 30 | import { addGraphQLSchema } from 'meteor/vulcan:core'; 31 | 32 | const customSchema = ` 33 | input Custom { 34 | _id: String 35 | userId: String 36 | title: String 37 | } 38 | `; 39 | addGraphQLSchema(customSchema); 40 | ``` 41 | -------------------------------------------------------------------------------- /source/head-tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Head Tags 3 | --- 4 | 5 | You'll often need to add tags to your app's `` section. There are three ways to do this: using the [Helmet](https://github.com/nfl/react-helmet) library, using the `` component, or using the `Head` object. 6 | 7 | ## Helmet 8 | 9 | You can use Helmet inside a VulcanJS app just like inside any other React app: 10 | 11 | ```js 12 | const Layout = ({children}) => 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | {children} 24 | 25 |
26 | 27 |
28 | 29 | registerComponent('Layout', Layout); 30 | ``` 31 | 32 | ## Components.HeadTags 33 | 34 | ### Using HeadTags 35 | 36 | Out of the box, a VulcanJS app adds a few default tags such as ``, `<meta name='description'/>`, etc. These are all added using Helmet by `Components.HeadTags` (called from inside the `App` component), using values provided by your `settings.json` file. 37 | 38 | In some cases, you'll want to change these values for a given page. For example, a single post page will want to have a different title than your homepage. You can do that by calling `Components.HeadTags` directly anywhere inside your component tree with one or more of the following props: 39 | 40 | - `title` 41 | - `url` 42 | - `image` 43 | - `description` 44 | 45 | ```js 46 | <Components.HeadTags url={Posts.getPageUrl(post, true)} title={post.title} image={post.thumbnailUrl} description={post.excerpt} /> 47 | ``` 48 | 49 | By calling `HeadTags` again, you'll be calling Helmet again, which in turn will override the head tags previously set by the `App` component. 50 | 51 | ### Overriding Headtags 52 | 53 | Since `Components.HeadTags` is a component, you can also override it completely using `replaceComponent('HeadTags', MyComponent)`. 54 | 55 | ## The Head Object 56 | 57 | Finally, there are also some cases where you want to add or remove a tag from *outside* your React components. You can do so using the `Head` object. 58 | 59 | For example. here is how the `vulcan:rss` package adds an RSS feed link to the head: 60 | 61 | ```js 62 | import { Head, Utils } from 'meteor/vulcan:core'; 63 | 64 | Head.link.push({ 65 | name: 'rss', 66 | rel: 'alternate', 67 | type: 'application/rss+xml', 68 | href: `${Utils.getSiteUrl()}feed.xml` 69 | }); 70 | ``` 71 | 72 | You can also remove a tag using `removeFromHeadTags`: 73 | 74 | ```js 75 | import { removeFromHeadTags } from 'meteor/vulcan:core'; 76 | 77 | removeFromHeadTags('link', 'rss'); 78 | ``` 79 | 80 | ### Head Components 81 | 82 | You can also use the `Head` object to add React components to every page of your app (this can be useful for adding analytics integrations wrapped inside React components, for example). Just push the components to `Head.components`. 83 | 84 | If you push an array (such as `[myComponent, withCurrentUser]`) the array will be interpreted as a component + HoC array similar to the arguments of `registerComponent`. -------------------------------------------------------------------------------- /source/images/how-vulcan-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-docs/3cb0cd142609b9efc00ead23c30de424a304a605/source/images/how-vulcan-works.png -------------------------------------------------------------------------------- /source/learn.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Learn More 3 | --- 4 | 5 | - [React for Beginners](https://reactforbeginners.com) 6 | - [Meteor Documentation](http://docs.meteor.com/) 7 | - [Apollo Documentation](http://dev.apollodata.com/) -------------------------------------------------------------------------------- /source/logo/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-docs/3cb0cd142609b9efc00ead23c30de424a304a605/source/logo/favicon.png -------------------------------------------------------------------------------- /source/logo/icon-apollo-white-200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-docs/3cb0cd142609b9efc00ead23c30de424a304a605/source/logo/icon-apollo-white-200x200.png -------------------------------------------------------------------------------- /source/logo/logo-apollo-space-left.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 4 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 5 | width="807.682px" height="331.561px" viewBox="0 0 807.682 331.561" enable-background="new 0 0 807.682 331.561" 6 | xml:space="preserve"> 7 | <g id="Layer_1_1_"> 8 | </g> 9 | <g id="logo_apollo_white"> 10 | <g> 11 | <polygon fill="#333333" points="130.863,114.38 106.552,114.38 71.451,205.477 93.44,205.477 99.177,190.045 132.343,190.045 12 | 126.339,172.971 104.5,172.971 118.706,133.773 143.976,205.477 165.964,205.477 "/> 13 | <path fill="#333333" d="M500.165,205.477V114.38h19.528v74.024h38.517v17.072H500.165z"/> 14 | <path fill="#333333" d="M612.729,205.477V114.38h19.531v74.024h38.516v17.072H612.729z"/> 15 | <path fill="#333333" d="M391.998,130.705c16.152,0,29.293,13.14,29.293,29.292c0,16.151-13.141,29.292-29.293,29.292 16 | c-16.151,0-29.292-13.141-29.292-29.292C362.706,143.842,375.847,130.705,391.998,130.705 M391.998,112.88 17 | c-26.022,0-47.118,21.096-47.118,47.117c0,26.023,21.096,47.12,47.118,47.12s47.12-21.097,47.12-47.12 18 | C439.118,133.974,418.021,112.88,391.998,112.88L391.998,112.88z"/> 19 | <path fill="#333333" d="M760.561,130.705c16.154,0,29.295,13.14,29.295,29.292c0,16.151-13.141,29.292-29.295,29.292 20 | c-16.152,0-29.291-13.141-29.291-29.292C731.27,143.842,744.409,130.705,760.561,130.705 M760.561,112.88 21 | c-26.021,0-47.119,21.096-47.119,47.117c0,26.023,21.101,47.12,47.119,47.12c26.023,0,47.121-21.097,47.121-47.12 22 | C807.682,133.974,786.585,112.88,760.561,112.88L760.561,112.88z"/> 23 | <path fill="#333333" d="M265.263,114.38h-23.082h-3.312h-16.084v91.099h19.396v-31.353h23.082 24 | c16.189,0,29.314-13.685,29.314-29.874C294.577,128.061,281.452,114.38,265.263,114.38z M265.263,156.298h-23.082v-24.089h23.082 25 | c6.336,0,11.486,5.711,11.486,12.045C276.749,150.589,271.599,156.298,265.263,156.298z"/> 26 | <g> 27 | <path fill="#333333" d="M206.038,233.124c-1.688,0-3.178,0.836-4.088,2.118c0,0-4.375,4.991-6.732,7.354 28 | c-9.979,9.979-21.6,17.812-34.533,23.281c-13.387,5.663-27.611,8.534-42.281,8.534c-14.672,0-28.896-2.871-42.281-8.534 29 | c-12.936-5.472-24.555-13.305-34.532-23.281c-9.979-9.981-17.813-21.602-23.285-34.534c-5.66-13.39-8.533-27.613-8.533-42.282 30 | s2.873-28.895,8.533-42.281c5.472-12.936,13.306-24.554,23.285-34.533s21.598-17.812,34.532-23.285 31 | c13.386-5.661,27.609-8.532,42.281-8.532c14.67,0,28.896,2.871,42.281,8.532c9.422,3.986,18.141,9.23,26.041,15.641 32 | c-0.422,1.243-0.658,2.576-0.658,3.963c0,6.77,5.488,12.258,12.256,12.258c6.771,0,12.258-5.488,12.258-12.258 33 | c0-6.771-5.487-12.257-12.258-12.257c-1.68,0-3.283,0.339-4.742,0.953c-20.467-16.782-46.645-26.858-75.178-26.858 34 | c-65.535,0-118.66,53.125-118.66,118.66s53.125,118.661,118.66,118.661c36.658,0,69.42-16.636,91.185-42.758 35 | c0.905-0.907,1.469-2.16,1.469-3.543C211.056,235.367,208.812,233.124,206.038,233.124z"/> 36 | </g> 37 | </g> 38 | </g> 39 | <g> 40 | </g> 41 | </svg> 42 | -------------------------------------------------------------------------------- /source/logo/logo-apollo-space.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 4 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 5 | width="972.627px" height="331.561px" viewBox="0 0 972.627 331.561" enable-background="new 0 0 972.627 331.561" 6 | xml:space="preserve"> 7 | <symbol id="logo_apollo_space" viewBox="-486.326 -165.78 972.652 331.561"> 8 | <g> 9 | <polygon fill="#333333" points="-284.735,51.4 -309.046,51.4 -344.147,-39.697 -322.157,-39.697 -316.421,-24.266 10 | -283.255,-24.266 -289.259,-7.191 -311.095,-7.191 -296.89,32.006 -271.622,-39.697 -249.634,-39.697 "/> 11 | <path fill="#333333" d="M84.567-39.697V51.4h19.531v-74.025h38.514v-17.072H84.567z"/> 12 | <path fill="#333333" d="M197.132-39.697V51.4h19.531v-74.025h38.516v-17.072H197.132z"/> 13 | <path fill="#333333" d="M-23.599,35.074c16.152,0,29.293-13.139,29.293-29.292c0-16.151-13.141-29.292-29.293-29.292 14 | S-52.89-10.369-52.89,5.782C-52.89,21.936-39.751,35.074-23.599,35.074 M-23.599,52.901c-26.023,0-47.119-21.096-47.119-47.119 15 | s21.096-47.12,47.119-47.12s47.119,21.097,47.119,47.12S2.425,52.901-23.599,52.901L-23.599,52.901z"/> 16 | <path fill="#333333" d="M344.964,35.074c16.154,0,29.295-13.139,29.295-29.292c0-16.151-13.141-29.292-29.295-29.292 17 | c-16.152,0-29.291,13.141-29.291,29.292C315.673,21.936,328.812,35.074,344.964,35.074 M344.964,52.901 18 | c-26.021,0-47.119-21.096-47.119-47.119s21.098-47.12,47.119-47.12c26.023,0,47.121,21.097,47.121,47.12 19 | S370.987,52.901,344.964,52.901L344.964,52.901z"/> 20 | <path fill="#333333" d="M-150.335,51.4h-23.082h-3.311h-16.084v-91.098h19.395v31.35h23.082c16.191,0,29.316,13.685,29.316,29.874 21 | C-121.019,37.718-134.144,51.4-150.335,51.4z M-150.335,9.481h-23.082v24.091h23.082c6.336,0,11.488-5.711,11.488-12.046 22 | S-143.999,9.481-150.335,9.481z"/> 23 | <g> 24 | <path fill="#333333" d="M-209.558-67.345c-1.688,0-3.178-0.836-4.088-2.118c0,0-4.375-4.991-6.734-7.353 25 | c-9.979-9.979-21.598-17.812-34.533-23.284c-13.387-5.663-27.611-8.534-42.281-8.534c-14.672,0-28.896,2.871-42.281,8.534 26 | c-12.936,5.472-24.555,13.305-34.533,23.284c-9.98,9.981-17.814,21.599-23.285,34.534c-5.661,13.387-8.533,27.613-8.533,42.282 27 | s2.872,28.895,8.533,42.282c5.471,12.935,13.305,24.553,23.285,34.532c9.979,9.98,21.598,17.813,34.533,23.285 28 | c13.385,5.663,27.609,8.534,42.281,8.534c14.67,0,28.895-2.871,42.281-8.534c9.422-3.985,18.141-9.23,26.041-15.639 29 | c-0.422-1.243-0.658-2.576-0.658-3.963c0-6.769,5.488-12.257,12.256-12.257c6.77,0,12.258,5.488,12.258,12.257 30 | c0,6.77-5.488,12.257-12.258,12.257c-1.68,0-3.283-0.339-4.742-0.953c-20.467,16.782-46.645,26.858-75.178,26.858 31 | c-65.535,0-118.661-53.125-118.661-118.66s53.126-118.661,118.661-118.661c36.658,0,69.42,16.636,91.184,42.758 32 | c0.906,0.907,1.469,2.16,1.469,3.543C-204.542-69.588-206.786-67.345-209.558-67.345z"/> 33 | </g> 34 | </g> 35 | <polygon fill="none" points="486.325,-165.78 -486.326,-165.78 -486.326,165.78 486.325,165.78 "/> 36 | </symbol> 37 | <g id="Layer_1"> 38 | </g> 39 | <g id="Layer_2"> 40 | 41 | <use xlink:href="#logo_apollo_space" width="972.652" height="331.561" x="-486.326" y="-165.78" transform="matrix(1 0 0 -1 486.3135 165.7803)" overflow="visible"/> 42 | </g> 43 | </svg> 44 | -------------------------------------------------------------------------------- /source/logo/logo-apollo-subbrands-developers-space.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 4 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 5 | width="78.688px" height="13.622px" viewBox="0 0 78.688 13.622" enable-background="new 0 0 78.688 13.622" xml:space="preserve"> 6 | <g> 7 | <path fill="#333333" d="M0,10.676V0h3.648c3.33,0,5.475,2.321,5.475,5.347c0,3.041-2.146,5.329-5.475,5.329H0z M7.747,5.347 8 | c0-2.306-1.457-4.162-4.099-4.162H1.329v8.308h2.32C6.243,9.491,7.747,7.635,7.747,5.347z"/> 9 | <path fill="#333333" d="M10.704,6.803c0-2.241,1.601-4.05,3.81-4.05c2.337,0,3.713,1.824,3.713,4.146v0.303h-6.243 10 | c0.097,1.457,1.104,2.673,2.77,2.673c0.896,0,1.776-0.352,2.4-0.992l0.576,0.785c-0.785,0.769-1.825,1.199-3.072,1.199 11 | C12.353,10.868,10.704,9.22,10.704,6.803z M14.498,3.745c-1.649,0-2.465,1.393-2.529,2.562h5.074 12 | C17.026,5.17,16.258,3.745,14.498,3.745z"/> 13 | <path fill="#333333" d="M22.225,10.676l-3.217-7.73h1.312l2.562,6.34l2.577-6.34h1.296l-3.217,7.73H22.225z"/> 14 | <path fill="#333333" d="M27.552,6.803c0-2.241,1.601-4.05,3.81-4.05c2.338,0,3.714,1.824,3.714,4.146v0.303h-6.243 15 | c0.096,1.457,1.104,2.673,2.769,2.673c0.896,0,1.777-0.352,2.401-0.992l0.576,0.785c-0.785,0.769-1.825,1.199-3.073,1.199 16 | C29.2,10.868,27.552,9.22,27.552,6.803z M31.345,3.745c-1.649,0-2.465,1.393-2.529,2.562h5.074 17 | C33.874,5.17,33.106,3.745,31.345,3.745z"/> 18 | <path fill="#333333" d="M37.008,10.676V0h1.2v10.676H37.008z"/> 19 | <path fill="#333333" d="M40.159,6.803c0-2.241,1.473-4.05,3.824-4.05c2.354,0,3.826,1.809,3.826,4.05 20 | c0,2.24-1.473,4.064-3.826,4.064C41.632,10.868,40.159,9.043,40.159,6.803z M46.545,6.803c0-1.569-0.912-2.978-2.562-2.978 21 | c-1.647,0-2.561,1.408-2.561,2.978c0,1.585,0.913,2.992,2.561,2.992C45.633,9.795,46.545,8.388,46.545,6.803z"/> 22 | <path fill="#333333" d="M50.96,9.508v4.113h-1.201V2.945h1.201v1.152c0.561-0.784,1.52-1.345,2.608-1.345 23 | c2.032,0,3.441,1.537,3.441,4.05c0,2.498-1.409,4.066-3.441,4.066C52.513,10.868,51.6,10.356,50.96,9.508z M55.745,6.803 24 | c0-1.713-0.929-2.978-2.48-2.978c-0.944,0-1.889,0.561-2.305,1.232v3.49c0.416,0.672,1.359,1.248,2.305,1.248 25 | C54.817,9.795,55.745,8.516,55.745,6.803z"/> 26 | <path fill="#333333" d="M58.463,6.803c0-2.241,1.601-4.05,3.81-4.05c2.337,0,3.713,1.824,3.713,4.146v0.303h-6.241 27 | c0.096,1.457,1.104,2.673,2.77,2.673c0.896,0,1.776-0.352,2.4-0.992l0.576,0.785c-0.784,0.769-1.824,1.199-3.072,1.199 28 | C60.111,10.868,58.463,9.22,58.463,6.803z M62.257,3.745c-1.649,0-2.465,1.393-2.529,2.562h5.074 29 | C64.785,5.17,64.017,3.745,62.257,3.745z"/> 30 | <path fill="#333333" d="M67.919,10.676V2.945h1.2v1.232c0.624-0.8,1.521-1.409,2.576-1.409V4c-0.145-0.016-0.287-0.031-0.465-0.031 31 | c-0.752,0-1.76,0.607-2.111,1.232v5.475H67.919L67.919,10.676z"/> 32 | <path fill="#333333" d="M72.542,9.651l0.607-0.863c0.514,0.592,1.489,1.104,2.545,1.104c1.186,0,1.842-0.562,1.842-1.328 33 | c0-1.873-4.785-0.722-4.785-3.586c0-1.217,1.022-2.225,2.865-2.225c1.327,0,2.256,0.496,2.864,1.121L77.92,4.706 34 | c-0.465-0.561-1.297-0.977-2.305-0.977c-1.058,0-1.714,0.528-1.714,1.217c0,1.68,4.785,0.576,4.785,3.586 35 | c0,1.297-1.041,2.336-3.024,2.336C74.383,10.868,73.327,10.467,72.542,9.651z"/> 36 | </g> 37 | </svg> 38 | -------------------------------------------------------------------------------- /source/newsletter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Newsletter 3 | --- 4 | 5 | 6 | This package automatically generates and sends a newsletter for posts and comments on a predefined schedule. 7 | 8 | ![Newsletter](http://f.cl.ly/items/0V0F351k1R1i3L1k1D0J/telescope-newsletter.png) 9 | 10 | Every `x` days, it builds a digest consisting of the top `y` items posted in the past `x` days that haven't yet been sent out in a newsletter. It then sends the campaign, and sends you a confirmation email. 11 | 12 | Note: this package requires using a third-party email provider ([MailChimp](http://mailchimp.com) and [Sendy](http://sendy.co) are currently supported). 13 | 14 | ## Install 15 | 16 | `meteor add vulcan:newsletter` 17 | 18 | ## Dependencies 19 | 20 | #### Vulcan Dependencies 21 | 22 | - vulcan:core 23 | - vulcan:posts 24 | - vulcan:comments 25 | - vulcan:categories 26 | - vulcan:email 27 | 28 | #### NPM Dependencies 29 | 30 | - [mailchimp](https://github.com/gomfunkel/node-mailchimp) 31 | - [sendy-api](https://github.com/igord/sendy-api) 32 | 33 | ## Settings 34 | 35 | ``` 36 | "newsletter": { 37 | "enabled":true, 38 | "enabledInDev":false, 39 | "autoSubscribe":false, 40 | "frequency": [1,2,3,4,5,6,7], 41 | "time": "00:00", 42 | "provider": "mailchimp" 43 | }, 44 | "sendy": { 45 | "server": "http://sendy.myapp.com/", 46 | "apiKey": "123foo", 47 | "listId": "456bar", 48 | "fromName": "Bruce Willis", 49 | "fromEmail": "hello@myapp.com", 50 | "replyTo": "hello@myapp.com" 51 | }, 52 | "mailchimp": { 53 | "apiKey": "123foo", 54 | "listId": "456bar", 55 | "fromName": "hello@myapp.com", 56 | "fromEmail": "hello@myapp.com" 57 | }, 58 | ``` 59 | 60 | - `enabled`: enable/disable automated newsletter sending. 61 | - `enabledInDev`: enable/disable newsletter while in development mode. 62 | - `autoSubscribe`: automatically subscribe every new user to your newsletter. 63 | - `frequency`: which days the newsletter should go out. `1` is Monday, `2` is Tuesday, etc. 64 | - `time`: what time the newsletter should go out. 65 | - `provider`: the name of your newsletter provider. 66 | 67 | #### Sendy Settings 68 | 69 | - `server`: The URL to your self-hosted Sendy server (including trailing `/`). 70 | - `apiKey`: Your API key. 71 | - `listId`: List ID. 72 | - `fromName`: "From" name. 73 | - `fromEmail`: "From" email. 74 | - `replyTo`: "Reply to" email. 75 | 76 | #### MailChimp Settings 77 | 78 | - `apiKey`: Your API key. 79 | - `listId`: List ID. 80 | 81 | ## Test Routes 82 | 83 | If you want to preview your email templates, you can do so at the following routes: 84 | 85 | - **Campaign**: [http://localhost:3000/email/newsletter](http://localhost:3000/email/newsletter) 86 | - **Confirmation**: [http://localhost:3000/email/newsletter-confirmation](http://localhost:3000/email/newsletter-confirmation) 87 | 88 | (Replace `http://localhost:3000` with your app's URL) 89 | 90 | ## Mutations 91 | 92 | The package exposes the following GraphQL mutations: 93 | 94 | - `sendNewsletter`: generate and send the next newsletter. 95 | - `testNewsletter`: same as `sendNewsletter`, but without marking posts as sent. 96 | - `addUserNewsletter(userId: String)`: add a user to your subscriber list. 97 | - `addEmailNewsletter(email: String)`: add an email to your subscriber list. 98 | - `removeUserNewsletter(userId: String)`: remove a user from your subscriber list. 99 | 100 | You can call these mutations from any React component using the [`withMutation` HoC](mutations.html#Higher-Order-Components): 101 | 102 | ``` 103 | import React, { PropTypes, Component } from 'react'; 104 | import { withMutation } from 'meteor/vulcan:core'; 105 | 106 | const SendButton = ({ sendNewsletter }) => <button onClick={sendNewsletter}>Send Newsletter</button> 107 | 108 | export default withMutation({name: 'sendNewsletter'})(SendButton); 109 | ``` 110 | 111 | ## Providers 112 | 113 | To add a provider, you can start from the `sample.js` template included in the package. 114 | 115 | Any new provider must implement the following methods: 116 | 117 | - `Newsletters.providerName.subscribe(email)`: subscribe an email to your list. 118 | - `Newsletters.providerName.unsubscribe(email)`: unsubscribe an email from your list. 119 | - `Newsletters.providerName.send({ title, subject, text, html, isTest})`: send a newsletter. 120 | 121 | ## NewsletterSubscribe Component 122 | 123 | This package also exports a `NewsletterSubscribe` custom form field component used to add a subscribe/unsubscribe button to the user account page. 124 | 125 | ## Settings 126 | 127 | | Setting | Example | Description | 128 | | --- | --- | --- | 129 | | enabled | false | Enable the automated newsletter | 130 | | enabledInDev | false | Enable the automated newsletter while in development mode | 131 | | frequency | [1,2,3,4,5,6,7] | The days on which to send the newsletter (1 = Monday, 2 = Tuesday, etc.) | 132 | | time | "07:55" | When to send out the newsletter 133 | | autoSubscribe | false | Whether new users should be automatically subscribed to the newsletter | -------------------------------------------------------------------------------- /source/notifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Notifications 3 | --- 4 | 5 | Coming soon… -------------------------------------------------------------------------------- /source/nutshell.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vulcan in a Nutshell 3 | --- 4 | 5 | The best way to understand how Vulcan works is to consider its three main aspects: the role of the schema, how Vulcan reads data, and how Vulcan writes data. 6 | 7 | ## Overview 8 | 9 | [![/images/how-vulcan-works.svg](/images/how-vulcan-works.svg)](/images/how-vulcan-works.svg) 10 | 11 | ## The Schema 12 | 13 | At its core, a Vulcan schema is just a JavaScript object containing a list of fields such as `name`, `_id`, `createdAt`, `description`, etc. describing a **type of document** (a movie, a post, a photo, a review, and so on). 14 | 15 | The schema is what defines how a [Collection](/schemas.html) (you might also be more familiar with the equivalent term “model”) behaves, and it fulfills many important functions: 16 | 17 | 1. It's used to generate your GraphQL schema, which in turn controls your app's GraphQL API. 18 | 2. It's used to [control permissions](http://docs.vulcanjs.org/groups-permissions.html). 19 | 3. It's used to [generate forms](http://docs.vulcanjs.org/forms.html). 20 | 21 | ## Reading Data (Queries) 22 | 23 | Reading data basically means getting data from your database all the way to the user's browser. 24 | 25 | Let's assume we want to take a list of movies currently stored in our database and display it inside a `Movies.jsx` component. Here's a quick overview of the entire data lifecycle: 26 | 27 | #### Component 28 | 29 | The `Movies.jsx` component expects a `results` prop. But how will it receive it? 30 | 31 | #### Hook/Higher-Order Component 32 | 33 | In order to receive that prop, the component will need to be wrapped with the [`withMulti` higher-order component](/resolvers.html#withMulti). You just need to specify the appropriate [Collection](/schemas.html), and optionally also specify a [fragment](/fragments.html) to define which document fields to load. 34 | 35 | Alternatively, you can do the same thing through the `useMulti` hook. 36 | 37 | #### GraphQL Query 38 | 39 | The `withMulti` HoC (or `useMulti` hook) will trigger an Apollo query to the app's GraphQL endpoint using the [`movies` query resolver](/queries.html). 40 | 41 | This is the same kind of query you would write manually in any regular GraphQL app, but in this case the query is auto-generated by the HoC or hook. 42 | 43 | #### Resolver 44 | 45 | The query triggers a [resolver function](/queries.html). The job of that function is to take the [query arguments](/filtering.html) and output the corresponding data in return, after making sure the current user is authorized to access said data. 46 | 47 | #### Connector 48 | 49 | Finally, the resolver queries the database to retrieve the data. This is done through a [connector](/database.html), a function that translates a generic `find` request into instructions specific to the current database. 50 | 51 | ## Writing Data (Mutations) 52 | 53 | Now let's consider the opposite operation: writing data, such as editing a movie's description using a form. 54 | 55 | #### Component 56 | 57 | First, you should know that the movie update form component can be [automatically generated from your schema](/forms.html), meaning you don't actually need to code it or worry about hooking it up to your GraphQL API. 58 | 59 | #### Hook/Higher-Order Component 60 | 61 | That form is wrapped with the [`withUpdate` HoC](/mutations.html#Higher-Order-Components). 62 | 63 | #### GraphQL Query 64 | 65 | The HoC in turn will call the `updateMovie` mutation on the server (which again can be [automatically generated from default mutations](/mutations.html#Default-Mutations)). 66 | 67 | #### Resolver 68 | 69 | On the server, the `update` resolver takes in the mutation's arguments (an object indicating which document to update, as well as the payload containing the actual changes) and after some permission checks to make sure the current user is authorized to perform the mutation passes them on to the mutator. 70 | 71 | #### Mutator 72 | 73 | `updateMovie` will then call a [boilerplate mutator](/mutations.html#Boilerplate-Mutations) which will perform **validation** based on your schema, and finally call the database connector. 74 | 75 | #### Connector 76 | 77 | The database connector then modifies the document inside your database. 78 | -------------------------------------------------------------------------------- /source/other-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Other Components 3 | --- 4 | 5 | Out of the box, VulcanJS provides a set of useful components you can use to quickly build applications. 6 | 7 | These live in the `vulcan:core` package and can all be called using `<Components.XYZ/>`. Most of them are implemented using a [component library](/ui-components) such as React Bootstrap. 8 | 9 | 10 | ### App 11 | 12 | This is the main component that wraps everything else in your app. Its main job is to initialize the internationalization context. 13 | 14 | ### Avatar 15 | 16 | The `Avatar` component takes in a `user` and displays the user's avatar. Additionally, if you pass `link = {true}`, the avatar will be linked to the user's profile. 17 | 18 | ### Card 19 | 20 | The `Card` component is used to display a card for a single document. It takes the following props: 21 | 22 | - `document`: the document to display. 23 | - `collection`: the collection the document belongs to. 24 | - `currentUser`: if passed, an "Edit Document" button will be shown (if the user has edit permissions on the document). 25 | - `fields`: optionally, you can pass a list of field names to limit the fields shown in the card. 26 | 27 | The component will do its best to "guess" how to display each of the document's properties based on their type and the collection schema: 28 | 29 | ```js 30 | <Components.Card 31 | fields={['name', 'year', 'review']} 32 | collection={Movies} 33 | document={movie} 34 | currentUser={currentUser} 35 | /> 36 | ``` 37 | 38 | ### EditButton 39 | 40 | A button that triggers a modal window for editing a document. 41 | 42 | ### Flash 43 | 44 | Adds a zone to dynamically show “flash” messages (errors, successes, notifications, etc.).` 45 | 46 | ### HeadTags 47 | 48 | See [Head Tags](/head-tags.html) section. 49 | 50 | ### Layout 51 | 52 | The default layout is just a placeholder that can be replaced by your own. 53 | 54 | ### Loading 55 | 56 | Show a loading animation. 57 | 58 | ### MutationButton 59 | 60 | TODO 61 | 62 | ### NewButton 63 | 64 | A button that triggers a modal window for creating a new document. -------------------------------------------------------------------------------- /source/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugins 3 | --- 4 | 5 | ## Plugins 6 | 7 | VulcanJS is open-sourced and community-based, so it features some plugins developped by the community: 8 | 9 | - [ErikDakoda/vulcan-material-ui](https://github.com/ErikDakoda/vulcan-material-ui) : Replacement for Vulcan components using Material-UI. 10 | - [voodooattack/file-scalar](https://github.com/voodooattack/file-scalar) : A `File` scalar for Vulcan.js to facilitate file uploads with GraphQL. 11 | - [VulcanJS/vulcan-places](https://github.com/VulcanJS/vulcan-places) : Google Maps Places integration for Vulcan. 12 | - [Apollinaire/vulcan-crisp](https://github.com/Apollinaire/vulcan-crisp) : Crisp integration 13 | - [OrigenStudio/vulcan-files](https://github.com/OrigenStudio/vulcan-files): File System integration for Vulcan. Upload files to S3 or other storage systems. 14 | - [OrigenStudio/vulcan-sentry](https://github.com/OrigenStudio/vulcan-sentry) Sentry integration for Vulcan. 15 | - [OrigenStudio/vulcan-logrocket](https://github.com/OrigenStudio/vulcan-logrocket) LogRocket integration for Vulcan. 16 | - [OrigenStudio/vulcan-hotjar](https://github.com/OrigenStudio/vulcan-hotjar) Hotjar integration for Vulcan. 17 | - [arctop/vulcan-organizations-manager](https://https://github.com/arctop/vulcan-organizations-manager) Manage and create organizations. 18 | - [LiveForGood/vulcan-google-recaptcha](https://github.com/live-for-good/vulcan-google-recaptcha/) Google reCAPTCHA integration for your Vulcan forms. 19 | 20 | If you have written a package and would like to see it registered here, you can [edit this page](https://github.com/VulcanJS/vulcan-docs/blob/master/source/plugins.md) and submit a PR to the docs. 21 | 22 | ## Tooling 23 | 24 | ### VS Code 25 | 26 | Text editors might have trouble providing IntelliSense for Meteor packages. [The following extension from Matt Black](https://marketplace.visualstudio.com/items?itemName=mattblack.meteor-package-intellisense) provides a script to update your `jsconfig.json` file and thus provide intellisense for Vulcan packages. 27 | 28 | Snippets for common features are also provided by the [VulcanJS snippets extension from Apollinaire Lecocq](https://marketplace.visualstudio.com/items?itemName=Apollinaire.vulcanjs-snippets). 29 | 30 | ### Commande Line Interface 31 | 32 | [A CLI is available](https://www.npmjs.com/package/vulcanjs-cli) for fast generation of new Vulcan packages or modules. 33 | -------------------------------------------------------------------------------- /source/recipes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Recipes 3 | --- 4 | 5 | Short code snippets teaching you how to do various things in Vulcan. 6 | 7 | ## Using Router Hooks 8 | 9 | The router exposes an `onUpdate` hook that triggers every time the route changes (including when the initial route is triggered). 10 | 11 | Here's how you'd use it in conjunction with Apollo's [imperative API](http://dev.apollodata.com/core/read-and-write.html) to check if the current user has completed their profile: 12 | 13 | ```js 14 | import { addCallback, getFragment } from 'meteor/vulcan:core'; 15 | import Users from 'meteor/vulcan:users'; 16 | import gql from 'graphql-tag'; 17 | 18 | function checkProfileOnUpdate (unusedItem, store, apolloClient) { 19 | const query = gql` 20 | query getCurrentUser { 21 | currentUser { 22 | ...UsersCurrent 23 | } 24 | } 25 | ${getFragment('UsersCurrent')} 26 | ` 27 | 28 | const currentUser = apolloClient.readQuery({query}).currentUser; 29 | 30 | if (currentUser && !Users.hasCompletedProfile(currentUser)) { 31 | alert(`Current user hasn't completed their profile!`) 32 | } 33 | } 34 | 35 | addCallback('router.onUpdate', checkProfileOnUpdate); 36 | ``` 37 | 38 | ## Load a Random Post 39 | 40 | On the server: 41 | 42 | ```js 43 | const randomResolver = { 44 | Query: { 45 | postsRandom(root, args, context) { 46 | const postCount = context.Posts.find({status: 2}).count(); 47 | return context.Posts.find({status: 2}, {limit: 1, skip: _.random(postCount)}).fetch()[0]; 48 | } 49 | } 50 | }; 51 | 52 | addGraphQLResolvers(randomResolver); 53 | addGraphQLQuery(`postsRandom: Post`); 54 | ``` 55 | 56 | On the client: 57 | 58 | ```js 59 | const withRandomPost = graphql(gql` 60 | query postsRandom { 61 | postsRandom { 62 | ...PostsPage 63 | } 64 | } 65 | ${getFragment('PostsPage')} 66 | `); 67 | 68 | export default withRandomPost(PostsItem); 69 | ``` -------------------------------------------------------------------------------- /source/redux.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Redux [REVIEW] 3 | --- 4 | 5 | Redux is used transparently by Apollo to cache your data on the client, and it's perfectly fine to use Vulcan without ever worrying about it. 6 | 7 | That being said, if you need to manage state at an application-wide level (for example, you might want to track if a sidebar is open or closed), using Redux explicitly can be very useful. 8 | 9 | ## A Redux Higher-Order Component 10 | 11 | Vulcan doesn't come with any built-in Redux HoCs, but it's not too hard to build one. For example, here's how to create a HoC to manage a sidebar's shown/hidden state. 12 | 13 | First, we'll create a new action and a new reducer in a new `redux.js` file. The action defines a specific type of event that can happen inside your app; while the reducer defines what to do when that action happens. 14 | 15 | ```js 16 | import { addAction, addReducer } from 'meteor/vulcan:core'; 17 | 18 | // register messages actions 19 | addAction({ 20 | ui: { 21 | toggleSidebar() { 22 | return { 23 | type: 'TOGGLESIDEBAR', 24 | }; 25 | }, 26 | } 27 | }); 28 | 29 | // register messages reducer 30 | addReducer({ 31 | ui: (state = {showSidebar: false}, action) => { 32 | switch(action.type) { 33 | case 'TOGGLESIDEBAR': 34 | return { 35 | ...state, 36 | showSidebar: !state.showSidebar 37 | }; 38 | default: 39 | return state; 40 | } 41 | }, 42 | }); 43 | ``` 44 | 45 | Next, let's create our higher-order component in a separate `withUI.js` file: 46 | 47 | ```js 48 | import { getActions,} from 'meteor/vulcan:lib'; 49 | import { bindActionCreators } from 'redux'; 50 | import { connect } from 'react-redux'; 51 | 52 | const mapStateToProps = state => ({ ui: state.ui, }); 53 | const mapDispatchToProps = dispatch => bindActionCreators(getActions().ui, dispatch); 54 | 55 | const withUI = (component) => connect(mapStateToProps, mapDispatchToProps)(component); 56 | 57 | export default withUI; 58 | ``` 59 | 60 | Once we wrap a component with `withUI`, the following props will be available to it: 61 | 62 | - `this.props.ui.showSidebar`: a boolean that indicates if the sidebar is shown or not. 63 | - `this.props.toggleSidebar`: a function that toggles the sidebar on and off. 64 | -------------------------------------------------------------------------------- /source/relations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Relations 3 | --- 4 | 5 | A common use case for field resolvers is fetching one or more item associated with the current document, such as the user corresponding to a post's `userId` field; or the array of categories correponding to its `categoriesIds` field. 6 | 7 | While you can achieve this by explicitly adding an API schema field, Vulcan also offers a shortcut syntax using the `relation` property: 8 | 9 | ``` 10 | userId: { 11 | type: String, 12 | optional: true, 13 | canRead: ['guests'], 14 | relation: { 15 | fieldName: 'user', 16 | typeName: 'User', 17 | kind: 'hasOne', // one of 'hasOne' or 'hasMany' 18 | } 19 | }, 20 | ``` 21 | 22 | Since this is a `hasOne` relation, Vulcan will assume that the current field stores the `_id` of the related document to look up; and because we know that the type of the returned item should be `User` we can also easily figure out that we need to search the `Users` collection. 23 | 24 | Similarly, you can also define `hasMany` relations: 25 | 26 | ``` 27 | categoriesIds: { 28 | type: Array, 29 | arrayItem: { 30 | type: String, 31 | optional: true, 32 | canRead: ['guests'], 33 | } 34 | optional: true, 35 | canRead: ['guests'], 36 | relation: { 37 | fieldName: 'categories', 38 | typeName: '[Categories]', 39 | kind: 'hasMany' 40 | } 41 | }, 42 | ``` 43 | 44 | ## Relations, Cards, and Datatables 45 | 46 | As an added bonus, if you define the `userId` field as having a relation to the `User` type, Cards and Datatables will be able to understand this relation and use smarter formatting when displaying this data. 47 | -------------------------------------------------------------------------------- /source/resolvers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resolvers 3 | --- 4 | 5 | A resolver is the function on the server that receives a GraphQL query, decides what to do with it (how to _resolve_ it), and then returns some data. 6 | 7 | Each level of a GraphQL query (in other words, each field) can have its own custom resolver; or if no resolver exists the field will default to look for a value on its parent object. 8 | 9 | ### Implicit Resolvers 10 | 11 | Technically speaking, every single field in the fragment has to resolve to _something_. But if for example you've specified a `Post` resolver and you're asking for a post's `title` and `createdAt` properties, GraphQL will be smart enough to look for `post.title` and `post.createdAt` without you needing to define additional resolvers for these two fields. 12 | 13 | That being said if you wanted `createdAt` to resolve to, say, `formatDate(post.createdAt)` instead of just `post.createdAt`, then you could write a custom resolver [just for that specific field](/field-resolvers.html). 14 | 15 | ### Data Updating 16 | 17 | As long as you use `withMulti` or `useMulti` in conjunction with `withCreate`, `withUpdate`, and `withDelete`, your lists will automatically be updated after any mutation. This includes: 18 | 19 | * Inserting new items in lists after they're inserted. 20 | * Removing items when they're removed. 21 | * Reordering lists when an item is edited in a way that changes a sort. 22 | * Removing an item after it's been edited when it doesn't match a list's filters anymore. 23 | 24 | At this time, it's only possible to benefit from this auto-updating behavior if you're using the three built-in mutation HoCs, although making `withMulti` more flexible towards custom mutations is on the roadmap (PRs welcome!). 25 | 26 | #### Alternative Approach 27 | 28 | You can replace any of Vulcan's generic HoCs with your own tailor-made HoCs (whether it is for queries or mutations) using the `graphql` utility. Note that if you do so, you will need to manually [update your queries](http://dev.apollodata.com/react/cache-updates.html) after each mutation. 29 | -------------------------------------------------------------------------------- /source/server-queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Querying on the Server 3 | --- 4 | 5 | Most of the Data Layer section's contents applies to the client. After all, if you're on the server, you can simply connect to your database directly, so why would you need to worry about the data layer? 6 | 7 | But the truth is that connecting to the database directly can lead to problems. For example, let's assume you have a `user` resolver on your `Post` type that gives you access to the post's author. On the client, you could write: 8 | 9 | ```js 10 | const authorName = post.user.displayName; 11 | ``` 12 | 13 | On the server, it's easy enough to obtain that post document: 14 | 15 | ```js 16 | const post = Posts.findOne(documentId); 17 | ``` 18 | 19 | But the document you fetch directly from MongoDB won't have a `user` property, let alone a `user.displayName` (since those are provided by the GraphQL resolver). 20 | 21 | Thankfully, VulcanJS features a few special utilities to let you query for data through GraphQL *on the server* 22 | 23 | ### runGraphQL(query, variables) 24 | 25 | `runGraphQL` lets you execute any GraphQL query or mutation against your schema and get its result back on the server. 26 | 27 | ```js 28 | import { runGraphQL } from 'meteor/vulcan:core'; 29 | 30 | async function foo = () => { 31 | 32 | const query = ` 33 | query OneRoom($documentId: String){ 34 | RoomsSingle(documentId: $documentId){ 35 | name 36 | description 37 | user{ 38 | _id 39 | displayName 40 | } 41 | } 42 | } 43 | `; 44 | return await runGraphQL(query, {documentId: '123foo'}); 45 | 46 | } 47 | ``` 48 | 49 | (Note that you'll need to be inside an `async` context to use the `await` keyword) 50 | 51 | ### collection.queryOne(inputOrId, options) 52 | 53 | A common need on the server is fetching a single document by its `_id`. You can do so with `collection.queryOne`. The first argument is either a string `_id`, or a full `input` object (as used by `single` queries). 54 | 55 | The second argument is an `options` object with a `fragmentText` property. If no fragment is provided, the query will use the collection's `defaultFragment`. 56 | 57 | ```js 58 | const user = await Users.queryOne(userId); 59 | 60 | // or 61 | 62 | const user = await Users.queryOne(userId, { 63 | fragmentText: ` 64 | fragment MyUsersFragment on User { 65 | _id 66 | username 67 | createdAt 68 | posts{ 69 | _id 70 | title 71 | } 72 | } 73 | ` 74 | }); 75 | ``` 76 | 77 | ### `queryOne` vs `load` 78 | 79 | It might seem like `queryOne` does the same thing as the [Dataloader layer](/performance.html#Caching-amp-Batching)'s `load`, but there's a crucial difference in how they're used. 80 | 81 | `queryOne` uses your existing GraphQL resolvers behind the scenes, which means it itself can't be used *inside* a resolver (or you'd risk an infinite loop). It's thus meant to be used from outside of your GraphQL resolver tree, from a server script, cron job, migration, etc. or any kind of server code unrelated to the client. 82 | 83 | `load` on the other hand is specifically made to be used inside resolvers and add a caching and batching layer to them. 84 | -------------------------------------------------------------------------------- /source/settings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Settings 3 | --- 4 | 5 | You can configure the following settings in your `settings.json` file. 6 | 7 | ## Debugging 8 | 9 | **If you've got the [debug package](/debug.html) enabled, a settings debugging UI is available at [http://0.0.0.0:3000/debug/settings](http://0.0.0.0:3000/debug/settings).** 10 | 11 | ## Overview 12 | 13 | You can call Vulcan.showSettings() in your meteor shell to get a quick overview of all settings, or go to `/settings` if you have the `vulcan:debug` package enabled. 14 | 15 | ### Public Settings 16 | 17 | These settings are defined on the `public` property, meaning they will be shared with the client. 18 | 19 | #### Global Settings 20 | 21 | | Setting | Example | Description | 22 | | --- | --- | --- | 23 | | title | "My Vulcan Site" | Your site's title | 24 | | siteUrl | "http://mysite.com" | Your site's main URL | 25 | | tagline | "The best site ever!" | Your site's tagline or description | 26 | | language | "en" | Your site's language | 27 | 28 | #### Social Settings 29 | 30 | | Setting | Example | Description | 31 | | --- | --- | --- | 32 | | twitterAccount | "TelescopeApp" | Your main twitter account (without the "@")| 33 | | facebookPage | "https://facebook.com/TelescopeApp" | Your Facebook page URL| 34 | | googleAnalyticsId | "UA-123456-9" | Your Google Analytics code | 35 | 36 | ### Server Settings 37 | 38 | These settings are defined at the root of the settings object, (or optionally on the `private` property), and kept on the server. 39 | 40 | #### Global Settings 41 | 42 | | Setting | Example | Description | 43 | | --- | --- | --- | 44 | | mailUrl | "smtp://username:password@smtp.mailgun.org:587/" | The SMTP URL used by your email provider | 45 | 46 | **MailGun** Note: 47 | For `username` use your **Default SMTP Login** and for `password` use **Default Password**. 48 | See screenshot below. 49 | 50 | ![](https://i.imgur.com/pRwaQzX.png) 51 | 52 | #### OAuth Settings 53 | 54 | You can use the settings file to store your oAuth configurations: 55 | 56 | ```js 57 | "oAuth": { 58 | "twitter": { 59 | "consumerKey": "foo", 60 | "secret": "bar" 61 | }, 62 | "facebook": { 63 | "appId": "foo", 64 | "secret": "bar" 65 | }, 66 | "github": { 67 | "clientId": "foo", 68 | "secret": "bar" 69 | } 70 | } 71 | ``` 72 | Don't forget to add the matching meteor package, e.g. `accounts-twitter`, `accounts-facebook`, `accounts-github` etc. 73 | 74 | ## registerSetting 75 | 76 | You can optionally register a new setting with `registerSetting(name, defaultValue, description)`. This is optional, but it will let your setting be displayed on the `/settings` route and when calling `Vulcan.showSettings()`. 77 | 78 | Note that `registerSetting` also both works with nested settings objects (e.g. `registerSetting('newsletter.frequency', [1,3,5], 'Newsletter frequency')`). 79 | 80 | ## getSetting 81 | 82 | To retrieve a setting, just call `getSetting(settingName)`. If no setting has been assigned in `settings.json`, the default value assigned through `registerSetting` will be used (if it exists). 83 | 84 | Just like `registerSetting`, `getSetting` also works with nested settings objects (e.g. `getSetting('newsletter.frequency')`). 85 | -------------------------------------------------------------------------------- /source/sponsorship-tutorial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sponsorship Flow Tutorial 3 | --- 4 | 5 | This series of video will take you through a case study on building a flow for submitting, scheduling, and paying for a sponsored link on [Sidebar](http://sidebar.io). 6 | 7 | This tutorial will cover many topics, including: 8 | 9 | - Creating new routes. 10 | - Using custom fields. 11 | - Writing custom query resolvers. 12 | - Writing custom mutations. 13 | - Creating custom views. 14 | - Using the Payments package. 15 | - Creating new email objects. 16 | - Using callbacks. 17 | 18 | Let's get started! 19 | 20 | - [Start the playlist](https://www.youtube.com/watch?v=4MMILATC9Ds&list=PLBoa_Q6hVeSzGqmpl4xUS9913lTP15Zve) 21 | - [Overview](https://docs.google.com/document/d/1dxFlI7QdG4UniCCNAX_DLV6wO2U50zlcwSA4b4N3AJ0/edit?usp=sharing) (Google Docs) 22 | - [Code](https://github.com/SachaG/Sidebar2017) 23 | 24 | Individual videos: 25 | 26 | - [Intro](https://www.youtube.com/watch?v=4MMILATC9Ds&list=PLBoa_Q6hVeSzGqmpl4xUS9913lTP15Zve&index=1) (3:24) 27 | - [Sponsor Page](https://www.youtube.com/watch?v=OQ0zOgwj0fk&list=PLBoa_Q6hVeSzGqmpl4xUS9913lTP15Zve&index=2) (6:09) 28 | - [Submit Link](https://www.youtube.com/watch?v=1N58_LRVZis&index=3&list=PLBoa_Q6hVeSzGqmpl4xUS9913lTP15Zve) (5:24) 29 | - [Pick Date](https://www.youtube.com/watch?v=wkfx1bypuw0&index=4&list=PLBoa_Q6hVeSzGqmpl4xUS9913lTP15Zve) (8:35) 30 | - [Complete Payment](https://www.youtube.com/watch?v=jdDPFZTRt0s&list=PLBoa_Q6hVeSzGqmpl4xUS9913lTP15Zve&index=5) (12:11) -------------------------------------------------------------------------------- /source/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Themes 3 | --- 4 | 5 | Coming soon… -------------------------------------------------------------------------------- /source/toc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Table of Contents 3 | --- 4 | 5 | A quick overview of this documentation. 6 | 7 | --- 8 | 9 | | *Section* | *Description* | 10 | |---|---| 11 | | **Schemas** | **How to define model schemas to power your app** | 12 | | [Schemas & Collections](/schemas.html) | The basics of creating a new collection | 13 | | [Schema Properties](/schema-properties.html) | Available schema properties | 14 | | [Field Resolvers](/field-resolvers.html) | How to make schema fields "resolve" into complex objects | 15 | | [Relations](/relations.html) | How to define relations between documents | 16 | | **GraphQL API** | **How to define model schemas to power your app** | 17 | | [GraphQL Schema](/graphql-schema.html) | How to access your generated GraphQL schema | 18 | | [Queries](/queries.html) | Querying for data (read) | 19 | | [Filtering](/filtering.html) | How to narrow down the data you query for | 20 | | [Mutations](/mutations.html) | Mutating data (create, update, delete) | 21 | | [Fragments](/fragments.html) | How to control which fields you receive on the client | 22 | | [Querying on the Server](/server-queries.html) | Using GraphQL queries from the server | 23 | | [Connecting Remotely](/connecting-remotely.html) | Using your GraphQL API from another server | 24 | | **Features** | **Core Vulcan features** | 25 | | [Routing](/routing.html) | How to declare routes | 26 | | [Settings](/settings.html) | Storing and retrieving app-level settings | 27 | | [Groups & Permissions](/groups-permissions.html) | Controlling permissions with groups and functions | 28 | | [Callbacks](/callbacks.html) | How to add callback functions after key operations (create, update, etc.) | 29 | | [Users](/users.html) | Managing users | 30 | | [Internationalization](internationalization.html) | Making your UI and database contents available in multiple languages | 31 | | [Head Tags](/head-tags.html) | Control your app's `meta` tags | 32 | | [Errors & Messages](/errors.html) | Showing users error messages | 33 | | **Components** | **Vulcan's built-in components** | 34 | | [Components](/components.html) | Registering, using, and replacing components | 35 | | [UI Components](/ui-components.html) | Using UI themes (Bootstrap, Material, etc.) with Vulcan | 36 | | [Datatable](/datatable.html) | The Datatable component | 37 | | [Form Components](/form-components.html) | Components used to build forms | 38 | | [Other Components](/other-components.html) | Other built-in components | 39 | | **Forms** | **Vulcan's form generation and form handling** | 40 | | [Forms](/forms.html) | Using “new” and “edit” forms | 41 | | [Custom Forms](/custom-forms.html) | Customizing your forms | 42 | | **Server** | **Server-level concerns** | 43 | | [Performance](/performance.html) | Ensuring your Vulcan app runs smoothly | 44 | | [Database Layer](/database.html) | Accessing the database | 45 | | **Testing** | **Testing your app**| 46 | | [Unit Testing](/unit-testing.html) | Unit Testing with Mocha | 47 | | [Storybook](/storybook.html) | Component testing with Storybook | -------------------------------------------------------------------------------- /source/tutorials.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tutorials 3 | --- 4 | 5 | - [Interactive Tutorial](https://github.com/VulcanJS/Vulcan-Starter) 6 | -------------------------------------------------------------------------------- /source/ui-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: UI Components 3 | --- 4 | 5 | While Vulcan itself doesn't provide its own UI components (besides core utilites such as `Layout`, `App`, etc.), it does make use of a common component API that can serve as a bridge between your app and UI frameworks such as [React Bootstrap](https://react-bootstrap.github.io/) or [Material UI](https://www.material-ui.com/#/). 6 | 7 | By using this API instead of depending on a framework's components directly, Vulcan plugins and projects can make it possible to easily swap from one UI framework to another. 8 | 9 | ## Form Components 10 | 11 | - Checkbox 12 | - Checkboxgroup 13 | - Date 14 | - Datetime 15 | - Default 16 | - Email 17 | - FormControl 18 | - Number 19 | - Radiogroup 20 | - Select 21 | - Textarea 22 | - Time 23 | - URL 24 | 25 | ## Other Components 26 | 27 | ### Alert 28 | 29 | ### Button 30 | 31 | ### Dropdown 32 | 33 | ### Modal 34 | 35 | ### ModalTrigger 36 | 37 | The `ModalTrigger` component can be used to display its child component inside a modal window when the trigger component is clicked: 38 | 39 | ```js 40 | <Components.ModalTrigger size={size} title="New Post" component={<MyButton/>}> 41 | <PostsNewForm /> 42 | </Components.ModalTrigger> 43 | ``` 44 | 45 | It accepts the following props: 46 | 47 | - `className` 48 | - `component`: the trigger component. 49 | - `label`: if no trigger component is passed, you can also specify a label for a text link. 50 | - `size`: `large` or `small` (defaults to `large`). 51 | - `title`: the modal popup's title. 52 | 53 | All props are optional, but you should pass at least either `component` or `label`. 54 | 55 | Note that the element passed as `component` needs to accept an `onClick` handler. In some cases, it might be necessary to wrap it inside an extra `<div>`: 56 | 57 | ```js 58 | <Components.ModalTrigger size={size} title="New Post" component={<div><MyButton/></div>}> 59 | <PostsNewForm /> 60 | </Components.ModalTrigger> 61 | ``` 62 | 63 | ### Table 64 | 65 | ### Tooltip -------------------------------------------------------------------------------- /source/unit-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Unit Testing 3 | --- 4 | 5 | Currently we are using [Mocha](https://mochajs.org/) for unit and integration testing. 6 | At some point soon we will be migrating to [Jest](https://jestjs.io), but we will make the migration process as smooth as possible. 7 | This is the reason we are using the Jest Expect library instead of Chai. 8 | 9 | You'll find documentation about Meteor testing at the following places: 10 | 11 | * [Meteor Guide: Testing](https://guide.meteor.com/testing.html) 12 | * [MeteorTesting:Mocha](<https://github.com/meteortesting/meteor-mocha/blob/master/README.md>) 13 | * [Mocha Getting Started](https://mochajs.org/#getting-started) 14 | * [Jest Expect API Reference](https://jestjs.io/docs/en/expect) 15 | * [Jest Extended Expect API Reference](https://jestjs.io/docs/en/expect) 16 | * [Chai BDD Expect API Reference](https://www.chaijs.com/api/bdd/) 17 | 18 | ## Getting started 19 | 20 | Vulcan comes pre-configured for unit testing, but for your own project you'll have to install it. 21 | 22 | If you are NOT using a [two repo install](http://docs.vulcanjs.org/index.html#Two-Repo-Install-Optional) copy vulcan/packages/meteor-mocha to your project's packages directory. 23 | 24 | In your app's root directory run these terminal commands: 25 | 26 | ``` 27 | meteor add meteortesting:mocha 28 | meteor npm install --save-dev chai 29 | meteor npm install --save-dev chai-as-promised 30 | meteor npm install --save-dev selenium-webdriver@3.6.0 31 | meteor npm install --save-dev chromedriver@2.41.0 32 | meteor npm install --save-dev enzyme 33 | meteor npm install --save-dev enzyme-adapter-react-16 34 | meteor npm install --save-dev jsdom 35 | meteor npm install --save-dev jsdom-global 36 | ``` 37 | 38 | Open `.meteor/versions` and make sure the meteortesting packages are at their latest versions, 39 | and if not, update them individually using `meteor update meteortesting:browser-tests` etc.: 40 | 41 | ``` 42 | meteortesting:browser-tests@1.2.0 43 | meteortesting:mocha@1.1.3 44 | meteortesting:mocha-core@6.1.2 45 | ``` 46 | 47 | Open `package.json` in your app root and add the following line to the scripts section: 48 | 49 | ```json 50 | { 51 | "name": "MyApp", 52 | . . . 53 | "scripts": { 54 | . . . 55 | "unit-test": "TEST_BROWSER_DRIVER=chrome ROOT_URL=http://localhost:60859 METEOR_PACKAGE_DIRS=~/Dev/Erik-Vulcan/packages meteor test-packages ./packages/* --port 60859 --settings settings-dev.json --driver-package meteortesting:mocha --raw-logs" 56 | } 57 | } 58 | ``` 59 | 60 | Make sure to replace `~/Dev/Erik-Vulcan` with the actual path to you local copy of Vuclan in your two-repo install. 61 | Before `meteor` you can define additional environment variables that you may need, such as `MAIL_URL` or `MONGO_URL`. 62 | 63 | ## Running the tests 64 | 65 | To start testing initiate unit-test, for example type in your console: 66 | 67 | ``` 68 | npm run test-unit 69 | ``` 70 | 71 | The unit test suite will then run and later re-run when you make code changes. You can terminate it with **CTRL-C**. 72 | 73 | ## Adding code coverage 74 | 75 | Add the following packages in your terminal: 76 | 77 | ``` 78 | meteor add lmieulet:meteor-coverage meteortesting:mocha 79 | meteor npm install --save-dev babel-plugin-istanbul 80 | ``` 81 | 82 | In order to instrument your code, you need to add the babel-plugin-istanbul the .babelrc file in the root of your app: 83 | 84 | ```json 85 | { 86 | "env": { 87 | "COVERAGE": { 88 | "plugins": [ 89 | "istanbul" 90 | ] 91 | } 92 | }, 93 | "presets": [ 94 | . . . 95 | ] 96 | } 97 | ``` 98 | 99 | You must wrap the istanbul plugin with the env setting to disable the file-instrumentation of your project when you are not running the test coverage script. Just keep in mind that if you follow the here under script, babel will use the istanbul package only when BABEL_ENV=COVERAGE. 100 | 101 | Now, to run the coverage process, just add these new scripts inside your package.json in the root folder of your app: 102 | 103 | ```json 104 | { 105 | "name": "MyApp", 106 | . . . 107 | "scripts": { 108 | . . . 109 | "unit-test": "TEST_BROWSER_DRIVER=chrome ROOT_URL=http://localhost:60859 METEOR_PACKAGE_DIRS=~/Dev/Erik-Vulcan/packages meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha --raw-logs", 110 | "unit-test-coverage": "BABEL_ENV=COVERAGE COVERAGE=1 COVERAGE_VERBOSE=1 COVERAGE_APP_FOLDER=$PWD/ TEST_BROWSER_DRIVER=chrome ROOT_URL=http://localhost:60859 METEOR_PACKAGE_DIRS=~/Dev/Erik-Vulcan/packages meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha --raw-logs" 111 | } 112 | } 113 | ``` 114 | -------------------------------------------------------------------------------- /source/users.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Users 3 | --- 4 | 5 | Vulcan uses the standard [Meteor user accounts system](https://guide.meteor.com/accounts.html), along with the [`std:accounts-ui`](https://github.com/studiointeract/accounts-ui) package for the front-end. 6 | 7 | ### Preloaded Properties 8 | 9 | A number of properties (such as a user's `_id`, `email`, etc.) are preloaded for the current user when the app loads. These properties are controlled by the `UsersCurrent` fragment: 10 | 11 | ``` 12 | fragment UsersCurrent on User { 13 | _id 14 | username 15 | createdAt 16 | isAdmin 17 | displayName 18 | email 19 | emailHash 20 | slug 21 | groups 22 | } 23 | ``` 24 | 25 | You can extend or replace it just like any other [fragment](fragments.html). -------------------------------------------------------------------------------- /source/videos.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Videos 3 | --- 4 | 5 | - [Vulcan on YouTube](https://www.youtube.com/channel/UCGIvQQ6zw7ov2cHgD70HFlA). -------------------------------------------------------------------------------- /source/vulcan.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: From Telescope to Vulcan 3 | --- 4 | 5 | **Vulcan** is the name of the next big evolution of the [Telescope](http://www.telescopeapp.org/) project. 6 | 7 | Over the years, we've realized that Telescope could be used to do a lot more than simply build Product Hunt clones. That's why we've decided to focus our energy on building a more flexible and more generic platform, and we thought this new approach deserved a new name. 8 | 9 | In the near future, this will mean two big changes for Telescope: 10 | 11 | First, all various assets (repo/documentation/homepage/etc.) will progressively transition to the new VulcanJS name and branding. This might be a bit confusing at first, but we'll get there eventually! 12 | 13 | Second, Vulcan will adopt a new “framework-first” approach. Practically speaking, this means you'll start new projects with the `framework-demo` package, and not with the optional `vulcan:posts`, `vulcan:comments`, etc. feature packages. Of course, these packages will still be available if you choose to use them. We just think it's important for new users to understand how the underlying framework works before they start using its more advanced capabilities. 14 | 15 | ### Telescope, Nova, & Vulcan 16 | 17 | Just to clear things up, here's a short timeline of Telescope's various versions: 18 | 19 | #### Telescope Legacy (2012-2016) 20 | 21 | The original Telescope, built with Meteor and Blaze. Focused on being usable out of the box as a Product Hunt/Hacker News clone. 22 | 23 | #### Telescope Nova (2016-2017) 24 | 25 | The Meteor/React refactor of Telescope. Contained fewer features than Legacy, and was focused mainly on extensibility and flexibility. Nova originally used the Meteor pub/sub data layer (“Nova Classic”). 26 | 27 | Later in 2016, Nova was ported to the Apollo data layer while still using Meteor (“Nova Apollo”), and all internal APIs were refactored to eliminate global variables. 28 | 29 | #### Vulcan (2017-???) 30 | 31 | The new name for the Apollo version of Telescope Nova. 32 | 33 | As of March 2017, Vulcan is merely a rebranding of Telescope Nova 1.2, and does not feature any code changes. 34 | 35 | ### Why “Vulcan”? 36 | 37 | [Vulcan](https://en.wikipedia.org/wiki/Vulcan_(mythology) is the Roman god of fire and volcanoes. Since Vulcan uses React and Apollo, we thought this was an appropriate name. After all Apollo is a Roman god too, and the React team has used [volcanoes](https://facebook.github.io/react/blog/2016/09/28/our-first-50000-stars.html) in their imagery before. 38 | 39 | Also, the logo has a hidden reference to Telescope's original space-themed roots. Can you find it? 40 | -------------------------------------------------------------------------------- /source/why-vulcan.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VulcanJS/vulcan-docs/3cb0cd142609b9efc00ead23c30de424a304a605/source/why-vulcan.md -------------------------------------------------------------------------------- /talk-outline.md: -------------------------------------------------------------------------------- 1 | 2 | ## Background 3 | 4 | 1. Telescope/Sidebar 5 | 2. Discover Meteor 6 | 3. React rewrite (Nova) + Xavier comes on board 7 | 4. Apollo rewrite (Vulcan) 8 | 9 | ## What Is It? 10 | 11 | A full-stack toolkit of components and packages that helps you quickly build 12 | web apps, both on the fƒront-end and back-end. 13 | 14 | Show some projects: 15 | 1. Crater 16 | 2. Sidebar 17 | 3. Huttle 18 | 4. SmartHosts 19 | 20 | ## Principles 21 | 22 | 1. No Dumb Work 23 | 2. Flexible (Extend, Don't Edit) 24 | 3. Eject Anytime 25 | 26 | ## Technology 27 | 28 | 1. Front-End: React 29 | 2. Data Layer: GraphQL & Redux (Apollo) 30 | 3. Server & Build Tool: Meteor 31 | 32 | 4. The bridge between Meteor's ease of use and Apollo's flexibility 33 | 5. Package-based architecture 34 | 35 | ## Movies Demo + Core Features 36 | 37 | 1. Schema 38 | 2. Data Layer (list pagination, load detailed views) 39 | - Show Redux/Apollo devtools 40 | 3. Forms 41 | 42 | ## Posts Demo + Other Features 43 | 44 | 1. Posts & Comments 45 | 2. Categories 46 | 3. Newsletter 47 | 48 | ## Code + Docs 49 | 50 | 1. Data layer HoCs 51 | 2. Components & theming 52 | 3. Fragments extension 53 | 4. Callback Hooks 54 | 55 | ## Next Steps 56 | 57 | 1. Last 10% 58 | 2. Relaunch with clear messaging 59 | 3. Find more contributors! (you!) 60 | -------------------------------------------------------------------------------- /themes/meteor/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /themes/meteor/_config.yaml: -------------------------------------------------------------------------------- 1 | logo: 2 | #nav_mobile: images/logo-apollo-space-left.svg 3 | #subbrand: images/logo-apollo-subbrands-developers-space.svg 4 | title: Vulcan Documentation 5 | subtitle: Vulcan Documentation 6 | url: https://docs.vulcanjs.org/ 7 | favicon: images/favicon.ico 8 | 9 | nav_links: 10 | 'GitHub': 11 | url: https://github.com/vulcanjs/vulcan 12 | 'Slack': 13 | url: http://slack.vulcanjs.org 14 | 15 | -------------------------------------------------------------------------------- /themes/meteor/layout/layout.ejs: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8"> 5 | 6 | <!-- Basic --> 7 | <title><%- page.title %> | <%- config.title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | <%- body %> 36 | 37 | 38 | 39 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /themes/meteor/layout/page.ejs: -------------------------------------------------------------------------------- 1 | <% var githubUrl = 'https://github.com/' + config.github_repo + 2 | '/tree/master/' + (config.content_root || 'content') + '/' + 3 | page.path.replace(/\.html$/, '.md'); %> 4 | 5 |