Add SEO Dungeon#224
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the "seo-dungeon" plugin, a gamified local SEO audit tool integrated with Codex, Claude, and Gemini CLI workflows. The changes include adding the plugin to the marketplace, README, and plugins registry, alongside its extensive suite of SEO skills, templates, and references. The review feedback highlights a critical runtime issue where the hook configuration references scripts that are missing from the repository. Additionally, several robustness improvements are recommended in the interactive cluster map template, specifically to prevent potential TypeErrors when parsing malformed data and to handle edge cases in the color-darkening utility. Finally, a naming inconsistency in the plugin registry should be resolved to use the canonical lowercase name.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| "${CLAUDE_PLUGIN_ROOT}/hooks/run-python-hook.js", | ||
| "${CLAUDE_PLUGIN_ROOT}/hooks/validate-schema.py", |
| function darken(hex) { | ||
| var r = parseInt(hex.slice(1, 3), 16); | ||
| var g = parseInt(hex.slice(3, 5), 16); | ||
| var b = parseInt(hex.slice(5, 7), 16); | ||
| r = Math.max(0, Math.floor(r * 0.75)); | ||
| g = Math.max(0, Math.floor(g * 0.75)); | ||
| b = Math.max(0, Math.floor(b * 0.75)); | ||
| return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); | ||
| } |
There was a problem hiding this comment.
The darken function assumes that the hex parameter is always a 7-character string starting with # followed by 6 hex digits. If a 3-character hex code (e.g., #fff) or a CSS color name is passed, this function will produce NaN values and return an invalid color string (e.g., #an). Consider adding support for 3-character hex codes and a fallback mechanism.
function darken(hex) {
if (!hex || hex.indexOf('#') !== 0) return '#000000';
var color = hex.slice(1);
if (color.length === 3) {
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
}
if (color.length !== 6) return '#000000';
var r = parseInt(color.slice(0, 2), 16);
var g = parseInt(color.slice(2, 4), 16);
var b = parseInt(color.slice(4, 6), 16);
r = Math.max(0, Math.floor(r * 0.75));
g = Math.max(0, Math.floor(g * 0.75));
b = Math.max(0, Math.floor(b * 0.75));
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}| } else { | ||
| const parts = nodeId.match(/cluster-(\d+)-post-(\d+)/); | ||
| if (!parts) return; | ||
| info = data.clusters[parseInt(parts[1])].posts[parseInt(parts[2])]; | ||
| } |
There was a problem hiding this comment.
Accessing data.clusters[parseInt(parts[1])].posts directly without verifying if the cluster at the parsed index exists can lead to a TypeError if the index is out of bounds or the data is malformed. Additionally, it is a best practice to specify the radix (base 10) for parseInt.
| } else { | |
| const parts = nodeId.match(/cluster-(\d+)-post-(\d+)/); | |
| if (!parts) return; | |
| info = data.clusters[parseInt(parts[1])].posts[parseInt(parts[2])]; | |
| } | |
| } else { | |
| const parts = nodeId.match(/cluster-(\d+)-post-(\d+)/); | |
| if (!parts) return; | |
| const cluster = data.clusters[parseInt(parts[1], 10)]; | |
| if (cluster && cluster.posts) { | |
| info = cluster.posts[parseInt(parts[2], 10)]; | |
| } | |
| } |
| } else { | ||
| const parts = nodeId.match(/cluster-(\d+)-post-(\d+)/); | ||
| if (parts) { | ||
| const post = data.clusters[parseInt(parts[1])].posts[parseInt(parts[2])]; | ||
| label = "Spoke page: " + post.title; | ||
| } | ||
| } |
There was a problem hiding this comment.
Accessing data.clusters[parseInt(parts[1])].posts directly without verifying if the cluster at the parsed index exists can lead to a TypeError if the index is out of bounds or the data is malformed. Additionally, it is a best practice to specify the radix (base 10) for parseInt.
| } else { | |
| const parts = nodeId.match(/cluster-(\d+)-post-(\d+)/); | |
| if (parts) { | |
| const post = data.clusters[parseInt(parts[1])].posts[parseInt(parts[2])]; | |
| label = "Spoke page: " + post.title; | |
| } | |
| } | |
| } else { | |
| const parts = nodeId.match(/cluster-(\d+)-post-(\d+)/); | |
| if (parts) { | |
| const cluster = data.clusters[parseInt(parts[1], 10)]; | |
| const post = cluster && cluster.posts ? cluster.posts[parseInt(parts[2], 10)] : null; | |
| if (post) { | |
| label = "Spoke page: " + post.title; | |
| } | |
| } | |
| } |
| "install_url": "https://raw.githubusercontent.com/jingjing2222/rust-reverse-engineering-skill/HEAD/.codex-plugin/plugin.json" | ||
| }, | ||
| { | ||
| "name": "SEO Dungeon", |
There was a problem hiding this comment.
The name of the plugin in plugins.json is set to "SEO Dungeon", but in .agents/plugins/marketplace.json and the plugin's own plugin.json it is defined as "seo-dungeon". It is recommended to use the lowercase canonical name "seo-dungeon" to maintain consistency across all registry files.
| "name": "SEO Dungeon", | |
| "name": "seo-dungeon", |
Summary
plugins/avalonreset/seo-dungeon.plugins.jsonand.agents/plugins/marketplace.jsonwith the new marketplace entry.Validation
python scripts\validate-plugin-pr.py --base-ref origin/mainpasses.plugin-scanner scan plugins\avalonreset\seo-dungeon --format text --min-score 80 --fail-on-severity highpasses: 91/100, no critical/high findings.