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: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/general.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/library-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions .github/ISSUE_TEMPLATE/new-exploration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

## Taxonomy (new explorations only)

<!-- Fill in if adding a new exploration. See https://docs.feelyourprotocol.org/guide/architecture -->
<!-- Fill in if adding a new exploration. See https://docs.feelyourprotocol.org/guide/architecture.html -->

- **Topic:** <!-- e.g. Scaling -->
- **Timeline:** <!-- e.g. Fusaka -->
Expand All @@ -30,13 +30,13 @@

<!-- Tick off what applies. Not every item is required for every PR. -->

- [ ] 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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion community-token/src/components/SiteFooter.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
const MAIN_SITE_URL = 'https://feelyourprotocol.org'
const DOCS_URL = 'https://docs.feelyourprotocol.org'
const DOCS_URL = 'https://docs.feelyourprotocol.org/index.html'
const GITHUB_URL = 'https://github.com/feelyourprotocol/website'
const LAST_UPDATED = 'June 5, 2026'
</script>
Expand Down
10 changes: 10 additions & 0 deletions cypress/e2e/sites.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('/')
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions scripts/generate-spa-fallbacks.ts
Original file line number Diff line number Diff line change
@@ -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`,
)
9 changes: 9 additions & 0 deletions scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.node.json",
"include": ["./**/*.ts", "../env.d.ts"],
"compilerOptions": {
"paths": {
"@/*": ["../src/*"]
}
}
}
2 changes: 2 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ watch(
>Community Token</a
>
<span class="text-purple-500 mx-2">◆</span>
<a href="https://x.com/FeelEthereum" target="_blank" rel="noopener">X</a>
<span class="text-purple-500 mx-2">◆</span>
<a href="https://github.com/feelyourprotocol/website" target="_blank" rel="noopener"
>GitHub</a
>
Expand Down
11 changes: 10 additions & 1 deletion src/explorations/REGISTRY.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,20 @@ export interface Explorations {
[key: string]: Exploration
}

export function pickRandom<T>(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[] {
Expand Down
19 changes: 19 additions & 0 deletions src/libs/__tests__/docsUrls.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
)
})
})
59 changes: 59 additions & 0 deletions src/libs/__tests__/spaRoutes.spec.ts
Original file line number Diff line number Diff line change
@@ -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('<?xml version="1.0"')
expect(xml).toContain(`${SITE_ORIGIN}/scaling`)
expect(xml).not.toContain('/404')

const robots = generateRobotsTxt()
expect(robots).toContain(`Sitemap: ${SITE_ORIGIN}/sitemap.xml`)
expect(robots).toContain('Allow: /')
})
})
12 changes: 12 additions & 0 deletions src/libs/docsUrls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** Production docs URLs (static host serves VitePress output as `.html` files). */
export const DOCS_ORIGIN = 'https://docs.feelyourprotocol.org'

export function docsPage(path = '', hash?: string): string {
const slug = path.replace(/^\//, '').replace(/\.html$/, '')
const fragment = hash ? `#${hash.replace(/^#/, '')}` : ''
if (!slug) return `${DOCS_ORIGIN}/index.html${fragment}`
return `${DOCS_ORIGIN}/${slug}.html${fragment}`
}

export const DOCS_HOME = docsPage()
export const DOCS_ADD_EXPLORATION = docsPage('contributing/adding-an-exploration')
58 changes: 58 additions & 0 deletions src/libs/spaRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Valid routes and SEO artifacts for the main site (SPA = Single Page Application).
*
* This module is the source of truth for which URLs exist in the Vue app. It is
* imported by unit tests and by the post-build script `scripts/generate-spa-fallbacks.ts`,
* which writes static files into `dist/website/` for nginx (see server-config README).
*
* Keep route lists in sync with `src/router/index.ts` — both derive from REGISTRY/TOPICS.
*/
import { EXPLORATIONS } from '@/explorations/REGISTRY'
import { TOPICS } from '@/explorations/TOPICS'

export const SITE_ORIGIN = 'https://feelyourprotocol.org'

/** Paths handled by Vue Router (excluding the catch-all 404 route). */
export function getValidSpaPaths(): string[] {
const paths = ['/', '/imprint', '/all']

for (const topic of Object.values(TOPICS)) {
paths.push(topic.path)
}
for (const exploration of Object.values(EXPLORATIONS)) {
paths.push(exploration.path)
}

return paths
}

/** Canonical absolute URLs for sitemap.xml (same routes as the SPA). */
export function getSitemapUrls(): string[] {
return getValidSpaPaths().map((path) =>
path === '/' ? `${SITE_ORIGIN}/` : `${SITE_ORIGIN}${path}`,
)
}

export function generateSitemapXml(): string {
const urls = getSitemapUrls()
const entries = urls.map((loc) => ` <url><loc>${loc}</loc></url>`).join('\n')

return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
entries,
'</urlset>',
'',
].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(/^\//, ''))
}
11 changes: 10 additions & 1 deletion src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading