jekyll-recker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.org ADDED
@@ -0,0 +1,119 @@
1
+ #+TITLE: jekyll-recker
2
+ #+SLUG: jekyll-recker.html
3
+ #+PERMALINK: jekyll-recker.html
4
+ #+STARTUP: showall
5
+ #+DESCRIPTION: my website's custom jekyll plugin
6
+
7
+ This is the jekyll plugin for my personal website.
8
+
9
+ * Installation
10
+
11
+ Add =jekyll-recker= to the =jekyll_plugins= group of your =Gemfile=.
12
+
13
+ #+BEGIN_SRC ruby
14
+ group :jekyll_plugins do
15
+ gem 'jekyll-recker', :git => 'https://github.com/arecker/jekyll-recker.git'
16
+ end
17
+ #+END_SRC
18
+
19
+ Add =jekyll-recker= to the list of plugins in jekyll's =_config.yml=.
20
+
21
+ #+BEGIN_SRC yaml
22
+ # _config.yaml
23
+ plugins:
24
+ - jekyll-recker
25
+ #+END_SRC
26
+
27
+ Set the theme.
28
+
29
+ #+BEGIN_SRC yaml
30
+ theme: jekyll-recker
31
+ #+END_SRC
32
+
33
+ Install and enjoy.
34
+
35
+ #+BEGIN_SRC sh
36
+ bundle install
37
+ bundle exec jekyll serve
38
+ #+END_SRC
39
+
40
+ * Usage
41
+
42
+ ** Commands
43
+
44
+ *** =tweet=
45
+
46
+ The =tweet= command tweets a link to the latest published jekyll blog
47
+ post.
48
+
49
+ Ensure the following environment variables are set,.
50
+
51
+ #+BEGIN_SRC sh
52
+ export ACCESS_TOKEN_SECRET="..."
53
+ export ACCESS_TOKEN="..."
54
+ export CONSUMER_API_KEY="..."
55
+ export CONSUMER_API_SECRET="..."
56
+ #+END_SRC
57
+
58
+ Alternatively, configure which commands to run to fetch the secrets.
59
+
60
+ #+BEGIN_SRC yaml
61
+ # _config.yml
62
+ recker:
63
+ twitter:
64
+ access_token_secret_cmd: cat secrets/access-token-secret
65
+ access_token_cmd: cat secrets/access-token
66
+ consumer_api_key_cmd: cat secrets/consumer-api-key
67
+ consumer_api_secret_cmd: cat secrets/consumer-api-secret-key
68
+ #+END_SRC
69
+
70
+ Run =bundle exec jekyll tweet= to let it rip!
71
+
72
+ [[assets/images/example-tweet.png]]
73
+
74
+ ** Generators
75
+
76
+ *** =stats=
77
+
78
+ On build time, =jekyll-recker= calculates and stores the following
79
+ stats in the =site.data.stats= object, which are by default rendered in a
80
+ widget on the home page layout.
81
+
82
+ [[assets/images/example-stats.png]]
83
+
84
+ If you'd like, you can override the template with your own stats
85
+ widget by providing your own =_includes/stats.html=.
86
+
87
+ | Field Name | Field Description |
88
+ |-----------------+------------------------------------------------------|
89
+ | =posts= | The total number of published posts. |
90
+ | =words.total= | The total number of words from all published post. |
91
+ | =words.average= | The average number of words for each published post. |
92
+ | =days.days= | Current streak of daily, consecutive posts. |
93
+ | =days.start= | First day of current streak. |
94
+ | =days.end= | Last day of current streak. |
95
+
96
+ Example:
97
+
98
+ #+BEGIN_SRC html
99
+ <!-- _includes/stats.html -->
100
+
101
+ <table>
102
+ <tr>
103
+ <th>Total Posts</th>
104
+ <th>Total Words</th>
105
+ <th>Average Words per Post</th>
106
+ <th>Current Streak</th>
107
+ <th>First day of current streak</th>
108
+ <th>Last day of current streak</th>
109
+ </tr>
110
+ <tr>
111
+ <td>{{ site.data.stats.posts }}</td>
112
+ <td>{{ site.data.stats.words.total }}</td>
113
+ <td>{{ site.data.stats.words.average }}</td>
114
+ <td>{{ site.data.stats.days.days }}</td>
115
+ <td>{{ site.data.stats.days.start }}</td>
116
+ <td>{{ site.data.stats.days.end }}</td>
117
+ </tr>
118
+ </table>
119
+ #+END_SRC
@@ -0,0 +1,23 @@
1
+ <div class="ui two column stackable grid">
2
+ {%- for post in site.posts -%}
3
+ {%- capture this_month -%}{{ post.date | date: '%B %Y' }}{%- endcapture -%}
4
+ {%- capture next_month -%}{{ post.previous.date | date: '%B %Y' }}{%- endcapture -%}
5
+ {%- if forloop.first -%}
6
+ <div class="center aligned column">
7
+ <h3 class="ui large header">{{ this_month }}</h3>
8
+ <div class="ui middle aligned large link list">
9
+ {%- endif -%}
10
+ <a class="item" href="{{ post.url }}">{{ post.title }}</a>
11
+ {%- if this_month != next_month -%}
12
+ </div>
13
+ </div>
14
+ <div class="center aligned column">
15
+ <h3 class="ui large header">{{ next_month }}</h3>
16
+ <div class="ui middle aligned large link list">
17
+ {%- endif -%}
18
+ {%- if forloop.last -%}
19
+ </div>
20
+ </div>
21
+ {%- endif -%}
22
+ {%- endfor -%}
23
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="ui container">
2
+ <h1 class="ui massive dividing header">
3
+ {{ include.title }}
4
+ <div class="sub header">
5
+ {{ include.subtitle }}
6
+ </div>
7
+ </h1>
8
+ <div class="ui huge breadcrumb">
9
+ <a href="{{ site.baseurl}}/" class="section">
10
+ <i class="home icon"></i>
11
+ </a>
12
+ <i class="right angle icon divider"></i>
13
+ <div class="active section">{{ include.slug }}</div>
14
+ </div>
15
+ </div>
@@ -0,0 +1,10 @@
1
+ {%- if page.next -%}
2
+ <a href="{{ page.next.url }}" class="ui basic left floated button">
3
+ <i class="angle left icon"></i> {{ page.next.slug }}
4
+ </a>
5
+ {%- endif -%}
6
+ {%- if page.previous -%}
7
+ <a href="{{ page.previous.url }}" class="ui basic right floated button">
8
+ {{ page.previous.slug }} <i class="angle right icon"></i>
9
+ </a>
10
+ {%- endif -%}
@@ -0,0 +1,36 @@
1
+ <div class="ui center aligned four small statistics">
2
+ <div class="ui small statistic">
3
+ <div class="value">
4
+ {{ site.data.stats.words.total }}
5
+ </div>
6
+ <div class="label">
7
+ Total Words
8
+ </div>
9
+ </div>
10
+ <div class="ui small statistic">
11
+ <div class="value">
12
+ {{ site.data.stats.words.average }}
13
+ </div>
14
+ <div class="label">
15
+ Ave. Words per post
16
+ </div>
17
+ </div>
18
+ <div class="ui small statistic">
19
+ <div class="value">
20
+ {{ site.data.stats.posts }}
21
+ </div>
22
+ <div class="label">
23
+ Posts
24
+ </div>
25
+ </div>
26
+ <div class="ui small statistic">
27
+ <div class="value">
28
+ {{ site.data.stats.days.days }}
29
+ </div>
30
+ <div class="label">
31
+ Consecutive Days
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" integrity="sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=" crossorigin="anonymous" />
7
+ <link href="/assets/jekyll-recker.css" rel="stylesheet"/>
8
+ <title>
9
+ {{ site.title }} | {% if page.title != nil %}{{ page.title }}{% else %}{{ site.description }}{% endif %}
10
+ </title>
11
+ </head>
12
+ <body>
13
+ <div class="ui container">
14
+ {{ content }}
15
+ </div>
16
+ <div class="ui vertical footer segment">
17
+ <div class="ui center aligned container">
18
+ <div class="ui horizontal large divided link list">
19
+ <div class="ui buttons">
20
+ <a class="ui circular rss basic button" href="/feed.xml"><i class="rss icon"></i></a>
21
+ <a class="ui circular email basic button" href="mailto:{{ site.email }}"><i class="envelope icon"></i></a>
22
+ <a class="ui circular twitter basic button" href="https://www.twitter.com/{{ site.twitter_username }}"><i class="twitter icon"></i></a>
23
+ <a class="ui circular github basic button" href="https://www.github.com/{{ site.github_username }}"><i class="github icon"></i></a>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <div class="ui center aligned container">
28
+ <div class="ui centered mini">
29
+ <p>
30
+ powered by <a title="Jekyll is a simple, blog-aware, static site generator." href="http://jekyllrb.com/">jekyll</a> &
31
+ <a title="Alex Recker's custom jekyll plugin" href="{{ site.baseurl }}{% link README.org %}">jekyll-recker</a> v{% recker_version %}
32
+ </p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </body>
37
+ </html>
@@ -0,0 +1,21 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+ <div class="ui container">
5
+ {%- include nav.html slug="index.html" title=site.title subtitle=site.description %}
6
+ <div class="ui text container center aligned">
7
+ {{ content }}
8
+ </div>
9
+ <br/>
10
+ <div class="ui container center aligned segment">
11
+ {% include stats.html %}
12
+ </div>
13
+ <br/>
14
+ <div class="ui container">
15
+ <img class="ui fluid image" src="{{ site.baseurl }}assets/images/words.png">
16
+ </div>
17
+ <br/>
18
+ <div class="ui container">
19
+ {% include archive.html %}
20
+ </div>
21
+ </div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+ <div class="ui container">
5
+ {%- include nav.html slug=page.slug title=page.title subtitle=page.description %}
6
+ <div class="ui container">
7
+ {{ content }}
8
+ </div>
9
+ </div>
10
+
@@ -0,0 +1,12 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+ <div class="ui container">
5
+ {% capture datestring %}{{ page.date | date: '%A, %B %d %Y' }}{% endcapture %}
6
+ {%- include nav.html slug=page.slug title=datestring subtitle=page.title %}
7
+ <div class="ui container">
8
+ {{ content }}
9
+ {%- include pager.html %}
10
+ </div>
11
+ </div>
12
+
@@ -0,0 +1,15 @@
1
+ .ui
2
+ .container
3
+ margin: 2em
4
+
5
+ .footer
6
+ margin-top: 50px !important
7
+
8
+ p
9
+ font-size: 20px
10
+ line-height: 160%
11
+ -moz-osx-font-smoothing: grayscale
12
+ -webkit-font-smoothing: antialiased !important
13
+ -moz-font-smoothing: antialiased !important
14
+ text-rendering: optimizelegibility !important
15
+ letter-spacing: .03em
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ ---
2
+ ---
3
+ @import "{{ site.theme }}"
data/lib/blog/cli.rb ADDED
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commander'
4
+ require 'fileutils'
5
+
6
+ module Blog
7
+ # CLI
8
+ class CLI
9
+ include Commander::Methods
10
+
11
+ def logger
12
+ Blog::Log.logger
13
+ end
14
+
15
+ def config_path
16
+ @config_path ||= File.expand_path '~/.blog.yml'
17
+ end
18
+
19
+ def config
20
+ Blog::Config.load_from_file(config_path)
21
+ end
22
+
23
+ def journal
24
+ @journal ||= Blog::Journal.from_file(config.journal_path)
25
+ end
26
+
27
+ def latest
28
+ @latest ||= journal.public_entries.first
29
+ end
30
+
31
+ def build
32
+ logger.info "deleting #{config.site_dir.pretty_path}"
33
+ FileUtils.rm_rf(config.site_dir)
34
+ logger.info "parsing #{config.journal_path.pretty_path}"
35
+ journal = Blog::Journal.from_file(config.journal_path)
36
+ logger.info "writing #{journal.public_entries.count.pretty} public entries"
37
+ journal.write_public_entries! config.posts_dir
38
+ logger.info "building jekyll"
39
+ Blog::Jekyll.build(config)
40
+ end
41
+
42
+ def commit
43
+ git = Blog::Git.new(config.blog_repo)
44
+ git.run!
45
+ end
46
+
47
+ def slack
48
+ logger.info "fetched latest entry: #{latest.excerpt}"
49
+ config.slacks.each do |info|
50
+ Blog::Slacky.post(latest, `#{info['webhook_cmd']}`, info)
51
+ end
52
+ end
53
+
54
+ def run
55
+ program :name, 'blog'
56
+ program :version, 'v0.0.0'
57
+ program :description, 'script to generate and publish my blog'
58
+
59
+ default_command :all
60
+
61
+ global_option '--config FILE', String, 'path to blog.yml' do |file|
62
+ @config_path = file
63
+ end
64
+
65
+ command :build do |c|
66
+ c.syntax = 'build'
67
+ c.description = 'build jekyll site'
68
+ c.action do |_args, _options|
69
+ build
70
+ end
71
+ end
72
+
73
+ command :commit do |c|
74
+ c.syntax = 'commit'
75
+ c.description = 'commit and push new post'
76
+ c.action do |_args, _options|
77
+ commit
78
+ end
79
+ end
80
+
81
+ command :slack do |c|
82
+ c.syntax = 'slack'
83
+ c.description = 'send slack notifications'
84
+ c.action do |_args, _options|
85
+ slack
86
+ end
87
+ end
88
+
89
+ command :all do |c|
90
+ c.syntax = 'all'
91
+ c.description = 'build, commit, and slack'
92
+ c.action do |_args, _options|
93
+ build
94
+ commit
95
+ slack
96
+ end
97
+ end
98
+
99
+ run!
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'yaml'
5
+
6
+ module Blog
7
+ # Config
8
+ class Config
9
+ attr_reader :data
10
+
11
+ def self.load_from_file(config_path = File.expand_path('~/.blog.yml'))
12
+ new(YAML.load_file(config_path) || {})
13
+ end
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ end
18
+
19
+ def journal_path
20
+ File.join blog_repo, 'journal.org'
21
+ end
22
+
23
+ def posts_dir
24
+ File.join blog_repo, '_posts'
25
+ end
26
+
27
+ def site_dir
28
+ File.join(File.expand_path(blog_repo), '_site')
29
+ end
30
+
31
+ def blog_repo
32
+ Bundler.root.to_s
33
+ end
34
+
35
+ def stats_path
36
+ File.join blog_repo, '_data/stats.json'
37
+ end
38
+
39
+ def log_level
40
+ @data.fetch('log_level', 'INFO').upcase
41
+ end
42
+
43
+ def twitter_creds
44
+ twitter = @data.fetch('twitter')
45
+ creds = {}
46
+ [
47
+ 'access_token_secret',
48
+ 'access_token',
49
+ 'consumer_api_key',
50
+ 'consumer_api_secret'
51
+ ].each do |key|
52
+ creds[key] = `#{twitter.fetch(key + '_cmd')}`.strip
53
+ end
54
+ creds
55
+ end
56
+
57
+ def slacks
58
+ @data.fetch('slacks', [])
59
+ end
60
+
61
+ private
62
+
63
+ def missing_fields
64
+ required_keys.reject { |k| @data.key? k }
65
+ end
66
+ end
67
+ end
data/lib/blog/entry.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'org-ruby'
5
+
6
+ module Blog
7
+ # Entry
8
+ class Entry
9
+ def initialize(headline)
10
+ @headline = headline
11
+ end
12
+
13
+ def title
14
+ date.strftime('%A, %B %-e %Y')
15
+ end
16
+
17
+ def subtitle
18
+ @subtitle ||= @headline.headline_text.split(' ').drop(2).join(' ')
19
+ end
20
+
21
+ alias excerpt subtitle
22
+
23
+ def tags
24
+ @tags ||= @headline.tags
25
+ end
26
+
27
+ def date
28
+ @date ||= Date.strptime(
29
+ @headline.headline_text.split(' ').take(2).join(' '),
30
+ '%Y-%m-%d %A'
31
+ )
32
+ end
33
+
34
+ def date_slug
35
+ date.strftime('%Y-%m-%d')
36
+ end
37
+
38
+ def public?
39
+ !tags.include? 'private'
40
+ end
41
+
42
+ def filename
43
+ "#{date_slug}-#{date_slug}.html.html"
44
+ end
45
+
46
+ def permalink
47
+ "https://www.alexrecker.com/#{date_slug}.html"
48
+ end
49
+
50
+ def body_text
51
+ @headline.body_lines.drop(1).collect(&:output_text).join(' ')
52
+ end
53
+
54
+ def body_html
55
+ Orgmode::Parser.new(body_text).to_html
56
+ end
57
+
58
+ def to_html
59
+ <<~HTML
60
+ ---
61
+ title: #{title}
62
+ excerpt: #{excerpt}
63
+ ---
64
+ #{body_html}
65
+ HTML
66
+ end
67
+ end
68
+ end
data/lib/blog/git.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git'
4
+
5
+ module Blog
6
+ # Git
7
+ class Git
8
+ attr_reader :client
9
+
10
+ def initialize(path)
11
+ @client = ::Git.open(path)
12
+ @logger = Blog::Log.logger
13
+ end
14
+
15
+ def run!
16
+ commit!
17
+ end
18
+
19
+ def commit!
20
+ commit = '[auto] Automatic Publish'
21
+ @logger.info "writing commit: #{commit}"
22
+ @client.add
23
+ @client.commit(commit)
24
+ @logger.info 'pushing commit'
25
+ @client.push
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jekyll'
4
+
5
+ module Blog
6
+ module Jekyll
7
+ def self.build(config)
8
+ conf = ::Jekyll.configuration(
9
+ {
10
+ 'source' => config.blog_repo,
11
+ 'destination' => config.site_dir
12
+ }
13
+ )
14
+ ::Jekyll::Site.new(conf).process
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'org-ruby'
4
+
5
+ module Blog
6
+ # Journal
7
+ class Journal
8
+ def self.from_file(path)
9
+ new(Orgmode::Parser.load(path))
10
+ end
11
+
12
+ def logger
13
+ Blog::Log.logger
14
+ end
15
+
16
+ def initialize(parser)
17
+ @parser = parser
18
+ end
19
+
20
+ def public_entries
21
+ @public_entries ||= all_entries.select(&:public?).sort_by(&:date).reverse
22
+ end
23
+
24
+ def private_entries
25
+ @private_entries ||= all_entries.reject(&:public?).sort_by(&:date).reverse
26
+ end
27
+
28
+ def write_public_entries!(dir)
29
+ public_entries.each do |entry|
30
+ target = File.join(dir, entry.filename)
31
+ logger.debug "writing #{entry.title} to #{target.pretty_path}"
32
+ File.open(target, 'w+') { |f| f.write(entry.to_html) }
33
+ end
34
+ end
35
+
36
+ def all_entries
37
+ entry_headlines.map { |h| Entry.new(h) }
38
+ end
39
+
40
+ private
41
+
42
+ def entry_headlines
43
+ @parser.headlines.select { |h| h.level == 3 }
44
+ end
45
+ end
46
+ end
data/lib/blog/log.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Blog
6
+ # Log
7
+ module Log
8
+ def self.logger
9
+ @logger ||= default_logger
10
+ end
11
+
12
+ def self.default_logger
13
+ logger = Logger.new(STDOUT)
14
+ logger.level = Logger::INFO
15
+ logger.formatter = proc do |severity, _datetime, _progname, msg|
16
+ "blog: #{msg}\n"
17
+ end
18
+ logger
19
+ end
20
+
21
+ def self.level=(setting)
22
+ @logger.level = case setting.upcase
23
+ when 'DEBUG'
24
+ Logger::DEBUG
25
+ else
26
+ Logger::INFO
27
+ end
28
+ end
29
+ end
30
+ end