jekyll-subscriber_only 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []