├── README.md
├── article.html
├── cli.js
├── index.css
├── index.html
├── index.js
├── package.json
└── static
└── screenshot.png
/README.md:
--------------------------------------------------------------------------------
1 | # ipfs-blog
2 |
3 | > opinionated ipfs-powered blog
4 |
5 | ## Background
6 |
7 | Wouldn't it be nice to have a simple static HTML blog that you didn't need to
8 | worry about hosting?
9 |
10 | ## Prerequisites
11 |
12 | You'll need [IPFS](https://ipfs.io) installed, at least
13 | [0.4.0](http://dist.ipfs.io/#go-ipfs).
14 |
15 | ## Usage
16 |
17 | ```sh
18 | $ npm install -g ipfs-blog
19 |
20 | $ cd /tmp
21 |
22 | $ cat > article.md
23 | # Hello world!
24 |
25 | This is my very first blog entry to the permanent web! Huzzah!
26 | ^D
27 |
28 | $ ipfs-blog --title "My Permanent Blarg"
29 | wrote article.md
30 | https://ipfs.io/ipfs/QmR8kn6CQzBADU6BvHPnvnkpXKykkJVwjJVujPqZz3nWDj
31 | ```
32 |
33 | 
34 |
35 | Note that you'll need a local [IPFS daemon](https://dist.ipfs.io/#go-ipfs)
36 | running in order to publish.
37 |
38 | ## CLI
39 |
40 | All markdown files (`*.markdown`, `*.md`) in the current directory will be
41 | published.
42 |
43 | Each article will use its `mtime` (last modified time) as its publish date. You
44 | can use e.g. `touch` to fudge this to a custom time if you'd like: `touch -d
45 | "Fri Sep 23 17:44:32 PDT 2016" article.md`.
46 |
47 | You can pass `-t | --title` with a string to specify the name of the blog.
48 |
49 | ## Customization
50 |
51 | All flavour text is hard-coded for now. PRs that better facilitate user
52 | customization (color scheme, etc) are very welcome!
53 |
54 | ## License
55 |
56 | ISC
57 |
--------------------------------------------------------------------------------
/article.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | The Permanent Blog
6 |
7 | The Permanent Blog
8 |
13 |
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var publish = require('./index')
4 | var fs = require('fs')
5 |
6 | var args = require('minimist')(process.argv)
7 |
8 | var files = fs.readdirSync(process.cwd())
9 | files = files.filter(function isMarkdown (name) {
10 | return name.endsWith('.markdown') || name.endsWith('.md')
11 | })
12 |
13 | if (!files.length) {
14 | console.error('no .md or .markdown files in this directory')
15 | process.exit(1)
16 | }
17 |
18 | var opts = {
19 | title: args.t || args.title || 'The Permanent Blog'
20 | }
21 |
22 | publish(files, opts)
23 |
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: Arial, sans-serif;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | }
8 |
9 | h1, h2, h3 {
10 | color: rgb(55, 94, 171);
11 | font-weight: bold;
12 | padding: 0px;
13 | }
14 |
15 | .title {
16 | margin-left: 15px;
17 | }
18 |
19 | #blog-title {
20 | background-color: #E0EBF5;
21 | height: 24px;
22 | padding-top: 25px;
23 | padding-bottom: 20px;
24 | padding-left: 15px;
25 | margin: 0;
26 | color: #666;
27 | font-size: 20px;
28 | font-weight: normal;
29 | }
30 |
31 | .article-item {
32 | margin-bottom: 8px;
33 | }
34 |
35 | h4 {
36 | color: #999;
37 | margin: 25px;
38 | font-size: 16px;
39 | font-weight: normal;
40 | }
41 |
42 | li {
43 | color: #222;
44 | }
45 |
46 | p {
47 | margin: 25px;
48 | -webkit-margin-before: 1em;
49 | -webkit-margin-after: 1em;
50 | -webkit-margin-start: 0px;
51 | -webkit-margin-end: 0px;
52 | }
53 |
54 | #body {
55 | line-height: 1.3em;
56 | font-size: 16px;
57 | }
58 |
59 | .wrapper {
60 | width: 700px;
61 | margin-left: auto;
62 | margin-right: auto;
63 | }
64 |
65 | .site-footer {
66 | margin-top: 20px;
67 | padding-top: 20px;
68 | border-top: 1px solid #e8e8e8;
69 | padding-bottom: 20px;
70 | }
71 |
72 | .created-note {
73 | text-align: right;
74 | color: #888888;
75 | margin-top: 6px;
76 | margin-bottom: 2px;
77 | }
78 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | The Permanent Blog
6 |
7 | The Permanent Blog
8 | Articles
9 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | M1:
3 | x grab all *.(md|markdown) files
4 | x compile them all to HTML
5 | x generate an index.html that links to all articles
6 | x add all to IPFS and output public gateway link
7 |
8 | M2:
9 | x inject css into index
10 | x show titles on index.html
11 | x inject css into each article
12 | x date on each article
13 |
14 | M3:
15 | - show blog title on all pages in dynamic fashion
16 | */
17 |
18 | var fs = require('fs')
19 | var marked = require('marked')
20 | var trumpet = require('trumpet')
21 | var bl = require('bl')
22 | var comandante = require('comandante')
23 | var tmp = require('tmp')
24 | var path = require('path')
25 |
26 | module.exports = function (files, opts) {
27 | opts = opts || {}
28 | opts.title = opts.title || 'The Permanent Blog'
29 |
30 | // create a temp dir
31 | var tmpdir = tmp.dirSync({
32 | unsafeCleanup: true
33 | })
34 |
35 | // Copy over CSS
36 | comandante('cp', [__dirname + '/index.css', tmpdir.name])
37 |
38 | // fire up trumpet
39 | var tr = trumpet()
40 |
41 | // prepare to write index.html
42 | var index = path.join(tmpdir.name, 'index.html')
43 | tr.pipe(fs.createWriteStream(index))
44 |
45 | var bws = tr.select('#blog-title').createWriteStream()
46 | bws.end(opts.title)
47 |
48 | tr.select('title').createWriteStream().end(opts.title)
49 |
50 | // prepare to fill in articles
51 | var ws = tr.select('#blog-articles').createWriteStream()
52 |
53 | files.sort(function compare(a, b) {
54 | return fs.statSync(b).mtime - fs.statSync(a).mtime
55 | })
56 |
57 | var articlesToWrite = files.length + 1
58 |
59 | // process all articles
60 | files.forEach(function (file) {
61 | var articleMd = fs.readFileSync(file).toString()
62 |
63 | // extract title
64 | var title = articleMd.substring(0, articleMd.indexOf('\n'))
65 | .replace(/#+ /, '')
66 | articleMd = articleMd.substring(articleMd.indexOf('\n')+1)
67 |
68 | // compile to HTML
69 | var html = marked(articleMd)
70 | var fileHtml = file
71 | .replace('.md', '.html')
72 | .replace('.markdown', '.html')
73 |
74 | // write entry to index.html
75 | var stat = fs.statSync(file)
76 | ws.write('\n ' + stat.mtime + ' - ' + title + ' \n')
77 |
78 | // prepare to write article into article.html template
79 | var atr = trumpet()
80 | var fname = path.join(tmpdir.name, fileHtml)
81 | atr.pipe(fs.createWriteStream(fname))
82 |
83 | // write HTML to article body
84 | var buf = new bl()
85 | buf.append(new Buffer(html))
86 | buf.pipe(atr.select('#body').createWriteStream())
87 |
88 | var tws = atr.select('.title').createWriteStream()
89 | tws.end(title)
90 |
91 | atr.select('title').createWriteStream().end(opts.title)
92 |
93 | var bws = atr.select('#blog-title').createWriteStream()
94 | bws.end(opts.title)
95 |
96 | var dws = atr.select('#date').createWriteStream()
97 | dws.end(stat.mtime.toString())
98 |
99 | // use template
100 | fs.createReadStream(__dirname + '/article.html').pipe(atr)
101 |
102 | atr.on('end', function() {
103 | articlesToWrite--
104 | console.error('wrote', title)
105 | if (articlesToWrite <= 0) {
106 | publish()
107 | }
108 | })
109 | })
110 |
111 | ws.end()
112 |
113 | var rootHash = ''
114 | fs.createReadStream(__dirname + '/index.html').pipe(tr)
115 | .on('end', function () {
116 | articlesToWrite--
117 | if (articlesToWrite <= 0) {
118 | publish()
119 | }
120 | })
121 |
122 | // publish to IPFS using local daemon
123 | function publish () {
124 | comandante('ipfs', ('add -rq ' + tmpdir.name).split(' '))
125 | .on('data', function (hash) {
126 | rootHash = hash.toString().trim()
127 | })
128 | .on('end', function () {
129 | console.log('https://ipfs.io/ipfs/' + rootHash)
130 | console.log('http://localhost:8080/ipfs/' + rootHash)
131 | tmpdir.removeCallback()
132 | })
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ipfs-blog",
3 | "description": "opinionated IPFS-powered blog",
4 | "version": "0.2.1",
5 | "repository": {
6 | "url": "git://github.com/noffle/ipfs-blog.git"
7 | },
8 | "bin": {
9 | "ipfs-blog": "cli.js"
10 | },
11 | "main": "index.js",
12 | "dependencies": {
13 | "bl": "^1.1.2",
14 | "comandante": "0.0.1",
15 | "marked": "^0.3.5",
16 | "minimist": "^1.2.0",
17 | "tmp": "0.0.28",
18 | "trumpet": "^1.7.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/static/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackergrrl/ipfs-blog/4b58725034da34299bb5cc17ecfdd88073bf2a4e/static/screenshot.png
--------------------------------------------------------------------------------