The source code for the Nimiq website.
- Quick Start Guide
- Architecture Overview
- Custom Modules & Extensions
- Component Architecture
- Prismic + Slicemachine
- Code Style Guide
- Available Scripts
- Testing
- Development Setup
- Dynamic Page Generation
- API Endpoints
- CI/CD Workflow
- Debugging & Troubleshooting
- Security & Best Practices
- Contributing Guidelines
- Node.js (version 18 or higher)
- pnpm package manager
- Access to Prismic CMS (for content management)
-
Clone and install dependencies
git clone https://github.com/nimiq/nimiq-website.git cd nimiq-website pnpm install
-
Environment configuration
cp .env.example .env # Edit .env with your PRISMIC_ACCESS_TOKEN (required)
-
Start development server
pnpm dev
-
Optional: Install git hooks
npx simple-git-hooks
- Build fails: Ensure
PRISMIC_ACCESS_TOKEN
is set in your.env
file - Components not rendering: Check if you're connected to the internet (required for Prismic)
- Linting errors: Run
pnpm lint
to see all issues,pnpm lint:fix
to auto-fix
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Nuxt 4 App │ │ Prismic CMS │ │ UnoCSS │
│ │◄───┤ │ │ │
│ - SSG/SPA │ │ - Content Mgmt │ │ - Atomic CSS │
│ - Vue Components│ │ - Slices │ │ - Nimiq Preset │
│ - Composables │ │ - API/CDN │ │ - Responsive │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
┌─────────────────┐
│ Static Build │
│ │
│ - Pre-rendered │
│ - CDN Ready │
│ - Multi-env │
└─────────────────┘
- Build Time: Prismic content is fetched and pre-rendered into static pages
- Runtime: Interactive components hydrate on client-side
- Content: Managed through Prismic Slicemachine, deployed via Git
The project supports multiple deployment environments:
- Local: Development with hot reload and draft content
- Production: Live site (nimiq.com)
- GitHub Pages: PR previews and testing
- NuxtHub: Serverless deployment with edge functions
- Internal Static: Internal previews with/without drafts
The project includes custom Nuxt modules for enhanced functionality:
prerender-routes.ts
- Automatically generates dynamic routes for static prerenderingrobots-generator.ts
- Generates SEO-optimized robots.txt files per environment
The project uses a catalog-based dependency system via pnpm-workspace.yaml
:
catalog:
frontend: # Frontend dependencies with shared versions
nuxt: # Nuxt ecosystem packages
nimiq: # Nimiq blockchain packages
prismic: # Prismic CMS packages
# ... other catalogs
Benefits:
- Consistent versions across all packages
- Easier dependency updates
- Reduced bundle size through deduplication
- Centralized package management
The project leverages WebAssembly for performance:
@nimiq/core
- Core blockchain functionality in WASMvite-plugin-wasm
- WASM support in Vitevite-plugin-top-level-await
- Modern async/await support
app/
├── components/
│ ├── [UI]/ # UI components (no prefix)
│ ├── [Backgrounds]/ # Background components
│ └── Feature/ # Feature-specific components
├── slices/ # Prismic slices (auto-generated)
│ ├── HeroSection/
│ ├── RichText/
│ └── ...
└── composables/ # Shared logic
- Linting: All code must pass
pnpm lint
(ESLint + stylistic rules) - Type checking: All code must pass
pnpm typecheck
(TypeScript strict mode) - Formatting: Follow ESLint configuration (runs automatically on save)
- Composables: Use VueUse first, then create custom composables
- Props: Always use typed props with Vue's
defineProps
- State: Use Pinia for complex state,
useState
for simple reactive data - Imports: Use auto-imports for Vue APIs and composables
pnpm lint # Check all linting issues
pnpm lint:fix # Auto-fix linting issues
pnpm typecheck # Check TypeScript types
The project uses simple-git-hooks
with lint-staged
for automatic code quality:
- Pre-commit hook: Automatically runs
pnpm lint:fix
on staged files - ESLint config: Uses
@antfu/eslint-config
with UnoCSS and Vue support - Install hooks: Run
npx simple-git-hooks
after cloning
pnpm build # Production build
pnpm build:github-pages # GitHub Pages build with prerendering
pnpm build:nuxthub # NuxtHub build (uses NUXTHUB_ENV)
pnpm build:internal-static # Internal static build (no drafts)
pnpm build:internal-static-drafts # Internal static build (with drafts)
pnpm generate # Generate static site
pnpm preview # Preview built site locally
pnpm dev # Start development server
pnpm slicemachine # Start Prismic Slicemachine interface
pnpm cryptomap:types # Generate Supabase types from schema
pnpm lint # Check linting issues
pnpm lint:fix # Auto-fix linting issues
pnpm typecheck # TypeScript type checking
Note: This project currently has no testing setup. All quality assurance is done through:
- TypeScript type checking
- ESLint static analysis
- Manual testing in different environments
- PR reviews and staging deployments
The project uses environment variables for configuration. You can find an example in .env.example
file. Copy it to create your own .env
file:
cp .env.example .env
The website can be built for different runtime environments, each with its own configuration. The environment is set using the NUXT_ENVIRONMENT
variable:
local
: Development environment (default when runningpnpm dev
)production
: Production environment (nimiq.com)github-pages
: GitHub Pages preview environmentnuxthub-production
: NuxtHub production environmentnuxthub-preview
: NuxtHub preview environmentinternal-static
: Internal static site that mirrors production (no drafts shown)internal-static-drafts
: Internal static site with draft content visible
The build commands in package.json
are set up to use these environments:
# Production build
pnpm build
# GitHub Pages build
pnpm build:github-pages
# NuxtHub builds (uses NUXTHUB_ENV to determine production/preview)
pnpm build:nuxthub
# Internal static builds
pnpm build:internal-static
pnpm build:internal-static-drafts
The runtime configuration includes environment-specific flags that can be accessed in your components:
const {
name, // Typed as EnvironmentName
isLocal,
isProduction,
isGitHubPages,
isNuxthubProduction,
isNuxthubPreview,
isInternalStatic,
isInternalStaticDrafts,
} = useRuntimeConfig().public.environment
// Draft content visibility: True in local and internal-static-drafts environments
const { showDrafts } = useRuntimeConfig().public
We use UnoCSS instead of TailwindCSS for more flexibility. Key features include:
- Nimiq UnoCSS: Custom utilities, reset, typography and base styles
- Nimiq icons: Custom icon set
- Attributify mode: Supports both traditional class strings and attribute syntax
- Custom presets (via
unocss-preset-onmax
):- Reka preset: Provides variants like reka-open:bg-pink → data-[state:open]:bg-pink
- Fluid sizing: Use
f-pt-md
for responsive padding that scales between breakpoints - Scale px: Different from Tailwind, p-4 equals 4px not 16px
app
: Nuxt application codeserver
: Backend server codecustomtypes
: Prismic custom type definitions. Automatically generated from Prismic slicemachine.shared
: Shared utilities and componentspublic
: Static assets
We use Prismic as our headless CMS with Slicemachine for content-driven components. The content is managed through the Prismic dashboard, and the custom types are defined in the slicemachine
.
- Start Slicemachine: Run
pnpm slicemachine
and go tohttp://localhost:9999
- Create/Edit slices: Define fields and variations in the Slicemachine interface
- Implement component: Use the template below in
app/slices/YourSlice/index.vue
- Check linting: Run
pnpm lint
to see any type errors from changes
All slices follow this standardized pattern:
<script setup lang="ts">
import type { Content } from '@prismicio/client'
// Always use typed props for slices
const { slice } = defineProps(getSliceComponentProps<Content.YourSliceSlice>())
// Handle background colors consistently
const bgClass = getColorClass(slice.primary.bgColor)
</script>
<template>
<!-- All slices must be wrapped in a section -->
<section :class="bgClass">
<!-- Slice content here -->
</section>
</template>
Note
It is important that all slices are wrapped in a section tag, so that the css can apply the correct styles. The section should have the background color: bg-neutral-0
, bg-neutral-100
or bg-darkblue
.
When you work with Prismic, you will create "documents" in your Prismic repository. We have multiple types of documents, each with its own purpose. Here are the main ones:
- "Pages": These are the main pages of our website. The
uid
field is used to create the URL for the page. For example, if you create a page with theuid
"about", it will be accessible athttps://nimiq.com/about
. - "Posts": These are blog posts. They are created in the same way as pages, but they are displayed in a different section of the website. The
uid
field is also used to create the URL for the post. - "Exchanges": These are the exchanges where Nimiq is listed. They are created in the same way as pages, but they are fetched from the Prismic API and displayed in a grid on the website inside the
Exchanges
component. - "Apps": In the past "Apps" were the same as "Exchanges", but now they have been migrated to the nimiq-awesome repo for easier management.
The draft
field is a boolean field that indicates whether the document is a draft or not. If the draft
field is set to true
, the document will be visible only in the local and internal-static-drafts environments. In all other environments, the document will be hidden.
Some pages require special CSS that is only loaded when those specific routes are accessed. This is handled through route middleware in the [...uid].vue
page component:
// Example from [...uid].vue
definePageMeta({
middleware: [
async function (to) {
// Special styling for specific pages
if (to.path === '/onepager')
await import('~/assets/css/onepager.css')
// Other conditionally loaded stylesheets can be added here
},
],
})
Additionally, certain pages may have special header styling (dark vs light) based on page data or specific route conditions:
// Logic for dark header styling
const darkHeader = computed(() => page.value?.data.darkHeader || isHome || uid === 'supersimpleswap')
When creating new pages that need special styling:
- Add the CSS file in
assets/css/
- Import it conditionally in the middleware function
- Consider if it needs special header/footer treatment
- Document any unique styling requirements
The website uses a dynamic page generation system powered by our crawler utility to pre-render routes for better performance.
The crawler.ts
utility is responsible for:
- Fetching all page and blog documents from Prismic
- Generating URL paths for static site generation
- Filtering draft content based on environment settings
- Supporting pagination for large content sets
This utility is integrated with Nuxt's prerendering system and is called during the build process to ensure all dynamic routes are generated properly.
The project includes several API endpoints for data fetching:
/api/exchanges
- Fetch exchange data from Prismic/api/albatross/*
- Blockchain data endpoints for Nimiq network/api/newsletter/*
- Newsletter subscription management
// Fetch exchange data
const exchanges = await $fetch('/api/exchanges')
// Use in composables
const { data } = await useFetch('/api/exchanges')
The project includes both frontend and backend components:
- API endpoints can be configured per environment via environment variables
- The
useRuntimeConfig()
composable provides access to API configuration in components - Prismic API is used for content retrieval with authentication
- Server routes are defined in the
server/
directory - API endpoints follow RESTful conventions
- Server middleware handles CORS and authentication
- NuxtHub integration provides serverless functions when enabled
The project uses Pinia for state management with additional features:
- Stores are located in
app/stores/
- The Pinia Colada plugin provides persistence capabilities
- Use the
useSyncedState
composable for reactive state that syncs across components - Environment-specific configuration is available via
useRuntimeConfig()
We build the website statically. We don't have SSR in production. This influences how we should fetch and manage data:
await useFetch
/useAsyncData
: Use for data that's only needed at build time and will be included in the static build- Pinia Colada (
useQuery
): Use when we need to fetch new data on every client visit - Pinia stores: Used primarily for legacy reasons; prefer the approaches above for new code
Important
Before proceeding with a new composable, make sure to check if it already exists in VueUse
.
It is preferable that all logic is wrapped in a composable, even if that composable is not shared. You can create inline composables within components:
<script setup>
// Inline composable example
function useMyFeature() {
const data = ref<string>()
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
data.value = await $fetch('/api/some-endpoint')
}
finally {
loading.value = false
}
}
return { data, loading, fetchData }
}
// Use the composable within the component
const { data, loading, fetchData } = useMyFeature()
</script>
This approach helps maintain cleaner, more testable code by:
- Separating concerns
- Making logic reusable
- Improving testability
- Creating clearer component structure
The project uses three GitHub Actions workflows for comprehensive CI/CD:
- Trigger: All pushes to any branch
- Actions: Linting, type checking, dependency installation
- Environment: Uses
preview
environment with secrets - Node: Version 22 with pnpm caching
- Trigger: Pull requests and main branch changes
- Purpose: Deploy preview builds to GitHub Pages
- URL:
https://nimiq.github.io/nimiq-website/
- Environment:
github-pages
with prerendering enabled
- Trigger: Main branch changes (production) and manual dispatch
- Purpose: Deploy to NuxtHub serverless infrastructure
- Environments: Both production and preview
- Features: Edge functions, KV storage, caching
Secrets:
PRISMIC_ACCESS_TOKEN
- Prismic CMS accessNUXT_ALBATROSS_NODE_RPC_URL
- Blockchain RPC endpointNUXT_PUBLIC_CRYPTO_MAP_SUPABASE_KEY
- Supabase authenticationNUXT_ZOHO_*
- Zoho CRM integration (optional)
Variables:
NUXT_HUB_ENV
- NuxtHub environmentNUXT_HUB_PROJECT_KEY
- NuxtHub project identifierNUXT_PUBLIC_CRYPTO_MAP_SUPABASE_URL
- Supabase URLNUXT_PUBLIC_VALIDATORS_API
- Validators API endpoint
Build Failures:
# Check environment variables
echo $PRISMIC_ACCESS_TOKEN # Should not be empty
# Clear cache and reinstall
rm -rf node_modules .nuxt .output
pnpm install
# Check internet connection (required for Prismic)
curl -I https://nimiq.cdn.prismic.io/api/v2
Type Errors:
# Regenerate Prismic types
# This happens automatically when slicemachine updates
pnpm typecheck
# Regenerate Supabase types
pnpm cryptomap:types
Linting Issues:
# See all issues
pnpm lint
# Auto-fix most issues
pnpm lint:fix
# Check specific files
pnpm lint path/to/file.vue
Development Server Issues:
# Clear Nuxt cache
rm -rf .nuxt
# Restart with fresh state
pnpm dev --force
Bundle Analysis:
# Build and analyze bundle
pnpm build
# Check .output/public for generated files
# Monitor build sizes
ls -la .output/public/_nuxt/
Network Issues:
- Check browser Network tab for failed requests
- Verify API endpoints are responding
- Ensure Prismic access token is valid
Recommended VS Code Extensions:
- Vue Language Features (Volar)
- TypeScript Vue Plugin
- ESLint
- UnoCSS
- Prismic
Settings:
{
"typescript.preferences.preferTypeOnlyAutoImports": true,
"vue.server.hybridMode": true
}
- Never commit secrets - Use
.env
files (gitignored) - Use different tokens for different environments
- Rotate tokens regularly - Especially for production
- CORS configuration - Defined in
nuxt.config.ts
- Rate limiting - Handled by deployment platforms
- Input validation - Use Valibot schemas for runtime validation
- Draft content - Only visible in local/internal-static-drafts environments
- Prismic access - Controlled via access tokens
- Static builds - No server-side secrets exposed
- Bundle optimization - Automatic code splitting
- Image optimization -
@nuxt/image
with multiple providers - Caching strategy - Static assets with long-term caching
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests and linters (automatic on PR)
- Submit a PR with a clear description. Link to any relevant issues.
- Follow the ESLint configuration
- Use TypeScript for all new code
- Document complex functions and components
- Follow the existing project structure