0

I have a project in which the client is insistent on all content URLs being top level. This includes URLs across very different resource types, for example:

/products      #=> The Category "products"
/privacy       #=> A content-managed SystemPage "privacy"
/foo-bar-baz   #=> An Article "foo bar baz" (user generated, no less)

Obiviously, this is problematic, raising at least two technical issues:

  1. Routing cannot be handled via a pattern-based approach
  2. The slugs for these different pieces of content need to be unique across tables.

This is not to mention that it feels antithetical to Rails itself, and is certainly not congruent with the framework's expected patterns.

That being said, I'm wondering if there's a reasonably sane way to achieve this.

My first instinct is to maintain a table of "slugs" which map slugs to resource type and perform a lookup by slug at site root, then, if somehow possible, forward the request to the appropriate controller. I'm about to research that possibility, but I thought I should see if anyone else has a solution to this problem (besides firing the client).

numbers1311407
  • 33,686
  • 9
  • 90
  • 92

2 Answers2

1

in routes.rb, you can

match '/*', :controller => proc { (lookup the db here) }

See How to pass params to a block in Rails routes?

Another way is to try and convince the client to accept prefixed URLs like /category_products or /article_foo_bar, which are easy to setup the rails way

Community
  • 1
  • 1
DeeY
  • 922
  • 1
  • 8
  • 15
  • +1 thanks, this got me thinking and led me to remember that controller actions were just rack endpoints. However I don't believe you *can* pass a proc to the `controller` option, at least not in rails 4. – numbers1311407 Dec 07 '13 at 23:41
  • yes you can have :controller => proc { ...}.call, but unfortunately it seems to be evaluated only once and cached for subsequent requests. But I guess it should be possible to override that. – DeeY Dec 11 '13 at 07:55
1

My thought to forward the request from one controller to another was incorrect. In Rails 3+, you can route to Rack endpoints out of the box. This is actually how controller actions themselves are implemented, as rack endpoints.

So to solve this I created a simple Rack app that looks up a Permalink for the given slug, which has a polymorphic reference to one of the different resource types. The controller then determined and the env is passed along to the action. Basically a middleware layer inside the router.

Essentially:

class PermalinkRouter
  def call(env)
    req = ActionDispatch::Request.new(env)

    # will throw if not found
    permalink = Permalink.find_by!(slug: req.params['id'])

    # resource_controller is a method of permalink, which constantizes
    # a controller based on its resource_type.
    permalink.resource_controller.action(:show).call(env)
  end
end

get "/:id", to: PermalinkRouter.new

The permalinks themselves are generated whenever one of the resource types are created, and handle building unique slugs.

It's outside the scope of this question, but treating links as their own resource in this way has allowed me to do a few other interesting things, including implementing a permalink "history", where old links live on as 301 redirects to the current resource URL.

numbers1311407
  • 33,686
  • 9
  • 90
  • 92