Complete guide for adding new content and categories to the StaPH-B Nexus search functionality.
- Overview
- Architecture
- Quick Start
- Adding a New Category
- Adding Content to Existing Categories
- Data Format Requirements
- Component Reference
- Advanced Customization
- Troubleshooting
The Nexus search system is a multi-category search interface that allows users to discover StaPH-B resources including:
- Pipelines - Bioinformatics analysis pipelines
- Trainings - Educational resources (ready to add)
- Resources - General resources (ready to add)
- Real-time search with instant filtering
- Category-based filtering
- Keyword tagging system
- Flexible data sources (CSV, JSON, Markdown)
- Responsive card-based results display
app/pages/nexus.vue ← Main search page (data + logic)
├── app/components/Search.vue ← Search input + category dropdown
├── app/components/SearchResultCard.vue ← Individual result display
└── content/ ← Data sources
├── pipelines/
│ └── piplines.csv ← Pipeline data (CSV)
└── [new-category]/ ← Your new categories here
1. Data Loading (nexus.vue)
↓
2. Data Transformation (parseKeywords, transformPipelineData)
↓
3. Search State (searchTerm, selectedCategory)
↓
4. Search Logic (handleSearch, createSearchableText)
↓
5. Display Results (SearchResultCard)
- Open
/content/pipelines/piplines.csv - Add a new row with the required fields:
pipeline_name,pipeline_url,pipeline_description,pipeline_language,pipeline_ownership,pipeline_keywords MyPipeline,https://github.com/user/repo,Analysis pipeline for XYZ,Nextflow,My Organization,"keyword1, keyword2, keyword3"
- Save and refresh - your content appears automatically! ✨
Follow these steps to add a completely new category (e.g., "Trainings", "Tools", "Datasets").
Create /content/trainings/trainings.csv:
training_name,training_url,training_description,training_format,training_provider,training_keywords
Introduction to Genomics,https://example.com/course,Learn genomic analysis basics,Online Course,StaPH-B,"genomics, beginner, bioinformatics"
Advanced Phylogenetics,https://example.com/advanced,Deep dive into phylogenetic analysis,Workshop,University of XYZ,"phylogenetics, advanced, trees"Create /content/trainings/trainings.json:
[
{
"name": "Introduction to Genomics",
"url": "https://example.com/course",
"description": "Learn genomic analysis basics",
"format": "Online Course",
"provider": "StaPH-B",
"keywords": ["genomics", "beginner", "bioinformatics"]
}
]Create /content/trainings/intro-genomics.md:
---
name: Introduction to Genomics
url: https://example.com/course
format: Online Course
provider: StaPH-B
keywords:
- genomics
- beginner
---
Learn genomic analysis basics with hands-on exercises.Edit /app/pages/nexus.vue:
// Around line 72-76
const searchCategories = [
{ label: 'Pipelines', value: 'pipelines' },
{ label: 'Trainings', value: 'trainings' }, // ← Add this
{ label: 'Resources', value: 'resources' }
]Add interface for your category data structure in /app/pages/nexus.vue:
// Add after PipelineRawData interface (around line 61)
interface TrainingRawData {
training_name: string
training_url: string
training_description: string
training_format: string
training_provider: string
training_keywords: string | string[]
}Add loader in /app/pages/nexus.vue (after line 77):
// Load trainings data
const { data: trainingsData } = await useAsyncData('trainings', async () => {
return await queryCollection('trainings').all()
})Add transformer in /app/pages/nexus.vue:
// Add after transformPipelineData (around line 100)
/**
* Transforms raw training data into SearchResultItem format
*/
const transformTrainingData = (rawData: TrainingRawData): SearchResultItem => ({
name: rawData.training_name || '',
url: rawData.training_url || '',
description: rawData.training_description || '',
category: 'Trainings',
language: rawData.training_format || '', // Reuse 'language' field for format
ownership: rawData.training_provider || '', // Reuse 'ownership' field for provider
keywords: parseKeywords(rawData.training_keywords)
})Update the allResults loading section (around line 105):
const allResults = ref<SearchResultItem[]>([])
// Load pipelines
if (pipelinesData.value?.[0]?.meta?.body) {
const csvData = pipelinesData.value[0].meta.body as PipelineRawData[]
allResults.value.push(...csvData.map(transformPipelineData))
}
// Load trainings
if (trainingsData.value?.[0]?.meta?.body) {
const csvData = trainingsData.value[0].meta.body as TrainingRawData[]
allResults.value.push(...csvData.map(transformTrainingData))
}If you want custom field labels for different categories, edit /app/components/SearchResultCard.vue:
<div v-if="item.language" class="result-card-meta">
<svg class="icon-code">...</svg>
<span>{{ getLabelForLanguage(item) }}</span>
</div>
<script>
const getLabelForLanguage = (item) => {
// For trainings, 'language' field contains 'format'
return item.language
}
</script>For Pipelines, edit /content/pipelines/piplines.csv:
pipeline_name,pipeline_url,pipeline_description,pipeline_language,pipeline_ownership,pipeline_keywords
NewPipeline,https://github.com/org/new,Pipeline description here,Nextflow,Organization Name,"keyword1, keyword2, keyword3"Field Requirements:
pipeline_name(required) - Display namepipeline_url(required) - Link to resourcepipeline_description(required) - Brief descriptionpipeline_language(optional) - Programming language/technologypipeline_ownership(optional) - Organization or ownerpipeline_keywords(optional) - Comma-separated keywords for search
For dynamic content, you can fetch from external APIs:
// In nexus.vue
const { data: externalData } = await useFetch('https://api.example.com/pipelines')
if (externalData.value) {
const transformed = externalData.value.map(item => ({
name: item.title,
url: item.html_url,
description: item.description,
category: 'Pipelines',
language: item.language,
ownership: item.owner,
keywords: item.topics || []
}))
allResults.value.push(...transformed)
}Headers: First row must contain field names Encoding: UTF-8 Line endings: Unix (LF) or Windows (CRLF) Quotes: Use quotes for fields containing commas
Example:
name,url,description,keywords
"Item 1",https://example.com,"Description with, comma","tag1, tag2"
Item 2,https://example2.com,Simple description,single-tagKeywords can be:
- String:
"keyword1, keyword2, keyword3" - Array:
["keyword1", "keyword2", "keyword3"](JSON only)
The parseKeywords() function handles both automatically.
All search results must conform to this interface:
interface SearchResultItem {
name: string // ← REQUIRED: Display title
url: string // ← REQUIRED: Link to resource
description: string // ← REQUIRED: Brief description
category: string // ← REQUIRED: Category name (matches dropdown)
language?: string // ← OPTIONAL: Language/format/type
ownership?: string // ← OPTIONAL: Owner/provider/organization
keywords?: string[] // ← OPTIONAL: Searchable keywords
}interface Props {
items?: DropdownItem[] // Category options for dropdown
placeholder?: string // Search input placeholder text
}
// Emits
emit('search', query: string, category: string)interface Props {
item: SearchResultItem // Result data to display
}Modify createSearchableText() in /app/pages/nexus.vue to customize what fields are searchable:
const createSearchableText = (item: SearchResultItem): string => {
// Add custom fields to search
return [
item.name,
item.description,
item.language,
item.ownership,
item.customField, // ← Your custom field
...(item.keywords || [])
].join(' ').toLowerCase()
}Prioritize certain fields:
const searchScore = (item: SearchResultItem, query: string): number => {
let score = 0
const lowerQuery = query.toLowerCase()
// Name match = highest priority
if (item.name.toLowerCase().includes(lowerQuery)) score += 10
// Keyword match = medium priority
if (item.keywords?.some(k => k.toLowerCase().includes(lowerQuery))) score += 5
// Description match = low priority
if (item.description.toLowerCase().includes(lowerQuery)) score += 1
return score
}
// Sort by score
filteredResults.value = results
.map(item => ({ item, score: searchScore(item, query) }))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.map(({ item }) => item)Create a category-specific card component:
<!-- app/components/TrainingResultCard.vue -->
<template>
<div class="training-card">
<div class="training-header">
<h3>{{ item.name }}</h3>
<span class="duration">{{ item.duration }}</span>
</div>
<p>{{ item.description }}</p>
<div class="training-meta">
<span class="format">{{ item.format }}</span>
<span class="level">{{ item.level }}</span>
</div>
</div>
</template>Then conditionally render in nexus.vue:
<component
:is="getCardComponent(result.category)"
:key="result.url"
:item="result"
/>
<script>
const getCardComponent = (category: string) => {
const components = {
'Trainings': TrainingResultCard,
'Pipelines': SearchResultCard,
// default
}
return components[category] || SearchResultCard
}
</script>Install fuzzy search library:
pnpm add fuse.jsImplement in nexus.vue:
import Fuse from 'fuse.js'
const fuse = new Fuse(allResults.value, {
keys: ['name', 'description', 'keywords'],
threshold: 0.3,
includeScore: true
})
const fuzzySearch = (query: string) => {
const results = fuse.search(query)
return results.map(r => r.item)
}Here's a complete example of adding a new "Tools" category:
/content/tools/tools.csv:
tool_name,tool_url,tool_description,tool_type,tool_platform,tool_keywords
FastQC,https://github.com/s-andrews/FastQC,Quality control for sequencing data,Quality Control,Java/Command Line,"QC, FASTQ, Quality"
SAMtools,https://github.com/samtools/samtools,Utilities for SAM/BAM file manipulation,File Processing,C/Command Line,"SAM, BAM, Alignment"In /app/pages/nexus.vue:
const searchCategories = [
{ label: 'Pipelines', value: 'pipelines' },
{ label: 'Trainings', value: 'trainings' },
{ label: 'Resources', value: 'resources' },
{ label: 'Tools', value: 'tools' } // ← Add
]interface ToolRawData {
tool_name: string
tool_url: string
tool_description: string
tool_type: string
tool_platform: string
tool_keywords: string | string[]
}const { data: toolsData } = await useAsyncData('tools', async () => {
return await queryCollection('tools').all()
})const transformToolData = (rawData: ToolRawData): SearchResultItem => ({
name: rawData.tool_name || '',
url: rawData.tool_url || '',
description: rawData.tool_description || '',
category: 'Tools',
language: rawData.tool_type || '',
ownership: rawData.tool_platform || '',
keywords: parseKeywords(rawData.tool_keywords)
})// Load tools
if (toolsData.value?.[0]?.meta?.body) {
const csvData = toolsData.value[0].meta.body as ToolRawData[]
allResults.value.push(...csvData.map(transformToolData))
}Done! Your Tools category is now searchable.
Solutions:
- Check CSV formatting (proper headers, no syntax errors)
- Clear Nuxt cache:
rm -rf .nuxtand rebuild - Verify file is in correct
/content/[category]/directory - Check browser console for errors
- Ensure data transformation function is called
Solutions:
- Verify
createSearchableText()includes your fields - Check that keywords are properly parsed
- Test search query matches field content
- Ensure category filter is correct
- Check for case sensitivity issues
Solutions:
- Verify
searchCategoriesarray includes your category - Check spelling matches exactly (case-sensitive)
- Ensure Search component receives updated items prop
Solutions:
- Define proper interface for your data structure
- Ensure all required SearchResultItem fields are provided
- Use optional chaining for nullable fields:
rawData?.field || '' - Run
pnpm run typecheckto see all errors
Solutions:
- Ensure UTF-8 encoding
- Wrap fields with commas in quotes:
"field, with, commas" - Escape quotes in content:
"He said ""hello""" - Check for extra/missing columns
- Verify no trailing commas
- Use consistent naming conventions for fields (e.g.,
category_name,category_url) - Provide meaningful keywords for better searchability
- Keep descriptions concise (100-200 characters ideal)
- Test search with various queries
- Use semantic field names in transformers
- Add TypeScript types for all data structures
- Document custom fields in comments
- Hardcode category names (use variables)
- Mix data formats in a single category
- Omit required fields (name, url, description)
- Use special characters in CSV without quoting
- Forget to update the category dropdown
- Skip data transformation step
- Leave console.log statements in production
- Implement Pagination:
const pageSize = 50
const currentPage = ref(1)
const paginatedResults = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredResults.value.slice(start, start + pageSize)
})- Debounce Search Input:
import { debounce } from 'lodash-es'
const debouncedSearch = debounce((query, category) => {
handleSearch(query, category)
}, 300)- Virtual Scrolling:
pnpm add vue-virtual-scroller- Category appears in dropdown
- All items load without errors
- Search returns correct results
- Keywords are searchable
- Result cards display properly
- Links work correctly
- Mobile responsive
- No console errors
- Dark mode compatible
Create test entries to verify:
Test Item 1,https://test.com,Short description,Type,Owner,"test, keyword"
Test Item 2 with long name that wraps,https://test2.com,Much longer description to test card layout and text wrapping behavior,Different Type,Different Owner,"multiple, test, keywords, here"Q: Can I use multiple data sources for one category?
A: Yes! Load multiple files and merge them:
const source1Data = await queryCollection('pipelines').all()
const source2Data = await queryCollection('pipelines-extra').all()
// Merge arrays before transformingQ: How do I change the order of categories in the dropdown?
A: Reorder items in the searchCategories array.
Q: Can I make a category private/hidden?
A: Yes, simply don't add it to searchCategories. Data can still be loaded programmatically.
Q: What's the maximum number of items?
A: No hard limit, but consider pagination above 500 items for performance.
Q: Can I integrate external APIs?
A: Yes! Use useFetch() or useAsyncData() to fetch from any API endpoint.
- Check existing categories (Pipelines) as reference implementation
- Review component source code with detailed comments
- Test with minimal example first before adding full dataset
- Use TypeScript error messages as debugging guides
Last Updated: January 22, 2026
Version: 1.0.0
Maintainer: StaPH-B Development Team