{"id":553,"date":"2025-08-31T10:56:56","date_gmt":"2025-08-31T10:56:56","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=553"},"modified":"2025-08-31T10:59:00","modified_gmt":"2025-08-31T10:59:00","slug":"how-to-generate-seo-friendly-urls-and-slugs-in-laravel","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/how-to-generate-seo-friendly-urls-and-slugs-in-laravel\/","title":{"rendered":"How to Generate SEO-Friendly URLs and Slugs in Laravel"},"content":{"rendered":"\n<p>Clean and descriptive URLs are essential for SEO. Instead of numeric IDs like <code>\/posts\/123<\/code>, you should use slugs like <code>\/posts\/my-first-laravel-app<\/code>. In this article, we\u2019ll explore how to generate slugs automatically, prevent duplicates, allow manual editing in forms, and integrate slugs into routes and controllers.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Auto-Generating Slugs with <code>booted()<\/code><\/strong><\/h2>\n\n\n\n<p>Laravel\u2019s model events make it easy to generate slugs whenever a record is created. You can use <code>booted()<\/code> with the <code>creating<\/code> event.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Post.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Model<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Str<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">Post<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Model<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">booted<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">static<\/span>::creating(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($post)<\/span> <\/span>{\n            $post-&gt;slug = Str::slug($post-&gt;title);\n        });\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Whenever a new post is created, its title is converted to a slug. For example: \u201cMy First Laravel App\u201d \u2192 <code>my-first-laravel-app<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Performance Note on <code>booted()<\/code><\/strong><\/h3>\n\n\n\n<p>The <code>booted()<\/code> method is called <strong>once per request<\/strong>, registering model event listeners. The slug logic itself only runs on the <code>creating<\/code> event (when saving new records). Simply retrieving or initializing a model does not regenerate the slug \u2014 this keeps performance safe.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Preventing Duplicate Slugs<\/strong><\/h2>\n\n\n\n<p>Duplicate slugs cause routing conflicts and SEO issues. For example, two posts titled \u201cHello World\u201d would both generate <code>hello-world<\/code>. A better approach is to check existing slugs and append a number if needed.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Post.php<\/span>\n<span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">booted<\/span><span class=\"hljs-params\">()<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">static<\/span>::creating(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($post)<\/span> <\/span>{\n        $base = Str::slug($post-&gt;title);\n\n        <span class=\"hljs-comment\">\/\/ If slug is free, use it directly<\/span>\n        <span class=\"hljs-keyword\">if<\/span> (! <span class=\"hljs-keyword\">static<\/span>::where(<span class=\"hljs-string\">'slug'<\/span>, $base)-&gt;exists()) {\n            $post-&gt;slug = $base;\n            <span class=\"hljs-keyword\">return<\/span>;\n        }\n\n        <span class=\"hljs-comment\">\/\/ Otherwise compute next suffix<\/span>\n        $pattern = <span class=\"hljs-string\">'^'<\/span> . preg_quote($base, <span class=\"hljs-string\">'\/'<\/span>) . <span class=\"hljs-string\">'(-&#91;0-9]+)?$'<\/span>;\n\n        $maxSuffix = <span class=\"hljs-keyword\">static<\/span>::whereRaw(<span class=\"hljs-string\">'slug REGEXP ?'<\/span>, &#91;$pattern])\n            -&gt;selectRaw(<span class=\"hljs-string\">\"\n                MAX(\n                    CASE\n                        WHEN slug = ? THEN 0\n                        ELSE CAST(SUBSTRING_INDEX(slug, '-', -1) AS UNSIGNED)\n                    END\n                ) AS max_suffix\n            \"<\/span>, &#91;$base])\n            -&gt;value(<span class=\"hljs-string\">'max_suffix'<\/span>);\n\n        $post-&gt;slug = $base . <span class=\"hljs-string\">'-'<\/span> . (((int) $maxSuffix) + <span class=\"hljs-number\">1<\/span>);\n    });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>With this strategy, your slugs remain unique:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>hello-world<\/code><\/li>\n\n\n\n<li><code>hello-world-1<\/code><\/li>\n\n\n\n<li><code>hello-world-2<\/code><\/li>\n<\/ul>\n\n\n\n<p>For extra safety, add a unique index on the <code>slug<\/code> column at the database level. This prevents race conditions under high concurrency.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">$table-&gt;string(<span class=\"hljs-string\">'slug'<\/span>)-&gt;unique();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Using Slugs in Routes and Controllers<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">PostController<\/span>;\n\nRoute::get(<span class=\"hljs-string\">'\/posts\/{post:slug}'<\/span>, &#91;PostController::class, <span class=\"hljs-string\">'show'<\/span>]);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/PostController.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Post<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PostController<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Controller<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">show<\/span><span class=\"hljs-params\">(Post $post)<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> view(<span class=\"hljs-string\">'posts.show'<\/span>, compact(<span class=\"hljs-string\">'post'<\/span>));\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>By using <code>{post:slug}<\/code>, Laravel automatically looks up posts by their slug instead of their ID.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Allowing Manual Slug Editing in Forms<\/strong><\/h2>\n\n\n\n<p>Sometimes you want editors to override the auto-generated slug. Add a <code>slug<\/code> field in your form.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;form action=<span class=\"hljs-string\">\"{{ route('posts.store') }}\"<\/span> method=<span class=\"hljs-string\">\"POST\"<\/span>&gt;\n  @csrf\n  &lt;label&gt;Title&lt;\/label&gt;\n  &lt;input type=<span class=\"hljs-string\">\"text\"<\/span> name=<span class=\"hljs-string\">\"title\"<\/span> id=<span class=\"hljs-string\">\"title\"<\/span>&gt;\n\n  &lt;label&gt;Slug (optional)&lt;\/label&gt;\n  &lt;input type=<span class=\"hljs-string\">\"text\"<\/span> name=<span class=\"hljs-string\">\"slug\"<\/span> id=<span class=\"hljs-string\">\"slug\"<\/span>&gt;\n\n  &lt;button type=<span class=\"hljs-string\">\"submit\"<\/span>&gt;Save&lt;\/button&gt;\n&lt;\/form&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In your model\u2019s <code>creating<\/code> event, only auto-generate the slug if it wasn\u2019t provided:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">empty<\/span>($post-&gt;slug)) {\n    $post-&gt;slug = Str::slug($post-&gt;title);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>UI Enhancement: Slug Preview with JavaScript<\/strong><\/h2>\n\n\n\n<p>For better UX, you can show a live preview of the slug as the user types the title.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">&lt;script&gt;\n<span class=\"hljs-built_in\">document<\/span>.getElementById(<span class=\"hljs-string\">'title'<\/span>).addEventListener(<span class=\"hljs-string\">'input'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> (<span class=\"hljs-params\"><\/span>) <\/span>{\n    <span class=\"hljs-keyword\">let<\/span> slug = <span class=\"hljs-keyword\">this<\/span>.value.toLowerCase()\n        .replace(<span class=\"hljs-regexp\">\/&#91;^a-z0-9]+\/g<\/span>, <span class=\"hljs-string\">'-'<\/span>)\n        .replace(<span class=\"hljs-regexp\">\/(^-|-$)\/g<\/span>, <span class=\"hljs-string\">''<\/span>);\n    <span class=\"hljs-built_in\">document<\/span>.getElementById(<span class=\"hljs-string\">'slug'<\/span>).value = slug;\n});\n&lt;<span class=\"hljs-regexp\">\/script&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This script converts the title into a slug format on the fly and updates the slug input field.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Best Practices for Slugs<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Keep them short:<\/strong> Ideally under 60 characters.<\/li>\n\n\n\n<li><strong>Use lowercase only:<\/strong> Prevents duplicate variations.<\/li>\n\n\n\n<li><strong>Avoid stop words:<\/strong> Remove words like \u201cand,\u201d \u201cthe,\u201d \u201cof\u201d unless needed.<\/li>\n\n\n\n<li><strong>Hyphens not underscores:<\/strong> Use <code>-<\/code> as Google prefers it over <code>_<\/code>.<\/li>\n\n\n\n<li><strong>Unique per resource:<\/strong> Always enforce uniqueness in DB with a unique index.<\/li>\n\n\n\n<li><strong>Match content keywords:<\/strong> Include relevant keywords for better SEO ranking.<\/li>\n<\/ul>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Good vs Bad Slug Examples<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table is-style-stripes\"><table><thead><tr><th>Bad Slug<\/th><th>Good Slug<\/th><th>Why<\/th><\/tr><\/thead><tbody><tr><td><code>Post_123!!!<\/code><\/td><td><code>my-first-laravel-app<\/code><\/td><td>Readable, keyword-rich, clean format.<\/td><\/tr><tr><td><code>My First Laravel App<\/code><\/td><td><code>my-first-laravel-app<\/code><\/td><td>Lowercase + hyphens instead of spaces.<\/td><\/tr><tr><td><code>laravel---guide<\/code><\/td><td><code>laravel-guide<\/code><\/td><td>No unnecessary dashes.<\/td><\/tr><tr><td><code>the-best-guide-to-laravel-php-framework<\/code><\/td><td><code>laravel-guide<\/code><\/td><td>Shorter and more focused on keywords.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>This table shows how simple adjustments make slugs much cleaner for both SEO and users.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Wrapping Up<\/strong><\/h2>\n\n\n\n<p>SEO-friendly slugs make your URLs readable, keyword-rich, and unique. You learned how to auto-generate slugs, prevent duplicates with an efficient strategy, allow manual overrides in forms, preview slugs in the UI, follow best practices, and identify good vs bad slug patterns. Combined with clean routes, this gives your Laravel app a strong SEO foundation.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What\u2019s Next<\/strong><\/h2>\n\n\n\n<p>Continue improving your Laravel SEO setup with these related guides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/laravel-seo-guide-optimizing-meta-slugs-and-sitemaps\">Laravel SEO Guide: Optimizing Meta, Slugs, and Sitemaps<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/adding-meta-tags-and-open-graph-data-dynamically-in-laravel\">Adding Meta Tags and Open Graph Data Dynamically in Laravel<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/how-to-build-an-xml-sitemap-generator-in-laravel\">How to Build an XML Sitemap Generator in Laravel<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Clean and descriptive URLs are essential for SEO. Instead of numeric IDs like \/posts\/123, you should use slugs like \/posts\/my-first-laravel-app. In this article, we\u2019ll explore how to generate slugs automatically, prevent duplicates, allow manual editing in forms, and integrate slugs into routes and controllers. Auto-Generating Slugs with booted() Laravel\u2019s model events make it easy to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":557,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[115,114,109,111,113],"class_list":["post-553","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-forms","tag-routing","tag-seo","tag-slugs","tag-urls"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/553","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=553"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/553\/revisions"}],"predecessor-version":[{"id":556,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/553\/revisions\/556"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/557"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=553"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=553"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=553"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}