Merge pull request #111 from CalcsLive/github-pages-enhancement
Some checks failed
CI/CD Pipeline / Run Tests (3.1) (push) Has been cancelled
CI/CD Pipeline / Run Tests (3.11) (push) Has been cancelled
CI/CD Pipeline / Run Tests (3.9) (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Send Notifications (push) Has been cancelled
Deploy GitHub Pages / build (push) Has been cancelled
Deploy GitHub Pages / deploy (push) Has been cancelled
Docker Build and Test / Build and Test Docker Image (push) Has been cancelled
Docker Build and Test / Test Multi-platform Build (push) Has been cancelled

feat: Add GitHub Pages public search interface and enhanced documentation system
This commit is contained in:
Eliad Shahar
2025-09-30 15:15:06 +03:00
committed by GitHub
16 changed files with 45032 additions and 1325 deletions

75
.github/workflows/deploy-pages.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Deploy GitHub Pages
on:
push:
branches: [ main ]
paths:
- 'workflows/**'
- 'docs/**'
- 'scripts/**'
- 'workflow_db.py'
- 'create_categories.py'
workflow_dispatch: # Allow manual triggering
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Setup database and generate search index
run: |
# Create database directory
mkdir -p database
# Index all workflows
python workflow_db.py --index --force
# Generate categories
python create_categories.py
# Generate static search index for GitHub Pages
python scripts/generate_search_index.py
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './docs'
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

61
.github/workflows/update-readme.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Update README Stats
on:
push:
branches: [ main ]
paths:
- 'workflows/**'
schedule:
# Run weekly on Sunday at 00:00 UTC
- cron: '0 0 * * 0'
workflow_dispatch: # Allow manual triggering
jobs:
update-stats:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Generate workflow statistics
run: |
# Create database directory
mkdir -p database
# Index all workflows to get latest stats
python workflow_db.py --index --force
# Generate categories
python create_categories.py
# Get stats and update README
python scripts/update_readme_stats.py
- name: Commit changes
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add README.md
if git diff --staged --quiet; then
echo "No changes to README.md"
else
git commit -m "📊 Update workflow statistics
- Updated workflow counts and statistics
- Generated from latest workflow analysis
🤖 Automated update via GitHub Actions"
git push
fi

77
CHANGELOG.md Normal file
View File

@@ -0,0 +1,77 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **GitHub Pages Public Search Interface** - Complete client-side search application accessible at https://zie619.github.io/n8n-workflows
- Responsive HTML/CSS/JavaScript interface with mobile optimization
- Real-time search across 2,057+ workflows with instant results
- Category filtering across 15 workflow categories
- Dark/light theme support with system preference detection
- Direct workflow JSON download functionality
- Professional n8n-themed styling and animations
- **CalcsLive Custom Node Workflow** - Engineering calculations workflow showcasing CalcsLive custom node
- Added `workflows/Calcslive/2058_Calcslive_Engineering_Calculations_Manual.json`
- Comprehensive tags for searchability (calculation, engineering, custom-node, etc.)
- Professional description with npm package reference
- **GitHub Actions Automation**
- `deploy-pages.yml` - Automated deployment to GitHub Pages on workflow changes
- `update-readme.yml` - Weekly automated README statistics updates
- **Search Index Generation System**
- `scripts/generate_search_index.py` - Static search index generation for GitHub Pages
- `scripts/update_readme_stats.py` - Automated README statistics synchronization
- Support for both developer-chosen and integration-based categorization
- **Enhanced Documentation System**
- Real-time workflow statistics in README
- Accurate category counts (updated from 12 to 15 categories)
- GitHub Pages interface solving Issue #84
### Enhanced
- **Workflow Database System** (`workflow_db.py`)
- Enhanced CalcsLive custom node detection with pattern exclusions
- Fixed false positive "Cal.com" detection from "CalcsLive" node names
- Improved JSON description preservation and indexing
- Better Unicode handling and error reporting
- **Categorization System** (`create_categories.py`)
- Added CalcsLive to "Data Processing & Analysis" category
- Enhanced service name recognition patterns
- Improved category mapping for custom nodes
- **Search Index Prioritization**
- Modified `generate_search_index.py` to respect developer-chosen categories
- Added `load_existing_categories()` function to prioritize `create_categories.py` assignments
- Maintains fairness by not favoring specific custom nodes
### Fixed
- **Unicode Encoding Issues** - Resolved 'charmap' codec errors in Python scripts
- **Category Assignment Logic** - Search index now properly respects developer category choices
- **Statistics Accuracy** - README now reflects live database statistics instead of hardcoded numbers
- **Documentation Inconsistencies** - Updated category documentation to match actual implementation
### Changed
- **README.md** - Updated with current workflow statistics (2,057 workflows, 367 integrations)
- **Repository Organization** - Enhanced with automated maintenance and public accessibility
## [Previous] - 2024-08-14
### Changed
- Repository history rewritten due to DMCA compliance (Issue #85)
- All existing workflows maintained with improved organization
---
## Contributing to the Changelog
When adding new changes:
- Use **Added** for new features
- Use **Changed** for changes in existing functionality
- Use **Deprecated** for soon-to-be removed features
- Use **Removed** for now removed features
- Use **Fixed** for any bug fixes
- Use **Security** for vulnerability fixes
Each entry should briefly explain the change and its impact on users or contributors.

1559
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,7 @@ def categorize_by_filename(filename):
return "Technical Infrastructure & DevOps"
# Data Processing & File Operations
if any(word in filename_lower for word in ['process', 'writebinaryfile', 'readbinaryfile', 'extractfromfile', 'converttofile', 'googlefirebasecloudfirestore', 'supabase', 'surveymonkey', 'renamekeys', 'readpdf', 'wufoo', 'splitinbatches', 'airtop', 'comparedatasets', 'spreadsheetfile']):
if any(word in filename_lower for word in ['process', 'writebinaryfile', 'readbinaryfile', 'extractfromfile', 'converttofile', 'googlefirebasecloudfirestore', 'supabase', 'surveymonkey', 'renamekeys', 'readpdf', 'wufoo', 'splitinbatches', 'airtop', 'comparedatasets', 'spreadsheetfile', 'calcslive']):
return "Data Processing & Analysis"
# Utility & Business Process Automation

17
docs/api/categories.json Normal file
View File

@@ -0,0 +1,17 @@
[
"AI Agent Development",
"Business Process Automation",
"CRM & Sales",
"Cloud Storage & File Management",
"Communication & Messaging",
"Creative Content & Video Automation",
"Creative Design Automation",
"Data Processing & Analysis",
"E-commerce & Retail",
"Financial & Accounting",
"Marketing & Advertising Automation",
"Project Management",
"Social Media Management",
"Technical Infrastructure & DevOps",
"Web Scraping & Data Extraction"
]

202
docs/api/integrations.json Normal file
View File

@@ -0,0 +1,202 @@
[
{
"name": "Httprequest",
"count": 822
},
{
"name": "OpenAI",
"count": 573
},
{
"name": "Agent",
"count": 368
},
{
"name": "Webhook",
"count": 323
},
{
"name": "Form Trigger",
"count": 309
},
{
"name": "Splitout",
"count": 286
},
{
"name": "Google Sheets",
"count": 285
},
{
"name": "Splitinbatches",
"count": 222
},
{
"name": "Gmail",
"count": 198
},
{
"name": "Memorybufferwindow",
"count": 196
},
{
"name": "Chainllm",
"count": 191
},
{
"name": "Executeworkflow",
"count": 189
},
{
"name": "Telegram",
"count": 184
},
{
"name": "Chat",
"count": 180
},
{
"name": "Google Drive",
"count": 174
},
{
"name": "Outputparserstructured",
"count": 154
},
{
"name": "Slack",
"count": 150
},
{
"name": "Cal.com",
"count": 147
},
{
"name": "Airtable",
"count": 118
},
{
"name": "Extractfromfile",
"count": 114
},
{
"name": "Lmchatgooglegemini",
"count": 113
},
{
"name": "Documentdefaultdataloader",
"count": 99
},
{
"name": "Toolworkflow",
"count": 82
},
{
"name": "Html",
"count": 80
},
{
"name": "Respondtowebhook",
"count": 80
},
{
"name": "Textsplitterrecursivecharactertextsplitter",
"count": 76
},
{
"name": "Markdown",
"count": 71
},
{
"name": "Lmchatopenai",
"count": 71
},
{
"name": "Emailsend",
"count": 71
},
{
"name": "Notion",
"count": 69
},
{
"name": "Converttofile",
"count": 69
},
{
"name": "N8N",
"count": 52
},
{
"name": "PostgreSQL",
"count": 50
},
{
"name": "Chainsummarization",
"count": 48
},
{
"name": "GitHub",
"count": 45
},
{
"name": "Informationextractor",
"count": 45
},
{
"name": "Vectorstoreqdrant",
"count": 45
},
{
"name": "Toolhttprequest",
"count": 44
},
{
"name": "Itemlists",
"count": 44
},
{
"name": "LinkedIn",
"count": 43
},
{
"name": "Readwritefile",
"count": 41
},
{
"name": "Textclassifier",
"count": 41
},
{
"name": "Spreadsheetfile",
"count": 36
},
{
"name": "Hubspot",
"count": 35
},
{
"name": "Twitter/X",
"count": 34
},
{
"name": "Removeduplicates",
"count": 32
},
{
"name": "Rssfeedread",
"count": 30
},
{
"name": "Discord",
"count": 30
},
{
"name": "Mattermost",
"count": 30
},
{
"name": "Wordpress",
"count": 29
}
]

42542
docs/api/search-index.json Normal file

File diff suppressed because it is too large Load Diff

19
docs/api/stats.json Normal file
View File

@@ -0,0 +1,19 @@
{
"total_workflows": 2057,
"active_workflows": 215,
"inactive_workflows": 1842,
"total_nodes": 29528,
"unique_integrations": 367,
"categories": 15,
"triggers": {
"Complex": 832,
"Manual": 478,
"Scheduled": 226,
"Webhook": 521
},
"complexity": {
"high": 716,
"low": 566,
"medium": 775
}
}

492
docs/css/styles.css Normal file
View File

@@ -0,0 +1,492 @@
/* CSS Variables for Theming */
:root {
--primary-color: #ea4b71;
--primary-dark: #d63859;
--secondary-color: #6b73ff;
--accent-color: #00d4aa;
--text-primary: #2d3748;
--text-secondary: #4a5568;
--text-muted: #718096;
--background: #ffffff;
--surface: #f7fafc;
--border: #e2e8f0;
--border-light: #edf2f7;
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--border-radius: 8px;
--border-radius-lg: 12px;
--transition: all 0.2s ease-in-out;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--text-primary: #f7fafc;
--text-secondary: #e2e8f0;
--text-muted: #a0aec0;
--background: #1a202c;
--surface: #2d3748;
--border: #4a5568;
--border-light: #2d3748;
}
}
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: var(--text-primary);
background-color: var(--background);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Header */
.header {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 2rem 0;
text-align: center;
}
.logo {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.logo-emoji {
font-size: 3rem;
}
.tagline {
font-size: 1.25rem;
opacity: 0.9;
font-weight: 300;
}
/* Search Section */
.search-section {
padding: 3rem 0;
background-color: var(--surface);
}
.search-container {
max-width: 800px;
margin: 0 auto;
}
.search-box {
position: relative;
margin-bottom: 1.5rem;
}
#search-input {
width: 100%;
padding: 1rem 3rem 1rem 1.5rem;
font-size: 1.125rem;
border: 2px solid var(--border);
border-radius: var(--border-radius-lg);
background-color: var(--background);
color: var(--text-primary);
transition: var(--transition);
}
#search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(234, 75, 113, 0.1);
}
.search-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: var(--primary-color);
border: none;
border-radius: var(--border-radius);
padding: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.search-btn:hover {
background: var(--primary-dark);
}
.search-icon {
font-size: 1.25rem;
}
/* Filters */
.filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.filters select {
padding: 0.75rem 1rem;
border: 1px solid var(--border);
border-radius: var(--border-radius);
background-color: var(--background);
color: var(--text-primary);
font-size: 0.875rem;
cursor: pointer;
}
.filters select:focus {
outline: none;
border-color: var(--primary-color);
}
/* Stats Section */
.stats-section {
padding: 2rem 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.stat-card {
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--border-radius-lg);
padding: 1.5rem;
text-align: center;
box-shadow: var(--shadow);
transition: var(--transition);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary-color);
line-height: 1;
}
.stat-label {
color: var(--text-muted);
font-size: 0.875rem;
font-weight: 500;
margin-top: 0.5rem;
}
/* Results Section */
.results-section {
padding: 3rem 0;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.results-header h2 {
font-size: 1.875rem;
font-weight: 600;
}
.results-count {
color: var(--text-muted);
font-size: 0.875rem;
}
/* Loading State */
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 3rem;
color: var(--text-muted);
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Results Grid */
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
/* Workflow Cards */
.workflow-card {
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--border-radius-lg);
padding: 1.5rem;
box-shadow: var(--shadow);
transition: var(--transition);
cursor: pointer;
}
.workflow-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-color);
}
.workflow-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
line-height: 1.4;
}
.workflow-description {
color: var(--text-secondary);
font-size: 0.875rem;
margin-bottom: 1rem;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.workflow-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.meta-tag {
background: var(--surface);
color: var(--text-muted);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.meta-tag.category {
background: var(--accent-color);
color: white;
}
.meta-tag.trigger {
background: var(--secondary-color);
color: white;
}
.workflow-integrations {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.integration-tag {
background: var(--primary-color);
color: white;
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.workflow-actions {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-light);
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: var(--border-radius);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
transition: var(--transition);
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--surface);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--border-light);
}
/* No Results */
.no-results {
text-align: center;
padding: 3rem;
color: var(--text-muted);
}
.no-results-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.no-results h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
/* Load More Button */
.load-more {
display: block;
margin: 2rem auto 0;
padding: 0.75rem 2rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.load-more:hover {
background: var(--primary-dark);
}
/* Footer */
.footer {
background: var(--surface);
border-top: 1px solid var(--border);
padding: 2rem 0;
text-align: center;
color: var(--text-muted);
font-size: 0.875rem;
}
.footer-links {
margin-top: 0.5rem;
}
.footer-links a {
color: var(--primary-color);
text-decoration: none;
margin: 0 0.5rem;
}
.footer-links a:hover {
text-decoration: underline;
}
/* Utility Classes */
.hidden {
display: none !important;
}
.text-center {
text-align: center;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 0 0.75rem;
}
.header {
padding: 1.5rem 0;
}
.logo {
font-size: 2rem;
}
.logo-emoji {
font-size: 2.5rem;
}
.tagline {
font-size: 1rem;
}
.search-section {
padding: 2rem 0;
}
.results-grid {
grid-template-columns: 1fr;
}
.results-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.filters {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.workflow-actions {
flex-direction: column;
}
}

127
docs/index.html Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>N8N Workflow Collection - Search & Browse 2000+ Workflows</title>
<meta name="description" content="Browse and search through 2000+ n8n workflow automations. Find workflows for Telegram, Discord, Gmail, AI, and hundreds of other integrations.">
<link rel="stylesheet" href="css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
</head>
<body>
<header class="header">
<div class="container">
<h1 class="logo">
<span class="logo-emoji"></span>
N8N Workflow Collection
</h1>
<p class="tagline">Search & Browse <span id="total-count">2000+</span> Professional Automation Workflows</p>
</div>
</header>
<main class="container">
<!-- Search Section -->
<section class="search-section">
<div class="search-container">
<div class="search-box">
<input
type="text"
id="search-input"
placeholder="Search workflows... (e.g., telegram, calculation, gmail)"
autocomplete="off"
>
<button id="search-btn" class="search-btn">
<span class="search-icon">🔍</span>
</button>
</div>
<div class="filters">
<select id="category-filter">
<option value="">All Categories</option>
</select>
<select id="complexity-filter">
<option value="">All Complexity</option>
<option value="low">Low (≤5 nodes)</option>
<option value="medium">Medium (6-15 nodes)</option>
<option value="high">High (16+ nodes)</option>
</select>
<select id="trigger-filter">
<option value="">All Triggers</option>
<option value="Manual">Manual</option>
<option value="Webhook">Webhook</option>
<option value="Scheduled">Scheduled</option>
<option value="Complex">Complex</option>
</select>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="workflows-count">-</div>
<div class="stat-label">Total Workflows</div>
</div>
<div class="stat-card">
<div class="stat-number" id="active-count">-</div>
<div class="stat-label">Active Workflows</div>
</div>
<div class="stat-card">
<div class="stat-number" id="integrations-count">-</div>
<div class="stat-label">Integrations</div>
</div>
<div class="stat-card">
<div class="stat-number" id="categories-count">-</div>
<div class="stat-label">Categories</div>
</div>
</div>
</section>
<!-- Results Section -->
<section class="results-section">
<div class="results-header">
<h2 id="results-title">Featured Workflows</h2>
<div class="results-count" id="results-count"></div>
</div>
<div id="loading" class="loading hidden">
<div class="spinner"></div>
<span>Loading workflows...</span>
</div>
<div id="results-grid" class="results-grid">
<!-- Workflow cards will be inserted here -->
</div>
<div id="no-results" class="no-results hidden">
<div class="no-results-icon">🔍</div>
<h3>No workflows found</h3>
<p>Try adjusting your search terms or filters</p>
</div>
<button id="load-more" class="load-more hidden">Load More Workflows</button>
</section>
</main>
<footer class="footer">
<div class="container">
<p>
🚀 Powered by the
<a href="https://github.com/Zie619/n8n-workflows" target="_blank">N8N Workflow Collection</a>
| Built with ❤️ for the n8n community
</p>
<p class="footer-links">
<a href="https://n8n.io" target="_blank">n8n.io</a> |
<a href="https://community.n8n.io" target="_blank">Community</a> |
<a href="https://docs.n8n.io" target="_blank">Documentation</a>
</p>
</div>
</footer>
<script src="js/search.js"></script>
<script src="js/app.js"></script>
</body>
</html>

209
docs/js/app.js Normal file
View File

@@ -0,0 +1,209 @@
/**
* Main application script for N8N Workflow Collection
* Handles additional UI interactions and utilities
*/
class WorkflowApp {
constructor() {
this.init();
}
init() {
this.setupThemeToggle();
this.setupKeyboardShortcuts();
this.setupAnalytics();
this.setupServiceWorker();
}
setupThemeToggle() {
// Add theme toggle functionality if needed
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark-theme');
}
}
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Focus search on '/' key
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.focus();
}
}
// Clear search on 'Escape' key
if (e.key === 'Escape') {
const searchInput = document.getElementById('search-input');
if (searchInput && searchInput.value) {
searchInput.value = '';
searchInput.dispatchEvent(new Event('input'));
}
}
});
}
setupAnalytics() {
// Basic analytics for tracking popular workflows
this.trackEvent = (category, action, label) => {
// Could integrate with Google Analytics or other tracking
console.debug('Analytics:', { category, action, label });
};
// Track search queries
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', this.debounce((e) => {
if (e.target.value.length > 2) {
this.trackEvent('Search', 'query', e.target.value);
}
}, 1000));
}
// Track workflow downloads
document.addEventListener('click', (e) => {
if (e.target.matches('a[href*=".json"]')) {
const filename = e.target.href.split('/').pop();
this.trackEvent('Download', 'workflow', filename);
}
});
}
setupServiceWorker() {
// Register service worker for offline functionality (if needed)
if ('serviceWorker' in navigator) {
// Uncomment when service worker is implemented
// navigator.serviceWorker.register('/service-worker.js');
}
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Utility functions for the application
window.WorkflowUtils = {
/**
* Format numbers with appropriate suffixes
*/
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
},
/**
* Debounce function for search input
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Copy text to clipboard
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
const success = document.execCommand('copy');
document.body.removeChild(textArea);
return success;
}
},
/**
* Show temporary notification
*/
showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#48bb78' : type === 'error' ? '#f56565' : '#4299e1'};
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(0)';
}, 10);
// Animate out and remove
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification);
}
}, 300);
}, duration);
}
};
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new WorkflowApp();
// Add helpful hints
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.setAttribute('title', 'Press / to focus search, Escape to clear');
}
});
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// Refresh data if page has been hidden for more than 5 minutes
const lastRefresh = localStorage.getItem('lastRefresh');
const now = Date.now();
if (!lastRefresh || now - parseInt(lastRefresh) > 5 * 60 * 1000) {
// Could refresh search index here if needed
localStorage.setItem('lastRefresh', now.toString());
}
}
});

439
docs/js/search.js Normal file
View File

@@ -0,0 +1,439 @@
/**
* Client-side search functionality for N8N Workflow Collection
* Handles searching, filtering, and displaying workflow results
*/
class WorkflowSearch {
constructor() {
this.searchIndex = null;
this.currentResults = [];
this.displayedCount = 0;
this.resultsPerPage = 20;
this.isLoading = false;
// DOM elements
this.searchInput = document.getElementById('search-input');
this.categoryFilter = document.getElementById('category-filter');
this.complexityFilter = document.getElementById('complexity-filter');
this.triggerFilter = document.getElementById('trigger-filter');
this.resultsGrid = document.getElementById('results-grid');
this.resultsTitle = document.getElementById('results-title');
this.resultsCount = document.getElementById('results-count');
this.loadingEl = document.getElementById('loading');
this.noResultsEl = document.getElementById('no-results');
this.loadMoreBtn = document.getElementById('load-more');
this.init();
}
async init() {
try {
await this.loadSearchIndex();
this.setupEventListeners();
this.populateFilters();
this.updateStats();
this.showFeaturedWorkflows();
} catch (error) {
console.error('Failed to initialize search:', error);
this.showError('Failed to load workflow data. Please try again later.');
}
}
async loadSearchIndex() {
this.showLoading(true);
try {
const response = await fetch('api/search-index.json');
if (!response.ok) {
throw new Error('Failed to load search index');
}
this.searchIndex = await response.json();
} finally {
this.showLoading(false);
}
}
setupEventListeners() {
// Search input
this.searchInput.addEventListener('input', this.debounce(this.handleSearch.bind(this), 300));
this.searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleSearch();
}
});
// Filters
this.categoryFilter.addEventListener('change', this.handleSearch.bind(this));
this.complexityFilter.addEventListener('change', this.handleSearch.bind(this));
this.triggerFilter.addEventListener('change', this.handleSearch.bind(this));
// Load more button
this.loadMoreBtn.addEventListener('click', this.loadMoreResults.bind(this));
// Search button
document.getElementById('search-btn').addEventListener('click', this.handleSearch.bind(this));
}
populateFilters() {
// Populate category filter
this.searchIndex.categories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
this.categoryFilter.appendChild(option);
});
}
updateStats() {
const stats = this.searchIndex.stats;
document.getElementById('total-count').textContent = stats.total_workflows.toLocaleString();
document.getElementById('workflows-count').textContent = stats.total_workflows.toLocaleString();
document.getElementById('active-count').textContent = stats.active_workflows.toLocaleString();
document.getElementById('integrations-count').textContent = stats.unique_integrations.toLocaleString();
document.getElementById('categories-count').textContent = stats.categories.toLocaleString();
}
handleSearch() {
const query = this.searchInput.value.trim().toLowerCase();
const category = this.categoryFilter.value;
const complexity = this.complexityFilter.value;
const trigger = this.triggerFilter.value;
this.currentResults = this.searchWorkflows(query, { category, complexity, trigger });
this.displayedCount = 0;
this.displayResults(true);
this.updateResultsHeader(query, { category, complexity, trigger });
}
searchWorkflows(query, filters = {}) {
let results = [...this.searchIndex.workflows];
// Text search
if (query) {
results = results.filter(workflow =>
workflow.searchable_text.includes(query)
);
// Sort by relevance (name matches first, then description)
results.sort((a, b) => {
const aNameMatch = a.name.toLowerCase().includes(query);
const bNameMatch = b.name.toLowerCase().includes(query);
if (aNameMatch && !bNameMatch) return -1;
if (!aNameMatch && bNameMatch) return 1;
return 0;
});
}
// Apply filters
if (filters.category) {
results = results.filter(workflow => workflow.category === filters.category);
}
if (filters.complexity) {
results = results.filter(workflow => workflow.complexity === filters.complexity);
}
if (filters.trigger) {
results = results.filter(workflow => workflow.trigger_type === filters.trigger);
}
return results;
}
showFeaturedWorkflows() {
// Show recent workflows or popular ones when no search
const featured = this.searchIndex.workflows
.filter(w => w.integrations.length > 0)
.slice(0, this.resultsPerPage);
this.currentResults = featured;
this.displayedCount = 0;
this.displayResults(true);
this.resultsTitle.textContent = 'Featured Workflows';
this.resultsCount.textContent = '';
}
displayResults(reset = false) {
if (reset) {
this.resultsGrid.innerHTML = '';
this.displayedCount = 0;
}
if (this.currentResults.length === 0) {
this.showNoResults();
return;
}
this.hideNoResults();
const startIndex = this.displayedCount;
const endIndex = Math.min(startIndex + this.resultsPerPage, this.currentResults.length);
const resultsToShow = this.currentResults.slice(startIndex, endIndex);
resultsToShow.forEach(workflow => {
const card = this.createWorkflowCard(workflow);
this.resultsGrid.appendChild(card);
});
this.displayedCount = endIndex;
// Update load more button
if (this.displayedCount < this.currentResults.length) {
this.loadMoreBtn.classList.remove('hidden');
} else {
this.loadMoreBtn.classList.add('hidden');
}
}
createWorkflowCard(workflow) {
const card = document.createElement('div');
card.className = 'workflow-card';
card.onclick = () => this.openWorkflowDetails(workflow);
const integrationTags = workflow.integrations
.slice(0, 3)
.map(integration => `<span class="integration-tag">${integration}</span>`)
.join('');
const moreIntegrations = workflow.integrations.length > 3
? `<span class="integration-tag">+${workflow.integrations.length - 3} more</span>`
: '';
card.innerHTML = `
<h3 class="workflow-title">${this.escapeHtml(workflow.name)}</h3>
<p class="workflow-description">${this.escapeHtml(workflow.description)}</p>
<div class="workflow-meta">
<span class="meta-tag category">${workflow.category}</span>
<span class="meta-tag trigger">${workflow.trigger_type}</span>
<span class="meta-tag">${workflow.complexity} complexity</span>
<span class="meta-tag">${workflow.node_count} nodes</span>
</div>
<div class="workflow-integrations">
${integrationTags}
${moreIntegrations}
</div>
<div class="workflow-actions">
<a href="${workflow.download_url}" class="btn btn-primary" target="_blank" onclick="event.stopPropagation()">
📥 Download JSON
</a>
<button class="btn btn-secondary" onclick="event.stopPropagation(); window.copyWorkflowId('${workflow.filename}')">
📋 Copy ID
</button>
</div>
`;
return card;
}
openWorkflowDetails(workflow) {
// Create modal or expand card with more details
const modal = this.createDetailsModal(workflow);
document.body.appendChild(modal);
// Add event listener to close modal
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
}
createDetailsModal(workflow) {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
`;
const allIntegrations = workflow.integrations
.map(integration => `<span class="integration-tag">${integration}</span>`)
.join('');
const allTags = workflow.tags
.map(tag => `<span class="meta-tag">${tag}</span>`)
.join('');
modalContent.innerHTML = `
<button onclick="this.parentElement.parentElement.remove()" style="position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
<h2 style="margin-bottom: 1rem;">${this.escapeHtml(workflow.name)}</h2>
<div style="margin-bottom: 1.5rem;">
<strong>Description:</strong>
<p style="margin-top: 0.5rem;">${this.escapeHtml(workflow.description)}</p>
</div>
<div style="margin-bottom: 1.5rem;">
<strong>Details:</strong>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-top: 0.5rem;">
<div><strong>Category:</strong> ${workflow.category}</div>
<div><strong>Trigger:</strong> ${workflow.trigger_type}</div>
<div><strong>Complexity:</strong> ${workflow.complexity}</div>
<div><strong>Nodes:</strong> ${workflow.node_count}</div>
<div><strong>Status:</strong> ${workflow.active ? 'Active' : 'Inactive'}</div>
<div><strong>File:</strong> ${workflow.filename}</div>
</div>
</div>
<div style="margin-bottom: 1.5rem;">
<strong>Integrations:</strong>
<div style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.25rem;">
${allIntegrations}
</div>
</div>
${workflow.tags.length > 0 ? `
<div style="margin-bottom: 1.5rem;">
<strong>Tags:</strong>
<div style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.25rem;">
${allTags}
</div>
</div>
` : ''}
<div style="display: flex; gap: 1rem;">
<a href="${workflow.download_url}" class="btn btn-primary" target="_blank">
📥 Download JSON
</a>
<button class="btn btn-secondary" onclick="window.copyWorkflowId('${workflow.filename}')">
📋 Copy Filename
</button>
</div>
`;
modal.appendChild(modalContent);
return modal;
}
updateResultsHeader(query, filters) {
let title = 'Search Results';
let filterDesc = [];
if (query) {
title = `Search: "${query}"`;
}
if (filters.category) filterDesc.push(`Category: ${filters.category}`);
if (filters.complexity) filterDesc.push(`Complexity: ${filters.complexity}`);
if (filters.trigger) filterDesc.push(`Trigger: ${filters.trigger}`);
if (filterDesc.length > 0) {
title += ` (${filterDesc.join(', ')})`;
}
this.resultsTitle.textContent = title;
this.resultsCount.textContent = `${this.currentResults.length} workflows found`;
}
loadMoreResults() {
this.displayResults(false);
}
showLoading(show) {
this.isLoading = show;
this.loadingEl.classList.toggle('hidden', !show);
}
showNoResults() {
this.noResultsEl.classList.remove('hidden');
this.loadMoreBtn.classList.add('hidden');
}
hideNoResults() {
this.noResultsEl.classList.add('hidden');
}
showError(message) {
const errorEl = document.createElement('div');
errorEl.className = 'error-message';
errorEl.style.cssText = `
background: #fed7d7;
color: #c53030;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
text-align: center;
`;
errorEl.textContent = message;
this.resultsGrid.innerHTML = '';
this.resultsGrid.appendChild(errorEl);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Global functions
window.copyWorkflowId = function(filename) {
navigator.clipboard.writeText(filename).then(() => {
// Show temporary success message
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✅ Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = filename;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✅ Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
};
// Initialize search when page loads
document.addEventListener('DOMContentLoaded', () => {
new WorkflowSearch();
});

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""
Generate Static Search Index for GitHub Pages
Creates a lightweight JSON index for client-side search functionality.
"""
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Any
# Add the parent directory to path for imports
sys.path.append(str(Path(__file__).parent.parent))
from workflow_db import WorkflowDatabase
def generate_static_search_index(db_path: str, output_dir: str) -> Dict[str, Any]:
"""Generate a static search index for client-side searching."""
# Initialize database
db = WorkflowDatabase(db_path)
# Get all workflows
workflows, total = db.search_workflows(limit=10000) # Get all workflows
# Get statistics
stats = db.get_stats()
# Get categories from service mapping
categories = db.get_service_categories()
# Load existing categories from create_categories.py system
existing_categories = load_existing_categories()
# Create simplified workflow data for search
search_workflows = []
for workflow in workflows:
# Create searchable text combining multiple fields
searchable_text = ' '.join([
workflow['name'],
workflow['description'],
workflow['filename'],
' '.join(workflow['integrations']),
' '.join(workflow['tags']) if workflow['tags'] else ''
]).lower()
# Use existing category from create_categories.py system, fallback to integration-based
category = get_workflow_category(workflow['filename'], existing_categories, workflow['integrations'], categories)
search_workflow = {
'id': workflow['filename'].replace('.json', ''),
'name': workflow['name'],
'description': workflow['description'],
'filename': workflow['filename'],
'active': workflow['active'],
'trigger_type': workflow['trigger_type'],
'complexity': workflow['complexity'],
'node_count': workflow['node_count'],
'integrations': workflow['integrations'],
'tags': workflow['tags'],
'category': category,
'searchable_text': searchable_text,
'download_url': f"https://raw.githubusercontent.com/Zie619/n8n-workflows/main/workflows/{extract_folder_from_filename(workflow['filename'])}/{workflow['filename']}"
}
search_workflows.append(search_workflow)
# Create comprehensive search index
search_index = {
'version': '1.0',
'generated_at': stats.get('last_indexed', ''),
'stats': {
'total_workflows': stats['total'],
'active_workflows': stats['active'],
'inactive_workflows': stats['inactive'],
'total_nodes': stats['total_nodes'],
'unique_integrations': stats['unique_integrations'],
'categories': len(get_category_list(categories)),
'triggers': stats['triggers'],
'complexity': stats['complexity']
},
'categories': get_category_list(categories),
'integrations': get_popular_integrations(workflows),
'workflows': search_workflows
}
return search_index
def load_existing_categories() -> Dict[str, str]:
"""Load existing categories from search_categories.json created by create_categories.py."""
try:
with open('context/search_categories.json', 'r', encoding='utf-8') as f:
categories_data = json.load(f)
# Convert to filename -> category mapping
category_mapping = {}
for item in categories_data:
if item.get('category'):
category_mapping[item['filename']] = item['category']
return category_mapping
except FileNotFoundError:
print("Warning: search_categories.json not found, using integration-based categorization")
return {}
def get_workflow_category(filename: str, existing_categories: Dict[str, str],
integrations: List[str], service_categories: Dict[str, List[str]]) -> str:
"""Get category for workflow, preferring existing assignment over integration-based."""
# First priority: Use existing category from create_categories.py system
if filename in existing_categories:
return existing_categories[filename]
# Fallback: Use integration-based categorization
return determine_category(integrations, service_categories)
def determine_category(integrations: List[str], categories: Dict[str, List[str]]) -> str:
"""Determine the category for a workflow based on its integrations."""
if not integrations:
return "Uncategorized"
# Check each category for matching integrations
for category, services in categories.items():
for integration in integrations:
if integration in services:
return format_category_name(category)
return "Uncategorized"
def format_category_name(category_key: str) -> str:
"""Format category key to display name."""
category_mapping = {
'messaging': 'Communication & Messaging',
'email': 'Communication & Messaging',
'cloud_storage': 'Cloud Storage & File Management',
'database': 'Data Processing & Analysis',
'project_management': 'Project Management',
'ai_ml': 'AI Agent Development',
'social_media': 'Social Media Management',
'ecommerce': 'E-commerce & Retail',
'analytics': 'Data Processing & Analysis',
'calendar_tasks': 'Project Management',
'forms': 'Data Processing & Analysis',
'development': 'Technical Infrastructure & DevOps'
}
return category_mapping.get(category_key, category_key.replace('_', ' ').title())
def get_category_list(categories: Dict[str, List[str]]) -> List[str]:
"""Get formatted list of all categories."""
formatted_categories = set()
for category_key in categories.keys():
formatted_categories.add(format_category_name(category_key))
# Add categories from the create_categories.py system
additional_categories = [
"Business Process Automation",
"Web Scraping & Data Extraction",
"Marketing & Advertising Automation",
"Creative Content & Video Automation",
"Creative Design Automation",
"CRM & Sales",
"Financial & Accounting"
]
for cat in additional_categories:
formatted_categories.add(cat)
return sorted(list(formatted_categories))
def get_popular_integrations(workflows: List[Dict]) -> List[Dict[str, Any]]:
"""Get list of popular integrations with counts."""
integration_counts = {}
for workflow in workflows:
for integration in workflow['integrations']:
integration_counts[integration] = integration_counts.get(integration, 0) + 1
# Sort by count and take top 50
sorted_integrations = sorted(
integration_counts.items(),
key=lambda x: x[1],
reverse=True
)[:50]
return [
{'name': name, 'count': count}
for name, count in sorted_integrations
]
def extract_folder_from_filename(filename: str) -> str:
"""Extract folder name from workflow filename."""
# Most workflows follow pattern: ID_Service_Purpose_Trigger.json
# Extract the service name as folder
parts = filename.replace('.json', '').split('_')
if len(parts) >= 2:
return parts[1].capitalize() # Second part is usually the service
return 'Misc'
def save_search_index(search_index: Dict[str, Any], output_dir: str):
"""Save the search index to multiple formats for different uses."""
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
# Save complete index
with open(os.path.join(output_dir, 'search-index.json'), 'w', encoding='utf-8') as f:
json.dump(search_index, f, indent=2, ensure_ascii=False)
# Save stats only (for quick loading)
with open(os.path.join(output_dir, 'stats.json'), 'w', encoding='utf-8') as f:
json.dump(search_index['stats'], f, indent=2, ensure_ascii=False)
# Save categories only
with open(os.path.join(output_dir, 'categories.json'), 'w', encoding='utf-8') as f:
json.dump(search_index['categories'], f, indent=2, ensure_ascii=False)
# Save integrations only
with open(os.path.join(output_dir, 'integrations.json'), 'w', encoding='utf-8') as f:
json.dump(search_index['integrations'], f, indent=2, ensure_ascii=False)
print(f"Search index generated successfully:")
print(f" {search_index['stats']['total_workflows']} workflows indexed")
print(f" {len(search_index['categories'])} categories")
print(f" {len(search_index['integrations'])} popular integrations")
print(f" Files saved to: {output_dir}")
def main():
"""Main function to generate search index."""
# Paths
db_path = "database/workflows.db"
output_dir = "docs/api"
# Check if database exists
if not os.path.exists(db_path):
print(f"Database not found: {db_path}")
print("Run 'python run.py --reindex' first to create the database")
sys.exit(1)
try:
print("Generating static search index...")
search_index = generate_static_search_index(db_path, output_dir)
save_search_index(search_index, output_dir)
print("Static search index ready for GitHub Pages!")
except Exception as e:
print(f"Error generating search index: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
Update README.md with current workflow statistics
Replaces hardcoded numbers with live data from the database.
"""
import json
import os
import re
import sys
from pathlib import Path
from datetime import datetime
# Add the parent directory to path for imports
sys.path.append(str(Path(__file__).parent.parent))
from workflow_db import WorkflowDatabase
def get_current_stats():
"""Get current workflow statistics from the database."""
db_path = "database/workflows.db"
if not os.path.exists(db_path):
print("Database not found. Run workflow indexing first.")
return None
db = WorkflowDatabase(db_path)
stats = db.get_stats()
# Get categories count
categories = db.get_service_categories()
return {
'total_workflows': stats['total'],
'active_workflows': stats['active'],
'inactive_workflows': stats['inactive'],
'total_nodes': stats['total_nodes'],
'unique_integrations': stats['unique_integrations'],
'categories_count': len(get_category_list(categories)),
'triggers': stats['triggers'],
'complexity': stats['complexity'],
'last_updated': datetime.now().strftime('%Y-%m-%d')
}
def get_category_list(categories):
"""Get formatted list of all categories (same logic as search index)."""
formatted_categories = set()
# Map technical categories to display names
category_mapping = {
'messaging': 'Communication & Messaging',
'email': 'Communication & Messaging',
'cloud_storage': 'Cloud Storage & File Management',
'database': 'Data Processing & Analysis',
'project_management': 'Project Management',
'ai_ml': 'AI Agent Development',
'social_media': 'Social Media Management',
'ecommerce': 'E-commerce & Retail',
'analytics': 'Data Processing & Analysis',
'calendar_tasks': 'Project Management',
'forms': 'Data Processing & Analysis',
'development': 'Technical Infrastructure & DevOps'
}
for category_key in categories.keys():
display_name = category_mapping.get(category_key, category_key.replace('_', ' ').title())
formatted_categories.add(display_name)
# Add categories from the create_categories.py system
additional_categories = [
"Business Process Automation",
"Web Scraping & Data Extraction",
"Marketing & Advertising Automation",
"Creative Content & Video Automation",
"Creative Design Automation",
"CRM & Sales",
"Financial & Accounting"
]
for cat in additional_categories:
formatted_categories.add(cat)
return sorted(list(formatted_categories))
def update_readme_stats(stats):
"""Update README.md with current statistics."""
readme_path = "README.md"
if not os.path.exists(readme_path):
print("README.md not found")
return False
with open(readme_path, 'r', encoding='utf-8') as f:
content = f.read()
# Define replacement patterns and their new values
replacements = [
# Main collection description
(r'A professionally organized collection of \*\*[\d,]+\s+n8n workflows\*\*',
f'A professionally organized collection of **{stats["total_workflows"]:,} n8n workflows**'),
# Total workflows in various contexts
(r'- \*\*[\d,]+\s+workflows\*\* with meaningful',
f'- **{stats["total_workflows"]:,} workflows** with meaningful'),
# Statistics section
(r'- \*\*Total Workflows\*\*: [\d,]+',
f'- **Total Workflows**: {stats["total_workflows"]:,}'),
(r'- \*\*Active Workflows\*\*: [\d,]+ \([\d.]+%',
f'- **Active Workflows**: {stats["active_workflows"]:,} ({(stats["active_workflows"]/stats["total_workflows"]*100):.1f}%'),
(r'- \*\*Total Nodes\*\*: [\d,]+ \(avg [\d.]+ nodes',
f'- **Total Nodes**: {stats["total_nodes"]:,} (avg {(stats["total_nodes"]/stats["total_workflows"]):.1f} nodes'),
(r'- \*\*Unique Integrations\*\*: [\d,]+ different',
f'- **Unique Integrations**: {stats["unique_integrations"]:,} different'),
# Update complexity/trigger distribution
(r'- \*\*Complex\*\*: [\d,]+ workflows \([\d.]+%\)',
f'- **Complex**: {stats["triggers"].get("Complex", 0):,} workflows ({(stats["triggers"].get("Complex", 0)/stats["total_workflows"]*100):.1f}%)'),
(r'- \*\*Webhook\*\*: [\d,]+ workflows \([\d.]+%\)',
f'- **Webhook**: {stats["triggers"].get("Webhook", 0):,} workflows ({(stats["triggers"].get("Webhook", 0)/stats["total_workflows"]*100):.1f}%)'),
(r'- \*\*Manual\*\*: [\d,]+ workflows \([\d.]+%\)',
f'- **Manual**: {stats["triggers"].get("Manual", 0):,} workflows ({(stats["triggers"].get("Manual", 0)/stats["total_workflows"]*100):.1f}%)'),
(r'- \*\*Scheduled\*\*: [\d,]+ workflows \([\d.]+%\)',
f'- **Scheduled**: {stats["triggers"].get("Scheduled", 0):,} workflows ({(stats["triggers"].get("Scheduled", 0)/stats["total_workflows"]*100):.1f}%)'),
# Update total in current collection stats
(r'\*\*Total Workflows\*\*: [\d,]+ automation',
f'**Total Workflows**: {stats["total_workflows"]:,} automation'),
(r'\*\*Active Workflows\*\*: [\d,]+ \([\d.]+% active',
f'**Active Workflows**: {stats["active_workflows"]:,} ({(stats["active_workflows"]/stats["total_workflows"]*100):.1f}% active'),
(r'\*\*Total Nodes\*\*: [\d,]+ \(avg [\d.]+ nodes',
f'**Total Nodes**: {stats["total_nodes"]:,} (avg {(stats["total_nodes"]/stats["total_workflows"]):.1f} nodes'),
(r'\*\*Unique Integrations\*\*: [\d,]+ different',
f'**Unique Integrations**: {stats["unique_integrations"]:,} different'),
# Categories count
(r'Our system automatically categorizes workflows into [\d]+ service categories',
f'Our system automatically categorizes workflows into {stats["categories_count"]} service categories'),
# Update any "2000+" references
(r'2000\+', f'{stats["total_workflows"]:,}+'),
(r'2,000\+', f'{stats["total_workflows"]:,}+'),
# Search across X workflows
(r'Search across [\d,]+ workflows', f'Search across {stats["total_workflows"]:,} workflows'),
# Instant search across X workflows
(r'Instant search across [\d,]+ workflows', f'Instant search across {stats["total_workflows"]:,} workflows'),
]
# Apply all replacements
updated_content = content
replacements_made = 0
for pattern, replacement in replacements:
old_content = updated_content
updated_content = re.sub(pattern, replacement, updated_content)
if updated_content != old_content:
replacements_made += 1
# Write back to file
with open(readme_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f"README.md updated with current statistics:")
print(f" - Total workflows: {stats['total_workflows']:,}")
print(f" - Active workflows: {stats['active_workflows']:,}")
print(f" - Total nodes: {stats['total_nodes']:,}")
print(f" - Unique integrations: {stats['unique_integrations']:,}")
print(f" - Categories: {stats['categories_count']}")
print(f" - Replacements made: {replacements_made}")
return True
def main():
"""Main function to update README statistics."""
try:
print("Getting current workflow statistics...")
stats = get_current_stats()
if not stats:
print("Failed to get statistics")
sys.exit(1)
print("Updating README.md...")
success = update_readme_stats(stats)
if success:
print("README.md successfully updated with latest statistics!")
else:
print("Failed to update README.md")
sys.exit(1)
except Exception as e:
print(f"Error updating README stats: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -199,8 +199,12 @@ class WorkflowDatabase:
workflow['trigger_type'] = trigger_type
workflow['integrations'] = list(integrations)
# Generate description
workflow['description'] = self.generate_description(workflow, trigger_type, integrations)
# Use JSON description if available, otherwise generate one
json_description = data.get('description', '').strip()
if json_description:
workflow['description'] = json_description
else:
workflow['description'] = self.generate_description(workflow, trigger_type, integrations)
return workflow
@@ -353,7 +357,7 @@ class WorkflowDatabase:
service_name = service_mappings.get(raw_service, raw_service.title() if raw_service else None)
# Handle custom nodes
elif '-' in node_type:
elif '-' in node_type or '@' in node_type:
# Try to extract service name from custom node names like "n8n-nodes-youtube-transcription-kasha.youtubeTranscripter"
parts = node_type.lower().split('.')
for part in parts:
@@ -366,10 +370,16 @@ class WorkflowDatabase:
elif 'discord' in part:
service_name = 'Discord'
break
elif 'calcslive' in part:
service_name = 'CalcsLive'
break
# Also check node names for service hints
# Also check node names for service hints (but avoid false positives)
for service_key, service_value in service_mappings.items():
if service_key in node_name and service_value:
# Avoid false positive: "cal" in calcslive-related terms should not match "Cal.com"
if service_key == 'cal' and any(term in node_name.lower() for term in ['calcslive', 'calc', 'calculation']):
continue
service_name = service_value
break
@@ -530,33 +540,16 @@ class WorkflowDatabase:
where_conditions.append("w.complexity = ?")
params.append(complexity_filter)
# Use FTS search if query provided and FTS table exists
# Use FTS search if query provided
if query.strip():
# Check if FTS table exists
cursor_check = conn.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='workflows_fts'
""")
fts_exists = cursor_check.fetchone() is not None
if fts_exists:
# FTS search with ranking
base_query = """
SELECT w.*, rank
FROM workflows_fts fts
JOIN workflows w ON w.id = fts.rowid
WHERE workflows_fts MATCH ?
"""
params.insert(0, query)
else:
# Fallback to LIKE search if FTS not available
base_query = """
SELECT w.*, 0 as rank
FROM workflows w
WHERE (w.name LIKE ? OR w.description LIKE ? OR w.filename LIKE ?)
"""
search_term = f"%{query}%"
params.extend([search_term, search_term, search_term])
# FTS search with ranking
base_query = """
SELECT w.*, rank
FROM workflows_fts fts
JOIN workflows w ON w.id = fts.rowid
WHERE workflows_fts MATCH ?
"""
params.insert(0, query)
else:
# Regular query without FTS
base_query = """
@@ -566,10 +559,7 @@ class WorkflowDatabase:
"""
if where_conditions:
if "WHERE" in base_query:
base_query += " AND " + " AND ".join(where_conditions)
else:
base_query += " WHERE " + " AND ".join(where_conditions)
base_query += " AND " + " AND ".join(where_conditions)
# Count total results
count_query = f"SELECT COUNT(*) as total FROM ({base_query}) t"
@@ -669,7 +659,7 @@ class WorkflowDatabase:
'cloud_storage': ['Google Drive', 'Google Docs', 'Google Sheets', 'Dropbox', 'OneDrive', 'Box'],
'database': ['PostgreSQL', 'MySQL', 'MongoDB', 'Redis', 'Airtable', 'Notion'],
'project_management': ['Jira', 'GitHub', 'GitLab', 'Trello', 'Asana', 'Monday.com'],
'ai_ml': ['OpenAI', 'Anthropic', 'Hugging Face'],
'ai_ml': ['OpenAI', 'Anthropic', 'Hugging Face', 'CalcsLive'],
'social_media': ['LinkedIn', 'Twitter/X', 'Facebook', 'Instagram'],
'ecommerce': ['Shopify', 'Stripe', 'PayPal'],
'analytics': ['Google Analytics', 'Mixpanel'],