jekyll-subscriber_only 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eaa1a515b0026b7274bbaf7b2ff047cc8aa24f0910851758badab865248eb1cc
4
+ data.tar.gz: 69bd7488dc9c1eac6cc3e286318f944e9d71067e07aa3b2c22ca353533142ade
5
+ SHA512:
6
+ metadata.gz: 07e0d6cf897d0f483c9b2f31f492f365411c16bcbb32de0c7ea7a7440944bc7d5ddee88eced2985ef08f11467a5ca373fdd16b4305c933daf461a14bfe6b6535
7
+ data.tar.gz: 88e4b8c0e72d847c1b71e9781d13bb8cde480f1a70cd1f76f601f5ee4d55028929b13b9bf670596d4db08ea380701dad1942afd589cd32dda971221fccb9277a
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Vitor Manuel de Sousa Pereira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Jekyll Subscriber Only
2
+
3
+ Monetize your Jekyll site with paid subscriber-only content.
4
+
5
+ Subscriber Only allows you to monetize your Jekyll site with paid subscriptions. Make selected content available only to paid subscribers by adding a single line to the post's front matter. Leave subscriptions, payment processing and access control to us and focus solely on making great content. Go to https://subscriber-only.com for more details.
6
+
7
+ ## Installation
8
+
9
+ First, you need to [sign up on Subscriber Only](https://app.subscriber-only.com/site/new) -- it's easy, it takes 10 minutes!
10
+
11
+ Then, add the `jekyll-subscriber_only` gem to your application's Gemfile, in the `jekyll_plugins` group. Make sure it comes after `jekyll-feed`, if you're using it:
12
+
13
+ ```ruby
14
+ group :jekyll_plugins do
15
+ # gem "jekyll-feed", "~> 0.12"
16
+ gem "jekyll-subscriber_only"
17
+ end
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle install
23
+
24
+ Finally, copy your tokens to your site's `_config.yml`:
25
+
26
+ ```yaml
27
+ subscriber_only:
28
+ public_token: MY_PUBLIC_TOKEN
29
+ secret_token: MY_SECRET_TOKEN
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ If you want to make a particular post subscriber-only, add `subscriber_only: true` to the post's front matter:
35
+
36
+ ```yml
37
+ layout: post
38
+ title: My premium post
39
+ subscriber_only: true
40
+ ```
41
+
42
+ **That's it!**
43
+
44
+ Note that your posts will only be paywalled when building with `JEKYLL_ENV=production`. I.e.
45
+
46
+ $ JEKYLL_ENV=production bundle exec jekyll build
47
+
48
+ ## Contributing
49
+
50
+ Bug reports and pull requests are welcome on GitHub at https://github.com/subscriber-only/jekyll-subscriber_only.
51
+
@@ -0,0 +1,123 @@
1
+ {% comment %}
2
+ # Copyright (c) 2023 Vitor Manuel de Sousa Pereira
3
+ # Copyright (c) 2015-present Ben Balter and jekyll-feed contributors
4
+ # SPDX-License-Identifier: MIT
5
+ {% endcomment %}
6
+
7
+ <?xml version="1.0" encoding="utf-8"?>
8
+ {% if page.xsl %}
9
+ <?xml-stylesheet type="text/xml" href="{{ '/feed.xslt.xml' | absolute_url }}"?>
10
+ {% endif %}
11
+ <feed xmlns="http://www.w3.org/2005/Atom" {% if site.lang %}xml:lang="{{ site.lang }}"{% endif %}>
12
+ <generator uri="https://jekyllrb.com/" version="{{ jekyll.version }}">Jekyll</generator>
13
+ <link href="{{ page.url | absolute_url }}" rel="self" type="application/atom+xml" />
14
+ <link href="{{ '/' | absolute_url }}" rel="alternate" type="text/html" {% if site.lang %}hreflang="{{ site.lang }}" {% endif %}/>
15
+ <updated>{{ site.time | date_to_xmlschema }}</updated>
16
+ <id>{{ page.url | absolute_url | xml_escape }}</id>
17
+
18
+ {% assign title = site.title | default: site.name %}
19
+ {% if page.collection != "posts" %}
20
+ {% assign collection = page.collection | capitalize %}
21
+ {% assign title = title | append: " | " | append: collection %}
22
+ {% endif %}
23
+ {% if page.category %}
24
+ {% assign category = page.category | capitalize %}
25
+ {% assign title = title | append: " | " | append: category %}
26
+ {% endif %}
27
+
28
+ {% if title %}
29
+ <title type="html">{{ title | smartify | xml_escape }}</title>
30
+ {% endif %}
31
+
32
+ {% if site.description %}
33
+ <subtitle>{{ site.description | xml_escape }}</subtitle>
34
+ {% endif %}
35
+
36
+ {% if site.author %}
37
+ <author>
38
+ <name>{{ site.author.name | default: site.author | xml_escape }}</name>
39
+ {% if site.author.email %}
40
+ <email>{{ site.author.email | xml_escape }}</email>
41
+ {% endif %}
42
+ {% if site.author.uri %}
43
+ <uri>{{ site.author.uri | xml_escape }}</uri>
44
+ {% endif %}
45
+ </author>
46
+ {% endif %}
47
+
48
+ {% if page.tags %}
49
+ {% assign posts = site.tags[page.tags] %}
50
+ {% else %}
51
+ {% assign posts = site[page.collection] %}
52
+ {% endif %}
53
+ {% if page.category %}
54
+ {% assign posts = posts | where: "categories", page.category %}
55
+ {% endif %}
56
+ {% unless site.show_drafts %}
57
+ {% assign posts = posts | where_exp: "post", "post.draft != true" %}
58
+ {% endunless %}
59
+ {% assign posts = posts | sort: "date" | reverse %}
60
+ {% assign posts_limit = site.feed.posts_limit | default: 10 %}
61
+ {% for post in posts limit: posts_limit %}
62
+ <entry{% if post.lang %}{{" "}}xml:lang="{{ post.lang }}"{% endif %}>
63
+ {% assign post_title = post.title | smartify | strip_html | normalize_whitespace | xml_escape %}
64
+
65
+ <title type="html">{{ post_title }}</title>
66
+ <link href="{{ post.url | absolute_url }}" rel="alternate" type="text/html" title="{{ post_title }}" />
67
+ <published>{{ post.date | date_to_xmlschema }}</published>
68
+ <updated>{{ post.last_modified_at | default: post.date | date_to_xmlschema }}</updated>
69
+ <id>{{ post.id | absolute_url | xml_escape }}</id>
70
+ {% assign excerpt_only = post.feed.excerpt_only | default: site.feed.excerpt_only %}
71
+ {% unless excerpt_only %}
72
+ {% if post["subscriber_only"] == true %}
73
+ {% assign body = "This post is for paying subscribers only." %}
74
+ {% else %}
75
+ {% assign body = post.content %}
76
+ {% endif %}
77
+ <content type="html" xml:base="{{ post.url | absolute_url | xml_escape }}"><![CDATA[{{ body | strip }}]]></content>
78
+ {% endunless %}
79
+
80
+ {% assign post_author = post.author | default: post.authors[0] | default: site.author %}
81
+ {% assign post_author = site.data.authors[post_author] | default: post_author %}
82
+ {% assign post_author_email = post_author.email | default: nil %}
83
+ {% assign post_author_uri = post_author.uri | default: nil %}
84
+ {% assign post_author_name = post_author.name | default: post_author %}
85
+
86
+ <author>
87
+ <name>{{ post_author_name | default: "" | xml_escape }}</name>
88
+ {% if post_author_email %}
89
+ <email>{{ post_author_email | xml_escape }}</email>
90
+ {% endif %}
91
+ {% if post_author_uri %}
92
+ <uri>{{ post_author_uri | xml_escape }}</uri>
93
+ {% endif %}
94
+ </author>
95
+
96
+ {% if post.category %}
97
+ <category term="{{ post.category | xml_escape }}" />
98
+ {% elsif post.categories %}
99
+ {% for category in post.categories %}
100
+ <category term="{{ category | xml_escape }}" />
101
+ {% endfor %}
102
+ {% endif %}
103
+
104
+ {% for tag in post.tags %}
105
+ <category term="{{ tag | xml_escape }}" />
106
+ {% endfor %}
107
+
108
+ {% assign post_summary = post.description | default: post.excerpt %}
109
+ {% if post_summary and post_summary != empty %}
110
+ <summary type="html"><![CDATA[{{ post_summary | strip_html | normalize_whitespace }}]]></summary>
111
+ {% endif %}
112
+
113
+ {% assign post_image = post.image.path | default: post.image %}
114
+ {% if post_image %}
115
+ {% unless post_image contains "://" %}
116
+ {% assign post_image = post_image | absolute_url %}
117
+ {% endunless %}
118
+ <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="{{ post_image | xml_escape }}" />
119
+ <media:content medium="image" url="{{ post_image | xml_escape }}" xmlns:media="http://search.yahoo.com/mrss/" />
120
+ {% endif %}
121
+ </entry>
122
+ {% endfor %}
123
+ </feed>
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2023 Vitor Manuel de Sousa Pereira
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ # frozen_string_literal: true
5
+
6
+ module Jekyll
7
+ module SubscriberOnly
8
+ module JekyllFeedPatch
9
+ private
10
+
11
+ def feed_source_path
12
+ @feed_source_path ||= File.expand_path("feed.xml", __dir__)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ if Object.const_defined?("JekyllFeed::Generator")
19
+ JekyllFeed::Generator.prepend(Jekyll::SubscriberOnly::JekyllFeedPatch)
20
+ end
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2023 Vitor Manuel de Sousa Pereira
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ # frozen_string_literal: true
5
+
6
+ module Jekyll
7
+ module SubscriberOnly
8
+ VERSION = "1.0.0"
9
+ end
10
+ end
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2023 Vitor Manuel de Sousa Pereira
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ # frozen_string_literal: true
5
+
6
+ require "net/http"
7
+ require "uri"
8
+
9
+ require "jekyll"
10
+
11
+ require "jekyll/subscriber_only/jekyll_feed_patch"
12
+ require "jekyll/subscriber_only/version"
13
+
14
+ module Jekyll
15
+ module SubscriberOnly
16
+ extend self
17
+
18
+ Conf = Struct.new(
19
+ :env, :public_token, :secret_token, :base_url, :script_url, :upload_url,
20
+ keyword_init: true
21
+ )
22
+
23
+ def configure(site)
24
+ jekyll_conf = site.config["subscriber_only"]
25
+ base_url = "https://app.subscriber-only.com"
26
+
27
+ if jekyll_conf.nil?
28
+ die("The site must be configured before you're able to make your " \
29
+ "posts subscriber-only. Go to #{new_site_url} to configure the " \
30
+ "site.")
31
+ end
32
+
33
+ env = jekyll_conf["env"]
34
+ base_url = "http://localhost:3000" if env == "development"
35
+
36
+ @conf = Conf.new(
37
+ env: env,
38
+ public_token: jekyll_conf["public_token"],
39
+ secret_token: jekyll_conf["secret_token"],
40
+ base_url: base_url,
41
+ script_url: "#{base_url}/so.js",
42
+ upload_url: URI("#{base_url}/api/v1/posts"),
43
+ )
44
+
45
+ return unless @conf.public_token.nil? || @conf.secret_token.nil?
46
+
47
+ die("Public and secret tokens must be configured to enable subscriber-" \
48
+ "only posts. Go to #{edit_site_url} to get the tokens.")
49
+ end
50
+
51
+ def paywall(doc)
52
+ return unless subscriber_only?(doc)
53
+
54
+ info("Paywalling post \"#{doc['title']}\"")
55
+ upload_post(doc)
56
+ replace_content(doc)
57
+ end
58
+
59
+ private
60
+
61
+ def upload_post(doc)
62
+ res = Net::HTTP.post(
63
+ @conf.upload_url,
64
+ { path: doc.url, title: doc["title"], content: doc.content }.to_json,
65
+ {
66
+ Authorization: "Bearer #{@conf.secret_token}",
67
+ Accept: "application/json",
68
+ "Content-Type": "application/json",
69
+ },
70
+ )
71
+
72
+ handle_api_error(res)
73
+ rescue Errno::ECONNREFUSED
74
+ die("Failed to connect to #{@conf.base_url}. Please contact us if this " \
75
+ "issue persists.")
76
+ rescue JSON::ParserError
77
+ File.write("parser_error.html", res.body) if @conf.env == "development"
78
+ die("Failed to get a correct response.")
79
+ end
80
+
81
+ def handle_api_error(res)
82
+ return if res.is_a?(Net::HTTPSuccess)
83
+
84
+ error_details = JSON.parse(res.body)
85
+ die("Failed to upload \"#{doc['title']}\".\n#{error_details['message']}")
86
+ end
87
+
88
+ def replace_content(doc)
89
+ doc.output.sub!("</head>", "#{head_html}</head>")
90
+ doc.output.sub!(doc.content, content_html)
91
+ end
92
+
93
+ def head_html
94
+ "<script type=\"module\" src=\"#{@conf.script_url}\"></script>"
95
+ end
96
+
97
+ def content_html
98
+ "<div data-subscriber-only data-public-token=\"#{@conf.public_token}\">" \
99
+ "</div>"
100
+ end
101
+
102
+ def new_site_url
103
+ "#{@conf.base_url}/site/edit"
104
+ end
105
+
106
+ def edit_site_url
107
+ "#{@conf.base_url}/site/edit"
108
+ end
109
+
110
+ def subscriber_only?(doc)
111
+ Jekyll.env == "production" &&
112
+ !doc.draft? &&
113
+ doc.published? &&
114
+ doc.write? &&
115
+ doc["subscriber_only"] == true
116
+ end
117
+
118
+ def info(message)
119
+ Jekyll.logger.info("Subscriber-Only:", message)
120
+ end
121
+
122
+ def die(message)
123
+ Jekyll.logger.error("Subscriber-Only:", message)
124
+ # Jekyll's command handler catches all Exceptions so we must be more brusque
125
+ # when aborting or the user will be drowning in stack traces.
126
+ exit!
127
+ end
128
+ end
129
+ end
130
+
131
+ Jekyll::Hooks.register(:site, :after_init) do |site|
132
+ Jekyll::SubscriberOnly.configure(site)
133
+ end
134
+
135
+ Jekyll::Hooks.register(:posts, :post_render) do |doc|
136
+ Jekyll::SubscriberOnly.paywall(doc)
137
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-subscriber_only
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Vitor Manuel de Sousa Pereira
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jekyll
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.7'
27
+ description: |2
28
+ Subscriber Only allows you to monetize your Jekyll site with paid
29
+ subscriptions. Make selected content available only to paid subscribers by
30
+ adding a single line to the post's front matter. Leave subscriptions,
31
+ payment processing and access control to us and focus solely on making great
32
+ content. Go to https://subscriber-only.com for more details.
33
+ email: vmsousapereira@gmail.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - LICENSE.txt
39
+ - README.md
40
+ - lib/jekyll/subscriber_only.rb
41
+ - lib/jekyll/subscriber_only/feed.xml
42
+ - lib/jekyll/subscriber_only/jekyll_feed_patch.rb
43
+ - lib/jekyll/subscriber_only/version.rb
44
+ homepage: https://subscriber-only.com
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://subscriber-only.com
49
+ rubygems_mfa_required: 'true'
50
+ source_code_uri: https://github.com/subscriber-only/jekyll-subscriber_only
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.7.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.1.6
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Monetize your Jekyll site with paid subscriber-only content.
70
+ test_files: []