How OpenSEO Detects Project Targets: Domain vs. Keyword Classification Explained
OpenSEO uses a deterministic heuristic in the detectTarget function to classify user input as either a domain or keyword by checking for whitespace absence, dot presence, and valid URL normalisation.
Every SEO project in OpenSEO starts with a target—either a website you want to track or a keyword you want to monitor. The system must correctly interpret this input to route data to the appropriate API endpoints (DataForSEO for domains, brand-lookup queries for keywords). This article explains how OpenSEO implements target detection based on the source code in every-app/open-seo.
The Target Detection Pipeline
The core logic lives in src/shared/targetDetection.ts. The detectTarget function follows a strict five-step pipeline to categorise input without external dependencies or machine learning.
Step 1: Sanitise Input
Raw strings are trimmed of leading and trailing whitespace to ensure consistent processing.
const trimmed = rawInput.trim();
This prevents accidental misclassification from copy-paste artifacts like trailing spaces.
Step 2: Apply Domain Heuristics
The code performs a lightweight check to flag potential domains. Input qualifies for deeper validation only if it:
- Contains no whitespace characters
- Includes at least one dot (
.)
const looksLikeDomain = trimmed.length > 0 && !/\s/.test(trimmed) && trimmed.includes(".");
This filter quickly eliminates obvious keywords like "OpenAI" or "content marketing" while preserving candidates like "example.com" or "subdomain.example.co.uk".
Step 3: Normalise Valid Domains
When heuristic checks pass, detectTarget delegates to normalizeDomain from src/types/schemas/domain.ts. This helper:
- Prepends
https://if no protocol exists - Parses the string using the native
URLclass - Strips
www.prefixes and lower-cases the hostname - Throws on malformed inputs (invalid protocols, unparseable strings)
const hostname = normalizeDomain(trimmed);
The normalisation ensures canonical storage—HTTPS://WWW.Example.COM/ becomes example.com.
Step 4: Confirm Domain Structure
After normalisation, the function verifies the hostname still contains a dot, confirming a valid domain structure rather than a single-segment hostname like localhost.
if (hostname.includes(".")) {
return { type: "domain", value: hostname };
}
Step 5: Fallback to Keyword Classification
Any failure—whitespace detection, missing dot, or normalizeDomain exception—triggers keyword classification. The original trimmed string is preserved to maintain user intent.
return { type: "keyword", value: trimmed };
Practical Implementation Examples
Basic Usage
import { detectTarget } from "@/shared/targetDetection";
// Bare domain
detectTarget("example.com");
// → { type: "domain", value: "example.com" }
// Full URL with protocol and path
detectTarget("https://www.example.com/features");
// → { type: "domain", value: "example.com" }
// Keyword with surrounding whitespace
detectTarget(" Example Brand ");
// → { type: "keyword", value: "Example Brand" }
// Sentence containing a domain (keyword intent)
detectTarget("Visit example.com today");
// → { type: "keyword", value: "Visit example.com today" }
Project Creation Integration
In the actual project workflow, detectTarget determines how downstream services process the target:
function createProject(rawTarget: string) {
const target = detectTarget(rawTarget);
// Stores: { type: "domain" | "keyword", value: string }
const project = await db.projects.create({
targetType: target.type,
targetValue: target.value,
});
// Later: DataForSEO hostname queries for domains,
// brand-lookup API for keywords
}
Where Target Detection Integrates
OpenSEO relies on consistent target classification across multiple features:
-
Share-of-Voice analysis —
resolveCompetitorGroupsinsrc/server/features/ai-search/services/shareOfVoice.tsdeduplicates competitor entries usingdetectTargetresults, preventing duplicate tracking of the same domain entered in different formats. -
Brand Lookup — Both the server service (
brandLookup.ts) and the UI component (BrandLookupPage.tsx) normalise primary targets and competitors before API submission. -
Project management — All project creation and edit flows pass targets through this pipeline to ensure canonical storage and correct API routing.
Key Source Files
| File | Responsibility |
|---|---|
src/shared/targetDetection.ts |
Core heuristic implementation with five-step pipeline |
src/shared/targetDetection.test.ts |
Unit tests for domains, keywords, edge cases, and error handling |
src/types/schemas/domain.ts |
normalizeDomain helper for URL parsing and canonicalisation |
src/server/features/ai-search/services/shareOfVoice.ts |
Competitor deduplication using detection results |
src/server/features/ai-search/services/brandLookup.ts |
Brand lookup service integration |
src/client/features/ai-search/BrandLookupPage.tsx |
UI component calling detection before server submission |
Summary
- OpenSEO target detection is deterministic and synchronous—no external calls or ML inference required.
- The dot-and-whitespace heuristic filters most keywords before expensive URL parsing occurs.
normalizeDomainenforces canonical form: lowercase, nowww, validated hostname structure.- Fallback-to-keyword design ensures user input is never lost, even for edge cases.
- Detection results drive API endpoint selection (DataForSEO hostnames vs. brand-lookup queries) across project creation, competitor analysis, and brand monitoring features.
Frequently Asked Questions
What happens if a user enters "localhost" as a project target?
detectTarget initially flags "localhost" as a domain candidate (no spaces, contains no dot initially—actually "localhost" has no dot, so it fails the heuristic). Wait: correcting—"localhost" has no dot, so looksLikeDomain evaluates false at step 2, immediately falling through to keyword classification. If a user somehow passed a dot-containing localhost variant like "localhost.localdomain", it would reach normalizeDomain, which would parse it but fail the final dot check in the hostname (after normalisation), again falling back to keyword. OpenSEO treats these as keywords, not domains, since they cannot be resolved as public internet hostnames.
How does OpenSEO handle internationalised domain names (IDNs)?
The normalizeDomain function uses the native URL class for parsing, which automatically handles Punycode conversion. An input like "münchen.de" would be normalised to its Punycode form "xn--mnchen-3ya.de" by the URL parser before the dot check and hostname extraction. The detectTarget function then returns this canonical ASCII representation with type: "domain".
Can the detection be overridden if OpenSEO classifies incorrectly?
The source analysis does not reveal explicit override mechanisms in detectTarget itself. The function returns a discriminated union with type and value fields, suggesting downstream consumers could allow manual type correction through UI toggles. However, the core detection logic is strictly heuristic-based with no configuration parameters exposed in src/shared/targetDetection.ts.
Why use a dot check instead of a TLD validation list?
OpenSEO avoids maintaining a TLD whitelist to minimise maintenance overhead and support private/internal domains used in enterprise contexts. The dot check catches the vast majority of real-world cases while remaining resilient to new gTLDs, country-code variations, and internal domain suffixes without code changes. Invalid domains that pass the loose check fail during normalizeDomain's URL parsing or final hostname validation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →