diff --git a/.github/ISSUE_TEMPLATE/general.yml b/.github/ISSUE_TEMPLATE/general.yml index a774aab..2cee510 100644 --- a/.github/ISSUE_TEMPLATE/general.yml +++ b/.github/ISSUE_TEMPLATE/general.yml @@ -8,7 +8,7 @@ body: value: | Use this template for questions, feature ideas, or anything that doesn't fit the other templates. - **Docs:** [How to Contribute](https://docs.feelyourprotocol.org/contributing/how-to-contribute) · [Architecture](https://docs.feelyourprotocol.org/guide/architecture) + **Docs:** [How to Contribute](https://docs.feelyourprotocol.org/contributing/how-to-contribute.html) · [Architecture](https://docs.feelyourprotocol.org/guide/architecture.html) - type: textarea id: description diff --git a/.github/ISSUE_TEMPLATE/library-request.yml b/.github/ISSUE_TEMPLATE/library-request.yml index 702511e..4b6a33d 100644 --- a/.github/ISSUE_TEMPLATE/library-request.yml +++ b/.github/ISSUE_TEMPLATE/library-request.yml @@ -8,7 +8,7 @@ body: value: | Use this template to request a new third-party dependency or a managed fork branch. - **Docs:** [Third-Party Libraries](https://docs.feelyourprotocol.org/contributing/third-party-libraries) + **Docs:** [Third-Party Libraries](https://docs.feelyourprotocol.org/contributing/third-party-libraries.html) - type: dropdown id: type diff --git a/.github/ISSUE_TEMPLATE/new-exploration.yml b/.github/ISSUE_TEMPLATE/new-exploration.yml index bc2d5e4..48ae000 100644 --- a/.github/ISSUE_TEMPLATE/new-exploration.yml +++ b/.github/ISSUE_TEMPLATE/new-exploration.yml @@ -8,7 +8,7 @@ body: value: | Thanks for proposing a new exploration! Please fill out the sections below so we can discuss scope, library needs, and taxonomy placement before you start coding. - **Docs:** [Adding an Exploration](https://docs.feelyourprotocol.org/contributing/adding-an-exploration) · [Architecture](https://docs.feelyourprotocol.org/guide/architecture) + **Docs:** [Adding an Exploration](https://docs.feelyourprotocol.org/contributing/adding-an-exploration.html) · [Architecture](https://docs.feelyourprotocol.org/guide/architecture.html) - type: input id: eip @@ -31,7 +31,7 @@ body: id: topic attributes: label: Topic - description: "Which strategic pillar does this belong to? See [Topics](https://docs.feelyourprotocol.org/guide/architecture#topics)." + description: "Which strategic pillar does this belong to? See [Topics](https://docs.feelyourprotocol.org/guide/architecture.html#topics)." options: - Scaling - Privacy @@ -47,7 +47,7 @@ body: id: timeline attributes: label: Timeline - description: "Where does this sit on the protocol timeline? See [Timeline](https://docs.feelyourprotocol.org/guide/architecture#timeline)." + description: "Where does this sit on the protocol timeline? See [Timeline](https://docs.feelyourprotocol.org/guide/architecture.html#timeline)." options: - Fusaka - Glamsterdam @@ -62,7 +62,7 @@ body: id: tags attributes: label: Tags (up to 3–4) - description: "Existing tags or proposed new ones. See [Tags](https://docs.feelyourprotocol.org/guide/architecture#tags) for rules." + description: "Existing tags or proposed new ones. See [Tags](https://docs.feelyourprotocol.org/guide/architecture.html#tags) for rules." placeholder: "e.g. EVM, Gas Costs, Precompiles" - type: textarea @@ -72,7 +72,7 @@ body: description: | Which libraries will the exploration use? Are they already in `package.json`? Do you need a custom fork or new dependency? - See [Third-Party Libraries](https://docs.feelyourprotocol.org/contributing/third-party-libraries) for the process. + See [Third-Party Libraries](https://docs.feelyourprotocol.org/contributing/third-party-libraries.html) for the process. placeholder: "e.g. @ethereumjs/evm (already available), noble-curves (needs adding)" - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0932685..ccca10e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ ## Taxonomy (new explorations only) - + - **Topic:** - **Timeline:** @@ -30,13 +30,13 @@ -- [ ] I have read the [contributing guide](https://docs.feelyourprotocol.org/contributing/how-to-contribute) +- [ ] I have read the [contributing guide](https://docs.feelyourprotocol.org/contributing/how-to-contribute.html) - [ ] Linting and type checking pass (`npm run lf && npm run type-check`) - [ ] Unit tests pass (`npx vitest run`) - [ ] E2E tests pass (`npm run test:e2e`) - [ ] Production build succeeds (`npm run build` — website + community-token + docs) - [ ] New exploration is registered in `REGISTRY.ts` -- [ ] Library needs were discussed in a separate issue (if applicable — see [Third-Party Libraries](https://docs.feelyourprotocol.org/contributing/third-party-libraries)) +- [ ] Library needs were discussed in a separate issue (if applicable — see [Third-Party Libraries](https://docs.feelyourprotocol.org/contributing/third-party-libraries.html)) ## Screenshots / recordings diff --git a/README.md b/README.md index 075bcc3..e9e6366 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Production builds (`dist/website`, `dist/docs`, `dist/community-token`) are **no ## Documentation -Full documentation is available at **[docs.feelyourprotocol.org](https://docs.feelyourprotocol.org)**. +Full documentation is available at **[docs.feelyourprotocol.org](https://docs.feelyourprotocol.org/index.html)**. ## Community Token Site diff --git a/community-token/src/components/SiteFooter.vue b/community-token/src/components/SiteFooter.vue index cf87165..6d78d7f 100644 --- a/community-token/src/components/SiteFooter.vue +++ b/community-token/src/components/SiteFooter.vue @@ -1,6 +1,6 @@ diff --git a/cypress/e2e/sites.cy.ts b/cypress/e2e/sites.cy.ts index c757117..d053445 100644 --- a/cypress/e2e/sites.cy.ts +++ b/cypress/e2e/sites.cy.ts @@ -34,6 +34,16 @@ describe('Imprint', () => { }) }) +describe('404', () => { + it('shows a friendly not-found page with navigation options', () => { + cy.visit('/this-path-does-not-exist', { failOnStatusCode: false }) + cy.contains("This path isn't in the registry.") + cy.contains('← Home') + cy.contains('Add an exploration') + cy.get('img').should('be.visible') + }) +}) + describe('Navigation', () => { it('full navigation flow through the site', () => { cy.visit('/') diff --git a/package.json b/package.json index ca8dfe9..4a352a3 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,9 @@ "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", "type-check": "vue-tsc --build", "website:build": "run-p type-check \"website:build-only {@}\" --", - "website:build-only": "vite build", - "website:build:deploy": "vite build", + "website:build-only": "vite build && npm run generate:spa-fallbacks", + "website:build:deploy": "vite build && npm run generate:spa-fallbacks", + "generate:spa-fallbacks": "vite-node scripts/generate-spa-fallbacks.ts", "website:preview": "vite preview --outDir dist/website", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", diff --git a/scripts/generate-spa-fallbacks.ts b/scripts/generate-spa-fallbacks.ts new file mode 100644 index 0000000..eed824e --- /dev/null +++ b/scripts/generate-spa-fallbacks.ts @@ -0,0 +1,38 @@ +/** + * Post-build step (`npm run generate:spa-fallbacks`, after `vite build`). + * + * 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` + * + * Route logic lives in spaRoutes.ts (testable, no filesystem I/O); this file only writes files. + */ +import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + generateRobotsTxt, + generateSitemapXml, + getSpaFallbackDirectories, +} from '../src/libs/spaRoutes' + +const outDir = join(dirname(fileURLToPath(import.meta.url)), '../dist/website') +const indexPath = join(outDir, 'index.html') + +for (const dir of getSpaFallbackDirectories()) { + const targetDir = join(outDir, dir) + mkdirSync(targetDir, { recursive: true }) + copyFileSync(indexPath, join(targetDir, 'index.html')) +} + +copyFileSync(indexPath, join(outDir, '404.html')) +writeFileSync(join(outDir, 'sitemap.xml'), generateSitemapXml()) +writeFileSync(join(outDir, 'robots.txt'), generateRobotsTxt()) + +console.log( + `Wrote ${getSpaFallbackDirectories().length} SPA fallbacks, 404.html, sitemap.xml, robots.txt`, +) diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..925672c --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./**/*.ts", "../env.d.ts"], + "compilerOptions": { + "paths": { + "@/*": ["../src/*"] + } + } +} diff --git a/src/App.vue b/src/App.vue index e7dd407..6fdf0a3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -125,6 +125,8 @@ watch( >Community Token + X + GitHub diff --git a/src/explorations/REGISTRY.ts b/src/explorations/REGISTRY.ts index ade183f..13e4164 100644 --- a/src/explorations/REGISTRY.ts +++ b/src/explorations/REGISTRY.ts @@ -42,11 +42,20 @@ export interface Explorations { [key: string]: Exploration } +export function pickRandom(items: T[]): T | undefined { + if (items.length === 0) return undefined + return items[Math.floor(Math.random() * items.length)] +} + +export function getRandomExplorationWithImage(): Exploration | undefined { + return pickRandom(Object.values(EXPLORATIONS).filter((e) => e.image)) +} + export function getRandomTopicExplorationImage(topicId: string): string | undefined { const images = Object.values(EXPLORATIONS) .filter((e) => e.topic === topicId && e.image) .map((e) => e.image!) - return images.length > 0 ? images[Math.floor(Math.random() * images.length)] : undefined + return pickRandom(images) } export function getTopicExplorationIds(topicId: string): string[] { diff --git a/src/libs/__tests__/docsUrls.spec.ts b/src/libs/__tests__/docsUrls.spec.ts new file mode 100644 index 0000000..5b190ad --- /dev/null +++ b/src/libs/__tests__/docsUrls.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' + +import { DOCS_ADD_EXPLORATION, DOCS_HOME, docsPage } from '../docsUrls' + +describe('docsUrls', () => { + it('builds home URL with index.html', () => { + expect(DOCS_HOME).toBe('https://docs.feelyourprotocol.org/index.html') + expect(docsPage()).toBe(DOCS_HOME) + }) + + it('builds page URLs with .html suffix', () => { + expect(DOCS_ADD_EXPLORATION).toBe( + 'https://docs.feelyourprotocol.org/contributing/adding-an-exploration.html', + ) + expect(docsPage('guide/architecture', 'topics')).toBe( + 'https://docs.feelyourprotocol.org/guide/architecture.html#topics', + ) + }) +}) diff --git a/src/libs/__tests__/spaRoutes.spec.ts b/src/libs/__tests__/spaRoutes.spec.ts new file mode 100644 index 0000000..2e6508b --- /dev/null +++ b/src/libs/__tests__/spaRoutes.spec.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' + +import { EXPLORATIONS } from '@/explorations/REGISTRY' +import { TOPICS } from '@/explorations/TOPICS' + +import { + generateRobotsTxt, + generateSitemapXml, + getSitemapUrls, + getSpaFallbackDirectories, + getValidSpaPaths, + SITE_ORIGIN, +} from '../spaRoutes' + +describe('spaRoutes', () => { + it('lists all router paths except the catch-all 404 route', () => { + const paths = getValidSpaPaths() + + expect(paths).toContain('/') + expect(paths).toContain('/imprint') + expect(paths).toContain('/all') + + for (const topic of Object.values(TOPICS)) { + expect(paths).toContain(topic.path) + } + for (const exploration of Object.values(EXPLORATIONS)) { + expect(paths).toContain(exploration.path) + } + }) + + it('derives fallback directories for nginx try_files', () => { + const dirs = getSpaFallbackDirectories() + + expect(dirs).not.toContain('') + expect(dirs).toContain('imprint') + expect(dirs).toContain('scaling') + expect(dirs).toContain(Object.values(EXPLORATIONS)[0]!.path.replace(/^\//, '')) + }) + + it('builds sitemap URLs for all valid SPA paths', () => { + const urls = getSitemapUrls() + + expect(urls).toContain(`${SITE_ORIGIN}/`) + expect(urls).toContain(`${SITE_ORIGIN}/imprint`) + expect(urls.length).toBe(getValidSpaPaths().length) + expect(urls.every((url) => url.startsWith(SITE_ORIGIN))).toBe(true) + }) + + it('generates sitemap.xml and robots.txt', () => { + const xml = generateSitemapXml() + expect(xml).toContain(' + path === '/' ? `${SITE_ORIGIN}/` : `${SITE_ORIGIN}${path}`, + ) +} + +export function generateSitemapXml(): string { + const urls = getSitemapUrls() + const entries = urls.map((loc) => ` ${loc}`).join('\n') + + return [ + '', + '', + entries, + '', + '', + ].join('\n') +} + +export function generateRobotsTxt(): string { + return ['User-agent: *', 'Allow: /', '', `Sitemap: ${SITE_ORIGIN}/sitemap.xml`, ''].join('\n') +} + +/** Directory names under dist/website/ that receive a copied index.html (all except `/`). */ +export function getSpaFallbackDirectories(): string[] { + return getValidSpaPaths() + .filter((path) => path !== '/') + .map((path) => path.replace(/^\//, '')) +} diff --git a/src/router/index.ts b/src/router/index.ts index d386922..80173d8 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -45,7 +45,16 @@ function loadRoutes() { }) } - return [...homeRs, ...explorationRs, ...topicRs] + return [ + ...homeRs, + ...explorationRs, + ...topicRs, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: baseViews['../views/NotFoundView.vue'], + }, + ] } const router = createRouter({ diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 434847a..29340c9 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -2,6 +2,7 @@ import ExplorationC from '@/explorations/ExplorationC.vue' import { EXPLORATIONS, getRandomTopicExplorationImage } from '@/explorations/REGISTRY' import { TOPICS } from '@/explorations/TOPICS' +import { DOCS_HOME } from '@/libs/docsUrls' import TagCloudView from './TagCloudView.vue' import TimelineNaviView from './TimelineNaviView.vue' @@ -58,7 +59,7 @@ for (const topicId of activeTopicIds) {

Want to contribute? Check the docs +import { getRandomExplorationWithImage } from '@/explorations/REGISTRY' +import { TOPIC_COLORS, topicCSSVars, TOPICS } from '@/explorations/TOPICS' +import { DOCS_ADD_EXPLORATION } from '@/libs/docsUrls' + +const pick = getRandomExplorationWithImage() +const topic = pick ? TOPICS[pick.topic] : undefined +const cssVars = topic ? topicCSSVars(topic.color) : undefined +const borderCard = topic ? TOPIC_COLORS[topic.color].classes.borderCard : 'border border-slate-200' + +function getImageUrl(image: string): string { + return image.includes('/') + ? image + : new URL(`../assets/imgs/dancers/${image}`, import.meta.url).href +} + + + diff --git a/src/views/__tests__/AppLayout.spec.ts b/src/views/__tests__/AppLayout.spec.ts index 3b63340..82a456b 100644 --- a/src/views/__tests__/AppLayout.spec.ts +++ b/src/views/__tests__/AppLayout.spec.ts @@ -104,5 +104,11 @@ describe('App layout', () => { const ghLink = wrapper.find('footer a[href="https://github.com/feelyourprotocol/website"]') expect(ghLink.exists()).toBe(true) }) + + it('has X link', async () => { + const wrapper = await mountApp(makeRouter()) + const xLink = wrapper.find('footer a[href="https://x.com/FeelEthereum"]') + expect(xLink.exists()).toBe(true) + }) }) }) diff --git a/src/views/__tests__/HomeView.spec.ts b/src/views/__tests__/HomeView.spec.ts index a1c7819..0932bc2 100644 --- a/src/views/__tests__/HomeView.spec.ts +++ b/src/views/__tests__/HomeView.spec.ts @@ -5,6 +5,7 @@ import { mount, RouterLinkStub } from '@vue/test-utils' import { EXPLORATIONS } from '@/explorations/REGISTRY' import { Tag } from '@/explorations/TAGS' import { TOPICS } from '@/explorations/TOPICS' +import { DOCS_HOME } from '@/libs/docsUrls' import HomeView from '../HomeView.vue' @@ -58,7 +59,7 @@ describe('HomeView', () => { }) it('has contributor docs link pointing to docs site', () => { - const link = wrapper.find('a[href="https://docs.feelyourprotocol.org"]') + const link = wrapper.find(`a[href="${DOCS_HOME}"]`) expect(link.exists()).toBe(true) expect(link.attributes('target')).toBe('_blank') }) diff --git a/src/views/__tests__/NotFoundView.spec.ts b/src/views/__tests__/NotFoundView.spec.ts new file mode 100644 index 0000000..b8e0f67 --- /dev/null +++ b/src/views/__tests__/NotFoundView.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' +import { createMemoryHistory, createRouter } from 'vue-router' +import { mount } from '@vue/test-utils' + +import { EXPLORATIONS } from '@/explorations/REGISTRY' +import { DOCS_ADD_EXPLORATION } from '@/libs/docsUrls' +import NotFoundView from '@/views/NotFoundView.vue' + +function makeRouter() { + const explorationRoutes = Object.entries(EXPLORATIONS).map(([id, e]) => ({ + path: e.path, + name: id, + component: { template: '

' }, + })) + + return createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/', component: { template: '
' } }, ...explorationRoutes], + }) +} + +describe('NotFoundView', () => { + it('shows a 404 headline and contributor call-to-action', () => { + const wrapper = mount(NotFoundView, { + global: { plugins: [makeRouter()] }, + }) + + expect(wrapper.text()).toContain('404') + expect(wrapper.text()).toContain("This path isn't in the registry.") + expect(wrapper.text()).toContain('Add an exploration') + expect(wrapper.text()).toContain('← Home') + }) + + it('links to the contributor guide', () => { + const wrapper = mount(NotFoundView, { + global: { plugins: [makeRouter()] }, + }) + + const docsLinks = wrapper.findAll(`a[href="${DOCS_ADD_EXPLORATION}"]`) + expect(docsLinks.length).toBeGreaterThanOrEqual(1) + }) + + it('shows a random exploration image when available', () => { + const wrapper = mount(NotFoundView, { + global: { plugins: [makeRouter()] }, + }) + + const img = wrapper.find('img') + expect(img.exists()).toBe(true) + expect(wrapper.text()).toContain('Random pick') + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 5304731..501c04e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,9 @@ }, { "path": "./tsconfig.vitest.json" + }, + { + "path": "./scripts/tsconfig.json" } ], "compilerOptions": {