Skip to content
Merged
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
2 changes: 2 additions & 0 deletions SCARP-DOCS-ENGINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ The renderer escapes unreviewed raw Markdown HTML. It explicitly recognizes only

Source validators now resolve every Markdown link, English/Hindi partner, frontmatter action and published release from the **independent Scarp route and release contracts**; they do not read the obsolete VitePress navigation configuration. Full generated-site, sitemap and browser checks remain separate gates. This is one more prerequisite for deleting the unused legacy website sources.

The independently authored SEO module is now the single source of canonical/hreflang/Open Graph tags for generated pages and legacy compatibility. Sitemap, live Pages smoke tests and build checks resolve all URLs through the Scarp-owned route contract; smoke tests also cover the latest published compiler release in both languages. The obsolete VitePress site-meta helper is no longer needed.

The Scarp-owned renderer is now the production-build candidate. The VitePress and vue-tsc npm dependencies have been removed; old theme/config source fixtures and some evidence checks remain temporarily to protect compiler and bilingual regression coverage. None of these remaining files render or serve Pages. Remove them after live Pages, offline archive, and release CI have independently confirmed the new output. Track completion in Issue #70.
2 changes: 1 addition & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig } from 'vitepress'
import { pageSeoHead } from './site-meta'
import { pageSeoHead } from '../../scripts/scarp-site/seo'
import { localSearchOptions } from './local-search'

/** Scarp source-language grammar; deliberately limited to implemented syntax. */
Expand Down
53 changes: 0 additions & 53 deletions docs/.vitepress/site-meta.ts

This file was deleted.

12 changes: 9 additions & 3 deletions scripts/check-site-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import assert from 'node:assert/strict'
import { existsSync, readFileSync } from 'node:fs'
import { join, resolve, relative } from 'node:path'
import { fileURLToPath } from 'node:url'
import { pairedUrls } from '../docs/.vitepress/site-meta.ts'
import { pairedRoutes } from './scarp-site/routes.ts'
import { pageSeoHead } from './scarp-site/seo.ts'
import { documentationPages } from './lib/docs-pages.ts'
import { validateSitemap } from './lib/sitemap-policy.ts'

Expand All @@ -24,7 +25,7 @@ for (const path of pages) {
const rendered = join(dist, path.replace(/\.md$/, '.html'))
if (!existsSync(rendered)) { failures.push(`${path}: no generated HTML at ${relative(root, rendered)}`); continue }
const html = readFileSync(rendered, 'utf8')
const expected = pairedUrls(path)
const expected = pairedRoutes(path)
const links = headTags(html, 'link')
const canonical = links.filter(attrs => attrs.rel === 'canonical')
if (canonical.length !== 1 || canonical[0].href !== expected.canonical) {
Expand All @@ -36,7 +37,12 @@ for (const path of pages) {
}
}
const meta = headTags(html, 'meta')
if (!meta.some(attrs => attrs.property === 'og:url' && attrs.content === expected.canonical)) failures.push(`${path}: missing og:url`)
for (const [tag, attrs] of pageSeoHead(path)) {
const actual = tag === 'link' ? links : meta
if (actual.filter(found => Object.entries(attrs).every(([key,value]) => found[key] === value)).length !== 1) {
failures.push(`${path}: missing/duplicate ${tag} SEO metadata ${JSON.stringify(attrs)}`)
}
}
const lang = path.startsWith('hi/') ? 'hi-IN' : 'en-US'
if (!new RegExp(`<html\\b[^>]*\\blang="${lang}"`, 'i').test(html)) failures.push(`${path}: expected <html lang="${lang}">`)
checked++
Expand Down
13 changes: 8 additions & 5 deletions scripts/lib/live-site.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict'
import { DOCS_BASE, SITE_ORIGIN, pairedUrls, urlForPage } from '../../docs/.vitepress/site-meta.ts'
import { BASE, ORIGIN, pairedRoutes, urlForPage } from '../scarp-site/routes.ts'
import { validateSitemap } from './sitemap-policy.ts'
import { LATEST_RELEASE } from './command-finder.ts'

/**
* Exercises real public GitHub Pages URLs after deployment, not just the
Expand All @@ -16,6 +17,8 @@ export const LIVE_PAGE_SOURCES = [
'hi/guide/installation-alpha6.md',
'releases/v0.0.6-alpha.1.md',
'hi/releases/v0.0.6-alpha.1.md',
`releases/${LATEST_RELEASE}.md`,
`hi/releases/${LATEST_RELEASE}.md`,
'tutorials/project-symbol-navigation-alpha6.md',
'hi/tutorials/project-symbol-navigation-alpha6.md',
'community/social.md',
Expand Down Expand Up @@ -43,7 +46,7 @@ async function request(fetcher: SiteFetcher, url: string): Promise<Response> {
const response = await fetcher(url)
assert.equal(response.status, 200, url + ': expected HTTP 200, got ' + response.status)
// Do not accept a successful redirect to an unrelated host.
if (response.url) assert.equal(new URL(response.url).origin, SITE_ORIGIN, url + ': unexpected redirect origin')
if (response.url) assert.equal(new URL(response.url).origin, ORIGIN, url + ': unexpected redirect origin')
return response
}

Expand All @@ -54,7 +57,7 @@ export async function verifyLiveSite(fetcher: SiteFetcher, sitemapSources: reado
const response = await request(fetcher, url)
assert.match(response.headers.get('content-type') ?? '', /text\/html/i, url + ': expected HTML')
const html = await response.text()
const expected = pairedUrls(source)
const expected = pairedRoutes(source)
const lang = source.startsWith('hi/') ? 'hi-IN' : 'en-US'
assert.match(html, new RegExp('<html\\b[^>]*\\blang="' + lang + '"', 'i'), url + ': wrong HTML locale')
const links = tags(html, 'link')
Expand All @@ -74,9 +77,9 @@ export async function verifyLiveSite(fetcher: SiteFetcher, sitemapSources: reado
const styles = assets.filter(path => path.endsWith('.css'))
assert.ok(scripts.length, 'English homepage is missing its versioned JavaScript bundle')
assert.ok(styles.length, 'English homepage is missing its versioned CSS bundle')
for (const asset of [scripts[0], styles[0]]) await request(fetcher, SITE_ORIGIN + asset)
for (const asset of [scripts[0], styles[0]]) await request(fetcher, ORIGIN + asset)

const sitemap = SITE_ORIGIN + DOCS_BASE + 'sitemap.xml'
const sitemap = ORIGIN + BASE + 'sitemap.xml'
const sitemapResponse = await request(fetcher, sitemap)
const xml = await sitemapResponse.text()
const checked = validateSitemap(xml, sitemapSources)
Expand Down
2 changes: 1 addition & 1 deletion scripts/lib/sitemap-policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { urlForPage } from '../../docs/.vitepress/site-meta.ts'
import { urlForPage } from '../scarp-site/routes.ts'

/**
* Validate the complete generated sitemap against all English/Hindi source
Expand Down
8 changes: 2 additions & 6 deletions scripts/scarp-site/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { commandAdvice, FINDER_TEXT, RELEASES, TASKS } from '../lib/command-find
import { escapeHtml, localLink, htmlPath, pairedRoutes, BASE } from './routes.ts'
import { renderMarkdown, type RenderedPage, type HomeAction, type HomeFeature } from './markdown.ts'
import { currentPublishedCompilerTag, currentCompilerReleaseRoute } from './current-release.ts'
import { renderSeoHead } from './seo.ts'

const root=resolve(fileURLToPath(new URL('../..',import.meta.url)))
const docsRoot=join(root,'docs')
Expand Down Expand Up @@ -117,12 +118,7 @@ function htmlDocument(view:PageView, views:PageView[], currentTag:string):string
'<meta name="viewport" content="width=device-width,initial-scale=1"/>'+
'<meta name="description" content="'+htmlAttr(description)+'"/>'+
'<title>'+escapeHtml(title)+'</title>'+
'<link rel="canonical" href="'+meta.canonical+'"/>'+
'<link rel="alternate" hreflang="en" href="'+meta.english+'"/>'+
'<link rel="alternate" hreflang="hi" href="'+meta.hindi+'"/>'+
'<link rel="alternate" hreflang="x-default" href="'+meta.english+'"/>'+
'<meta property="og:url" content="'+meta.canonical+'"/>'+
'<meta property="og:locale" content="'+(locale==='hi'?'hi_IN':'en_US')+'"/>'+
renderSeoHead(source)+
'<link rel="icon" type="image/svg+xml" href="'+BASE+'assets/scarp-mark.svg"/>'+
'<link rel="stylesheet" href="'+BASE+'assets/scarp-site.css"/>'+
'<script defer src="'+BASE+'assets/scarp-site.js"></script></head><body>'+
Expand Down
5 changes: 5 additions & 0 deletions scripts/scarp-site/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export function htmlPath(source: string): string {
return source.slice(0, -3) + '.html'
}

/** The independent renderer, sitemap and live-site checks share a single URL contract. */
export function urlForPage(source: string): string {
return ORIGIN + sourceRoute(source)
}

export function pairedRoutes(source: string): {
locale: Locale; route: string; canonical: string; english: string; hindi: string
} {
Expand Down
24 changes: 24 additions & 0 deletions scripts/scarp-site/seo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { escapeHtml, pairedRoutes } from './routes.ts'

/** Framework-independent metadata. Do not infer the locale from rendered text. */
export function pageSeoHead(source: string): Array<[string, Record<string, string>]> {
if (source === '404.md') return []
const { canonical, english, hindi, locale } = pairedRoutes(source)
return [
['link', { rel: 'canonical', href: canonical }],
['link', { rel: 'alternate', hreflang: 'en', href: english }],
['link', { rel: 'alternate', hreflang: 'hi', href: hindi }],
['link', { rel: 'alternate', hreflang: 'x-default', href: english }],
['meta', { property: 'og:url', content: canonical }],
['meta', { property: 'og:locale', content: locale === 'hi' ? 'hi_IN' : 'en_US' }]
]
}

/** Escapes all attributes instead of joining untrusted URLs into HTML. */
export function renderSeoHead(source: string): string {
return pageSeoHead(source).map(([tag, attrs]) =>
'<' + tag + ' ' + Object.entries(attrs)
.map(([key, value]) => key + '="' + escapeHtml(value) + '"')
.join(' ') + '/>'
).join('')
}
2 changes: 1 addition & 1 deletion scripts/scarp-site/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { buildSite, previewRoot } from './build.ts'
import { renderMarkdown, splitFrontMatter } from './markdown.ts'
import { escapeHtml, localLink, htmlPath, pairedRoutes, sourceRoute } from './routes.ts'
import { documentationPages } from '../lib/docs-pages.ts'
import { urlForPage } from '../../docs/.vitepress/site-meta.ts'
import { urlForPage } from './routes.ts'
import { validateSitemap } from '../lib/sitemap-policy.ts'
import { LATEST_RELEASE } from '../lib/command-finder.ts'
import { currentPublishedCompilerTag, currentCompilerReleaseRoute } from './current-release.ts'
Expand Down
10 changes: 5 additions & 5 deletions scripts/test-live-site.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict'
import { DOCS_BASE, SITE_ORIGIN, pairedUrls, urlForPage } from '../docs/.vitepress/site-meta.ts'
import { BASE, ORIGIN, pairedRoutes, urlForPage } from './scarp-site/routes.ts'
import { LIVE_PAGE_SOURCES, verifyLiveSite, type SiteFetcher } from './lib/live-site.ts'

const htmlRouteByUrl = new Map(LIVE_PAGE_SOURCES.map(source => [urlForPage(source), source]))

function fakeSite(options: { wrongCanonical?: boolean; missingHindi?: boolean; missingInstaller?: boolean; missingCss?: boolean; missingSitemapPage?: boolean; duplicateSitemapPage?: boolean } = {}): SiteFetcher {
return async url => {
if (url === SITE_ORIGIN + DOCS_BASE + 'sitemap.xml') {
if (url === ORIGIN + BASE + 'sitemap.xml') {
const sourcePages = options.missingSitemapPage
? LIVE_PAGE_SOURCES.filter(source => source !== 'hi/community/social.md')
: [...LIVE_PAGE_SOURCES]
Expand All @@ -18,7 +18,7 @@ function fakeSite(options: { wrongCanonical?: boolean; missingHindi?: boolean; m
{ status: 200, headers: { 'content-type': 'application/xml' } }
)
}
if (url.startsWith(SITE_ORIGIN + DOCS_BASE + 'assets/')) {
if (url.startsWith(ORIGIN + BASE + 'assets/')) {
return new Response('asset', { status: 200, headers: { 'content-type': 'application/octet-stream' } })
}
const source = htmlRouteByUrl.get(url)
Expand All @@ -29,7 +29,7 @@ function fakeSite(options: { wrongCanonical?: boolean; missingHindi?: boolean; m
if (options.missingInstaller && source === 'hi/guide/installation-alpha6.md') {
return new Response('Not found', { status: 404 })
}
const links = pairedUrls(source)
const links = pairedRoutes(source)
const lang = source.startsWith('hi/') ? 'hi-IN' : 'en-US'
const canonical = options.wrongCanonical && source === 'guide/learning-path.md'
? links.hindi : links.canonical
Expand All @@ -50,7 +50,7 @@ function fakeSite(options: { wrongCanonical?: boolean; missingHindi?: boolean; m
const healthy = await verifyLiveSite(fakeSite(), LIVE_PAGE_SOURCES)
assert.equal(healthy.pages, LIVE_PAGE_SOURCES.length)
assert.equal(healthy.assets, 2)
assert.equal(healthy.sitemap, SITE_ORIGIN + DOCS_BASE + 'sitemap.xml')
assert.equal(healthy.sitemap, ORIGIN + BASE + 'sitemap.xml')
assert.equal(healthy.sitemapPages, LIVE_PAGE_SOURCES.length)
await assert.rejects(() => verifyLiveSite(fakeSite({ wrongCanonical: true }), LIVE_PAGE_SOURCES), /canonical mismatch/)
await assert.rejects(() => verifyLiveSite(fakeSite({ missingHindi: true }), LIVE_PAGE_SOURCES), /HTTP 200, got 404/)
Expand Down
11 changes: 8 additions & 3 deletions scripts/test-site-meta.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import assert from 'node:assert/strict'
import { pairedUrls, pageSeoHead, urlForPage } from '../docs/.vitepress/site-meta.ts'
import { pairedRoutes, urlForPage } from './scarp-site/routes.ts'
import { pageSeoHead, renderSeoHead } from './scarp-site/seo.ts'

const en = pairedUrls('guide/first-program.md')
const hi = pairedUrls('hi/guide/first-program.md')
const en = pairedRoutes('guide/first-program.md')
const hi = pairedRoutes('hi/guide/first-program.md')
assert.equal(en.english, 'https://scarp-lang.github.io/scarp-docs/guide/first-program')
assert.equal(en.hindi, 'https://scarp-lang.github.io/scarp-docs/hi/guide/first-program')
assert.equal(en.canonical, en.english)
Expand All @@ -23,4 +24,8 @@ assert.deepEqual(enHead.filter(([tag, props]) => tag === 'link' && props.rel ===
assert.deepEqual(hiHead.filter(([tag, props]) => tag === 'link' && props.rel === 'alternate').map(([, props]) => props.hreflang), ['en', 'hi', 'x-default'])
assert.equal(hiHead.find(([tag, props]) => tag === 'meta' && props.property === 'og:locale')?.[1].content, 'hi_IN')
assert.equal(pageSeoHead('404.md').length, 0)
assert.match(renderSeoHead('hi/index.md'), /<meta property="og:locale" content="hi_IN"\/>/)
assert.match(renderSeoHead('guide/first-program.md'), /<link rel="canonical" href="https:\/\/scarp-lang.github.io\/scarp-docs\/guide\/first-program"\/>/)
assert.ok(!renderSeoHead('hi/index.md').includes('<script'))
assert.equal(urlForPage('guide/कक्षा.md'), 'https://scarp-lang.github.io/scarp-docs/guide/%E0%A4%95%E0%A4%95%E0%A5%8D%E0%A4%B7%E0%A4%BE')
console.log('PASS: canonical URL, paired hreflang, homepage, locale and invalid path checks')
4 changes: 2 additions & 2 deletions scripts/test-sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { urlForPage } from '../docs/.vitepress/site-meta.ts'
import { urlForPage } from './scarp-site/routes.ts'
import { documentationPages } from './lib/docs-pages.ts'
import { validateSitemap } from './lib/sitemap-policy.ts'

Expand Down Expand Up @@ -41,7 +41,7 @@ mustReject('duplicate URL', sitemap([...urls, urls[indexHi]]), /Duplicate URLs/)
mustReject('foreign host',
sitemap(urls.map((url, index) => index === indexEn
? url.replace('scarp-lang.github.io', 'example.invalid') : url)), /Unexpected sitemap URL/)
mustReject('wrong VitePress base',
mustReject('wrong GitHub Pages base',
sitemap(urls.map((url, index) => index === indexHi
? url.replace('/scarp-docs/', '/other-docs/') : url)), /Unexpected sitemap URL/)
mustReject('HTML extension',
Expand Down
1 change: 0 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
},
"include": [
"scripts/**/*.ts",
"docs/.vitepress/site-meta.ts",
"docs/.vitepress/local-search.ts",
"docs/.vitepress/theme/lib/**/*.ts"
]
Expand Down
Loading