| name | linkyee-plugin-builder |
| description | Use when the user wants to add dynamic data (GitHub stars, latest blog posts, weather, follower counts, repo activity, anything fetched from a URL) to their linkyee site by writing a build-time plugin. Triggers on phrases like "add a plugin", "show my latest Medium post on the page", "fetch X and display it", "make linkyee pull data from Y", "inject a value into the page". Generates `plugins/<PluginName>.rb`, wires it up in `config.yml`, references it in the right Liquid spot, and runs the build to verify. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
linkyee Plugin Builder
You help users add build-time plugins to linkyee — a Ruby/Liquid
static-site generator. A plugin is a small Ruby class that fetches some
data at build time and exposes it to the templates as vars.<PluginName>.
The canonical reference for the plugin contract lives at
plugins/README.md. Read it before you
write code; it contains examples, helpers, and conventions that this
skill expects you to follow. Do not duplicate that information here —
this skill is the workflow, the wiki is the spec.
Mental model in one paragraph
config.yml lists enabled plugins. At build time scaffold.rb
instantiates <PluginName>.new(yaml_values), calls execute(), and
stores the return value at settings["vars"][<PluginName>]. Liquid then
renders {{ vars.<PluginName> }} (and friends) inside config.yml
strings and the theme's index.html. If a plugin raises, scaffold logs
the error and sets the value to nil — the build still completes.
Workflow
Follow these steps in order. Don't skip steps; the verification step
in particular catches most mistakes.
1. Understand the request
Before writing anything, confirm:
- What data does the user want? (a star count, a list of posts, a
weather temp, a follower count, the latest commit, …)
- From where? (specific URL, RSS feed, JSON API, scraped HTML)
- Where on the page should it appear? (footer, a link's text, a new
link, a new section in
index.html)
If anything is ambiguous, ask one focused question. Don't ask three
clarifying questions at once.
2. Check whether a built-in plugin already covers it
Read plugins/README.md § "Built-in plugins". If GithubRepoStarsCountPlugin,
GithubLastCommitPlugin, GithubProfilePlugin, or RSSFeedPlugin
already does the job, don't write a new one — just enable and
reference it. Tell the user.
3. Read the existing config
Run Read on config.yml to see how the user already structures their
links and what's already in vars. This affects naming and where to
inject the value.
4. Write the plugin
Create plugins/<PluginName>.rb:
- Name the class to match the file. Use a descriptive
*Plugin suffix
(e.g. WeatherPlugin, MediumLatestPostsPlugin).
require_relative 'Plugin' at the top.
- Inherit from
Plugin.
- Use the helpers (
http_get, http_get_json, args, params, cache,
log) — do not call Net::HTTP or URI directly. They're already
wrapped with redirect following, timeouts, and error handling.
- Be defensive. Return a sensible default (
0, "", {}, []) on
failure. Plugins must not raise.
- Use String keys in returned hashes (Liquid can't look up symbol
keys).
- Match the data shape on success and failure (don't return a Hash on
success and
nil on failure — Liquid breaks).
Look at the existing built-in plugins as templates — they're short and
demonstrate the patterns.
5. Wire it up in config.yml
Add to plugins: using the YAML style that matches the plugin's
arguments (list-style for args, hash-style for params). Then
reference {{ vars.<PluginName>… }} in the appropriate
links/socials/title/tagline/footer field, or edit the
theme's index.html if the user wants a loop / conditional.
6. Verify the build
Run:
bundle exec ruby ./scaffold.rb
Then check:
- Build exits 0
- No
[<PluginName>] or [scaffold] Plugin '<PluginName>' failed lines
in stderr
- The injected value appears in
_output/index.html (grep for it)
If the value is missing, read the stderr output — the new
scaffold.rb swallows plugin failures but logs them clearly. Do not
guess; fix the underlying error.
7. Secrets — non-negotiable
If the plugin needs an API key, personal access token, OAuth token,
or any other credential:
If the user asks you to "just paste the token in config.yml for now",
refuse and explain why. The secret will end up in git history and
on the deployed gh-pages branch — no easy way to revoke once leaked.
8. Report back
Tell the user:
- What plugin was created
- What got added to
config.yml
- Where in the rendered page the value now appears
- How to extend it (e.g. "add more entries to the list under
MyPlugin:")
What NOT to do
- Don't add new gems to the
Gemfile unless absolutely required.
nokogiri (already in Gemfile) plus stdlib net/http and json
cover the vast majority of cases.
- Don't scrape sites that have an obvious public JSON/Atom endpoint.
RSS / Atom (
/feed, /commits.atom) and JSON APIs are far more
stable than HTML scraping.
- Don't hardcode credentials. If a token is required, read it from
ENV and document the secret name in the plugin's header comment.
- Don't add per-request side effects (writing files, sending
notifications).
execute should be a pure data fetch.
- Don't invoke things at build time that can take more than ~30s in
total — the GitHub Actions build is on a free runner and we run it
daily. Cap the work.
- Don't touch theme files unless the user asked for layout changes.
Most plugin requests are satisfied by editing
config.yml only.
- Don't create a new plugin when an existing one (
RSSFeedPlugin
especially) already handles the case.
Anti-patterns to refuse
- "Make it run in the visitor's browser instead." — linkyee is a
static site; plugins are build-time only. If they want runtime data,
they need separate JS in the theme — out of scope for this skill.
- "Have the plugin email me when X." — no side effects in plugins.
Suggest a separate GitHub Actions workflow.