feat(marketing): Complete marketing site with all pages, SEO, Remotion, and auth isolation#694
feat(marketing): Complete marketing site with all pages, SEO, Remotion, and auth isolation#6942witstudios merged 18 commits intomasterfrom
Conversation
- Configure shadcn/ui with 55+ components - Copy public assets (favicons, icons, manifest) from web app - Create .env.example with marketing site configuration - Fix lint errors in combobox component - Create tracking docs (PROJECT_MIRROR.md, PROGRESS.md, SESSION_LOG.md) - Create ARCHITECTURE.md documenting marketing site structure Issue #638 acceptance criteria met: - Next.js 15 App Router configured - Tailwind + shadcn/ui working - TypeScript strict mode enabled - Environment variables documented - Build passes with no errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create dynamic sitemap.ts with all planned marketing routes - Include 12 URLs: /, /pricing, /downloads, /tour, /integrations, /faq, /changelog, /docs/*, /blog - Update robots.txt for marketing site (disallow /screenshots/, /api/, /_next/) - Configure proper priorities and change frequencies Issue #642 acceptance criteria met: - sitemap.xml generated at build time - All public pages included - robots.txt allows crawling - Sitemap accessible at /sitemap.xml - No private/auth pages in sitemap Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create src/lib/metadata.ts with reusable SEO helpers - Add createMetadata() for per-page metadata generation - Define siteMetadata with full OG, Twitter, icons, manifest config - Create pageMetadata presets for all planned marketing pages - Update layout.tsx with comprehensive site-wide metadata - Add dynamic /og-image.png route for Open Graph images - Configure theme-color meta for light/dark modes Issue #644 acceptance criteria met: - Every page has unique title/description (via metadata presets) - Open Graph images for all pages (dynamic route) - Twitter cards working (configured in siteMetadata) - Canonical URLs set (via alternates.canonical) - No duplicate meta tags Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create src/lib/schema.tsx with comprehensive JSON-LD schemas - Add Organization schema (site-wide in layout.tsx) - Add WebSite schema with search action - Add WebApplication schema for landing page - Add Product schema with all 4 pricing tiers - Add SoftwareApplication schema for downloads - Add FAQPage schema helper (createFAQSchema) - Add Article schema helper (createArticleSchema) - Add BreadcrumbList schema helper - Add JsonLd component for rendering scripts Issue #643 acceptance criteria met: - Organization schema on all pages - WebApplication schema on landing - Product schema with offers - FAQPage schema ready for FAQ page - Article schema ready for blog posts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace screenshot generator with full marketing landing page - Create Hero section with core value proposition - Headline: "You, your team, and AI—working together" - AI-native badge - Clear subheadline - Get Started / View Pricing CTAs - Desktop/mobile app availability - Add full UI mockup showing PageSpace interface - Sidebar with navigation - Document editor view - AI Assistant panel - Add sticky navigation header - Add features preview section - Add minimal footer with navigation - Include WebApplication schema JSON-LD - Use proper metadata from lib/metadata.ts Issue #646 acceptance criteria met: - Clear value proposition in <5 seconds - CTAs prominent and accessible - Responsive across all breakpoints - Accessible design Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add comprehensive AI Architecture section explaining: - Global Assistant (personal, cross-workspace AI) - Page Agents (file tree, custom prompts, conversation history) - Nested Context (hierarchical awareness) - Team AI (multi-user collaboration) - Create visual file tree mockup showing: - Global Assistant at top level - Project-level AI agents - Document hierarchy with context flow - Add ICP example quote from founder perspective - Clearly differentiate from ChatGPT/Notion AI approach Issue #647 acceptance criteria met: - Clear explanation of Global vs Page agents - Visual shows context hierarchy - Differentiates from ChatGPT/Notion AI - ICP example resonates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Landing Page sections implemented: - Documents section with document editor mockup and AI suggestions - Channels + DMs section with @mention AI demonstration - Tasks section with AI assignee and rollup visualization - Calendar section with unified multi-source view - Final CTA section with trust signals and quick links - Enhanced footer with categorized navigation Remotion Video System setup: - Installed Remotion 4.x with Tailwind support - Created Root.tsx with DESIGN_TOKENS and theme system - Sample and Hero compositions with light/dark variants - Render scripts for MP4 output - Separate tsconfig.remotion.json for Remotion build - Full documentation in remotion/README.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Downloads page (/downloads): - Desktop downloads for macOS (Silicon/Intel), Windows, Linux (AppImage/deb) - Mobile apps section with iOS TestFlight and Android Beta links - System requirements for all platforms - Auto-update information and version display - Links to changelog Pricing page (/pricing): - 4 pricing tiers (Free, Pro, Founder, Business) with feature cards - Full feature comparison table with tooltips - Enterprise section with contact CTA - FAQ preview section Remotion partial work (deferred pending UI parity): - DocumentEditing and Channels compositions created but not complete 14 static pages now build successfully. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Complete implementation of 10 marketing site pages: Tour (/tour): - 5-step product tour workflow - AI Architecture overview section - Visual mockups with placeholders Integrations (/integrations): - MCP Servers showcase with 6 servers - Native integrations section - Developer section with API documentation Blog (/blog, /blog/[slug]): - Blog index with category filtering - 4 initial blog posts with full content - Newsletter signup CTA FAQ (/faq): - 20 FAQs across 6 categories - Expandable details/summary UI Documentation (/docs, /docs/getting-started): - Documentation hub with search - Getting started guide with 5 steps Changelog (/changelog): - Version history with 5 releases - Timeline UI with change type badges Also: - Updated sitemap.ts with all new routes - Updated tracking files (PROGRESS.md, SESSION_LOG.md, PROJECT_MIRROR.md) Build: 25 static pages, all passing Issues completed: #665, #666, #667, #668, #669, #670, #671, #672, #673, #674, #641 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updated GitHub Project with all completed issues: - Marked 14 additional issues as Done via gh CLI - Project status: 20/30 Done (67%) Remaining 10 issues: - 5 Remotion compositions (deferred - awaiting UI parity) - 5 requiring external resources (auth, Google, app stores) Extra pages implemented beyond original project scope: - /blog with 4 posts - /faq - /docs with getting-started guide - /changelog Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…p + add auth pages Overhaul the marketing site to pixel-match the actual PageSpace UI, add a branded oklch color system, extract SiteNavbar, build login/signup auth flow, and add the motion library for animations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s, clean up UI - Rebuild "AI Architecture" section as "Everything is a page" with real sidebar mock matching actual PageSpace UI (drive switcher, nav, search, page tree with nesting) - Add deeper nesting (channel with child pages) to show semantic parent/child - Rewrite feature cards to 3 focused cards: Everything is a Page, Context is Structure, AI at Every Level - Extract SiteFooter component (full + compact variants) used across all 12 pages - Fix logo everywhere: replace Sparkles icon with real PageSpace logo - Update SiteNavbar: add centered nav links, replace Home icon with logo, remove sidebar/nav buttons - Remove all section header badges and testimonial cards - Remove "Free tier / No credit card / Cancel anytime" from CTA section - Remove unused Globe import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Register Channels Remotion composition (light/dark variants) - Port privacy policy and terms of service pages from main app - Add contact page with isolated Resend email integration (no DB) - Delete main app's public /api/contact route (admin route kept) - Fix dead footer links: remove About, Careers, API Reference - Remove auth pages, redirect all auth links to main app - Add Google One Tap passive host component - Add Google Search Console verification placeholder - Add downloads "Coming Soon" state + deep link scaffolding - Clean up "Coming Soon" placeholder text - Replace app.pagespace.ai fallbacks with pagespace.ai Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a standalone marketing app (apps/marketing) with pages, PWA assets, extensive shadcn-style UI component library (50+ components), Remotion compositions + render tooling, contact API with per‑IP rate limiting and Resend email integration, Google One Tap, SEO/schema utilities, docs and project tracking. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant MarketingAPI as Marketing API
participant Resend as Resend Email Service
Browser->>MarketingAPI: POST /api/contact {name,email,subject,message}
MarketingAPI->>MarketingAPI: extract client IP & check in-memory rate limit (5/hr)
alt rate limit exceeded
MarketingAPI-->>Browser: 429 Retry-After
else validation fails
MarketingAPI-->>Browser: 400 { validation errors }
else success
MarketingAPI->>Resend: send email (FROM_EMAIL -> TO_EMAIL) with payload + client IP
Resend-->>MarketingAPI: 200 OK
MarketingAPI-->>Browser: 200 { success }
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db293591d5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| }, 10 * 60 * 1000); // Every 10 minutes | ||
|
|
||
| export async function POST(request: Request) { |
There was a problem hiding this comment.
Keep /api/contact available on the web app host
This commit relocates the contact handler into apps/marketing/src/app/api/contact/route.ts, but the web app still submits to /api/contact (for example in apps/web/src/components/shared/ContactForm.tsx), so requests made on the web app origin now lose their matching route and fail. In production this breaks the in-app contact flow unless every existing caller is migrated or a compatibility route is restored in apps/web.
Useful? React with 👍 / 👎.
apps/marketing/src/app/docs/page.tsx
Outdated
| title: "AI Features", | ||
| description: "Deep dive into AI capabilities including Page Agents and Global Assistant.", | ||
| icon: <Sparkles className="h-5 w-5" />, | ||
| href: "/docs/ai", |
There was a problem hiding this comment.
Point docs navigation only to implemented routes
The docs landing page links users to routes like /docs/ai, /docs/mcp, /docs/teams, and nested paths under them, but this commit only adds /docs and /docs/getting-started, so these links resolve to 404s. Because these links are rendered as primary navigation cards, users frequently hit dead ends unless the missing routes are added or the links are gated as coming soon.
Useful? React with 👍 / 👎.
apps/marketing/src/app/sitemap.ts
Outdated
| priority: 0.6, | ||
| }, | ||
| { | ||
| url: `${BASE_URL}/docs/api`, |
There was a problem hiding this comment.
Remove unimplemented docs URLs from sitemap
The sitemap advertises /docs/api (and similarly /docs/mcp) even though those pages are not present in this commit, which causes crawlers to repeatedly fetch 404 entries from a supposedly authoritative sitemap. This degrades SEO signal quality and should be corrected by listing only implemented routes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/package.json`:
- Around line 52-60: The package.json lists dependencies that are not imported
anywhere in the marketing app (react-hook-form, `@hookform/resolvers`, recharts,
react-day-picker, embla-carousel-react, react-resizable-panels, input-otp, cmdk,
vaul, sonner); either remove these entries, move them to devDependencies if they
are only needed for development or storybook, or add a clear inline comment in
package.json explaining they are intentionally present as placeholders for
upcoming shadcn/ui components; after making the change, update the lockfile
(npm/yarn/pnpm install) and run the app build/test to ensure no runtime or CI
breakage.
- Around line 18-60: Remove the individual `@radix-ui/react-`* dependencies and
rely only on the unified radix-ui package to avoid duplicate Radix internals and
version skew: delete all entries that match "@radix-ui/react-accordion",
"@radix-ui/react-avatar", "@radix-ui/react-checkbox", "@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu", "@radix-ui/react-label",
"@radix-ui/react-navigation-menu", "@radix-ui/react-scroll-area",
"@radix-ui/react-select", "@radix-ui/react-separator", "@radix-ui/react-slot",
"@radix-ui/react-tabs", and "@radix-ui/react-tooltip" from package.json and
ensure "radix-ui" remains in dependencies; after editing, run your package
manager install and test the app to confirm tree-shaken primitives resolve
correctly.
In `@apps/marketing/remotion/compositions/Channels.tsx`:
- Around line 67-78: The ai typing interpolation uses interpolate(...) without
an extrapolation config which is inconsistent and fragile; update the
interpolate call used to compute aiTypedChars (referencing aiText and
aiTypedChars) to include extrapolateRight: "clamp" (matching other
interpolations in this file) and then remove the external Math.min guard so
aiTypedChars can be set directly from the clamped interpolate result (still
bounded by aiText.length).
In `@apps/marketing/remotion/compositions/DocumentEditing.tsx`:
- Around line 21-44: The color values are HSL strings but the component appends
hex-style alpha suffixes (e.g., producing "hsl(...)40"), breaking CSS; update
the code that composes translucent colors (references: the colors object keys
background, foreground, muted, mutedForeground, border, primary,
primaryForeground and the ~10 places noted in the review) to stop
string-concatenating hex alpha. Replace those concatenations with a proper alpha
helper (e.g., applyAlpha(color, alpha) or convert to "hsl(... / alpha)" or use
color-mix) and use that helper wherever opacity was appended (the occurrences
called out in the review: around lines 141-142, 180-181, 208-209, 223-224,
266-267, 352-353, 383-384, 466-467) so HSL tokens render correctly with
transparency.
- Around line 200-286: The Sequence wrapper around the toolbar is redundant
because Sequence defaults to from={0}; remove the <Sequence from={0}> ...
</Sequence> wrapper (or if you intend to delay the toolbar later, change the
from value) so the toolbar div renders directly; look for the Sequence component
usage that wraps the toolbar block in DocumentEditing and either delete that
wrapper or update its from prop accordingly.
In `@apps/marketing/remotion/Root.tsx`:
- Around line 53-146: Replace the repeated hardcoded width/height on each
<Composition> with the landscape dimensions from DESIGN_TOKENS to keep a single
source of truth; locate the Composition declarations (e.g., ids "Sample",
"SampleDark", "Hero", "HeroDark", "DocumentEditing", "DocumentEditingDark",
"Channels", "ChannelsDark" using component symbols SampleComposition,
HeroComposition, DocumentEditingComposition, ChannelsComposition) and substitute
width={1920} height={1080} with values from DESIGN_TOKENS.dimensions.landscape
(e.g., reference DESIGN_TOKENS.dimensions.landscape.width and .height or use the
landscape object directly) so all compositions derive their size from the token.
In `@apps/marketing/remotion/styles.css`:
- Around line 10-55: The CSS variables in apps/marketing/remotion/styles.css
under .light and .dark duplicate the HSL values declared in DESIGN_TOKENS in
Root.tsx and will drift; either remove these dead variables or make them the
single source of truth by generating/consuming them from DESIGN_TOKENS. Update
the code by (a) if Tailwind utilities are the only consumer, add a clear comment
in Root.tsx and styles.css stating that inline styles come from DESIGN_TOKENS
and the CSS vars are only for Tailwind, or (b) better: derive the stylesheet
from DESIGN_TOKENS (or export DESIGN_TOKENS into a CSS generation step) and
remove the hardcoded --primary/--secondary/... definitions in .light and .dark
so values remain in sync with DESIGN_TOKENS used by Root.tsx.
In `@apps/marketing/scripts/render-videos.ts`:
- Around line 45-49: Progress reporting in the onProgress handler is broken
because it tests `progress % 10 === 0` on a fractional [0,1] value; change the
check to operate on percentage (e.g., compute Math.round(progress * 100) or
Math.floor(progress * 100) and test that percent % 10 === 0) or maintain a
lastReportedPercent to only write when the integer percent increases by 10;
update the onProgress callback (the onProgress function and its uses of the
progress variable and process.stdout.write) accordingly so that progress updates
are emitted at 10% intervals.
- Around line 69-72: The bundle call in render-videos.ts is overriding webpack
with a no-op (webpackOverride: (config) => config) so the Tailwind webpack
plugin declared in remotion.config.ts via Config.overrideWebpackConfig() isn't
applied; update the bundle invocation to import and call enableTailwind from
"@remotion/tailwind" and pass webpackOverride: (config) =>
enableTailwind(config) so Tailwind is processed (this will restore styles
referenced by className={theme} on AbsoluteFill and styles.css).
In `@apps/marketing/src/app/api/contact/route.ts`:
- Around line 46-61: In POST, the call to request.json() can throw for
malformed/non-JSON bodies and the outer catch currently yields a generic 500;
wrap the parsing step (the await request.json() inside the POST function) in its
own try/catch and when JSON parsing fails return a 400 Response.json with a
clear error like "Malformed JSON body" (include headers/status), otherwise
proceed as normal; reference the POST handler and the request.json() invocation
when making this change.
- Around line 14-44: The in-memory rate limiter (rateLimitMap,
RATE_LIMIT_WINDOW, RATE_LIMIT_MAX) and the module-level setInterval leak are
unsafe for serverless; replace or gate them: prefer calling the existing
Redis-backed rate limiter when available (use that implementation instead of
this checkRateLimit), and if you must keep the in-memory fallback, remove the
module-level timer leak by calling interval.unref() on the handle returned by
setInterval and add a clear comment documenting that this in-memory approach is
per-process and will reset on cold starts (i.e., not suitable for serverless
deployments); update checkRateLimit to delegate to Redis when a Redis client is
configured, and ensure the fallback path uses unref() and explicit comments
about the limitation.
In `@apps/marketing/src/app/blog/`[slug]/page.tsx:
- Around line 592-615: The current line-by-line renderer (the
post.content.split('\n').map(...) callback in page.tsx) produces bare <li>
elements, drops fenced code blocks by returning null for lines starting with
"```", and doesn't parse inline markdown; replace this custom parser with a real
markdown renderer (e.g., react-markdown or marked) to automatically produce
proper <ul>/<ol> wrappers for list items, preserve fenced code blocks and their
content, and render inline formatting like **bold**; specifically, remove the
map(...) block that handles headings/list items/code fences and instead render
post.content via the chosen markdown component (ensuring code blocks use a
syntax-highlighted component if desired and enabling list rendering and inline
HTML handling).
- Around line 21-462: The file contains a large hard-coded blogPosts Record
(symbol: blogPosts) which should be extracted into its own module; create a new
file that exports the blogPosts constant (and any related type BlogPost if
needed) e.g., export const blogPosts = {...}, then in page.tsx remove the
in-file blogPosts and import it (import { blogPosts } from '...') and ensure the
component or function that uses blogPosts still references the imported symbol;
update any type imports (BlogPost) and adjust imports/exports so tests/usage
remain unchanged.
- Around line 513-544: This page defines a custom header/nav block (the header
element rendering the PageSpace brand, Sparkles icon, links and Buttons) which
duplicates site navigation and causes inconsistency; replace that header with
the shared SiteNavbar component instead. Locate the header block in page.tsx
(the <header> rendering PageSpace, Sparkles, the nav links and Button usages
that reference APP_URL) and remove it, then import and render the existing
SiteNavbar component in its place so the blog route uses the same navigation
markup and props as other pages. Ensure any required props passed to SiteNavbar
(e.g., auth URLs or className) are forwarded or derived from APP_URL/parent
props to preserve login/signup behavior.
In `@apps/marketing/src/app/blog/page.tsx`:
- Around line 257-281: The Newsletter CTA currently renders a decorative input
and Button (the email input and the Button/ChevronRight) with no submission
logic; wrap the input and Button in a <form> with an onSubmit handler (e.g.,
handleSubscribe) that tracks the email value (use state tied to the input),
validates the email, calls your newsletter API or external signup endpoint
(e.g., subscribeNewsletter), and updates UI state for loading, success, and
error (disable the Button while loading and show a success or error message
below the form). Ensure you prevent default on submit, sanitize/trim the input,
handle API errors, and keep references to the existing Button and ChevronRight
components so the visual stays the same.
- Around line 107-138: The header block in blog/page.tsx duplicates the navbar
markup instead of using the shared SiteNavbar component; replace the inline
<header>…</header> block with a single <SiteNavbar /> usage (same as
contact/page.tsx) to centralize nav logic, ensure props/variants match existing
SiteNavbar API, and remove the copied JSX (including Link, Button, Sparkles
usage) so future nav changes are maintained in SiteNavbar only.
In `@apps/marketing/src/app/changelog/page.tsx`:
- Around line 282-292: The subscribe UI currently has no logic — wrap the input
and Button in a <form> and add a controlled email state plus a submit handler
(e.g., useState for email and a handleSubmit that calls preventDefault,
validates the email, sets loading state, POSTs to your mailing-list endpoint
like "/api/subscribe", and handles success/error states) so clicking the Button
triggers the request; reference the existing input element and the Button
component (and ArrowRight icon) in your update. Alternatively, if you don't want
a live endpoint yet, disable the input and Button and change the Button label to
"Coming Soon" and add aria-disabled/tooltip so users aren't misled. Ensure to
update the Button usage to reflect loading/disabled states and show minimal
feedback (success or error message) for the user.
- Around line 111-139: Change the parameter types for getChangeIcon and
getChangeLabel from string to the explicit union "feature" | "improvement" |
"fix" | "breaking" to enforce compile-time type safety; update the function
signatures for getChangeIcon(type: ...) and getChangeLabel(type: ...)
accordingly (and adjust any callers if they pass a broader string) so typos are
caught and the switch remains exhaustive for the known change kinds.
In `@apps/marketing/src/app/docs/getting-started/page.tsx`:
- Around line 75-106: Replace the duplicated header markup in
getting-started/page.tsx with the shared SiteNavbar component: remove the entire
<header>…</header> block and add an import for SiteNavbar (from the existing
marketing components) and render <SiteNavbar /> in its place; also remove any
now-unused imports referenced only by the old header (e.g., Sparkles, Button)
and ensure any props SiteNavbar requires (e.g., APP_URL or handlers) are passed
through if the original header used them.
- Around line 8-11: The metadata object is defined inline as export const
metadata which bypasses shared defaults; replace its usage with the
createMetadata helper (or the appropriate pageMetadata.* entry) from
"@/lib/metadata" so the page inherits site-wide defaults like OG image and site
name suffix — locate the export const metadata in this file and call
createMetadata({...}) (or return pageMetadata.gettingStarted /
pageMetadata.faq-style entry) passing the existing title and description so the
resulting metadata merges with the shared defaults.
In `@apps/marketing/src/app/downloads/page.tsx`:
- Around line 92-123: Replace the inlined header block in downloads/page.tsx
with the shared SiteNavbar component: remove the <header>...</header> markup and
import and render <SiteNavbar /> instead; if the page needs a different link set
or active state, pass the appropriate prop (e.g., links, activeItem, or variant)
to SiteNavbar or extend SiteNavbar to accept those props so Downloads,
Changelog, and Integrations can reuse the same component and stay consistent;
also remove any duplicate constants (like APP_URL usage inside the header) and
rely on SiteNavbar to handle auth links.
In `@apps/marketing/src/app/globals.css`:
- Around line 340-407: Remove the deprecated redundant property by deleting
"word-wrap: break-word" inside the .tiptap rules (the class block containing
overflow-wrap and related typographic styles) and replace the hardcoded
blockquote border color by changing "border-left: 3px solid `#ccc`" to use a theme
CSS variable (for example "border-left: 3px solid var(--border)" or
"var(--muted-foreground)") so the blockquote adapts to dark mode and theming.
In `@apps/marketing/src/app/integrations/page.tsx`:
- Around line 152-161: Several Link hrefs (e.g., the Link components pointing to
"/docs/integrations", "/docs/mcp", "/docs/api", etc., and the anchor using
APP_URL) reference non-existent routes and will produce 404s; update the
Link/anchor href values in this file (look for Link usages and the Button with
asChild anchor) to point to existing routes (/docs or /docs/getting-started) or
to the correct external documentation URLs, or add a feature-flag/guard that
conditionally renders those links only when the corresponding doc pages exist;
ensure you update every occurrence mentioned (the Link components around "View
Documentation", other Link usages for
integrations/mcp/api/sdk/webhooks/quickstart) so no Link points to a missing
/docs/* route.
In `@apps/marketing/src/app/og-image.png/route.tsx`:
- Around line 5-6: The GET handler returns an ImageResponse for a purely static
OG image but has no caching, causing unnecessary regenerations; update the GET
function that returns ImageResponse to include Cache-Control headers (e.g.
"public, max-age=31536000, immutable" or a suitable TTL) on the outgoing
response so crawlers and browsers can cache the image; adjust the return path
around ImageResponse (or wrap it in a Response/NextResponse with headers) to
ensure the Cache-Control header is applied.
In `@apps/marketing/src/app/page.tsx`:
- Around line 14-1493: The Home component is monolithic (~1,480 lines) and
should be split into smaller, maintainable components; extract each major
section (e.g., the Hero JSX block, Page Tree, Documents, Channels, Tasks,
Calendar, Final CTA) into dedicated components like HeroSection,
PageTreeSection, DocumentsSection, ChannelsSection, TasksSection,
CalendarSection, FinalCTA and import them into Home; move the corresponding JSX,
any local markup/state, and related assets (icons/props) into their own files
(named exports or default) and replace the large inline blocks inside export
default function Home() with these component calls to keep structure and props
intact (ensure to preserve usages like JsonLd/webApplicationSchema, SiteNavbar,
SiteFooter, and APP_URL references).
In `@apps/marketing/src/app/privacy/page.tsx`:
- Around line 5-11: Replace the inline duplicate metadata in the privacy page
with the shared constant: remove the createMetadata call in the privacy page and
import and use pageMetadata.privacy instead (replace the exported metadata
assignment in the privacy page). Locate the exported const metadata in
apps/marketing/src/app/privacy/page.tsx and swap its value to reference
pageMetadata.privacy (ensure you add an import for pageMetadata from
src/lib/metadata if not already present).
In `@apps/marketing/src/app/security/page.tsx`:
- Line 20: Replace the duplicated inline APP_URL constant (the APP_URL
declaration) by creating a single shared export (e.g., export const APP_URL =
process.env.NEXT_PUBLIC_APP_URL || "https://pagespace.ai") in a new central
constants module, then update all places that currently define APP_URL (pages
like security, blog, pricing, docs, faq, downloads, components such as
SiteNavbar, and lib files like schema.tsx) to import APP_URL from that shared
module; ensure the constant name matches existing references and update imports
accordingly so there is one canonical source of truth for APP_URL.
- Around line 32-75: Replace the duplicated header markup in security/page.tsx
with the shared SiteNavbar component: remove the inline <header> block and
render <SiteNavbar /> (or <SiteNavbar links={...} variant="liquid-glass-thin">
if page-specific links/styling are required). If SiteNavbar cannot currently
accept custom links or variants, add props (e.g., links: NavLink[] and variant?:
string) to the SiteNavbar component and update its implementation to honor them
so the security page can pass the `/downloads` nav and the
`backdrop-blur`/`liquid-glass-thin` style as needed; preserve the existing auth
link behavior that uses APP_URL for signin/signup when wiring the component into
security/page.tsx.
In `@apps/marketing/src/app/sitemap.ts`:
- Around line 5-6: The sitemap currently sets lastModified to a fresh timestamp
on every request (const lastModified = new Date()) inside sitemap(), which
undermines SEO; change it to use a stable build-time value or real content
modification dates instead: replace the runtime new Date() with a build-time
constant (e.g., process.env.BUILD_DATE or a generated constant injected at
build) or compute per-URL lastmod from your source content/metadata (for example
by reading file timestamps or a CMS lastUpdated field) and return those values
from sitemap() so the returned lastModified values are stable and accurate.
In `@apps/marketing/src/app/terms/page.tsx`:
- Around line 6-12: Replace the inline createMetadata usage in the exported
metadata constant with the shared pageMetadata.terms entry to avoid duplication;
specifically, remove the createMetadata(...) call in the metadata export and
instead assign metadata = pageMetadata.terms (referencing the existing symbol
pageMetadata and its property terms) so the page uses the centralized metadata
definition rather than duplicating values.
In `@apps/marketing/src/app/tour/page.tsx`:
- Around line 225-232: The Next button currently looks up the subsequent step
using tourSteps[step.number], which is brittle because step.number is 1-based
and may diverge from array indices; change the component to use the map() index
(e.g., the index passed into tourSteps.map) to compute the next step (use
tourSteps[index + 1] for title and href) or otherwise locate the next step by
array position instead of relying on step.number; update references to
step.number in the Next button block (the Button/asChild anchor generation) to
use the map index so the lookup is robust to reordering or renumbering.
In `@apps/marketing/src/components/ContactForm.tsx`:
- Around line 88-107: The code calls await res.json() unconditionally which can
throw on non-JSON responses (e.g., HTML 502) and produce an unfriendly error;
change the fetch handling in ContactForm.tsx so you first read the raw response
(e.g., await res.text()), then attempt to parse JSON (try JSON.parse or check
Content-Type) and fall back to a sensible message (res.statusText or the raw
text) if parsing fails, then use that fallback when throwing so setSubmitError
receives a user-friendly message; ensure setIsSubmitted and setFormData are only
run on successful (res.ok) responses and keep setIsSubmitting(false) in finally.
- Around line 25-108: The thank-you state in ContactForm leaves users stuck when
isSubmitted is true; add a way to return to the form by rendering a "Send
another" (or "Reset") control when isSubmitted is true that calls a handler to
setIsSubmitted(false) and reset form state via setFormData({ name: "", email:
"", subject: "", message: "" }) (or reuse existing reset logic from
handleSubmit), and ensure the handler also clears submitError and errors so the
form is ready to use again.
In `@apps/marketing/src/components/MockAppPreview.tsx`:
- Around line 299-349: PageTreeItem uses hardcoded Tailwind gray/blue classes
(e.g., bg-gray-200, dark:bg-gray-700, hover:bg-gray-200, hover:bg-gray-300,
text-gray-900, dark:text-gray-100, bg-blue-500, text-gray-500) which bypass the
design tokens; replace those hardcoded classes in the PageTreeItem JSX (the root
div, chevron/button hover states, title span, change dot and icon color) with
the theme token classes used by NavLink (e.g., bg-accent / hover:bg-accent /
text-accent-foreground / etc.) so the component responds to theme changes—update
the cn calls for the root container, the chevron/button hover classes, the title
span and the change-indicator span to use token names instead of gray/blue
classes.
In `@apps/marketing/src/components/ui/alert-dialog.tsx`:
- Around line 1-196: The file incorrectly imports primitives from the unified
"radix-ui" package; replace that import with the focused Radix alert-dialog
package and keep the rest of the code unchanged. Specifically, update the import
so AlertDialogPrimitive refers to the `@radix-ui/react-alert-dialog` namespace (so
ComponentProps<typeof AlertDialogPrimitive.Root>, Trigger, Portal, Overlay,
Content, Title, Description, Action, Cancel still resolve). This aligns the
import style with other UI files (e.g., dropdown-menu) and ensures
AlertDialogPrimitive and all components like AlertDialogTrigger,
AlertDialogContent, AlertDialogAction, AlertDialogCancel work correctly.
In `@apps/marketing/src/components/ui/button-group.tsx`:
- Around line 1-6: The file references React.ComponentProps in its type
annotations but never imports the React namespace; add an explicit namespace
import (e.g. import * as React from "react" or import React from "react") at the
top of the file so the React.ComponentProps type usages compile; update the top
imports near cva/VariantProps/Slot/cn/Separator to include that React import.
In `@apps/marketing/src/components/ui/carousel.tsx`:
- Around line 78-89: The keyboard handler handleKeyDown only checks for
"ArrowLeft"/"ArrowRight" and ignores the orientation prop, so vertical carousels
don't respond to keyboard navigation; update handleKeyDown to check the
component's orientation (the orientation variable/prop) and map
ArrowUp/ArrowDown to scrollPrev/scrollNext when orientation === "vertical"
(while keeping ArrowLeft/ArrowRight for horizontal), preserving
event.preventDefault() and using the existing scrollPrev and scrollNext
callbacks.
In `@apps/marketing/src/components/ui/checkbox.tsx`:
- Around line 3-5: The import style for Radix differs here: replace the scoped
package import of CheckboxPrimitive from "@radix-ui/react-checkbox" with the
unified barrel import used across the other UI components (the same module used
by toggle.tsx and progress.tsx) so that CheckboxPrimitive and CheckIcon are
imported consistently; update the import statement to pull CheckboxPrimitive
(and keep CheckIcon from lucide-react) from the project's shared radix barrel
module to match the other components.
In `@apps/marketing/src/components/ui/combobox.tsx`:
- Around line 288-290: The useComboboxAnchor hook is just a thin wrapper
returning useRef<HTMLDivElement | null>(null); add a short JSDoc above the
useComboboxAnchor declaration explaining its purpose and intended usage: that
callers should spread the returned ref onto the anchor element (as ref) and pass
the same ref object to the ComboboxContent component’s anchor prop so
positioning works correctly; mention the returned type (RefObject<HTMLDivElement
| null>) and an example usage note (attach to anchor element and to
ComboboxContent) so future readers know why the hook exists and how to wire it
up.
- Around line 227-242: The ComboboxChips component type declaration redundantly
intersects React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> with
ComboboxPrimitive.Chips.Props; update the props type to use only
ComboboxPrimitive.Chips.Props (matching other subcomponents) by removing the
React.ComponentPropsWithRef<...> intersection and leaving the parameter
signature as (props: ComboboxPrimitive.Chips.Props) (retain the rest of the
function, className handling, and spread of props).
In `@apps/marketing/src/components/ui/empty.tsx`:
- Around line 31-44: The icon variant in the emptyMediaVariants cva duplicates
base utilities (flex, shrink-0, items-center, justify-center); remove those
redundant classes from the "icon" variant string so it only contains
variant-specific utilities (e.g., bg-muted text-foreground rounded-lg
[&_svg:not([class*='size-'])]:size-6 size-10 if needed), keeping shared layout
in the base styles and preserving defaultVariants.variant = "default".
In `@apps/marketing/src/components/ui/field.tsx`:
- Around line 156-184: The FieldSeparator component's props type unnecessarily
intersects React.ComponentProps<"div"> with an explicit children?:
React.ReactNode; remove the redundant & { children?: React.ReactNode } and
simply type the props as React.ComponentProps<"div"> (keeping the function name
FieldSeparator and its usage of children intact) so the component relies on the
built-in children typing and avoids the duplicate declaration.
- Around line 128-139: FieldTitle currently reuses data-slot="field-label" (same
as FieldLabel) which causes both elements to be targeted by the same CSS; to
fix, decide whether FieldTitle should be styled independently—if so, change its
data-slot value from "field-label" to "field-title" in the FieldTitle component,
update any related selectors (e.g., fieldVariants) that need to target the new
slot, and add a short comment in FieldTitle noting why it uses "field-title"; if
identical styling is intentional, add a clarifying comment in FieldTitle
explaining that it intentionally shares data-slot="field-label" with FieldLabel.
- Line 207: The condition using loose equality should be changed to strict
equality: in the component where the variable uniqueErrors is checked (the if
statement referencing uniqueErrors?.length == 1), replace the loose comparison
with a strict one (use ===) so the expression becomes uniqueErrors?.length ===
1; update this in the relevant function/component in
apps/marketing/src/components/ui/field.tsx to follow project convention.
In `@apps/marketing/src/components/ui/form.tsx`:
- Around line 45-66: The guard in useFormField is unreachable because
FormFieldContext default is a truthy empty object and you call
useFormContext/useFormState before validating context; move the presence check
for FormFieldContext (or validate a required property such as fieldContext.name)
to the top of useFormField before calling useFormContext, useFormState, or
getFieldState so you throw early when not inside a <FormField> provider;
reference and adjust logic around useFormField, FormFieldContext,
FormItemContext, useFormContext, useFormState and getFieldState to ensure the
guard runs first.
In `@apps/marketing/src/components/ui/item.tsx`:
- Around line 156-180: ItemHeader and ItemFooter currently duplicate the same
class string; either document the semantic intent or extract the shared class
into a single source of truth: create a constant (e.g., BASE_SLOT_CLASS = "flex
basis-full items-center justify-between gap-2") and use it in both ItemHeader
and ItemFooter (or create a small shared component like ItemSlot that accepts
data-slot and props) so the styles are centralized; update usages to reference
BASE_SLOT_CLASS (or the new ItemSlot) and add a one-line comment if the
duplication is intentional for semantics.
In `@apps/marketing/src/components/ui/kbd.tsx`:
- Around line 18-26: KbdGroup's prop typing is wrong: it currently uses
React.ComponentProps<"div"> but renders a <kbd>, so update the component
signature to accept React.ComponentProps<"kbd"> (change the type on the KbdGroup
props) and keep forwarding {...props} and className as-is to ensure correct
DOM/ARIA typings and IDE completions; locate the KbdGroup function declaration
to make this change.
In `@apps/marketing/src/components/ui/navigation-menu.tsx`:
- Around line 1-6: This file is missing the "use client" directive required for
any module using client-side hooks (Radix NavigationMenu primitives); add a
top-of-file "use client" directive as the very first line so the
NavigationMenuPrimitive imports and any exported components (the NavigationMenu
wrapper and related components) run on the client, ensuring the module won't be
imported into a Server Component and cause runtime errors.
In `@apps/marketing/src/components/ui/scroll-area.tsx`:
- Around line 8-28: ScrollArea currently uses React.forwardRef while the rest of
the PR uses modern function components; convert ScrollArea into a plain function
component typed with React.ComponentProps<typeof ScrollAreaPrimitive.Root> and
accept props ({ className, children, ...props }) and an optional ref if needed
via a local ref passed to ScrollAreaPrimitive.Viewport (use ref forwarding only
if strictly required); update the export to a named const and set
ScrollArea.displayName to "ScrollArea" (or reuse
ScrollAreaPrimitive.Root.displayName if preferred). Also update the Tailwind
v3->v4 modifiers in the Viewport class string from "[&>div]:!block
[&>div]:!w-full" to "[&>div]:block! [&>div]:w-full!" so the important modifier
is at the end. Ensure references to ScrollAreaPrimitive.Viewport,
ScrollAreaPrimitive.Root, and ScrollBar are preserved.
In `@apps/marketing/src/components/ui/skeleton.tsx`:
- Around line 33-55: Remove the redundant animate-pulse class from the wrapper
container divs so only the inner Skeleton components drive the pulsing;
specifically, in the component that renders the message bubble (the div with
data-slot="skeleton-message-bubble") and the wrapper divs used in SkeletonCard
and SkeletonListItem, delete animate-pulse from the className concatenation and
leave the Skeleton base component (and its className) unchanged so children
continue to animate without parent/child opacity timer conflicts.
In `@apps/marketing/src/components/ui/tabs.tsx`:
- Line 4: Import from the unified package to match sibling components: replace
the direct import of TabsPrimitive from "@radix-ui/react-tabs" with the unified
module used elsewhere (e.g., import from "radix-ui") so TabsPrimitive in this
file aligns with other components like switch.tsx and toggle-group.tsx; update
any import statement referencing "@radix-ui/react-tabs" to use the unified
"radix-ui" module while keeping the same named import (TabsPrimitive) and
ensuring any exports/usage of TabsPrimitive remain unchanged.
In `@apps/marketing/src/hooks/use-mobile.ts`:
- Around line 1-19: The file currently named use-mobile.ts exports the hook
useIsMobile; rename the file to useIsMobile.ts so the filename matches the
exported hook (useIsMobile) in camelCase, and update any imports that reference
use-mobile to import from useIsMobile instead; ensure tests, index barrel files,
or component imports are updated accordingly to avoid unresolved module errors.
In `@apps/marketing/src/lib/metadata.ts`:
- Around line 151-154: The metadata object currently has an empty scaffolded
verification block; populate the verification.google value by reading a secure
environment variable (e.g., process.env.GOOGLE_SITE_VERIFICATION) and assign it
to the verification.google key inside the exported metadata object so the Google
Search Console token is injected at build/runtime; add a fallback (empty string
or undefined) to avoid breaking local dev and update any README or environment
sample to document the new GOOGLE_SITE_VERIFICATION variable.
In `@apps/marketing/src/lib/schema.tsx`:
- Around line 233-246: The websiteSchema exports a SearchAction with urlTemplate
`${SITE_URL}/search?q={search_term_string}` but there is no /search page,
causing a broken sitelinks search box; fix by either (A) implement a /search
route in the marketing app (create a page/component at
apps/marketing/src/app/search that reads the query param `q`, performs or
delegates search and returns results/redirects appropriately) or (B) remove the
SearchAction block (delete the potentialAction property or the entire
SearchAction object inside websiteSchema) so the schema no longer advertises a
non-existent search endpoint; update the symbol websiteSchema and its
urlTemplate/SearchAction accordingly.
- Around line 251-261: The JsonLd component currently injects
JSON.stringify(jsonLd) into dangerouslySetInnerHTML without escaping the closing
script sequence; fix JsonLd by generating the serialized string (from jsonLd)
then defensively escaping any occurrences of "</script>" (and optionally "<!--")
before assigning to dangerouslySetInnerHTML so the script tag cannot be
prematurely closed—e.g., take the string produced by JSON.stringify(jsonLd) and
replace all "</script>" with "<\/script>" (and likewise escape "<!--" if
desired) before using it in the returned <script> element.
- Around line 13-28: The organizationSchema currently includes a top-level
"@context" which should not be nested when used as the author of other schemas;
update the code by extracting a context-free version (e.g.,
organizationSchemaForAuthor or organizationSchemaNested) that removes the
"@context" key and reuse that for the author fields in webApplicationSchema and
softwareApplicationSchema while keeping the original organizationSchema
unchanged for top-level output; modify references in webApplicationSchema and
softwareApplicationSchema to use the new context-free symbol instead of
organizationSchema.
| "@base-ui/react": "^1.2.0", | ||
| "@hookform/resolvers": "^5.2.2", | ||
| "@pagespace/lib": "workspace:*", | ||
| "@radix-ui/react-accordion": "^1.2.12", | ||
| "@radix-ui/react-avatar": "^1.1.10", | ||
| "@radix-ui/react-checkbox": "^1.3.3", | ||
| "@radix-ui/react-dialog": "^1.1.15", | ||
| "@radix-ui/react-dropdown-menu": "^2.1.16", | ||
| "@radix-ui/react-label": "^2.1.7", | ||
| "@radix-ui/react-navigation-menu": "^1.2.14", | ||
| "@radix-ui/react-scroll-area": "^1.2.10", | ||
| "@radix-ui/react-select": "^2.2.6", | ||
| "@radix-ui/react-separator": "^1.1.7", | ||
| "@radix-ui/react-slot": "^1.2.3", | ||
| "@radix-ui/react-tabs": "^1.1.13", | ||
| "@radix-ui/react-tooltip": "^1.2.8", | ||
| "@remotion/bundler": "^4.0.421", | ||
| "@remotion/cli": "^4.0.421", | ||
| "@remotion/renderer": "^4.0.421", | ||
| "@remotion/tailwind": "^4.0.421", | ||
| "class-variance-authority": "^0.7.1", | ||
| "clsx": "^2.1.1", | ||
| "cmdk": "^1.1.1", | ||
| "date-fns": "^4.1.0", | ||
| "embla-carousel-react": "^8.6.0", | ||
| "input-otp": "^1.4.2", | ||
| "lucide-react": "^0.525.0", | ||
| "motion": "^12.23.22", | ||
| "next": "15.3.9", | ||
| "next-themes": "^0.4.6", | ||
| "radix-ui": "^1.4.3", | ||
| "react": "^19.1.2", | ||
| "react-day-picker": "^9.13.2", | ||
| "react-dom": "^19.1.2", | ||
| "tailwind-merge": "^3.3.1" | ||
| "react-hook-form": "^7.63.0", | ||
| "react-resizable-panels": "^4.6.2", | ||
| "recharts": "2.15.4", | ||
| "remotion": "^4.0.421", | ||
| "resend": "^6.1.2", | ||
| "sonner": "^2.0.7", | ||
| "tailwind-merge": "^3.3.1", | ||
| "vaul": "^1.1.2", | ||
| "zod": "^4.1.11" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
radix-ui unified package vs individual @radix-ui packages 2025
💡 Result:
What changed in 2025
Radix introduced a single, unified package named radix-ui on January 22, 2025. It “exposes the latest version of all Radix Primitives from a single place” and is explicitly described as a tree-shakable entrypoint, intended to make it easier to use multiple primitives without ending up with conflicting/duplicate dependencies across many @radix-ui/react-* packages. [1]
Unified radix-ui vs individual @radix-ui/react-* packages
Use radix-ui when you want…
- One dependency instead of many (cleaner
package.json). Shadcn’s “new-york” style switched to this approach, changing imports like@radix-ui/react-dialog→radix-ui. [2] - Fewer version-mismatch headaches across multiple Radix primitives (one package version to update/align), which is one of the motivations called out in Radix’s release notes. [1]
- Tree-shaking while still importing only what you use (assuming your bundler supports ESM tree-shaking). [1]
Use individual @radix-ui/react-* packages when you want…
- Explicit, per-component dependencies (e.g., only
@radix-ui/react-dialog). This is still a normal, supported install path on npm and commonly used by CLIs/docs. [3] - More granular version control (pin/upgrade one primitive independently—at the cost of potentially reintroducing multi-version duplication if different primitives pull different internal Radix versions). [1][3]
Practical recommendation (2025+)
- New apps / shadcn-style codebases using many primitives: prefer
radix-uito simplify dependency management and reduce duplicate/conflicting Radix internals. [1][2] - Tiny usage (1–2 primitives) or very strict per-primitive version pinning: individual
@radix-ui/react-*packages can still be fine, just watch for multi-version installs as your usage grows. [1][3]
Sources:
[1] Radix Primitives release notes (Jan 22, 2025) — radix-ui package + rationale
[2] shadcn/ui changelog (Feb 2026) — migration and import changes to radix-ui
[3] npm package page for @radix-ui/react-dialog — ongoing individual-package install path
🌐 Web query:
@radix-ui packages how to use correctly with radix-ui meta-package
💡 Result:
Recommended (single “meta” package): install radix-ui and import from "radix-ui"
Radix Primitives docs recommend this as the simplest approach and to avoid version conflicts/duplication (it’s tree-shakeable). [1]
npm i radix-uiimport { Dialog, DropdownMenu, Tooltip } from "radix-ui";[1]
Alternative (individual packages): install @radix-ui/react-* and import from each package
npm i `@radix-ui/react-dialog` `@radix-ui/react-dropdown-menu` `@radix-ui/react-tooltip`import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tooltip from "@radix-ui/react-tooltip";[1]
Don’t mix both approaches (unless you’re very deliberate)
If you install radix-ui and some @radix-ui/react-* packages, you can end up with duplicated Radix internals or version skew; if you do install separately, Radix recommends updating all Radix packages together to reduce duplication/bundle size issues. [1]
Notes
- Some component pages show installing
@radix-ui/react-…but importing from"radix-ui"in examples; the “rule of thumb” is: import from the package you installed (use the “Introduction” page patterns above as the canonical guidance). [1][2] - The npm README for
radix-uihas historically noted it’s “not officially maintained by the Radix UI team,” so if that matters for your org, prefer the individual@radix-ui/react-*packages. [3] - If you’re using shadcn/ui, it recently migrated its “new-york” style to the unified
radix-uiimports. [4]
Sources: Radix Primitives intro/install guidance [1], Radix DropdownMenu docs [2], npm radix-ui README note [3], shadcn/ui changelog (Feb 2026) [4].
🏁 Script executed:
#!/bin/bash
# Check if package.json file is formatted with Prettier
# First, let's check the file content to see if it's properly formatted
cat -n apps/marketing/package.json | head -70Repository: 2witstudios/PageSpace
Length of output: 2909
Remove individual @radix-ui/react-* packages and keep only the unified radix-ui package.
Having both "radix-ui": "^1.4.3" (line 48) and 13 individual @radix-ui/react-* packages (lines 21–33) is a documented anti-pattern. This creates duplicate Radix internals in the bundle and version skew between primitives. Since this codebase uses many Radix primitives, Radix's official guidance (Jan 2025+) recommends the unified radix-ui package as the single dependency, which is tree-shakeable and handles all primitives with a consistent version.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/package.json` around lines 18 - 60, Remove the individual
`@radix-ui/react-`* dependencies and rely only on the unified radix-ui package to
avoid duplicate Radix internals and version skew: delete all entries that match
"@radix-ui/react-accordion", "@radix-ui/react-avatar",
"@radix-ui/react-checkbox", "@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu", "@radix-ui/react-label",
"@radix-ui/react-navigation-menu", "@radix-ui/react-scroll-area",
"@radix-ui/react-select", "@radix-ui/react-separator", "@radix-ui/react-slot",
"@radix-ui/react-tabs", and "@radix-ui/react-tooltip" from package.json and
ensure "radix-ui" remains in dependencies; after editing, run your package
manager install and test the app to confirm tree-shaken primitives resolve
correctly.
| "react-hook-form": "^7.63.0", | ||
| "react-resizable-panels": "^4.6.2", | ||
| "recharts": "2.15.4", | ||
| "remotion": "^4.0.421", | ||
| "resend": "^6.1.2", | ||
| "sonner": "^2.0.7", | ||
| "tailwind-merge": "^3.3.1", | ||
| "vaul": "^1.1.2", | ||
| "zod": "^4.1.11" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check which of these potentially unused dependencies are actually imported in the marketing app source
for pkg in "react-hook-form" "@hookform/resolvers" "recharts" "react-day-picker" "embla-carousel-react" "react-resizable-panels" "input-otp" "cmdk" "vaul" "sonner"; do
echo "=== $pkg ==="
rg "$pkg" --type=ts --type=tsx -g '!node_modules' -g '!package.json' -g '!*.lock' apps/marketing/ 2>/dev/null | head -20
doneRepository: 2witstudios/PageSpace
Length of output: 277
Remove or document the purpose of unused dependencies in package.json.
The following packages are listed in apps/marketing/package.json but have no imports anywhere in the marketing app source code: react-hook-form, @hookform/resolvers, recharts, react-day-picker, embla-carousel-react, react-resizable-panels, input-otp, cmdk, vaul, and sonner. If these are placeholders for future shadcn/ui components, document them via comments or move to a devDependencies section. Otherwise, remove them to reduce install time and lockfile size.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/package.json` around lines 52 - 60, The package.json lists
dependencies that are not imported anywhere in the marketing app
(react-hook-form, `@hookform/resolvers`, recharts, react-day-picker,
embla-carousel-react, react-resizable-panels, input-otp, cmdk, vaul, sonner);
either remove these entries, move them to devDependencies if they are only
needed for development or storybook, or add a clear inline comment in
package.json explaining they are intentionally present as placeholders for
upcoming shadcn/ui components; after making the change, update the lockfile
(npm/yarn/pnpm install) and run the app build/test to ensure no runtime or CI
breakage.
| import { cva, type VariantProps } from "class-variance-authority" | ||
| import { Slot } from "radix-ui" | ||
|
|
||
| import { cn } from "@/lib/utils" | ||
| import { Separator } from "@/components/ui/separator" | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the full content of button-group.tsx
cat -n apps/marketing/src/components/ui/button-group.tsxRepository: 2witstudios/PageSpace
Length of output: 2847
🏁 Script executed:
#!/bin/bash
# Check if React is imported anywhere in the file
grep -i "import.*react" apps/marketing/src/components/ui/button-group.tsxRepository: 2witstudios/PageSpace
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Verify the lines mentioned (28 and 44) and surrounding context
sed -n '20,50p' apps/marketing/src/components/ui/button-group.tsxRepository: 2witstudios/PageSpace
Length of output: 620
🏁 Script executed:
#!/bin/bash
# Check TypeScript/Next.js configuration for JSX settings
cat apps/marketing/tsconfig.json | head -50Repository: 2witstudios/PageSpace
Length of output: 824
🏁 Script executed:
#!/bin/bash
# Check other component files for React import patterns
fd -e tsx -p 'apps/marketing/src/components/ui' --exec sh -c 'echo "=== {} ===" && head -10 "$1"' _ {} \; | head -100Repository: 2witstudios/PageSpace
Length of output: 3000
🏁 Script executed:
#!/bin/bash
# Specifically check if other files in the same directory import React
grep -r "import.*React" apps/marketing/src/components/ui/ | head -10Repository: 2witstudios/PageSpace
Length of output: 827
🏁 Script executed:
#!/bin/bash
# Check if there are any global type definitions for React
find apps/marketing -name "*.d.ts" -o -name "globals.ts" -o -name "ambient.ts" | xargs cat 2>/dev/null | grep -i react | head -20Repository: 2witstudios/PageSpace
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Verify field.tsx as a comparison - it uses React.ComponentProps too
head -20 apps/marketing/src/components/ui/field.tsxRepository: 2witstudios/PageSpace
Length of output: 634
🏁 Script executed:
#!/bin/bash
# Double-check button-group.tsx imports one more time to be absolutely certain
head -10 apps/marketing/src/components/ui/button-group.tsxRepository: 2witstudios/PageSpace
Length of output: 598
Missing React import — React.ComponentProps references on lines 28, 44, and 64 will fail.
React.ComponentProps is used in the type annotations but React is never imported. With the project's jsx: "preserve" configuration, the React namespace must be explicitly imported when referenced in type positions. All other UI component files in this directory follow this pattern.
Proposed fix
+import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { cva, type VariantProps } from "class-variance-authority" | |
| import { Slot } from "radix-ui" | |
| import { cn } from "@/lib/utils" | |
| import { Separator } from "@/components/ui/separator" | |
| import * as React from "react" | |
| import { cva, type VariantProps } from "class-variance-authority" | |
| import { Slot } from "radix-ui" | |
| import { cn } from "@/lib/utils" | |
| import { Separator } from "@/components/ui/separator" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/src/components/ui/button-group.tsx` around lines 1 - 6, The
file references React.ComponentProps in its type annotations but never imports
the React namespace; add an explicit namespace import (e.g. import * as React
from "react" or import React from "react") at the top of the file so the
React.ComponentProps type usages compile; update the top imports near
cva/VariantProps/Slot/cn/Separator to include that React import.
- Add "use client" directive to navigation-menu.tsx for Radix hooks - Use shared pageMetadata for terms/privacy pages instead of inline createMetadata - Remove SearchAction from JSON-LD schema (no /search page exists) - Fix HSL color opacity in Remotion DocumentEditing (withOpacity helper replaces broken hex concatenation) - Fix render-videos.ts progress logging and wire enableTailwind into webpack override - Remove unimplemented /docs/api and /docs/mcp from sitemap - Point broken /docs/* links to /docs fallback across integrations, docs, and getting-started pages - Replace copy-pasted inline headers with SiteNavbar in 7 pages - Replace line-by-line blog markdown parser with react-markdown + remark-gfm Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/package.json`:
- Around line 12-15: The package.json scripts remotion:render,
remotion:render:light, and remotion:render:dark invoke ts-node against
scripts/render-videos.ts which uses ESM imports; update those script commands to
pass the --esm flag to ts-node so ESM syntax is recognized (e.g., change the
invocations for the "remotion:render*" scripts to include --esm before the
script path) and keep remotion:studio unchanged; ensure the referenced
scripts/render-videos.ts remains the target.
In `@apps/marketing/scripts/render-videos.ts`:
- Line 64: The code uses the CommonJS __dirname when computing outputDir;
replace that with an ESM-compatible dirname derived from import.meta.url by
using fileURLToPath(import.meta.url) and path.dirname(...) and then use that
dirname in the existing path.join calls (e.g. the expression that sets outputDir
and the similar path.join usage at the other location). Ensure you import or
reference fileURLToPath and use path.dirname(fileURLToPath(import.meta.url)) as
the source for joining instead of __dirname.
- Around line 46-49: The onProgress handler currently calls process.stdout.write
on every event even when Math.round(progress * 100) yields the same integer,
causing redundant writes; introduce a module- or function-scoped tracker (e.g.,
lastReportedPercent initialized to -1) and inside the onProgress callback
compute const pct = Math.round(progress * 100) and only call
process.stdout.write when pct !== lastReportedPercent, then set
lastReportedPercent = pct (keep the existing %10 check if you still want only
tens). This change touches the onProgress callback and uses the
Math.round(progress * 100) value and process.stdout.write to avoid duplicate
prints.
In `@apps/marketing/src/app/blog/`[slug]/page.tsx:
- Around line 572-585: The share buttons are inert because they lack client-side
handlers; extract the share block into a small "use client" React component
(e.g., ShareButtons) and replace the static JSX in page.tsx with that component.
In ShareButtons, import the existing Button, Twitter, Linkedin, Share2 symbols
and add onClick handlers: for Twitter and LinkedIn build and open the
appropriate share-intent URLs (using window.open with the current page URL from
window.location.href), and for Copy Link use
navigator.clipboard.writeText(window.location.href) with a simple
success/failure fallback (e.g., temporary UI state or alert). Ensure the
component is marked "use client" so window and navigator are available and keep
the original styling/props for Button.
- Around line 24-465: The page component currently embeds a large blogPosts
constant which reduces maintainability; extract that data into a dedicated
module and import it into the page component. Create a new export (e.g., export
const blogPosts = {...}) in a separate data file (or individual markdown files
mapped to slugs) and replace the inline blogPosts definition in the page
component with an import; update any references in the page rendering logic (the
blogPosts identifier and slug-based lookup used in the page component) to use
the imported data, and ensure dates/readTime/author fields remain the same and
TypeScript types (BlogPost) are exported/imported as needed.
- Around line 471-492: The openGraph object returned by generateMetadata is
missing the canonical url field; update generateMetadata (in the function
generateMetadata) to include openGraph.url set to the full canonical page URL
(e.g. construct from your site base URL + `/blog/${slug}`), using an existing
site constant or an env var (like NEXT_PUBLIC_SITE_URL) to avoid hardcoding the
origin and ensure the value is included alongside title, description, type,
publishedTime and authors.
In `@apps/marketing/src/app/docs/getting-started/page.tsx`:
- Around line 149-178: The three Link components rendering the "What's Next?"
cards (the Link elements containing headings "Page Agents Deep Dive", "Connect
Integrations", and "API Reference") all point to the same href "/docs" which is
misleading; update each Link to point to the correct destination (e.g., section
anchors like "/docs#page-agents", "/docs#integrations", "/docs#api" or
individual routes) or, if those pages aren't ready, replace the Link with a
non-clickable card or add a "Coming Soon" visual indicator and remove/disable
the href so users aren't directed to the wrong page; locate and edit the three
Link components in page.tsx to implement the corrected hrefs or add the
coming-soon state.
- Around line 9-12: Replace the inline export const metadata with the shared
helper from your metadata module: import pageMetadata from "@/lib/metadata" (or
the named export used elsewhere) and set export const metadata =
pageMetadata.gettingStarted (or add a gettingStarted entry to pageMetadata if it
doesn't exist). Update the top of the file to import the helper and remove the
hardcoded title/description block so this page matches the pattern used by
pageMetadata.docs.
In `@apps/marketing/src/app/docs/page.tsx`:
- Around line 18-91: The items inside the docSections array share the same
item.href ("/docs") which causes duplicate React keys because the render loop
currently uses item.href as the key; update the render loop to use item.title
(or another unique identifier) as the React key (instead of item.href) when
mapping over docSections items to eliminate duplicate-key warnings — reference
the docSections array, each item's href/title fields, and the mapping/render
loop that currently uses item.href as the key.
- Around line 206-225: The Popular Articles list uses article.href as the React
key and contains near-duplicate entries; update the data array to remove the
duplicate "Setting Up MCP Servers" vs "MCP Server Setup" entry and ensure each
item has a stable unique key (e.g., use a combined unique identifier like
`${article.href}-${article.title}` or a dedicated id field) so replace
key={article.href} in the map with that unique key and deduplicate the array
items accordingly; look for the array literal and the map rendering where
article.href and key={article.href} are used to make these changes.
- Around line 109-122: Search input is non-interactive because this is a server
component; either convert the component to a client component or replace the
input with a static control. To fix, choose one: (A) make the file a client
component by adding "use client" at the top of
apps/marketing/src/app/docs/page.tsx, then implement local state and handlers
(useState, onChange, onKeyDown) for the <input> and hook the ⌘K keyboard
shortcut to open the command palette (refer to the <input> element and the kbd
hint), or (B) keep it server-side but replace the <input> with a non-interactive
button/link that visually matches the design and triggers navigation/opening of
a client-side search modal when clicked (create a small client SearchButton
component and reference it in place of the <input> and kbd).
In `@apps/marketing/src/app/integrations/page.tsx`:
- Around line 287-298: Replace the series of <div> elements used for the curl
snippet with a semantic pre/code block that preserves whitespace and improves
accessibility: locate the code preview block (the rounded-lg bg-muted/50 p-4
font-mono text-sm mb-6 overflow-x-auto container) and change its inner structure
to a single <pre><code> element that keeps the existing classes (font-mono,
overflow-x-auto, etc.), remove the manual HTML entity quoting and nested
indentation <div>s (the text-green-600 "curl" span can remain inside the
<code>), and place the full curl payload as literal text inside the <code> so
whitespace and quoting are preserved for screen readers and browsers.
In `@apps/marketing/src/app/terms/page.tsx`:
- Around line 208-212: Replace the raw anchor for the Discord URL in the Terms
page with the framework Link component and add target="_blank" and rel="noopener
noreferrer" to open externally and prevent window.opener access; locate the
anchor inside apps/marketing/src/app/terms/page.tsx (the list item with
"PageSpace Discord") and swap the <a href="https://discord.gg/kve8qgzZ8x" ...>
usage to use Link (keeping the existing className "text-primary
hover:underline") and include target="_blank" rel="noopener noreferrer"
attributes.
- Line 16: Extract the hardcoded "Last updated: November 1, 2025" string into a
shared constant or the existing metadata module (e.g., alongside
pageMetadata.terms) and replace the literal in the <p
className="text-muted-foreground"> element in terms/page.tsx with that constant;
this centralizes the date so both Terms and Privacy pages can reference the same
value and be updated in one place.
In `@apps/marketing/src/app/tour/page.tsx`:
- Around line 195-198: The code currently uses step.number to index tourSteps
(tourSteps[step.number].title), which couples array order to the step.number
field; change the map callback to accept the index parameter and compute the
next step by index (e.g., const nextIndex = index + 1), then use
tourSteps[nextIndex].title for the button label and tourSteps[nextIndex].number
(or nextIndex+1 if you want sequential ids) for the href; update the usage of
step.number in this block to use index/nextIndex so the UI relies on array
position rather than the step.number field.
In `@apps/marketing/src/components/ui/navigation-menu.tsx`:
- Around line 108-123: The wrapper div is calling cn() around a single static
class string unnecessarily; remove the cn() call and pass the string directly to
the div's className to match the simpler pattern used elsewhere (leave
NavigationMenuPrimitive.Viewport unchanged, keep using cn() where it merges
dynamic className/props). Locate the wrapper div rendering the
NavigationMenuPrimitive.Viewport and replace className={cn("absolute top-full
left-0 isolate z-50 flex justify-center")} with className="absolute top-full
left-0 isolate z-50 flex justify-center".
- Around line 63-65: The NavigationMenuContent class string is too long and hard
to read due to a large block of group-data-[viewport=false]/navigation-menu:
utilities; extract that viewport-false block into a named constant (e.g.,
navigationMenuViewportFalseClasses) and then use it inside the cn() call for
NavigationMenuContent (and any other similar usage between lines 87-102) or
split it into a separate array entry passed to cn(); update references in the
NavigationMenuContent definition to include the new constant to improve
readability while preserving the exact class tokens.
In `@apps/marketing/src/lib/schema.tsx`:
- Around line 135-149: The softwareApplicationSchema currently sets downloadUrl
to a landing page (downloadUrl inside softwareApplicationSchema using SITE_URL),
but Schema.org expects a direct file URL; update the downloadUrl property in
softwareApplicationSchema to point to the actual binary file URLs (e.g., per
platform installers) once those files are published, or remove the downloadUrl
property now and rely on the offers/url fields until real binaries exist so the
schema remains accurate.
- Around line 243-253: JsonLd uses dangerouslySetInnerHTML with
JSON.stringify(jsonLd) which can contain the sequence "</script>" and allow
injection; fix by post-processing the serialized JSON before assigning to __html
in the JsonLd component: after JSON.stringify(jsonLd) replace any "<" characters
with the unicode escape sequence ("\u003c") so the JSON remains valid but cannot
close the script tag; keep the existing Array.isArray handling and assign the
sanitized string to __html.
- Around line 59-60: Extract the duplicated literal "1.0" into a shared constant
(e.g., SOFTWARE_VERSION) and reference that constant from both
webApplicationSchema and softwareApplicationSchema instead of the hard-coded
string; add the constant near the top of the file (or an existing constants
section), replace the softwareVersion entries in webApplicationSchema and
softwareApplicationSchema with SOFTWARE_VERSION, and ensure any exports/imports
are updated if other modules need access.
- Line 61: The embedded organizationSchema includes its own "@context", which
causes nested `@context` warnings when used as author in softwareApplicationSchema
and publisher in createArticleSchema; remove the "@context" when embedding by
creating a context-free variant (e.g., derive organizationRef from
organizationSchema by excluding the "@context" key) and use that organizationRef
for author and publisher fields while keeping organizationSchema (with
"@context") for standalone JSON-LD output.
🧹 Nitpick comments (14)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/scripts/render-videos.ts`: - Around line 46-49: The onProgress handler currently calls process.stdout.write on every event even when Math.round(progress * 100) yields the same integer, causing redundant writes; introduce a module- or function-scoped tracker (e.g., lastReportedPercent initialized to -1) and inside the onProgress callback compute const pct = Math.round(progress * 100) and only call process.stdout.write when pct !== lastReportedPercent, then set lastReportedPercent = pct (keep the existing %10 check if you still want only tens). This change touches the onProgress callback and uses the Math.round(progress * 100) value and process.stdout.write to avoid duplicate prints. In `@apps/marketing/src/app/blog/`[slug]/page.tsx: - Around line 24-465: The page component currently embeds a large blogPosts constant which reduces maintainability; extract that data into a dedicated module and import it into the page component. Create a new export (e.g., export const blogPosts = {...}) in a separate data file (or individual markdown files mapped to slugs) and replace the inline blogPosts definition in the page component with an import; update any references in the page rendering logic (the blogPosts identifier and slug-based lookup used in the page component) to use the imported data, and ensure dates/readTime/author fields remain the same and TypeScript types (BlogPost) are exported/imported as needed. - Around line 471-492: The openGraph object returned by generateMetadata is missing the canonical url field; update generateMetadata (in the function generateMetadata) to include openGraph.url set to the full canonical page URL (e.g. construct from your site base URL + `/blog/${slug}`), using an existing site constant or an env var (like NEXT_PUBLIC_SITE_URL) to avoid hardcoding the origin and ensure the value is included alongside title, description, type, publishedTime and authors. In `@apps/marketing/src/app/docs/getting-started/page.tsx`: - Around line 9-12: Replace the inline export const metadata with the shared helper from your metadata module: import pageMetadata from "@/lib/metadata" (or the named export used elsewhere) and set export const metadata = pageMetadata.gettingStarted (or add a gettingStarted entry to pageMetadata if it doesn't exist). Update the top of the file to import the helper and remove the hardcoded title/description block so this page matches the pattern used by pageMetadata.docs. In `@apps/marketing/src/app/docs/page.tsx`: - Around line 109-122: Search input is non-interactive because this is a server component; either convert the component to a client component or replace the input with a static control. To fix, choose one: (A) make the file a client component by adding "use client" at the top of apps/marketing/src/app/docs/page.tsx, then implement local state and handlers (useState, onChange, onKeyDown) for the <input> and hook the ⌘K keyboard shortcut to open the command palette (refer to the <input> element and the kbd hint), or (B) keep it server-side but replace the <input> with a non-interactive button/link that visually matches the design and triggers navigation/opening of a client-side search modal when clicked (create a small client SearchButton component and reference it in place of the <input> and kbd). In `@apps/marketing/src/app/integrations/page.tsx`: - Around line 287-298: Replace the series of <div> elements used for the curl snippet with a semantic pre/code block that preserves whitespace and improves accessibility: locate the code preview block (the rounded-lg bg-muted/50 p-4 font-mono text-sm mb-6 overflow-x-auto container) and change its inner structure to a single <pre><code> element that keeps the existing classes (font-mono, overflow-x-auto, etc.), remove the manual HTML entity quoting and nested indentation <div>s (the text-green-600 "curl" span can remain inside the <code>), and place the full curl payload as literal text inside the <code> so whitespace and quoting are preserved for screen readers and browsers. In `@apps/marketing/src/app/terms/page.tsx`: - Around line 208-212: Replace the raw anchor for the Discord URL in the Terms page with the framework Link component and add target="_blank" and rel="noopener noreferrer" to open externally and prevent window.opener access; locate the anchor inside apps/marketing/src/app/terms/page.tsx (the list item with "PageSpace Discord") and swap the <a href="https://discord.gg/kve8qgzZ8x" ...> usage to use Link (keeping the existing className "text-primary hover:underline") and include target="_blank" rel="noopener noreferrer" attributes. - Line 16: Extract the hardcoded "Last updated: November 1, 2025" string into a shared constant or the existing metadata module (e.g., alongside pageMetadata.terms) and replace the literal in the <p className="text-muted-foreground"> element in terms/page.tsx with that constant; this centralizes the date so both Terms and Privacy pages can reference the same value and be updated in one place. In `@apps/marketing/src/app/tour/page.tsx`: - Around line 195-198: The code currently uses step.number to index tourSteps (tourSteps[step.number].title), which couples array order to the step.number field; change the map callback to accept the index parameter and compute the next step by index (e.g., const nextIndex = index + 1), then use tourSteps[nextIndex].title for the button label and tourSteps[nextIndex].number (or nextIndex+1 if you want sequential ids) for the href; update the usage of step.number in this block to use index/nextIndex so the UI relies on array position rather than the step.number field. In `@apps/marketing/src/components/ui/navigation-menu.tsx`: - Around line 108-123: The wrapper div is calling cn() around a single static class string unnecessarily; remove the cn() call and pass the string directly to the div's className to match the simpler pattern used elsewhere (leave NavigationMenuPrimitive.Viewport unchanged, keep using cn() where it merges dynamic className/props). Locate the wrapper div rendering the NavigationMenuPrimitive.Viewport and replace className={cn("absolute top-full left-0 isolate z-50 flex justify-center")} with className="absolute top-full left-0 isolate z-50 flex justify-center". - Around line 63-65: The NavigationMenuContent class string is too long and hard to read due to a large block of group-data-[viewport=false]/navigation-menu: utilities; extract that viewport-false block into a named constant (e.g., navigationMenuViewportFalseClasses) and then use it inside the cn() call for NavigationMenuContent (and any other similar usage between lines 87-102) or split it into a separate array entry passed to cn(); update references in the NavigationMenuContent definition to include the new constant to improve readability while preserving the exact class tokens. In `@apps/marketing/src/lib/schema.tsx`: - Around line 243-253: JsonLd uses dangerouslySetInnerHTML with JSON.stringify(jsonLd) which can contain the sequence "</script>" and allow injection; fix by post-processing the serialized JSON before assigning to __html in the JsonLd component: after JSON.stringify(jsonLd) replace any "<" characters with the unicode escape sequence ("\u003c") so the JSON remains valid but cannot close the script tag; keep the existing Array.isArray handling and assign the sanitized string to __html. - Around line 59-60: Extract the duplicated literal "1.0" into a shared constant (e.g., SOFTWARE_VERSION) and reference that constant from both webApplicationSchema and softwareApplicationSchema instead of the hard-coded string; add the constant near the top of the file (or an existing constants section), replace the softwareVersion entries in webApplicationSchema and softwareApplicationSchema with SOFTWARE_VERSION, and ensure any exports/imports are updated if other modules need access. - Line 61: The embedded organizationSchema includes its own "@context", which causes nested `@context` warnings when used as author in softwareApplicationSchema and publisher in createArticleSchema; remove the "@context" when embedding by creating a context-free variant (e.g., derive organizationRef from organizationSchema by excluding the "@context" key) and use that organizationRef for author and publisher fields while keeping organizationSchema (with "@context") for standalone JSON-LD output.apps/marketing/src/app/terms/page.tsx (2)
208-212: Nit: consider usingLinkwithtarget="_blank"andrel="noopener noreferrer"for the Discord link.The raw
<a>tag works for external URLs, but addingtarget="_blank"(if opening in a new tab is desired) along withrel="noopener noreferrer"is a best practice for external links to preventwindow.openeraccess.♻️ Suggested tweak
- <li><strong>Community:</strong> <a href="https://discord.gg/kve8qgzZ8x" className="text-primary hover:underline">PageSpace Discord</a></li> + <li><strong>Community:</strong> <a href="https://discord.gg/kve8qgzZ8x" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">PageSpace Discord</a></li>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/terms/page.tsx` around lines 208 - 212, Replace the raw anchor for the Discord URL in the Terms page with the framework Link component and add target="_blank" and rel="noopener noreferrer" to open externally and prevent window.opener access; locate the anchor inside apps/marketing/src/app/terms/page.tsx (the list item with "PageSpace Discord") and swap the <a href="https://discord.gg/kve8qgzZ8x" ...> usage to use Link (keeping the existing className "text-primary hover:underline") and include target="_blank" rel="noopener noreferrer" attributes.
16-16: Nit: consider extracting the "Last updated" date to a constant or the metadata module.Hardcoded dates in legal pages tend to drift. Centralizing it (e.g., alongside
pageMetadata.terms) would make it easier to keep the Privacy Policy and Terms dates in sync when either is updated.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/terms/page.tsx` at line 16, Extract the hardcoded "Last updated: November 1, 2025" string into a shared constant or the existing metadata module (e.g., alongside pageMetadata.terms) and replace the literal in the <p className="text-muted-foreground"> element in terms/page.tsx with that constant; this centralizes the date so both Terms and Privacy pages can reference the same value and be updated in one place.apps/marketing/src/app/tour/page.tsx (1)
195-198: Fragile coupling betweenstep.numberand array index.Line 198 uses
tourSteps[step.number].titleto get the next step's title. This works only because eachstep.numberequals its array index + 1. If anyone reorders items or changes the numbering, this silently shows the wrong title (or throws at runtime ifstep.numbergoes out of bounds).Using the
indexfrom.map()makes the intent explicit and decouples from the data'snumberfield.Proposed fix
- {tourSteps.map((step, index) => ( + {tourSteps.map((step, index) => { + const nextStep = tourSteps[index + 1]; + return ( <section key={step.number} id={`step-${step.number}`} className={`py-16 md:py-24 ${index % 2 === 1 ? "bg-muted/30" : ""}`} > ... <div className="mt-8 flex items-center gap-4"> - {step.number < tourSteps.length ? ( + {nextStep ? ( <Button variant="outline" asChild> <a href={`#step-${step.number + 1}`}> - Next: {tourSteps[step.number].title} + Next: {nextStep.title} <ArrowRight className="ml-2 h-4 w-4" /> </a> </Button> ) : ( ... )} </div> ... </section> - ))} + ); + })}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/tour/page.tsx` around lines 195 - 198, The code currently uses step.number to index tourSteps (tourSteps[step.number].title), which couples array order to the step.number field; change the map callback to accept the index parameter and compute the next step by index (e.g., const nextIndex = index + 1), then use tourSteps[nextIndex].title for the button label and tourSteps[nextIndex].number (or nextIndex+1 if you want sequential ids) for the href; update the usage of step.number in this block to use index/nextIndex so the UI relies on array position rather than the step.number field.apps/marketing/src/app/integrations/page.tsx (1)
287-298: Use semantic<pre><code>for the code preview block.The curl example is rendered with plain
<div>elements, which don't preserve whitespace semantics and aren't recognized by screen readers or browser features as preformatted code. A<pre><code>block is the correct semantic element here and would also simplify the markup (no need for manual"/'entities orpl-4indentation divs).♻️ Suggested refactor
- <div className="rounded-lg bg-muted/50 p-4 font-mono text-sm mb-6 overflow-x-auto"> - <div className="text-muted-foreground"># Create a new document with AI</div> - <div className="mt-2"> - <span className="text-green-600 dark:text-green-400">curl</span> -X POST https://api.pagespace.ai/v1/pages \ - </div> - <div className="pl-4"> - -H "Authorization: Bearer $API_KEY" \ - </div> - <div className="pl-4"> - -d '{"title": "Meeting Notes", "ai_assist": true}' - </div> - </div> + <pre className="rounded-lg bg-muted/50 p-4 font-mono text-sm mb-6 overflow-x-auto whitespace-pre-wrap"> + <code>{`# Create a new document with AI +curl -X POST https://api.pagespace.ai/v1/pages \\ + -H "Authorization: Bearer $API_KEY" \\ + -d '{"title": "Meeting Notes", "ai_assist": true}'`}</code> + </pre>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/integrations/page.tsx` around lines 287 - 298, Replace the series of <div> elements used for the curl snippet with a semantic pre/code block that preserves whitespace and improves accessibility: locate the code preview block (the rounded-lg bg-muted/50 p-4 font-mono text-sm mb-6 overflow-x-auto container) and change its inner structure to a single <pre><code> element that keeps the existing classes (font-mono, overflow-x-auto, etc.), remove the manual HTML entity quoting and nested indentation <div>s (the text-green-600 "curl" span can remain inside the <code>), and place the full curl payload as literal text inside the <code> so whitespace and quoting are preserved for screen readers and browsers.apps/marketing/src/components/ui/navigation-menu.tsx (2)
108-123: Unnecessarycn()on the wrapper div.Line 110-112: The
cn()call wraps a single static string with noclassNameprop to merge. It works but adds an unnecessary function call. This is a very minor nit — consistent with the pattern elsewhere in the file, so feel free to leave as-is.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/components/ui/navigation-menu.tsx` around lines 108 - 123, The wrapper div is calling cn() around a single static class string unnecessarily; remove the cn() call and pass the string directly to the div's className to match the simpler pattern used elsewhere (leave NavigationMenuPrimitive.Viewport unchanged, keep using cn() where it merges dynamic className/props). Locate the wrapper div rendering the NavigationMenuPrimitive.Viewport and replace className={cn("absolute top-full left-0 isolate z-50 flex justify-center")} with className="absolute top-full left-0 isolate z-50 flex justify-center".
63-65: Long Tailwind class strings — consider extracting the viewport-false block.The
NavigationMenuContentclass string on lines 95-96 is extremely long, particularly the repeatedgroup-data-[viewport=false]/navigation-menu:prefixed utilities. While this follows the shadcn/ui convention of co-locating styles, the viewport-false block alone contains ~15 chained selectors on a single line, making it hard to review or modify.If readability becomes a concern, consider splitting the viewport-false classes into a separate array entry within the
cn()call, or extracting them into a named constant similar tonavigationMenuTriggerStyle.Also applies to: 87-102
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/components/ui/navigation-menu.tsx` around lines 63 - 65, The NavigationMenuContent class string is too long and hard to read due to a large block of group-data-[viewport=false]/navigation-menu: utilities; extract that viewport-false block into a named constant (e.g., navigationMenuViewportFalseClasses) and then use it inside the cn() call for NavigationMenuContent (and any other similar usage between lines 87-102) or split it into a separate array entry passed to cn(); update references in the NavigationMenuContent definition to include the new constant to improve readability while preserving the exact class tokens.apps/marketing/src/lib/schema.tsx (3)
243-253: Defend against</script>injection in JSON-LD output.The static analysis flags
dangerouslySetInnerHTMLhere. For JSON-LD with developer-controlled data this is the standard Next.js pattern and generally safe. However,JSON.stringifydoes not escape</script>sequences. If user-influenced data ever flows into a schema (e.g., article titles from a CMS), a crafted</script>could break out of the tag. A simple defensive measure:🛡️ Suggested hardening
<script type="application/ld+json" dangerouslySetInnerHTML={{ - __html: JSON.stringify(jsonLd), + __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"), }} />This replaces
<with its unicode escape inside the JSON string, which is valid JSON and prevents</script>injection.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/lib/schema.tsx` around lines 243 - 253, JsonLd uses dangerouslySetInnerHTML with JSON.stringify(jsonLd) which can contain the sequence "</script>" and allow injection; fix by post-processing the serialized JSON before assigning to __html in the JsonLd component: after JSON.stringify(jsonLd) replace any "<" characters with the unicode escape sequence ("\u003c") so the JSON remains valid but cannot close the script tag; keep the existing Array.isArray handling and assign the sanitized string to __html.
59-60:softwareVersion: "1.0"is duplicated across schemas.This string literal appears in both
webApplicationSchema(line 60) andsoftwareApplicationSchema(line 142). Extract it into a shared constant to keep versions in sync.♻️ Suggested fix
const SITE_URL = process.env.NEXT_PUBLIC_MARKETING_URL || "https://pagespace.ai"; const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://pagespace.ai"; +const SOFTWARE_VERSION = "1.0";Then reference
SOFTWARE_VERSIONin both schemas.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/lib/schema.tsx` around lines 59 - 60, Extract the duplicated literal "1.0" into a shared constant (e.g., SOFTWARE_VERSION) and reference that constant from both webApplicationSchema and softwareApplicationSchema instead of the hard-coded string; add the constant near the top of the file (or an existing constants section), replace the softwareVersion entries in webApplicationSchema and softwareApplicationSchema with SOFTWARE_VERSION, and ensure any exports/imports are updated if other modules need access.
61-61: Nested@contextin embeddedorganizationSchemamay trigger structured data warnings.
organizationSchemacarries its own"@context": "https://schema.org"(line 14), and it's embedded verbatim asauthorhere, insidesoftwareApplicationSchema(line 148), and aspublisherincreateArticleSchema(line 200). Google's Rich Results Test can flag nested@contextdeclarations. Consider destructuring out@contextwhen embedding:♻️ Suggested approach
Extract a context-free variant for embedding:
// At module level, after organizationSchema: const { "@context": _, ...organizationRef } = organizationSchema;Then use
organizationRefwherever the schema is embedded (author,publisher), keepingorganizationSchema(with@context) for standalone use in the site-wide JSON-LD.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/lib/schema.tsx` at line 61, The embedded organizationSchema includes its own "@context", which causes nested `@context` warnings when used as author in softwareApplicationSchema and publisher in createArticleSchema; remove the "@context" when embedding by creating a context-free variant (e.g., derive organizationRef from organizationSchema by excluding the "@context" key) and use that organizationRef for author and publisher fields while keeping organizationSchema (with "@context") for standalone JSON-LD output.apps/marketing/src/app/docs/getting-started/page.tsx (1)
9-12: Consider using the sharedpageMetadatahelper for consistency.The sibling
docs/page.tsxusespageMetadata.docsfrom@/lib/metadata, and commit messages mention replacing inline metadata with shared helpers. This page still defines metadata inline. If apageMetadata.gettingStarted(or similar) entry exists or can be added, using it would keep the docs section consistent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/docs/getting-started/page.tsx` around lines 9 - 12, Replace the inline export const metadata with the shared helper from your metadata module: import pageMetadata from "@/lib/metadata" (or the named export used elsewhere) and set export const metadata = pageMetadata.gettingStarted (or add a gettingStarted entry to pageMetadata if it doesn't exist). Update the top of the file to import the helper and remove the hardcoded title/description block so this page matches the pattern used by pageMetadata.docs.apps/marketing/src/app/docs/page.tsx (1)
109-122: Search input is non-interactive.This is a server component so the
<input>cannot hold state or handle events. The⌘Khint suggests a command palette is intended. Consider either making this a client component with search functionality, or replacing the input with a static button/link that visually indicates "search coming soon."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/docs/page.tsx` around lines 109 - 122, Search input is non-interactive because this is a server component; either convert the component to a client component or replace the input with a static control. To fix, choose one: (A) make the file a client component by adding "use client" at the top of apps/marketing/src/app/docs/page.tsx, then implement local state and handlers (useState, onChange, onKeyDown) for the <input> and hook the ⌘K keyboard shortcut to open the command palette (refer to the <input> element and the kbd hint), or (B) keep it server-side but replace the <input> with a non-interactive button/link that visually matches the design and triggers navigation/opening of a client-side search modal when clicked (create a small client SearchButton component and reference it in place of the <input> and kbd).apps/marketing/src/app/blog/[slug]/page.tsx (2)
24-465: ~440 lines of hardcoded blog content in the page component hurts maintainability.Consider extracting
blogPostsinto a dedicated data file (e.g.,@/data/blog-posts.tsor individual.mdfiles) and importing it here. This keeps the page component focused on rendering and makes blog content easier to find and edit independently.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/blog/`[slug]/page.tsx around lines 24 - 465, The page component currently embeds a large blogPosts constant which reduces maintainability; extract that data into a dedicated module and import it into the page component. Create a new export (e.g., export const blogPosts = {...}) in a separate data file (or individual markdown files mapped to slugs) and replace the inline blogPosts definition in the page component with an import; update any references in the page rendering logic (the blogPosts identifier and slug-based lookup used in the page component) to use the imported data, and ensure dates/readTime/author fields remain the same and TypeScript types (BlogPost) are exported/imported as needed.
471-492: OG metadata missingurlfield.The
openGraphobject specifiestype: "article"but omits the canonicalurl, which helps social platforms resolve the correct page. Consider adding it:💡 Suggested improvement
openGraph: { title: post.title, description: post.description, type: "article", publishedTime: post.date, authors: [post.author], + url: `https://pagespace.ai/blog/${slug}`, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/blog/`[slug]/page.tsx around lines 471 - 492, The openGraph object returned by generateMetadata is missing the canonical url field; update generateMetadata (in the function generateMetadata) to include openGraph.url set to the full canonical page URL (e.g. construct from your site base URL + `/blog/${slug}`), using an existing site constant or an env var (like NEXT_PUBLIC_SITE_URL) to avoid hardcoding the origin and ensure the value is included alongside title, description, type, publishedTime and authors.apps/marketing/scripts/render-videos.ts (1)
46-49: Progress logging fix applied but can still emit duplicate lines per percentage.The past review's core concern (fractional
progress % 10) is resolved. However,Math.round(progress * 100)can yield the same integer for many consecutiveonProgresscalls, soprocess.stdout.writewill fire repeatedly for the same bucket (e.g., "Progress: 50%" printed dozens of times). Since\roverwrites in-place this is cosmetically fine, but you could reduce unnecessary writes by tracking the last reported percentage:Optional improvement
+ let lastReported = -1; await renderMedia({ composition, serveUrl: bundleLocation, codec: "h264", outputLocation: outputPath, onProgress: ({ progress }) => { - if (Math.round(progress * 100) % 10 === 0) { - process.stdout.write(` Progress: ${Math.round(progress * 100)}%\r`); - } + const percent = Math.round(progress * 100); + if (percent % 10 === 0 && percent !== lastReported) { + process.stdout.write(` Progress: ${percent}%\r`); + lastReported = percent; + } }, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/scripts/render-videos.ts` around lines 46 - 49, The onProgress handler currently calls process.stdout.write on every event even when Math.round(progress * 100) yields the same integer, causing redundant writes; introduce a module- or function-scoped tracker (e.g., lastReportedPercent initialized to -1) and inside the onProgress callback compute const pct = Math.round(progress * 100) and only call process.stdout.write when pct !== lastReportedPercent, then set lastReportedPercent = pct (keep the existing %10 check if you still want only tens). This change touches the onProgress callback and uses the Math.round(progress * 100) value and process.stdout.write to avoid duplicate prints.
| const all = !darkOnly && !lightOnly; | ||
|
|
||
| // Create output directory | ||
| const outputDir = path.join(__dirname, "../public/videos"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check the tsconfig used by the marketing app
fd -t f 'tsconfig' apps/marketing --exec sh -c 'echo "=== {} ===" && cat {}'Repository: 2witstudios/PageSpace
Length of output: 1151
🏁 Script executed:
# Check the package.json in marketing app for scripts configuration
cat apps/marketing/package.json | head -50Repository: 2witstudios/PageSpace
Length of output: 1873
🏁 Script executed:
# Examine the actual file mentioned in the review
cat -n apps/marketing/scripts/render-videos.ts | head -80Repository: 2witstudios/PageSpace
Length of output: 3023
🏁 Script executed:
# Check the root tsconfig to understand base configuration
cat -n tsconfig.json | head -30Repository: 2witstudios/PageSpace
Length of output: 667
Replace __dirname with ESM-compatible equivalent.
The file uses ESM imports but references __dirname, a CommonJS-only global. To align with the codebase requirement to use ESM modules throughout, replace with:
+import { fileURLToPath } from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);This applies to lines 64 and 71.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/scripts/render-videos.ts` at line 64, The code uses the
CommonJS __dirname when computing outputDir; replace that with an ESM-compatible
dirname derived from import.meta.url by using fileURLToPath(import.meta.url) and
path.dirname(...) and then use that dirname in the existing path.join calls
(e.g. the expression that sets outputDir and the similar path.join usage at the
other location). Ensure you import or reference fileURLToPath and use
path.dirname(fileURLToPath(import.meta.url)) as the source for joining instead
of __dirname.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ESM: Add --esm to remotion scripts, polyfill __dirname/fileURLToPath Blog: Extract blogPosts to data.ts, create ShareButtons client component, add openGraph.url Docs: Fix duplicate React keys, dedup Popular Articles, replace search input with button, shared metadata for getting-started, Coming Soon cards Integrations: Replace div-based curl snippet with semantic pre/code Legal: Add LEGAL_LAST_UPDATED constant for terms and privacy, Discord link attrs Tour: Use map index instead of fragile step.number coupling Navigation: Remove unnecessary cn() wrapper, extract viewportFalseClasses Schema: Remove invalid downloadUrl, sanitize JsonLd XSS, extract SOFTWARE_VERSION, strip nested @context from embedded organizationRef
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
apps/marketing/src/lib/schema.tsx (2)
246-257: JSON-LD rendering pattern is correct; static analysis flag is a false positive here.The
dangerouslySetInnerHTMLusage is the standard approach for embedding JSON-LD in React. The.replace(/</g, "\\u003c")correctly mitigates script-injection attacks by escaping<characters in the serialized output. The data flowing through this component is developer-controlled schema objects, not user input.Minor nit: the
dataparameter typed asobject | object[]is fairly loose. A narrower type would be more self-documenting per the project's "explicit over implicit" guideline.Optional: tighter type for the data prop
-export function JsonLd({ data }: { data: object | object[] }) { +type JsonLdObject = Record<string, unknown>; +export function JsonLd({ data }: { data: JsonLdObject | JsonLdObject[] }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/lib/schema.tsx` around lines 246 - 257, The JsonLd component currently types its prop as data: object | object[] which is too loose; tighten it by changing the prop type on function JsonLd to a more explicit shape such as Record<string, unknown> | Record<string, unknown>[] (or define a named interface/type alias like JsonLdData = Record<string, unknown> and use JsonLdData | JsonLdData[]), update the function signature JsonLd({ data }: { data: ... }) accordingly, and run typechecks to ensure existing callers conform to the narrower type.
7-8: Both URL constants share the same fallback value.
SITE_URLandAPP_URLboth default to"https://pagespace.ai", so in local development (without env vars), auth redirect links and marketing links will be indistinguishable. Consider using distinct fallbacks (e.g.,http://localhost:3000vshttp://localhost:3001) or at least adding a comment explaining that the env vars are required for proper separation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/lib/schema.tsx` around lines 7 - 8, SITE_URL and APP_URL currently share the same fallback which causes marketing and app links to be identical in dev; update the fallback values so they are distinct (for example use "http://localhost:3000" for SITE_URL and "http://localhost:3001" for APP_URL) or add a clear comment above the constants requiring NEXT_PUBLIC_MARKETING_URL and NEXT_PUBLIC_APP_URL to be set in development; edit the SITE_URL and APP_URL declarations in schema.tsx to use the new distinct defaults or add the explanatory comment.apps/marketing/src/app/docs/getting-started/page.tsx (1)
8-8:APP_URLis redefined in every page file.
APP_URLappears identically ingetting-started/page.tsx,tour/page.tsx,integrations/page.tsx, andSiteNavbar.tsx. Consider exporting it from a shared location (e.g., alongsidemetadata.tsor aconfig.ts) to avoid drift if the env var name changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/docs/getting-started/page.tsx` at line 8, APP_URL is duplicated across multiple files (getting-started/page.tsx, tour/page.tsx, integrations/page.tsx, SiteNavbar.tsx); extract it into a single exported constant (e.g., export const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "https://pagespace.ai") in a shared module such as config.ts or alongside metadata.ts, then import APP_URL from that module in each of the pages and SiteNavbar to remove duplication and prevent drift if the env var changes.apps/marketing/src/app/blog/[slug]/page.tsx (1)
137-141: Consider usingLinkwith explicit external navigation, or addrelattributes to the anchor.The CTA link navigates to the main app (a different origin). Using a raw
<a>is correct for cross-origin navigation, but consider addingrel="noopener"as a minor hardening measure since the target URL is constructed from an environment variable.🔧 Suggested tweak
<Button size="lg" asChild> - <a href={`${APP_URL}/auth/signup`}> + <a href={`${APP_URL}/auth/signup`} rel="noopener"> Get Started Free </a> </Button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/marketing/src/app/blog/`[slug]/page.tsx around lines 137 - 141, The CTA uses a raw anchor inside Button to navigate to APP_URL (cross-origin) without rel attributes; update the anchor in the Button (referencing Button and the APP_URL usage in page.tsx) to either use your framework Link configured for external navigation or add rel="noopener" (and optionally "noreferrer") and target="_blank" if opening in a new tab, so the external navigation is hardened.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/marketing/src/app/blog/`[slug]/data.ts:
- Around line 390-393: The blog post "mcp-servers-explained" contains a broken
markdown link "[MCP documentation](/docs/mcp)" in
apps/marketing/src/app/blog/[slug]/data.ts; update that link to a valid docs
route or canonical URL (e.g., replace "/docs/mcp" with the correct internal path
or full external docs URL) inside the post content string so the rendered
markdown doesn't 404. Ensure you update the exact markdown link text "[MCP
documentation](/docs/mcp)" in the post body (or use your central docs base URL
variable if available) to point to the correct destination.
In `@apps/marketing/src/app/blog/`[slug]/ShareButtons.tsx:
- Around line 26-30: The copyLink function should handle failures from
navigator.clipboard.writeText by wrapping the await call in a try/catch: catch
errors and avoid throwing unhandled rejections, setCopied only on success (call
setCopied(true) inside the try), and in the catch branch provide a graceful
fallback (e.g., show an error state or fallback to prompting the user with
window.prompt or selecting an input) and ensure setCopied(false) or no
optimistic UI change occurs; update the copyLink implementation and any UI state
usage (setCopied, copied) accordingly so clipboard permission or
insecure-context errors are handled without console errors.
In `@apps/marketing/src/app/docs/page.tsx`:
- Around line 18-91: docSections contains many placeholder item hrefs pointing
to "/docs", causing confusing circular navigation; update the items in the
docSections array (each item's href) to either point to meaningful anchors
(e.g., "/docs#getting-started", "/docs#google-calendar",
"/docs#data-encryption") or add a "comingSoon" boolean on items (and set href to
null/undefined) and update the rendering logic to show a non-clickable "Coming
soon" treatment for items with comingSoon true; modify the DocSection/item
objects in the docSections constant and the consumer component that renders
items to respect either the anchor hrefs or the comingSoon flag so users aren’t
routed back to the same page.
In `@apps/marketing/src/app/integrations/page.tsx`:
- Around line 309-342: The three Link elements rendering "API Reference", "SDK
Libraries", and "Webhook Events" currently all point to "/docs"; update these so
each label points to its correct destination (e.g., "/docs/api", "/docs/sdks",
"/docs/webhooks") or collapse them into a single Link labeled "Documentation"
that links to "/docs" and remove or mark the other two as "Coming soon" (e.g.,
replace with non-clickable text or add a visual cue). Locate the three Link
components with the visible texts "API Reference", "SDK Libraries", and "Webhook
Events" and adjust their hrefs or their rendering accordingly while leaving the
external GitHub anchor unchanged.
In `@apps/marketing/src/lib/metadata.ts`:
- Line 29: The page title is being suffixed twice because createMetadata builds
fullTitle with " | PageSpace" (const fullTitle) while the root layout exports
siteMetadata with title.template ("%s | PageSpace"); to fix, update
createMetadata to stop appending the suffix and return the base title (let
siteMetadata.title.template apply the suffix) OR change the metadata structure
returned by createMetadata to use title: { absolute: fullTitle } so Next.js will
not reapply the template; locate the fullTitle constant and the createMetadata
return and implement one of these two fixes.
---
Duplicate comments:
In `@apps/marketing/src/app/blog/`[slug]/page.tsx:
- Around line 111-116: Replace the broken custom markdown parser with
ReactMarkdown using the remarkGfm plugin: ensure the ReactMarkdown component
(ReactMarkdown) is imported and used to render post.content with
remarkPlugins={[remarkGfm]} inside the existing div (className="prose
prose-neutral dark:prose-invert max-w-none"), remove any remaining references to
the old custom parser or its helper functions so only ReactMarkdown/remarkGfm
render markdown and Tailwind typography handles styling.
- Around line 118-124: The Share buttons have been moved into a dedicated
component but you must ensure ShareButtons is a client component that accepts
and uses the passed title prop so handlers work; update the ShareButtons
component to include "use client" at the top, accept a prop like (props: {
title: string }), use props.title for share payloads, and ensure any click
handlers and aria attributes are implemented so buttons are interactive and
accessible; verify the page passes post.title (as you already do) and remove any
duplicate/inert markup left in page.tsx.
In `@apps/marketing/src/app/docs/page.tsx`:
- Around line 207-210: The duplicate "MCP Server Setup" entry was merged into
"Setting Up MCP Servers" but ensure the rendered list uses a stable unique key
(use article.title as the React key) and that the source array (the literal with
items including "Quick Start Guide", "Understanding Page Agents", "Setting Up
MCP Servers") contains only deduplicated entries; update the array to remove the
duplicate string and verify any map/render logic uses article.title (or a
derived uniqueTitle) as the key to prevent reintroducing duplicates.
---
Nitpick comments:
In `@apps/marketing/src/app/blog/`[slug]/page.tsx:
- Around line 137-141: The CTA uses a raw anchor inside Button to navigate to
APP_URL (cross-origin) without rel attributes; update the anchor in the Button
(referencing Button and the APP_URL usage in page.tsx) to either use your
framework Link configured for external navigation or add rel="noopener" (and
optionally "noreferrer") and target="_blank" if opening in a new tab, so the
external navigation is hardened.
In `@apps/marketing/src/app/docs/getting-started/page.tsx`:
- Line 8: APP_URL is duplicated across multiple files (getting-started/page.tsx,
tour/page.tsx, integrations/page.tsx, SiteNavbar.tsx); extract it into a single
exported constant (e.g., export const APP_URL = process.env.NEXT_PUBLIC_APP_URL
|| "https://pagespace.ai") in a shared module such as config.ts or alongside
metadata.ts, then import APP_URL from that module in each of the pages and
SiteNavbar to remove duplication and prevent drift if the env var changes.
In `@apps/marketing/src/lib/schema.tsx`:
- Around line 246-257: The JsonLd component currently types its prop as data:
object | object[] which is too loose; tighten it by changing the prop type on
function JsonLd to a more explicit shape such as Record<string, unknown> |
Record<string, unknown>[] (or define a named interface/type alias like
JsonLdData = Record<string, unknown> and use JsonLdData | JsonLdData[]), update
the function signature JsonLd({ data }: { data: ... }) accordingly, and run
typechecks to ensure existing callers conform to the narrower type.
- Around line 7-8: SITE_URL and APP_URL currently share the same fallback which
causes marketing and app links to be identical in dev; update the fallback
values so they are distinct (for example use "http://localhost:3000" for
SITE_URL and "http://localhost:3001" for APP_URL) or add a clear comment above
the constants requiring NEXT_PUBLIC_MARKETING_URL and NEXT_PUBLIC_APP_URL to be
set in development; edit the SITE_URL and APP_URL declarations in schema.tsx to
use the new distinct defaults or add the explanatory comment.
| ### Learn More | ||
|
|
||
| Visit our [MCP documentation](/docs/mcp) for detailed setup guides. | ||
| `, |
There was a problem hiding this comment.
Broken internal link in blog content: /docs/mcp doesn't exist.
The "mcp-servers-explained" post's content includes a markdown link to [MCP documentation](/docs/mcp) (Line 392). This route isn't part of the marketing app, so if the content is rendered as HTML/markdown, users will hit a 404.
Suggested fix
-Visit our [MCP documentation](/docs/mcp) for detailed setup guides.
+Visit our [documentation](/docs) for detailed setup guides.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/src/app/blog/`[slug]/data.ts around lines 390 - 393, The blog
post "mcp-servers-explained" contains a broken markdown link "[MCP
documentation](/docs/mcp)" in apps/marketing/src/app/blog/[slug]/data.ts; update
that link to a valid docs route or canonical URL (e.g., replace "/docs/mcp" with
the correct internal path or full external docs URL) inside the post content
string so the rendered markdown doesn't 404. Ensure you update the exact
markdown link text "[MCP documentation](/docs/mcp)" in the post body (or use
your central docs base URL variable if available) to point to the correct
destination.
| const copyLink = async () => { | ||
| await navigator.clipboard.writeText(window.location.href); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| }; |
There was a problem hiding this comment.
Missing error handling for navigator.clipboard.writeText.
The clipboard API can reject (e.g., permissions denied, insecure context, or Firefox requiring a user gesture). The unhandled rejection will surface as a console error. Wrap in try/catch so the UI degrades gracefully.
Suggested fix
const copyLink = async () => {
- await navigator.clipboard.writeText(window.location.href);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ try {
+ await navigator.clipboard.writeText(window.location.href);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Clipboard access denied — silently ignore
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const copyLink = async () => { | |
| await navigator.clipboard.writeText(window.location.href); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }; | |
| const copyLink = async () => { | |
| try { | |
| await navigator.clipboard.writeText(window.location.href); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| } catch { | |
| // Clipboard access denied — silently ignore | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/src/app/blog/`[slug]/ShareButtons.tsx around lines 26 - 30,
The copyLink function should handle failures from navigator.clipboard.writeText
by wrapping the await call in a try/catch: catch errors and avoid throwing
unhandled rejections, setCopied only on success (call setCopied(true) inside the
try), and in the catch branch provide a graceful fallback (e.g., show an error
state or fallback to prompting the user with window.prompt or selecting an
input) and ensure setCopied(false) or no optimistic UI change occurs; update the
copyLink implementation and any UI state usage (setCopied, copied) accordingly
so clipboard permission or insecure-context errors are handled without console
errors.
| const docSections: DocSection[] = [ | ||
| { | ||
| title: "Getting Started", | ||
| description: "Learn the basics of PageSpace and set up your first workspace.", | ||
| icon: <Book className="h-5 w-5" />, | ||
| href: "/docs/getting-started", | ||
| items: [ | ||
| { title: "Quick Start Guide", href: "/docs/getting-started" }, | ||
| { title: "Creating Your First Workspace", href: "/docs/getting-started" }, | ||
| { title: "Understanding AI Agents", href: "/docs/getting-started" }, | ||
| { title: "Keyboard Shortcuts", href: "/docs/getting-started" }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "AI Features", | ||
| description: "Deep dive into AI capabilities including Page Agents and Global Assistant.", | ||
| icon: <Sparkles className="h-5 w-5" />, | ||
| href: "/docs", | ||
| items: [ | ||
| { title: "Global Assistant", href: "/docs" }, | ||
| { title: "Page Agents", href: "/docs" }, | ||
| { title: "AI Rollback", href: "/docs" }, | ||
| { title: "Custom Prompts", href: "/docs" }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "Integrations", | ||
| description: "Connect PageSpace to external tools and services.", | ||
| icon: <Zap className="h-5 w-5" />, | ||
| href: "/docs", | ||
| items: [ | ||
| { title: "MCP Overview", href: "/docs" }, | ||
| { title: "Google Calendar", href: "/docs" }, | ||
| { title: "GitHub Integration", href: "/docs" }, | ||
| { title: "Webhooks", href: "/docs" }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "MCP Servers", | ||
| description: "Connect AI to external tools with Model Context Protocol.", | ||
| icon: <Server className="h-5 w-5" />, | ||
| href: "/docs", | ||
| items: [ | ||
| { title: "What is MCP?", href: "/docs" }, | ||
| { title: "Available Servers", href: "/docs" }, | ||
| { title: "Building Custom Servers", href: "/docs" }, | ||
| { title: "Security & Permissions", href: "/docs" }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "Team & Collaboration", | ||
| description: "Learn about team features, permissions, and real-time collaboration.", | ||
| icon: <Users className="h-5 w-5" />, | ||
| href: "/docs", | ||
| items: [ | ||
| { title: "Inviting Team Members", href: "/docs" }, | ||
| { title: "Permissions & Roles", href: "/docs" }, | ||
| { title: "Channels", href: "/docs" }, | ||
| { title: "Task Assignment", href: "/docs" }, | ||
| ], | ||
| }, | ||
| { | ||
| title: "Security & Privacy", | ||
| description: "Authentication, data protection, and enterprise security features.", | ||
| icon: <Shield className="h-5 w-5" />, | ||
| href: "/docs", | ||
| items: [ | ||
| { title: "Passkeys (WebAuthn)", href: "/docs" }, | ||
| { title: "Magic Links", href: "/docs" }, | ||
| { title: "Zero Trust Architecture", href: "/docs" }, | ||
| { title: "Data Encryption", href: "/docs" }, | ||
| ], | ||
| }, | ||
| ]; |
There was a problem hiding this comment.
Nearly all doc-section item href values are placeholder /docs — misleading UX.
20 of the 24 items across docSections resolve to the same /docs destination (the current page). Clicking "Google Calendar", "Data Encryption", or "Task Assignment" all land the user back here with no useful differentiation. Consider adding a "Coming Soon" visual treatment (like the pattern used on the Getting Started page's "What's Next?" cards) so users aren't surprised by the circular navigation, or at minimum use anchor links to the corresponding section.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/src/app/docs/page.tsx` around lines 18 - 91, docSections
contains many placeholder item hrefs pointing to "/docs", causing confusing
circular navigation; update the items in the docSections array (each item's
href) to either point to meaningful anchors (e.g., "/docs#getting-started",
"/docs#google-calendar", "/docs#data-encryption") or add a "comingSoon" boolean
on items (and set href to null/undefined) and update the rendering logic to show
a non-clickable "Coming soon" treatment for items with comingSoon true; modify
the DocSection/item objects in the docSections constant and the consumer
component that renders items to respect either the anchor hrefs or the
comingSoon flag so users aren’t routed back to the same page.
| <div className="lg:w-64 w-full space-y-4"> | ||
| <h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wide">Resources</h3> | ||
| <div className="space-y-3"> | ||
| <Link | ||
| href="/docs" | ||
| className="flex items-center gap-2 text-sm hover:text-primary transition-colors" | ||
| > | ||
| <Code className="h-4 w-4" /> | ||
| API Reference | ||
| </Link> | ||
| <Link | ||
| href="/docs" | ||
| className="flex items-center gap-2 text-sm hover:text-primary transition-colors" | ||
| > | ||
| <Blocks className="h-4 w-4" /> | ||
| SDK Libraries | ||
| </Link> | ||
| <Link | ||
| href="/docs" | ||
| className="flex items-center gap-2 text-sm hover:text-primary transition-colors" | ||
| > | ||
| <Zap className="h-4 w-4" /> | ||
| Webhook Events | ||
| </Link> | ||
| <a | ||
| href="https://github.com/pagespace" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="flex items-center gap-2 text-sm hover:text-primary transition-colors" | ||
| > | ||
| <Github className="h-4 w-4" /> | ||
| GitHub Examples | ||
| </a> | ||
| </div> |
There was a problem hiding this comment.
Four distinct resource links ("API Reference", "SDK Libraries", "Webhook Events", "GitHub Examples") but three resolve to /docs.
"API Reference", "SDK Libraries", and "Webhook Events" (Lines 312–332) all route to /docs. Users clicking different labels expect different destinations. Consider collapsing these into a single "Documentation" link, or adding a visual cue that specific pages are forthcoming.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/src/app/integrations/page.tsx` around lines 309 - 342, The
three Link elements rendering "API Reference", "SDK Libraries", and "Webhook
Events" currently all point to "/docs"; update these so each label points to its
correct destination (e.g., "/docs/api", "/docs/sdks", "/docs/webhooks") or
collapse them into a single Link labeled "Documentation" that links to "/docs"
and remove or mark the other two as "Coming soon" (e.g., replace with
non-clickable text or add a visual cue). Locate the three Link components with
the visible texts "API Reference", "SDK Libraries", and "Webhook Events" and
adjust their hrefs or their rendering accordingly while leaving the external
GitHub anchor unchanged.
apps/marketing/src/lib/metadata.ts
Outdated
| keywords = [], | ||
| }: PageMetadata): Metadata { | ||
| const url = `${SITE_URL}${path}`; | ||
| const fullTitle = path === "" ? title : `${title} | ${SITE_NAME}`; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for siteMetadata usage in layout files
echo "=== Looking for siteMetadata usage in layout files ==="
rg -n 'siteMetadata' --type-add 'tsx:*.tsx' -ttsx -tts apps/marketing/src/app
echo ""
echo "=== Looking for root layout metadata export ==="
find apps/marketing/src/app -name 'layout.tsx' -maxdepth 1 -exec cat {} \;
echo ""
echo "=== Full metadata.ts file for context ==="
cat apps/marketing/src/lib/metadata.tsRepository: 2witstudios/PageSpace
Length of output: 9885
Double title suffix confirmed: createMetadata appends " | PageSpace" and siteMetadata.title.template applies "%s | PageSpace" in the root layout.
The root layout (apps/marketing/src/app/layout.tsx) exports metadata = siteMetadata, which includes template: "%s | PageSpace". When createMetadata returns a page title like "Pricing | PageSpace" on line 29, Next.js applies the template, producing "Pricing | PageSpace | PageSpace" in the <title> tag.
Resolve by either:
- Removing the manual suffix from
createMetadataand letting the template handle it, or - Using
title: { absolute: fullTitle }increateMetadatato opt out of template application per page.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/marketing/src/lib/metadata.ts` at line 29, The page title is being
suffixed twice because createMetadata builds fullTitle with " | PageSpace"
(const fullTitle) while the root layout exports siteMetadata with title.template
("%s | PageSpace"); to fix, update createMetadata to stop appending the suffix
and return the base title (let siteMetadata.title.template apply the suffix) OR
change the metadata structure returned by createMetadata to use title: {
absolute: fullTitle } so Next.js will not reapply the template; locate the
fullTitle constant and the createMetadata return and implement one of these two
fixes.
Fix broken /docs/mcp markdown link in blog data Add try/catch to clipboard API in ShareButtons Add comingSoon flag to docs placeholder items, render as non-clickable Mark SDK Libraries and Webhook Events as coming soon on integrations page Fix double title suffix: createMetadata no longer appends '| PageSpace' (layout template handles it) Extract shared APP_URL constant from metadata.ts, deduplicate across 12 files Tighten JsonLd prop type from object to Record<string, unknown> Add rel='noopener' to external CTA anchor on blog page Add dev env comment for SITE_URL/APP_URL fallback values in schema.tsx
Summary
NEXT_PUBLIC_APP_URL.well-knowndeep link scaffolding for iOS/Androidapps/web/src/app/api/contact/route.ts(moved to marketing app)Test plan
pnpm --filter marketing buildpasses cleanly/auth/signinand/auth/signupNEXT_PUBLIC_ENABLE_ONE_TAP=trueand valid client ID/og-image.png/sitemap.xml🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores