This PR enhances Action Text's to_markdown method so that ActiveStorage blob attachments can generate real Markdown links ( for images, [title](url) for non-images) instead of placeholder [caption] text. The feature is opt-in via an attachment_links: false keyword argument that threads through RichText, Content, Attachment, and all attachable implementations. URL generation leverages the existing thread-local ActionText::Content.renderer set by the engine's around_action, and correctly inherits request context including script_name for multi-tenant apps. The change is backward-compatible — default behavior is unchanged, the new keyword defaults to false, and custom attachables only need to add the keyword to their method signature.
actiontext/lib/action_text/engine.rb — The core logic lives here. The attachable_markdown_representation method on ActiveStorage::Blob is where URL generation and image/non-image branching happens. Start here to understand the feature.actiontext/lib/action_text/attachment.rb — The to_markdown method that delegates to attachable_markdown_representation. Shows how the keyword argument threads through and how the documentation is updated.actiontext/lib/action_text/content.rb — Threads attachment_links: through the rendering pipeline. Important to verify the keyword propagates correctly.actiontext/app/models/action_text/rich_text.rb — The public-facing model method. Simple pass-through but confirms the API surface.actiontext/lib/action_text/attachables/remote_image.rb — Signature update only. Confirms backward compatibility for existing attachable implementations.actiontext/lib/action_text/attachables/content_attachment.rb — Signature update only.actiontext/lib/action_text/attachables/missing_attachable.rb — Signature update only.actiontext/test/fixtures/files/report.txt — New test fixture for non-image blob testing.actiontext/test/unit/markdown_conversion_test.rb — The bulk of the diff. Comprehensive test coverage for all combinations of blob type, caption, renderer context, and edge cases. Read last since the implementation context makes the tests easier to follow.attachment_links: true)
┌─────────────────┐ ┌──────────────────┐ ┌────────────────────────────────────┐
│ RichText │ │ Content │ │ Attachment │
│ #to_markdown │────▶│ #to_markdown │────▶│ #to_markdown │
│ (keyword fwd) │ │ (render loop) │ │ (delegates to attachable) │
└─────────────────┘ └──────────────────┘ └──────────┬─────────────────────────┘
│
┌─────────────┴──────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ RemoteImage │ │ ActiveStorage::Blob │
│ (always generates │ │ (engine.rb initializer) │
│ ) │ │ │
└──────────────────────┘ └──────────┬───────────────┘
│
┌────────────┴────────────┐
│ attachment_links: true? │
└────────────┬────────────┘
no │ │ yes
▼ ▼
┌──────────────┐ ┌──────────────────────┐
│ "[title]" │ │ Content.renderer │
│ (placeholder)│ │ .url_for(blob) │
└──────────────┘ └──────────┬───────────┘
│
┌────────────┴───────────┐
│ image? │
└────────────┬───────────┘
yes │ │ no
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ "" │ │ "[t](url)" │
└──────────────┘ └──────────────────┘
┌─────────────────────┐ around_action ┌──────────────────────────────────┐
│ Controller/Mailer │ ──────────────────────▶ │ ActionText::Content.renderer = │
│ (request context) │ │ controller (thread-local) │
└─────────────────────┘ └──────────────────────────────────┘
│
▼
┌──────────────────────┐
│ renderer.url_for │
│ (inherits host, │
│ protocol, │
│ script_name) │
└──────────────────────┘
| File | Type | Before | After | Impact | Risk |
|---|---|---|---|---|---|
actiontext/lib/action_text/engine.rb |
Modified | Blob's attachable_markdown_representation always returned [caption] placeholder |
When attachment_links: true and a renderer exists, generates  for images or [title](url) for non-images. Falls back to [title] otherwise. |
Core feature implementation. Blobs can now produce real Markdown links with URLs from the request context. | 🟢 Low — opt-in via keyword, default unchanged |
actiontext/lib/action_text/attachment.rb |
Modified | to_markdown took no arguments |
Accepts attachment_links: keyword and passes it to attachable_markdown_representation. Documentation expanded with examples. |
API surface expanded but backward-compatible via default value | 🟢 Low — keyword with default |
actiontext/lib/action_text/content.rb |
Modified | to_markdown took no arguments |
Accepts and forwards attachment_links: through the render pipeline |
Plumbing change to propagate the option | 🟢 Low — pure pass-through |
actiontext/app/models/action_text/rich_text.rb |
Modified | to_markdown took no arguments |
Accepts and forwards attachment_links: to body.to_markdown |
Public model API updated | 🟢 Low — pure pass-through |
actiontext/lib/action_text/attachables/remote_image.rb |
Modified | attachable_markdown_representation(caption) |
Adds attachment_links: false keyword (ignored — remote images always generate links) |
Signature alignment for interface consistency | 🟢 Low — no behavior change |
actiontext/lib/action_text/attachables/content_attachment.rb |
Modified | attachable_markdown_representation(caption) |
Adds attachment_links: false keyword (ignored) |
Signature alignment | 🟢 Low — no behavior change |
actiontext/lib/action_text/attachables/missing_attachable.rb |
Modified | attachable_markdown_representation(caption = nil) |
Adds attachment_links: false keyword (ignored — still returns ☒) |
Signature alignment | 🟢 Low — no behavior change |
actiontext/test/fixtures/files/report.txt |
Added | N/A | A one-line text fixture file for testing non-image blob attachments | Enables testing [title](url) output for non-image blobs |
🟢 Low — test fixture |
actiontext/test/unit/markdown_conversion_test.rb |
Modified | Tests covered blob markdown as [caption] only. ~93 lines of attachment tests. |
Expanded to ~200 lines covering: image vs non-image blobs, with/without captions, with/without attachment_links, controller and mailer renderers, script_name propagation, missing renderer error, metacharacter escaping. Added with_controller_renderer helper. |
Comprehensive coverage for all new behavior and edge cases | 🟢 Low — tests only |
attachable_markdown_representation
Medium
What could break: Any application that has a custom attachable class implementing attachable_markdown_representation(caption) without the new attachment_links: keyword will receive an ArgumentError when attachment_links: true is passed, since Ruby will reject the unexpected keyword argument.
Cause: The signature of Attachment#to_markdown now always passes attachment_links: to the attachable's attachable_markdown_representation.
Severity: Medium — only affects apps with custom attachables AND only when attachment_links: true is explicitly used. Default behavior (false) still works because Ruby keyword arguments with defaults don't raise on the caller side.
Mitigation: The commit message documents this: "Custom attachables should add the keyword to their method signature." The PR description also notes this. A CHANGELOG entry or upgrade guide note would help, but this is a minor migration and only affects the opt-in path.
What could break: Calling to_markdown(attachment_links: true) outside of a controller/mailer context (e.g., in a background job or console) raises ArgumentError.
Cause: The code explicitly checks renderer = ActionText::Content.renderer and raises with a clear message if nil.
Severity: Low — this is intentional and well-documented behavior. The error message is descriptive: "attachment_links requires a rendering context". Tests verify this.
Mitigation: Already handled. The default (attachment_links: false) works everywhere. The explicit error is the correct design choice.
What could break: If ActionText::Content.renderer leaks across threads or fibers, URL generation could use the wrong request context.
Cause: This PR doesn't introduce the thread-local pattern — it was already established by the around_action in the engine initializer. The PR merely reads the existing value.
Severity: Low — pre-existing pattern, not introduced by this change.
Mitigation: No action needed. The existing with_renderer block pattern ensures cleanup.
File: CHANGELOG (missing)
The PR checklist shows CHANGELOG is unchecked. This is a user-facing feature addition — to_markdown(attachment_links: true) is a new capability. A CHANGELOG entry in actiontext/CHANGELOG.md would help users discover the feature. The PR author likely intended to add this in a follow-up, and the checkbox was struck through, suggesting this was a conscious decision. Still worth noting.
File: actiontext/lib/action_text/engine.rb:67
The raise ArgumentError when no renderer is present is good defensive programming. Consider whether a nil return or falling back to [title] would be more forgiving for callers who might not realize they're outside a request context. However, the current "fail loud" approach is arguably better for Rails — it prevents silent data loss (URLs silently omitted) and matches the PR description's intent.
File: actiontext/test/unit/markdown_conversion_test.rb
The test renaming (e.g., "attachment with surrounding text" → "RemoteImage attachment with surrounding text") is a nice clarity improvement. Some tests that were simply reorganized (not changed in behavior) could have been in a separate commit to reduce noise in the diff, but this is a minor style preference and doesn't affect the PR quality.
This is a clean, well-scoped feature addition by an experienced Rails contributor. The design is sound: opt-in via keyword argument, backward-compatible defaults, proper error handling for missing context, and URL generation that correctly inherits request state. The test coverage is thorough — covering image vs non-image blobs, controller vs mailer renderers, script_name propagation, missing renderer errors, metacharacter escaping, and the default behavior remaining unchanged. The code is easy to follow, the documentation is updated, and the signature changes to existing attachable implementations are minimal and non-breaking for the default path. Ship it.