Skip to content

Add Firestore caching and SEO meta tags#74

Merged
dyoung522 merged 2 commits intomainfrom
enhance/caching-and-seo
Feb 10, 2026
Merged

Add Firestore caching and SEO meta tags#74
dyoung522 merged 2 commits intomainfrom
enhance/caching-and-seo

Conversation

@dyoung522
Copy link
Contributor

Combined PR for #60 and #66.

Firestore Caching (#60)

  • Mod.all and Tool.all now use Rails.cache.fetch with a 5-minute TTL
  • Added expire_cache class methods for manual invalidation
  • Enabled memory_store cache in production (appropriate for single-server deploy)
  • With 409 mods, this avoids hitting Firestore on every page load

SEO Meta Tags (#66)

  • Dynamic <title> tags: Page Name | ProjectDaedalus
  • <meta name="description"> with context-aware defaults
  • OpenGraph tags (og:title, og:description, og:type, og:site_name)
  • Per-mod og:image when image_url is present
  • Mods index meta description includes total mod count

234 specs passing, RuboCop clean.

Note: The rubocop:disable Metrics/ClassLength on Mod will be resolved when PR #72 (Displayable concern) is merged.

Closes #60, closes #66

Copilot AI review requested due to automatic review settings February 10, 2026 01:45
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR combines #60 and #66 by adding short-lived caching around Firestore “fetch all” queries for Mod/Tool, and introducing dynamic page titles + meta/OG tags via content_for and the application layout.

Changes:

  • Cache Mod.all / Tool.all via Rails.cache.fetch (5-minute TTL) and add manual invalidation methods.
  • Configure production to use :memory_store caching.
  • Add dynamic <title>, meta description, and OpenGraph tags with per-page overrides in key views.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
app/models/mod.rb Wraps .all in Rails.cache.fetch, adds fetch_all and expire_cache, adds RuboCop class-length disable/enable.
app/models/tool.rb Wraps .all in Rails.cache.fetch, adds fetch_all and expire_cache.
config/environments/production.rb Enables :memory_store cache store with a size limit.
app/views/layouts/application.html.erb Adds dynamic title, meta description, and OG tags with content_for defaults.
app/views/mods/index.html.erb Sets content_for(:title) and content_for(:meta_description) (includes mod count).
app/views/mods/show.html.erb Sets per-mod title/description and og:image when available.
app/views/tools/index.html.erb Sets tools index title and meta description.
spec/models/mod_spec.rb Adds caching specs for Mod.all and Mod.expire_cache.
spec/models/tool_spec.rb Adds caching specs for Tool.all and Tool.expire_cache.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


require "net/http"

# rubocop:disable Metrics/ClassLength -- Will shrink when Displayable concern (#63) is merged
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RuboCop note says "Will shrink when ... (#63) is merged" but #63 appears to be an issue reference (issues don't get merged). Consider updating the reference to the actual PR number (e.g., #72) or rewording to avoid implying an issue will be merged.

Suggested change
# rubocop:disable Metrics/ClassLength -- Will shrink when Displayable concern (#63) is merged
# rubocop:disable Metrics/ClassLength -- Will shrink after extracting Displayable concern (see #63)

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +218
it "caches results from .all" do
described_class.all
expect(Rails.cache.exist?("firestore/tools")).to be true
end
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "caches results from .all" example only asserts that the cache key exists after a single call, which doesn't actually prove the caching behavior (i.e., that the Firestore fetch is skipped on subsequent calls). Consider stubbing/expecting described_class.fetch_all (or the Firestore collection .get) and calling .all twice, asserting the underlying fetch happens only once.

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +212
original_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
example.run
ensure
Rails.cache = original_store
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reassigning Rails.cache globally in an around hook can create order-dependent failures if specs are ever run in parallel (or if other examples rely on the default :null_store). Prefer stubbing Rails.cache (e.g., allow(Rails).to receive(:cache).and_return(store)) or using ActiveSupport::Cache.lookup_store with stub_const/around in a way that doesn't mutate global state.

Suggested change
original_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
example.run
ensure
Rails.cache = original_store
store = ActiveSupport::Cache::MemoryStore.new
allow(Rails).to receive(:cache).and_return(store)
example.run

Copilot uses AI. Check for mistakes.
Comment on lines +367 to +370
it "caches results from .all" do
described_class.all
expect(Rails.cache.exist?("firestore/mods")).to be true
end
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "caches results from .all" example only checks that the cache key exists after one call, which doesn't verify that subsequent calls avoid hitting Firestore. Consider calling .all twice while asserting described_class.fetch_all (or the Firestore .get) is invoked only once.

Copilot uses AI. Check for mistakes.
Comment on lines +360 to +364
original_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
example.run
ensure
Rails.cache = original_store
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reassigning Rails.cache in this around hook mutates global state for the duration of the example, which can become flaky if the suite is parallelized or other specs depend on the default cache store. Prefer stubbing Rails.cache to a MemoryStore for these examples rather than assigning to Rails.cache=.

Suggested change
original_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
example.run
ensure
Rails.cache = original_store
memory_store = ActiveSupport::Cache::MemoryStore.new
allow(Rails).to receive(:cache).and_return(memory_store)
example.run

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 24
def self.all
Rails.cache.fetch("firestore/tools", expires_in: 5.minutes) do
fetch_all
end
end

def self.fetch_all
firestore.col("tools").get.filter_map do |tool|
new(
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch_all looks like an internal helper used only by .all to populate the cache, but it's currently a public class method (expanding the public API surface and making it easy to bypass caching accidentally). Consider marking it as private_class_method (or prefixing it as an internal method) so callers consistently use .all/.expire_cache.

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 24
def self.all
Rails.cache.fetch("firestore/mods", expires_in: 5.minutes) do
fetch_all
end
end

def self.fetch_all
firestore.col("mods").get.filter_map do |mod|
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as Tool: fetch_all is only referenced internally by .all and is now part of the public API. To reduce accidental cache bypass and keep the external API smaller, consider making fetch_all a private_class_method (or otherwise clearly internal).

Copilot uses AI. Check for mistakes.
Caching (#60):
- Add Rails.cache.fetch with 5-minute TTL to Mod.all and Tool.all
- Add expire_cache class methods for manual cache invalidation
- Enable memory_store cache in production (single-server app)
- Add caching specs for both models

SEO (#66):
- Dynamic page titles via content_for(:title)
- Meta description tags with sensible defaults
- OpenGraph tags (og:title, og:description, og:type, og:site_name)
- Per-mod og:image when image_url is present
- Mods index shows total mod count in meta description

Closes #60, closes #66
- Make fetch_all a private_class_method on Mod and Tool to prevent
  accidental cache bypass
- Improve caching specs: verify fetch_all is called only once across
  two .all calls (proves caching works)
- Use allow(Rails).to receive(:cache) instead of reassigning
  Rails.cache to avoid global state mutation
@dyoung522 dyoung522 merged commit 09bb96c into main Feb 10, 2026
2 checks passed
@dyoung522 dyoung522 deleted the enhance/caching-and-seo branch February 10, 2026 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SEO meta tags (description, OG tags, etc.) Add caching for Firestore queries

2 participants