793 lines
27 KiB
Plaintext
793 lines
27 KiB
Plaintext
|
|
(BUTTON)
|
|
|
|
Lucian Ghinda
|
|
|
|
All about coding
|
|
|
|
(BUTTON) (BUTTON)
|
|
Follow
|
|
|
|
[1]All about coding
|
|
|
|
Follow
|
|
Ruby open source: feedbin Ruby open source: feedbin
|
|
|
|
Ruby open source: feedbin
|
|
|
|
[2]Lucian Ghinda's photo Lucian Ghinda's photo
|
|
[3]Lucian Ghinda
|
|
·[4]Nov 17, 2023·
|
|
|
|
11 min read
|
|
|
|
Table of contents
|
|
|
|
* [5]The product
|
|
* [6]Open source
|
|
+ [7]License
|
|
* [8]Technical review
|
|
+ [9]Ruby and Rails version
|
|
+ [10]Architecture
|
|
+ [11]Stats
|
|
+ [12]Style Guide
|
|
+ [13]Storage, Persistence and in-memory storage
|
|
+ [14]Gems used
|
|
+ [15]Code & Design Patterns
|
|
o [16]Code Organisation
|
|
o [17]Routes
|
|
o [18]Controllers
|
|
o [19]Models
|
|
o [20]Jobs
|
|
o [21]Presenters
|
|
o [22]ApplicationComponents
|
|
o [23]ComponentsPreview
|
|
* [24]Testing
|
|
+ [25]Custom assertions
|
|
* [26]Conclusion
|
|
|
|
The product
|
|
|
|
[27]https://feedbin.com
|
|
|
|
"Feedbin is the best way to enjoy content on the Web. By combining
|
|
RSS, and newsletters, you can get all the good parts of the Web in
|
|
one convenient location"
|
|
|
|
Open source
|
|
|
|
The open-source repository can be found at
|
|
[28]https://github.com/feedbin/feedbin
|
|
|
|
License
|
|
|
|
The [29]license they use is MIT:
|
|
|
|
Technical review
|
|
|
|
Ruby and Rails version
|
|
|
|
They are currently using:
|
|
* Ruby version 3.2.2
|
|
* They used a fork of Rails at [30]https://github.com/feedbin/rails
|
|
forked from [31]https://github.com/Shopify/rails. They are using a
|
|
branch called [32]7-1-stable-invalid-cache-entries - It seems to be
|
|
Rails 7.1 and about 1 month behind the Shopify/rails which is
|
|
usually pretty up to date with main Rails
|
|
|
|
Architecture
|
|
|
|
Code Architecture:
|
|
* They are using the standard Rails organisation of MVC.
|
|
|
|
Database:
|
|
* The DB is PostgreSQL
|
|
|
|
Jobs queue:
|
|
* Sidekiq
|
|
|
|
On the front-end side:
|
|
* They use .html.erb
|
|
* They are using Phlex for [33]components
|
|
* They are using [34]Jquery for the JS library
|
|
* They have some custom JS code written in [35]CoffeeScript
|
|
* They are using Hotwire via [36]importmaps
|
|
* They are using [37]Tailwind
|
|
|
|
Stats
|
|
|
|
Running /bin/rails stats will output the following:
|
|
|
|
Running VSCodeCounter will give the following stats:
|
|
|
|
Style Guide
|
|
|
|
For Ruby:
|
|
* They are using [38]standardrb as the Style Guide with no
|
|
customisations.
|
|
|
|
Storage, Persistence and in-memory storage
|
|
|
|
The DB is PostgreSQL.
|
|
|
|
They are not using the schema.rb but the [39]structure.sql format for
|
|
DB schema dump is configured via application.rb:
|
|
module Feedbin
|
|
class Application < Rails::Application
|
|
# other configs
|
|
config.active_record.schema_format = :sql
|
|
# other configs
|
|
end
|
|
end
|
|
|
|
Enabled PSQL extensions:
|
|
* hstore - "data type for storing sets of (key, value) pairs"
|
|
* pg_stat_statements - "track planning and execution statistics of
|
|
all SQL statements executed"
|
|
* uuid-ossp - "generate universally unique identifiers (UUIDs)"
|
|
|
|
CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public;
|
|
COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs
|
|
';
|
|
|
|
CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public;
|
|
COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statist
|
|
ics of all SQL statements executed';
|
|
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
|
|
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UU
|
|
IDs)';
|
|
|
|
Redis is configured to be used with Sidekiq.
|
|
|
|
This is what the [40]redis initializer looks like:
|
|
# https://github.com/feedbin/feedbin/blob/main/config/initializers/redis.rb#L1
|
|
|
|
defaults = {connect_timeout: 5, timeout: 5}
|
|
defaults[:url] = ENV["REDIS_URL"] if ENV["REDIS_URL"]
|
|
|
|
$redis = {}.tap do |hash|
|
|
options2 = defaults.dup
|
|
if ENV["REDIS_URL_PUBLIC_IDS"] || ENV["REDIS_URL_CACHE"]
|
|
options2[:url] = ENV["REDIS_URL_PUBLIC_IDS"] || ENV["REDIS_URL_CACHE"]
|
|
end
|
|
hash[:refresher] = ConnectionPool.new(size: 10) { Redis.new(options2) }
|
|
end
|
|
|
|
Further, there is a [41]RedisLock configured like this:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
|
|
|
|
class RedisLock
|
|
def self.acquire(lock_name, expiration_in_seconds = 55)
|
|
Sidekiq.redis { _1.set(lock_name, "locked", ex: expiration_in_seconds, nx: t
|
|
rue) }
|
|
end
|
|
end
|
|
|
|
Further down this is used in a [42]clock.rb (that defines scheduled
|
|
tasks to run):
|
|
# https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
|
|
|
|
every(10.seconds, "clockwork.very_frequent") do
|
|
if RedisLock.acquire("clockwork:send_stats:v3", 8)
|
|
SendStats.perform_async
|
|
end
|
|
|
|
if RedisLock.acquire("clockwork:cache_entry_views", 8)
|
|
CacheEntryViews.perform_async(nil, true)
|
|
end
|
|
|
|
if RedisLock.acquire("clockwork:downloader_migration", 8)
|
|
FeedCrawler::PersistCrawlData.perform_async
|
|
end
|
|
end
|
|
|
|
every(1.minutes, "clockwork.frequent") do
|
|
if RedisLock.acquire("clockwork:feed:refresher:scheduler:v2")
|
|
FeedCrawler::ScheduleAll.perform_async
|
|
end
|
|
|
|
if RedisLock.acquire("clockwork:harvest:embed:data")
|
|
HarvestEmbeds.perform_async(nil, true)
|
|
end
|
|
end
|
|
|
|
every(1.day, "clockwork.daily", at: "7:00", tz: "UTC") do
|
|
if RedisLock.acquire("clockwork:delete_entries:v2")
|
|
EntryDeleterScheduler.perform_async
|
|
end
|
|
|
|
if RedisLock.acquire("clockwork:trial_expiration:v2")
|
|
TrialExpiration.perform_async
|
|
end
|
|
|
|
if RedisLock.acquire("clockwork:web_sub_maintenance")
|
|
WebSub::Maintenance.perform_async
|
|
end
|
|
end
|
|
|
|
Gems used
|
|
|
|
Here are some of the gems used:
|
|
* [43]sax-machine - "A declarative sax parsing library backed by
|
|
Nokogiri"
|
|
* [44]feedjira - "Feedjira is a Ruby library designed to parse feeds"
|
|
* [45]html-pipeline - "HTML processing filters and utilities. This
|
|
module is a small framework for defining CSS-based content filters
|
|
and applying them to user provided content"
|
|
* [46]apnotic - "A Ruby APNs HTTP/2 gem able to provide instant
|
|
feedback"
|
|
* [47]autoprefixer-rails - "Autoprefixer is a tool to parse CSS and
|
|
add vendor prefixes to CSS rules using values from the Can I Use
|
|
database. This gem provides Ruby and Ruby on Rails integration with
|
|
this JavaScript tool"
|
|
* [48]clockwork - "Clockwork is a cron replacement. It runs as a
|
|
lightweight, long-running Ruby process which sits alongside your
|
|
web processes (Mongrel/Thin) and your worker processes
|
|
(DJ/Resque/Minion/Stalker) to schedule recurring work at particular
|
|
times or dates"
|
|
* [49]down - "Streaming downloads using net/http, http.rb, HTTPX or
|
|
wget"
|
|
* [50]phlex-rails - "Phlex is a framework that lets you compose web
|
|
views in pure Ruby"
|
|
* [51]premailer-rails - "This gem is a drop in solution for styling
|
|
HTML emails with CSS without having to do the hard work yourself"
|
|
* [52]raindrops - "raindrops is a real-time stats toolkit to show
|
|
statistics for Rack HTTP servers. It is designed for preforking
|
|
servers such as unicorn, but should support any Rack HTTP server on
|
|
platforms supporting POSIX shared memory"
|
|
* [53]strong_migrations - "Catch unsafe migrations in development"
|
|
* [54]web-push - "This gem makes it possible to send push messages to
|
|
web browsers from Ruby backends using the Web Push Protocol"
|
|
* [55]stripe-ruby-mock - "A drop-in library to test stripe without
|
|
hitting their servers"
|
|
* [56]rails-controller-testing - "Brings back assigns and
|
|
assert_template to your Rails tests"
|
|
|
|
There are many other gems used, I only selected few here. Browse the
|
|
[57]Gemfile to discover more.
|
|
|
|
What could be mentioned is that they use their fork for some of the
|
|
gems included in the file:
|
|
# https://github.com/feedbin/feedbin/blob/main/Gemfile
|
|
|
|
# other gems
|
|
|
|
gem "rails", github: "feedbin/rails", branch: "7-1-stable-invalid-cache-entries"
|
|
|
|
# some other gems
|
|
|
|
gem "http", github: "feedbin/http", branch: "feedb
|
|
in"
|
|
gem "carrierwave", github: "feedbin/carrierwave", branch: "feedb
|
|
in"
|
|
gem "sax-machine", github: "feedbin/sax-machine", branch: "feedb
|
|
in"
|
|
gem "feedjira", github: "feedbin/feedjira", branch: "f2"
|
|
gem "feedkit", github: "feedbin/feedkit", branch: "maste
|
|
r"
|
|
gem "html-pipeline", github: "feedbin/html-pipeline", branch: "feedb
|
|
in"
|
|
gem "html_diff", github: "feedbin/html_diff", ref: "013e1bb"
|
|
gem "twitter", github: "feedbin/twitter", branch: "feedb
|
|
in"
|
|
|
|
# other gems
|
|
|
|
group :development, :test do
|
|
gem "stripe-ruby-mock", github: "feedbin/stripe-ruby-mock", branch: "feedbin",
|
|
require: "stripe_mock"
|
|
# other gems
|
|
end
|
|
|
|
# other gem groups
|
|
|
|
Code & Design Patterns
|
|
|
|
Code Organisation
|
|
|
|
Under /app there are 3 folders different from the ones that Rails comes
|
|
with:
|
|
* presenters
|
|
* uploaders
|
|
* validators
|
|
|
|
The lib folder includes very few extra objects. Most of them seems to
|
|
be related to communicating with external services.
|
|
|
|
Maybe worth mentioning from lib folder is the
|
|
[58]ConditionalSassCompressor
|
|
# https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.r
|
|
b#L1
|
|
|
|
class ConditionalSassCompressor
|
|
def compress(string)
|
|
return string if string =~ /tailwindcss/
|
|
options = { syntax: :scss, cache: false, read_cache: false, style: :compress
|
|
ed}
|
|
begin
|
|
Sprockets::Autoload::SassC::Engine.new(string, options).render
|
|
rescue => e
|
|
puts "Could not compress '#{string[0..65]}'...: #{e.message}, skipping com
|
|
pression"
|
|
string
|
|
end
|
|
end
|
|
end
|
|
|
|
This is used to configure:
|
|
# https://github.com/feedbin/feedbin/blob/main/config/application.rb#L47
|
|
config.assets.css_compressor = ConditionalSassCompressor.new
|
|
|
|
Routes
|
|
|
|
There is a combination of RESTful routes and non-restful routes.
|
|
|
|
Here is an example from entries in the [59]routes.rb :
|
|
# https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
|
|
|
|
resources :entries, only: [:show, :index, :destroy] do
|
|
member do
|
|
post :content
|
|
post :unread_entries, to: "unread_entries#update"
|
|
post :starred_entries, to: "starred_entries#update"
|
|
post :mark_as_read, to: "entries#mark_as_read"
|
|
post :recently_read, to: "recently_read_entries#create"
|
|
post :recently_played, to: "recently_played_entries#create"
|
|
get :push_view
|
|
get :newsletter
|
|
end
|
|
collection do
|
|
get :starred
|
|
get :unread
|
|
get :preload
|
|
get :search
|
|
get :recently_read, to: "recently_read_entries#index"
|
|
get :recently_played, to: "recently_played_entries#index"
|
|
get :updated, to: "updated_entries#index"
|
|
post :mark_all_as_read
|
|
post :mark_direction_as_read
|
|
end
|
|
end
|
|
|
|
Controllers
|
|
|
|
The controllers are mostly what I would call vanilla Rails controllers.
|
|
|
|
Three notes about them:
|
|
* Some of them are responding with JS usually using USJ or JQuery to
|
|
change elements from the page.
|
|
* They contain non-Rails standard actions (actions that are not show,
|
|
index, new, create ...)
|
|
* There is a namespaced api folder that contains APIs used by mobile
|
|
apps
|
|
|
|
Here is one simple example for DELETE /entries/:id , the controller
|
|
looks like this:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/controllers/entries_controlle
|
|
r.rb#L238
|
|
def destroy
|
|
@user = current_user
|
|
@entry = @user.entries.find(params[:id])
|
|
if @entry.feed.pages?
|
|
EntryDeleter.new.delete_entries(@entry.feed_id, @entry.id)
|
|
end
|
|
end
|
|
|
|
And here is the view [60]destroy.js.erb :
|
|
$('[data-behavior~=entries_target] [data-entry-id=<%= @entry.id %>]').remove();
|
|
|
|
feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'unrea
|
|
d')
|
|
feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'starr
|
|
ed')
|
|
feedbin.applyCounts(true)
|
|
|
|
feedbin.clearEntry();
|
|
feedbin.fullScreen(false)
|
|
|
|
The main pattern adopted to controllers is to have some logic in them
|
|
and delegate to jobs some part of the processing.
|
|
|
|
The repo contains mostly straight-forward controllers like this one:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/controllers/pages_internal_co
|
|
ntroller.rb#L1
|
|
class PagesInternalController < ApplicationController
|
|
|
|
def create
|
|
@entry = SavePage.new.perform(current_user.id, params[:url], nil)
|
|
get_feeds_list
|
|
end
|
|
end
|
|
|
|
But also few controllers that include some logic:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/controllers/api/podcasts/v1/f
|
|
eeds_controller.rb#L8
|
|
|
|
def show
|
|
url = hex_decode(params[:id])
|
|
@feed = Feed.find_by_feed_url(url)
|
|
if @feed.present?
|
|
if @feed.standalone_request_at.blank?
|
|
FeedStatus.new.perform(@feed.id)
|
|
FeedUpdate.new.perform(@feed.id)
|
|
end
|
|
else
|
|
feeds = FeedFinder.feeds(url)
|
|
@feed = feeds.first
|
|
end
|
|
|
|
if @feed.present?
|
|
@feed.touch(:standalone_request_at)
|
|
else
|
|
status_not_found
|
|
end
|
|
rescue => exception
|
|
if Rails.env.production?
|
|
ErrorService.notify(exception)
|
|
status_not_found
|
|
else
|
|
raise exception
|
|
end
|
|
end
|
|
|
|
Even with this structure, I find all controllers easy to read and I
|
|
think they can be easier to change.
|
|
|
|
Models
|
|
|
|
The app/models folders contain both ActiveRecord and normal Ruby
|
|
objects. With few exceptions, they are not namespaced.
|
|
|
|
Jobs
|
|
|
|
The jobs folder contains Sidekiq jobs which are used to do processing
|
|
on various objects. They are usually called from controllers and most
|
|
of them are async.
|
|
|
|
Here is one job that is caching views:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/jobs/cache_entry_views.rb#L1
|
|
|
|
class CacheEntryViews
|
|
include Sidekiq::Worker
|
|
include SidekiqHelper
|
|
|
|
SET_NAME = "#{name}-ids"
|
|
|
|
def perform(entry_id, process = false)
|
|
if process
|
|
cache_views
|
|
else
|
|
add_to_queue(SET_NAME, entry_id)
|
|
end
|
|
end
|
|
|
|
def cache_views
|
|
entry_ids = dequeue_ids(SET_NAME)
|
|
entries = Entry.where(id: entry_ids).includes(feed: [:favicon])
|
|
ApplicationController.render({
|
|
partial: "entries/entry",
|
|
collection: entries,
|
|
format: :html,
|
|
cached: true
|
|
})
|
|
ApplicationController.render({
|
|
layout: nil,
|
|
template: "api/v2/entries/index",
|
|
assigns: {entries: entries},
|
|
format: :html,
|
|
locals: {
|
|
params: {mode: "extended"}
|
|
}
|
|
})
|
|
end
|
|
end
|
|
|
|
Presenters
|
|
|
|
There is a [61]BasePresenter and all other presenters are extending it
|
|
via inheritance:
|
|
|
|
This controller defines a private method called presents:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#
|
|
L1
|
|
|
|
class BasePresenter
|
|
def initialize(object, locals, template)
|
|
@object = object
|
|
@locals = locals
|
|
@template = template
|
|
end
|
|
|
|
# ...
|
|
|
|
private
|
|
|
|
def self.presents(name)
|
|
define_method(name) do
|
|
@object
|
|
end
|
|
end
|
|
end
|
|
|
|
and it is used like this for example:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/presenters/user_presenter.rb#
|
|
L2
|
|
|
|
class UserPresenter < BasePresenter
|
|
presents :user
|
|
delegate_missing_to :user
|
|
|
|
# ... more code
|
|
|
|
def theme
|
|
result = settings["theme"].present? ? settings["theme"] : nil
|
|
result || user.theme || "auto"
|
|
end
|
|
# ... other code
|
|
end
|
|
|
|
To use the presenters, there is a helper defined in ApplicationHelper
|
|
will instantiate the proper helper based on the object class:
|
|
module ApplicationHelper
|
|
def present(object, locals = nil, klass = nil)
|
|
klass ||= "#{object.class}Presenter".constantize
|
|
presenter = klass.new(object, locals, self)
|
|
yield presenter if block_given?
|
|
presenter
|
|
end
|
|
|
|
# more code ...
|
|
end
|
|
|
|
and it is used [62]like this in views:
|
|
<% present @user do |user_presenter| %>
|
|
<% @class = "settings-body settings-#{params[:action]} theme-#{user_presente
|
|
r.theme}"%>
|
|
<% end %>
|
|
|
|
ApplicationComponents
|
|
|
|
Components are based on Phlex and they inherit from
|
|
[63]ApplicationComponent
|
|
|
|
It defines a method to add Stimulus controller in components like this:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/views/components/application_
|
|
component.rb#L25
|
|
|
|
def stimulus(controller:, actions: {}, values: {}, outlets: {}, classes: {}, d
|
|
ata: {})
|
|
stimulus_controller = controller.to_s.dasherize
|
|
|
|
action = actions.map do |event, function|
|
|
"#{event}->#{stimulus_controller}##{function.camelize(:lower)}"
|
|
end.join(" ").presence
|
|
|
|
values.transform_keys! do |key|
|
|
[controller, key, "value"].join("_").to_sym
|
|
end
|
|
|
|
outlets.transform_keys! do |key|
|
|
[controller, key, "outlet"].join("_").to_sym
|
|
end
|
|
|
|
classes.transform_keys! do |key|
|
|
[controller, key, "class"].join("_").to_sym
|
|
end
|
|
|
|
{ controller: stimulus_controller, action: }.merge!({ **values, **outlets, *
|
|
*classes, **data})
|
|
end
|
|
|
|
Where we can also see a bit of hash literal omission at {controller:
|
|
stimulus_controller, action: }
|
|
|
|
But more interesting that this method that helps defining a Stimulus
|
|
controller, is the method used to define a Stimulus item that uses
|
|
binding to get variables from the object where it is used:
|
|
# https://github.com/feedbin/feedbin/blob/main/app/views/components/application_
|
|
component.rb#L47
|
|
|
|
def stimulus_item(target: nil, actions: {}, params: {}, data: {}, for:)
|
|
stimulus_controller = binding.local_variable_get(:for).to_s.dasherize
|
|
|
|
action = actions.map do |event, function|
|
|
"#{event}->#{stimulus_controller}##{function.to_s.camelize(:lower)}"
|
|
end.join(" ").presence
|
|
|
|
params.transform_keys! do |key|
|
|
:"#{binding.local_variable_get(:for)}_#{key}_param"
|
|
end
|
|
|
|
defaults = { **params, **data }
|
|
|
|
if action
|
|
defaults[:action] = action
|
|
end
|
|
|
|
if target
|
|
defaults[:"#{binding.local_variable_get(:for)}_target"] = target.to_s.came
|
|
lize(:lower)
|
|
end
|
|
|
|
defaults
|
|
end
|
|
|
|
The part with binding does the following:
|
|
* stimulus_controller =
|
|
binding.local_variable_get(:for).to_s.dasherize This line retrieves
|
|
the value of the local variable for, converts it to a string, and
|
|
then applies the dasherize method (presumably to format it for use
|
|
in a specific context, like a CSS class or an identifier in HTML).
|
|
* Apparently binding.local_variable_get should not be needed as the
|
|
variable is passed a keyword parameter to the method. But the name
|
|
of the variable is for which is a reserved word and thus if the
|
|
code would have been stimulus_controller = for.to_s_dasherize that
|
|
would have raised syntax error, unexpected '.' (SyntaxError)
|
|
|
|
This is a way to have keyword arguments named as reserved words and
|
|
still be able to use them.
|
|
|
|
ComponentsPreview
|
|
|
|
All components can be previewed via Lookbook and they can be found in
|
|
test/components
|
|
|
|
Testing
|
|
|
|
For testing it uses Minitest, the default testing framework from Rails.
|
|
It uses fixtures to set up the test db.
|
|
|
|
Tests are simple and direct, containing all preconditions and
|
|
postconditions in each test. This is great for following what each test
|
|
is doing.
|
|
|
|
There are controller tests, model tests, job tests and some system
|
|
tests. There are more controller tests than system tests making the
|
|
test suite run quite fast. Also the jobs are covered pretty good with
|
|
testing as there is a log of logic in the jobs.
|
|
|
|
Custom assertions
|
|
|
|
There are some custom assertions created specifically to work with
|
|
collections: assert_has_keys will check if all keys are included in the
|
|
hash and assert_equal_ids will check if the two collections provided
|
|
have the same ids (one being a collection of objects and the other one
|
|
being a hash).
|
|
# https://github.com/feedbin/feedbin/blob/main/test/support/assertions.rb#L3
|
|
|
|
def assert_has_keys(keys, hash)
|
|
assert(keys.all? { |key| hash.key?(key) })
|
|
end
|
|
|
|
def assert_equal_ids(collection, results)
|
|
expected = Set.new(collection.map(&:id))
|
|
actual = Set.new(results.map { |result| result["id"] })
|
|
assert_equal(expected, actual)
|
|
end
|
|
|
|
Conclusion
|
|
|
|
In conclusion, Feedbin is an open-source project that combines RSS
|
|
feeds and newsletters into a convenient platform.
|
|
|
|
It utilizes Ruby on Rails, PostgreSQL, Sidekiq, and various other
|
|
technologies to provide a robust and efficient service.
|
|
|
|
The code is well-organized and simple to follow the logic and what is
|
|
happening. I think it will make it easy for anyone to contribute to
|
|
this repo. If you want to run this yourself locally you should take a
|
|
look at the [64]feedbin-docker.
|
|
__________________________________________________________________
|
|
|
|
Enjoyed this article?
|
|
|
|
Join my [65]Short Ruby News newsletter for weekly Ruby updates from the
|
|
community. For more Ruby learning resources, visit
|
|
[66]rubyandrails.info. You can also find me on [67]Ruby.social or
|
|
[68]Linkedin or [69]Twitter where I post mostly about Ruby and Rails.
|
|
|
|
Did you find this article valuable?
|
|
|
|
Support Lucian Ghinda by becoming a sponsor. Any amount is appreciated!
|
|
(BUTTON) Sponsor
|
|
[70]See recent sponsors | [71]Learn more about Hashnode Sponsors
|
|
[72]Ruby[73]Ruby on Rails[74]Open Source[75]coding[76]Programming Blogs
|
|
|
|
References
|
|
|
|
Visible links:
|
|
1. file:///?source=top_nav_blog_home
|
|
2. https://hashnode.com/@lucianghinda
|
|
3. https://hashnode.com/@lucianghinda
|
|
4. https://allaboutcoding.ghinda.com/ruby-open-source-feedbin
|
|
5. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-the-product
|
|
6. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-open-source
|
|
7. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-license
|
|
8. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-technical-review
|
|
9. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-ruby-and-rails-version
|
|
10. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-architecture
|
|
11. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-stats
|
|
12. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-style-guide
|
|
13. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-storage-persistence-and-in-memory-storage
|
|
14. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-gems-used
|
|
15. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-code-amp-design-patterns
|
|
16. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-code-organisation
|
|
17. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-routes
|
|
18. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-controllers
|
|
19. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-models
|
|
20. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-jobs
|
|
21. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-presenters
|
|
22. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-applicationcomponents
|
|
23. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-componentspreview
|
|
24. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-testing
|
|
25. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-custom-assertions
|
|
26. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L39092-8199TMP.html#heading-conclusion
|
|
27. https://feedbin.com/
|
|
28. https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
|
29. https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
|
30. https://github.com/feedbin/rails
|
|
31. https://github.com/Shopify/rails
|
|
32. https://github.com/feedbin/rails/tree/7-1-stable-invalid-cache-entries
|
|
33. https://github.com/feedbin/feedbin/tree/main/app/views/components
|
|
34. https://github.com/feedbin/feedbin/blob/main/Gemfile#L38
|
|
35. https://github.com/feedbin/feedbin/tree/main/app/assets/javascripts/web
|
|
36. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/importmap.rb#L1
|
|
37. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L66
|
|
38. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L94
|
|
39. https://github.com/feedbin/feedbin/blob/main/db/structure.sql
|
|
40. https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/initializers/redis.rb#L1
|
|
41. https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
|
|
42. https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
|
|
43. https://github.com/pauldix/sax-machine
|
|
44. https://github.com/feedjira/feedjira
|
|
45. https://github.com/feedbin/html-pipeline
|
|
46. https://github.com/ostinelli/apnotic
|
|
47. https://github.com/ai/autoprefixer-rails
|
|
48. https://github.com/Rykian/clockwork
|
|
49. https://github.com/janko/down
|
|
50. https://github.com/phlex-ruby/phlex-rails
|
|
51. https://github.com/fphilipe/premailer-rails
|
|
52. https://rubygems.org/gems/raindrops
|
|
53. https://github.com/ankane/strong_migrations
|
|
54. https://github.com/pushpad/web-push
|
|
55. https://github.com/stripe-ruby-mock/stripe-ruby-mock
|
|
56. https://github.com/rails/rails-controller-testing
|
|
57. https://github.com/feedbin/feedbin/blob/main/Gemfile
|
|
58. https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.rb#L1
|
|
59. https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
|
|
60. https://github.com/feedbin/feedbin/blob/main/app/views/entries/destroy.js.erb#L1
|
|
61. https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#L1
|
|
62. https://github.com/feedbin/feedbin/blob/main/app/views/layouts/settings.html.erb#L1
|
|
63. https://github.com/feedbin/feedbin/blob/main/app/views/components/application_component.rb#L3
|
|
64. https://github.com/angristan/feedbin-docker
|
|
65. https://shortruby.com/
|
|
66. http://rubyandrails.info/
|
|
67. http://Ruby.social/
|
|
68. https://linkedin.com/in/lucianghinda
|
|
69. https://x.com/lucianghinda
|
|
70. file:///sponsor
|
|
71. https://hashnode.com/sponsors
|
|
72. file:///tag/ruby?source=tags_bottom_blogs
|
|
73. file:///tag/ruby-on-rails?source=tags_bottom_blogs
|
|
74. file:///tag/opensource?source=tags_bottom_blogs
|
|
75. file:///tag/coding?source=tags_bottom_blogs
|
|
76. file:///tag/programming-blogs?source=tags_bottom_blogs
|
|
|
|
Hidden links:
|
|
78. file://localhost/
|
|
79. file://localhost/?source=top_nav_blog_home
|
|
80. https://twitter.com/lucianghinda
|
|
81. https://github.com/lucianghinda
|
|
82. https://shortruby.com/
|
|
83. https://hashnode.com/@lucianghinda
|
|
84. https://app.daily.dev/lucianghinda
|
|
85. https://linkedin.com/in/lucianghinda
|
|
86. https://ruby.social/@lucian
|
|
87. file://localhost/rss.xml
|
|
88. https://feedbin.com/about
|
|
89. https://github.com/feedbin/feedbin/blob/main/LICENSE.md
|
|
90. http://rubyandrails.info/
|
|
91. https://ruby.social/@lucian
|