The source code for the Nimiq website.
Our website is built with the following technologies:
- Nuxt 4: The core framework powering our application
- UnoCSS: Utility-first CSS engine with enhanced flexibility
- Prismic: Headless CMS for content management
- Reka UI: Component library (similar to Radix UI)
Make sure to install the dependencies:
pnpm install
pnpm dev
npx simple-git-hooks # Optionally install git hooks to run linters on commit
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. The content is managed through the Prismic dashboard, and the custom types are defined in the slicemachine
.
In order to modify the slices, you need to run locally the Prismic CLI:
pnpm slicemachine
Then, go to the http://localhost:9999
URL to see the Prismic Slicemachine interface and edit the slices.
Once you are done making changes you should see the changes in the ./app/slices
:
- If you modified an existing slice, the types will be updated and therefore the linter will complain. You can use
pnpm run lint
to see the errors. - If you added a new slice, a new folder will be created in
./app/slices
. I recommend you to use this template:
<script setup lang="ts">
import type { Content } from '@prismicio/client'
const { slice } = defineProps(getSliceComponentProps<Content.YourNewSlice>()) // Safely typed
const bgClass = getColorClass(slice.primary.bgColor)
</script>
<template>
<section :class="bgClass">
Your new slice content goes 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 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
Components are organized in the following structure:
- General components in
components/
root - UI components in
components/[UI]/
- Background components in
components/[Backgrounds]/
- Feature-specific components in dedicated folders (e.g.,
components/Wallet/
)
- Use typed props with Vue's
defineProps
- Prefer composables for shared logic
- Follow the single responsibility principle
- Document complex components with inline comments
- Use Prismic slice components for CMS-driven content
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 is deployed through GitHub Actions workflows:
- PRs trigger preview deployments to GitHub Pages
- Merged changes to main deploy to staging
- Manual deployment to production.
- 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