# Getting started (/docs/getting-started) ## 1. Install [#1-install] ```bash npm install @sargonpiraev/seokit @playwright/test ``` ## 2. Extend expect [#2-extend-expect] Create `src/test/seokit.ts`: ```typescript import { expect as baseExpect, test } from "@playwright/test"; import { extendSeokitExpect } from "@sargonpiraev/seokit"; const expect = extendSeokitExpect(baseExpect); export { test, expect }; ``` Import `test` / `expect` from this file in SEO specs — not from `@playwright/test` directly. ## 3. Playwright project [#3-playwright-project] ```typescript import { defineConfig } from "@playwright/test"; export default defineConfig({ testDir: "./src", testMatch: "**/*.seokit.spec.ts", use: { baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000", }, }); ``` ```bash npx playwright test ``` ## 4. First spec [#4-first-spec] Colocate next to the page, e.g. `src/app/[locale]/page.seokit.spec.ts`. Recommended pattern: known locales × known URLs, with expectations in a test-only fixture file (not imported by the app): ```typescript import { test, expect } from "@/test/seokit"; const LOCALES = ["en", "de", "fr"] as const; for (const locale of LOCALES) { test(`/${locale}`, async ({ page }) => { const response = await page.goto(`/${locale}`); expect(response?.ok()).toBeTruthy(); await expect(page).toHaveMetadata({ lang: locale, title: /.+/, description: /.+/, alternates: { canonical: `https://example.com/${locale}`, languages: { en: "https://example.com/en", de: "https://example.com/de", fr: "https://example.com/fr", "x-default": "https://example.com/en", }, }, }); await expect(page).toHaveJsonLd([{ "@type": "Organization" }]); }); } ``` See the [example app](https://github.com/sargonpiraev/seokit/tree/main/apps/example) (`src/test/seo-fixtures.ts` + colocated `*.seokit.spec.ts`). Full matcher reference: [Matchers](/docs/matchers). ## Optional: Next route helpers [#optional-next-route-helpers] `@sargonpiraev/seokit/next` also exports `createSeokitPageRoutes` / `assertSeokitRouteBasics` for next-intl route matrices driven by the Next build manifest. They need a prior `next build`. The example app uses the simpler fixture loops above; helpers are covered by package unit tests under `packages/seokit/src/next/`. # Overview (/docs) **seokit** adds two Playwright matchers for page SEO contracts: | Matcher | What it does | | ---------------- | ------------------------------------------ | | `toHaveMetadata` | HTML → Next-like metadata object → compare | | `toHaveJsonLd` | JSON-LD scripts → compare | ```typescript await expect(page).toHaveMetadata({ lang: "en", title: /Acme/, description: /.+/, alternates: { canonical: "https://example.com/en", languages: { en: "https://example.com/en", "x-default": "https://example.com/en", }, }, robots: { index: true, follow: true }, }); await expect(page).toHaveJsonLd([{ "@type": "Organization" }]); ``` ## In scope / out of scope [#in-scope--out-of-scope] | In seokit | Use Playwright directly | | -------------------------------------------------------------------- | ------------------------------------------------------------- | | Page metadata (title, description, canonical, hreflang, robots meta) | `response?.ok()`, status, headers | | JSON-LD on HTML pages | `robots.txt` / sitemap XML (see example `app.seokit.spec.ts`) | ## Next [#next] * [Getting started](/docs/getting-started) * [Matchers](/docs/matchers) * [Skill](/docs/skill) # Matchers (/docs/matchers) Both matchers take an `expected` value. seokit extracts the actual value from the page and deep-partial compares. Strings may be `RegExp`. ## `toHaveMetadata` [#tohavemetadata] Extracts a Next.js-like metadata object from HTML and compares. ```typescript await expect(page).toHaveMetadata({ lang: "en", title: "Acme", description: /tools/, alternates: { canonical: "https://example.com/en", languages: { en: "https://example.com/en", eo: "https://example.com/eo", "x-default": "https://example.com/en", }, }, robots: { index: true, follow: true }, }); ``` | Field | Source | | ---------------------- | -------------------------------------------- | | `lang` | `` | | `title` | `` | | `description` | `<meta name="description">` | | `alternates.canonical` | `<link rel="canonical">` | | `alternates.languages` | `<link rel="alternate" hreflang>` map | | `robots` | `<meta name="robots">` → `{ index, follow }` | `lang` is an extension — it is not part of Next.js `Metadata`, but is required for i18n pages. Open Graph / Twitter cards are **not** extracted yet — assert them with Playwright locators if needed. ## `toHaveJsonLd` [#tohavejsonld] ```typescript await expect(page).toHaveJsonLd("Organization"); await expect(page).toHaveJsonLd([{ "@type": "Product", name: "Widget" }]); ``` Accepts `@type` string(s) and/or deep-partial JSON-LD object(s). Arrays are matched order-independently (each expected item must match some actual entity). `@graph` entries are flattened. ## Not in seokit [#not-in-seokit] Use Playwright for HTTP checks: ```typescript const response = await page.goto("/en"); expect(response?.ok()).toBeTruthy(); expect(response?.status()).toBe(200); expect(response?.headers()["content-type"]).toMatch(/text\/html/); ``` robots.txt / sitemaps: use `request.get(...)` and assert the body (see the [example app](https://github.com/sargonpiraev/seokit/blob/main/apps/example/src/app/app.seokit.spec.ts)). # Skill (/docs/skill) The package ships a **seokit** skill — a short playbook for `toHaveMetadata` and `toHaveJsonLd`. Docs: * [Overview](/docs) * [Getting started](/docs/getting-started) * [Matchers](/docs/matchers) ## Install via skills.sh [#install-via-skillssh] ```bash npx skills add https://github.com/sargonpiraev/seokit/tree/main/packages/seokit/skills/seokit \ -a cursor \ -y ``` ## Update via skills.sh [#update-via-skillssh] ```bash npx skills update seokit ``` ## Install via skills-npm [#install-via-skills-npm] Optional. Locks the skill to the installed `@sargonpiraev/seokit` version via [skills-npm](https://github.com/antfu/skills-npm): ```bash npm i -D skills-npm npx skills-npm setup npm install @sargonpiraev/seokit ```