fix(web): match YouTube URLs by hostname instead of substring (#1182)

This commit is contained in:
Abhay Singh 2026-07-11 04:50:18 +05:30 committed by GitHub
parent 59a0147296
commit c00f3e1b22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 97 additions and 12 deletions

View file

@ -17,6 +17,7 @@ import {
} from "@ui/assets/icons"
import { Globe, FileText, FileCode, Image } from "lucide-react"
import { cn } from "@lib/utils"
import { isYouTubeUrl } from "@/lib/url-helpers"
function MCPIcon({ className }: { className?: string }) {
return (
@ -206,7 +207,7 @@ export function DocumentIcon({
return <MCPIcon className={iconClassName} />
}
if (url?.includes("youtube.com") || url?.includes("youtu.be")) {
if (isYouTubeUrl(url)) {
return <YouTubeIcon className={iconClassName} />
}

View file

@ -3,7 +3,7 @@
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import dynamic from "next/dynamic"
import { isTwitterUrl } from "@/lib/url-helpers"
import { isTwitterUrl, isYouTubeUrl } from "@/lib/url-helpers"
import { ImagePreview } from "./image-preview"
import { TweetContent } from "./tweet"
import { NotionDoc } from "./notion-doc"
@ -67,7 +67,7 @@ function getContentType(document: DocumentWithMemories | null): ContentType {
if (document.type === "google_doc") return "google_doc"
if (document.type === "google_sheet") return "google_sheet"
if (document.type === "google_slide") return "google_slide"
if (document.url?.includes("youtube.com")) return "youtube"
if (isYouTubeUrl(document.url)) return "youtube"
if (document.type === "webpage") return "webpage"
return null

View file

@ -6,6 +6,7 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { isYouTubeUrl } from "@/lib/url-helpers"
import { SyncLogoIcon } from "@ui/assets/icons"
import { DocumentIcon } from "@/components/document-icon"
import { CheckIcon, ChevronDownIcon } from "lucide-react"
@ -41,7 +42,7 @@ type CategoryInfo = { label: string; singularLabel: string; key: string }
function getDocumentTypeInfo(doc: DocumentWithMemories): CategoryInfo {
if (doc.source === "mcp")
return { label: "MCP Items", singularLabel: "MCP Item", key: "mcp" }
if (doc.url?.includes("youtube.com") || doc.url?.includes("youtu.be"))
if (isYouTubeUrl(doc.url))
return {
label: "YouTube Videos",
singularLabel: "YouTube Video",

View file

@ -1,15 +1,9 @@
"use client"
import { useQuery } from "@tanstack/react-query"
import { isYouTubeUrl } from "@/lib/url-helpers"
export function isYouTubeUrl(url: string | undefined | null): boolean {
if (!url) return false
return (
url.includes("youtube.com") ||
url.includes("youtu.be") ||
url.includes("m.youtube.com")
)
}
export { isYouTubeUrl }
export function extractYouTubeVideoId(
url: string | undefined | null,

View file

@ -0,0 +1,73 @@
import { describe, expect, it } from "bun:test"
import { isYouTubeUrl } from "./url-helpers"
describe("isYouTubeUrl", () => {
it("matches canonical youtube.com watch URLs", () => {
expect(isYouTubeUrl("https://youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true)
expect(isYouTubeUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(
true,
)
})
it("matches real youtube subdomains", () => {
expect(isYouTubeUrl("https://m.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true)
expect(isYouTubeUrl("https://music.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(
true,
)
})
it("matches youtu.be short links", () => {
expect(isYouTubeUrl("https://youtu.be/dQw4w9WgXcQ")).toBe(true)
expect(isYouTubeUrl("https://www.youtu.be/dQw4w9WgXcQ")).toBe(true)
})
it("matches embed and shorts paths", () => {
expect(isYouTubeUrl("https://www.youtube.com/embed/dQw4w9WgXcQ")).toBe(true)
expect(isYouTubeUrl("https://www.youtube.com/shorts/dQw4w9WgXcQ")).toBe(
true,
)
})
it("is case-insensitive for scheme and host", () => {
expect(isYouTubeUrl("HTTPS://youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true)
expect(isYouTubeUrl("https://WWW.YOUTUBE.COM/watch?v=dQw4w9WgXcQ")).toBe(
true,
)
})
it("matches scheme-less URLs", () => {
expect(isYouTubeUrl("youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true)
expect(isYouTubeUrl("www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(true)
})
it("rejects lookalike domains", () => {
expect(isYouTubeUrl("https://notyoutube.com/watch?v=dQw4w9WgXcQ")).toBe(
false,
)
expect(isYouTubeUrl("https://myyoutu.be/dQw4w9WgXcQ")).toBe(false)
})
it("rejects hosts that merely start with youtube.com", () => {
expect(
isYouTubeUrl("https://youtube.com.evil.example/watch?v=dQw4w9WgXcQ"),
).toBe(false)
expect(isYouTubeUrl("https://youtu.be.evil.example/dQw4w9WgXcQ")).toBe(
false,
)
})
it("rejects URLs that only contain youtube.com in the path", () => {
expect(
isYouTubeUrl("https://evil.example/youtube.com/watch?v=dQw4w9WgXcQ"),
).toBe(false)
expect(isYouTubeUrl("https://evil.example/redirect?to=youtu.be/x")).toBe(
false,
)
})
it("rejects empty and nullish input", () => {
expect(isYouTubeUrl("")).toBe(false)
expect(isYouTubeUrl(null)).toBe(false)
expect(isYouTubeUrl(undefined)).toBe(false)
})
})

View file

@ -157,6 +157,22 @@ export const isTwitterUrl = (url: string): boolean => {
)
}
/**
* Checks if a URL is a YouTube URL by matching the hostname against
* youtube.com / youtu.be (and their subdomains, e.g. www / m).
* Lookalike hosts (`notyoutube.com`, `youtube.com.evil.example`) and URLs
* that only contain "youtube.com" in the path do not match.
*/
export const isYouTubeUrl = (url: string | undefined | null): boolean => {
if (!url) return false
const parsed = parseWebUrl(url)
if (!parsed) return false
return (
hostnameMatches(parsed.hostname, "youtube.com") ||
hostnameMatches(parsed.hostname, "youtu.be")
)
}
/**
* Checks if a URL is a LinkedIn profile URL (not a company page).
*/