{"id":22819539,"url":"https://github.com/dcalixto/friendly_id","last_synced_at":"2025-10-18T08:37:01.230Z","repository":{"id":266025115,"uuid":"897134762","full_name":"dcalixto/friendly_id","owner":"dcalixto","description":"FriendlyId for Crystal - Create human-readable URLs and slugs","archived":false,"fork":false,"pushed_at":"2024-12-22T01:25:14.000Z","size":4373,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-30T23:13:51.596Z","etag":null,"topics":["crystal","crystal-lang","seo","seo-optimization","slug","slug-generator","slugify"],"latest_commit_sha":null,"homepage":"https://github.com/dcalixto/friendly_id","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dcalixto.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-02T04:59:11.000Z","updated_at":"2024-12-22T01:25:18.000Z","dependencies_parsed_at":"2024-12-22T06:01:11.485Z","dependency_job_id":null,"html_url":"https://github.com/dcalixto/friendly_id","commit_stats":null,"previous_names":["dcalixto/friendly_id"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/dcalixto/friendly_id","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcalixto%2Ffriendly_id","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcalixto%2Ffriendly_id/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcalixto%2Ffriendly_id/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcalixto%2Ffriendly_id/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dcalixto","download_url":"https://codeload.github.com/dcalixto/friendly_id/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dcalixto%2Ffriendly_id/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279495234,"owners_count":26180194,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-18T02:00:06.492Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crystal","crystal-lang","seo","seo-optimization","slug","slug-generator","slugify"],"created_at":"2024-12-12T15:12:50.575Z","updated_at":"2025-10-18T08:37:01.195Z","avatar_url":"https://github.com/dcalixto.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FriendlyId\n\nA Crystal shard for creating human-readable URLs and slugs. FriendlyId lets you create pretty URLs and slugs for your resources, with support for history tracking and customization.\n\n[![Crystal Test](https://github.com/dcalixto/friendly_id/actions/workflows/crystal-test.yml/badge.svg?branch=master)](https://github.com/dcalixto/friendly_id/actions/workflows/crystal-test.yml)\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n```yaml\ndependencies:\n  friendly_id:\n    github: dcalixto/friendly_id\n```\n\n\u003e [!NOTE]\n\u003e Make sure your database table has a slug column:\n\n```yaml\nALTER TABLE posts ADD COLUMN slug VARCHAR;\n```\n\n2. Run\n\n```yaml\nshards install\n```\n\nGenerate and run the required migrations:\n\n```crystal\ncrystal ../friendly_id/src/friendly_id/install.cr\n```\n\nThis will create the necessary database tables and indexes for FriendlyId to work:\n\n```crystal\nCREATE TABLE friendly_id_slugs (\n  id BIGSERIAL PRIMARY KEY,\n  slug VARCHAR NOT NULL,\n  sluggable_id BIGINT NOT NULL,\n  sluggable_type VARCHAR(50) NOT NULL,\n  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n```\n\n## Setup\n\nConfigure FriendlyId in your application:\n\n\u003e [!NOTE]\n\u003e set a initializer # friendly_id.cr\n\n```crystal\nrequire \"friendly_id\"\nFriendlyId.configure do |config|\nconfig.migration_dir = \"db/migrations\"\nend\n```\n\nUpdate your model's save method to include the `generate_slug` method:\n\n```crystal\n\nclass Post\n  include FriendlyId::Slugged\n  friendly_id :title\n  # Model-level slug generation\n  def save\n  generate_slug  # Generate the slug before saving\n    @updated_at = Time.utc\n\n    if id\n      @@db.exec \u003c\u003c-SQL, title, slug, body, user_id, created_at, updated_at, id\n        UPDATE posts\n        SET title = ?, slug = ?, body = ?, user_id = ?, created_at = ?, updated_at = ?\n        WHERE id = ?\n      SQL\n    else\n      @@db.exec \u003c\u003c-SQL, title, slug, body, user_id, created_at, updated_at\n        INSERT INTO posts (title, slug, body, user_id, created_at, updated_at)\n        VALUES (?, ?, ?, ?, ?, ?)\n      SQL\n    end\n    self\n  end\nend\n\n```\n\nOr Update your controller save method to include the `generate_slug` method:\n\n```crystal\nclass PostsController\n  def create(env)\n    title = env.params.body[\"title\"]\n    body = env.params.body[\"body\"]\n    user_id = current_user(env).id\n\n    post = Post.new(\n      title: title,\n      body: body,\n      user_id: user_id\n    )\n\n    # Controller-level slug generation\n    post.generate_slug # Generate the slug before saving\n\n    if post.save\n      env.redirect \"/posts/#{post.slug}\"\n    else\n      env.redirect \"/posts/new\"\n    end\n  end\nend\n```\n\n## Usage\n\nBasic Slugging\n\n```crystal\nclass Post\n  include FriendlyId::Slugged\n  include FriendlyId::Finders\n\n  property id : Int64?\n  property title : String\n  property slug : String?\nend\n\npost = Post.new(\"Hello World!\")\npost.slug # =\u003e \"hello-world\"\n```\n\nThe Slug is Update Automatically\n\n```crystal\npost = Post.new(\"Hello World!\")\npost.save\nputs post.slug # =\u003e \"hello-world\"\n\npost.title = \"Updated Title\"\npost.save\nputs post.slug # =\u003e \"updated-title\"\n\n```\n\nWith History Tracking\n\n```crystal\nclass Post\n  include FriendlyId::Slugged\n  include FriendlyId::History\n\n  property id : Int64?\n  property title : String\n  property slug : String?\n\n  def initialize(@title)\n  end\nend\n\npost = Post.new(\"Hello World!\")\npost.save\npost.slug # =\u003e \"hello-world\"\n\npost.title = \"Updated Title\"\npost.save\npost.slug_history # =\u003e [\"hello-world\"]\n```\n\nUsing a Custom Attribute\n\n```crystal\nclass User\n  include FriendlyId::Slugged\n\n  property id : Int64?\n  property name : String\n  property slug : String?\n  friendly_id :name  # Use name instead of title for slugs\n\n  def initialize(@name); end\nend\n\nuser = User.new(\"John Doe\")\nuser.save\nputs user.slug # =\u003e \"john-doe\"\n```\n\n## Friendly ID Support\n\nThe `FriendlyId::Finders` module provides smart URL slug handling with ID and Historical Slug fallback:\n\n### lookup records by:\n\n- Current slug\n- Numeric ID\n- Historical slugs\n\n```crystal\nclass Post\n  include FriendlyId::Finders\nend\n\n```\n\nFinding Records\n\n```crystal\n# All these will work:\nPost.find_by_friendly_id(\"my-awesome-post\")  # Current slug\nPost.find_by_friendly_id(\"123\")              # ID\nPost.find_by_friendly_id(\"old-post-slug\")    # Historical slug\n# Regular find still works\npost = Post.find(1)\n```\n\n## Configuration\n\n```crystal\ndef should_generate_new_friendly_id?\n  title_changed? || slug.nil?\nend\n```\n\n```crystal\nclass Post\n  include DB::Serializable\n  include FriendlyId::Slugged\n  include FriendlyId::Finders\n  include FriendlyId::History\n\n  # ... your existing code ...\n\n  def should_generate_new_friendly_id?\n    title_changed? || slug.nil?\n  end\nend\n```\n\n### Custom Slug Generation\n\n```crystal\nclass Post\n  include FriendlyId::Slugged\n   def normalize_friendly_id(value)\n   value.downcase.gsub(/\\s+/, \"-\")\n  end\nend\n```\n\n## URL Helpers\n\nTo use friendly URLs in your controller, include the `FriendlyId::UrlHelper` module:\n\n```crystal\n# In your Controller\ninclude FriendlyId::UrlHelper\n```\n\n```crystal\n\u003ca href=\"/posts/\u003c%= friendly_path(post) %\u003e\"\u003e\n  \u003c%= post.title %\u003e\n\u003c/a\u003e\n```\n\n## Features\n\n- Slug generation from specified fields\n- SEO-friendly URL formatting\n- History tracking of slug changes\n- Custom slug normalization\n- Special character handling\n- Database-backed slug storage\n\nRun tests\n\n```crystal\ncrystal spec\n\n```\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\nContributors\n\nDaniel Calixto - creator and maintainer\n\n## License\n\nMIT License. See LICENSE for details.\n\n```\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdcalixto%2Ffriendly_id","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdcalixto%2Ffriendly_id","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdcalixto%2Ffriendly_id/lists"}