Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/__tests__/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const intlHandler = vi.fn(() => 'intl-result')

vi.mock('next-intl/middleware', () => ({
default: vi.fn(() => intlHandler),
}))

const redirect = vi.fn((url: { pathname: string }, status: number) => ({
kind: 'redirect',
pathname: url.pathname,
status,
}))

vi.mock('next/server', () => ({
NextResponse: { redirect },
NextRequest: class {},
}))

const { proxy, config } = await import('../proxy')

type FakeUrl = { pathname: string; clone: () => FakeUrl }

function makeRequest(pathname: string) {
const url: FakeUrl = {
pathname,
clone() {
return makeRequest(this.pathname).nextUrl
},
}
return { nextUrl: url } as never
}

beforeEach(() => {
redirect.mockClear()
intlHandler.mockClear()
})

describe('proxy', () => {
it('redirects two-letter locale docs URLs to the non-localized path with 307', () => {
const result = proxy(makeRequest('/es/docs/getting-started')) as {
pathname: string
status: number
}

expect(redirect).toHaveBeenCalledTimes(1)
expect(result.pathname).toBe('/docs/getting-started')
expect(result.status).toBe(307)
})

it('redirects region-qualified locales like /pt-BR/docs/*', () => {
const result = proxy(makeRequest('/pt-BR/docs/hive/setup')) as { pathname: string }

expect(result.pathname).toBe('/docs/hive/setup')
})

it('redirects the /SC/docs/* pseudo-locale', () => {
const result = proxy(makeRequest('/SC/docs/hive')) as { pathname: string }

expect(result.pathname).toBe('/docs/hive')
})

it('redirects the root path to /docs with 307', () => {
const result = proxy(makeRequest('/')) as { pathname: string; status: number }

expect(result.pathname).toBe('/docs')
expect(result.status).toBe(307)
})

it('delegates non-docs, non-root paths to the i18n middleware', () => {
const request = makeRequest('/en/community')
const result = proxy(request)

expect(result).toBe('intl-result')
expect(intlHandler).toHaveBeenCalledWith(request)
expect(redirect).not.toHaveBeenCalled()
})

it('does not treat non-locale prefixes as localized docs URLs', () => {
// three-letter prefix does not match the locale pattern
const result = proxy(makeRequest('/abc/docs/page'))

expect(result).toBe('intl-result')
expect(redirect).not.toHaveBeenCalled()
})

it('exports a matcher config that excludes docs, api, and static assets', () => {
expect(config.matcher).toContain('/')
expect(config.matcher.some((m: string) => m.includes('?!docs|api'))).toBe(true)
})
})
15 changes: 15 additions & 0 deletions src/__tests__/robots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'

import robots from '../app/robots'

describe('robots', () => {
it('allows all user agents on the whole site', () => {
const result = robots()

expect(result.rules).toEqual({ userAgent: '*', allow: '/' })
})

it('points crawlers at the canonical sitemap URL', () => {
expect(robots().sitemap).toBe('https://docs.hivecommons.dev/sitemap.xml')
})
})
133 changes: 133 additions & 0 deletions src/__tests__/sitemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import fs from 'fs'
import os from 'os'
import path from 'path'

import sitemap from '../app/sitemap'

const SITE_URL = 'https://docs.hivecommons.dev'

let fixtureRoot: string

function write(relPath: string, content = '# stub\n') {
const full = path.join(fixtureRoot, relPath)
fs.mkdirSync(path.dirname(full), { recursive: true })
fs.writeFileSync(full, content)
}

beforeAll(() => {
fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sitemap-fixture-'))

// hive project content
write('docs/content/hive/index.mdx')
write('docs/content/hive/guides/setup.md')
write('docs/content/hive/guides/advanced/scaling.mdx')
// excluded: underscore-prefixed file and directory
write('docs/content/hive/_partial.md')
write('docs/content/hive/_drafts/wip.md')
// excluded: hidden dir, node_modules, common-subs, images
write('docs/content/hive/.hidden/secret.md')
write('docs/content/hive/node_modules/pkg/readme.md')
write('docs/content/hive/common-subs/shared.md')
write('docs/content/hive/images/pic.md')
// excluded: non-markdown file
write('docs/content/hive/diagram.svg')

// pluk project with an index file inside a subdirectory
write('docs/content/pluk/cli/index.md')

vi.spyOn(process, 'cwd').mockReturnValue(fixtureRoot)
})

afterAll(() => {
vi.restoreAllMocks()
fs.rmSync(fixtureRoot, { recursive: true, force: true })
})

describe('sitemap', () => {
it('always includes homepage, locale marketing page, and docs landing page', () => {
const entries = sitemap()
const urls = entries.map((e) => e.url)

expect(urls).toContain(SITE_URL)
expect(urls).toContain(`${SITE_URL}/en`)
expect(urls).toContain(`${SITE_URL}/docs`)
})

it('assigns the documented priorities to each page tier', () => {
const entries = sitemap()
const byUrl = new Map(entries.map((e) => [e.url, e]))

expect(byUrl.get(SITE_URL)?.priority).toBe(1.0)
expect(byUrl.get(`${SITE_URL}/en`)?.priority).toBe(0.8)
expect(byUrl.get(`${SITE_URL}/docs`)?.priority).toBe(0.9)
expect(byUrl.get(`${SITE_URL}/docs/hive`)?.priority).toBe(0.9)
expect(byUrl.get(`${SITE_URL}/docs/hive/guides/setup`)?.priority).toBe(0.7)
})

it('emits a project root entry for every project even without content', () => {
const entries = sitemap()
const urls = entries.map((e) => e.url)

for (const base of ['hive', 'hotshot', 'pluk', 'rationguard', 'promptargs', 'dibs', 'spektacular']) {
expect(urls).toContain(`${SITE_URL}/docs/${base}`)
}
})

it('maps markdown files to routes, stripping extensions and /index suffixes', () => {
const entries = sitemap()
const urls = entries.map((e) => e.url)

expect(urls).toContain(`${SITE_URL}/docs/hive/guides/setup`)
expect(urls).toContain(`${SITE_URL}/docs/hive/guides/advanced/scaling`)
// cli/index.md collapses to the parent folder route
expect(urls).toContain(`${SITE_URL}/docs/pluk/cli`)
expect(urls).not.toContain(`${SITE_URL}/docs/pluk/cli/index`)
})

it('excludes underscore, hidden, node_modules, common-subs, images, and non-markdown entries', () => {
const entries = sitemap()
const urls = entries.map((e) => e.url)

expect(urls.some((u) => u.includes('_partial'))).toBe(false)
expect(urls.some((u) => u.includes('_drafts'))).toBe(false)
expect(urls.some((u) => u.includes('.hidden') || u.includes('secret'))).toBe(false)
expect(urls.some((u) => u.includes('node_modules'))).toBe(false)
expect(urls.some((u) => u.includes('common-subs') || u.includes('shared'))).toBe(false)
expect(urls.some((u) => u.includes('/images/') || u.includes('pic'))).toBe(false)
expect(urls.some((u) => u.includes('diagram'))).toBe(false)
})

it('uses file mtime as lastModified for docs pages', () => {
const setupPath = path.join(fixtureRoot, 'docs/content/hive/guides/setup.md')
const mtime = fs.statSync(setupPath).mtime

const entries = sitemap()
const entry = entries.find((e) => e.url === `${SITE_URL}/docs/hive/guides/setup`)

expect(entry?.lastModified).toEqual(mtime)
})

it('marks docs pages weekly and marketing pages monthly', () => {
const entries = sitemap()
const byUrl = new Map(entries.map((e) => [e.url, e]))

expect(byUrl.get(SITE_URL)?.changeFrequency).toBe('monthly')
expect(byUrl.get(`${SITE_URL}/en`)?.changeFrequency).toBe('monthly')
expect(byUrl.get(`${SITE_URL}/docs`)?.changeFrequency).toBe('weekly')
expect(byUrl.get(`${SITE_URL}/docs/hive/guides/setup`)?.changeFrequency).toBe('weekly')
})

it('returns no docs-page entries when the content root is missing', () => {
const emptyRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sitemap-empty-'))
const spy = vi.spyOn(process, 'cwd').mockReturnValue(emptyRoot)
try {
const entries = sitemap()
// homepage + /en + /docs + 7 project roots, nothing else
expect(entries).toHaveLength(10)
} finally {
spy.mockReturnValue(fixtureRoot)
fs.rmSync(emptyRoot, { recursive: true, force: true })
}
})
})
Loading