Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 1x 1x 12x 10x 7x 7x 7x 12x 1x 2x 25x 24x 18x 12x 6x 4x 1x 3x 4x 4x 3x 1x 2x 26x 26x 26x 24x 22x 2x | /**
* SEO title helpers.
*
* Search engines (incl. Bing Site Scan) flag `<title>` text longer than ~60
* characters as "Title too long". Editorial titles can run much longer, so the
* primary fix is an editor-set `metaTitle`; this helper is the backstop that
* keeps the rendered `<title>` within budget when no `metaTitle` is provided.
*/
import { BRAND } from "../content/brand";
/** Max length of the full rendered `<title>` (including brand suffix). */
export const TITLE_MAX_LEN = 60;
const DANGLING_WORD =
/(?:\s+(?:in|at|on|of|for|to|by|with|from|and|or|&|the|a|an|near))+$/iu;
/**
* Clamp a title's text to `maxLen` at a word boundary. Returns the input
* unchanged when it already fits. No ellipsis (cleaner in SERPs); trailing
* separators/punctuation left by the cut are stripped.
*/
export function clampTitleText(text: string, maxLen: number): string {
if (maxLen <= 0) return "";
if (text.length <= maxLen) return text;
const slice = text.slice(0, maxLen);
const lastSpace = slice.lastIndexOf(" ");
const cut = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
// Drop a trailing separator/punctuation/space left dangling by the cut, and
// any preposition/conjunction the cut stranded ("… Kitchens in").
return cut
.replace(/[\s–—:;,.-]+$/u, "")
.replace(DANGLING_WORD, "")
.replace(/[\s–—:;,.-]+$/u, "");
}
/**
* H1 and `<title>` text for the zone and locality pages.
* Hyderabad keeps the indexed capitalisation. Other cities use the
* pan-India pattern from the city-pages spec.
*/
export const areaHeading = (place: string, cityName: string): string =>
cityName === "Hyderabad"
? `Interior Designers in ${place}, Hyderabad`
: `Interior designers in ${place}, ${cityName}`;
/**
* H1 and `<title>` for a city hub. Hyderabad keeps its indexed title. Other
* cities promise only what the page has: areas when it lists zones or
* localities, cost when its prices are approved.
*/
export function cityHubTitle(
publicName: string,
has: { areas: boolean; prices: boolean },
): string {
if (publicName === "Hyderabad") return "Hyderabad Interior Designers by Area & Cost";
if (has.areas && has.prices) return `${publicName} interior designers — areas and cost`;
if (has.areas) return `${publicName} interior designers by area`;
if (has.prices) return `${publicName} interior designers and costs`;
return `Interior designers in ${publicName}`;
}
/**
* Hub meta description. New cities: one sentence with the city, the
* published designer count when above 0, and "free quotes" (#1116).
*/
export function cityHubDescription(
publicName: string,
designerCount = 0,
): string {
if (publicName === "Hyderabad")
return "Find verified interior designers in Hyderabad. Browse portfolios, compare quotes, and hire top professionals across Gachibowli, Banjara Hills, and more.";
const who =
designerCount > 0
? `${designerCount} verified interior designer${designerCount === 1 ? "" : "s"}`
: "verified interior designers";
return `Compare ${who} in ${publicName} — browse portfolios and get free quotes.`;
}
/**
* Hero count line on a city hub. The Claims Register numbers are
* platform-wide and approved for Hyderabad's page; a new city shows only
* how many designers the page actually lists, so "28+" never sits above
* three Bengaluru cards (#1120 QA).
*/
export function cityHubCountLine(
publicName: string,
listedCount: number,
platformClaim: string,
): string {
if (publicName === "Hyderabad") return `${platformClaim} verified designers across India`;
return `${listedCount} verified designer${listedCount === 1 ? "" : "s"} in ${publicName}`;
}
export function servicePageTitle(
serviceName: string,
publicName: string,
): string {
return `${serviceName} in ${publicName}`;
}
/** Hyderabad keeps the indexed cost-and-timeline title. */
export function bhkPageTitle(label: string, publicName: string): string {
return publicName === "Hyderabad"
? `${label} Interior Design in Hyderabad — Cost & Timeline`
: `${label} interiors in ${publicName} — cost`;
}
export function cityHubCrumb(publicName: string): string {
return publicName === "Hyderabad"
? "Interior Designers in Hyderabad"
: `Interior designers in ${publicName}`;
}
/**
* Full `<title>` as Layout renders it. Dedupes a trailing brand suffix so a
* page that hardcodes "... | Interioring" doesn't get it twice; appends the
* suffix only when it still fits in TITLE_MAX_LEN (Bing "Title too long"),
* otherwise drops it and clamps the text at a word boundary. Pages should set
* a concise title; this is the backstop. See docs/rules/seo.md.
*/
export function buildFullTitle(title: string): string {
const brandSuffix = ` | ${BRAND.name}`;
const rawTitle = title.endsWith(brandSuffix)
? title.slice(0, -brandSuffix.length)
: title;
if (rawTitle === BRAND.name) return rawTitle;
if (rawTitle.length + brandSuffix.length <= TITLE_MAX_LEN)
return `${rawTitle}${brandSuffix}`;
return clampTitleText(rawTitle, TITLE_MAX_LEN);
}
|