├── 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 | ![](https://raw.githubusercontent.com/noffle/ipfs-blog/master/static/screenshot.png) 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 |
9 |

10 |

11 |
12 |
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 --------------------------------------------------------------------------------