lovstudio-maintain-partners
Maintain the LovStudio website's partners section AND align partner logo rows on event posters / hero strips: reuse lovstudio-find-logo for brand logo discovery, normalize collected logos to a 240px-tall content canvas (retina-ready), rasterize SVGs via rsvg-convert before normal
What it does
maintain-partners — LovStudio 合作伙伴板块维护
Maintains the configured website repo. Resolve the path from --repo,
LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, or the shared user profile. The partners
strip usually lives in app/(main)/(home)/PartnersGrid.tsx as a PARTNERS: Partner[] array; older sites may still keep it in
app/(main)/(home)/WorkshopDispatch.tsx. Logos live in
public/partners/<slug>/logo.png; taglines in
src/i18n/messages/{zh-CN,en,ja,th}.json under dispatch.partner*Tagline.
User Configuration
Before touching files, resolve:
SKILL_ROOT="${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}"
SKILL_DIR="${SKILL_DIR:-$SKILL_ROOT/lovstudio-maintain-partners}"
WEB_ROOT="${LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT:?Set this or pass --repo}"
PARTNERS_TSX="${LOVSTUDIO_MAINTAIN_PARTNERS_FILE:-app/(main)/(home)/PartnersGrid.tsx}"
Use this precedence for the website root:
- Explicit
--repo <path>onadd_partner.py/audit_partners.py. LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT.- Shared profile JSON at
${LOVSTUDIO_SKILLS_PROFILE:-$HOME/.lovstudio/skills/profile.json}.
LOVSTUDIO_WEB_ROOT and PARTNERS_SITE_ROOT are accepted as legacy aliases,
but should not be the public contract for reusable skills.
Use this precedence for the partners TSX file:
- Explicit
--partners-file <path>. LOVSTUDIO_MAINTAIN_PARTNERS_FILE.- Shared profile keys
sites.partners_file,lovstudio.partners_file,partners.file, orworkspace.partners_file. app/(main)/(home)/PartnersGrid.tsx, then legacyapp/(main)/(home)/WorkshopDispatch.tsx.
LOVSTUDIO_PARTNERS_FILE and PARTNERS_FILE are accepted as legacy aliases,
but should not be the public contract for reusable skills.
For details and supported profile keys, read references/user-config.md.
Skill Dependencies
lovstudio-find-logois required for all logo discovery. This skill must not scrape homepages itself or keep a separate fallback crawler.- Use the
depends_onfrontmatter field to declare skill-level dependencies. This mirrors thedepends_onfield inlovstudio-general-skills/skills.yaml; unknown frontmatter keys are expected to be ignored by agents that do not consume dependency metadata.
When to Use
- User asks to add one or more new partners (with or without a logo URL).
- User asks to standardize / normalize a logo (sizing wrong, white-on-white, etc.).
- User provides a local file and asks to replace an existing partner's logo.
- User asks to audit the partners section before a release.
Standards
- Logo canvas: 80px content height for the website partners strip
(light grayscale, CSS
height: 32px≈ 2.5× density, sharp enough), 240px for event posters or any retina export atscale: 2or higher. - For white-on-transparent logos: invert (full or selective) so they show on the light grayscale strip.
- For icon-only logos < ~40px wide after normalization: pass
--show-namewhen adding so the brand name renders next to the icon. - Tagline format:
<品牌名> · <一句话定位>in Chinese; mirror style in en/ja/th.
Workflow
Op 1: Add a new partner
- Ask the user for the brand name + homepage URL via
AskUserQuestion. - Collect the logo with
lovstudio-find-logo:
Use the archived primary asset underpython3 "$SKILL_ROOT/lovstudio-find-logo/scripts/find_logo.py" \ --name "<显示名>" --url <URL> --slug <slug> --json~/.lovstudio/logo-collection/<slug>/logo.<ext>. Iffind_logo.pyreturns no candidates, stop and ask the user for a better official URL / press-kit URL, then rerunfind_logo.py. Do not call a local scraper from this skill. - Visually verify the archived primary asset before normalizing.
- If the primary asset is SVG, rasterize it before normalization:
Use the rasterizedrsvg-convert -h 240 ~/.lovstudio/logo-collection/<slug>/logo.svg \ -o /tmp/<slug>-raw.png/tmp/<slug>-raw.pngas--src. For non-SVG sources, use the archived primary asset directly. - Normalize:
python3 "$SKILL_DIR/scripts/normalize_logo.py" \ --src <archived-or-rasterized-logo> \ --dst "$WEB_ROOT/public/partners/<slug>/logo.png" \ --invert auto - Read the normalized PNG to confirm it's visible (not white-on-white).
- Append to PARTNERS + all 4 locale JSONs:
python3 "$SKILL_DIR/scripts/add_partner.py" \ --repo "$WEB_ROOT" \ --partners-file "$PARTNERS_TSX" \ --name "<显示名>" --href "<URL>" \ --logo "/partners/<slug>/logo.png" \ --key partner<Slug>Tagline \ --category community \ --zh "..." --en "..." --ja "..." --th "..." \ [--show-name]
Op 2: Normalize an existing logo
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src public/partners/<slug>/logo.png \
--dst public/partners/<slug>/logo.png \
--invert auto
Re-read after to verify.
Op 3: Replace logo from a user-provided file
Ask for the source file path directly, or read it from the user's configured workspace/profile. Do not assume a private partners folder.
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src "<user-provided path>" \
--dst "$WEB_ROOT/public/partners/<slug>/logo.png" \
--invert auto
JPEG inputs auto-strip near-white background to transparent before crop.
Op 4: Audit
python3 "$SKILL_DIR/scripts/audit_partners.py" \
--repo "$WEB_ROOT" \
--partners-file "$PARTNERS_TSX"
# add --probe to also HTTP-check every href (slow, requires proxy)
Reports: missing logo files, missing i18n keys per locale, dead URLs.
Op 5: Align a row of partner logos (cross-asset visual height parity)
When: putting 3+ partner logos in a single horizontal strip and they look
different sizes despite having the same CSS height. Common in event posters,
hero sections, "联办 / co-host" rows.
Root cause: each source file has different internal padding (designer
canvas margin), so two PNGs both set to height: 24px render at different
visible heights because their content occupies different fractions of the
canvas. Per-logo CSS height tweaks based on eyeballed content ratios are
unstable—different displays / scaling will diverge again.
Reliable fix — trim at file level, uniform CSS box:
-
Normalize every logo to identical content height. Default raster file target is 240px (3× density for retina poster export at
scale: 2; 80px gives only 1.7× and looks soft after PNG export). Use--invert offif the source is already light-on-transparent (don't double-invert):for f in lujiazui juanyi citic-bookstore citic-thinker-lab; do python3 "$SKILL_DIR/scripts/normalize_logo.py" \ --src "<configured-partners-source>/<brand>/<file>.png" \ --dst <event-assets>/partners/$f.png \ --height 240 --invert auto doneAlways normalize from the original source, never from a previously normalized 80px file (upscaling = blurry — burned by this on juanyi).
-
For SVG sources, rasterize first.
normalize_logo.pyoperates on raster pixels and cannot crop SVG viewBox padding. Without this step an SVG always renders smaller than rasterized PNG siblings:rsvg-convert -h 720 brand.svg -o /tmp/brand-raw.png # 3× of 240 python3 "$SKILL_DIR/scripts/normalize_logo.py" \ --src /tmp/brand-raw.png --dst <event-assets>/partners/brand.png \ --height 240 --invert offrsvg-convertships withlibrsvg(brew install librsvg). -
For SVG with embedded background rect (icon wrapped in a black/colored rounded square — common in app-icon-style SVGs from
find-logo), strip the background before rasterizing, otherwise filterbrightness(0) invert(1)flattens it into a solid white block that hides the icon:# Drop the outer <rect fill="#000"...> wrapper sed -E 's|<rect[^/]*fill="#0+"[^/]*/>||' brand.svg > /tmp/brand-clean.svg rsvg-convert -h 720 /tmp/brand-clean.svg -o /tmp/brand-raw.png -
Wrap each logo in a fixed-size box (recommended over auto-width flex):
<span class="ps-logo-box"><img src="..." class="ps-logo"></span>.ps-logo-box { width: 96px; height: 30px; /* fixed grid cell */ display: inline-flex; align-items: center; justify-content: center; border: 1px solid rgba(255,255,255,0.10); border-radius: 4px; padding: 3px 6px; box-sizing: border-box; } .ps-logo { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }Fixed boxes give a stable matrix look — narrow logos (icon-only) and wide logos (icon + wordmark) all occupy the same footprint, with the asset scaled to fit. Auto-width flex (the older recipe) makes per-row total widths unpredictable as logos get added/removed.
-
Dark-background unification — when the row sits on a dark canvas (e.g. event poster), most brand logos are designed for white BG and look inconsistent (some have black text, some have brand-colored marks). The stable recipe:
.ps-logo { filter: brightness(0) invert(1) opacity(0.88); } /* logos already white-on-transparent — opt out of inversion */ .ps-logo.ps-logo-original { filter: opacity(0.88); }brightness(0)flattens all colors to black, theninvert(1)produces uniform white at the configured opacity. The.ps-logo-originalescape hatch is for source files that are already white-on-transparent (white SVG variants from a brand kit) so you don't double-process them into invisible black-on-dark. -
Icon-only SVG → composite icon + wordmark — if the brand SVG only has an icon (no "BrandName" wordmark beside it), don't ship just the icon in a 96×30 box (it'll look like an unidentified mark). Compose the wordmark with PIL using the brand's own font when possible:
from PIL import Image, ImageDraw, ImageFont, ImageOps # 1. rasterize cleaned SVG, invert white→black so default filter works icon = Image.open('/tmp/brand-icon.png').convert('RGBA') r, g, b, a = icon.split() inv = Image.merge('RGB', (ImageOps.invert(r), ImageOps.invert(g), ImageOps.invert(b))) icon = Image.merge('RGBA', (*inv.split(), a)) icon = icon.crop(icon.getbbox()) target_h = 240 icon = icon.resize((int(icon.width * target_h / icon.height), target_h), Image.LANCZOS) # 2. render wordmark in brand font (find-logo bundles fonts/ when found) font = ImageFont.truetype('partners/<brand>/fonts/<Family>.ttf', 150) # 3. compose icon + gap + text on transparent canvasThe PNG goes through the same
brightness(0) invert(1)filter as raster logos — match colors with all other entries automatically. Use the brand's own font (often shipped under<brand>/fonts/by the find-logo skill); fall back to system SF / Helvetica only if no brand font is available. -
Anti-pattern — do not try to fix alignment by setting per-logo heights like
.ps-logo-juanyi { height: 26px }. It's brittle (every new logo needs another magic number), unstable across browsers, and breaks the moment a designer reships the source asset with different padding.
CLI Reference
normalize_logo.py
| Flag | Default | Notes |
|---|---|---|
--src | required | input image (PNG/JPG/rasterized SVG) |
--dst | required | output PNG path; parent dirs auto-created |
--height | 80 | target content height. Use 240 for retina poster export (scale: 2) — 80 looks soft after 2× downscale. |
--invert | auto | auto / off / full / selective (selective preserves colored icons) |
add_partner.py
| Flag | Notes |
|---|---|
--repo | website repo root; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, profile JSON, or legacy LOVSTUDIO_WEB_ROOT / PARTNERS_SITE_ROOT |
--partners-file | PARTNERS TSX file; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_FILE, profile JSON, legacy LOVSTUDIO_PARTNERS_FILE / PARTNERS_FILE, PartnersGrid.tsx, or WorkshopDispatch.tsx |
--name | display name (CJK ok) |
--href | brand URL |
--logo | path under /public, e.g. /partners/foo/logo.png |
--key | i18n key, e.g. partnerFooTagline |
--category | compute / peer / invest / media / community; default community |
--zh / --en / --ja / --th | tagline strings (all required) |
--show-name | render name next to icon for narrow logos |
audit_partners.py
| Flag | Notes |
|---|---|
--repo | website repo root; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, profile JSON, or legacy LOVSTUDIO_WEB_ROOT / PARTNERS_SITE_ROOT |
--partners-file | PARTNERS TSX file; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_FILE, profile JSON, legacy LOVSTUDIO_PARTNERS_FILE / PARTNERS_FILE, PartnersGrid.tsx, or WorkshopDispatch.tsx |
--probe | HTTP-probe every href (slow, needs proxy env vars) |
Network proxy
Sandbox child processes don't inherit the system ClashX proxy. Before
fetching logos with lovstudio-find-logo or probing partner URLs, export:
export https_proxy=http://127.0.0.1:7890 \
http_proxy=http://127.0.0.1:7890 \
all_proxy=socks5://127.0.0.1:7891
audit_partners.py already injects these for curl invocations.
Dependencies
git clone https://github.com/lovstudio/find-logo-skill \
"${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}/lovstudio-find-logo"
python3 -m pip install Pillow
brew install librsvg # for SVG logo sources
Capabilities
Install
Quality
deterministic score 0.48 from registry signals: · indexed on github topic:agent-skills · 54 github stars · SKILL.md body (13,688 chars)