Make Ladill Link short URLs fully dynamic at click time.
Deploy Ladill Link / deploy (push) Successful in 1m55s
Deploy Ladill Link / deploy (push) Successful in 1m55s
Forward query strings and path suffixes to the live destination, resolve public hosts from config/custom domains instead of hardcoding ladl.link, and keep proxy Location rewrites on the visitor host.
This commit is contained in:
@@ -41,6 +41,8 @@ class LinkController extends Controller
|
|||||||
'canCreate' => $this->billing->canCreate($account),
|
'canCreate' => $this->billing->canCreate($account),
|
||||||
'pricePerLink' => \App\Models\LinkWallet::pricePerLink(),
|
'pricePerLink' => \App\Models\LinkWallet::pricePerLink(),
|
||||||
'topupUrl' => ladill_account_url('wallet'),
|
'topupUrl' => ladill_account_url('wallet'),
|
||||||
|
'publicHost' => parse_url($account->publicLinkBaseUrl(), PHP_URL_HOST) ?: ShortLink::publicHost(),
|
||||||
|
'publicBaseUrl' => $account->publicLinkBaseUrl(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +53,8 @@ class LinkController extends Controller
|
|||||||
return view('links.create', [
|
return view('links.create', [
|
||||||
'canCreate' => $this->billing->canCreate($account),
|
'canCreate' => $this->billing->canCreate($account),
|
||||||
'pricePerLink' => \App\Models\LinkWallet::pricePerLink(),
|
'pricePerLink' => \App\Models\LinkWallet::pricePerLink(),
|
||||||
|
'publicHost' => parse_url($account->publicLinkBaseUrl(), PHP_URL_HOST) ?: ShortLink::publicHost(),
|
||||||
|
'publicBaseUrl' => $account->publicLinkBaseUrl(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class OverviewController extends Controller
|
|||||||
'wallet' => $wallet,
|
'wallet' => $wallet,
|
||||||
'recentLinks' => $recentLinks,
|
'recentLinks' => $recentLinks,
|
||||||
'pricePerLink' => \App\Models\LinkWallet::pricePerLink(),
|
'pricePerLink' => \App\Models\LinkWallet::pricePerLink(),
|
||||||
|
'publicHost' => parse_url($account->publicLinkBaseUrl(), PHP_URL_HOST) ?: ShortLink::publicHost(),
|
||||||
|
'publicBaseUrl' => $account->publicLinkBaseUrl(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ class SettingsController extends Controller
|
|||||||
if ($validated['domain'] === 'platform') {
|
if ($validated['domain'] === 'platform') {
|
||||||
LinkCustomDomain::where('user_id', $account->id)->update(['is_default' => false]);
|
LinkCustomDomain::where('user_id', $account->id)->update(['is_default' => false]);
|
||||||
|
|
||||||
return back()->with('success', 'Default short-link domain set to ladl.link.');
|
$domain = (string) config('link.public_domain', 'ladl.link');
|
||||||
|
|
||||||
|
return back()->with('success', 'Default short-link domain set to '.$domain.'.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$domain = LinkCustomDomain::query()
|
$domain = LinkCustomDomain::query()
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Public;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\Link\LinkManagerService;
|
use App\Services\Link\LinkManagerService;
|
||||||
use App\Services\Link\LinkPlatformProxy;
|
use App\Services\Link\LinkPlatformProxy;
|
||||||
use App\Support\LadillLink;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -19,13 +18,15 @@ class LinkRedirectController extends Controller
|
|||||||
|
|
||||||
public function resolve(Request $request, string $slug, ?string $path = null): RedirectResponse|Response
|
public function resolve(Request $request, string $slug, ?string $path = null): RedirectResponse|Response
|
||||||
{
|
{
|
||||||
if ($path === null || $path === '') {
|
$link = $this->links->resolve($request, $slug);
|
||||||
$link = $this->links->resolve($request, $slug);
|
|
||||||
if ($link !== null && $link->isDirectRedirect()) {
|
if ($link !== null && $link->isDirectRedirect()) {
|
||||||
return redirect()->away($link->destination_url, 302);
|
$target = $link->resolveRedirectUrl($request, $path);
|
||||||
}
|
|
||||||
|
return redirect()->away($target, 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog / QR ecosystem slugs: proxy so destination stays live in the source app.
|
||||||
return $this->platform->forward($request, $slug, $path);
|
return $this->platform->forward($request, $slug, $path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ShortLink extends Model
|
class ShortLink extends Model
|
||||||
{
|
{
|
||||||
@@ -44,6 +45,10 @@ class ShortLink extends Model
|
|||||||
return $this->hasMany(LinkClick::class);
|
return $this->hasMany(LinkClick::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shareable short URL for this slug — always derived at runtime from the
|
||||||
|
* owner's default domain (custom or platform), never stored.
|
||||||
|
*/
|
||||||
public function publicUrl(?User $owner = null): string
|
public function publicUrl(?User $owner = null): string
|
||||||
{
|
{
|
||||||
$owner ??= $this->user;
|
$owner ??= $this->user;
|
||||||
@@ -52,11 +57,74 @@ class ShortLink extends Model
|
|||||||
return rtrim($base, '/').'/'.$this->slug;
|
return rtrim($base, '/').'/'.$this->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function publicHost(): string
|
||||||
|
{
|
||||||
|
return strtolower((string) config('link.public_domain', 'ladl.link'));
|
||||||
|
}
|
||||||
|
|
||||||
public static function publicBaseUrl(): string
|
public static function publicBaseUrl(): string
|
||||||
{
|
{
|
||||||
$domain = (string) config('link.public_domain', 'ladl.link');
|
return 'https://'.self::publicHost();
|
||||||
|
}
|
||||||
|
|
||||||
return 'https://'.$domain;
|
/**
|
||||||
|
* Build the live redirect target: current destination_url plus any path
|
||||||
|
* suffix and query string from the inbound request (query keys on the
|
||||||
|
* short link override destination keys when both are set).
|
||||||
|
*/
|
||||||
|
public function resolveRedirectUrl(Request $request, ?string $path = null): string
|
||||||
|
{
|
||||||
|
$base = trim((string) $this->destination_url);
|
||||||
|
if ($base === '') {
|
||||||
|
return $this->publicUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
$suffix = $path !== null ? trim($path, '/') : '';
|
||||||
|
if ($suffix !== '') {
|
||||||
|
$parsed = parse_url($base);
|
||||||
|
$basePath = rtrim((string) ($parsed['path'] ?? ''), '/');
|
||||||
|
$rebuilt = ($parsed['scheme'] ?? 'https').'://'.($parsed['host'] ?? '');
|
||||||
|
if (! empty($parsed['port'])) {
|
||||||
|
$rebuilt .= ':'.$parsed['port'];
|
||||||
|
}
|
||||||
|
$rebuilt .= $basePath.'/'.$suffix;
|
||||||
|
if (! empty($parsed['query'])) {
|
||||||
|
$rebuilt .= '?'.$parsed['query'];
|
||||||
|
}
|
||||||
|
if (! empty($parsed['fragment'])) {
|
||||||
|
$rebuilt .= '#'.$parsed['fragment'];
|
||||||
|
}
|
||||||
|
$base = $rebuilt;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestQuery = $request->query();
|
||||||
|
if ($requestQuery === []) {
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = parse_url($base);
|
||||||
|
$existing = [];
|
||||||
|
if (! empty($parts['query'])) {
|
||||||
|
parse_str((string) $parts['query'], $existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inbound short-link params win so campaigns can override destination defaults.
|
||||||
|
$merged = array_merge($existing, $requestQuery);
|
||||||
|
$query = http_build_query($merged);
|
||||||
|
|
||||||
|
$target = ($parts['scheme'] ?? 'https').'://'.($parts['host'] ?? '');
|
||||||
|
if (! empty($parts['port'])) {
|
||||||
|
$target .= ':'.$parts['port'];
|
||||||
|
}
|
||||||
|
$target .= $parts['path'] ?? '';
|
||||||
|
if ($query !== '') {
|
||||||
|
$target .= '?'.$query;
|
||||||
|
}
|
||||||
|
if (! empty($parts['fragment'])) {
|
||||||
|
$target .= '#'.$parts['fragment'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $target;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isExpired(): bool
|
public function isExpired(): bool
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class AfiaService
|
|||||||
What Ladill Link does and where things live:
|
What Ladill Link does and where things live:
|
||||||
- Overview (Dashboard): link count, recent clicks, and wallet balance.
|
- Overview (Dashboard): link count, recent clicks, and wallet balance.
|
||||||
- My Links: create, edit, enable/disable, and delete short links. Each new link debits a small fee from the Ladill wallet (see Settings or the create screen for the current price).
|
- My Links: create, edit, enable/disable, and delete short links. Each new link debits a small fee from the Ladill wallet (see Settings or the create screen for the current price).
|
||||||
- Public URLs: default domain is ladl.link/{slug}. Links can also use a verified custom domain from Custom Domains.
|
- Public URLs: default domain is configured (usually ladl.link/{slug}) and always resolved live — custom domains from Settings replace it when set as default.
|
||||||
- Analytics: click totals and trends across links.
|
- Analytics: click totals and trends across links.
|
||||||
- Custom Domains: connect a domain you own, verify DNS, and set a default domain for new links.
|
- Custom Domains: connect a domain you own, verify DNS, and set a default domain for new links.
|
||||||
- Settings: default domain preference and account shortcuts.
|
- Settings: default domain preference and account shortcuts.
|
||||||
|
|||||||
@@ -103,11 +103,11 @@ class LinkCustomDomainService
|
|||||||
public function isAppHost(string $host): bool
|
public function isAppHost(string $host): bool
|
||||||
{
|
{
|
||||||
$host = preg_replace('/^www\./', '', strtolower(trim($host)));
|
$host = preg_replace('/^www\./', '', strtolower(trim($host)));
|
||||||
|
$public = (string) config('customdomain.public_host', config('link.public_domain', 'ladl.link'));
|
||||||
$known = array_filter([
|
$known = array_filter([
|
||||||
(string) config('customdomain.app_host'),
|
(string) config('customdomain.app_host'),
|
||||||
(string) config('customdomain.public_host'),
|
$public,
|
||||||
'ladl.link',
|
'www.'.$public,
|
||||||
'www.ladl.link',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return in_array($host, $known, true);
|
return in_array($host, $known, true);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class LinkPlatformProxy
|
|||||||
return $followUp;
|
return $followUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->headers->set('Location', $this->rewriteLocation($location, $slug));
|
$response->headers->set('Location', $this->rewriteLocation($request, $location, $slug));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +97,7 @@ class LinkPlatformProxy
|
|||||||
}
|
}
|
||||||
|
|
||||||
$host = strtolower((string) ($parsed['host'] ?? ''));
|
$host = strtolower((string) ($parsed['host'] ?? ''));
|
||||||
$linkHost = strtolower((string) parse_url(LadillLink::baseUrl(), PHP_URL_HOST));
|
if (! $this->isLinkPublicHost($host, $request)) {
|
||||||
if ($host !== $linkHost) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +110,18 @@ class LinkPlatformProxy
|
|||||||
return $this->forward($request, $slug, substr($path, strlen($slugPrefix)));
|
return $this->forward($request, $slug, substr($path, strlen($slugPrefix)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isLinkPublicHost(string $host, Request $request): bool
|
||||||
|
{
|
||||||
|
$host = preg_replace('/^www\./', '', strtolower($host)) ?: $host;
|
||||||
|
$candidates = array_filter([
|
||||||
|
strtolower((string) parse_url(LadillLink::baseUrl(), PHP_URL_HOST)),
|
||||||
|
strtolower((string) config('link.public_domain', 'ladl.link')),
|
||||||
|
strtolower((string) $request->getHost()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return in_array($host, $candidates, true);
|
||||||
|
}
|
||||||
|
|
||||||
private function isEventsRegistrationPath(string $path): bool
|
private function isEventsRegistrationPath(string $path): bool
|
||||||
{
|
{
|
||||||
return $path === 'register'
|
return $path === 'register'
|
||||||
@@ -179,14 +190,16 @@ class LinkPlatformProxy
|
|||||||
return $flat;
|
return $flat;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function rewriteLocation(string $location, string $slug): string
|
private function rewriteLocation(Request $request, string $location, string $slug): string
|
||||||
{
|
{
|
||||||
|
$publicBase = $this->publicBaseForRequest($request);
|
||||||
|
|
||||||
foreach ([$this->platformBaseUrl(), $this->eventsBaseUrl()] as $base) {
|
foreach ([$this->platformBaseUrl(), $this->eventsBaseUrl()] as $base) {
|
||||||
$prefix = $base.'/q/'.$slug;
|
$prefix = $base.'/q/'.$slug;
|
||||||
if (str_starts_with($location, $prefix)) {
|
if (str_starts_with($location, $prefix)) {
|
||||||
$suffix = substr($location, strlen($prefix));
|
$suffix = substr($location, strlen($prefix));
|
||||||
|
|
||||||
return LadillLink::url($slug).$suffix;
|
return rtrim($publicBase, '/').'/'.$slug.$suffix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,9 +207,28 @@ class LinkPlatformProxy
|
|||||||
if (str_starts_with($location, $transfer)) {
|
if (str_starts_with($location, $transfer)) {
|
||||||
$suffix = substr($location, strlen($transfer));
|
$suffix = substr($location, strlen($transfer));
|
||||||
|
|
||||||
return LadillLink::url($slug).$suffix;
|
return rtrim($publicBase, '/').'/'.$slug.$suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $location;
|
return $location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Keep the visitor on the host they used (custom domain or platform public domain). */
|
||||||
|
private function publicBaseForRequest(Request $request): string
|
||||||
|
{
|
||||||
|
$host = strtolower((string) $request->getHost());
|
||||||
|
$appHost = strtolower((string) (
|
||||||
|
config('customdomain.app_host')
|
||||||
|
?: (parse_url((string) config('app.url'), PHP_URL_HOST) ?: '')
|
||||||
|
));
|
||||||
|
|
||||||
|
// Dashboard host (link.ladill.com) is not a public short-link host.
|
||||||
|
if ($host === '' || $host === $appHost) {
|
||||||
|
return LadillLink::baseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = $request->getScheme() ?: 'https';
|
||||||
|
|
||||||
|
return $scheme.'://'.$host;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonical short-link URLs for the Ladill platform (ladl.link).
|
* Canonical short-link URLs for the Ladill platform.
|
||||||
|
* Domain is always read from config (LINK_PUBLIC_DOMAIN) — never hard-coded.
|
||||||
*/
|
*/
|
||||||
final class LadillLink
|
final class LadillLink
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<x-user-layout>
|
<x-user-layout>
|
||||||
<x-slot name="title">Create Link</x-slot>
|
<x-slot name="title">Create Link</x-slot>
|
||||||
<div class="mx-auto max-w-xl space-y-6">
|
<div class="mx-auto max-w-xl space-y-6" x-data="{ slug: @js(old('custom_slug', '')) }">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold text-slate-900">Create short link</h1>
|
<h1 class="text-2xl font-semibold text-slate-900">Create short link</h1>
|
||||||
<p class="mt-1 text-sm text-slate-500">
|
<p class="mt-1 text-sm text-slate-500">
|
||||||
Your link will be published on <span class="font-medium text-emerald-700">ladl.link</span>.
|
Your link will be published on <span class="font-medium text-emerald-700">{{ $publicHost }}</span>.
|
||||||
GHS {{ number_format($pricePerLink, 2) }} will be debited from your wallet.
|
GHS {{ number_format($pricePerLink, 2) }} will be debited from your wallet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,11 +35,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<label for="custom_slug" class="block text-sm font-medium text-slate-700">Custom slug (optional)</label>
|
<label for="custom_slug" class="block text-sm font-medium text-slate-700">Custom slug (optional)</label>
|
||||||
<div class="mt-1 flex rounded-lg shadow-sm">
|
<div class="mt-1 flex rounded-lg shadow-sm">
|
||||||
<span class="inline-flex items-center rounded-l-lg border border-r-0 border-slate-300 bg-slate-50 px-3 text-sm text-slate-500">ladl.link/</span>
|
<span class="inline-flex items-center rounded-l-lg border border-r-0 border-slate-300 bg-slate-50 px-3 text-sm text-slate-500">{{ $publicHost }}/</span>
|
||||||
<input type="text" name="custom_slug" id="custom_slug" value="{{ old('custom_slug') }}"
|
<input type="text" name="custom_slug" id="custom_slug" value="{{ old('custom_slug') }}"
|
||||||
pattern="[a-z0-9][a-z0-9-]{1,18}[a-z0-9]"
|
pattern="[a-z0-9][a-z0-9-]{1,18}[a-z0-9]"
|
||||||
|
x-model="slug"
|
||||||
class="block w-full rounded-r-lg border-slate-300 focus:border-emerald-500 focus:ring-emerald-500">
|
class="block w-full rounded-r-lg border-slate-300 focus:border-emerald-500 focus:ring-emerald-500">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-1.5 text-xs text-slate-400" x-show="slug" x-cloak>
|
||||||
|
Preview: <span class="font-medium text-emerald-700" x-text="'{{ $publicBaseUrl }}/' + slug"></span>
|
||||||
|
</p>
|
||||||
@error('custom_slug')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
@error('custom_slug')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<h1 class="truncate text-lg font-semibold text-slate-900 lg:text-2xl">Overview</h1>
|
<h1 class="truncate text-lg font-semibold text-slate-900 lg:text-2xl">Overview</h1>
|
||||||
<p class="mt-1 hidden text-sm text-slate-500 sm:block">Short links on <span class="font-medium text-emerald-700">ladl.link</span> — billed from your Ladill wallet</p>
|
<p class="mt-1 hidden text-sm text-slate-500 sm:block">Short links on <span class="font-medium text-emerald-700">{{ $publicHost }}</span> — billed from your Ladill wallet</p>
|
||||||
</div>
|
</div>
|
||||||
@include('partials.mobile-header-btn', [
|
@include('partials.mobile-header-btn', [
|
||||||
'href' => route('user.links.create'),
|
'href' => route('user.links.create'),
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div class="max-w-xl">
|
<div class="max-w-xl">
|
||||||
<div class="inline-flex items-center gap-1.5 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700">
|
<div class="inline-flex items-center gap-1.5 rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700">
|
||||||
Short links · Click analytics · ladl.link
|
Short links · Click analytics · {{ $publicHost }}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="mt-3 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">My Links</h1>
|
<h1 class="mt-3 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">My Links</h1>
|
||||||
<p class="mt-2 text-sm leading-6 text-slate-600">
|
<p class="mt-2 text-sm leading-6 text-slate-600">
|
||||||
Create branded short URLs on ladl.link, track clicks, and manage destinations from one place.
|
Create branded short URLs on {{ $publicHost }}, track clicks, and manage destinations from one place.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@if($links->isEmpty())
|
@if($links->isEmpty())
|
||||||
<div class="px-6 py-12 text-center">
|
<div class="px-6 py-12 text-center">
|
||||||
<p class="text-sm text-slate-500">No links yet. Create your first short link on ladl.link.</p>
|
<p class="text-sm text-slate-500">No links yet. Create your first short link on {{ $publicHost }}.</p>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<table class="min-w-full divide-y divide-slate-100">
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
@if (! $link->is_managed_here)
|
@if (! $link->is_managed_here)
|
||||||
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||||
This ladl.link 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 it there — changes sync back here automatically.
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,59 @@ class LinkRedirectTest extends TestCase
|
|||||||
->assertRedirect('https://example.com/target');
|
->assertRedirect('https://example.com/target');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_direct_redirect_forwards_query_string_dynamically(): void
|
||||||
|
{
|
||||||
|
$user = $this->user();
|
||||||
|
ShortLink::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'slug' => 'promo',
|
||||||
|
'source_app' => 'link',
|
||||||
|
'source_kind' => 'redirect',
|
||||||
|
'is_managed_here' => true,
|
||||||
|
'destination_url' => 'https://example.com/landing?ref=home',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get('https://ladl.link/promo?utm_source=ig&ref=campaign')
|
||||||
|
->assertRedirect('https://example.com/landing?ref=campaign&utm_source=ig');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_direct_redirect_appends_path_suffix_dynamically(): void
|
||||||
|
{
|
||||||
|
$user = $this->user();
|
||||||
|
ShortLink::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'slug' => 'docs',
|
||||||
|
'source_app' => 'link',
|
||||||
|
'source_kind' => 'redirect',
|
||||||
|
'is_managed_here' => true,
|
||||||
|
'destination_url' => 'https://example.com/help',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get('https://ladl.link/docs/getting-started?src=nav')
|
||||||
|
->assertRedirect('https://example.com/help/getting-started?src=nav');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_public_url_uses_config_domain_dynamically(): void
|
||||||
|
{
|
||||||
|
config(['link.public_domain' => 'go.example.test']);
|
||||||
|
|
||||||
|
$user = $this->user();
|
||||||
|
$link = ShortLink::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'slug' => 'branded',
|
||||||
|
'source_app' => 'link',
|
||||||
|
'source_kind' => 'redirect',
|
||||||
|
'is_managed_here' => true,
|
||||||
|
'destination_url' => 'https://example.com',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('https://go.example.test/branded', $link->publicUrl());
|
||||||
|
$this->assertSame('go.example.test', ShortLink::publicHost());
|
||||||
|
}
|
||||||
|
|
||||||
public function test_catalog_qr_links_proxy_to_platform_instead_of_bad_destination(): void
|
public function test_catalog_qr_links_proxy_to_platform_instead_of_bad_destination(): void
|
||||||
{
|
{
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
|||||||
Reference in New Issue
Block a user