diff --git a/bin/init.js b/bin/init.js index 9bf99d0..da8aaa1 100644 --- a/bin/init.js +++ b/bin/init.js @@ -185,7 +185,44 @@ export async function init() { type: 'input', name: 'description', message: 'Give a one-line description of your mini app (optional):', - default: 'A Farcaster mini-app created with Neynar' + default: 'A Farcaster mini app created with Neynar' + }, + { + type: 'list', + name: 'primaryCategory', + message: 'It is strongly recommended to choose a primary category and tags to help users discover your mini app.\n\nSelect a primary category:', + choices: [ + { name: 'Games', value: 'games' }, + { name: 'Social', value: 'social' }, + { name: 'Finance', value: 'finance' }, + { name: 'Utility', value: 'utility' }, + { name: 'Productivity', value: 'productivity' }, + { name: 'Health & Fitness', value: 'health-fitness' }, + { name: 'News & Media', value: 'news-media' }, + { name: 'Music', value: 'music' }, + { name: 'Shopping', value: 'shopping' }, + { name: 'Education', value: 'education' }, + { name: 'Developer Tools', value: 'developer-tools' }, + { name: 'Entertainment', value: 'entertainment' }, + { name: 'Art & Creativity', value: 'art-creativity' }, + new inquirer.Separator(), + { name: 'Skip (not recommended)', value: null } + ], + default: 'social' + }, + { + type: 'input', + name: 'tags', + message: 'Enter tags for your mini app (separate with spaces or commas, optional):', + default: '', + filter: (input) => { + if (!input.trim()) return []; + // Split by both spaces and commas, trim whitespace, and filter out empty strings + return input + .split(/[,\s]+/) + .map(tag => tag.trim()) + .filter(tag => tag.length > 0); + } }, { type: 'input', @@ -333,6 +370,8 @@ export async function init() { // Append all remaining environment variables fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`); + fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_PRIMARY_CATEGORY="${answers.primaryCategory}"`); + fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_TAGS="${answers.tags.join(',')}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`); fs.appendFileSync(envPath, `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`); if (useNeynar && neynarApiKey && neynarClientId) { diff --git a/index.d.ts b/index.d.ts index 857cffe..069d671 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,5 @@ /** - * Initialize a new Farcaster mini-app project + * Initialize a new Farcaster mini app project * @returns Promise */ export function init(): Promise; \ No newline at end of file diff --git a/package.json b/package.json index 0df22b8..ba870e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.2.24", + "version": "1.2.25", "type": "module", "private": false, "access": "public", @@ -22,6 +22,9 @@ "frame", "frames-v2", "farcaster-frames", + "miniapps", + "miniapp", + "mini-apps", "mini-app", "neynar", "web3" diff --git a/scripts/build.js b/scripts/build.js index d070daf..2ea2932 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -161,6 +161,8 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase }); const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); + const tags = process.env.NEXT_PUBLIC_FRAME_TAGS?.split(','); + return { accountAssociation: { header: encodedHeader, @@ -177,6 +179,9 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase splashImageUrl: `https://${domain}/splash.png`, splashBackgroundColor: "#f7f7f7", webhookUrl, + description: process.env.NEXT_PUBLIC_FRAME_DESCRIPTION, + primaryCategory: process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY, + tags, }, }; } @@ -346,6 +351,8 @@ async function main() { // Frame metadata `NEXT_PUBLIC_FRAME_NAME="${frameName}"`, `NEXT_PUBLIC_FRAME_DESCRIPTION="${process.env.NEXT_PUBLIC_FRAME_DESCRIPTION || ''}"`, + `NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY="${process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY || ''}"`, + `NEXT_PUBLIC_FRAME_TAGS="${process.env.NEXT_PUBLIC_FRAME_TAGS || ''}"`, `NEXT_PUBLIC_FRAME_BUTTON_TEXT="${buttonText}"`, // Neynar configuration (if it exists in current env) diff --git a/scripts/deploy.js b/scripts/deploy.js index b0c7404..b5dd3e5 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -72,6 +72,8 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase }); const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); + const tags = process.env.NEXT_PUBLIC_FRAME_TAGS?.split(','); + return { accountAssociation: { header: encodedHeader, @@ -80,14 +82,17 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase }, frame: { version: "1", - name: process.env.NEXT_PUBLIC_FRAME_NAME?.trim(), + name: process.env.NEXT_PUBLIC_FRAME_NAME, iconUrl: `https://${trimmedDomain}/icon.png`, homeUrl: `https://${trimmedDomain}`, imageUrl: `https://${trimmedDomain}/api/opengraph-image`, - buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT?.trim(), + buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT, splashImageUrl: `https://${trimmedDomain}/splash.png`, splashBackgroundColor: "#f7f7f7", webhookUrl: webhookUrl?.trim(), + description: process.env.NEXT_PUBLIC_FRAME_DESCRIPTION, + primaryCategory: process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY, + tags, }, }; } @@ -113,6 +118,8 @@ async function loadEnvLocal() { 'SEED_PHRASE', 'NEXT_PUBLIC_FRAME_NAME', 'NEXT_PUBLIC_FRAME_DESCRIPTION', + 'NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY', + 'NEXT_PUBLIC_FRAME_TAGS', 'NEXT_PUBLIC_FRAME_BUTTON_TEXT', 'NEYNAR_API_KEY', 'NEYNAR_CLIENT_ID' diff --git a/scripts/dev.js b/scripts/dev.js index aa181e5..ab49b8c 100755 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -101,7 +101,7 @@ async function startDev() { 1. Open the localtunnel URL in your browser: ${tunnel.url} 2. Enter your IP address in the password field${ip ? `: ${ip}` : ''} (note that this IP may be incorrect if you are using a VPN) 3. Click "Click to Submit" -- your mini app should now load in the browser - 4. Navigate to the Warpcast Mini App Developer Tools: https://warpcast.com/~/developers/mini-apps + 4. Navigate to the Warpcast Mini App Developer Tools: https://warpcast.com/~/developers 5. Enter your mini app URL: ${tunnel.url} 6. Click "Preview" to launch your mini app within Warpcast (note that it may take ~10 seconds to load) @@ -120,7 +120,7 @@ async function startDev() { frameUrl = 'http://localhost:3000'; console.log(` 💻 To test your mini app: - 1. Open the Warpcast Mini App Developer Tools: https://warpcast.com/~/developers/mini-apps + 1. Open the Warpcast Mini App Developer Tools: https://warpcast.com/~/developers 2. Scroll down to the "Preview Mini App" tool 3. Enter this URL: ${frameUrl} 4. Click "Preview" to test your mini app (note that it may take ~5 seconds to load the first time) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d4d0168..fb7324a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,6 +1,8 @@ export const APP_URL = process.env.NEXT_PUBLIC_URL!; export const APP_NAME = process.env.NEXT_PUBLIC_FRAME_NAME; export const APP_DESCRIPTION = process.env.NEXT_PUBLIC_FRAME_DESCRIPTION; +export const APP_PRIMARY_CATEGORY = process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY; +export const APP_TAGS = process.env.NEXT_PUBLIC_FRAME_TAGS?.split(','); export const APP_ICON_URL = `${APP_URL}/icon.png`; export const APP_OG_IMAGE_URL = `${APP_URL}/api/opengraph-image`; export const APP_SPLASH_URL = `${APP_URL}/splash.png`; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3bb6c65..d0383c0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,26 +1,31 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { mnemonicToAccount } from 'viem/accounts'; -import { APP_BUTTON_TEXT, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_SPLASH_BACKGROUND_COLOR, APP_URL, APP_WEBHOOK_URL } from './constants'; +import { APP_BUTTON_TEXT, APP_DESCRIPTION, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_TAGS, APP_URL, APP_WEBHOOK_URL } from './constants'; import { APP_SPLASH_URL } from './constants'; interface FrameMetadata { + version: string; + name: string; + iconUrl: string; + homeUrl: string; + imageUrl?: string; + buttonTitle?: string; + splashImageUrl?: string; + splashBackgroundColor?: string; + webhookUrl?: string; + description?: string; + primaryCategory?: string; + tags?: string[]; +}; + +interface FrameManifest { accountAssociation?: { header: string; payload: string; signature: string; }; - frame: { - version: string; - name: string; - iconUrl: string; - homeUrl: string; - imageUrl: string; - buttonTitle: string; - splashImageUrl: string; - splashBackgroundColor: string; - webhookUrl: string; - }; + frame: FrameMetadata; } export function cn(...inputs: ClassValue[]) { @@ -51,12 +56,15 @@ export function getFrameEmbedMetadata(ogImageUrl?: string) { splashImageUrl: APP_SPLASH_URL, iconUrl: APP_ICON_URL, splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR, + description: APP_DESCRIPTION, + primaryCategory: APP_PRIMARY_CATEGORY, + tags: APP_TAGS, }, }, }; } -export async function getFarcasterMetadata(): Promise { +export async function getFarcasterMetadata(): Promise { // First check for FRAME_METADATA in .env and use that if it exists if (process.env.FRAME_METADATA) { try { @@ -123,6 +131,9 @@ export async function getFarcasterMetadata(): Promise { splashImageUrl: APP_SPLASH_URL, splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR, webhookUrl: APP_WEBHOOK_URL, + description: APP_DESCRIPTION, + primaryCategory: APP_PRIMARY_CATEGORY, + tags: APP_TAGS, }, }; }