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();