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