Commit 5e265f9d authored by Rubén Beltran del Río's avatar Rubén Beltran del Río 🐝

Merge branch 'release/1.0.0'

parents 4ae55e06 be33bb82
{
"extends": "eslint-config-hapi",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": [
2,
2
],
"no-undef": 2,
"require-yield": 0
}
}
......@@ -57,3 +57,12 @@ typings/
# dotenv environment variables file
.env
# Apple files
.DS_Store
# Data store
.posts
# Generated files
static/assets
static/index.html
# 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
# 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
#!/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();
'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')))
};
{
"plugins": ["plugins/markdown"],
"opts": {
"destination": "doc",
"readme": "README.md",
"template": "node_modules/docdash",
"recurse": true
}
}
# 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
'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);
}
};
{
"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"
}
}
* {
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;
}
'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);
<!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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment