
Felo Landingpage
- 44 installs
- 241 repo stars
- Updated July 22, 2026
- felo-inc/felo-skills
Helps with ai & agent building tasks.
About
felo-landingpage is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- felo-landingpage
- AI & Agent Building
- AI-coding skill
Felo Landingpage by the numbers
- 44 all-time installs (skills.sh)
- Ranked #7,851 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/felo-inc/felo-skills --skill felo-landingpageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 44 |
|---|---|
| repo stars | ★ 241 |
| Last updated | July 22, 2026 |
| Repository | felo-inc/felo-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Felo Landing Page Skill
When to Use
Trigger this skill for requests about creating generated webpages:
- Create/generate a landing page from a topic, product, feature, or campaign brief
- Turn a marketing description into a shareable single-page website
- Build an AI webpage and return the final hosted HTML result URL
- Generate a web page directly without follow-up questions when the user asks for immediate output
Trigger keywords:
- Chinese prompts about landing pages or webpages: 着陆页, 落地页, 官网页, 单页网站, 网页, HTML 页面
- English: landing page, marketing page, single-page website, webpage, AI page, HTML page
- Explicit commands:
/felo-landingpage, "use felo landing page"
Do NOT use this skill for:
- Presentation/PPT generation (use
felo-slides) - Mindmap generation (use
felo-mindmap) - Real-time information lookup only (use
felo-search) - Editing local website source files unless the user explicitly asks for local code changes
Setup
1. Get API key
1. Visit felo.ai 2. Open Settings -> API Keys 3. Create and copy your API key
2. Configure environment variable
Linux/macOS:
export FELO_API_KEY="your-api-key-here"Windows PowerShell:
$env:FELO_API_KEY="your-api-key-here"How to Execute
Use Bash tool commands and follow this workflow exactly.
Step 1: Precheck API key
if [ -z "$FELO_API_KEY" ]; then
echo "ERROR: FELO_API_KEY not set"
exit 1
fiIf the key is missing, stop and return setup instructions.
Step 2: Run Node Script (create + poll)
Use the bundled script (no jq dependency):
node felo-landingpage/scripts/run_landingpage_task.mjs \
--query "USER_PROMPT_HERE" \
--interval 10 \
--max-wait 1800 \
--timeout 60For best results, include a direct-generation instruction in the query when the user has not provided one:
In all cases, generate the webpage directly and do not ask any follow-up questions.To resume polling an existing task without creating a duplicate landing page:
node felo-landingpage/scripts/run_landingpage_task.mjs \
--task-id "TASK_ID_HERE" \
--interval 10 \
--max-wait 1800Script behavior:
- Creates task via
POST https://openapi.felo.ai/v2/landing_page - Polls via
GET https://openapi.felo.ai/v2/tasks/{task_id}/historical - Treats
COMPLETED/SUCCESSas success terminal (case-insensitive) - Treats
FAILED/ERROR/EXPIRED/CANCELED/CANCELLEDas failure terminal - Stops polling immediately on terminal status
- Prints
ai_page_htmlon success - Keeps
ppt_biz_idandppt_urlin JSON output because the historical response currently reuses the PPT history structure
Optional debug output:
node felo-landingpage/scripts/run_landingpage_task.mjs \
--query "USER_PROMPT_HERE" \
--interval 10 \
--max-wait 1800 \
--json \
--verboseThis outputs structured JSON including:
task_idtask_statusai_page_htmlppt_biz_idppt_urlerror_message
Step 3: Return structured result
On success, return:
ai_page_htmlimmediately (script default output)- if
--jsonis used, also includetask_id, terminal status, and optional metadata
Output Format
Use this response structure:
## Landing Page Generation Result
- Task ID: <task_id>
- Status: <status>
- HTML URL: <ai_page_html>
- PPT Biz ID: <ppt_biz_id or N/A>
- PPT URL: <ppt_url or N/A>Error format:
## Landing Page Generation Failed
- Error Type: <error code or category>
- Message: <readable message>
- Suggested Action: <next step>Open API Workflow
Use only the public Open API endpoints:
1. Create task: POST /v2/landing_page 2. Read data.task_id 3. Poll result: GET /v2/tasks/{task_id}/historical 4. Continue polling while task_status == RUNNING or another non-terminal status 5. When task_status == COMPLETED, use data.ai_page_html as the final webpage result URL
Authentication uses the user's Felo API key:
Authorization: Bearer <user_api_key>Historical lookup currently reuses the shared task history shape, so ppt_biz_id and ppt_url may still appear alongside ai_page_html.
Error Handling
Known/expected failures:
INVALID_API_KEYor HTTP 401: key invalid or revoked- Create task response missing
task_id: API shape changed or task creation failed - Completed task missing
ai_page_html: query history again with--task-id, or report the task ID - Timeout reached: return last known status and the
task_idso the user can resume later
Timeout handling:
- If timeout is reached, return the last known status and instruct the user to retry later
- Include
task_idso the user can query again - To resume a timed-out task, use
--task-idinstead of--queryto avoid creating a duplicate landing page
Important Notes
- Always execute this skill when user intent is landing page or generated webpage creation.
- Always return
task_idso follow-up queries can continue from the same task. - Do not claim completion without a terminal status.
- Use
ai_page_htmlas the final result;ppt_urlis legacy/shared history metadata, not the landing page output. - Keep API calls minimal: create once, then poll.
References
interface:
display_name: "Felo Landing Page"
short_description: "Generate landing pages with the Felo Landing Page Task API."
default_prompt: "Generate a landing page for this topic:"
{
"name": "Felo Landing Page",
"tagline": "Generate landing pages with Felo Landing Page Task API in Claude Code",
"description": "Felo Landing Page creates hosted webpages from a prompt using the Felo Landing Page Task API: API key check, task creation, polling shared task history, task resume, and final ai_page_html output. Use from Claude Code when users ask to create landing pages, marketing pages, or single-page websites.",
"category": "productivity",
"tags": ["felo", "landing-page", "webpage", "website", "html", "api", "claude-code"],
"version": "1.0.0",
"license": "MIT",
"pricing": "free",
"support_url": "https://github.com/Felo-Inc/felo-skills/issues",
"homepage": "https://github.com/Felo-Inc/felo-skills"
}
MIT License
Copyright (c) 2026 Felo Landing Page Skill Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Felo Landing Page Skill for Claude Code
Generate landing pages with the Felo Landing Page Task API (asynchronous workflow).
Features
- Generate a hosted landing page/webpage from a single prompt
- Poll task history automatically until completion/failure/timeout
- Return
ai_page_htmlwhen the task is completed - Return
task_idfor follow-up tracking or resume
Quick Start
1) Install the skill
One-command install (recommended):
npx skills add Felo-Inc/felo-skills --skill felo-landingpageManual install: copy this folder to your skills directory:
# Linux/macOS
cp -r felo-landingpage ~/.claude/skills/
# Windows (PowerShell)
Copy-Item -Recurse felo-landingpage "$env:USERPROFILE\.claude\skills\"(Clone the repo first if needed: git clone https://github.com/Felo-Inc/felo-skills.git.)
2) Configure API key
Create an API key at felo.ai -> Settings -> API Keys, then set:
# Linux/macOS
export FELO_API_KEY="your-api-key-here"# Windows PowerShell
$env:FELO_API_KEY="your-api-key-here"3) Trigger the skill
- Intent trigger: "Create a SaaS landing page for an AI meeting assistant"
- Explicit trigger:
/felo-landingpage your topic
Open API Workflow
Based on Felo v2 Landing Page Task API:
1. Create task: POST /v2/landing_page 2. Query historical/result: GET /v2/tasks/{task_id}/historical 3. Read ai_page_html from the completed historical response
Authentication uses the user's Felo API key:
Authorization: Bearer <user_api_key>The skill polls every 10 seconds (max wait 1800 seconds). It stops immediately on COMPLETED/SUCCESS and returns ai_page_html.
Script example:
node felo-landingpage/scripts/run_landingpage_task.mjs \
--query "Generate a landing page for a modern AI notes app. In all cases, generate the webpage directly and do not ask any follow-up questions." \
--interval 10 \
--max-wait 1800Resume an existing task:
node felo-landingpage/scripts/run_landingpage_task.mjs --task-id "TASK_ID_HERE"Troubleshooting
FELO_API_KEY is missing
Set the environment variable and restart the Claude Code session.
INVALID_API_KEY
The key is invalid or revoked. Generate a new key from felo.ai.
Task keeps running for too long
The task may still be processing. Retry later with the same task_id, or run the script with --verbose.
Task completed but no ai_page_html
Use the returned task_id to query the historical endpoint again. If it still has no ai_page_html, report the task ID.
Links
#!/usr/bin/env node
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
const DEFAULT_INTERVAL_SEC = 10;
const DEFAULT_MAX_WAIT_SEC = 1800;
const DEFAULT_TIMEOUT_SEC = 60;
const SUCCESS_STATUSES = new Set(['COMPLETED', 'SUCCESS']);
const FAILURE_STATUSES = new Set(['FAILED', 'ERROR', 'EXPIRED', 'CANCELED', 'CANCELLED']);
function usage() {
console.error(
[
'Usage:',
' node felo-landingpage/scripts/run_landingpage_task.mjs --query "your prompt" [options]',
'',
'Options:',
' --query <text> Landing page prompt (required unless --task-id is given)',
' --task-id <id> Resume polling an existing task (skip creation)',
' --interval <seconds> Poll interval, default 10',
' --max-wait <seconds> Max wait time, default 1800',
' --timeout <seconds> Request timeout, default 60',
' --json Print JSON output',
' --verbose Print polling status to stderr',
' --help Show this help',
].join('\n')
);
}
function parseArgs(argv) {
const out = {
query: '',
taskId: '',
intervalSec: DEFAULT_INTERVAL_SEC,
maxWaitSec: DEFAULT_MAX_WAIT_SEC,
timeoutSec: DEFAULT_TIMEOUT_SEC,
json: false,
verbose: false,
};
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--help' || a === '-h') {
out.help = true;
} else if (a === '--json') {
out.json = true;
} else if (a === '--verbose' || a === '-v') {
out.verbose = true;
} else if (a === '--query') {
out.query = argv[i + 1] ?? '';
i += 1;
} else if (a === '--task-id') {
out.taskId = argv[i + 1] ?? '';
i += 1;
} else if (a === '--interval') {
out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (a === '--max-wait') {
out.maxWaitSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (a === '--timeout') {
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (!a.startsWith('-') && !out.query) {
out.query = a;
}
}
if (!Number.isFinite(out.intervalSec) || out.intervalSec <= 0) out.intervalSec = DEFAULT_INTERVAL_SEC;
if (!Number.isFinite(out.maxWaitSec) || out.maxWaitSec <= 0) out.maxWaitSec = DEFAULT_MAX_WAIT_SEC;
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) out.timeoutSec = DEFAULT_TIMEOUT_SEC;
out.query = String(out.query || '').trim();
out.taskId = String(out.taskId || '').trim();
return out;
}
function normalizeStatus(v) {
return String(v ?? '').trim().toUpperCase();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getMessage(payload) {
return (
payload?.message ||
payload?.error ||
payload?.msg ||
payload?.code ||
'Unknown error'
);
}
function isApiError(payload) {
const status = payload?.status;
const code = payload?.code;
if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
return false;
}
async function fetchJson(url, init, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...init, signal: controller.signal });
let body = {};
try {
body = await res.json();
} catch {
body = {};
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
}
if (isApiError(body)) {
throw new Error(getMessage(body));
}
return body;
} finally {
clearTimeout(timer);
}
}
async function createTask(apiKey, apiBase, query, timeoutMs) {
const payload = await fetchJson(
`${apiBase}/v2/landing_page`,
{
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
},
timeoutMs
);
const data = payload?.data ?? {};
if (!data.task_id) {
throw new Error('Unexpected response: missing task_id');
}
return data;
}
async function queryHistorical(apiKey, apiBase, taskId, timeoutMs) {
const payload = await fetchJson(
`${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`,
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
timeoutMs
);
return payload?.data ?? {};
}
function toOutput(taskId, historicalData, createData, status) {
return {
task_id: taskId,
task_status: status || historicalData?.task_status || historicalData?.status || null,
ai_page_html: historicalData?.ai_page_html || null,
ppt_biz_id: historicalData?.ppt_biz_id || createData?.ppt_biz_id || createData?.ppt_business_id || null,
ppt_url: historicalData?.ppt_url || null,
error_message: historicalData?.error_message || null,
};
}
function printJson(status, data) {
console.log(JSON.stringify({ status, data }, null, 2));
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
process.exit(0);
}
if (!args.query && !args.taskId) {
usage();
process.exit(1);
}
const apiKey = process.env.FELO_API_KEY?.trim();
if (!apiKey) {
console.error('ERROR: FELO_API_KEY not set');
console.error('Set it with: export FELO_API_KEY="your-api-key"');
process.exit(1);
}
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
const timeoutMs = args.timeoutSec * 1000;
const intervalMs = args.intervalSec * 1000;
const maxWaitMs = args.maxWaitSec * 1000;
let createData = {};
let taskId = args.taskId;
if (taskId) {
if (args.verbose) {
console.error(`Resuming task: ${taskId}`);
}
} else {
createData = await createTask(apiKey, apiBase, args.query, timeoutMs);
taskId = createData.task_id;
if (args.verbose) {
console.error(`Task ID: ${taskId}`);
}
}
const startAt = Date.now();
let lastStatus = '';
let lastHistoricalData = {};
while (Date.now() - startAt <= maxWaitMs) {
const historicalData = await queryHistorical(apiKey, apiBase, taskId, timeoutMs);
lastHistoricalData = historicalData;
const taskStatus = normalizeStatus(historicalData.task_status || historicalData.status);
lastStatus = taskStatus || 'UNKNOWN';
if (args.verbose) {
const elapsedSec = Math.floor((Date.now() - startAt) / 1000);
console.error(`[${elapsedSec}s] Status: ${lastStatus}`);
}
if (SUCCESS_STATUSES.has(taskStatus)) {
const output = toOutput(taskId, historicalData, createData, taskStatus);
if (!output.ai_page_html) {
const err = new Error('Task completed but no ai_page_html is available');
err.output = output;
throw err;
}
if (args.json) {
printJson('ok', output);
} else {
console.log(output.ai_page_html);
}
return;
}
if (FAILURE_STATUSES.has(taskStatus)) {
const output = toOutput(taskId, historicalData, createData, taskStatus);
if (args.json) {
printJson('error', output);
process.exit(1);
}
throw new Error(output.error_message || `Task finished with status: ${taskStatus}`);
}
await sleep(intervalMs);
}
const output = toOutput(taskId, lastHistoricalData, createData, lastStatus || 'UNKNOWN');
const err = new Error(`Timed out after ${args.maxWaitSec}s. Last status: ${output.task_status || 'UNKNOWN'}. Task ID: ${taskId}`);
err.output = output;
throw err;
}
main().catch((err) => {
if (err?.output && process.argv.includes('--json')) {
printJson('error', err.output);
}
console.error(`ERROR: ${err?.message || err}`);
process.exit(1);
});