# core/validators/site.validator.ts

> 125 lines of code and 22 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-validators-site-validator-ts
Source text: https://banes-lab.com/source/build-scripts/core/validators/site.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/build-scripts/core/validators.md), after [core/validators/security.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/security.validator.ts.md) and before [core/validators/source.page.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/source.page.validator.ts.md).

## Definitions

- `checkText` (lexical_declaration, line 109, exported)
- `checkSitemap` (lexical_declaration, line 96, exported)
- `checkIndexable` (lexical_declaration, line 54, exported)
- `checkSearchKey` (lexical_declaration, line 34, exported)
- `checkPage` (lexical_declaration, line 66, exported)
- `checkLinks` (lexical_declaration, line 124, exported)
- `isPageLink` (lexical_declaration, line 120)
- `isKeyText` (lexical_declaration, line 25)
- `checkAlternates` (lexical_declaration, line 40, exported)
- `NOINDEX` (lexical_declaration, line 20)
- `FRAGMENT` (lexical_declaration, line 21)
- `QUERY` (lexical_declaration, line 22)
- `SLASH` (lexical_declaration, line 23)
- `at` (lexical_declaration, line 26)
- `pairs` (lexical_declaration, line 45, exported)
- `excluded` (lexical_declaration, line 59, exported)
- `findings` (lexical_declaration, line 72, exported)
- `listed` (lexical_declaration, line 97, exported)
- `registered` (lexical_declaration, line 98, exported)
- `routeOf` (lexical_declaration, line 115)
- `stops` (lexical_declaration, line 116)
- `route` (lexical_declaration, line 132, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-validators.md)

## Uses

- [configuration/strings/site.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/site.strings.ts.md)
- [core/predicates/route.predicate.ts](https://banes-lab.com/source/build-scripts/core/predicates/route.predicate.ts.md)

## Used by

- [core/coordinators/validation.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/validation.coordinator.ts.md)
- [core/validators/source.page.validator.ts](https://banes-lab.com/source/build-scripts/core/validators/source.page.validator.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/predicates](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-predicates.md)
- [core/validators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-validators.md)

## Source

```typescript
import {
    EMPTY_MAIN,
    EXCLUDED_ROUTE,
    INDEXED_ERROR_PAGE,
    WEAK_DESCRIPTION,
    malformedSearchKey,
    missingText,
    strayRoute,
    unlistedRoute,
    unservedLink,
    weakTitle,
    wrongAlternate,
    wrongCanonical,
} from "#configuration/strings/site.strings";
import type { Finding, PageArtifact, Seen, Twins } from "#types/validation.types";
import { JSON_ROUTE, SEARCH_KEY_CHARACTERS, SEARCH_KEY_LENGTH } from "#configuration/constants/site.constants";
import { isFileRoute } from "#core/predicates/route.predicate";
import { locationsOf } from "#core/converters/build.converter";

const NOINDEX = "noindex";
const FRAGMENT = "#";
const QUERY = "?";
const SLASH = "/";

const isKeyText = function isKeyText(text: string): boolean {
    for (let at = 0; at < text.length; at += 1) {
        if (!SEARCH_KEY_CHARACTERS.has(text.charAt(at))) {
            return false;
        }
    }
    return text.length >= SEARCH_KEY_LENGTH.min && text.length <= SEARCH_KEY_LENGTH.max;
};

export const checkSearchKey = function checkSearchKey(file: string, text: string): Finding[] {
    return isKeyText(text)
        ? []
        : [{ file, message: malformedSearchKey(text.length, SEARCH_KEY_LENGTH.min, SEARCH_KEY_LENGTH.max) }];
};

export const checkAlternates = function checkAlternates(
    file: string,
    artifact: PageArtifact,
    expected: Twins,
): Finding[] {
    const pairs: readonly (readonly [string, string | null, string | null])[] = [
        ["Markdown", artifact.alternates.markdown, expected.markdown],
        ["JSON", artifact.alternates.json, expected.json],
    ];
    return pairs
        .filter(([, found, wanted]) => found !== wanted)
        .map(([form, found, wanted]) => ({ file, message: wrongAlternate(form, String(found), String(wanted)) }));
};

export const checkIndexable = function checkIndexable(
    file: string,
    artifact: PageArtifact,
    indexable: boolean,
): Finding[] {
    const excluded = artifact.robots?.includes(NOINDEX) ?? false;
    if (indexable === !excluded) {
        return [];
    }
    return [{ file, message: indexable ? EXCLUDED_ROUTE : INDEXED_ERROR_PAGE }];
};

export const checkPage = function checkPage(
    file: string,
    artifact: PageArtifact,
    address: string,
    seen: Seen,
): Finding[] {
    const findings: Finding[] = [];
    if (artifact.text.length === 0) {
        findings.push({ file, message: EMPTY_MAIN });
    }
    if (artifact.title.length === 0 || seen.titles.has(artifact.title)) {
        findings.push({ file, message: weakTitle(artifact.title) });
    }
    if (
        artifact.description === null ||
        artifact.description.length === 0 ||
        seen.descriptions.has(artifact.description)
    ) {
        findings.push({ file, message: WEAK_DESCRIPTION });
    }
    if (artifact.canonical !== address) {
        findings.push({ file, message: wrongCanonical(String(artifact.canonical), address) });
    }
    seen.titles.add(artifact.title);
    if (artifact.description !== null) {
        seen.descriptions.add(artifact.description);
    }
    return findings;
};

export const checkSitemap = function checkSitemap(file: string, xml: string, addresses: readonly string[]): Finding[] {
    const listed = new Set(locationsOf(xml));
    const registered = new Set(addresses);
    return [
        ...addresses
            .filter((address) => !listed.has(address))
            .map((address) => ({ file, message: unlistedRoute(address) })),
        ...[...listed]
            .filter((location) => !registered.has(location))
            .map((location) => ({ file, message: strayRoute(location) })),
    ];
};

export const checkText = function checkText(file: string, text: string, required: readonly string[]): Finding[] {
    return required
        .filter((needle) => !text.includes(needle))
        .map((needle) => ({ file, message: missingText(needle) }));
};

const routeOf = function routeOf(href: string): string {
    const stops = [href.indexOf(QUERY), href.indexOf(FRAGMENT)].filter((at) => at !== -1);
    return stops.length === 0 ? href : href.slice(0, Math.min(...stops));
};

const isPageLink = function isPageLink(href: string): boolean {
    return href.startsWith(SLASH) && !isFileRoute(href) && !href.startsWith(JSON_ROUTE);
};

export const checkLinks = function checkLinks(
    file: string,
    artifact: PageArtifact,
    served: ReadonlySet<string>,
): Finding[] {
    return artifact.links
        .filter((href) => isPageLink(href))
        .filter((href) => {
            const route = routeOf(href);
            return route.length > 0 && !served.has(route);
        })
        .map((href) => ({ file, message: unservedLink(href) }));
};
```
