Skip to content
Merged
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
892 changes: 892 additions & 0 deletions SEO_AUDIT_PLAN.md

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion apps/docs/src/app/(docs)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,24 @@ export async function generateMetadata(
const page = source.getPage(params.slug);
if (!page) notFound();

const pageImage = getPageImage(page).url;

return {
title: page.data.title,
description: page.data.description,
alternates: {
canonical: page.url,
},
openGraph: {
images: getPageImage(page).url,
title: page.data.title,
description: page.data.description,
images: [pageImage],
},
twitter: {
card: "summary_large_image",
title: page.data.title,
description: page.data.description,
images: [pageImage],
},
};
}
42 changes: 41 additions & 1 deletion apps/docs/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RootProvider } from "fumadocs-ui/provider/next";
import type { Metadata } from "next";
import "./global.css";
import { COMPANY } from "@superset/shared/constants";
import { Inter } from "next/font/google";
import { NavigationBar } from "@/app/components/NavigationBar";
import { NavbarProvider } from "@/app/components/NavigationBar/components/NavigationMobile";
Expand All @@ -10,12 +11,51 @@ const inter = Inter({
});

export const metadata: Metadata = {
title: "Superset Docs",
metadataBase: new URL(COMPANY.DOCS_URL),
title: {
default: `${COMPANY.NAME} Documentation`,
template: `%s | ${COMPANY.NAME} Docs`,
},
description: `Official documentation for ${COMPANY.NAME} - the terminal for coding agents. Learn how to run parallel coding agents on your machine.`,
keywords: [
`${COMPANY.NAME} documentation`,
"coding agents docs",
"parallel execution guide",
"developer tools",
],
authors: [{ name: `${COMPANY.NAME} Team` }],
creator: COMPANY.NAME,
openGraph: {
type: "website",
locale: "en_US",
url: COMPANY.DOCS_URL,
siteName: `${COMPANY.NAME} Docs`,
title: `${COMPANY.NAME} Documentation`,
description: `Official documentation for ${COMPANY.NAME} - the terminal for coding agents.`,
},
twitter: {
card: "summary_large_image",
title: `${COMPANY.NAME} Documentation`,
description: `Official documentation for ${COMPANY.NAME} - the terminal for coding agents.`,
creator: "@superset_sh",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "32x32" },
{ url: "/favicon-192.png", sizes: "192x192", type: "image/png" },
],
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180" }],
},
};

Expand Down
7 changes: 6 additions & 1 deletion apps/docs/src/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);

return new Response(scanned.join("\n\n"));
return new Response(scanned.join("\n\n"), {
headers: {
"Content-Type": "text/plain",
"X-Robots-Tag": "noindex, nofollow",
},
});
}
1 change: 1 addition & 0 deletions apps/docs/src/app/llms.mdx/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function GET(
return new Response(await getLLMText(page), {
headers: {
"Content-Type": "text/markdown",
"X-Robots-Tag": "noindex, nofollow",
},
});
}
Expand Down
15 changes: 15 additions & 0 deletions apps/docs/src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { COMPANY } from "@superset/shared/constants";
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/", "/llms.mdx/", "/llms-full.txt"],
},
],
sitemap: `${COMPANY.DOCS_URL}/sitemap.xml`,
};
}
16 changes: 16 additions & 0 deletions apps/docs/src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { COMPANY } from "@superset/shared/constants";
import type { MetadataRoute } from "next";
import { source } from "@/lib/source";

export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = COMPANY.DOCS_URL;

const pages = source.getPages();

return pages.map((page) => ({
url: `${baseUrl}${page.url}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: page.url === "/quick-start" ? 1.0 : 0.8,
}));
}
16 changes: 16 additions & 0 deletions apps/marketing/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "Superset",
"short_name": "Superset",
"description": "Run 10+ parallel coding agents on your machine",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0a",
"theme_color": "#0a0a0a",
"icons": [
{
"src": "/favicon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
Comment on lines +9 to +15
Copy link
Copy Markdown
Contributor

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
# Step 1: Check if the manifest.json file exists and view its content
echo "=== Checking manifest.json ==="
if [ -f "apps/marketing/public/manifest.json" ]; then
  echo "✓ manifest.json exists"
  cat -n "apps/marketing/public/manifest.json"
else
  echo "✗ manifest.json NOT FOUND"
fi

echo -e "\n=== Checking referenced favicon-192.png ==="
if [ -f "apps/marketing/public/favicon-192.png" ]; then
  echo "✓ favicon-192.png exists"
  ls -lh "apps/marketing/public/favicon-192.png"
else
  echo "✗ favicon-192.png NOT FOUND"
fi

echo -e "\n=== Checking for favicon-512.png ==="
if [ -f "apps/marketing/public/favicon-512.png" ]; then
  echo "✓ favicon-512.png exists"
  ls -lh "apps/marketing/public/favicon-512.png"
else
  echo "✗ favicon-512.png NOT FOUND"
fi

echo -e "\n=== Searching for all icon/favicon files in marketing/public ==="
find "apps/marketing/public/" -maxdepth 1 \( -name "*favicon*" -o -name "*icon*" \) 2>/dev/null

Repository: superset-sh/superset

Length of output: 909


🏁 Script executed:

#!/bin/bash
echo "=== Contents of app-icons directory ==="
find "apps/marketing/public/app-icons" -type f 2>/dev/null | head -20

echo -e "\n=== Checking file sizes and types ==="
ls -lhR "apps/marketing/public/app-icons" 2>/dev/null

Repository: superset-sh/superset

Length of output: 1646


Add 512×512 icon for proper PWA installation on Android.

The manifest only includes a 192×192 icon. PWA best practices require a 512×512 icon for Android splash screens and optimal installation experience across devices. Without it, the PWA may show degraded visuals or fail installation checks on some Android devices.

Additionally, consider adding the "purpose" field (e.g., "any maskable") to support adaptive icons on modern devices.

📱 Recommended icon configuration
 	"icons": [
 		{
 			"src": "/favicon-192.png",
 			"sizes": "192x192",
-			"type": "image/png"
+			"type": "image/png",
+			"purpose": "any maskable"
+		},
+		{
+			"src": "/favicon-512.png",
+			"sizes": "512x512",
+			"type": "image/png",
+			"purpose": "any maskable"
 		}
 	]
🤖 Prompt for AI Agents
In `@apps/marketing/public/manifest.json` around lines 9 - 15, The manifest.json's
icons array only contains a 192x192 entry; add a 512x512 icon object to the
"icons" array (e.g., { "src": "/favicon-512.png", "sizes": "512x512", "type":
"image/png" }) and include the optional "purpose" field (e.g., "any maskable")
to support adaptive icons; ensure the referenced asset exists in public assets
and follow the same key names ("src", "sizes", "type", "purpose") so Android PWA
checks and splash screens use the 512×512 image.

}
21 changes: 20 additions & 1 deletion apps/marketing/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { COMPANY } from "@superset/shared/constants";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote/rsc";
import { ArticleJsonLd } from "@/components/JsonLd";
import { extractToc, getAllSlugs, getBlogPost } from "@/lib/blog";
import { mdxComponents } from "../components/mdx-components";
import { BlogPostLayout } from "./components/BlogPostLayout";
Expand All @@ -19,8 +21,18 @@ export default async function BlogPostPage({ params }: PageProps) {

const toc = extractToc(post.content);

const url = `${COMPANY.MARKETING_URL}/blog/${slug}`;

return (
<main>
<ArticleJsonLd
title={post.title}
description={post.description}
author={post.author}
publishedTime={new Date(post.date).toISOString()}
url={url}
image={post.image}
/>
<BlogPostLayout post={post} toc={toc}>
<MDXRemote source={post.content} components={mdxComponents} />
</BlogPostLayout>
Expand All @@ -42,13 +54,20 @@ export async function generateMetadata({
return {};
}

const url = `${COMPANY.MARKETING_URL}/blog/${slug}`;

return {
title: `${post.title} | Superset Blog`,
title: `${post.title} | ${COMPANY.NAME} Blog`,
description: post.description,
alternates: {
canonical: url,
},
openGraph: {
title: post.title,
description: post.description,
type: "article",
url,
siteName: COMPANY.NAME,
publishedTime: post.date,
authors: [post.author],
...(post.image && { images: [post.image] }),
Expand Down
20 changes: 20 additions & 0 deletions apps/marketing/src/app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ export const metadata: Metadata = {
title: "Blog | Superset",
description:
"News, updates, and insights from the Superset team about parallel coding agents and developer productivity.",
alternates: {
canonical: "/blog",
types: {
"application/rss+xml": "/feed.xml",
},
},
openGraph: {
title: "Blog | Superset",
description:
"News, updates, and insights from the Superset team about parallel coding agents and developer productivity.",
url: "/blog",
images: ["/opengraph-image"],
},
twitter: {
card: "summary_large_image",
title: "Blog | Superset",
description:
"News, updates, and insights from the Superset team about parallel coding agents and developer productivity.",
images: ["/opengraph-image"],
},
};

export default async function BlogPage() {
Expand Down
47 changes: 47 additions & 0 deletions apps/marketing/src/app/feed.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { COMPANY } from "@superset/shared/constants";
import { getBlogPosts } from "@/lib/blog";

export async function GET() {
const posts = getBlogPosts();
const baseUrl = COMPANY.MARKETING_URL;

const escapeXml = (str: string) =>
str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");

const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Superset Blog</title>
<link>${baseUrl}/blog</link>
<description>News, updates, and insights from the Superset team about parallel coding agents and developer productivity.</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="${baseUrl}/feed.xml" rel="self" type="application/rss+xml"/>
${posts
.map(
(post) => `
<item>
<title>${escapeXml(post.title)}</title>
<link>${baseUrl}/blog/${post.slug}</link>
<description>${escapeXml(post.description || "")}</description>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
<guid isPermaLink="true">${baseUrl}/blog/${post.slug}</guid>
<author>${escapeXml(post.author)}</author>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

RSS <author> element expects email format.

Per the RSS 2.0 specification, the <author> element should contain an email address (optionally with a name in parentheses), e.g., author@example.com (Author Name). Using just a name string may cause validation warnings in some feed validators.

Consider using <dc:creator> (with Dublin Core namespace) for plain author names, or omit the author element if email addresses aren't available.

Option: Use dc:creator instead
-<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">

Then in items:

-      <author>${escapeXml(post.author)}</author>
+      <dc:creator>${escapeXml(post.author)}</dc:creator>
🤖 Prompt for AI Agents
In `@apps/marketing/src/app/feed.xml/route.ts` at line 33, The RSS <author>
element is being populated with post.author (see escapeXml(post.author)) which
is a plain name; update the feed generation to either (A) emit a Dublin Core
creator element instead: replace <author>${escapeXml(post.author)}</author> with
<dc:creator>${escapeXml(post.author)}</dc:creator> and add the dc namespace
(xmlns:dc="http://purl.org/dc/elements/1.1/") to the feed root, or (B) if you
have an email field, format the <author> per RSS spec using post.authorEmail and
post.author like <author>${escapeXml(post.authorEmail)}
(${escapeXml(post.author)})</author>; adjust the code that builds the XML in
route.ts accordingly (references: escapeXml, post.author, post.authorEmail, feed
root).

</item>`,
)
.join("")}
</channel>
</rss>`;

return new Response(rss, {
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
}
73 changes: 69 additions & 4 deletions apps/marketing/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { COMPANY } from "@superset/shared/constants";
import type { Metadata } from "next";
import { IBM_Plex_Mono, Inter } from "next/font/google";
import Script from "next/script";

import { CookieConsent } from "@/components/CookieConsent";
import {
OrganizationJsonLd,
SoftwareApplicationJsonLd,
WebsiteJsonLd,
} from "@/components/JsonLd";

import { CTAButtons } from "./components/CTAButtons";
import { Footer } from "./components/Footer";
Expand All @@ -23,16 +28,73 @@ const inter = Inter({
variable: "--font-inter",
});

const siteDescription =
"Run 10+ parallel coding agents on your machine. Spin up new coding tasks while waiting for your current agent to finish. Quickly switch between tasks as they need your attention.";

export const metadata: Metadata = {
title: "Superset - Run 10+ parallel coding agents on your machine",
description:
"Run 10+ parallel coding agents on your machine. Spin up new coding tasks while waiting for your current agent to finish. Quickly switch between tasks as they need your attention.",
metadataBase: new URL(COMPANY.MARKETING_URL),
title: {
default: `${COMPANY.NAME} - Run 10+ parallel coding agents on your machine`,
template: `%s | ${COMPANY.NAME}`,
},
description: siteDescription,
keywords: [
"coding agents",
"parallel execution",
"developer tools",
"AI coding",
"git worktrees",
"code automation",
"Claude Code",
"Cursor",
"Codex",
],
authors: [{ name: `${COMPANY.NAME} Team` }],
creator: COMPANY.NAME,
openGraph: {
type: "website",
locale: "en_US",
url: COMPANY.MARKETING_URL,
siteName: COMPANY.NAME,
title: `${COMPANY.NAME} - Run 10+ parallel coding agents on your machine`,
description:
"Run 10+ parallel coding agents on your machine. Spin up new coding tasks while waiting for your current agent to finish.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: `${COMPANY.NAME} - The Terminal for Coding Agents`,
},
],
},
twitter: {
card: "summary_large_image",
title: `${COMPANY.NAME} - Run 10+ parallel coding agents on your machine`,
description:
"Run 10+ parallel coding agents on your machine. Spin up new coding tasks while waiting for your current agent to finish.",
images: ["/og-image.png"],
creator: "@superset_sh",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "32x32" },
{ url: "/favicon-192.png", sizes: "192x192", type: "image/png" },
],
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180" }],
},
manifest: "/manifest.json",
};

export default function RootLayout({
Expand All @@ -51,6 +113,9 @@ export default function RootLayout({
src="https://tally.so/widgets/embed.js"
strategy="afterInteractive"
/>
<OrganizationJsonLd />
<SoftwareApplicationJsonLd />
<WebsiteJsonLd />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</head>
<body className="overscroll-none font-sans">
<Providers>
Expand Down
Loading