Getting Started
Get up and running with deadend.ai in minutes.
Step 1: Subscribe to a Plan
Before you can start using deadend.ai:
- Visit deadend.ai (opens in a new tab) and sign up for an account
- Create your first project
- Subscribe to a plan - Choose the plan that fits your needs
- Generate your API key from the dashboard
Your API key will be in the format: da_live_... or da_test_...
Step 2: Index Your Content
Before deadend.ai can provide smart suggestions, you need to index your pages. Use the Pages API to push content directly from your application. This gives you complete control over what gets indexed and allows you to include custom metadata.
Recommended: Upsert Endpoint
The Upsert endpoint is the preferred way to sync content. It allows you to push full content with custom metadata:
curl -X POST https://api.deadend.ai/v1/pages/upsert \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/product/blue-widget",
"title": "Blue Widget - Premium Quality",
"content": "This is a premium blue widget with advanced features...",
"metadata": {
"price": 29.99,
"sku": "WIDGET-BLUE",
"category": "Widgets",
"in_stock": true
}
}'Benefits:
- ✅ Custom Metadata Support - Include product prices, categories, stock status, and more
- ✅ Smart Updates - Automatically detects content changes and only re-vectorizes when needed
- ✅ Complete Control - You decide exactly what content gets indexed
- ✅ Real-time Sync - Push content immediately when it changes in your CMS
Note: All API endpoints must be called from your server, never from the browser. Your API keys must remain private on your server.
Step 3: Integrate into Your Application
Once your content is indexed, you need to integrate deadend.ai into your application to provide smart suggestions when users encounter 404 pages. Choose the integration method that best fits your stack.
SDKs
Official SDKs provide the fastest way to integrate deadend.ai with minimal setup.
NextJS SDK
The @deadendai/nextjs (opens in a new tab) package provides server-side API handlers and client-side examples for Next.js applications.
Installation:
npm install @deadendai/nextjs
# or
yarn add @deadendai/nextjs
# or
pnpm add @deadendai/nextjsEnvironment Variables:
Create a .env.local file in your Next.js project root:
DEADEND_API_KEY=da_live_your_api_key_here
DEADEND_HOST=https://api.deadend.aiNote: DEADEND_HOST is optional and defaults to https://api.deadend.ai if not provided. Never expose your API key to the browser - this package is designed for server-side use only.
Server Setup:
Create a unified API route handler at app/api/deadend/[[...path]]/route.ts:
import { createDeadendHandlers, SearchThreshold } from '@deadendai/nextjs'
const handlers = createDeadendHandlers({
limit: 5,
searchThreshold: SearchThreshold.GOOD,
})
export const POST = handlers.POSTThe unified handler automatically detects the request type based on the payload:
- Click Event: Contains
clicked_urlandsearch_id - 404 Event: Contains
raw_pathonly - Suggest: Contains
pathfield
Client Usage Examples:
Here are two complete examples for your app/not-found.tsx file:
Automatic Redirect - Auto-redirect to top suggestion
This implementation automatically redirects users to the top suggestion when they hit a 404 page:
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
export default function NotFound() {
const pathname = usePathname()
useEffect(() => {
async function handleNotFound() {
const path = pathname || window.location.pathname
// Track the 404 event
fetch('/api/deadend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
raw_path: path,
origin: window.location.origin,
referrer: document.referrer || undefined,
}),
}).catch(() => {}) // Silently fail - don't block UX
// Fetch top suggestion (limit: 1)
try {
const response = await fetch('/api/deadend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path }),
})
if (response.ok) {
const data = await response.json()
if (data.suggestions && data.suggestions.length > 0) {
const suggestion = data.suggestions[0]
// Track click event
fetch('/api/deadend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clicked_url: suggestion.url,
clicked_rank: 1,
raw_path: path,
search_id: data.search_id,
origin: window.location.origin,
}),
}).catch(() => {}) // Silently fail
// Redirect to suggestion
window.location.href = suggestion.url
return
}
}
} catch (error) {
console.error('Failed to fetch suggestions:', error)
}
}
handleNotFound()
}, [pathname])
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-4xl font-bold mb-4">404 - Page Not Found</h1>
<p className="text-gray-600 mb-4">Redirecting to a similar page...</p>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
)
}Features:
- ✅ Automatically tracks 404 event
- ✅ Fetches single top suggestion
- ✅ Automatically tracks click event
- ✅ Immediate redirect to best match
- ✅ Graceful fallback if no suggestions found
User Choice - Present suggestions for user selection
This implementation shows users multiple suggestions and lets them choose which page to visit:
'use client'
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
interface Suggestion {
url: string
title: string
description?: string
score: number
}
interface SuggestResponse {
search_id: string
suggestions: Suggestion[]
}
export default function NotFound() {
const pathname = usePathname()
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
const [searchId, setSearchId] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [currentPath, setCurrentPath] = useState('')
useEffect(() => {
const path = pathname || window.location.pathname
setCurrentPath(path)
async function fetchSuggestionsAndTrack() {
try {
// Track the 404 event
fetch('/api/deadend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
raw_path: path,
origin: window.location.origin,
referrer: document.referrer || undefined,
}),
}).catch(() => {}) // Silently fail - don't block UX
// Fetch suggestions
const response = await fetch('/api/deadend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path }),
})
if (response.ok) {
const data: SuggestResponse = await response.json()
setSuggestions(data.suggestions || [])
setSearchId(data.search_id)
}
} catch (error) {
console.error('Failed to fetch suggestions:', error)
} finally {
setLoading(false)
}
}
fetchSuggestionsAndTrack()
}, [pathname])
const handleSuggestionClick = async (url: string, rank: number) => {
if (searchId) {
// Track the click event
fetch('/api/deadend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clicked_url: url,
clicked_rank: rank,
raw_path: currentPath,
search_id: searchId,
origin: window.location.origin,
}),
}).catch(() => {}) // Silently fail
}
}
return (
<main className="min-h-screen flex items-center justify-center p-8">
<div className="max-w-2xl w-full">
{/* 404 Header */}
<div className="text-center mb-12">
<h1 className="text-6xl font-bold mb-4">404</h1>
<h2 className="text-2xl font-semibold mb-3">Page not found</h2>
<p className="text-gray-600">
The page <code className="text-blue-600 bg-gray-100 px-2 py-1 rounded font-mono text-sm">{currentPath}</code> doesn't exist.
</p>
</div>
{/* Suggestions Section */}
<div className="space-y-4">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
<span className="ml-3 text-gray-600">Finding similar pages...</span>
</div>
) : suggestions.length > 0 ? (
<>
<div className="mb-6">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Were you looking for one of these?
</h3>
</div>
<div className="space-y-3">
{suggestions.map((suggestion, index) => (
<Link
key={suggestion.url}
href={suggestion.url}
onClick={() => handleSuggestionClick(suggestion.url, index + 1)}
className="group block p-4 rounded-lg border border-gray-200 hover:border-blue-500 hover:bg-blue-50 transition-all"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<h4 className="text-gray-900 font-medium group-hover:text-blue-600 transition-colors">
{suggestion.title || suggestion.url}
</h4>
{suggestion.description && (
<p className="text-gray-600 text-sm mt-1 line-clamp-2">
{suggestion.description}
</p>
)}
<p className="text-gray-500 text-xs mt-2 font-mono truncate">
{suggestion.url}
</p>
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<span className="text-xs text-gray-500 font-mono">
{Math.round(suggestion.score * 100)}%
</span>
<svg
className="w-4 h-4 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</Link>
))}
</div>
</>
) : (
<div className="text-center py-8">
<p className="text-gray-600">No similar pages found.</p>
</div>
)}
{/* Home Button */}
<div className="flex justify-center pt-8">
<Link
href="/"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-gray-900 text-white font-medium hover:bg-gray-800 transition-colors"
>
Back to Home
</Link>
</div>
</div>
</div>
</main>
)
}Features:
- ✅ Tracks 404 event on page load
- ✅ Fetches multiple suggestions (default: 3)
- ✅ Displays suggestions with relevance scores
- ✅ Tracks click events when user selects a suggestion
- ✅ Clean, user-friendly interface
- ✅ Loading and empty states
Note: This example uses basic Tailwind CSS classes. You can enhance it with animations using libraries like framer-motion or customize the styling to match your design system.
For more details, see the @deadendai/nextjs documentation (opens in a new tab).
Self Implementation
If you prefer to build your own integration or are using a different framework, here are manual implementation guides.
Server
Your server acts as a secure proxy between your client application and the deadend.ai API. This ensures your API keys remain private and allows you to add custom logic, rate limiting, or caching as needed.
NextJS Manual Implementation
For Next.js 13+ with the App Router, create API routes in the app/api directory. Here are the proxy endpoints you'll need:
1. Proxy Suggest Endpoint
Create app/api/deadend/suggest/route.ts:
import { NextRequest, NextResponse } from 'next/server'
const DEADEND_API_URL = 'https://api.deadend.ai/v1/suggest'
const DEADEND_API_KEY = process.env.DEADEND_API_KEY!
export async function POST(request: NextRequest) {
try {
if (!DEADEND_API_KEY) {
return NextResponse.json(
{ error: 'DEADEND_API_KEY not configured' },
{ status: 500 }
)
}
const body = await request.json()
const { path, limit, options } = body
if (!path) {
return NextResponse.json(
{ error: 'path is required' },
{ status: 400 }
)
}
const response = await fetch(DEADEND_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${DEADEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
path,
limit: limit || 3,
options: options || {},
}),
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
return NextResponse.json(
{ error: error.error || 'Failed to fetch suggestions' },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Error proxying suggest request:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}2. Proxy Semantic Search Endpoint
Create app/api/deadend/semantic-search/route.ts:
import { NextRequest, NextResponse } from 'next/server'
const DEADEND_API_URL = 'https://api.deadend.ai/v1/semantic-search'
const DEADEND_API_KEY = process.env.DEADEND_API_KEY!
export async function POST(request: NextRequest) {
try {
if (!DEADEND_API_KEY) {
return NextResponse.json(
{ error: 'DEADEND_API_KEY not configured' },
{ status: 500 }
)
}
const body = await request.json()
const { query, limit, options } = body
if (!query) {
return NextResponse.json(
{ error: 'query is required' },
{ status: 400 }
)
}
const response = await fetch(DEADEND_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${DEADEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
limit: limit || 3,
options: options || {},
}),
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
return NextResponse.json(
{ error: error.error || 'Failed to fetch suggestions' },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Error proxying semantic search request:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}3. Proxy Events Endpoint (Notfound & Click)
Create app/api/deadend/events/route.ts:
import { NextRequest, NextResponse } from 'next/server'
const DEADEND_API_URL = 'https://api.deadend.ai/v1/events'
const DEADEND_API_KEY = process.env.DEADEND_API_KEY!
export async function POST(request: NextRequest) {
try {
if (!DEADEND_API_KEY) {
return NextResponse.json(
{ error: 'DEADEND_API_KEY not configured' },
{ status: 500 }
)
}
const body = await request.json()
// Extract additional context from request headers
const origin = request.headers.get('origin') || request.headers.get('referer')
const userAgent = request.headers.get('user-agent')
const forwardedFor = request.headers.get('x-forwarded-for')
const referer = request.headers.get('referer')
// Build event payload
const eventPayload: any = {
...body,
origin: body.origin || origin,
}
// Add optional fields if available
if (userAgent && !body.user_agent) {
eventPayload.user_agent = userAgent
}
if (referer && !body.referrer) {
eventPayload.referrer = referer
}
if (forwardedFor && !body.user_ip) {
eventPayload.user_ip = forwardedFor.split(',')[0].trim()
}
const response = await fetch(DEADEND_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${DEADEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(eventPayload),
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
return NextResponse.json(
{ error: error.error || 'Failed to send event' },
{ status: response.status }
)
}
// Events API returns 204 No Content on success
return new NextResponse(null, { status: 204 })
} catch (error) {
console.error('Error proxying event:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Environment Variables
Add to your .env.local:
DEADEND_API_KEY=da_live_your_api_key_hereClient
The client-side integration handles displaying suggestions to users and tracking interactions. Your browser code needs to communicate with your server, which then proxies requests to the deadend.ai API to keep your API keys secure.
NextJS Client Implementation
For Next.js applications without using the SDK, create your own client-side integration:
'use client'
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
interface Suggestion {
url: string
title: string
score: number
}
export default function NotFound() {
const pathname = usePathname()
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
const [loading, setLoading] = useState(true)
const [searchId, setSearchId] = useState<string | null>(null)
useEffect(() => {
async function handleNotFound() {
// Track 404 event
await fetch('/api/deadend/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ raw_path: pathname }),
}).catch(() => {}) // Fail silently
// Fetch suggestions
try {
const response = await fetch('/api/deadend/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: pathname, limit: 3 }),
})
if (response.ok) {
const data = await response.json()
setSuggestions(data.suggestions || [])
setSearchId(data.search_id || null)
}
} catch (error) {
console.error('Error fetching suggestions:', error)
} finally {
setLoading(false)
}
}
handleNotFound()
}, [pathname])
const handleClick = async (suggestion: Suggestion, rank: number) => {
// Track click event
await fetch('/api/deadend/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clicked_url: suggestion.url,
clicked_rank: rank,
raw_path: pathname,
search_id: searchId,
}),
}).catch(() => {}) // Fail silently
}
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8">
<h1 className="text-4xl font-bold mb-4">404 - Page Not Found</h1>
{loading ? (
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
) : suggestions.length > 0 ? (
<div className="w-full max-w-2xl">
<h2 className="text-2xl font-semibold mb-4">Did you mean one of these?</h2>
<div className="space-y-4">
{suggestions.map((suggestion, index) => (
<Link
key={suggestion.url}
href={suggestion.url}
onClick={() => handleClick(suggestion, index + 1)}
className="block p-4 border border-gray-200 rounded-lg hover:border-gray-400 hover:bg-gray-50 transition-colors"
>
<h3 className="text-lg font-medium mb-1">{suggestion.title}</h3>
<p className="text-sm text-gray-600 mb-2">{suggestion.url}</p>
<p className="text-xs text-gray-500">Relevance: {Math.round(suggestion.score * 100)}%</p>
</Link>
))}
</div>
</div>
) : (
<div className="text-center">
<p className="text-gray-600 mb-4">No suggestions available.</p>
<Link href="/" className="text-blue-600 hover:underline">
Return to homepage
</Link>
</div>
)}
</div>
)
}Next Steps
- API Reference - Complete API documentation
- Pages API - Learn how to manage your indexed pages
- Search API - Understand semantic search endpoints
- Authentication - Manage your API keys
Need Help?
- Discord Community (opens in a new tab) - Get help from the community
- Email Support - Contact our team