Use when the user wants to design, customize, or generate a custom visual theme for their linkyee site (a Hexo-like LinkTree-style static site). Triggers on phrases like "design my linkyee page", "make a custom theme", "change the style", "I want my links to look like X", "create a new theme inspired by Y". Generates a complete theme directory under `themes/`, wires it up in `config.yml`, and verifies the build.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use when the user wants to design, customize, or generate a custom visual theme for their linkyee site (a Hexo-like LinkTree-style static site). Triggers on phrases like "design my linkyee page", "make a custom theme", "change the style", "I want my links to look like X", "create a new theme inspired by Y". Generates a complete theme directory under `themes/`, wires it up in `config.yml`, and verifies the build.
allowed-tools
Read, Write, Edit, Bash, Glob, Grep
linkyee Style Designer
You help users design custom visual themes for linkyee — a Ruby/Liquid static site generator that builds a single-page LinkTree-style site. This skill teaches you the project's mechanics so you don't have to re-derive them, and codifies design quality standards so the output doesn't look like generic AI slop.
How linkyee builds a site
Build command: bundle exec ruby ./scaffold.rb
Output: _output/ (committed to gh-pages branch by deploy.sh in CI)
Theme switch: edit the theme: field at the top of config.yml to a directory name under themes/
The build copies themes/<theme>/ verbatim into _output/, then renders _output/index.html through Liquid using config.yml as variable scope
Theme directory contract
A theme dir must contain:
index.html — Liquid template (see required hooks below)
— the look
styles.css
scripts.js — can be empty, but the file must exist (default theme references it)
A theme dir should contain:
images/profile.jpeg — sample avatar (so the theme works before the user replaces it)
images/favicons/favicon.ico — at minimum a single favicon
For new themes, copy these two image assets from themes/default/images/ rather than generating new ones.
Required Liquid variables and HTML hooks
Every index.html you generate must preserve these variables (the user's config.yml data flows through them):
Variable
Purpose
{{ lang }}
<html lang>
{{ title }}
<title>, OG/Twitter meta
{{ name }}
Display name
{{ tagline }}
Bio / description
{{ avatar }}
Avatar image src
{{ links }}
Loop: {% for item in links %}{% assign link = item.link %} then use link.url, link.icon, link.text, link.title, link.alt, link.target
{{ socials }}
Loop: {% for item in socials %}{% assign social = item.social %} then use social.url, social.icon, social.title, social.alt, social.target
{{ footer }}
Footer HTML
{{ copyright }}
Footer copyright HTML
{{ last_modified_at }}
Build timestamp meta
{{ google_analytics_id }}
Wrap GA snippet in {% if google_analytics_id %}…{% endif %}
{{ vars.<PluginName> }}
Plugin output (rare; user-controlled)
Required HTML structural elements (preserve from themes/default/index.html so SEO/PWA features keep working):
<title>, <meta name="description">, all og: and twitter: meta tags using the variables above
Favicon <link> tags (paths point to ./images/favicons/...)
The GA <script> block wrapped in the Liquid {% if %} guard
A <script src="./scripts.js"> reference at the end of <body>
Font Awesome: link from CDN to keep theme dirs small:
(The original default theme self-hosts Font Awesome; new themes use the CDN.)
Design principles — the "no AI slop" baseline
Distilled from alchaincyf/huashu-design. If the user has that skill installed (npx skills add alchaincyf/huashu-design), defer to it for deeper design philosophy and animation/export tooling. The rules below are the minimum bar this skill enforces.
Avoid these AI-design clichés:
Purple → pink gradients
Emoji used as functional icons (Font Awesome icons exist for a reason)
Generic "glassy gradient blob" backgrounds with no purpose
Center-aligned everything with no hierarchy
Drop shadows on every element
Prefer these moves:
A clear typographic hierarchy: one display face for the name/headline, one body face for tagline + links
text-wrap: pretty (or balance) on the tagline so line breaks don't look ragged
Real units: a single accent color, deliberate whitespace, consistent border radius across all elements (don't mix 4px buttons with 24px cards)
One signature move per theme (e.g. brutalist border-shadow, editorial drop cap, terminal scanlines) — don't pile effects
Quality bar every theme must meet:
WCAG AA contrast on body text and button text
Keyboard-accessible: visible :focus-visible styles on every link and button
Respects @media (prefers-reduced-motion: reduce) if you use any motion (disable transitions/animations in that block)
Dark mode (auto-switch): see the dedicated section below — non-negotiable.
Responsive (RWD): see the dedicated section below — non-negotiable, this is the single most common failure mode.
Dark mode — required, auto-switch only
Every theme must work in both system appearances. The user does not click a button — the OS toggles between light and dark and the theme MUST follow. This is non-negotiable and a frequent source of regressions, so verify it explicitly.
Hard rules:
Auto-switch via @media (prefers-color-scheme: dark) { ... } only. No JavaScript toggle. Nocolor-scheme: light only (that disables auto-switching). No<html data-theme="…"> workaround. The user's macOS / iOS / Windows / Android system setting is the source of truth.
Don't just invert. Define a separate dark palette. Pure #000 background causes banding on OLED panels and gives nothing for shadows or borders to push against — use elevated grays from the #0d1117 / #14110d / #1c1814 family. White-on-black body text is harsh — prefer #e6edf3 / #f3ecdf etc.
Re-tune accents. A red that reads on cream often goes muddy on charcoal; a soft pastel disappears. Test each accent color against the dark surface and either brighten/desaturate or pick a sibling hue. Both modes need WCAG AA (4.5:1 body, 3:1 large) — re-check on dark, don't assume.
Surface hierarchy still has to read. On dark, cards / link buttons need to be one step lighter than the body (e.g. body #0d1117, card #161b22). Borders that were rgba(0,0,0,0.12) on light should drop in opacity, not just flip color.
Don't lose the signature move. A brutalist drop-shadow in pure black disappears on a dark page — switch the shadow to a neon (cyan / yellow) on dark. A frosted glass card needs a different blur tint on a dark gradient. Keep the theme's identity in both modes; redesign the trick instead of dropping it.
Latest-link badge (.link-latest-badge) must be re-tuned per mode. The light-mode color usually doesn't read on the dark surface — add a @media (prefers-color-scheme: dark) { .link-latest-badge { ... } } override.
Focus rings. A #1f6feb ring that pops on white may need to brighten to #58a6ff on dark. Don't share :focus-visible styling blindly across modes.
Authoring patterns:
Prefer CSS-vars-driven palette swap: define :root { --bg: ...; --fg: ...; ... } once, then @media (prefers-color-scheme: dark) { :root { --bg: ...; ... } } only changes the values. The rest of the CSS reads var(--bg) etc. and never needs duplicate rules. This is the cleanest pattern and the one most built-in themes use.
Per-rule overrides are acceptable when only a few properties change (e.g. default theme), but the vars approach scales better.
terminal-retro is the dark-first reference: its :root defaults are dark (green-on-black CRT), and the LIGHT variant is added via @media (prefers-color-scheme: light) (the inverse of the usual pattern) — translating the CRT identity to "deep olive on cream printer-paper". This is the right move when your theme has a dominant identity in one mode: keep that identity dominant, but still translate it into the other mode rather than skipping it. A user who pins color_scheme: light deserves SOMETHING that fits the theme, not the dark version unchanged.
color_scheme override (required for new themes):
Users can pin the page to a specific mode via color_scheme: auto | light | dark in config.yml. The theme must respect this — the system-default auto-switch is not enough. Wire it up like this:
index.html — add the scheme class on <html>:
<html lang="{{ lang }}"{% if color_scheme == "light" %} class="scheme-light"{% elsif color_scheme == "dark" %} class="scheme-dark"{% endif %}>
For auto, no class is added; the @media query fires normally.
styles.css — gate the dark block on :not(.scheme-light) and add a .scheme-dark block that duplicates the dark vars. With CSS variables this is two short blocks:
:root { /* light vars */ }
/* Auto + OS=dark, OR explicit "dark" */@media (prefers-color-scheme: dark) {
:root:not(.scheme-light) { /* dark vars */ }
}
/* Forced "dark" — beats system preference */:root.scheme-dark { /* same dark vars */ }
Yes, the dark vars are duplicated. There is no clean CSS-only way to avoid it short of light-dark() (Baseline 2024 — fine to use if you want a single source of truth and don't need IE-era support). For per-rule themes (no vars, like default), prefix every dark selector with :root:not(.scheme-light) and duplicate every rule under :root.scheme-dark. Mechanical but verbose.
Verification — set color_scheme: light in config.yml, rebuild, force your OS into dark mode. The page should stay light. Set color_scheme: dark, force OS to light. Page should stay dark. Set color_scheme: auto, toggle OS. Page should swap live.
Dark-first themes (e.g. CRT, neon, vaporwave) should still ship a light variant — but the light variant should translate the genre, not invert it (e.g. terminal-retro becomes "olive-on-cream printer paper" in light mode, not a generic light theme). Skipping a light variant is a last resort; a thoughtfully translated one is almost always more respectful of users with color_scheme: light set.
Verification (do not skip):
After your edits, before declaring the theme done:
Run bundle exec ruby ./scaffold.rb then open _output/index.html.
Toggle macOS System Settings → Appearance between Light and Dark (or use DevTools → Rendering → "Emulate CSS prefers-color-scheme"). The page must swap palettes without refresh.
In each mode, confirm: body text ≥ 4.5:1 contrast against background, latest-badge readable, focus ring visible, signature move (shadow / glass / drop cap / scanlines) still has identity.
If you used ./scripts/screenshot-themes.sh or generated screenshots, capture both light and dark and compare side-by-side. Two visually-identical screenshots means the dark CSS is broken — investigate before shipping.
Responsive design (RWD) — required
linkyee is overwhelmingly viewed on phones. A theme that looks great at 1440px but breaks at 360px is a broken theme. Treat the small-screen pass as part of "the theme is done", not a polish step.
Layout rules:
Mobile-first. Author the base CSS for ~360px-wide phones; layer up with min-width media queries for tablet/desktop refinements. Don't write desktop-first then patch with max-width queries.
Container width: use width: 100% with a max-width (e.g. 480–640px for the link column). Never set fixed pixel widths on top-level containers.
No horizontal scroll at any width 320–1920px. If something would overflow (long unbroken strings like URLs, RSS post titles, repo names), use overflow-wrap: anywhere or text-overflow: ellipsis with white-space: nowrap — pick one per element and stick with it.
Test at minimum these widths: 320, 375, 414, 768, 1024, 1440. Eyeball each. If you used huashu-design or any preview/screenshot capability, capture at 375 and 1280 at minimum.
Typography:
Use clamp() for headline/name sizes so they scale fluidly without breakpoint jumps, e.g. font-size: clamp(1.6rem, 5vw, 2.4rem).
Body text floor of 16px on mobile (smaller triggers iOS auto-zoom on inputs and is below WCAG comfort threshold).
Long Latest-link titles (RSS feed posts, YouTube video titles) can be 60+ chars — clamp them. The standard pattern: .link-latest .link-text { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; } Adapt the badge per theme but keep the truncation.
Touch targets:
Minimum 44×44 CSS px hit area for every link, button, and social icon (Apple HIG / WCAG 2.5.5). Pad with padding, not just height, so the tappable area scales.
Minimum 8px gap between adjacent tappable elements (no rage-tap collisions).
Images / media:
max-width: 100%; height: auto on every <img>, including the avatar. The avatar should be a clamp()-sized circle so it doesn't dominate small viewports.
Don't use background-image for content images — they don't scale predictably across DPRs.
Density / safe areas:
Use padding: env(safe-area-inset-top) ... env(safe-area-inset-bottom) on the outermost container if you have edge-to-edge color, so notched iPhones don't crop content.
Reduce vertical padding on small viewports — what looks airy on desktop looks wasted on a 568px-tall iPhone SE.
Verification step (do not skip):
After running bundle exec ruby ./scaffold.rb, also do one of the following before declaring the theme done:
./preview.sh <new-theme> and resize the browser through 320 → 1440 manually, OR
Use Playwright/headless browser at minimum 375×667 and 1280×800, OR
Open _output/index.html and toggle DevTools device mode through iPhone SE, iPhone 14, iPad, desktop.
If any of: horizontal scroll appears, text overflows the container, tap targets shrink below 44px, or the avatar covers more than ~30% of viewport height — fix before reporting back.
When the user's brief is vague
Offer them 2–3 differentiated directions before writing code. Pick from these design schools (mirroring huashu-design's "schools" pattern, scoped to a single-page link list):
Serif headlines, drop caps, two-tone palette, magazine columns
Bloggers, journalists
Neo-brutalism
Thick borders, hard offset shadows, primary colors, no anti-aliasing of intent
Indie devs, artists
Soft & friendly
Pastel cards, rounded corners, hand-drawn accents, casual type
Creators, illustrators
Terminal/Retro
Monospace, CRT scanlines, blinking caret, green-on-black or amber
Hackers, infosec
Built-in themes already exist for each: minimal-mono, editorial-serif, neo-brutalism, paper-card, terminal-retro, plus glassmorphism. Read one of them as a starting point if the user's request is close to an existing direction.
Workflow you should follow
Read context first — Readconfig.yml to see the user's actual content (name, tagline, link count, link types). Don't design in the abstract.
Read a reference theme — Readthemes/default/index.html (canonical structure) and one closer-aesthetic theme from themes/.
Confirm direction — if the brief is vague, present 2–3 options with one-line aesthetic descriptions. Wait for the user to pick.
Generate the theme — create themes/<kebab-name>/:
index.html — copy the structural skeleton from themes/default/index.html, preserve every Liquid variable and meta tag listed above, swap Font Awesome for the CDN <link>, redesign the body markup as needed
styles.css — the actual style work
scripts.js — empty file (or actual JS if the design needs it)
images/profile.jpeg — copy from themes/default/images/profile.jpeg via cp
images/favicons/favicon.ico — copy from themes/default/images/favicons/favicon.ico via cp
Wire it up — update config.ymltheme: field via Edit. Match the exact existing format (theme: default → theme: <new-name>).
Verify the build — run bundle exec ruby ./scaffold.rb. Confirm:
Exit code 0 (no Liquid render errors)
_output/index.html exists
It contains the user's actual name and tagline text (no raw {{ left in output)
Tell the user how to preview — ./preview.sh <theme-name> builds + serves on http://localhost:8080.
Common pitfalls to avoid
Don't drop the GA {% if %} guard. Many users leave google_analytics_id blank; without the guard you'll inject a broken gtag config.
Don't change the loop variable assignment. Liquid uses {% assign link = item.link %} (not {% assign link = item %}) — config.yml nests link data one level deeper than feels natural.
Don't remove og:image meta — it uses {{ avatar }} and link previews on social media depend on it.
Don't hardcode the user's name/tagline into CSS (e.g. as a ::before pseudo-element). The site is rebuilt every time config.yml changes; the CSS must stay content-agnostic.
scripts.js must exist even if empty — index.html references it.
Don't break default. When generating a new theme, leave themes/default/ untouched.