Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,41 @@ import { getMetaTags } from "./utils";

export const runtime = "edge";

const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
};

export async function GET(req: NextRequest) {
try {
const origin = req.headers.get("origin");
// Validate the origin header and set CORS headers accordingly
const corsHeaders = {
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type",
};

if (origin && origin.endsWith(".dub.co")) {
corsHeaders["Access-Control-Allow-Origin"] = origin;
}

// Validate URL parameter
const { url } = getUrlQuerySchema.parse({
url: req.nextUrl.searchParams.get("url"),
});

// Rate limit by IP
await ratelimitOrThrow(req, "metatags");

// Get metatags
const metatags = await getMetaTags(url);

// Return response
return NextResponse.json(
{
...metatags,
poweredBy: "Dub.co - Link management for modern marketing teams",
},
{
headers: CORS_HEADERS,
headers: corsHeaders,
},
);
} catch (error) {
return handleAndReturnErrorResponse(error, CORS_HEADERS);
return handleAndReturnErrorResponse(error);
}
}

export function OPTIONS() {
return new Response(null, {
status: 204,
headers: CORS_HEADERS,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,36 @@ import he from "he";
import { parse } from "node-html-parser";

export const getHtml = async (url: string) => {
return await fetchWithTimeout(url, {
headers: {
"User-Agent": "Dub.co Metatags API (https://api.dub.co/metatags)",
},
})
.then((r) => r.text())
.catch(() => null);
try {
const response = await fetchWithTimeout(url);

if (!response.ok) {
// If we get a 406 or other error, check if it's a Cloudflare-protected site
const isCloudflare = response.headers.get("server") === "cloudflare";
if (isCloudflare) {
console.warn(`Cloudflare-protected site detected: ${url}`);
return null;
}
console.error(`HTTP error! status: ${response.status} for URL: ${url}`);
return null;
}

const text = await response.text();

// Check if the response contains Cloudflare's challenge page
if (
text.includes("challenge-platform") ||
text.includes("cf-browser-verification")
) {
console.warn(`Cloudflare challenge page detected for: ${url}`);
return null;
}

return text;
} catch (error) {
console.error(`Error fetching ${url}:`, error);
return null;
}
};

export const getHeadChildNodes = (html) => {
Expand Down Expand Up @@ -47,15 +70,42 @@ export const getRelativeUrl = (url: string, imageUrl: string) => {
return new URL(imageUrl, baseURL).toString();
};

export const getMetaTags = async (url: string) => {
const html = await getHtml(url);
if (!html) {
const generateFallbackMetadata = (url: string) => {
try {
const parsedUrl = new URL(url);
const hostname = parsedUrl.hostname;
const path = parsedUrl.pathname;

// Clean up the path for title
const pathParts = path.split("/").filter(Boolean);
const lastPathPart = pathParts[pathParts.length - 1] || "";
const formattedPath = lastPathPart
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");

return {
title: formattedPath || hostname.replace(/^www\./, ""),
description: `Visit ${hostname}${path}`,
image: null,
};
} catch (e) {
return {
title: url,
description: "No description",
description: "No description available",
image: null,
};
}
};

export const getMetaTags = async (url: string) => {
const html = await getHtml(url);
if (!html) {
// If we couldn't fetch the HTML (e.g., due to Cloudflare protection),
// generate fallback metadata from the URL
return generateFallbackMetadata(url);
}

const { metaTags, title: titleTag, linkTags } = getHeadChildNodes(html);

let object = {};
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/track/lead/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const POST = withWorkspace(
} = trackLeadRequestSchema
.extend({
// add backwards compatibility
externalId: z.string().optional(),
customerId: z.string().optional(),
externalId: z.string().nullish(),
customerId: z.string().nullish(),
})
.parse(body);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/track/sale/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export const POST = withWorkspace(
} = trackSaleRequestSchema
.extend({
// add backwards compatibility
externalId: z.string().optional(),
customerId: z.string().optional(),
externalId: z.string().nullish(),
customerId: z.string().nullish(),
})
.parse(body);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { authorizeRequestSchema } from "@/lib/zod/schemas/oauth";
import EmptyState from "@/ui/shared/empty-state";
import { BlurImage, Logo } from "@dub/ui";
import { CircleWarning, CubeSettings } from "@dub/ui/icons";
import { HOME_DOMAIN, constructMetadata } from "@dub/utils";
import { constructMetadata } from "@dub/utils";
import { ArrowLeftRight } from "lucide-react";
import { redirect } from "next/navigation";
import { Suspense } from "react";
Expand Down Expand Up @@ -62,7 +62,7 @@ export default async function Authorize({
)}
</a>
<ArrowLeftRight className="size-5 text-neutral-500" />
<a href={HOME_DOMAIN} target="_blank" rel="noreferrer">
<a href="https://dub.co" target="_blank" rel="noreferrer">
<Logo className="size-12" />
</a>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function Form() {
// If url is valid, continue to generate metatags, else return null
new URL(debouncedUrl);
setLoadingPreviewImage(true);
const res = await fetch(`/api/metatags?url=${debouncedUrl}`);
const res = await fetch(`/api/links/metatags?url=${debouncedUrl}`);
if (res.ok) {
const results = await res.json();
setPreviewImage(results.image);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/cloaked/[url]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
constructMetadata,
getApexDomain,
} from "@dub/utils";
import { getMetaTags } from "app/api/metatags/utils";
import { getMetaTags } from "app/api/links/metatags/utils";

export const runtime = "edge";
export const fetchCache = "force-no-store";
Expand Down
16 changes: 6 additions & 10 deletions apps/web/lib/middleware/api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { parse } from "@/lib/middleware/utils";
import { HOME_DOMAIN } from "@dub/utils";
import { NextRequest, NextResponse } from "next/server";

export default function ApiMiddleware(req: NextRequest) {
const { path, fullPath } = parse(req);
const { fullPath } = parse(req);

// special case for /metatags
if (path === "/metatags") {
const url = req.nextUrl.searchParams.get("url");
if (!url) {
return NextResponse.redirect(`${HOME_DOMAIN}/tools/metatags`, {
status: 301,
});
}
// redirect to dub.co for /metatags
if (fullPath.startsWith("/metatags")) {
return NextResponse.redirect("https://dub.co", {
status: 301,
});
}
// Note: we don't have to account for paths starting with `/api`
// since they're automatically excluded via our middleware matcher
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/middleware/utils/bots-list.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export const UA_BOTS = [
"bot", // most bots
"crawler", // most crawlers
"metatags", // Dub.co Metatags API (https://api.dub.co/metatags)
"chatgpt", // ChatGPT
"bluesky", // Bluesky crawler
"facebookexternalhit", // Facebook crawler
Expand All @@ -18,6 +17,7 @@ export const UA_BOTS = [
"MetaInspector", // metatags.io
"Go-http-client", // Go-http-client/1.1 is a bot: https://user-agents.net/string/go-http-client-1-1
"iframely", // https://iframely.com/docs/about (used by Notion, Linear)
"H1cbA69", // internal links/metatags API

// new
"ia_archiver",
Expand Down
2 changes: 0 additions & 2 deletions apps/web/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { embedTokensPaths } from "./embed-tokens";
import { eventsPath } from "./events";
import { foldersPaths } from "./folders";
import { linksPaths } from "./links";
import { metatagsPath } from "./metatags";
import { partnersPaths } from "./partners";
import { qrCodePaths } from "./qr";
import { tagsPaths } from "./tags";
Expand Down Expand Up @@ -56,7 +55,6 @@ export const document = createDocument({
...workspacesPaths,
...embedTokensPaths,
...qrCodePaths,
...metatagsPath,
},
components: {
schemas: {
Expand Down
38 changes: 0 additions & 38 deletions apps/web/lib/openapi/metatags/index.ts

This file was deleted.

3 changes: 0 additions & 3 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import z from "@/lib/zod";
import { metaTagsSchema } from "@/lib/zod/schemas/metatags";
import {
PartnerEarningsSchema,
PartnerProfileCustomerSchema,
Expand Down Expand Up @@ -278,8 +277,6 @@ export const tagColors = [

export type DashboardProps = z.infer<typeof dashboardSchema>;

export type MetaTag = z.infer<typeof metaTagsSchema>;

export type TokenProps = z.infer<typeof tokenSchema>;

export type OAuthAppProps = z.infer<typeof oAuthAppSchema>;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/upstash/record-metatags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getDomainWithoutWWW } from "@dub/utils";
import { redis } from "./redis";

/**
* Recording metatags that were generated via `/api/metatags`
* Recording metatags that were generated via "/api/links/metatags"
* If there's an error, it will be logged to a separate redis list for debugging
**/
export async function recordMetatags(url: string, error: boolean) {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/lib/zod/schemas/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,19 +538,19 @@ export const LinkSchema = z
.string()
.nullable()
.describe(
"The title of the short link generated via `api.dub.co/metatags`. Will be used for Custom Social Media Cards if `proxy` is true.",
"The title of the short link. Will be used for Custom Social Media Cards if `proxy` is true.",
),
description: z
.string()
.nullable()
.describe(
"The description of the short link generated via `api.dub.co/metatags`. Will be used for Custom Social Media Cards if `proxy` is true.",
"The description of the short link. Will be used for Custom Social Media Cards if `proxy` is true.",
),
image: z
.string()
.nullable()
.describe(
"The image of the short link generated via `api.dub.co/metatags`. Will be used for Custom Social Media Cards if `proxy` is true.",
"The image of the short link. Will be used for Custom Social Media Cards if `proxy` is true.",
),
video: z
.string()
Expand Down
23 changes: 0 additions & 23 deletions apps/web/lib/zod/schemas/metatags.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/web/lib/zod/schemas/sales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const trackSaleRequestSchema = z.object({
.optional()
.default("Purchase")
.describe("The name of the sale event.")
.openapi({ examples: ["Purchase", "Upgrade", "Payment"] }),
.openapi({ example: "Invoice paid" }),
invoiceId: z
.string()
.nullish()
Expand Down
Loading