diff --git a/cypress/e2e/explorations.cy.ts b/cypress/e2e/explorations.cy.ts index 7d5a6a3..da0823e 100644 --- a/cypress/e2e/explorations.cy.ts +++ b/cypress/e2e/explorations.cy.ts @@ -1,8 +1,8 @@ describe('EIP-7883 ModExp', () => { it('loads and displays exploration content', () => { cy.visit('/eip-7883-modexp-gas-cost-increase') - cy.contains('h1', 'Feel Your Protocol') - cy.contains('h3', 'ModExp') + cy.get('header').contains('Feel Your Protocol') + cy.contains('h1', 'ModExp') cy.get('#eip-7883-c', { timeout: 10000 }).should('exist') }) @@ -22,8 +22,8 @@ describe('EIP-7883 ModExp', () => { describe('EIP-7594 PeerDAS', () => { it('loads and displays exploration content', () => { cy.visit('/eip-7594-peerdas-data-availability-sampling') - cy.contains('h1', 'Feel Your Protocol') - cy.contains('h3', 'Peer Data Availability Sampling') + cy.get('header').contains('Feel Your Protocol') + cy.contains('h1', 'Peer Data Availability Sampling') cy.get('#eip-7594-c', { timeout: 10000 }).should('exist') }) @@ -42,8 +42,8 @@ describe('EIP-7594 PeerDAS', () => { describe('EIP-7951 secp256r1', () => { it('loads and displays exploration content', () => { cy.visit('/eip-7951-secp256r1-precompile') - cy.contains('h1', 'Feel Your Protocol') - cy.contains('h3', 'secp256r1 Precompile Support') + cy.get('header').contains('Feel Your Protocol') + cy.contains('h1', 'secp256r1 Precompile Support') cy.get('#eip-7951-c', { timeout: 10000 }).should('exist') }) diff --git a/cypress/e2e/sites.cy.ts b/cypress/e2e/sites.cy.ts index d053445..0f5d3d0 100644 --- a/cypress/e2e/sites.cy.ts +++ b/cypress/e2e/sites.cy.ts @@ -57,7 +57,7 @@ describe('Navigation', () => { cy.get('footer').contains('Imprint').click() cy.url().should('include', '/imprint') - cy.contains('h1', 'Feel Your Protocol').click() + cy.get('header').contains('Feel Your Protocol').click() cy.url().should('eq', Cypress.config().baseUrl + '/') }) }) diff --git a/index.html b/index.html index 70652dc..a9b7a04 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + Feel Your Protocol diff --git a/scripts/generate-spa-fallbacks.ts b/scripts/generate-spa-fallbacks.ts index eed824e..17344e8 100644 --- a/scripts/generate-spa-fallbacks.ts +++ b/scripts/generate-spa-fallbacks.ts @@ -4,35 +4,80 @@ * nginx serves static files with `try_files $uri $uri/ =404` (no blanket index.html * fallback). This script materializes what nginx needs: * - * - `scaling/index.html`, … — one copy per valid SPA route so deep links return 200 - * - `404.html` — same app shell; Vue Router shows NotFoundView for unknown paths - * - `sitemap.xml` and `robots.txt` — canonical URLs from `src/libs/spaRoutes.ts` + * - Per-route `index.html` with injected title, meta, canonical, Open Graph, JSON-LD + * - `404.html` — same app shell with noindex meta + * - `sitemap.xml` and `robots.txt` * - * Route logic lives in spaRoutes.ts (testable, no filesystem I/O); this file only writes files. + * Route logic lives in `pageSeo.ts` (testable); this file handles filesystem writes. */ -import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs' +import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' +import { EXPLORATIONS } from '../src/explorations/REGISTRY' +import { TOPICS } from '../src/explorations/TOPICS' import { generateRobotsTxt, generateSitemapXml, + getPageSeoForPath, getSpaFallbackDirectories, -} from '../src/libs/spaRoutes' + getValidSpaPaths, + injectSeoIntoHtml, +} from '../src/libs/pageSeo' -const outDir = join(dirname(fileURLToPath(import.meta.url)), '../dist/website') +const scriptDir = dirname(fileURLToPath(import.meta.url)) +const websiteRoot = join(scriptDir, '..') +const outDir = join(websiteRoot, 'dist/website') const indexPath = join(outDir, 'index.html') +function toLastmod(date: Date): string { + return date.toISOString().slice(0, 10) +} + +function lastmodForPath(path: string): string { + if (path === '/' || path === '/all') { + return toLastmod(statSync(join(websiteRoot, 'package.json')).mtime) + } + + if (path === '/imprint') { + return toLastmod(statSync(join(websiteRoot, 'src/views/ImprintView.vue')).mtime) + } + + const exploration = Object.values(EXPLORATIONS).find((entry) => entry.path === path) + if (exploration) { + return toLastmod(statSync(join(websiteRoot, 'src/explorations', exploration.id, 'info.ts')).mtime) + } + + const topic = Object.values(TOPICS).find((entry) => entry.path === path) + if (topic && topic.explorations.length > 0) { + const mtimes = topic.explorations.map((id) => + statSync(join(websiteRoot, 'src/explorations', id, 'info.ts')).mtimeMs, + ) + return toLastmod(new Date(Math.max(...mtimes))) + } + + return toLastmod(statSync(join(websiteRoot, 'package.json')).mtime) +} + +const shellHtml = readFileSync(indexPath, 'utf8') +const lastmodByPath = Object.fromEntries(getValidSpaPaths().map((path) => [path, lastmodForPath(path)])) + +writeFileSync(indexPath, injectSeoIntoHtml(shellHtml, getPageSeoForPath('/'))) + for (const dir of getSpaFallbackDirectories()) { + const path = `/${dir}` const targetDir = join(outDir, dir) mkdirSync(targetDir, { recursive: true }) - copyFileSync(indexPath, join(targetDir, 'index.html')) + writeFileSync(join(targetDir, 'index.html'), injectSeoIntoHtml(shellHtml, getPageSeoForPath(path))) } -copyFileSync(indexPath, join(outDir, '404.html')) -writeFileSync(join(outDir, 'sitemap.xml'), generateSitemapXml()) +writeFileSync( + join(outDir, '404.html'), + injectSeoIntoHtml(shellHtml, { ...getPageSeoForPath('/404-not-found'), noindex: true }), +) +writeFileSync(join(outDir, 'sitemap.xml'), generateSitemapXml(lastmodByPath)) writeFileSync(join(outDir, 'robots.txt'), generateRobotsTxt()) console.log( - `Wrote ${getSpaFallbackDirectories().length} SPA fallbacks, 404.html, sitemap.xml, robots.txt`, + `Wrote ${getSpaFallbackDirectories().length + 1} SEO HTML shells, 404.html, sitemap.xml, robots.txt`, ) diff --git a/src/App.vue b/src/App.vue index 6fdf0a3..763abe5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -37,13 +37,13 @@ watch(