├── submissions ├── .gitkeep ├── a-cat-this-is-not-fine.json ├── a-dog-this-is-fine.json ├── nuria-awesome-talk.json └── nuria-kill-the-back-end-bring-user-generated-content-to-jamstack-with-github-actions.json ├── .github ├── CODEOWNERS └── workflows │ ├── lint-pr.yml │ ├── gridsome-deploy-gh-pages.yml │ └── handle-submission.yml ├── src ├── favicon.png ├── components │ ├── README.md │ ├── Btn.vue │ ├── ExternalLink.vue │ ├── Submission.vue │ ├── SubmissionList.vue │ ├── Alert.vue │ ├── Field.vue │ └── ApplicationForm.vue ├── layouts │ ├── README.md │ └── Default.vue ├── pages │ ├── README.md │ ├── Index.vue │ ├── Submissions.vue │ └── Pending.vue ├── templates │ └── README.md └── main.js ├── netlify.toml ├── .gitignore ├── .eslintrc.js ├── gridsome.server.js ├── gridsome.config.js ├── actions └── process-input.js ├── functions └── submit.js ├── package.json └── README.md /submissions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nuria-fl 2 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegram/jamstack-cfp/master/src/favicon.png -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm run build" 4 | functions = "functions-dist" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .cache 3 | .DS_Store 4 | src/.temp 5 | node_modules 6 | dist 7 | .env 8 | .env.* 9 | functions-dist 10 | -------------------------------------------------------------------------------- /submissions/a-cat-this-is-not-fine.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": 1589229582451, 3 | "description": "Get these humans out of my house", 4 | "name": "a cat", 5 | "title": "This is not fine" 6 | } 7 | -------------------------------------------------------------------------------- /submissions/a-dog-this-is-fine.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": 1588762607767, 3 | "description": "I'm okay with the events that are unfolding currently", 4 | "name": "a dog", 5 | "title": "this is fine" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | Add components that will be imported to Pages and Layouts to this folder. 2 | Learn more about components here: https://gridsome.org/docs/components/ 3 | 4 | You can delete this file. 5 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | Layout components are used to wrap pages and templates. Layouts should contain components like headers, footers or sidebars that will be used across the site. 2 | 3 | Learn more about Layouts: https://gridsome.org/docs/layouts/ 4 | 5 | You can delete this file. 6 | -------------------------------------------------------------------------------- /src/pages/README.md: -------------------------------------------------------------------------------- 1 | Pages are usually used for normal pages or for listing items from a GraphQL collection. 2 | Add .vue files here to create pages. For example **About.vue** will be **site.com/about**. 3 | Learn more about pages: https://gridsome.org/docs/pages/ 4 | 5 | You can delete this file. 6 | -------------------------------------------------------------------------------- /src/templates/README.md: -------------------------------------------------------------------------------- 1 | Templates for **GraphQL collections** should be added here. 2 | To create a template for a collection called `WordPressPost` 3 | create a file named `WordPressPost.vue` in this folder. 4 | 5 | Learn more: https://gridsome.org/docs/templates/ 6 | 7 | You can delete this file. 8 | -------------------------------------------------------------------------------- /submissions/nuria-awesome-talk.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": 1588238709352, 3 | "description": "Illo sint voluptas. Error voluptates culpa eligendi. Hic vel totam vitae illo. Non aliquid explicabo necessitatibus unde. Sed exercitationem placeat consectetur nulla deserunt vel. Iusto corrupti dicta.", 4 | "name": "Núria", 5 | "title": "An awesome talk" 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["gridsome", "prettier", "simple-import-sort"], 3 | extends: [ 4 | "plugin:prettier/recommended", 5 | "prettier/vue", 6 | "plugin:gridsome/recommended", 7 | ], 8 | rules: { 9 | "simple-import-sort/sort": "error", 10 | "no-console": ["error", { allow: ["error"] }], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: amannn/action-semantic-pull-request@v1.1.1 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // This is the main.js file. Import global CSS and scripts here. 2 | // The Client API can be used here. Learn more: gridsome.org/docs/client-api 3 | 4 | import DefaultLayout from "~/layouts/Default.vue"; 5 | 6 | export default function (Vue, { router, head, isClient }) { 7 | // Set default layout as a global component 8 | Vue.component("Layout", DefaultLayout); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Btn.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/ExternalLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /gridsome.server.js: -------------------------------------------------------------------------------- 1 | // Server API makes it possible to hook into various parts of Gridsome 2 | // on server-side and add custom data to the GraphQL data layer. 3 | // Learn more: https://gridsome.org/docs/server-api/ 4 | 5 | // Changes here require a server restart. 6 | // To restart press CTRL + C in terminal and run `gridsome develop` 7 | 8 | module.exports = function (api) { 9 | api.loadSource(({ addCollection }) => { 10 | // Use the Data Store API here: https://gridsome.org/docs/data-store-api/ 11 | }) 12 | 13 | api.createPages(({ createPage }) => { 14 | // Use the Pages API here: https://gridsome.org/docs/pages-api/ 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Submission.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /src/pages/Submissions.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | query { 12 | submissions: allSubmission { 13 | edges { 14 | node { 15 | id 16 | date 17 | description 18 | name 19 | title 20 | } 21 | } 22 | } 23 | } 24 | 25 | 26 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/SubmissionList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 42 | -------------------------------------------------------------------------------- /src/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /gridsome.config.js: -------------------------------------------------------------------------------- 1 | // This is where project configuration and plugin options are located. 2 | // Learn more: https://gridsome.org/docs/config 3 | 4 | // Changes here require a server restart. 5 | // To restart press CTRL + C in terminal and run `gridsome develop` 6 | 7 | const lambdaBase = 8 | process.env.BRANCH === "master" 9 | ? process.env.URL 10 | : process.env.DEPLOY_PRIME_URL || "http://localhost:9000"; 11 | 12 | process.env.GRIDSOME_LAMBDA = `${lambdaBase}/.netlify/functions/submit`; 13 | 14 | module.exports = { 15 | siteName: "Gridsome", 16 | pathPrefix: process.env.BASE_PATH || "", 17 | plugins: [ 18 | { use: "gridsome-plugin-tailwindcss" }, 19 | { 20 | use: "@gridsome/source-filesystem", 21 | options: { 22 | typeName: "Submission", 23 | path: "./submissions/*.json", 24 | }, 25 | }, 26 | { 27 | use: "@gridsome/source-graphql", 28 | options: { 29 | url: "https://api.github.com/graphql", 30 | fieldName: "github", 31 | typeName: "githubTypes", 32 | 33 | headers: { 34 | Authorization: `Bearer ${process.env.GH_TOKEN_PERSONAL}`, 35 | }, 36 | }, 37 | }, 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /submissions/nuria-kill-the-back-end-bring-user-generated-content-to-jamstack-with-github-actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": 1588760728559, 3 | "description": "Static sites have been making a comeback these last few years. With the JAMStack approach (J-A-M stands for JavaScript, APIs and Markup), we can statically generate sites with lots of content, like blogs, that have fast performance, excellent SEO, better security, straightforward scalability, and great developer experience. But nothing is perfect and JAMStack sites have their drawbacks too, one of them being limited ability to handle user-generated content… until now!\n\nGitHub Actions are a way to automate software workflows. Running tests, linting, building and deploying is the most typical flow that will probably come to mind. But this is just the beginning, GitHub Actions open a new world of possibilities for JAMStack sites. In this talk we'll see how we can turn GitHub into our own Content Management System that will receive user submissions from the client-side application, validate them and publish them, all without any backend server involved!", 4 | "name": "Núria", 5 | "title": "Kill the back-end: Bring user-generated content to JAMStack with GitHub Actions" 6 | } 7 | -------------------------------------------------------------------------------- /actions/process-input.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const { client_payload } = require(process.env.GITHUB_EVENT_PATH); 4 | 5 | // Thanks @hagemann https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 6 | function slugify(string) { 7 | const a = 8 | "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;"; 9 | const b = 10 | "aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------"; 11 | const p = new RegExp(a.split("").join("|"), "g"); 12 | 13 | return string 14 | .toString() 15 | .toLowerCase() 16 | .replace(/\s+/g, "-") // Replace spaces with - 17 | .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters 18 | .replace(/&/g, "-and-") // Replace & with 'and' 19 | .replace(/[^\w\-]+/g, "") // Remove all non-word characters 20 | .replace(/\-\-+/g, "-") // Replace multiple - with single - 21 | .replace(/^-+/, "") // Trim - from start of text 22 | .replace(/-+$/, ""); // Trim - from end of text 23 | } 24 | 25 | const fileName = `${slugify(client_payload.name)}-${slugify( 26 | client_payload.title 27 | )}`; 28 | 29 | fs.writeFileSync( 30 | `submissions/${fileName}.json`, 31 | JSON.stringify(client_payload) 32 | ); 33 | -------------------------------------------------------------------------------- /src/components/Field.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 55 | -------------------------------------------------------------------------------- /.github/workflows/gridsome-deploy-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | repository_dispatch: 7 | types: trigger-deploy 8 | 9 | env: 10 | NODE_VERSION: "10.x" 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | GH_TOKEN_PERSONAL: ${{ secrets.GH_TOKEN_PERSONAL }} 13 | 14 | jobs: 15 | build-and-deploy: 16 | name: Install, Build and Deploy 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@master 20 | - name: Use Node.js ${{ env.NODE_VERSION }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ env.NODE_VERSION }} 24 | - name: Cache node_modules 25 | id: cache-primes 26 | uses: actions/cache@v1 27 | with: 28 | path: node_modules 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | - name: npm install, build, and test 33 | env: 34 | BASE_PATH: ${{ secrets.BASE_PATH }} 35 | run: | 36 | npm install 37 | npm run build 38 | - name: Push to pages 39 | uses: crazy-max/ghaction-github-pages@v1.2.4 40 | with: 41 | build_dir: dist 42 | env: 43 | GITHUB_PAT: ${{ secrets.GH_TOKEN_PERSONAL }} 44 | -------------------------------------------------------------------------------- /.github/workflows/handle-submission.yml: -------------------------------------------------------------------------------- 1 | name: "Handle Submission" 2 | on: 3 | repository_dispatch: 4 | types: handle-submission 5 | 6 | jobs: 7 | create-pr: 8 | name: Create PR 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Use Node.js ${{ env.NODE_VERSION }} 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: ${{ env.NODE_VERSION }} 16 | - name: Process input 17 | run: | 18 | node actions/process-input.js 19 | - name: Run prettier 20 | run: | 21 | npx prettier submissions/*.json --write 22 | - name: Create Pull Request 23 | uses: peter-evans/create-pull-request@v2 24 | with: 25 | token: ${{ secrets.GH_TOKEN_PERSONAL }} 26 | branch-suffix: random 27 | title: "feat(submission): ${{ github.event.client_payload.name }} - ${{ github.event.client_payload.title }}" 28 | body: | 29 | # ${{ github.event.client_payload.title }} 30 | ## by ${{ github.event.client_payload.name }} 31 | > ${{ github.event.client_payload.description }} 32 | labels: submission 33 | ## To deploy on GitHub Pages 34 | - name: Repository Dispatch 35 | uses: peter-evans/repository-dispatch@v1 36 | with: 37 | token: ${{ secrets.GH_TOKEN_PERSONAL }} 38 | event-type: trigger-deploy 39 | -------------------------------------------------------------------------------- /functions/submit.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const axios = require("axios"); 3 | 4 | const headers = { 5 | "content-type": "application/json", 6 | "Access-Control-Allow-Origin": "*", 7 | "Access-Control-Allow-Methods": "POST", 8 | "Access-Control-Allow-Headers": "Content-Type", 9 | }; 10 | 11 | function preflight() { 12 | return { 13 | statusCode: 204, 14 | headers, 15 | body: {}, 16 | }; 17 | } 18 | 19 | function submit(data) { 20 | const owner = "codegram"; 21 | const name = "jamstack-cfp"; 22 | 23 | return axios.request({ 24 | url: `https://api.github.com/repos/${owner}/${name}/dispatches`, 25 | method: "post", 26 | headers: { 27 | Accept: "application/vnd.github.everest-preview+json", 28 | Authorization: `token ${process.env.GH_TOKEN_PERSONAL}`, 29 | }, 30 | data: { 31 | event_type: "handle-submission", 32 | client_payload: { 33 | name: data.name, 34 | title: data.title, 35 | description: data.description, 36 | date: new Date().getTime(), 37 | }, 38 | }, 39 | }); 40 | } 41 | 42 | export async function handler(request) { 43 | if (request.httpMethod === "OPTIONS") { 44 | return preflight(); 45 | } 46 | 47 | const body = JSON.parse(request.body); 48 | 49 | return submit(body) 50 | .then(() => { 51 | return { 52 | statusCode: 200, 53 | headers, 54 | body: JSON.stringify({ 55 | message: `Success`, 56 | }), 57 | }; 58 | }) 59 | .catch(({ response }) => { 60 | return { 61 | statusCode: response.status || 500, 62 | headers, 63 | body: JSON.stringify(response.data), 64 | }; 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jamstack-cfp", 3 | "private": true, 4 | "scripts": { 5 | "build": "gridsome build && npm run functions:build", 6 | "develop": "env-cmd gridsome develop & npm run functions:serve", 7 | "explore": "gridsome explore", 8 | "test": "jest --watch", 9 | "test:ci": "jest --passWithNoTests", 10 | "functions:serve": "netlify-lambda serve functions", 11 | "functions:build": "netlify-lambda build functions" 12 | }, 13 | "dependencies": { 14 | "@gridsome/source-filesystem": "^0.6.2", 15 | "@gridsome/source-graphql": "^0.1.0", 16 | "@gridsome/transformer-json": "^0.2.1", 17 | "axios": "^0.19.2", 18 | "gridsome": "^0.7.0", 19 | "tailwindcss": "^1.2.0" 20 | }, 21 | "devDependencies": { 22 | "@testing-library/vue": "^5.0.2", 23 | "env-cmd": "^10.1.0", 24 | "eslint": "^6.8.0", 25 | "eslint-config-prettier": "^6.10.1", 26 | "eslint-plugin-gridsome": "^1.4.1", 27 | "eslint-plugin-prettier": "^3.1.2", 28 | "eslint-plugin-simple-import-sort": "^5.0.2", 29 | "gridsome-plugin-tailwindcss": "^2.2.43", 30 | "jest": "^25.3.0", 31 | "jest-transform-graphql": "^2.1.0", 32 | "netlify-lambda": "^1.6.3", 33 | "node-sass": "^4.13.1", 34 | "prettier": "^2.0.4", 35 | "sass-loader": "^8.0.2", 36 | "style-resources-loader": "^1.3.3", 37 | "vue-jest": "^3.0.5", 38 | "webpack-graphql-loader": "^1.0.2" 39 | }, 40 | "jest": { 41 | "moduleFileExtensions": [ 42 | "js", 43 | "json", 44 | "vue" 45 | ], 46 | "moduleNameMapper": { 47 | "^~/(.*)$": "/src/$1" 48 | }, 49 | "transform": { 50 | ".*\\.(vue)$": "vue-jest", 51 | "^.+\\.js$": "/node_modules/babel-jest", 52 | "\\.(gql|graphql)$": "jest-transform-graphql" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/Pending.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | query { 20 | github { 21 | repository(owner: "codegram", name: "jamstack-cfp") { 22 | pullRequests(last: 100, labels: "submission", states: OPEN) { 23 | edges { 24 | node { 25 | id 26 | body 27 | createdAt 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | 36 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JAMStack CFP 2 | 3 | This is an example call for papers application, made to showcase how to make use of GitHub Actions to add user generated content to your JAMStack static site! It's built with Gridsome but the same approach can be done with Gatsby or any other static generator. 4 | 5 | ## How it works 6 | 7 | When a user submits the from, a serverless function triggers a GitHub workflow that generates a pull request that adds a `json` file to the repository. Once this PR is merged, Gridsome generates the site statically listing all the accepted submissions. Connecting the GitHub's GraphQL API to Gridsome, it also generates the pending submissions list from the open PRs. The result is a 100% static site that is able to handle user generated content! 8 | 9 | ## Authentication 10 | 11 | To make it easier to test and play with, I have not added any authentication method, but you can easily connect the app with a GitHub OAuth app or similar to prevent abuse and be able to communicate with users. Just take into consideration not to store any sensitive information if your repository is public (I'm not sure if storing it in a private repository has any legal issues). 12 | 13 | ## When to use it 14 | 15 | Obviously, this approach has some limitations. The submission process can take a few minutes, so if you need immediate feedback it's not a good option. Same if the amount of submissions you need to handle in a short period of time is super high. As a rule of thumb, this is a good usecase for a proof of concept, MVP or small app. 16 | 17 | ## Fork the repository 18 | 19 | Feel free to fork this repository and adapt it to your project. If you do, I'd love to hear about it! Open an issue or reach me [on twitter](https://twitter.com/pincfloit). 20 | 21 | You need to add a `GH_TOKEN_PERSONAL` secret in your repository settings, containing a token with repo scope. You can create the token in [your GitHub settings](https://github.com/settings/tokens). It's going to be used to run GitHub Action workflows, connect with GitHub's GraphQL API and to be able to trigger a repository dispatch event to your repository. 22 | 23 | You will need to update the hardcoded `owner` and `name` values in `src/pages/Pending.vue` and `functions/submit.js` to match your GitHub's uername and repository name. 24 | 25 | ### Run the project locally 26 | 27 | Create an `.env` file containing the `GH_TOKEN_PERSONAL` variable, then run: 28 | 29 | ```npm run develop``` 30 | 31 | ### Deploy 32 | 33 | #### Netlify 34 | 35 | Deploying to [Netlify](https://www.netlify.com/) is the easiest option, as we need a serverless function. 36 | 37 | Add the `GH_TOKEN_PERSONAL` environment variable in your project settings. 38 | 39 | You will also need to configure a hook if you want your page to be rebuilt when a new submission PR is opened. Create a `Build hook` in your settings. Copy the url, and go to your GitHub repository settings: `https://github.com/your-username/your-repo/settings/hooks`. Add a new webook, paste the URL and select `Let me select individual events.`, then choose `Pull requests`. 40 | 41 | Ready to go! 🚀 42 | 43 | #### GitHub pages 44 | 45 | You can choose to deploy to GitHub pages too. The front-end deployment workflow is ready, including the trigger to rebuild the page after a PR is open, but you should handle yourself the deployment of the serverless function to AWS or similar. 46 | 47 | If you are not serving the project from the root of your domain (usually the case when using GitHub pages, when deploying to `https://your-username.github.io/your-repo`), you should also a `BASE_PATH` variable with the correct path (`/your-repo`) to your repository's secrets in order to load the assets correctly. 48 | -------------------------------------------------------------------------------- /src/components/ApplicationForm.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 129 | --------------------------------------------------------------------------------