From 86cac606e01abe198459b26d1705b1a8618eb8fe Mon Sep 17 00:00:00 2001 From: isaacclad Date: Thu, 16 Jul 2026 20:12:14 +0000 Subject: [PATCH] Surface destination edits as the core dynamic short-link feature. Move destination editing to the top of the link page, clarify that the short URL never changes, prevent caches from pinning old redirects, and test that later destination updates take effect immediately. --- app/Http/Controllers/Link/LinkController.php | 6 +- .../Public/LinkRedirectController.php | 9 +- resources/views/links/create.blade.php | 1 + resources/views/links/show.blade.php | 98 +++++++++++-------- tests/Feature/LinkRedirectTest.php | 33 +++++++ 5 files changed, 105 insertions(+), 42 deletions(-) diff --git a/app/Http/Controllers/Link/LinkController.php b/app/Http/Controllers/Link/LinkController.php index 2a63e98..ed1fb26 100644 --- a/app/Http/Controllers/Link/LinkController.php +++ b/app/Http/Controllers/Link/LinkController.php @@ -116,7 +116,11 @@ class LinkController extends Controller return back()->withInput()->withErrors(['destination_url' => $e->getMessage()]); } - return back()->with('success', 'Link updated.'); + $message = isset($validated['destination_url']) + ? 'Destination updated. Your short link is unchanged — new clicks go to the new URL.' + : 'Link updated.'; + + return back()->with('success', $message); } public function destroy(ShortLink $link): RedirectResponse diff --git a/app/Http/Controllers/Public/LinkRedirectController.php b/app/Http/Controllers/Public/LinkRedirectController.php index 96820d6..11c87bb 100644 --- a/app/Http/Controllers/Public/LinkRedirectController.php +++ b/app/Http/Controllers/Public/LinkRedirectController.php @@ -21,9 +21,16 @@ class LinkRedirectController extends Controller $link = $this->links->resolve($request, $slug); if ($link !== null && $link->isDirectRedirect()) { + // Always read the current destination from the DB so edits take effect + // immediately. 302 + no-store so browsers/CDNs do not pin an old Location. $target = $link->resolveRedirectUrl($request, $path); - return redirect()->away($target, 302); + return redirect() + ->away($target, 302) + ->withHeaders([ + 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', + 'Pragma' => 'no-cache', + ]); } // Catalog / QR ecosystem slugs: proxy so destination stays live in the source app. diff --git a/resources/views/links/create.blade.php b/resources/views/links/create.blade.php index dd0660d..e6986b7 100644 --- a/resources/views/links/create.blade.php +++ b/resources/views/links/create.blade.php @@ -23,6 +23,7 @@ +

You can change this later. Your short link stays the same — only where it points updates.

@error('destination_url')

{{ $message }}

@enderror diff --git a/resources/views/links/show.blade.php b/resources/views/links/show.blade.php index 56761d2..ee35a26 100644 --- a/resources/views/links/show.blade.php +++ b/resources/views/links/show.blade.php @@ -6,12 +6,21 @@

{{ $link->label ?: $link->slug }}

{{ $link->sourceAppLabel() }} + @if ($link->is_managed_here) + Dynamic destination + @endif
- - {{ $link->publicUrl() }} - - +
+ + {{ $link->publicUrl() }} + + + +
+

+ Your short link never changes. Update the destination anytime — the next click goes to the new URL. +

@if ($link->is_managed_here)
@@ -28,7 +37,50 @@ @if (! $link->is_managed_here)
- This {{ \App\Models\ShortLink::publicHost() }} slug was created in {{ $link->sourceAppLabel() }}. Edit it there — changes sync back here automatically. + This {{ \App\Models\ShortLink::publicHost() }} slug was created in {{ $link->sourceAppLabel() }}. Edit the destination there — the short link stays the same and updates automatically. +
+ @endif + + @if ($link->is_managed_here) + + @csrf @method('PATCH') + +
+

Destination

+

+ Change where {{ $link->publicUrl() }} sends people. + Printed cards, QR codes, and old shares keep working. +

+
+ +
+ + + @error('destination_url')

{{ $message }}

@enderror + @error('link')

{{ $message }}

@enderror +
+ +
+ + +
+ + + + +
+ @else +
+

Destination

+

{{ $link->destination_url }}

@endif @@ -99,39 +151,5 @@ - - @if ($link->is_managed_here) -
- @csrf @method('PATCH') - -

Settings

- -
- - -
- -
- - -
- - - - -
- @else -
-

Destination

-

{{ $link->destination_url }}

-
- @endif diff --git a/tests/Feature/LinkRedirectTest.php b/tests/Feature/LinkRedirectTest.php index 3c3903d..ad7a432 100644 --- a/tests/Feature/LinkRedirectTest.php +++ b/tests/Feature/LinkRedirectTest.php @@ -92,6 +92,39 @@ class LinkRedirectTest extends TestCase $this->assertSame('go.example.test', ShortLink::publicHost()); } + public function test_changing_destination_later_redirects_to_new_url(): void + { + $user = $this->user(); + $link = ShortLink::create([ + 'user_id' => $user->id, + 'slug' => 'campaign', + 'source_app' => 'link', + 'source_kind' => 'redirect', + 'is_managed_here' => true, + 'destination_url' => 'https://example.com/old-landing', + 'is_active' => true, + ]); + + $this->get('https://ladl.link/campaign') + ->assertRedirect('https://example.com/old-landing'); + + // Destination edit is a first-class Link feature (not frozen at create time). + app(\App\Services\Link\LinkManagerService::class)->update($link, [ + 'destination_url' => 'https://example.com/new-landing', + 'label' => 'Spring campaign', + ]); + + $link->refresh(); + $this->assertSame('https://example.com/new-landing', $link->destination_url); + // Short slug stays put — only the destination changes. + $this->assertSame('campaign', $link->slug); + $this->assertSame('https://ladl.link/campaign', $link->publicUrl()); + + $response = $this->get('https://ladl.link/campaign'); + $response->assertRedirect('https://example.com/new-landing'); + $this->assertStringContainsString('no-store', (string) $response->headers->get('Cache-Control')); + } + public function test_catalog_qr_links_proxy_to_platform_instead_of_bad_destination(): void { $user = $this->user();