refact: remove /s for short link
This commit is contained in:
@@ -177,7 +177,7 @@ See [How to Trigger Sync](https://wr.do/docs/developer/sync) for details.
|
||||
- Discord: https://discord.gg/AHPQYuZu3m
|
||||
- 微信群:
|
||||
|
||||
<img width="300" src="https://wr.do/s/group" />
|
||||
<img width="300" src="https://wr.do/group" />
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ pnpm dev
|
||||
- Discord: https://discord.gg/AHPQYuZu3m
|
||||
- 微信群:
|
||||
|
||||
<img width="300" src="https://wr.do/s/group" />
|
||||
<img width="300" src="https://wr.do/group" />
|
||||
|
||||
## 贡献者
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ const RealtimeLogs = ({
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
className="text-sm font-semibold"
|
||||
href={`https://${loc.userUrl?.prefix}/s/${loc.userUrl?.url}`}
|
||||
href={`https://${loc.userUrl?.prefix}/${loc.userUrl?.url}`}
|
||||
target="_blank"
|
||||
>
|
||||
{loc.userUrl?.url}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import UserUrlsList from "./url-list";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Short URLs",
|
||||
description: "List and manage records.",
|
||||
title: "Links",
|
||||
description: "List and manage short links.",
|
||||
});
|
||||
|
||||
export default async function DashboardPage() {
|
||||
|
||||
@@ -345,7 +345,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
<TableCell className="col-span-1 flex items-center gap-1 sm:col-span-2">
|
||||
<Link
|
||||
className="overflow-hidden text-ellipsis whitespace-normal text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-400"
|
||||
href={`https://${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
href={`https://${short.prefix}/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
target="_blank"
|
||||
prefetch={false}
|
||||
title={short.url}
|
||||
@@ -353,7 +353,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
{short.url}
|
||||
</Link>
|
||||
<CopyButton
|
||||
value={`${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
value={`${short.prefix}/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
className={cn(
|
||||
"size-[25px]",
|
||||
"duration-250 transition-all group-hover:opacity-100",
|
||||
@@ -495,7 +495,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
className="overflow-hidden text-ellipsis whitespace-normal text-sm font-semibold text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-300"
|
||||
href={`https://${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
href={`https://${short.prefix}/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
target="_blank"
|
||||
prefetch={false}
|
||||
title={short.url}
|
||||
@@ -503,7 +503,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
{short.url}
|
||||
</Link>
|
||||
<CopyButton
|
||||
value={`${short.prefix}/s/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
value={`https://${short.prefix}/${short.url}${short.password ? `?password=${short.password}` : ""}`}
|
||||
className={cn(
|
||||
"size-[25px]",
|
||||
"duration-250 transition-all group-hover:opacity-100",
|
||||
@@ -759,7 +759,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
|
||||
{selectedUrl && (
|
||||
<QRCodeEditor
|
||||
user={{ id: user.id, apiKey: user.apiKey || "", team: user.team! }}
|
||||
url={`https://${selectedUrl.prefix}/s/${selectedUrl.url}`}
|
||||
url={`https://${selectedUrl.prefix}/${selectedUrl.url}`}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const orderedResults = ids.map((id) => {
|
||||
const item = dataMap.get(id);
|
||||
return item ? `${item.prefix}/s/${item.url}` : "";
|
||||
return item ? `${item.prefix}/${item.url}` : "";
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const orderedResults = ids.map((id) => {
|
||||
const item = dataMap.get(id);
|
||||
return item ? `${item.prefix}/s/${item.url}` : "";
|
||||
return item ? `${item.prefix}/${item.url}` : "";
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "1.1.4",
|
||||
"versionName": "1.1.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function PasswordPrompt() {
|
||||
e.preventDefault();
|
||||
const fullPassword = password.join("");
|
||||
if (slug && !isPending && fullPassword.length === 6) {
|
||||
router.push(`/s/${slug}?password=${encodeURIComponent(fullPassword)}`);
|
||||
router.push(`/${slug}?password=${encodeURIComponent(fullPassword)}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -295,7 +295,7 @@ export function UrlForm({
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
{t("A random url suffix")}. {t("Final url like")}
|
||||
「wr.do/s/suffix」
|
||||
「wr.do/suffix」
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function UrlShotenerExp() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-base font-bold text-gray-700 dark:text-gray-50">
|
||||
wr.do/s/try
|
||||
wr.do/try
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
<button className="rounded-full border p-1.5 transition-colors hover:bg-gray-100 dark:bg-gray-600/50">
|
||||
|
||||
@@ -66,44 +66,4 @@ docker pull ghcr.io/oiov/wr.do/wrdo:latest
|
||||
|
||||
## 打包镜像
|
||||
|
||||
Fork 此仓库后,在 Actions 中触发打包镜像。
|
||||
|
||||
## 付费部署服务
|
||||
|
||||
**联系方式:** 微信 `oiovdev`
|
||||
|
||||
本项目提供专业的代部署服务,根据不同需求灵活收费。
|
||||
|
||||
### 部署方案与价格
|
||||
|
||||
| 部署方式 | 配置说明 | 服务费用 |
|
||||
| --- | --- | --- |
|
||||
| Vercel | 应用托管 + 自建数据库 + 域名配置 | ¥500 |
|
||||
| Vercel | 应用托管 + Neon 云数据库 + 域名配置 | ¥400 |
|
||||
| Docker | 服务器部署 + 自建数据库 + 域名配置 | ¥500 |
|
||||
| Docker | 服务器部署 + Neon 云数据库 + 域名配置 | ¥450 |
|
||||
|
||||
### 重要说明
|
||||
|
||||
- **数据库要求:** 自建数据库需要准备服务器
|
||||
- **域名要求:** 至少准备一个域名,并且需要托管到 Cloudflare
|
||||
- **服务器与域名费用需另付:** 服务器购买和域名注册费用不包含在上述服务费中,建议您提前自行准备
|
||||
- **默认环境配置:** 服务器部署默认安装宝塔面板进行管理
|
||||
|
||||
|
||||
> 推荐云服务器 RackNerd,美国免备案vps,2核2G 配置仅需20.98$≈145RMB/年,支持支付宝付款,[💁点击优惠链接直达](https://wr.do/s/20u) 。
|
||||
|
||||
> 推荐域名注册商 [NameSilo](https://www.namesilo.com/?rid=50fae21ln), 新用户使用优惠码下单可以减 1$,优惠码: wrdo
|
||||
|
||||
### 准备工作
|
||||
|
||||
在开始部署前,请提前注册以下平台账户:
|
||||
|
||||
- **Cloudflare**:https://dash.cloudflare.com (必须,域名管理服务)
|
||||
- **Resend**:https://resend.com (必须,邮件发送服务)
|
||||
- **Vercel**:https://vercel.com (可选,应用部署平台)
|
||||
- **Neon**:https://neon.tech (可选,云数据库服务)
|
||||
|
||||
### 联系方式
|
||||
|
||||
如需部署服务,请添加微信 `oiovdev` 详细咨询,将根据您的具体需求提供定制化的部署方案。
|
||||
Fork 此仓库后,在 Actions 中触发打包镜像。
|
||||
@@ -66,43 +66,4 @@ Find the official image here: [container/wr.do](https://github.com/oiov/wr.do/pk
|
||||
|
||||
## Build Image
|
||||
|
||||
Fork this repository and trigger the build image action in Actions.
|
||||
|
||||
## Paid Deployment Service
|
||||
|
||||
**Contact:** WeChat `oiovdev`
|
||||
|
||||
This project offers professional deployment services with flexible pricing based on different requirements.
|
||||
|
||||
### Deployment Plans & Pricing
|
||||
|
||||
| Deployment Method | Configuration | Service Fee |
|
||||
| --- | --- | --- |
|
||||
| Vercel | App hosting + Self-hosted database + Domain configuration | ¥500 |
|
||||
| Vercel | App hosting + Neon cloud database + Domain configuration | ¥400 |
|
||||
| Docker | Server deployment + Self-hosted database + Domain configuration | ¥500 |
|
||||
| Docker | Server deployment + Neon cloud database + Domain configuration | ¥450 |
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Database Requirements:** Self-hosted database requires server preparation
|
||||
- **Domain Requirements:** At least one domain required, must be managed through Cloudflare
|
||||
- **Server & Domain Costs Not Included:** Server purchase and domain registration fees are not included in the above service fees. Please prepare them in advance
|
||||
- **Default Environment Configuration:** Server deployment includes aaPanel (BaoTa) installation by default for management
|
||||
|
||||
> **Recommended Cloud Server:** RackNerd, US-based VPS (no ICP filing required), 2 cores 2GB configuration for only $20.98≈¥145/year, supports Alipay payment. [💁Click here for discount link](https://wr.do/s/20u)
|
||||
|
||||
> **Recommended Domain Registrar:** [NameSilo](https://wr.do/s/domain), new users can save $1 with coupon code: **wrdo**
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Please register accounts on the following platforms before deployment:
|
||||
|
||||
- **Cloudflare**: https://dash.cloudflare.com (Required, domain management service)
|
||||
- **Resend**: https://resend.com (Required, email delivery service)
|
||||
- **Vercel**: https://vercel.com (Optional, app deployment platform)
|
||||
- **Neon**: https://neon.tech (Optional, cloud database service)
|
||||
|
||||
### Contact Information
|
||||
|
||||
For deployment services, please add WeChat `oiovdev` for detailed consultation. We will provide customized deployment solutions based on your specific needs.
|
||||
Fork this repository and trigger the build image action in Actions.
|
||||
@@ -21,20 +21,70 @@ const redirectMap = {
|
||||
"IncorrectPassword[0005]": "/password-prompt?error=1&slug=",
|
||||
};
|
||||
|
||||
const systemRoutes = [
|
||||
"/docs",
|
||||
"/dashboard",
|
||||
"/admin",
|
||||
"/pricing",
|
||||
"/plan",
|
||||
"/privacy",
|
||||
"/terms",
|
||||
"/auth",
|
||||
"/login",
|
||||
"/register",
|
||||
"/emails",
|
||||
"/link-status",
|
||||
"/password-prompt",
|
||||
"/chat",
|
||||
"/manifest.json",
|
||||
"/robots.txt",
|
||||
"/opengraph-image.jpg",
|
||||
"/favicon.ico",
|
||||
];
|
||||
|
||||
async function handleShortUrl(req: NextAuthRequest) {
|
||||
if (!req.url.includes("/s/")) return NextResponse.next();
|
||||
const url = new URL(req.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
const slug = extractSlug(req.url);
|
||||
if (!slug)
|
||||
return NextResponse.redirect(`${siteConfig.url}/docs/short-urls`, 302);
|
||||
const isSystemRoute = systemRoutes.some(
|
||||
(route) => pathname === route || pathname.startsWith(route + "/"),
|
||||
);
|
||||
|
||||
if (isSystemRoute || pathname === "/") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 兼容旧版 /s
|
||||
if (pathname.startsWith("/s/")) {
|
||||
const slug = extractSlug(req.url);
|
||||
const newUrl = new URL(`/${slug}`, siteConfig.url);
|
||||
url.searchParams.forEach((value, key) => {
|
||||
newUrl.searchParams.set(key, value);
|
||||
});
|
||||
return NextResponse.redirect(newUrl.toString(), 302);
|
||||
}
|
||||
|
||||
const slug = pathname.substring(1);
|
||||
|
||||
if (!slug || slug.includes("/")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
const slugRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
if (!slugRegex.test(slug)) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return await processShortUrl(req, slug, url);
|
||||
}
|
||||
|
||||
async function processShortUrl(req: NextAuthRequest, slug: string, url: URL) {
|
||||
const headers = req.headers;
|
||||
const ip = isVercel ? ipAddress(req) : extractRealIP(headers);
|
||||
const ua = getUserAgent(req);
|
||||
|
||||
const geo = await getGeolocation(req, ip || "::1");
|
||||
|
||||
const url = new URL(req.url);
|
||||
const password = url.searchParams.get("password") || "";
|
||||
|
||||
const trackingData = {
|
||||
@@ -100,7 +150,7 @@ async function handleShortUrl(req: NextAuthRequest) {
|
||||
}
|
||||
|
||||
function extractSlug(url: string): string | null {
|
||||
const match = url.match(/([^/?]+)(?:\?.*)?$/);
|
||||
const match = url.match(/\/s\/([^/?]+)(?:\?.*)?$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,46 +77,11 @@ const nextConfig = {
|
||||
destination: "/docs/developer/installation",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/0",
|
||||
destination: "https://wr.do/s/0",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/9",
|
||||
destination: "https://wr.do/s/9",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/ai",
|
||||
destination: "https://wr.do/s/ai?ref=wrdo",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/cps",
|
||||
destination: "https://wr.do/s/cps",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/x",
|
||||
destination: "https://wr.do/s/x",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/solo",
|
||||
destination: "https://wr.do/s/solo",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/rmbg",
|
||||
destination: "https://wr.do/s/rmbg",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/llk",
|
||||
destination: "https://wr.do/s/llk",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wr.do",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.5",
|
||||
"author": {
|
||||
"name": "oiov",
|
||||
"url": "https://github.com/oiov"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "1.1.4",
|
||||
"versionName": "1.1.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "1.1.4",
|
||||
"versionName": "1.1.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user