feat(website): SEO, GEO, and accessibility improvements#294
feat(website): SEO, GEO, and accessibility improvements#294rafaeltonholo merged 28 commits intomainfrom
Conversation
- Creates /docs/alternatives with sections covering manual conversion, Android Studio import, community tools, and a feature comparison table. - Adds the Alternatives card to the docs index.
- Add missing `import java.time.LocalDate` to build.gradle.kts to fix script compilation error (Unresolved reference 'time') - Fix PascalCase-to-kebab-case conversion in sitemap generator; premature `.lowercase()` was applied before the regex ran, so GradlePlugin.kt was emitted as /docs/gradleplugin instead of /docs/gradle-plugin
- Use InlineCode for class names, CLI flags, file extensions, attributes - Use CodeBlock for command examples in FAQ - Use SiteLinkStyleVariant for links - Fix Android Studio Import: mention CMP workaround via composeResources - Add custom parser vs Android Studio algorithm context - Fix comparison table: SVG subset (not AVG), add SVG feature coverage row - Add SVGO/Avocado install instructions to optimization FAQ - Add FAQ and Alternatives to docs nav dropdown
Escape all JSON spec control characters (U+0000-U+001F) in escapeJsonString(), not just the common five. Add sameAs GitHub link to WebSiteStructuredData for stronger entity recognition.
- Trim title tags to fit within 60 characters (with suffix). - Trim meta descriptions for Alternatives and Docs Index to stay under 160 chars. - Enrich Docs Index title with keyword context.
Replace attr("role", "table") with Modifier.role("table") per project
convention. Remove unused PlatformTable* aliases that just re-assigned
DocsStyles values.
Match visible H2 headings to the question wording used in the FAQPage JSON-LD schema so screen readers and search engines see consistent content.
Add ArrowUp/Down/Home/End keyboard navigation for ARIA menu compliance. Add aria-label to menu panel. Extract doc link entries into a shared list to prevent drift with footer DocsLinks.
Move version badge outside H1 to keep heading keyword-focused. Change subheadline from div to H2 with fontWeight(Normal) so search engines give heading-level weight to the conversion keywords.
Add DocsLinks component using shared docLinkEntries in a nav landmark. Use CSS ::before middle-dot separators between links. Change Attribution from flex Row to inline Span for natural word wrapping. Reduce footer padding on mobile for better proportions.
ToC links inside CollapsibleSection rendered inline without a flex container, causing them to run together. Wrap in Column for proper vertical stacking.
Reusable action that configures Git user.name and user.email with the svg-to-compose-bot[bot] identity using the GitHub App ID.
Replace providers.exec git call with LAST_MODIFIED from app.properties. The deploy workflow updates the value before build, then commits it via svg-to-compose-bot after successful deployment. Uses GITHUB_TOKEN for the push to avoid triggering other workflows.
WalkthroughAdds site-generation tasks (sitemap, llms-full), new docs pages and structured-data types, UI/accessibility refinements, a GitHub composite action to configure a bot identity, and a workflow job that updates Changes
Sequence DiagramsequenceDiagram
participant GH as "GitHub Actions"
participant Export as "export job"
participant Gradle as "Gradle site tasks"
participant FS as "File System / Repo"
participant Deploy as "deploy job"
participant Update as "update-last-modified job"
participant Bot as "Configure-bot action"
GH->>Export: workflow starts
Export->>Gradle: run site generation tasks (generateLlmsTxt, generateLlmsFullTxt, generateSitemap)
Gradle->>FS: read `app.properties` (LAST_MODIFIED)
Gradle->>FS: write generated files (public/*.txt, .well-known/*, sitemap.xml)
Export->>Export: compute UTC TODAY, set job output `last-modified`
Export->>Deploy: continue deploy
GH->>Update: trigger update-last-modified (needs: deploy, export)
Update->>FS: checkout main
Update->>Bot: run configure-bot (`app-id`)
Update->>FS: replace LAST_MODIFIED in `app.properties`
Update->>FS: run `git diff`
alt changes detected
Update->>FS: git commit & push (using bot identity)
else no changes
Update-->>Update: skip commit/push
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.kt (1)
236-290: Consider extracting the repetitive Td styling into a helper function.Each
Tdcell applies the same modifier chain with onlytextAlignvarying. This could be simplified with a helper function to reduce duplication.♻️ Optional: Extract common cell styling
`@Composable` private fun DataCell( text: String, palette: SitePalette, textAlign: TextAlign = TextAlign.Center, ) { Td( attrs = DocsTableCellStyle .toModifier() .textAlign(textAlign) .color(palette.onSurface) .toAttrs(), ) { Text(text) } }Then usage becomes:
Tr(attrs = backgroundModifier.toAttrs()) { DataCell(row.feature, palette, TextAlign.Start) DataCell(row.svgToCompose, palette) DataCell(row.manualCoding, palette) DataCell(row.androidStudio, palette) DataCell(row.otherTools, palette) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.kt` around lines 236 - 290, The ComparisonTableRow contains repeated Td styling; extract that common modifier chain into a helper composable (e.g., DataCell) that accepts the cell text (String), palette (SitePalette), and an optional textAlign: TextAlign = TextAlign.Center, build the Td with DocsTableCellStyle.toModifier().textAlign(textAlign).color(palette.onSurface).toAttrs() and emit Text(text) inside; then replace the five inline Td blocks in ComparisonTableRow with DataCell(row.feature, palette, TextAlign.Start) and DataCell(row.svgToCompose, palette), DataCell(row.manualCoding, palette), DataCell(row.androidStudio, palette), DataCell(row.otherTools, palette).website/site/src/jsMain/resources/llms-full.txt.template (1)
302-319: Consider adding an "AI assistance" section with common prompt patterns.Since this file targets LLMs, including examples of how to effectively prompt for SVG-to-Compose help could enhance AI assistants' effectiveness when users ask questions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/resources/llms-full.txt.template` around lines 302 - 319, Add a new "AI assistance" subsection to the LLM-targeted template that provides a few concise prompt patterns and example prompts for common tasks (e.g., "convert this SVG to Compose with optimized paths", "generate a multi-layer icon with tintable colors", "reduce path commands while preserving shape") so AI agents can produce consistent, high-quality outputs; include guidance on expected inputs (SVG, desired target platform/size, color/tint rules) and example completions to illustrate desired responses — update the llms-full.txt.template content near the "Links" section by inserting this new subsection headed "AI assistance" with 3–5 prompt patterns and 2 short example prompts + expected outputs.website/site/build.gradle.kts (1)
174-177: PascalCase to kebab-case conversion handles only single transitions.The regex
([a-z])([A-Z])handlesGradlePlugin→gradle-pluginbut won't correctly handle consecutive uppercase likeCLITool(would becomec-l-i-toolinstead ofcli-tool).Given the current page naming conventions in the codebase (Faq, Alternatives, Cli, GradlePlugin), this should work correctly. Just noting for awareness if new pages with consecutive uppercase are added.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/build.gradle.kts` around lines 174 - 177, The current segment.replace using Regex("([a-z])([A-Z])") only handles single lower→upper transitions and splits acronyms incorrectly; update the transformation to first insert a hyphen between an uppercase-acronym and a following CapitalizedWord (e.g. use Regex("([A-Z]+)([A-Z][a-z])") -> "$1-$2"), then run the existing lower→upper replacement (e.g. Regex("([a-z0-9])([A-Z])") -> "$1-$2"), and finally .lowercase() so names like "CLITool" become "cli-tool" while preserving existing conversions (modify the segment.replace pipeline accordingly).website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt (1)
69-75: Include the docs landing page in the shared entries.
DocsLinks()inwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.ktalready has to special-case/docs, while this dropdown renders onlydocLinkEntries. Moving the overview page into the shared list would keep both navs aligned and make the docs index reachable from the desktop Docs menu too.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt` around lines 69 - 75, Add the docs landing page (path "/docs", label "Overview" or "Docs") to the shared docLinkEntries list so the desktop Docs dropdown and the footer's DocsLinks() stay in sync; update the list defined as docLinkEntries (which contains DocLinkEntry instances) to include a DocLinkEntry for "/docs" so the docs index becomes reachable from the desktop Docs menu as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/actions/configure-bot/action.yml:
- Around line 2-4: The action description says "Generates a GitHub App token"
but the implementation only configures Git identity; either update the
description string under the description key to reflect that it only configures
Git with the bot identity, or implement actual token creation (e.g., add a step
using tibdex/github-app-token or actions/create-github-app-token and expose the
token to git) so the behavior matches the description; check references to
GITHUB_TOKEN and the update-last-modified job to ensure they use the correct
token (GITHUB_TOKEN if relying on default checkout vs the newly created app
token) and adjust steps accordingly.
In @.github/workflows/deploy-website.yml:
- Around line 101-117: The job update-last-modified references
needs.export.outputs.last-modified but only lists needs: deploy; update the
needs array for the update-last-modified job to include the export job (e.g.
needs: [deploy, export]) so that the output needs.export.outputs.last-modified
is defined and available to the job.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt`:
- Around line 211-255: Add an "Escape" branch to the existing onKeyDown handler
(the attrsModifier onKeyDown in DocNavDropdown.kt) that calls
event.preventDefault() and event.stopPropagation(), closes the menu (invoke the
component's close routine such as closeDropdown()/onClose()/setOpen(false) —
whichever is used in this component) and returns focus to the dropdown trigger
(use the existing trigger ref if present, or add/store a triggerRef and call
triggerRef?.focus() or querySelector the trigger element with its ARIA
selector). Ensure this new case is placed alongside the
"ArrowDown"/"ArrowUp"/"Home"/"End" branches and targets menu items with
role='menuitem'.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.kt`:
- Around line 103-106: The separator dot (::before) on each DocsLinks item shows
up on wrapped lines; update Links.kt so the Row that uses
.flexWrap(FlexWrap.Wrap) also applies a distinguishing CSS class/modifier (e.g.,
"wrap" or "no-separators") and then change the DocsLinks item styling so the
::before pseudo-element is suppressed when its parent has that class (i.e., hide
::before under the wrapping container), relying on the existing .gap for spacing
instead; locate the Row usage and the DocsLinks component to add the container
class and adjust the selector that generates the separator.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.kt`:
- Around line 33-36: The Image call in the LogoAndDescription composable uses a
redundant alt text; make the logo decorative by setting the alt to an empty
string (alt = "") in the Image(...) invocation (the same call that currently
sets src = "/images/s2c-icon.svg" and modifier = Modifier.size(1.25.cssRem)) so
the adjacent visible product label remains the accessible name.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.kt`:
- Around line 83-98: The section ID in WhatIsS2cSection should match the FAQ
question slug: change DocSection(id = "what-is-s2c", ...) to id =
"what-is-svg-to-compose" inside the WhatIsS2cSection composable, and then update
the corresponding TocEntry/Toc list in Faq.kt (the entry associated with the
"What is SVG to Compose?" question in faqQuestions/TocEntry) to use the same
"what-is-svg-to-compose" fragment so the TOC and section IDs remain consistent
for SEO and URL fragments.
---
Nitpick comments:
In `@website/site/build.gradle.kts`:
- Around line 174-177: The current segment.replace using Regex("([a-z])([A-Z])")
only handles single lower→upper transitions and splits acronyms incorrectly;
update the transformation to first insert a hyphen between an uppercase-acronym
and a following CapitalizedWord (e.g. use Regex("([A-Z]+)([A-Z][a-z])") ->
"$1-$2"), then run the existing lower→upper replacement (e.g.
Regex("([a-z0-9])([A-Z])") -> "$1-$2"), and finally .lowercase() so names like
"CLITool" become "cli-tool" while preserving existing conversions (modify the
segment.replace pipeline accordingly).
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt`:
- Around line 69-75: Add the docs landing page (path "/docs", label "Overview"
or "Docs") to the shared docLinkEntries list so the desktop Docs dropdown and
the footer's DocsLinks() stay in sync; update the list defined as docLinkEntries
(which contains DocLinkEntry instances) to include a DocLinkEntry for "/docs" so
the docs index becomes reachable from the desktop Docs menu as well.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.kt`:
- Around line 236-290: The ComparisonTableRow contains repeated Td styling;
extract that common modifier chain into a helper composable (e.g., DataCell)
that accepts the cell text (String), palette (SitePalette), and an optional
textAlign: TextAlign = TextAlign.Center, build the Td with
DocsTableCellStyle.toModifier().textAlign(textAlign).color(palette.onSurface).toAttrs()
and emit Text(text) inside; then replace the five inline Td blocks in
ComparisonTableRow with DataCell(row.feature, palette, TextAlign.Start) and
DataCell(row.svgToCompose, palette), DataCell(row.manualCoding, palette),
DataCell(row.androidStudio, palette), DataCell(row.otherTools, palette).
In `@website/site/src/jsMain/resources/llms-full.txt.template`:
- Around line 302-319: Add a new "AI assistance" subsection to the LLM-targeted
template that provides a few concise prompt patterns and example prompts for
common tasks (e.g., "convert this SVG to Compose with optimized paths",
"generate a multi-layer icon with tintable colors", "reduce path commands while
preserving shape") so AI agents can produce consistent, high-quality outputs;
include guidance on expected inputs (SVG, desired target platform/size,
color/tint rules) and example completions to illustrate desired responses —
update the llms-full.txt.template content near the "Links" section by inserting
this new subsection headed "AI assistance" with 3–5 prompt patterns and 2 short
example prompts + expected outputs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1cfb096c-0288-4a7f-b40f-6fc8ef3937c3
📒 Files selected for processing (31)
.github/actions/configure-bot/action.yml.github/workflows/deploy-website.ymlapp.propertieswebsite/site/.gitignorewebsite/site/build.gradle.ktswebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/AppEntry.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/SeoHead.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/layouts/DocsLayout.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Attribution.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/HeroSection.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/NavHeader.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/CliDocsContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/DocsStyles.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/footer/Footer.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/footer/LinksAndAttributionStyle.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/Index.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Alternatives.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Cli.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Faq.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/GradlePlugin.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Index.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/theme/AppStyles.ktwebsite/site/src/jsMain/resources/llms-full.txt.templatewebsite/site/src/jsMain/resources/llms.txt.templatewebsite/site/src/jsMain/resources/public/sitemap.xml
💤 Files with no reviewable changes (1)
- website/site/src/jsMain/resources/public/sitemap.xml
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt
Show resolved
Hide resolved
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.kt
Outdated
Show resolved
Hide resolved
.../src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.kt
Show resolved
Hide resolved
| @Composable | ||
| private fun WhatIsS2cSection() { | ||
| DocSection(id = "what-is-s2c", title = "What is SVG to Compose?") { | ||
| Span(attrs = DocsBodyTextStyle.toAttrs()) { | ||
| Text( | ||
| "SVG to Compose is a Kotlin Multiplatform tool that converts SVG and Android XML " + | ||
| "Drawable files into Jetpack Compose ", | ||
| ) | ||
| InlineCode("ImageVector") | ||
| Text( | ||
| " code. It eliminates the manual, error-prone process of hand-writing " + | ||
| "ImageVector builder code. Available as a CLI tool, a Gradle plugin, and a library.", | ||
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Section ID mismatch with FAQ question title.
The section uses id = "what-is-s2c" but the corresponding question in faqQuestions is "What is SVG to Compose?". For better SEO consistency and URL fragment readability, consider using id = "what-is-svg-to-compose".
🔧 Suggested fix
`@Composable`
private fun WhatIsS2cSection() {
- DocSection(id = "what-is-s2c", title = "What is SVG to Compose?") {
+ DocSection(id = "what-is-svg-to-compose", title = "What is SVG to Compose?") {Also update the corresponding TocEntry in Faq.kt.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.kt`
around lines 83 - 98, The section ID in WhatIsS2cSection should match the FAQ
question slug: change DocSection(id = "what-is-s2c", ...) to id =
"what-is-svg-to-compose" inside the WhatIsS2cSection composable, and then update
the corresponding TocEntry/Toc list in Faq.kt (the entry associated with the
"What is SVG to Compose?" question in faqQuestions/TocEntry) to use the same
"what-is-svg-to-compose" fragment so the TOC and section IDs remain consistent
for SEO and URL fragments.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt (1)
146-155: Consider extracting the Regex to a constant for better performance.The
Regex("[\\u0000-\\u001F]")is instantiated on every call toescapeJsonString(). If this function is called frequently, compiling the regex repeatedly adds overhead.♻️ Proposed refactor
+private val CONTROL_CHAR_REGEX = Regex("[\\u0000-\\u001F]") + internal fun String.escapeJsonString(): String = replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t") .replace("\u000C", "\\f") .replace("\u0008", "\\b") - .replace(Regex("[\\u0000-\\u001F]")) { match -> + .replace(CONTROL_CHAR_REGEX) { match -> "\\u${match.value[0].code.toString(16).padStart(4, '0')}" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt` around lines 146 - 155, The Regex("[\\u0000-\\u001F]") used inside String.escapeJsonString() is allocated on every call causing unnecessary overhead; extract it as a private top-level constant (e.g., PRIVATE val CONTROL_CHAR_REGEX = Regex("[\\u0000-\\u001F]")) and then use that constant in escapeJsonString() to avoid recompiling the pattern each invocation while keeping the replacement logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/deploy-website.yml:
- Around line 107-125: The workflow currently checks out the triggering ref via
actions/checkout and later does git push origin HEAD:main, which can push an
arbitrary branch when run with workflow_dispatch; update the job to explicitly
checkout main (use the actions/checkout step to fetch and checkout the main
branch) before modifying app.properties and pushing, and replace pushing HEAD
with pushing the explicit main branch (or push the local main ref) to avoid
accidental branch push; also add a guard condition around the job (or the
LAST_MODIFIED commit step) to skip execution for preview refs or non-production
triggers by checking the triggering ref or an input (e.g., ensure
workflow_dispatch input or GITHUB_REF equals refs/heads/main) so the metadata
update runs only for production builds.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt`:
- Around line 219-220: DocNavDropdown is trying to reassign the immutable
parameter isOpen inside the onKeyDown handler; instead, add an onClose: () ->
Unit lambda in DocNavDropdown and pass it into DropdownPanel, then call
handleMenuKeyDown(event, onClose) (or pass the lambda as the second arg) so the
key handler invokes the provided onClose callback to set isOpen = false in the
parent scope; update the DropdownPanel invocation and handleMenuKeyDown call
sites to accept and forward the onClose lambda accordingly.
---
Nitpick comments:
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt`:
- Around line 146-155: The Regex("[\\u0000-\\u001F]") used inside
String.escapeJsonString() is allocated on every call causing unnecessary
overhead; extract it as a private top-level constant (e.g., PRIVATE val
CONTROL_CHAR_REGEX = Regex("[\\u0000-\\u001F]")) and then use that constant in
escapeJsonString() to avoid recompiling the pattern each invocation while
keeping the replacement logic intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7a42bf64-7cfa-4a27-ae98-7088d43ae9a7
📒 Files selected for processing (13)
.github/actions/configure-bot/action.yml.github/workflows/deploy-website.ymlwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/CliDocsContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/Index.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Cli.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Faq.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/GradlePlugin.kt
✅ Files skipped from review due to trivial changes (4)
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.kt
- .github/actions/configure-bot/action.yml
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/CliDocsContent.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/GradlePlugin.kt
🚧 Files skipped from review as they are similar to previous changes (5)
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Faq.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/Index.kt
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt
Outdated
Show resolved
Hide resolved
- Fix configure-bot action description to match implementation - Add missing 'export' job dependency in deploy workflow - Add Escape key handler to dropdown menu panel for a11y - Extract menu keyboard navigation to reduce complexity - Replace fully qualified DOM references with imports - Make footer logo image decorative (empty alt) - Use SEO-friendly FAQ section ID 'what-is-svg-to-compose' - Fix formatting and import ordering across website modules
700d275 to
58db23d
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt (1)
238-264: Keyboard navigation properly addresses the Escape key feedback.The implementation correctly handles ArrowUp/ArrowDown (with wrapping), Home/End, and Escape with focus restoration to the trigger.
Minor observation: The
Home/Endhandlers rely on safe casts to handle emptyitems, whileArrowDown/ArrowUpdelegate tofocusAdjacentMenuItemwhich has an explicit early return. Both approaches are safe, but an early return at the top would be slightly more consistent and explicit.♻️ Optional: Add early return for consistency
private fun handleMenuKeyDown(event: KeyboardEvent, closeMenu: () -> Unit) { val container = event.currentTarget as? Element ?: return val items = container.querySelectorAll("[role='menuitem']") + if (items.length == 0) return when (event.key) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt` around lines 238 - 264, The handlers in handleMenuKeyDown rely on safe casts for Home/End but other branches use focusAdjacentMenuItem's early return; make behavior consistent by adding an explicit early return when items.length == 0 at the top of handleMenuKeyDown (right after obtaining items) so all key branches (ArrowUp/ArrowDown, Home, End, Escape) assume non-empty items; update references to items and existing casts in handleMenuKeyDown accordingly and keep focusAdjacentMenuItem unchanged..github/workflows/deploy-website.yml (1)
69-75: Prefer passingLAST_MODIFIEDinto Gradle instead of rewritingapp.propertieshere.
website/site/build.gradle.kts:13-15and135-140read this value during configuration, so this step currently works by mutating repo state and treating that as an implicit task input. Passing the date as a Gradle property/env var would makegenerateSitemapdeterministic and keep the repo write-back isolated to the later job.♻️ Suggested direction
- sed -i "s/^LAST_MODIFIED=.*/LAST_MODIFIED=$TODAY/" app.properties - cd website && ./gradlew :site:generateSitemap + cd website && ./gradlew :site:generateSitemap -PlastModified="$TODAY"val lastModified = providers.gradleProperty("lastModified") .orElse(providers.provider { appProperties["LAST_MODIFIED"]?.toString() ?: error("LAST_MODIFIED not set in app.properties") })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy-website.yml around lines 69 - 75, The workflow step currently mutates app.properties; instead, keep TODAY and the GITHUB_OUTPUT but stop rewriting the repo and pass the date into Gradle as a property so the generateSitemap task reads it deterministically; specifically, remove the sed rewrite of LAST_MODIFIED and invoke the site generate task with a Gradle project property matching the build script's providers.gradleProperty("lastModified") (so pass lastModified=$TODAY to ./gradlew :site:generateSitemap) so the site task (generateSitemap) uses the supplied value instead of modifying app.properties.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In @.github/workflows/deploy-website.yml:
- Around line 69-75: The workflow step currently mutates app.properties;
instead, keep TODAY and the GITHUB_OUTPUT but stop rewriting the repo and pass
the date into Gradle as a property so the generateSitemap task reads it
deterministically; specifically, remove the sed rewrite of LAST_MODIFIED and
invoke the site generate task with a Gradle project property matching the build
script's providers.gradleProperty("lastModified") (so pass lastModified=$TODAY
to ./gradlew :site:generateSitemap) so the site task (generateSitemap) uses the
supplied value instead of modifying app.properties.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt`:
- Around line 238-264: The handlers in handleMenuKeyDown rely on safe casts for
Home/End but other branches use focusAdjacentMenuItem's early return; make
behavior consistent by adding an explicit early return when items.length == 0 at
the top of handleMenuKeyDown (right after obtaining items) so all key branches
(ArrowUp/ArrowDown, Home, End, Escape) assume non-empty items; update references
to items and existing casts in handleMenuKeyDown accordingly and keep
focusAdjacentMenuItem unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a9a2231c-2131-4840-a5c9-adc21745a63f
📒 Files selected for processing (13)
.github/actions/configure-bot/action.yml.github/workflows/deploy-website.ymlwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/CliDocsContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/Index.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Cli.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Faq.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/GradlePlugin.kt
✅ Files skipped from review due to trivial changes (3)
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/LogoAndDescription.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/CliDocsContent.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/AlternativesContent.kt
🚧 Files skipped from review as they are similar to previous changes (7)
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Faq.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/GradlePlugin.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/docs/Cli.kt
- .github/actions/configure-bot/action.yml
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/pages/Index.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.kt
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/docs/FaqContent.kt
Replace CSS ::before pseudo-element separators with inline SpanText middle-dot separators. Pseudo-elements appear at the start of wrapped lines; inline text wraps naturally with the content.
The attrsModifier onKeyDown callback provides SyntheticKeyboardEvent, not raw KeyboardEvent. Use nativeEvent.currentTarget to access the DOM element.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt (1)
257-262: Consider storing a direct reference to the trigger element.The current approach of navigating via
container.parentElement?.querySelector("[role='button']")relies on the DOM structure remaining stable. While this works with the current implementation, storing a ref to the trigger element would be more robust against future refactoring.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt` around lines 257 - 262, The Escape handler uses container.parentElement?.querySelector("[role='button']") to find the trigger which is brittle; change the component to store a direct reference to the trigger element (e.g., a property like triggerButtonRef or triggerElement) when rendering the trigger button and update the Escape branch to call focus() on that ref instead of querying the DOM; ensure the ref is assigned/cleared alongside open/close logic (refer to container, closeMenu(), and the Escape handler) so focus restoration works even if DOM structure changes.website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt (1)
146-157: Add regression tests aroundescapeJsonString().This helper now protects every interpolated JSON-LD field, and
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/SeoHead.kt:116-148writes its output straight into the<script type="application/ld+json">node. A small test matrix for quotes, backslashes, newlines, and rawU+0000..U+001Fcharacters would make this much safer to evolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt` around lines 146 - 157, Add regression tests for String.escapeJsonString() (and its CONTROL_CHAR_REGEX behavior) that cover a matrix of inputs: double quotes, single quotes, backslashes, newlines (\n and \r), tabs, formfeed, backspace, and representative control characters from U+0000 to U+001F (including U+0000, U+0001, U+0008, U+000C, U+001F). For each case assert the escaped output matches the expected JSON-escaped form (e.g., backslash-escaped characters and \\uXXXX for control chars) and include a round-trip check that embedding the escaped value into a minimal JSON object string used by SeoHead (the JSON-LD script output path) can be parsed by a JSON parser without error. Ensure tests exercise multi-character strings with mixed cases (quotes + control chars) and fail if any control char is not escaped by CONTROL_CHAR_REGEX or escapeJsonString().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt`:
- Around line 45-47: The long JSON "description" line in StructuredData.kt
exceeds the 120-char limit; extract that description text into a top-level
constant (e.g., APP_DESCRIPTION or SITE_DESCRIPTION) and replace the inline
literal in the raw JSON string with the constant interpolation (using the same
escapeJsonString() where needed). Update the raw string in the StructuredData
component to reference the new constant so the line length is reduced and future
edits only require changing the single constant rather than the embedded JSON.
---
Nitpick comments:
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt`:
- Around line 146-157: Add regression tests for String.escapeJsonString() (and
its CONTROL_CHAR_REGEX behavior) that cover a matrix of inputs: double quotes,
single quotes, backslashes, newlines (\n and \r), tabs, formfeed, backspace, and
representative control characters from U+0000 to U+001F (including U+0000,
U+0001, U+0008, U+000C, U+001F). For each case assert the escaped output matches
the expected JSON-escaped form (e.g., backslash-escaped characters and \\uXXXX
for control chars) and include a round-trip check that embedding the escaped
value into a minimal JSON object string used by SeoHead (the JSON-LD script
output path) can be parsed by a JSON parser without error. Ensure tests exercise
multi-character strings with mixed cases (quotes + control chars) and fail if
any control char is not escaped by CONTROL_CHAR_REGEX or escapeJsonString().
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.kt`:
- Around line 257-262: The Escape handler uses
container.parentElement?.querySelector("[role='button']") to find the trigger
which is brittle; change the component to store a direct reference to the
trigger element (e.g., a property like triggerButtonRef or triggerElement) when
rendering the trigger button and update the Escape branch to call focus() on
that ref instead of querying the DOM; ensure the ref is assigned/cleared
alongside open/close logic (refer to container, closeMenu(), and the Escape
handler) so focus restoration works even if DOM structure changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 10a58fa8-0d51-4d7b-b121-2da6a75254f5
📒 Files selected for processing (4)
website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/DocNavDropdown.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/molecules/footer/Links.ktwebsite/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/footer/Footer.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/organisms/footer/Footer.kt
| | "softwareVersion": "${BuildConfig.VERSION.escapeJsonString()}", | ||
| | "url": "${BASE_URL.escapeJsonString()}", | ||
| | "description": "Convert SVG and Android Vector Drawables into Jetpack Compose ImageVector code. Available as a CLI tool, Gradle plugin, and Kotlin Multiplatform library.", |
There was a problem hiding this comment.
Break the long description out of this raw string.
Line 47 is over the repository's 120-character Kotlin limit. Extracting that copy to a constant also makes future blurb updates less painful.
♻️ Proposed change
+private const val SOFTWARE_APPLICATION_DESCRIPTION =
+ "Convert SVG and Android Vector Drawables into Jetpack Compose ImageVector code. " +
+ "Available as a CLI tool, Gradle plugin, and Kotlin Multiplatform library."
+
data object SoftwareApplicationStructuredData : StructuredDataType {
// language=json
override fun toJsonLd(): String = """
|{
| "@context": "https://schema.org",
@@
- | "description": "Convert SVG and Android Vector Drawables into Jetpack Compose ImageVector code. Available as a CLI tool, Gradle plugin, and Kotlin Multiplatform library.",
+ | "description": "${SOFTWARE_APPLICATION_DESCRIPTION.escapeJsonString()}",
| "downloadUrl": "https://github.com/rafaeltonholo/svg-to-compose",
| "license": "https://opensource.org/licenses/MIT"
|}
""".trimMargin()
}As per coding guidelines, **/*.kt: Maximum line length: 120 characters.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@website/site/src/jsMain/kotlin/dev/tonholo/s2c/website/components/atoms/StructuredData.kt`
around lines 45 - 47, The long JSON "description" line in StructuredData.kt
exceeds the 120-char limit; extract that description text into a top-level
constant (e.g., APP_DESCRIPTION or SITE_DESCRIPTION) and replace the inline
literal in the raw JSON string with the constant interpolation (using the same
escapeJsonString() where needed). Update the raw string in the StructuredData
component to reference the new constant so the line length is reduced and future
edits only require changing the single constant rather than the embedded JSON.
Summary
llms.txtandllms-full.txtfor AI crawler discovery, AI-friendlyrobots.txtdirectivesconfigure-botGitHub App action and post-deployLAST_MODIFIEDcommit workflowChanges
SEO:
sameAsGitHub link to WebSite schema; harden JSON-LD control char escapingsitemap.xmlfrom@Pagefiles with per-page priority/changefreqlastmodfromapp.properties(configuration cache compatible)GEO:
llms.txtandllms-full.txttemplates with build-time version substitutionrel="llms"discovery link tags to HTML headAccessibility:
aria-labelto dropdown menu panelModifier.role()instead ofattr("role", ...)per project convention<nav>landmark witharia-labelon footer docs linksFooter:
docLinkEntriesused by both nav dropdown and footer)::beforemiddle-dot separators between doc linksCI:
configure-botcomposite action forsvg-to-compose-bot[bot]Git identityupdate-last-modifiedjob that commitsLAST_MODIFIEDafter successful deploySummary by CodeRabbit
New Features
Improvements
Documentation
Chores