--- /dev/null
+{
+ "extends": "eslint-config-hapi",
+ "parserOptions": {
+ "ecmaVersion": 2017
+ },
+ "rules": {
+ "indent": [
+ 2,
+ 2
+ ],
+ "no-undef": 2,
+ "require-yield": 0
+ }
+}
# dotenv environment variables file
.env
+# Apple files
+.DS_Store
+
+# Data store
+.posts
+
+# Generated files
+static/assets
+static/index.html
--- /dev/null
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+## v1.0.0 - 2017-07-03
+### Added
+- JSDoc config
+- Eslint config
+- Binary to add and update blog posts
+- Template for index
+- Static Files
+- Simple contributing guidelines
+- This CHANGELOG
+- A Readme
+
+[Unreleased]: https://github.com/rbdr/blog/compare/master...develop
--- /dev/null
+# Contributing to Blog
+
+This blog is a personal project, as such it may not be in the best
+condition for anyone to jump in or roll their own. However, if you find
+this useful and would like to send some improvements, please feel free
+to do so. I really appreciate any contribution!
+
+## The objective of blog
+
+The goal of blog is to have an ephemeral static blog that is generated from
+markdown files and their linked assets. It has a max number of posts at
+a time (the default is 3), and every time you publish it removes
+another.
+
+## How to contribute
+
+Above All: Be nice, always.
+
+* Ensure the linter shows no warnings or errors
+* Don't break the CI
+* Make the PRs according to [Git Flow][gitflow]: (features go to
+ develop, hotfixes go to master)
+
+[gitflow]: https://github.com/nvie/gitflow
# blog
-A temporary blog
+
+Blog at unlimited.pizza -> Only
+
+## How to install
+
+`npm install -g .` will expose the `blog` binary to your CLI.
+
+## How to add a new entry
+
+Create a directory with a `.md` markdown file, and an `/assets`
+directory with anything you want in there. This can be in any directory.
+
+```
+.
+└── this-is-an-example-post
+ ├── assets
+ │ └── example.png
+ └── this-is-an-example-post.md
+```
+
+You can add this to the blog using the following command, it will shift
+all entries and remove the oldest one if limit of posts is reached
+(defualts to 3):
+
+`blog --add path/to/blog_post`
+
+These commands will regenerate the static files. At that point you can
+preview your blog by serving the files on the `static` directory.
+
+If you need to make corrections use:
+
+`blog --update path/to/blog_post`
+
+This will replace the latest with the contents of the `path` without
+shifting the existing entries.
+
+`blog --publish`
+
+Will publish the blog.
+
+## How to publish
+
+At the moment, the app does not include any publishers. [surge][surge] is an easy
+way to do it, just point it to your static directory.
+
+
+[surge]: https://surge.sh
--- /dev/null
+#!/usr/bin/env node
+'use strict';
+
+const Config = require('../config/config');
+const Blog = require('..');
+const Minimist = require('minimist');
+
+const internals = {
+ blog: new Blog(Config),
+ expectedKeys: ['add', 'update', 'publish'],
+
+ // Application entry point. Reads arguments and calls the
+ // corresponding method from the blog lib
+
+ async main() {
+
+ try {
+ const parsedArguments = this._parseArguments();
+
+ for (const argument in parsedArguments) {
+ if (parsedArguments.hasOwnProperty(argument)) {
+
+ const value = parsedArguments[argument];
+
+ if (argument === 'add') {
+ await internals.blog.add(value);
+ return;
+ }
+
+ if (argument === 'update') {
+ await internals.blog.update(value);
+ return;
+ }
+
+ if (argument === 'publish') {
+ await internals.blog.publish(value);
+ return;
+ }
+ }
+ }
+ console.log('Not yet implemented');
+ }
+ catch (err) {
+ console.error(err.message || err);
+ this._printUsage();
+ process.exit(1);
+ }
+ },
+
+ // Parses arguments and returns them if valid. otherwise Throws
+
+ _parseArguments() {
+
+ const parsedArguments = Minimist(process.argv.slice(2));
+
+ if (!this._areArgumentsValid(parsedArguments)) {
+ throw new Error(internals.strings.invalidArguments);
+ }
+
+ return parsedArguments;
+ },
+
+ // Checks if the arguments are valid, returns a boolean value.
+
+ _areArgumentsValid(parsedArguments) {
+
+ const argumentKeys = Object.keys(parsedArguments);
+
+ return argumentKeys.some((key) => internals.expectedKeys.indexOf(key) >= 0);
+ },
+
+ // Prints the usage to stderr
+
+ _printUsage() {
+
+ console.error('\nUsage:\n');
+ console.error('blog --add path/to/blog_post\t\t(creates new blog post)');
+ console.error('blog --update path/to/blog_post\t(updates latest blog post)');
+ console.error('blog --publish \t\t\t(publishes the blog)');
+ }
+};
+
+// Add the strings, added after declaration so they can consume the
+// internals object.
+
+internals.strings = {
+ invalidArguments: `Invalid Arguments, expecting one of: ${internals.expectedKeys.join(', ')}`
+};
+
+
+
+internals.main();
--- /dev/null
+'use strict';
+
+const Path = require('path');
+const Getenv = require('getenv');
+
+const internals = {};
+
+/**
+ * The main configuration object for Blog. It will be used to
+ * initialize all of the sub-components. It can extend any property of
+ * the blog object.
+ *
+ * @memberof Blog
+ * @typedef {object} tConfiguration
+ * @property {number} maxPosts=3 the max number of posts that can exist
+ * at one time
+ * @property {string} postsDirectory=<project_root>/.posts the location of
+ * the directory where the posts will be stored.
+ * @property {string} staticDirectory=<project_root>/static the location of
+ * the directory where the generated files will be placed. NOTE: There
+ * are some pre-built style files in the default directory, if you
+ * select another one, make sure you include them manually.
+ * @property {string} templatesDirectory=<project_root>/templates the
+ * location of the templates we'll use to generate the index.html
+ */
+module.exports = internals.Config = {
+ maxPosts: Getenv.int('BLOG_MAX_POSTS', 3),
+ postsDirectory: Getenv('BLOG_POSTS_DIRECTORY', Path.resolve(Path.join(__dirname, '../.posts'))),
+ staticDirectory: Getenv('BLOG_STATIC_DIRECTORY', Path.resolve(Path.join(__dirname, '../static'))),
+ templatesDirectory: Getenv('BLOG_TEMPLATES_DIRECTORY', Path.resolve(Path.join(__dirname, '../templates')))
+};
--- /dev/null
+{
+ "plugins": ["plugins/markdown"],
+ "opts": {
+ "destination": "doc",
+ "readme": "README.md",
+ "template": "node_modules/docdash",
+ "recurse": true
+ }
+}
--- /dev/null
+# This is the title of another entry
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It should even support
+[links](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+## Subheading 1 (h2)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+* There will be **lists**
+* Lists will have *tags*
+* And everything else [in the world](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+![Picture: two persons in a ceremony][example-image]
+
+### Other types of subheadings, other types of lists (h3)
+
+1. There will be **lists**
+2. Lists will have *tags*
+3. And everything else [in the world](/)
+
+An entry will have paragraphs, these paragraphs will contain text. The
+text should be formatted correctly: visitors will want to read whatever
+is in the blog, so it should be readable. It should account for several
+types of tags, like **strong**, or *emphasised*. It
+should even support [links](/)
+
+#### Finally there are hfours (h4)
+
+And that's about it!
+
+[example-image]: /assets/example_image.png
--- /dev/null
+'use strict';
+
+const Fs = require('fs');
+const Markdown = require('markdown');
+const Mustache = require('mustache');
+const Ncp = require('ncp');
+const Path = require('path');
+const Rimraf = require('rimraf');
+const Util = require('util');
+
+const internals = {
+
+ // Promisified functions
+
+ fs: {
+ access: Util.promisify(Fs.access),
+ mkdir: Util.promisify(Fs.mkdir),
+ readdir: Util.promisify(Fs.readdir),
+ readFile: Util.promisify(Fs.readFile),
+ writeFile: Util.promisify(Fs.writeFile)
+ },
+ ncp: Util.promisify(Ncp.ncp),
+ rimraf: Util.promisify(Rimraf),
+ debuglog: Util.debuglog('blog'),
+
+ // constants
+
+ kAssetsDirectoryName: 'assets',
+ kIndexName: 'index.html',
+ kFileNotFoundError: 'ENOENT',
+ kMarkdownRe: /\.md$/i,
+
+ // Strings
+
+ strings: {
+ markdownNotFound: 'Markdown file was not found in blog directory. Please update.'
+ }
+};
+
+/**
+ * The Blog class is the blog generator, it's in charge of adding and
+ * updating posts, and handling the publishing.
+ *
+ * @class Blog
+ * @param {Potluck.tConfiguration} config the initialization options to
+ * extend the instance
+ */
+module.exports = class Blog {
+
+ constructor(config) {
+
+ Object.assign(this, config);
+ }
+
+ /**
+ * Shifts the blog posts, adds the passed path to slot 0, and
+ * generates files.
+ *
+ * @function add
+ * @memberof Blog
+ * @param {string} postLocation the path to the directory containing
+ * the post structure
+ * @return {Promise<undefined>} empty promise, returns no value
+ * @instance
+ */
+ async add(postLocation) {
+
+ await this._shift();
+ await this.update(postLocation);
+ }
+
+ /**
+ * Adds the passed path to slot 0, and generates files.
+ *
+ * @function update
+ * @memberof Blog
+ * @param {string} postLocation the path to the directory containing
+ * the post structure
+ * @return {Promise<undefined>} empty promise, returns no value
+ * @instance
+ */
+ async update(postLocation) {
+
+ await this._copyPost(postLocation);
+ await this._generate();
+ }
+
+ /**
+ * Publishes the files to a static host.
+ *
+ * @function publish
+ * @memberof Blog
+ * @return {Promise<undefined>} empty promise, returns no value
+ * @instance
+ */
+ async publish() {
+
+ console.error('Publishing not yet implemented');
+ }
+
+ // Parses markdown for each page, copies assets and generates index.
+
+ async _generate() {
+
+ const assetsTarget = Path.join(this.staticDirectory, internals.kAssetsDirectoryName);
+ const indexTarget = Path.join(this.staticDirectory, internals.kIndexName);
+ const indexLocation = Path.join(this.templatesDirectory, internals.kIndexName);
+ const posts = [];
+
+ internals.debuglog(`Removing ${assetsTarget}`);
+ await internals.rimraf(assetsTarget);
+
+ for (let i = 0; i < this.maxPosts; ++i) {
+ const sourcePath = Path.join(this.postsDirectory, `${i}`);
+ const assetsSource = Path.join(sourcePath, internals.kAssetsDirectoryName);
+ const postContentPath = await this._findBlogContent(sourcePath);
+
+ internals.debuglog(`Copying ${assetsSource} to ${assetsTarget}`);
+ await internals.ncp(assetsSource, assetsTarget);
+
+ internals.debuglog(`Reading ${postContentPath}`);
+ const postContent = await internals.fs.readFile(postContentPath, { encoding: 'utf8' });
+
+ internals.debuglog('Parsing markdown');
+ posts.push({
+ html: Markdown.markdown.toHTML(postContent),
+ id: i + 1
+ });
+ }
+
+ internals.debuglog(`Reading ${indexLocation}`);
+ const indexTemplate = await internals.fs.readFile(indexLocation, { encoding: 'utf8' });
+
+ internals.debuglog('Generating HTML');
+ const indexHtml = Mustache.render(indexTemplate, { posts });
+ await internals.fs.writeFile(indexTarget, indexHtml);
+ }
+
+ // Shift the posts, delete any remainder.
+
+ async _shift() {
+
+ await this._ensurePostsDirectoryExists();
+
+ for (let i = this.maxPosts - 1; i > 0; --i) {
+ const targetPath = Path.join(this.postsDirectory, `${i}`);
+ const sourcePath = Path.join(this.postsDirectory, `${i - 1}`);
+
+ try {
+ await internals.fs.access(sourcePath);
+
+ internals.debuglog(`Removing ${targetPath}`);
+ await internals.rimraf(targetPath);
+
+ internals.debuglog(`Shifting blog post ${sourcePath} to ${targetPath}`);
+ await internals.ncp(sourcePath, targetPath);
+ }
+ catch (error) {
+ if (error.code === internals.kFileNotFoundError) {
+ internals.debuglog(`Skipping ${sourcePath}: Does not exist.`);
+ continue;
+ }
+
+ throw error;
+ }
+ }
+ }
+
+ // Copies a post directory to the latest slot.
+
+ async _copyPost(postLocation) {
+
+ await this._ensurePostsDirectoryExists();
+
+ const targetPath = Path.join(this.postsDirectory, '0');
+
+ internals.debuglog(`Removing ${targetPath}`);
+ await internals.rimraf(targetPath);
+
+ internals.debuglog(`Adding ${postLocation} to ${targetPath}`);
+ await internals.ncp(postLocation, targetPath);
+ }
+
+ // Ensures the posts directory exists.
+
+ async _ensurePostsDirectoryExists() {
+
+ internals.debuglog(`Checking if ${this.postsDirectory} exists.`);
+ try {
+ await internals.fs.access(this.postsDirectory);
+ }
+ catch (error) {
+ if (error.code === internals.kFileNotFoundError) {
+ internals.debuglog('Creating posts directory');
+ await internals.fs.mkdir(this.postsDirectory);
+ return;
+ }
+
+ throw error;
+ }
+ }
+
+ // Looks for a `.md` file in the blog directory, and returns the path
+
+ async _findBlogContent(directory) {
+
+ const entries = await internals.fs.readdir(directory);
+
+ const markdownEntries = entries
+ .filter((entry) => internals.kMarkdownRe.test(entry))
+ .map((entry) => Path.join(directory, entry));
+
+ if (markdownEntries.length > 0) {
+ internals.debuglog(`Found markdown file: ${markdownEntries[0]}`);
+ return markdownEntries[0];
+ }
+
+ throw new Error(internals.strings.markdownNotFound);
+ }
+};
--- /dev/null
+{
+ "name": "blog",
+ "version": "1.0.0",
+ "description": "An ephemeral blog",
+ "main": "lib/blog.js",
+ "bin": {
+ "blog": "./bin/blog.js"
+ },
+ "scripts": {
+ "document": "jsdoc -c ./config/jsdoc.json lib config",
+ "lint": "eslint .",
+ "test": "echo \":(\""
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rbdr/blog.git"
+ },
+ "author": "Ben Beltran <ben@nsovocal.com>",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/rbdr/blog/issues"
+ },
+ "homepage": "https://github.com/rbdr/blog#readme",
+ "dependencies": {
+ "getenv": "0.7.x",
+ "markdown": "0.5.x",
+ "minimist": "1.2.x",
+ "mustache": "2.3.x",
+ "ncp": "2.0.x",
+ "rimraf": "2.6.x"
+ },
+ "devDependencies": {
+ "docdash": "0.4.x",
+ "eslint": "4.1.x",
+ "eslint-config-hapi": "10.0.x",
+ "eslint-plugin-hapi": "4.0.x",
+ "jsdoc": "3.4.x"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+}
--- /dev/null
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background-image: url('/images/header_background.png');
+ background-size: auto 300px;
+ background-attachment: fixed;
+ line-height: 1.45;
+}
+
+header {
+ background-image: url('/images/header_foreground.png');
+ background-repeat: no-repeat;
+ background-size: auto 300px;
+ height: 300px;
+}
+
+header a {
+ color: transparent;
+ display: block;
+ max-height: 500px;
+}
+
+main {
+ background-color: #fff;
+ padding: 1.414em;
+}
+
+h1, h2, h3, h4 {
+ margin: 1.414em 0 0.5em;
+ font-weight: 400;
+}
+
+p, ul, ol, img {
+ width: 100%;
+ margin: 1.414em 0;
+ max-width: 30em;
+}
+
+ul, ol { margin-left: 1.414em; }
+
+h1 { font-size: 3.998em; }
+h2 { font-size: 2.827em; }
+h3 { font-size: 1.999em; }
+h4 { font-size: 1.414em; }
+
+footer {
+ background-color: pink;
+ padding: 1.414em;
+}
--- /dev/null
+'use strict';
+
+/* globals window */
+
+((window) => {
+
+ const internals = {
+ kEntryPoint: '.event-details',
+
+ // Application entry point, for now just band to keyboard
+
+ main() {
+
+ window.document.addEventListener('keydown', internals._onKeyDown);
+ },
+
+ // Handles key events to point to the correct blog post.
+
+ _onKeyDown(event) {
+
+ if (['1','2','3'].indexOf(event.key) > -1) {
+ window.location.hash = event.key;
+ }
+ }
+ };
+
+ window.addEventListener('load', internals.main);
+})(window);
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="description" content="This is the blog at unlimited.pizza">
+
+ <title>blog 🍕</title>
+
+ <script src="/js/blog.js"></script>
+
+ <link href="css/style.css" rel="stylesheet">
+
+ </head>
+ <body>
+ <header class="main-header">
+ <a href="/">Blog</a>
+ </header>
+ <main>
+ {{#posts}}
+ <article id="{{id}}">
+ {{{html}}}
+ </article>
+ <hr>
+ {{/posts}}
+ {{^posts}}
+ <h1>This is a fresh blog!</h1>
+ <p>There are no posts yet.</p>
+ {{/posts}}
+ </main>
+ <footer>
+ <p>Only 3 entries kept at any time. Press 1, 2, and 3 to switch. <a href="https://unlimited.pizza">unlimited.pizza</a></p>
+ </footer>
+ </body>
+</html>