Browse Source

feat: migrate community workflows to R2 storage

- Update API routes to fetch from nodebananapro.com
- Remove local workflow JSON files (now hosted on R2)
- Remove LFS tracking (.gitattributes)
- Remove local communityWorkflows.ts metadata (now on server)
- Add COMMUNITY_WORKFLOWS_API_URL env variable option

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
7a859ae47c
  1. 4
      .env.example
  2. 1
      .gitattributes
  3. 3
      examples/Fashion-image-to-video.json
  4. 2487
      examples/contact-sheet-ChrisWalkman.json
  5. 2747
      examples/contact-sheet-billsSupra.json
  6. 55
      src/app/api/community-workflows/[id]/route.ts
  7. 96
      src/app/api/community-workflows/route.ts
  8. 93
      src/lib/quickstart/communityWorkflows.ts

4
.env.example

@ -13,3 +13,7 @@ REPLICATE_API_KEY=your_replicate_api_key_here
# fal.ai API Key (Optional - only needed if using fal.ai models)
# Get your API key from: https://fal.ai/dashboard/keys
FAL_API_KEY=your_fal_api_key_here
# Community Workflows API URL (Optional - defaults to node-banana-pro hosted service)
# Set this if you're self-hosting the community workflows
COMMUNITY_WORKFLOWS_API_URL=https://your-node-banana-pro-instance.com/api/public/community-workflows

1
.gitattributes

@ -1 +0,0 @@
examples/Fashion-image-to-video.json filter=lfs diff=lfs merge=lfs -text

3
examples/Fashion-image-to-video.json

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f14d979ca4fa7ba0acccc9992162eadae228e0721ec1ae1ec9d80e92431490c
size 275622403

2487
examples/contact-sheet-ChrisWalkman.json

File diff suppressed because one or more lines are too long

2747
examples/contact-sheet-billsSupra.json

File diff suppressed because one or more lines are too long

55
src/app/api/community-workflows/[id]/route.ts

@ -1,41 +1,60 @@
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
// Default to node-banana-pro hosted service
const COMMUNITY_WORKFLOWS_API_URL =
process.env.COMMUNITY_WORKFLOWS_API_URL ||
"https://nodebananapro.com/api/public/community-workflows";
interface RouteParams {
params: Promise<{ id: string }>;
}
/**
* GET: Load a specific community workflow by ID
* GET: Load a specific community workflow by ID from the remote API
*
* This proxies to the node-banana-pro hosted service which stores
* community workflows in R2 storage.
*/
export async function GET(request: Request, { params }: RouteParams) {
try {
const { id } = await params;
const filename = `${id}.json`;
const filePath = path.join(process.cwd(), "examples", filename);
// Check if file exists
try {
await fs.access(filePath);
} catch {
const response = await fetch(`${COMMUNITY_WORKFLOWS_API_URL}/${id}`, {
headers: {
Accept: "application/json",
},
// Cache for 10 minutes (individual workflows change less frequently)
next: { revalidate: 600 },
});
if (!response.ok) {
if (response.status === 404) {
return NextResponse.json(
{
success: false,
error: `Workflow not found: ${id}`,
},
{ status: 404 }
);
}
console.error(
"Error fetching community workflow:",
response.status,
response.statusText
);
return NextResponse.json(
{
success: false,
error: `Workflow not found: ${id}`,
error: "Failed to load workflow",
},
{ status: 404 }
{ status: response.status }
);
}
// Read and parse workflow file
const content = await fs.readFile(filePath, "utf-8");
const workflow = JSON.parse(content);
const data = await response.json();
return NextResponse.json({
success: true,
workflow,
});
return NextResponse.json(data);
} catch (error) {
console.error("Error loading community workflow:", error);
return NextResponse.json(

96
src/app/api/community-workflows/route.ts

@ -1,78 +1,44 @@
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { CommunityWorkflowMeta } from "@/types/quickstart";
import {
getCommunityWorkflowConfig,
getDefaultCommunityConfig,
} from "@/lib/quickstart/communityWorkflows";
// Default to node-banana-pro hosted service
const COMMUNITY_WORKFLOWS_API_URL =
process.env.COMMUNITY_WORKFLOWS_API_URL ||
"https://nodebananapro.com/api/public/community-workflows";
/**
* GET: List all community workflows from the examples directory
* GET: List all community workflows from the remote API
*
* This proxies to the node-banana-pro hosted service which stores
* community workflows in R2 storage.
*/
export async function GET() {
try {
const examplesDir = path.join(process.cwd(), "examples");
const response = await fetch(COMMUNITY_WORKFLOWS_API_URL, {
headers: {
Accept: "application/json",
},
// Cache for 5 minutes
next: { revalidate: 300 },
});
// Check if examples directory exists
try {
await fs.access(examplesDir);
} catch {
return NextResponse.json({
success: true,
workflows: [],
});
if (!response.ok) {
console.error(
"Error fetching community workflows:",
response.status,
response.statusText
);
return NextResponse.json(
{
success: false,
error: "Failed to fetch community workflows",
},
{ status: response.status }
);
}
// Read directory contents
const files = await fs.readdir(examplesDir);
// Filter for JSON files (exclude directories like sample-images)
const jsonFiles = files.filter((file) => file.endsWith(".json"));
// Get metadata for each workflow
const workflows: CommunityWorkflowMeta[] = await Promise.all(
jsonFiles.map(async (filename) => {
const filePath = path.join(examplesDir, filename);
const stats = await fs.stat(filePath);
const id = filename.replace(/\.json$/, "");
// Get config or generate default
const config = getCommunityWorkflowConfig(id) ?? getDefaultCommunityConfig(id, filename);
const data = await response.json();
// Parse workflow to get node count
let nodeCount = 0;
try {
const content = await fs.readFile(filePath, "utf-8");
const workflow = JSON.parse(content);
nodeCount = workflow.nodes?.length ?? 0;
} catch {
// If parsing fails, default to 0
}
return {
id,
name: config.name,
filename,
author: config.author,
size: stats.size,
description: config.description,
nodeCount,
tags: config.tags,
previewImage: config.previewImage,
hoverImage: config.hoverImage,
sortOrder: config.sortOrder,
};
})
);
// Sort by sortOrder (ascending)
workflows.sort((a, b) => (a.sortOrder ?? 999) - (b.sortOrder ?? 999));
return NextResponse.json({
success: true,
workflows,
});
return NextResponse.json(data);
} catch (error) {
console.error("Error listing community workflows:", error);
return NextResponse.json(

93
src/lib/quickstart/communityWorkflows.ts

@ -1,93 +0,0 @@
/**
* Community workflow metadata configurations
*
* This file provides rich metadata for community workflows that can't be
* derived from the JSON file itself (descriptions, tags, thumbnails, etc.)
*/
export interface CommunityWorkflowConfig {
id: string;
name: string;
description: string;
author: string;
tags: string[];
previewImage?: string;
hoverImage?: string;
sortOrder: number;
}
/**
* Metadata configurations for community workflows.
* Workflows not listed here will get default values derived from their filename.
*/
export const COMMUNITY_WORKFLOW_CONFIGS: CommunityWorkflowConfig[] = [
{
id: "Fashion-image-to-video",
name: "Fashion Image to Video",
description: "Image to video workflow for fashion style videos.",
author: "@ReflctWillie",
tags: ["Gemini", "Fal"],
previewImage: "/template-thumbnails/community/fashion-image-to-video.jpg",
sortOrder: 1,
},
{
id: "contact-sheet-billsSupra",
name: "Bills Supra",
description: "Contact sheet prompt chaining with embedded text.",
author: "@ReflctWillie",
tags: ["Gemini"],
previewImage: "/template-thumbnails/community/bills-supra.jpg",
sortOrder: 2,
},
{
id: "contact-sheet-ChrisWalkman",
name: "Chris Walkman",
description: "Multi subject contact sheet prompt chaining experiment.",
author: "@ReflctWillie",
tags: ["Gemini"],
previewImage: "/template-thumbnails/community/chris-walkman.jpg",
sortOrder: 3,
},
];
/**
* Get the configuration for a specific community workflow by ID
*/
export function getCommunityWorkflowConfig(id: string): CommunityWorkflowConfig | undefined {
return COMMUNITY_WORKFLOW_CONFIGS.find((config) => config.id === id);
}
/**
* Generate default configuration for a workflow not in COMMUNITY_WORKFLOW_CONFIGS
*/
export function getDefaultCommunityConfig(id: string, filename: string): CommunityWorkflowConfig {
// Derive a readable name from the filename
const nameWithoutExt = filename.replace(/\.json$/, "");
let name: string;
if (nameWithoutExt.startsWith("contact-sheet-")) {
const namePart = nameWithoutExt.replace("contact-sheet-", "");
name = namePart
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/^./, (s) => s.toUpperCase());
} else if (nameWithoutExt.startsWith("workflow-")) {
name = nameWithoutExt
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
} else {
name = nameWithoutExt
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
return {
id,
name,
description: "Community workflow",
author: "Community",
tags: [],
sortOrder: 999, // Default to end of list
};
}
Loading…
Cancel
Save