davideisinger.com

My personal website
Log | Files | Refs | README

allaboutcoding-ghinda-com-jnjy0d.txt (27322B)


      1 [2]
      2 
      3 [4]
      4 []Lucian Ghinda
      5 All about coding
      6 
      7 Follow
      8 
      9 [7]All about coding
     10 
     11 Follow
     12 [8][9][10][11][12][13][14][15]
     13 Ruby open source: feedbinRuby open source: feedbin
     14 
     15 Ruby open source: feedbin
     16 
     17 [16][]Lucian Ghinda's photoLucian Ghinda's photo
     18 [17]Lucian Ghinda
     19 ·[18]Nov 17, 2023·
     20 
     21 11 min read
     22 
     23 Table of contents
     24 
     25   • [19]
     26     The product
     27   • [20]
     28     Open source
     29       □ [21]
     30         License
     31   • [22]
     32     Technical review
     33       □ [23]
     34         Ruby and Rails version
     35       □ [24]
     36         Architecture
     37       □ [25]
     38         Stats
     39       □ [26]
     40         Style Guide
     41       □ [27]
     42         Storage, Persistence and in-memory storage
     43       □ [28]
     44         Gems used
     45       □ [29]
     46         Code & Design Patterns
     47           ☆ [30]
     48             Code Organisation
     49           ☆ [31]
     50             Routes
     51           ☆ [32]
     52             Controllers
     53           ☆ [33]
     54             Models
     55           ☆ [34]
     56             Jobs
     57           ☆ [35]
     58             Presenters
     59           ☆ [36]
     60             ApplicationComponents
     61           ☆ [37]
     62             ComponentsPreview
     63   • [38]
     64     Testing
     65       □ [39]
     66         Custom assertions
     67   • [40]
     68     Conclusion
     69 
     70 The product
     71 
     72 [41]https://feedbin.com
     73 
     74     "Feedbin is the best way to enjoy content on the Web. By combining RSS, and
     75     newsletters, you can get all the good parts of the Web in one convenient
     76     location"
     77 
     78 [42][dcaac2f3-3]
     79 
     80 Open source
     81 
     82 The open-source repository can be found at [43]https://github.com/feedbin/
     83 feedbin
     84 
     85 License
     86 
     87 The [44]license they use is MIT:
     88 
     89 [45][26c3731c-c]
     90 
     91 Technical review
     92 
     93 Ruby and Rails version
     94 
     95 They are currently using:
     96 
     97   • Ruby version 3.2.2
     98 
     99   • They used a fork of Rails at [46]https://github.com/feedbin/rails forked
    100     from [47]https://github.com/Shopify/rails. They are using a branch called 
    101     [48]7-1-stable-invalid-cache-entries - It seems to be Rails 7.1 and about 1
    102     month behind the Shopify/rails which is usually pretty up to date with main
    103     Rails
    104 
    105 Architecture
    106 
    107 Code Architecture:
    108 
    109   • They are using the standard Rails organisation of MVC.
    110 
    111 Database:
    112 
    113   • The DB is PostgreSQL
    114 
    115 Jobs queue:
    116 
    117   • Sidekiq
    118 
    119 On the front-end side:
    120 
    121   • They use .html.erb
    122 
    123   • They are using Phlex for [49]components
    124 
    125   • They are using [50]Jquery for the JS library
    126 
    127   • They have some custom JS code written in [51]CoffeeScript
    128 
    129   • They are using Hotwire via [52]importmaps
    130 
    131   • They are using [53]Tailwind
    132 
    133 Stats
    134 
    135 Running /bin/rails stats will output the following:
    136 
    137 [12169b38-4]
    138 
    139 Running VSCodeCounter will give the following stats:
    140 
    141 [99f9ce55-5]
    142 
    143 Style Guide
    144 
    145 For Ruby:
    146 
    147   • They are using [54]standardrb as the Style Guide with no customisations.
    148 
    149 Storage, Persistence and in-memory storage
    150 
    151 The DB is PostgreSQL.
    152 
    153 They are not using the schema.rb but the [55]structure.sql format for DB schema
    154 dump is configured via application.rb:
    155 
    156 module Feedbin
    157     class Application < Rails::Application
    158         # other configs
    159         config.active_record.schema_format = :sql
    160         # other configs
    161     end
    162 end
    163 
    164 Enabled PSQL extensions:
    165 
    166   • hstore - "data type for storing sets of (key, value) pairs"
    167 
    168   • pg_stat_statements - "track planning and execution statistics of all SQL
    169     statements executed"
    170 
    171   • uuid-ossp - "generate universally unique identifiers (UUIDs)"
    172 
    173 CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public;
    174 COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs';
    175 
    176 CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public;
    177 COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statistics of all SQL statements executed';
    178 
    179 
    180 CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
    181 COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
    182 
    183 Redis is configured to be used with Sidekiq.
    184 
    185 This is what the [56]redis initializer looks like:
    186 
    187 # https://github.com/feedbin/feedbin/blob/main/config/initializers/redis.rb#L1
    188 
    189 defaults = {connect_timeout: 5, timeout: 5}
    190 defaults[:url] = ENV["REDIS_URL"] if ENV["REDIS_URL"]
    191 
    192 $redis = {}.tap do |hash|
    193   options2 = defaults.dup
    194   if ENV["REDIS_URL_PUBLIC_IDS"] || ENV["REDIS_URL_CACHE"]
    195     options2[:url] = ENV["REDIS_URL_PUBLIC_IDS"] || ENV["REDIS_URL_CACHE"]
    196   end
    197   hash[:refresher] = ConnectionPool.new(size: 10) { Redis.new(options2) }
    198 end
    199 
    200 Further, there is a [57]RedisLock configured like this:
    201 
    202 # https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
    203 
    204 class RedisLock
    205   def self.acquire(lock_name, expiration_in_seconds = 55)
    206     Sidekiq.redis { _1.set(lock_name, "locked", ex: expiration_in_seconds, nx: true) }
    207   end
    208 end
    209 
    210 Further down this is used in a [58]clock.rb (that defines scheduled tasks to
    211 run):
    212 
    213 # https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
    214 
    215 every(10.seconds, "clockwork.very_frequent") do
    216   if RedisLock.acquire("clockwork:send_stats:v3", 8)
    217     SendStats.perform_async
    218   end
    219 
    220   if RedisLock.acquire("clockwork:cache_entry_views", 8)
    221     CacheEntryViews.perform_async(nil, true)
    222   end
    223 
    224   if RedisLock.acquire("clockwork:downloader_migration", 8)
    225     FeedCrawler::PersistCrawlData.perform_async
    226   end
    227 end
    228 
    229 every(1.minutes, "clockwork.frequent") do
    230   if RedisLock.acquire("clockwork:feed:refresher:scheduler:v2")
    231     FeedCrawler::ScheduleAll.perform_async
    232   end
    233 
    234   if RedisLock.acquire("clockwork:harvest:embed:data")
    235     HarvestEmbeds.perform_async(nil, true)
    236   end
    237 end
    238 
    239 every(1.day, "clockwork.daily", at: "7:00", tz: "UTC") do
    240   if RedisLock.acquire("clockwork:delete_entries:v2")
    241     EntryDeleterScheduler.perform_async
    242   end
    243 
    244   if RedisLock.acquire("clockwork:trial_expiration:v2")
    245     TrialExpiration.perform_async
    246   end
    247 
    248   if RedisLock.acquire("clockwork:web_sub_maintenance")
    249     WebSub::Maintenance.perform_async
    250   end
    251 end
    252 
    253 Gems used
    254 
    255 Here are some of the gems used:
    256 
    257   • [59]sax-machine - "A declarative sax parsing library backed by Nokogiri"
    258 
    259   • [60]feedjira - "Feedjira is a Ruby library designed to parse feeds"
    260 
    261   • [61]html-pipeline - "HTML processing filters and utilities. This module is
    262     a small framework for defining CSS-based content filters and applying them
    263     to user provided content"
    264 
    265   • [62]apnotic - "A Ruby APNs HTTP/2 gem able to provide instant feedback"
    266 
    267   • [63]autoprefixer-rails - "Autoprefixer is a tool to parse CSS and add
    268     vendor prefixes to CSS rules using values from the Can I Use database. This
    269     gem provides Ruby and Ruby on Rails integration with this JavaScript tool"
    270 
    271   • [64]clockwork - "Clockwork is a cron replacement. It runs as a lightweight,
    272     long-running Ruby process which sits alongside your web processes (Mongrel/
    273     Thin) and your worker processes (DJ/Resque/Minion/Stalker) to schedule
    274     recurring work at particular times or dates"
    275 
    276   • [65]down - "Streaming downloads using net/http, http.rb, HTTPX or wget"
    277 
    278   • [66]phlex-rails - "Phlex is a framework that lets you compose web views in
    279     pure Ruby"
    280 
    281   • [67]premailer-rails - "This gem is a drop in solution for styling HTML
    282     emails with CSS without having to do the hard work yourself"
    283 
    284   • [68]raindrops - "raindrops is a real-time stats toolkit to show statistics
    285     for Rack HTTP servers. It is designed for preforking servers such as
    286     unicorn, but should support any Rack HTTP server on platforms supporting
    287     POSIX shared memory"
    288 
    289   • [69]strong_migrations - "Catch unsafe migrations in development"
    290 
    291   • [70]web-push - "This gem makes it possible to send push messages to web
    292     browsers from Ruby backends using the Web Push Protocol"
    293 
    294   • [71]stripe-ruby-mock - "A drop-in library to test stripe without hitting
    295     their servers"
    296 
    297   • [72]rails-controller-testing - "Brings back assigns and assert_template to
    298     your Rails tests"
    299 
    300 There are many other gems used, I only selected few here. Browse the [73]
    301 Gemfile to discover more.
    302 
    303 What could be mentioned is that they use their fork for some of the gems
    304 included in the file:
    305 
    306 # https://github.com/feedbin/feedbin/blob/main/Gemfile
    307 
    308 # other gems
    309 
    310 gem "rails", github: "feedbin/rails", branch: "7-1-stable-invalid-cache-entries"
    311 
    312 # some other gems
    313 
    314 gem "http",                github: "feedbin/http",                branch: "feedbin"
    315 gem "carrierwave",         github: "feedbin/carrierwave",         branch: "feedbin"
    316 gem "sax-machine",         github: "feedbin/sax-machine",         branch: "feedbin"
    317 gem "feedjira",            github: "feedbin/feedjira",            branch: "f2"
    318 gem "feedkit",             github: "feedbin/feedkit",             branch: "master"
    319 gem "html-pipeline",       github: "feedbin/html-pipeline",       branch: "feedbin"
    320 gem "html_diff",           github: "feedbin/html_diff",           ref: "013e1bb"
    321 gem "twitter",             github: "feedbin/twitter",             branch: "feedbin"
    322 
    323 # other gems 
    324 
    325 group :development, :test do
    326   gem "stripe-ruby-mock", github: "feedbin/stripe-ruby-mock", branch: "feedbin", require: "stripe_mock"
    327 # other gems
    328 end
    329 
    330 # other gem groups
    331 
    332 Code & Design Patterns
    333 
    334 Code Organisation
    335 
    336 Under /app there are 3 folders different from the ones that Rails comes with:
    337 
    338   • presenters
    339 
    340   • uploaders
    341 
    342   • validators
    343 
    344 The lib folder includes very few extra objects. Most of them seems to be
    345 related to communicating with external services.
    346 
    347 Maybe worth mentioning from lib folder is the [74]ConditionalSassCompressor
    348 
    349 # https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.rb#L1
    350 
    351 class ConditionalSassCompressor
    352   def compress(string)
    353     return string if string =~ /tailwindcss/
    354     options = { syntax: :scss, cache: false, read_cache: false, style: :compressed}
    355     begin
    356       Sprockets::Autoload::SassC::Engine.new(string, options).render
    357     rescue => e
    358       puts "Could not compress '#{string[0..65]}'...: #{e.message}, skipping compression"
    359       string
    360     end
    361   end
    362 end
    363 
    364 This is used to configure:
    365 
    366 # https://github.com/feedbin/feedbin/blob/main/config/application.rb#L47
    367 config.assets.css_compressor = ConditionalSassCompressor.new
    368 
    369 Routes
    370 
    371 There is a combination of RESTful routes and non-restful routes.
    372 
    373 Here is an example from entries in the [75]routes.rb :
    374 
    375 # https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
    376 
    377   resources :entries, only: [:show, :index, :destroy] do
    378     member do
    379       post :content
    380       post :unread_entries, to: "unread_entries#update"
    381       post :starred_entries, to: "starred_entries#update"
    382       post :mark_as_read, to: "entries#mark_as_read"
    383       post :recently_read, to: "recently_read_entries#create"
    384       post :recently_played, to: "recently_played_entries#create"
    385       get :push_view
    386       get :newsletter
    387     end
    388     collection do
    389       get :starred
    390       get :unread
    391       get :preload
    392       get :search
    393       get :recently_read, to: "recently_read_entries#index"
    394       get :recently_played, to: "recently_played_entries#index"
    395       get :updated, to: "updated_entries#index"
    396       post :mark_all_as_read
    397       post :mark_direction_as_read
    398     end
    399   end
    400 
    401 Controllers
    402 
    403 The controllers are mostly what I would call vanilla Rails controllers.
    404 
    405 Three notes about them:
    406 
    407   • Some of them are responding with JS usually using USJ or JQuery to change
    408     elements from the page.
    409 
    410   • They contain non-Rails standard actions (actions that are not show, index,
    411     new, create ...)
    412 
    413   • There is a namespaced api folder that contains APIs used by mobile apps
    414 
    415 Here is one simple example for DELETE /entries/:id , the controller looks like
    416 this:
    417 
    418 # https://github.com/feedbin/feedbin/blob/main/app/controllers/entries_controller.rb#L238
    419   def destroy
    420     @user = current_user
    421     @entry = @user.entries.find(params[:id])
    422     if @entry.feed.pages?
    423       EntryDeleter.new.delete_entries(@entry.feed_id, @entry.id)
    424     end
    425   end
    426 
    427 And here is the view [76]destroy.js.erb :
    428 
    429 $('[data-behavior~=entries_target] [data-entry-id=<%= @entry.id %>]').remove();
    430 
    431 feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'unread')
    432 feedbin.Counts.get().removeEntry(<%= @entry.id %>, <%= @entry.feed_id %>, 'starred')
    433 feedbin.applyCounts(true)
    434 
    435 feedbin.clearEntry();
    436 feedbin.fullScreen(false)
    437 
    438 The main pattern adopted to controllers is to have some logic in them and
    439 delegate to jobs some part of the processing.
    440 
    441 The repo contains mostly straight-forward controllers like this one:
    442 
    443 # https://github.com/feedbin/feedbin/blob/main/app/controllers/pages_internal_controller.rb#L1
    444 class PagesInternalController < ApplicationController
    445 
    446   def create
    447     @entry = SavePage.new.perform(current_user.id, params[:url], nil)
    448     get_feeds_list
    449   end
    450 end
    451 
    452 But also few controllers that include some logic:
    453 
    454 # https://github.com/feedbin/feedbin/blob/main/app/controllers/api/podcasts/v1/feeds_controller.rb#L8
    455 
    456  def show
    457   url = hex_decode(params[:id])
    458   @feed = Feed.find_by_feed_url(url)
    459   if @feed.present?
    460     if @feed.standalone_request_at.blank?
    461       FeedStatus.new.perform(@feed.id)
    462       FeedUpdate.new.perform(@feed.id)
    463     end
    464   else
    465     feeds = FeedFinder.feeds(url)
    466     @feed = feeds.first
    467   end
    468 
    469   if @feed.present?
    470     @feed.touch(:standalone_request_at)
    471   else
    472     status_not_found
    473   end
    474 rescue => exception
    475   if Rails.env.production?
    476     ErrorService.notify(exception)
    477     status_not_found
    478   else
    479     raise exception
    480   end
    481 end
    482 
    483 Even with this structure, I find all controllers easy to read and I think they
    484 can be easier to change.
    485 
    486 Models
    487 
    488 The app/models folders contain both ActiveRecord and normal Ruby objects. With
    489 few exceptions, they are not namespaced.
    490 
    491 Jobs
    492 
    493 The jobs folder contains Sidekiq jobs which are used to do processing on
    494 various objects. They are usually called from controllers and most of them are
    495 async.
    496 
    497 Here is one job that is caching views:
    498 
    499 # https://github.com/feedbin/feedbin/blob/main/app/jobs/cache_entry_views.rb#L1
    500 
    501 class CacheEntryViews
    502   include Sidekiq::Worker
    503   include SidekiqHelper
    504 
    505   SET_NAME = "#{name}-ids"
    506 
    507   def perform(entry_id, process = false)
    508     if process
    509       cache_views
    510     else
    511       add_to_queue(SET_NAME, entry_id)
    512     end
    513   end
    514 
    515   def cache_views
    516     entry_ids = dequeue_ids(SET_NAME)
    517     entries = Entry.where(id: entry_ids).includes(feed: [:favicon])
    518     ApplicationController.render({
    519       partial: "entries/entry",
    520       collection: entries,
    521       format: :html,
    522       cached: true
    523     })
    524     ApplicationController.render({
    525       layout: nil,
    526       template: "api/v2/entries/index",
    527       assigns: {entries: entries},
    528       format: :html,
    529       locals: {
    530         params: {mode: "extended"}
    531       }
    532     })
    533   end
    534 end
    535 
    536 Presenters
    537 
    538 There is a [77]BasePresenter and all other presenters are extending it via
    539 inheritance:
    540 
    541 This controller defines a private method called presents:
    542 
    543 # https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#L1
    544 
    545 class BasePresenter
    546   def initialize(object, locals, template)
    547     @object = object
    548     @locals = locals
    549     @template = template
    550   end
    551 
    552   # ...
    553 
    554   private
    555 
    556   def self.presents(name)
    557     define_method(name) do
    558       @object
    559     end
    560   end
    561 end
    562 
    563 and it is used like this for example:
    564 
    565 # https://github.com/feedbin/feedbin/blob/main/app/presenters/user_presenter.rb#L2
    566 
    567 class UserPresenter < BasePresenter
    568   presents :user
    569   delegate_missing_to :user
    570 
    571   # ... more code
    572 
    573   def theme
    574     result = settings["theme"].present? ? settings["theme"] : nil
    575     result || user.theme || "auto"
    576   end
    577   # ... other code
    578 end
    579 
    580 To use the presenters, there is a helper defined in ApplicationHelper will
    581 instantiate the proper helper based on the object class:
    582 
    583 module ApplicationHelper
    584   def present(object, locals = nil, klass = nil)
    585     klass ||= "#{object.class}Presenter".constantize
    586     presenter = klass.new(object, locals, self)
    587     yield presenter if block_given?
    588     presenter
    589   end
    590 
    591   # more code ...
    592 end
    593 
    594 and it is used [78]like this in views:
    595 
    596 <% present @user do |user_presenter| %>
    597     <% @class = "settings-body settings-#{params[:action]} theme-#{user_presenter.theme}"%>
    598 <% end %>
    599 
    600 ApplicationComponents
    601 
    602 Components are based on Phlex and they inherit from [79]ApplicationComponent
    603 
    604 It defines a method to add Stimulus controller in components like this:
    605 
    606 # https://github.com/feedbin/feedbin/blob/main/app/views/components/application_component.rb#L25
    607 
    608   def stimulus(controller:, actions: {}, values: {}, outlets: {}, classes: {}, data: {})
    609     stimulus_controller = controller.to_s.dasherize
    610 
    611     action = actions.map do |event, function|
    612       "#{event}->#{stimulus_controller}##{function.camelize(:lower)}"
    613     end.join(" ").presence
    614 
    615     values.transform_keys! do |key|
    616       [controller, key, "value"].join("_").to_sym
    617     end
    618 
    619     outlets.transform_keys! do |key|
    620       [controller, key, "outlet"].join("_").to_sym
    621     end
    622 
    623     classes.transform_keys! do |key|
    624       [controller, key, "class"].join("_").to_sym
    625     end
    626 
    627     { controller: stimulus_controller, action: }.merge!({ **values, **outlets, **classes, **data})
    628   end
    629 
    630 Where we can also see a bit of hash literal omission at {controller:
    631 stimulus_controller, action: }
    632 
    633 But more interesting that this method that helps defining a Stimulus
    634 controller, is the method used to define a Stimulus item that uses binding to
    635 get variables from the object where it is used:
    636 
    637 # https://github.com/feedbin/feedbin/blob/main/app/views/components/application_component.rb#L47
    638 
    639   def stimulus_item(target: nil, actions: {}, params: {}, data: {}, for:)
    640     stimulus_controller = binding.local_variable_get(:for).to_s.dasherize
    641 
    642     action = actions.map do |event, function|
    643       "#{event}->#{stimulus_controller}##{function.to_s.camelize(:lower)}"
    644     end.join(" ").presence
    645 
    646     params.transform_keys! do |key|
    647       :"#{binding.local_variable_get(:for)}_#{key}_param"
    648     end
    649 
    650     defaults = { **params, **data }
    651 
    652     if action
    653       defaults[:action] = action
    654     end
    655 
    656     if target
    657       defaults[:"#{binding.local_variable_get(:for)}_target"] = target.to_s.camelize(:lower)
    658     end
    659 
    660     defaults
    661   end
    662 
    663 The part with binding does the following:
    664 
    665   • stimulus_controller = binding.local_variable_get(:for).to_s.dasherize This
    666     line retrieves the value of the local variable for, converts it to a
    667     string, and then applies the dasherize method (presumably to format it for
    668     use in a specific context, like a CSS class or an identifier in HTML).
    669 
    670   • Apparently binding.local_variable_get should not be needed as the variable
    671     is passed a keyword parameter to the method. But the name of the variable
    672     is for which is a reserved word and thus if the code would have been
    673     stimulus_controller = for.to_s_dasherize that would have raised syntax
    674     error, unexpected '.' (SyntaxError)
    675 
    676 This is a way to have keyword arguments named as reserved words and still be
    677 able to use them.
    678 
    679 ComponentsPreview
    680 
    681 All components can be previewed via Lookbook and they can be found in test/
    682 components
    683 
    684 Testing
    685 
    686 For testing it uses Minitest, the default testing framework from Rails. It uses
    687 fixtures to set up the test db.
    688 
    689 Tests are simple and direct, containing all preconditions and postconditions in
    690 each test. This is great for following what each test is doing.
    691 
    692 There are controller tests, model tests, job tests and some system tests. There
    693 are more controller tests than system tests making the test suite run quite
    694 fast. Also the jobs are covered pretty good with testing as there is a log of
    695 logic in the jobs.
    696 
    697 Custom assertions
    698 
    699 There are some custom assertions created specifically to work with collections:
    700 assert_has_keys will check if all keys are included in the hash and
    701 assert_equal_ids will check if the two collections provided have the same ids
    702 (one being a collection of objects and the other one being a hash).
    703 
    704 # https://github.com/feedbin/feedbin/blob/main/test/support/assertions.rb#L3
    705 
    706     def assert_has_keys(keys, hash)
    707       assert(keys.all? { |key| hash.key?(key) })
    708     end
    709 
    710     def assert_equal_ids(collection, results)
    711       expected = Set.new(collection.map(&:id))
    712       actual = Set.new(results.map { |result| result["id"] })
    713       assert_equal(expected, actual)
    714     end
    715 
    716 Conclusion
    717 
    718 In conclusion, Feedbin is an open-source project that combines RSS feeds and
    719 newsletters into a convenient platform.
    720 
    721 It utilizes Ruby on Rails, PostgreSQL, Sidekiq, and various other technologies
    722 to provide a robust and efficient service.
    723 
    724 The code is well-organized and simple to follow the logic and what is
    725 happening. I think it will make it easy for anyone to contribute to this repo.
    726 If you want to run this yourself locally you should take a look at the [80]
    727 feedbin-docker.
    728 
    729 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    730 
    731 Enjoyed this article?
    732 
    733 👉 Join my [81]Short Ruby News newsletter for weekly Ruby updates from the
    734 community and visit [82][83]rubyandrails.info, a directory with learning
    735 content about Ruby.
    736 
    737 👐 Subscribe to my Ruby and Ruby on rails courses over email at [84]
    738 learn.shortruby.com - effortless learning anytime, anywhere
    739 
    740 🤝 Let's connect on [85][86]Ruby.social or [87]Linkedin or [88]Twitter where I
    741 post mainly about Ruby and Rails.
    742 
    743 🎥 Follow me on [89]my YouTube channel for short videos about Ruby
    744 
    745 Did you find this article valuable?
    746 
    747 Support Lucian Ghinda by becoming a sponsor. Any amount is appreciated!
    748 
    749 Sponsor
    750 [91]See recent sponsors | [92]Learn more about Hashnode Sponsors
    751 [93]Ruby[94]Ruby on Rails[95]Open Source[96]coding[97]Programming Blogs
    752 
    753 References:
    754 
    755 [2] https://allaboutcoding.ghinda.com/
    756 [4] https://allaboutcoding.ghinda.com/?source=top_nav_blog_home
    757 [7] https://allaboutcoding.ghinda.com/?source=top_nav_blog_home
    758 [8] https://twitter.com/lucianghinda
    759 [9] https://github.com/lucianghinda
    760 [10] https://shortruby.com/
    761 [11] https://hashnode.com/@lucianghinda
    762 [12] https://app.daily.dev/lucianghinda
    763 [13] https://linkedin.com/in/lucianghinda
    764 [14] https://ruby.social/@lucian
    765 [15] https://allaboutcoding.ghinda.com/rss.xml
    766 [16] https://hashnode.com/@lucianghinda
    767 [17] https://hashnode.com/@lucianghinda
    768 [18] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin
    769 [19] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-the-product
    770 [20] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-open-source
    771 [21] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-license
    772 [22] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-technical-review
    773 [23] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-ruby-and-rails-version
    774 [24] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-architecture
    775 [25] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-stats
    776 [26] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-style-guide
    777 [27] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-storage-persistence-and-in-memory-storage
    778 [28] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-gems-used
    779 [29] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-code-amp-design-patterns
    780 [30] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-code-organisation
    781 [31] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-routes
    782 [32] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-controllers
    783 [33] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-models
    784 [34] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-jobs
    785 [35] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-presenters
    786 [36] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-applicationcomponents
    787 [37] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-componentspreview
    788 [38] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-testing
    789 [39] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-custom-assertions
    790 [40] https://allaboutcoding.ghinda.com/ruby-open-source-feedbin#heading-conclusion
    791 [41] https://feedbin.com/
    792 [42] https://feedbin.com/about
    793 [43] https://github.com/feedbin/feedbin/blob/main/LICENSE.md
    794 [44] https://github.com/feedbin/feedbin/blob/main/LICENSE.md
    795 [45] https://github.com/feedbin/feedbin/blob/main/LICENSE.md
    796 [46] https://github.com/feedbin/rails
    797 [47] https://github.com/Shopify/rails
    798 [48] https://github.com/feedbin/rails/tree/7-1-stable-invalid-cache-entries
    799 [49] https://github.com/feedbin/feedbin/tree/main/app/views/components
    800 [50] https://github.com/feedbin/feedbin/blob/main/Gemfile#L38
    801 [51] https://github.com/feedbin/feedbin/tree/main/app/assets/javascripts/web
    802 [52] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/importmap.rb#L1
    803 [53] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L66
    804 [54] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/Gemfile#L94
    805 [55] https://github.com/feedbin/feedbin/blob/main/db/structure.sql
    806 [56] https://github.com/feedbin/feedbin/blob/abf1ad883dab8a3464fe12e4653de6323296175b/config/initializers/redis.rb#L1
    807 [57] https://github.com/feedbin/feedbin/blob/main/app/models/redis_lock.rb#L1
    808 [58] https://github.com/feedbin/feedbin/blob/main/lib/clock.rb#L8
    809 [59] https://github.com/pauldix/sax-machine
    810 [60] https://github.com/feedjira/feedjira
    811 [61] https://github.com/feedbin/html-pipeline
    812 [62] https://github.com/ostinelli/apnotic
    813 [63] https://github.com/ai/autoprefixer-rails
    814 [64] https://github.com/Rykian/clockwork
    815 [65] https://github.com/janko/down
    816 [66] https://github.com/phlex-ruby/phlex-rails
    817 [67] https://github.com/fphilipe/premailer-rails
    818 [68] https://rubygems.org/gems/raindrops
    819 [69] https://github.com/ankane/strong_migrations
    820 [70] https://github.com/pushpad/web-push
    821 [71] https://github.com/stripe-ruby-mock/stripe-ruby-mock
    822 [72] https://github.com/rails/rails-controller-testing
    823 [73] https://github.com/feedbin/feedbin/blob/main/Gemfile
    824 [74] https://github.com/feedbin/feedbin/blob/main/lib/conditional_sass_compressor.rb#L1
    825 [75] https://github.com/feedbin/feedbin/blob/main/config/routes.rb#L133
    826 [76] https://github.com/feedbin/feedbin/blob/main/app/views/entries/destroy.js.erb#L1
    827 [77] https://github.com/feedbin/feedbin/blob/main/app/presenters/base_presenter.rb#L1
    828 [78] https://github.com/feedbin/feedbin/blob/main/app/views/layouts/settings.html.erb#L1
    829 [79] https://github.com/feedbin/feedbin/blob/main/app/views/components/application_component.rb#L3
    830 [80] https://github.com/angristan/feedbin-docker
    831 [81] https://shortruby.com/
    832 [82] http://rubyandrails.info/
    833 [83] http://rubyandrails.info/
    834 [84] https://learn.shortruby.com/
    835 [85] https://ruby.social/@lucian
    836 [86] http://ruby.social/
    837 [87] https://linkedin.com/in/lucianghinda
    838 [88] https://x.com/lucianghinda
    839 [89] https://www.youtube.com/@shortruby
    840 [91] https://allaboutcoding.ghinda.com/sponsor
    841 [92] https://hashnode.com/sponsors
    842 [93] https://allaboutcoding.ghinda.com/tag/ruby?source=tags_bottom_blogs
    843 [94] https://allaboutcoding.ghinda.com/tag/ruby-on-rails?source=tags_bottom_blogs
    844 [95] https://allaboutcoding.ghinda.com/tag/opensource?source=tags_bottom_blogs
    845 [96] https://allaboutcoding.ghinda.com/tag/coding?source=tags_bottom_blogs
    846 [97] https://allaboutcoding.ghinda.com/tag/programming-blogs?source=tags_bottom_blogs