├── .nvmrc ├── dummy ├── .gitignore ├── Dockerfile ├── static │ └── favicon.ico ├── assets │ └── css │ │ └── _user-settings.scss └── build-and-run.sh ├── static ├── icon.png └── favicon.png ├── .dockerignore ├── assets ├── img │ ├── gh-banner.jpg │ ├── gustavo.sketch │ ├── gh-banner.sketch │ └── gustavo.svg └── css │ ├── _user-settings.scss │ ├── _grid.scss │ ├── _settings.scss │ ├── _highlight.scss │ └── main.scss ├── server ├── babel-register.js ├── api │ ├── index.js │ └── content.js └── index.js ├── middleware └── hide-nav.js ├── plugins ├── vue-moment.js └── to-iso-date.js ├── .babelrc ├── components ├── post │ ├── Content.vue │ └── Post.vue ├── site │ ├── BackHome.vue │ ├── NavLink.vue │ ├── Head.vue │ └── Nav.vue └── posts │ └── Item.vue ├── .travis.yml ├── .gitignore ├── Dockerfile ├── layouts ├── error.vue └── default.vue ├── pages ├── index.vue ├── post │ └── _id.vue └── _id.vue ├── .eslintrc.js ├── nuxt.config.js ├── store ├── index.js └── parser.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /dummy/.gitignore: -------------------------------------------------------------------------------- 1 | gustavo.config.js 2 | -------------------------------------------------------------------------------- /dummy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eggplanet/gustavo 2 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggplanetio/gustavo/HEAD/static/icon.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | .nuxt 4 | dummy 5 | gustavo.config.js 6 | build 7 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggplanetio/gustavo/HEAD/static/favicon.png -------------------------------------------------------------------------------- /assets/img/gh-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggplanetio/gustavo/HEAD/assets/img/gh-banner.jpg -------------------------------------------------------------------------------- /dummy/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggplanetio/gustavo/HEAD/dummy/static/favicon.ico -------------------------------------------------------------------------------- /assets/img/gustavo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggplanetio/gustavo/HEAD/assets/img/gustavo.sketch -------------------------------------------------------------------------------- /server/babel-register.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | module.exports = require('./index.js').default 3 | -------------------------------------------------------------------------------- /assets/img/gh-banner.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggplanetio/gustavo/HEAD/assets/img/gh-banner.sketch -------------------------------------------------------------------------------- /middleware/hide-nav.js: -------------------------------------------------------------------------------- 1 | 2 | export default function (context) { 3 | context.store.commit('HIDE_NAV') 4 | } 5 | -------------------------------------------------------------------------------- /plugins/vue-moment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueMoment from 'vue-moment' 3 | Vue.use(VueMoment) 4 | -------------------------------------------------------------------------------- /dummy/assets/css/_user-settings.scss: -------------------------------------------------------------------------------- 1 | // Colors. 2 | $color-light: white; 3 | $color-gray: #eee; 4 | $color-dark: blue; 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "ignore": [ 6 | "build", 7 | "node_modules" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /assets/css/_user-settings.scss: -------------------------------------------------------------------------------- 1 | /* 2 | This file will eventually be used 3 | to allow other developers the ability 4 | to override colors, etc. 5 | */ 6 | -------------------------------------------------------------------------------- /plugins/to-iso-date.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.use({ 3 | install () { 4 | Vue.filter('toIsoDate', dateString => new Date(dateString)) 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /components/post/Content.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | before_install: 5 | - npm install -g yarn 6 | install: 7 | - yarn install 8 | script: 9 | - yarn run lint 10 | -------------------------------------------------------------------------------- /dummy/build-and-run.sh: -------------------------------------------------------------------------------- 1 | docker rmi -f eggplanet/gustavo 2 | docker build -t eggplanet/gustavo . 3 | 4 | cd ./dummy 5 | docker rmi -f gustavo-dummy 6 | docker rm -f gustavo-dummy 7 | docker build -t gustavo-dummy . 8 | docker run -p 4000:3000 --name gustavo-dummy gustavo-dummy 9 | 10 | # docker stop gustavo-dummy 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Build. 11 | dist 12 | build 13 | 14 | # gustavo config 15 | gustavo.config.js 16 | dummy/gustavo.config.js 17 | 18 | # @nuxt/workbox 19 | static/sw.js 20 | static/workbox-sw*.js 21 | 22 | .esm-cache 23 | -------------------------------------------------------------------------------- /assets/css/_grid.scss: -------------------------------------------------------------------------------- 1 | //
3 | [{{ error.statusCode }}] {{ error.message }}
4 |
5 |
6 |
11 |
12 |
29 |
--------------------------------------------------------------------------------
/components/site/BackHome.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | ←
4 |
5 |
6 |
7 |
11 |
12 |
24 |
--------------------------------------------------------------------------------
/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
/g, '')
32 |
33 | const firstSentence = content.slice(3)
34 | .split('.')[0] + '.'
35 |
36 | return {
37 | id,
38 | content,
39 | path,
40 | meta,
41 | firstSentence
42 | }
43 | },
44 |
45 | parsePost (rawHtml) {
46 | return this.parseItem(rawHtml, '.post.md')
47 | },
48 |
49 | parsePage (rawHtml) {
50 | return this.parseItem(rawHtml, '.page.md')
51 | },
52 |
53 | parsePosts (files) {
54 | return files
55 | .filter(file => file.filename.includes('.post.md'))
56 | .map(raw => this.parseItem(raw, '.post'))
57 | .sort((current, other) => new Date(other.meta.date) - new Date(current.meta.date))
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/assets/css/_highlight.scss:
--------------------------------------------------------------------------------
1 | /* Base16 Atelier Cave Light - Theme */
2 | /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave) */
3 | /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */
4 |
5 | /* Atelier-Cave Comment */
6 | .hljs-comment,
7 | .hljs-quote {
8 | color: #655f6d;
9 | }
10 |
11 | /* Atelier-Cave Red */
12 | .hljs-variable,
13 | .hljs-template-variable,
14 | .hljs-attribute,
15 | .hljs-tag,
16 | .hljs-name,
17 | .hljs-regexp,
18 | .hljs-link,
19 | .hljs-name,
20 | .hljs-name,
21 | .hljs-selector-id,
22 | .hljs-selector-class {
23 | color: #be4678;
24 | }
25 |
26 | /* Atelier-Cave Orange */
27 | .hljs-number,
28 | .hljs-meta,
29 | .hljs-built_in,
30 | .hljs-builtin-name,
31 | .hljs-literal,
32 | .hljs-type,
33 | .hljs-params {
34 | color: #aa573c;
35 | }
36 |
37 | /* Atelier-Cave Green */
38 | .hljs-string,
39 | .hljs-symbol,
40 | .hljs-bullet {
41 | color: #2a9292;
42 | }
43 |
44 | /* Atelier-Cave Blue */
45 | .hljs-title,
46 | .hljs-section {
47 | color: #576ddb;
48 | }
49 |
50 | /* Atelier-Cave Purple */
51 | .hljs-keyword,
52 | .hljs-selector-tag {
53 | color: #955ae7;
54 | }
55 |
56 | .hljs-deletion,
57 | .hljs-addition {
58 | color: #19171c;
59 | display: inline-block;
60 | width: 100%;
61 | }
62 |
63 | .hljs-deletion {
64 | background-color: #be4678;
65 | }
66 |
67 | .hljs-addition {
68 | background-color: #2a9292;
69 | }
70 |
71 | .hljs {
72 | display: block;
73 | overflow-x: auto;
74 | // background: #efecf4;
75 | color: #585260;
76 | }
77 |
78 | .hljs-emphasis {
79 | font-style: italic;
80 | }
81 |
82 | .hljs-strong {
83 | font-weight: bold;
84 | }
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gustavo",
3 | "version": "1.0.0",
4 | "description": "A (mostly) headless blogging platform built atop Nuxt & Gist.",
5 | "author": "Brian Gonzalez ",
6 | "scripts": {
7 | "dev": "nodemon ./server/babel-register.js",
8 | "start": "node ./server/babel-register.js",
9 | "precommit": "npm run lint",
10 | "lint": "eslint --ext .html,.js,.vue --ignore-path .gitignore .",
11 | "docker-publish": "docker build -t eggplanet/gustavo . && docker push eggplanet/gustavo"
12 | },
13 | "dependencies": {
14 | "@nuxtjs/component-cache": "^1.1.0",
15 | "@nuxtjs/google-analytics": "^1.0.0",
16 | "@nuxtjs/pwa": "^1.0.2",
17 | "apicache": "^1.1.0",
18 | "axios": "^0.16.2",
19 | "babel-polyfill": "^6.26.0",
20 | "babel-register": "^6.26.0",
21 | "compression": "^1.7.1",
22 | "express": "^4.16.1",
23 | "express-http-proxy": "^1.0.7",
24 | "highlight.js": "^9.12.0",
25 | "html-element": "^2.2.0",
26 | "lodash.uniqby": "^4.7.0",
27 | "morgan": "^1.9.0",
28 | "node-sass": "^4.5.3",
29 | "normalize.css": "^7.0.0",
30 | "nuxt": "^1.0.0-rc11",
31 | "sass-loader": "^6.0.6",
32 | "showdown": "^1.7.6",
33 | "source-map-support": "^0.5.0",
34 | "vue-moment": "^2.0.2",
35 | "xmldom": "^0.1.27"
36 | },
37 | "babel-cli": "^6.26.0",
38 | "devDependencies": {
39 | "babel-eslint": "^8.0.1",
40 | "babel-preset-env": "^1.6.0",
41 | "eslint": "^4.8.0",
42 | "eslint-config-standard": "^10.2.1",
43 | "eslint-plugin-html": "^3.2.2",
44 | "eslint-plugin-import": "^2.7.0",
45 | "eslint-plugin-node": "^5.2.0",
46 | "eslint-plugin-promise": "^3.5.0",
47 | "eslint-plugin-standard": "^3.0.1",
48 | "eslint-plugin-vue": "^2.1.0",
49 | "nodemon": "^1.12.1"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/assets/css/main.scss:
--------------------------------------------------------------------------------
1 | @import 'user-settings';
2 | @import 'settings';
3 | @import 'grid';
4 | @import 'highlight';
5 |
6 | * {
7 | box-sizing: border-box;
8 | }
9 |
10 | html {
11 | display: flex;
12 | flex-direction: column;
13 | min-height: 100vh;
14 | background: $color-background;
15 | }
16 |
17 | body {
18 | font-family: $font-family;
19 | font-size: $font-size;
20 | -webkit-font-smoothing: antialiased;
21 | padding: $size-unit * 3;
22 | flex: 1;
23 | display: flex;
24 | flex-direction: column;
25 | color: $color-font;
26 | }
27 |
28 | p {
29 | margin-top: 0;
30 | margin-bottom: $size-unit;
31 | }
32 |
33 | h1, h2, h3, h4 {
34 | font-size: $font-size;
35 | font-weight: normal;
36 | margin-top: 0;
37 | position: relative;
38 | z-index: -1;
39 |
40 | a { text-decoration: none; }
41 | &:before {
42 | color: $color-heading-hash;
43 | font-style: italic;
44 | }
45 | }
46 |
47 | main {
48 | h2,
49 | h3,
50 | h4 {
51 | margin: $size-unit 0;
52 | border-bottom: 1px solid $color-border;
53 | padding-right: $size-unit;
54 | display: inline-block;
55 | }
56 |
57 | h2:after,
58 | h3:after,
59 | h4:after { display: block }
60 | }
61 |
62 | main ul,
63 | main ol {
64 | padding-left: $size-unit;
65 |
66 | li {
67 | margin-bottom: $size-unit;
68 | }
69 | }
70 |
71 | main blockquote {
72 | margin: 0;
73 | padding-left: $size-unit;
74 | border-left: 1px solid $color-border;
75 | }
76 |
77 | a,
78 | a:hover,
79 | a:visited,
80 | a:active {
81 | color: $color-font;
82 | text-decoration: none;
83 |
84 | main & {
85 | background: $color-link-background;
86 | }
87 |
88 | }
89 |
90 | main {
91 | max-width: $size-unit * 25;
92 | flex: 1;
93 | line-height: 1.9;
94 |
95 | table {
96 | width: 100%;
97 | display: block;
98 | overflow-x: scroll;
99 | }
100 |
101 | }
102 |
103 | time,
104 | author {
105 | opacity: 0.3;
106 | }
107 |
108 | ul {
109 | margin-top: 0;
110 | }
111 |
112 | pre {
113 | background: $color-code;
114 | overflow-x: scroll;
115 | padding: $size-unit;
116 | }
117 |
118 | code {
119 | font-family: $font-family-code;
120 | }
121 |
122 | h1,
123 | h2,
124 | p,
125 | li {
126 | code { background: $color-code; }
127 | }
128 |
129 | table {
130 | background: $color-code;
131 |
132 | th {
133 | text-align: left;
134 | }
135 |
136 | tr td,
137 | tr th {
138 | border-bottom: 1px solid $color-border;
139 | padding: $size-unit $size-unit/2;
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/server/api/content.js:
--------------------------------------------------------------------------------
1 | import proxy from 'express-http-proxy'
2 | import config from '../../nuxt.config.js'
3 |
4 | const githubToken = config.gustavo.githubToken || process.env.GITHUB_TOKEN
5 | const gistId = config.gustavo.gistId || process.env.GIST_ID
6 |
7 | /* eslint-disable no-console */
8 | if (typeof githubToken === 'undefined') {
9 | console.warn(`Github Token not provided. You will be rate limited.`)
10 | }
11 |
12 | if (!gistId) {
13 | throw new Error(`No Gist ID found in config or via ENV variable.`)
14 | }
15 |
16 | const url = `api.github.com`
17 |
18 | const defaults = {
19 | https: true,
20 | proxyReqPathResolver: (req) => {
21 | return `/gists/${gistId}`
22 | },
23 | proxyReqOptDecorator (proxyReqOpts, srcReq) {
24 | proxyReqOpts.headers['Authorization'] = `token ${githubToken}`
25 | return proxyReqOpts
26 | }
27 | }
28 |
29 | export const post = proxy(url, Object.assign({}, defaults, {
30 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
31 | let files = JSON.parse(proxyResData.toString('utf8')).files
32 | const fileNames = Object.keys(files)
33 | const fileName = fileNames.find(name => name === `${userReq.params.id}.post.md`)
34 |
35 | if (!fileName) {
36 | userRes.status(404)
37 | return 'Not found.'
38 | } else {
39 | return JSON.stringify({ post: files[fileName] })
40 | }
41 | }
42 | }))
43 |
44 | export const page = proxy(url, Object.assign({}, defaults, {
45 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
46 | let files = JSON.parse(proxyResData.toString('utf8')).files
47 | const fileNames = Object.keys(files)
48 | const fileName = fileNames.find(name => name === `${userReq.params.id}.page.md`)
49 |
50 | if (!fileName) {
51 | userRes.status(404)
52 | return 'Not found.'
53 | } else {
54 | return JSON.stringify({ page: files[fileName] })
55 | }
56 | }
57 | }))
58 |
59 | export const posts = proxy(url, Object.assign({}, defaults, {
60 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
61 | let files = JSON.parse(proxyResData.toString('utf8')).files
62 | const fileNames = Object.keys(files)
63 | const posts = []
64 |
65 | fileNames
66 | .filter(name => !name.endsWith('.draft.post.md'))
67 | .forEach(name => {
68 | if (name.endsWith('.post.md')) {
69 | posts.push(files[name])
70 | }
71 | })
72 |
73 | return JSON.stringify({ posts })
74 | }
75 | }))
76 |
77 | export const links = proxy(url, Object.assign({}, defaults, {
78 | userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
79 | let files = JSON.parse(proxyResData.toString('utf8')).files
80 | const links = files[`links.md`]
81 | return JSON.stringify({ links })
82 | }
83 | }))
84 |
--------------------------------------------------------------------------------
/components/site/Nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
63 |
64 |
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | > A (mostly) headless blogging platform built atop Nuxt & Gist.
4 |
5 | [Demo](https://www.briangonzalez.org)
6 |
7 | ### Contents
8 |
9 | - [Overview](#overview)
10 | - [Creating content](#creating-content)
11 | - [Getting started](#getting-started)
12 | - [Deployment](#deployment)
13 |
14 | ## Overview
15 |
16 | Gustavo is an opinionated, (mostly) [headless](https://headlesscms.org/) blogging
17 | platform built to use:
18 |
19 | - Github Gist
20 | - Nuxt (Vue 2.x)
21 | - Docker
22 |
23 | Using a simple naming schema, Gustavo can create a whole blog for you in seconds. Don't believe me?
24 | Check out the [gist](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e) that is [my blog](https://www.briangonzalez.org).
25 |
26 | ## Creating content
27 |
28 | You can create content for your blog by simply creating
29 | files in a gist that follow this schema:
30 |
31 | | Type | Naming | Example |
32 | |-------------|-------------------------| --------------------------------------------------------------------------------------------------------------------|
33 | | post | `{name}.post.md` | [link](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e#file-lazy-leadership-post-md) |
34 | | page | `{name}.page.md` | [link](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e#file-about-page-md) |
35 | | navigation | `links.md` | [link](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e#file-links-md) |
36 | | navigation | `links.txt` (deprecated)| [link](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e#file-links-txt) |
37 | | image | clone gist and upload | [link](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e#file-your-speed-jpg) |
38 | | draft | `{name}.post.draft.md` | |
39 |
40 | Here is the [gist](https://gist.github.com/briangonzalez/2ece66bfffff31ddc230ca8342e80b3e)
41 | that powers [this blog](https://www.briangonzalez.org).
42 |
43 | ## Getting started
44 |
45 | To create a blog, follow these steps:
46 |
47 | 1. Create your gist and add some content.
48 |
49 | 2. Create `gustavo.config.js` with the following:
50 |
51 | ```js
52 | module.exports = {
53 | title: 'My gustavo blog',
54 | gistId: '<< gist id >>',
55 | githubToken: '<< token >>', /* optional, recommended */
56 | googleAnalyticsId: 'UA-X-XXXXX'
57 | }
58 | ```
59 |
60 | _Note: you'll want to create a [personal access token](https://github.com/settings/tokens) on Github because Gustavo uses the Gist API, and without the token and although it will still work, your blog will be rate limited._
61 |
62 | 3. Create a `Dockerfile` with the following:
63 |
64 | ```docker
65 | FROM eggplanet/gustavo:latest
66 | ```
67 |
68 | 4. Let's start it up:
69 |
70 | ```bash
71 | $ docker build -t my-gustavo-blog .
72 | $ docker run -p 3000:3000 my-gustavo-blog
73 | ```
74 |
75 | Your blog will be running at http://localhost:3000
76 |
77 | ## Deployment
78 |
79 | Deploying gustavo is simple. The recommended method is [Now by Zeit](https://zeit.co/now).
80 |
81 | ```bash
82 | $ now secrets add gustavo-github-token
83 | $ now secrets add gustavo-gist-id
84 | $ now -e GITHUB_TOKEN=@gustavo-github-token -e GIST_ID=@gustavo-gist-id --docker
85 | $ now alias my-gustavo-blog-wjdihnxorf.now.sh my-gustavo.blog
86 | ```
87 |
88 | ### License
89 |
90 | - MIT
91 |
92 | ### Credits
93 |
94 | - [Logo](https://thenounproject.com/search/?q=man&i=542085)
95 |
96 | ### Releasing a new image
97 |
98 | ```bash
99 | $ docker build -t eggplanet/gustavo:latest .
100 | $ docker push eggplanet/gustavo:latest
101 | ```
102 |
103 | ### Changelog
104 |
105 | _2.0.0_
106 |
107 | - Better caching using [apicache](https://github.com/kwhitley/apicache)
108 | - Cleaned up a bunch of dead code
109 | - nuxt@^1.0.0-rc11
110 |
111 | _1.0.0_
112 |
113 | - Initial release
114 |
--------------------------------------------------------------------------------