Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/www/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
User-agent: *
Allow: /

Disallow: /api/

Sitemap: https://ui.shipbase.xyz/sitemap-index.xml
Comment on lines +1 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify duplicate robots.txt sources and compare their content.
fd 'robots\.txt(\.ts)?$' apps/www
echo "----- apps/www/public/robots.txt -----"
cat apps/www/public/robots.txt 2>/dev/null || echo "[File not found]"
echo "----- apps/www/src/pages/robots.txt.ts -----"
cat apps/www/src/pages/robots.txt.ts 2>/dev/null || echo "[File not found]"

Repository: shipbase/ui

Length of output: 580


Consolidate robots.txt to one source of truth.

Both apps/www/public/robots.txt and apps/www/src/pages/robots.txt.ts exist. In Astro, the static file takes precedence, masking the dynamic route. The static file includes Disallow: /api/ and a hardcoded sitemap URL, while the route lacks the API disallow directive and uses environment-aware sitemap generation.

To resolve: add Disallow: /api/ to apps/www/src/pages/robots.txt.ts, then remove the static apps/www/public/robots.txt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/public/robots.txt` around lines 1 - 6, There are two conflicting
robots.txt sources: the static file and the dynamic robots.txt route; update the
dynamic route (robots.txt.ts) to include the missing "Disallow: /api/" directive
and ensure its sitemap generation uses the environment-aware URL logic currently
in the route, then delete the static public robots.txt so the dynamic route
becomes the single source of truth.

2 changes: 1 addition & 1 deletion apps/www/src/components/landing/components-overview.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ExampleCards from "../example-cards/index.astro"

<section class="relative overflow-hidden">
<div class="p-6 xs:p-8 flex flex-col gap-4">
<h3 class="text-3xl font-bold">Everything you need to build</h3>
<h2 class="text-3xl font-bold">Everything you need to build</h2>
<p class="text-muted-foreground leading-relaxed">
A comprehensive component library with 30+ production-ready components,
rich examples, and ready-to-use blocks. Built on Ark UI primitives with
Expand Down
16 changes: 8 additions & 8 deletions apps/www/src/components/landing/cta.astro
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ import Vue from "@/assets/logos/vue.svg"
</p>
<div class="flex flex-wrap justify-center gap-3 sm:gap-6">
<div class="flex items-center gap-2">
<TailwindCSS width="24" height="24" />
<TailwindCSS width="24" height="24" aria-hidden="true" />
<span class="text-sm">Tailwind CSS</span>
</div>
<div class="flex items-center gap-2">
<ArkUI width="24" height="24" />
<ArkUI width="24" height="24" aria-hidden="true" />
<span class="text-sm">Ark UI</span>
</div>
<div class="flex items-center gap-2">
<TypeScript width="24" height="24" />
<TypeScript width="24" height="24" aria-hidden="true" />
<span class="text-sm">TypeScript</span>
</div>
<div class="flex items-center gap-2">
<CVA width="24" height="24" />
<CVA width="24" height="24" aria-hidden="true" />
<span class="text-sm">CVA</span>
</div>
</div>
Expand All @@ -85,19 +85,19 @@ import Vue from "@/assets/logos/vue.svg"
</p>
<div class="flex flex-wrap justify-center gap-6 sm:gap-12 md:gap-16">
<div class="flex flex-col items-center gap-2">
<React width="32" height="32" />
<React width="32" height="32" aria-hidden="true" />
<span class="text-sm">React</span>
</div>
<div class="flex flex-col items-center gap-2">
<Vue width="32" height="32" />
<Vue width="32" height="32" aria-hidden="true" />
<span class="text-sm">Vue</span>
</div>
<div class="flex flex-col items-center gap-2 opacity-50">
<Svelte width="32" height="32" />
<Svelte width="32" height="32" aria-hidden="true" />
<span class="text-sm text-muted-foreground">Soon</span>
</div>
<div class="flex flex-col items-center gap-2 opacity-50">
<Solid width="32" height="32" />
<Solid width="32" height="32" aria-hidden="true" />
<span class="text-sm text-muted-foreground">Soon</span>
</div>
</div>
Expand Down
12 changes: 6 additions & 6 deletions apps/www/src/components/landing/features.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { cn } from "@/lib/utils"
<section class="relative overflow-hidden">
<!-- Featured item: Meta-Framework Support -->
<div class="p-6 xs:p-8 flex flex-col gap-2 border-b border-border/50 hover:bg-card/70">
<h3 class="text-2xl font-bold">Framework Agnostic</h3>
<h2 class="text-2xl font-bold">Framework Agnostic</h2>
<p class="text-muted-foreground leading-tight">
Works with any React, Vue, Solid, Svelte stack. Whether you're using
Next.js, Nuxt, Astro, or SvelteKit. our components integrate seamlessly
Expand Down Expand Up @@ -51,7 +51,7 @@ import { cn } from "@/lib/utils"
<div
class="p-6 xs:p-8 flex flex-col gap-2 border-r hover:bg-card/70 transition-colors"
>
<h3 class="text-xl font-bold">Composable Design</h3>
<h2 class="text-xl font-bold">Composable Design</h2>
<p class="text-muted-foreground leading-tight">
Every component uses a common, composable interface. Build complex UIs
effortlessly with modular components that work seamlessly together.
Expand All @@ -62,7 +62,7 @@ import { cn } from "@/lib/utils"
<div
class="p-6 xs:p-8 flex flex-col gap-2 hover:bg-card/70 transition-colors"
>
<h3 class="text-xl font-bold">Fully Accessible</h3>
<h2 class="text-xl font-bold">Fully Accessible</h2>
<p class="text-muted-foreground leading-tight">
Screen reader tested. Keyboard navigation perfected. Built-in focus
management. Accessibility isn't an afterthought—it's the foundation.
Expand All @@ -76,7 +76,7 @@ import { cn } from "@/lib/utils"
<div
class="p-6 xs:p-8 flex flex-col gap-2 border-r hover:bg-card/70 transition-colors"
>
<h3 class="text-xl font-bold">Beautiful Defaults</h3>
<h2 class="text-xl font-bold">Beautiful Defaults</h2>
<p class="text-muted-foreground leading-tight">
Thoughtfully crafted default styles that look professional from day one.
Beautiful foundations, endless customization possibilities.
Expand All @@ -87,7 +87,7 @@ import { cn } from "@/lib/utils"
<div
class="p-6 xs:p-8 flex flex-col gap-2 border-r hover:bg-card/70 transition-colors"
>
<h3 class="text-xl font-bold">Code Ownership</h3>
<h2 class="text-xl font-bold">Code Ownership</h2>
<p class="text-muted-foreground leading-tight">
No vendor lock-in. No hidden dependencies. Install via CLI or
copy-paste. All components live in your repo — you own your codebase.
Expand All @@ -102,7 +102,7 @@ import { cn } from "@/lib/utils"
"before:absolute before:inset-0 before:bg-gradient-to-br before:from-transparent before:to-primary/5 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300"
)}
>
<h3 class="text-xl font-bold">Open Source</h3>
<h2 class="text-xl font-bold">Open Source</h2>
<p class="text-muted-foreground leading-tight">
MIT licensed. Transparent development. Community contributions welcome.
Built by developers, for developers, with developers.
Expand Down
4 changes: 2 additions & 2 deletions apps/www/src/components/landing/shadcn.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ShadcnXTitle from "@/components/landing/shadcn-x-title.astro"

<section class="grid lg:grid-cols-2 items-center">
<div class="h-full p-6 xs:p-8 flex flex-col gap-2">
<h3 class="text-xl font-bold flex items-center gap-3">
<h2 class="text-xl font-bold flex items-center gap-3">
Inspired by
<span class="inline-flex items-center gap-1">
<ShadcnUIDark
Expand All @@ -23,7 +23,7 @@ import ShadcnXTitle from "@/components/landing/shadcn-x-title.astro"
/>
shadcn/ui
</span>
</h3>
</h2>
<p class="text-muted-foreground leading-tight">
shadcn/ui is a game changer that revolutionized the way to build component
libraries through Composition, Distribution system, and AI-Ready.
Expand Down
12 changes: 12 additions & 0 deletions apps/www/src/components/structured-data.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
export interface Props {
data: Record<string, unknown> | Record<string, unknown>[]
}

const { data } = Astro.props
const items = Array.isArray(data) ? data : [data]
---

{items.map((item) => (
<script type="application/ld+json" set:html={JSON.stringify(item)} />
Comment on lines +10 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the target file
find . -name "structured-data.astro" -type f 2>/dev/null

Repository: shipbase/ui

Length of output: 102


🏁 Script executed:

# Check file structure and content if it exists
cat -n apps/www/src/components/structured-data.astro 2>/dev/null || echo "File not found in expected location"

Repository: shipbase/ui

Length of output: 413


🏁 Script executed:

# Search for similar JSON-LD or set:html usage patterns in the codebase
rg "set:html.*JSON|JSON.*set:html" --type astro -A 2 -B 2 2>/dev/null || echo "No similar patterns found"

Repository: shipbase/ui

Length of output: 80


🌐 Web query:

Astro set:html directive HTML escaping security XSS prevention

💡 Result:

  • In Astro, normal template expressions like <h1>{value}</h1> HTML-escape by default, which is part of Astro’s “automatic XSS protection” work. If value contains <strong>, it renders as text (&lt;strong&gt;). [2]
  • set:html={value} is the explicit “dangerous” escape hatch: it injects the string as raw HTML (like element.innerHTML) and Astro does not escape it. Only use it with content you fully trust or have sanitized yourself, or you risk XSS. [1][2]

Practical XSS-prevention guidance

  • Treat set:html exactly like innerHTML: never pass untrusted/user-controlled strings unless you sanitize them first (e.g., with DOMPurify). OWASP explicitly advises avoiding innerHTML with untrusted data, and to sanitize if you must render user-authored HTML. [5]
  • Prefer safe alternatives when you can:
    • render as text: {value} or set:text={value} (Astro escapes automatically). [1]
  • Add guardrails:
    • Use an ESLint rule that disallows set:html to reduce accidental XSS risk (and selectively disable it only where you’ve proven sanitization). [3]
    • Consider enabling Astro’s experimental CSP support for defense-in-depth against script injection. [4]

Key takeaway: set:html prevents escaping (by design), so XSS prevention is on you: only trusted/sanitized HTML + defense-in-depth. [1][5]


Escape JSON-LD output before set:html to prevent script-breakout injection.

The component accepts untrusted data via the data prop and directly stringifies it. JSON.stringify(item) does not escape </script> sequences—if data contains </script> as a string value, it will terminate the script tag early and enable script injection. Since Astro's set:html is a no-escape directive (by design), sanitization is your responsibility.

🔒 Suggested hardening patch
 const { data } = Astro.props
 const items = Array.isArray(data) ? data : [data]
+const toSafeJsonLd = (value: Record<string, unknown>) =>
+  JSON.stringify(value)
+    .replace(/</g, "\\u003c")
+    .replace(/>/g, "\\u003e")
+    .replace(/&/g, "\\u0026")
+    .replace(/\u2028/g, "\\u2028")
+    .replace(/\u2029/g, "\\u2029")
 ---
 
 {items.map((item) => (
-  <script type="application/ld+json" set:html={JSON.stringify(item)} />
+  <script type="application/ld+json" set:html={toSafeJsonLd(item)} />
 ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/components/structured-data.astro` around lines 10 - 11, The
JSON-LD is being injected with set:html using JSON.stringify(item) which can be
broken by raw "</script>" or special line separator characters; before passing
to set:html (in the items.map render) produce a sanitized string from
JSON.stringify(item) that replaces any "</script>" occurrences with "<\/script>"
and also escapes U+2028 and U+2029 (e.g., replace those codepoints with their
escaped forms) so the script tag cannot be terminated early; update the mapping
that currently calls JSON.stringify(item) to use this sanitized string instead.

))}
23 changes: 21 additions & 2 deletions apps/www/src/layouts/root-layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ClientRouter } from "astro:transitions"

import SiteFooter from "@/components/side-footer.astro"
import SiteHeader from "@/components/site-header.astro"
import StructuredData from "@/components/structured-data.astro"
import TailwindIndicator from "@/components/tailwind-indicator.astro"
import { META_THEME_COLORS, siteConfig } from "@/config/site"

Expand All @@ -14,6 +15,7 @@ export interface Props {
metadata: {
title: string
description: string
robotsMeta?: string
openGraph: {
type: "website" | "article" | "book" | "profile" | (string & {})
title: string
Expand All @@ -31,10 +33,24 @@ export interface Props {
}
}
}
structuredData?: Record<string, unknown> | Record<string, unknown>[]
}

const { metadata } = Astro.props as Props
const { metadata, structuredData } = Astro.props as Props
const url = Astro.url
const robotsMeta = metadata.robotsMeta ?? "index,follow"

const organizationSchema = {
"@context": "https://schema.org",
"@type": "Organization",
name: "shipbase",
url: siteConfig.url,
logo: `${siteConfig.url}/logo.png`,
sameAs: [
siteConfig.links.twitter,
siteConfig.links.github,
],
}
---

<!doctype html>
Expand All @@ -52,7 +68,7 @@ const url = Astro.url
<meta name="author" content={siteConfig.author} />
<meta name="creator" content={siteConfig.creator} />
<meta name="theme-color" content={META_THEME_COLORS.light} />
<meta name="robots" content="index,follow" />
<meta name="robots" content={robotsMeta} />

<meta property="og:locale" content="en_US" />
<meta property="og:url" content={url.toString()} />
Expand All @@ -79,6 +95,9 @@ const url = Astro.url

<link rel="sitemap" href="/sitemap-index.xml" />

<StructuredData data={organizationSchema} />
{structuredData && <StructuredData data={structuredData} />}

<ClientRouter />

<script is:inline>
Expand Down
1 change: 1 addition & 0 deletions apps/www/src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const imageUrl = new URL(siteConfig.ogImage, url.origin).toString()
metadata={{
title: "Page Not Found",
description: siteConfig.description,
robotsMeta: "noindex,nofollow",
openGraph: {
type: "website",
title: "Page Not Found",
Expand Down
24 changes: 24 additions & 0 deletions apps/www/src/pages/docs/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ const imageURL = new URL(
---

<RootLayout
structuredData={{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: url.origin,
},
{
"@type": "ListItem",
position: 2,
name: "Docs",
item: `${url.origin}/docs`,
},
{
"@type": "ListItem",
position: 3,
name: entry.data.title,
item: url.toString(),
},
],
}}
metadata={{
title: entry.data.title,
description: entry.data.description,
Expand Down
32 changes: 31 additions & 1 deletion apps/www/src/pages/docs/components/[component].astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,41 @@ const imageURL = new URL(
---

<RootLayout
structuredData={{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: url.origin,
},
{
"@type": "ListItem",
position: 2,
name: "Docs",
item: `${url.origin}/docs`,
},
{
"@type": "ListItem",
position: 3,
name: "Components",
item: `${url.origin}/docs/components`,
},
{
"@type": "ListItem",
position: 4,
name: entry.data.title,
item: url.toString(),
},
],
}}
metadata={{
title: entry.data.title,
description: entry.data.description,
openGraph: {
type: "website",
type: "article",
title: entry.data.title,
description: entry.data.description,
image: { url: imageURL },
Expand Down