Files
davideisinger.com/static/archive/allaboutcoding-ghinda-com-jnjy0d.txt
2023-12-06 20:45:00 -05:00

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