Communitygithub.com

Portfolio Ship — Feishu Base → GitHub Pages + Actions

Turn a Feishu multi-dimensional table into a live portfolio on GitHub Pages, with Actions that refresh temporary Feishu media links on a schedule. Built for creators whose video/image assets live in Feishu (AI video, photo, design, sample libraries)—not blogs, ecommerce, or write-back backends.

What is Portfolio Ship — Feishu Base → GitHub Pages + Actions?

Personal websites Step 3 (ship): Jane-xiaoer/feishu-portfolio-launch. Content+media SSOT in Feishu → generated site → GitHub Pages + scheduled link refresh. Creator-facing publish path.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add Jane-xiaoer/feishu-portfolio-launch

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Feishu Portfolio Launch

What This Skill Does

In one sentence: Give me a Feishu Bitable link, I'll build a portfolio website, and automate handling the 'Feishu temporary link expiration' problem.

Default deliverables:

  • index.html or static frontend page
  • api/videos.json
  • api/covers.json
  • api/portfolio.json
  • refresh.py
  • .github/workflows/refresh.yml
  • Animated frontend interaction layer (CSS / JS / Canvas / Three.js all acceptable)

Built-in template resources:

Target outcome:

  • Website deployed on GitHub Pages
  • Feishu media links automatically refreshed every 12 hours via GitHub Actions
  • User doesn't need to keep their PC running
  • Pages not purely static; at least a complete set of entrance / scroll / hover / page-transition animations

Applicable Scenarios

✅ Suitable for:

  • AI video / image portfolios
  • Photography, design, creative sample libraries
  • Client case showcase sites
  • Any scenario where 'content is in Feishu attachments, frontend is only responsible for display'

❌ Not suitable for:

  • Textual blogs
  • E-commerce, payment, login systems
  • Websites requiring database writes and backend business logic

Core Principle

Never treat a Feishu temporary URL as a permanent resource.

Feishu attachment playback/download URLs expire. The correct approach is not to have users run scripts periodically on their local machines, but:

  1. Read the latest records from Feishu Bitable
  2. Call the Feishu API to resolve attachment file_token into temporary URLs
  3. Generate api/*.json
  4. Commit back to the GitHub repository
  5. Let GitHub Pages automatically redeploy

That is:

Feishu Bitable -> refresh.py -> api/*.json -> GitHub Pages


Execution Flow

Step 0: Environment Check

First check if the local environment has the required tools for initialization:

for cmd in node python3 gh git; do
  which "$cmd" >/dev/null 2>&1 && echo "OK  $cmd" || echo "MISS $cmd"
done
lark-cli --version >/dev/null 2>&1 && echo "OK  lark-cli" || echo "MISS lark-cli"
gh auth status >/dev/null 2>&1 && echo "OK  GitHub auth" || echo "MISS GitHub auth"

If tools are missing, ask the user to install them.
If gh is not logged in, complete GitHub login first.


Step 1: Get the Feishu Base and the Correct Data Table

Ask the user for the Feishu Bitable link, e.g.:

https://my.feishu.cn/base/AbCdEfGhIjk

Parse the app_token from the URL.

Then list all tables:

lark-cli api GET "/open-apis/bitable/v1/apps/${APP_TOKEN}/tables"

Do not default to selecting the first table.
Many Bases contain both an "empty template table" and a "real material table with works." Continue to read records and verify:

  • Which table has actual attachment fields
  • Which table has the correct number of records
  • Which table has field names that match the content the user wants to display

Prioritize finding these fields:

  • Video field: Sample / Video / Attachment
  • Cover field: Cover / Cover Image / Poster / Image
  • Title field: Content / Title / Work Name
  • Category field: Type / Category
  • Duration field: Duration
  • Tool field: AI Tool / Tool
  • Sort field: Order

If a table only has template fields like Text / Single Select / Date / Attachment, continue to the next table; do not generate the website.


Step 2: Verify Feishu Permissions

For GitHub Actions to refresh automatically, the Feishu app must at least be able to:

  • Read Bitable records
  • Read attachment media links

The execution plan should explicitly depend on these 4 secrets:

  • LARK_APP_ID
  • LARK_APP_SECRET
  • LARK_BASE_TOKEN
  • LARK_TABLE_ID

If the user already has a working Feishu app configuration on their machine, it can be reused. Otherwise, clearly instruct the user to add permissions and obtain keys from the Feishu Open Platform.


Step 3: Export Structured Work Data

Do not just extract a list of file_tokens.
Construct structured data directly consumable by the frontend, at least containing:

  • title
  • categories
  • duration
  • tools
  • order
  • video_token
  • cover_token

Recommended output:

  • api/portfolio.json: Complete array of works
  • api/videos.json: video_token -> tmp_download_url
  • api/covers.json: cover_token -> tmp_download_url

Structure examples can be directly referenced from assets/portfolio.sample.json in the repository.

The frontend can continue to use static HTML + JS, or be fully rendered from portfolio.json.


Step 4: Generate Frontend Page

Generate a static page by default, with the following requirements:

  • Responsive work grid
  • Category filtering
  • Click card to play
  • Full-screen playback
  • Only allow one video to play at a time
  • Cover preferably uses the Feishu cover field; if no cover, gracefully fallback to no poster

Animations are required to be delivered together by default; do not just create a "functional" static page:

  • At least one set of entrance animations on the first screen, e.g., title, description, stats fade-up in sequence
  • Navigation should allow blur / background / border changes after scrolling
  • Cards should have at least a lightweight hover feedback, e.g., lift, scale, overlay, title color change
  • Page or section transitions should have animations, not abrupt cuts
  • Information sections like About / Contact / Skills should preferably use scroll reveal
  • If the user desires stronger visual performance, Canvas or Three.js background animations can be added

Implementation boundaries:

  • Prioritize native CSS transitions / keyframes / IntersectionObserver
  • Use requestAnimationFrame only for continuously moving backgrounds
  • If Three.js is used, ensure it doesn't cause blank screens, freezes, or obscure main content on mobile
  • Animations must not affect video playback, filtering, modals, mobile scrolling, or first-screen readability
  • Provide graceful degradation; text and content should remain visible if scripts fail

Data binding principle:

  • HTML should not hardcode expired URLs
  • Page should fetch /api/videos.json and /api/covers.json at runtime
  • Or directly fetch /api/portfolio.json to render cards

If the user has an existing site, prioritize preserving the current visual style, only replacing the data source and refresh mechanism.

If the user explicitly mentions referencing an existing site style, prioritize reusing these animation types:

  • Hero text entrance
  • Sticky filter / nav scroll state transitions
  • Masonry card hover effects
  • Page fade-in/fade-out transitions
  • About page stagger reveal
  • Optional Three.js / Canvas background motion

Step 5: Create Repository and Deploy GitHub Pages

Standard flow:

gh repo create <repo-name> --public --source=. --remote=origin --push

Then confirm the Pages source:

  • branch: main
  • path: /

Check:

gh api repos/<owner>/<repo>/pages

Must confirm:

  • Pages is enabled
  • cname is correct (if custom domain)
  • Source points to main and root directory

Step 6: Generate Auto-Refresh Script

The refresh script should follow this route:

  1. Exchange app_id + app_secret for tenant_access_token
  2. Read Bitable records
  3. Extract video and cover attachment tokens from records
  4. Batch call batch_get_tmp_download_url
  5. Write to api/videos.json, api/covers.json, api/portfolio.json

Key requirements:

  • Support paginated reading of records
  • Batch requests in groups of 5 tokens
  • Do not hardcode the token list in the script
  • Prefer dynamic generation from actual table records
  • Allow multiple candidate field names

Recommended filename:

  • refresh.py

Prefer directly copying the skill's built-in template:

cp ~/.claude/skills/feishu-portfolio-launch/assets/refresh.py ./refresh.py

Then only modify the field candidate arrays; do not hardcode specific tokens or URLs.


Step 7: Generate GitHub Actions Workflow

Do not use local launchd as the default.
The default must be GitHub Actions.

Workflow requirements:

  • Support workflow_dispatch
  • Support schedule, default execution every 12 hours
  • Execute python refresh.py
  • Automatically commit api/videos.json, api/covers.json, api/portfolio.json

Standard secrets:

  • LARK_APP_ID
  • LARK_APP_SECRET
  • LARK_BASE_TOKEN
  • LARK_TABLE_ID

Standard file:

  • .github/workflows/refresh.yml

Prefer directly copying the skill's built-in template:

mkdir -p .github/workflows
cp ~/.claude/skills/feishu-portfolio-launch/assets/refresh.yml .github/workflows/refresh.yml

Step 8: Configure Secrets in Repository

If the current environment already has these values, and the user explicitly asks to apply them, directly configure:

gh secret set LARK_APP_ID -R <owner>/<repo>
gh secret set LARK_APP_SECRET -R <owner>/<repo>
gh secret set LARK_BASE_TOKEN -R <owner>/<repo>
gh secret set LARK_TABLE_ID -R <owner>/<repo>

Then verify:

gh secret list -R <owner>/<repo>

If any value is missing, stop here and clearly tell the user which one is missing; do not fabricate.


Step 9: Manually Trigger and Verify the Full Chain

After completing configuration, manually run the workflow once:

gh workflow run refresh.yml -R <owner>/<repo>
gh run list -R <owner>/<repo> --limit 3
gh run watch <run-id> -R <owner>/<repo> --exit-status

Then check the online result:

python3 - <<'PY'
import json, urllib.request, time
url = f'https://<domain-or-pages-url>/api/videos.json?t={int(time.time())}'
with urllib.request.urlopen(url, timeout=20) as r:
    data = json.load(r)
print(data.get('_refreshed_at'))
print(data.get('_count'))
PY

Then spot-check 1 video:

  • Status code should be 206 or 200
  • content-type should be video/mp4 or a playable media type

If the online api/videos.json still has an old timestamp:

  • First check if Pages deployment is complete
  • Then check if the workflow actually committed new JSON
  • Then confirm if Pages source is pointing to the correct branch and directory

Step 10: Domain Binding

If the user wants a custom domain, proceed with:

  • CNAME file
  • GitHub Pages custom domain settings
  • DNS configuration guidance

This part follows the standard GitHub Pages flow and does not need to be coupled with Feishu logic.


Default Explanation to Users

When encountering "website can open but video won't play," first diagnose:

  1. Is api/videos.json still old?
  2. Are the Feishu URLs inside expired?
  3. Did the auto-refresh workflow fail?
  4. Has Pages not redeployed yet?

By default, do not first suspect the player or the frontend styles.

If the user says "the page is too static" or "I want it to feel more alive like a portfolio," add animation layers by default, rather than just changing colors and layout.


Success Criteria

Upon completing this Skill, at least the following must be met:

  • Website is live
  • GitHub Pages is accessible
  • Videos are playable
  • Repository has auto-refresh workflow
  • Repository has the 4 LARK_* secrets
  • Manual trigger of the workflow succeeded
  • Online api/videos.json has a new _refreshed_at timestamp

What Not to Default To

  • Do not default to generating local launchd
  • Do not default to relying on the user's Mac being on indefinitely
  • Do not hardcode all file_tokens
  • Do not proceed with only the first table
  • Do not generate only videos.json, ignoring covers and structured records

Reference Implementation

If a verified approach is needed, first directly reuse the skill repository's built-in templates:

  • assets/refresh.py
  • assets/refresh.yml
  • assets/portfolio.sample.json

In the actual project, the following files will eventually be generated:

  • refresh.py
  • .github/workflows/refresh.yml
  • api/videos.json
  • api/covers.json
  • api/portfolio.json

Prioritize reusing the existing repository pattern, then adapt according to the user's site style.

Related Skills