Community Article
Community articles are authored by SitePoint Premium contributors. Content is screened before publication, and SitePoint reserves the right to moderate or remove articles that violate our guidelines. Views expressed are those of the authors and do not necessarily reflect those of SitePoint.
Designing Search-Friendly Public Pages for a Multi-Tenant Next.js SaaS Application
SASaifullah AdenwallaPublished inAI·
August 21, 2026
·Updated:August 22, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
A multi-tenant SaaS application usually has two very different sides.
/app/dashboard/settings/projects/billingThese routes exist for authenticated users and often contain tenant-specific data.
/features/analytics/integrations/slack/solutions/agencies/customers/acme/docs/apiThese pages explain the product, document its capabilities, and help prospective users discover whether the application solves their problem.
From an engineering perspective, treating both groups of routes the same can create unnecessary problems.
Private application routes generally prioritize authentication, personalization, and interactive state. Public routes need predictable URLs, useful server-rendered content, correct metadata, sensible HTTP responses, structured internal navigation, and good performance.
This article looks at how to design that public layer in a multi-tenant Next.js SaaS application without mixing search-facing content with tenant-private application data.
Start by Separating the Public Web Surface
A useful architectural decision is to make the distinction between public and private routes explicit.
app/├── (marketing)/│ ├── page.tsx│ ├── features/│ ├── integrations/│ ├── solutions/│ └── customers/│├── docs/│└── (application)/├── dashboard/├── settings/└── projects/Route groups make the intent clear even though they don’t necessarily alter the public URL.
The marketing layout might contain:
exportdefaultfunctionMarketingLayout({children}:{children:React.ReactNode;}){return(<><PublicNavigation/><main>{children}</main><PublicFooter/></>);}The application layout can independently require authentication:
exportdefaultasyncfunctionApplicationLayout({children}:{children:React.ReactNode;}){const session =awaitgetSession();if(!session){redirect('/login');}return(<AppShelluser={session.user}>{children}</AppShell>);}The separation prevents marketing requirements from leaking unnecessarily into the application shell.
It also makes it easier to reason about which URLs should be publicly discoverable.
SitePoint has previously demonstrated the architecture of a multi-tenant SaaS application with Next.js, including the importance of tenant isolation and enforcing authorization at the backend.
That same isolation principle should remain true even when parts of the website are public.
Don’t Make Tenant Data Public Just to Create Landing Pages
Suppose each customer has a tenant identifier:
acmenorthwindglobexIt can be tempting to automatically create routes such as:
/customers/acme/customers/northwind/customers/globexdirectly from tenant records.
That can become dangerous if the public route simply reads from the same object used by the authenticated application.
Imagine the internal tenant record contains:
{id:'tenant_842',slug:'acme',companyName:'Acme',plan:'enterprise',employeeCount:740,billingEmail:'finance@acme.example',accountOwner:'user_281',internalNotes:'Migration scheduled for September',publicCaseStudy:true}A public page should not receive that entire object and decide what to hide in JSX.
Instead, create a deliberately public representation:
{slug:'acme',name:'Acme',headline:'How Acme automated reporting',summary:'...',logo:'/customers/acme.svg',publishedAt:'2026-07-11'}Private tenant model↓Public publishing layer↓Public pagePrivate tenant model↓Send everything↓Hide sensitive fields in the browserAuthorization should remain a server concern.
The frontend should never be the security boundary protecting tenant information.
Render Important Public Content Predictably
Interactive SaaS dashboards often rely heavily on client-side behavior.
A marketing page doesn’t necessarily need to.
'use client';exportdefaultfunctionFeaturePage(){const[feature, setFeature]=useState(null);useEffect(()=>{fetch('/api/features/analytics').then(response => response.json()).then(setFeature);},[]);if(!feature){return<Loading/>;}return<h1>{feature.title}</h1>;}The page may work perfectly after hydration.
But if the feature data is available at request or build time, introducing an additional browser request may be unnecessary.
A server component is simpler:
exportdefaultasyncfunctionFeaturePage(){const feature =awaitgetFeature('analytics');return(<article><h1>{feature.title}</h1><p>{feature.description}</p></article>);}The browser receives useful HTML immediately.
Server rendering can also simplify metadata generation because page data is already available during rendering.
SitePoint’s discussion of asynchronous APIs in server-rendered React explores the broader challenge of making data available during server rendering rather than waiting for browser-side execution.
This doesn’t mean every public component must be server-rendered.
Interactive pricing calculators, demos, search interfaces, or personalization widgets may still require client-side code.
Don’t make essential public content depend on client-side JavaScript unless the interaction actually requires it.
Generate Metadata From the Same
Dynamic feature and integration pages should not all inherit generic metadata.
/integrations/slack/integrations/github/integrations/notionEach route describes something different.
A dynamic route can use the same data
typeProps={params:Promise<{slug:string;}>;};exportasyncfunctiongenerateMetadata({params}:Props){const{ slug }=await params;const integration =awaitgetIntegration(slug);if(!integration){return{};}return{title:`${integration.name}Integration | Example`,description: integration.summary};}The page can use the same resource:
exportdefaultasyncfunctionIntegrationPage({params}:Props){const{ slug }=await params;const integration =awaitgetIntegration(slug);if(!integration){notFound();}return(<article><h1>{integration.name}Integration</h1><p>{integration.summary}</p></article>);}Keeping visible content and metadata connected reduces the chance of contradictory information.
Avoid metadata-generation systems that know nothing about the actual page.
Return Real 404 Responses for Invalid Slugs
Dynamic SaaS sites often accumulate URLs that look legitimate even when no re
/integrations/slack/integrations/not-a-real-productdoes not.
Don’t render a generic integration template saying “Integration unavailable” while returning 200 OK.
Use the framework’s missing-re
import{ notFound }from'next/navigation';exportdefaultasyncfunctionIntegrationPage({params}:Props){const{ slug }=await params;const integration =awaitgetIntegration(slug);if(!integration){notFound();}return<IntegrationViewdata={integration}/>;}HTTP responses are part of the API contract of a webpage.
A valid page should behave like a valid page.
The distinction matters to browsers, monitoring systems, crawlers, APIs, caches, and developers debugging <a href="https://tooltechblog.com/how-to-design-automation-triggers-that-dont-fail-silently-in-production/” title=”How to Design Automation Triggers That Don't Fail Silently in Production”>production behavior.
Give Public Re
A SaaS application may represent the same concept in several places internally.
/app/integrations/41/settings/integrations/slack/admin/catalog/slackThose URLs exist for application workflows.
The public representation should have one predictable address:
/integrations/slackThis separation lets internal routes evolve without changing externally referenced URLs.
A public URL should generally represent a re
/integrations/slack/integrations?id=41&tab=details&source=dashboardThe second URL may be perfectly appropriate inside an application.
The first is much easier to reference, share, document, redirect, and maintain as part of a public site architecture.
Handle Canonicals Deliberately
Public SaaS websites often receive tracking parameters:
/pricing?utm_source=newsletter/pricing?campaign=launch/pricing?ref=partnerThose parameters may help analytics without representing different documents.
You can expose a canonical URL:
exportconst metadata ={alternates:{canonical:'https://example.com/pricing'}};Dynamic pages can generate their canonical from validated route data:
exportasyncfunctiongenerateMetadata({params}:Props){const{ slug }=await params;const integration =awaitgetIntegration(slug);if(!integration){return{};}return{title: integration.title,alternates:{canonical:`https://example.com/integrations/${integration.slug}`}};}Be especially careful when canonical logic lives inside a shared layout.
<linkrel="canonical"href="https://example.com/">being emitted across every route can scale one configuration mistake across the entire public site.
Treat canonical generation like any other template behavior: test multiple representative routes.
Use Structured Data as Application Data
Structured data is easier to maintain when it is generated from the same application models as visible content.
Suppose an article object contains:
const article ={title:'Designing Multi-Tenant Authorization',description:'...',publishedAt:'2026-08-01',updatedAt:'2026-08-12',author:'Sam Developer'};The JSON-LD representation can be derived from it:
const schema ={'@context':'https://schema.org','@type':'Article',headline: article.title,description: article.description,datePublished: article.publishedAt,dateModified: article.updatedAt,author:{'@type':'Person',name: article.author}};<scripttype="application/ld+json"dangerouslySetInnerHTML={{__html:JSON.stringify(schema)}}/>Avoid maintaining separate hardcoded values for visible page content and structured metadata.
Updated August 12dateModified: July 3your application has created two
SitePoint’s guide to implementing JSON-LD schema markup on modern websites covers dynamic implementations for React, Next.js, and other modern stacks.
The underlying development principle is the important part:
derive machine-readable metadata from trusted application data whenever possible.
Don’t Generate Thousands of Thin Tenant Pages
Multi-tenancy can make programmatic page generation deceptively easy.
Imagine a SaaS product serves 8,000 organizations.
/customers/company-1/customers/company-2.../customers/company-8000may require very little code.
That does not mean 8,000 public pages should exist.
A page should be public because it has a useful public purpose, not because a row exists in a database.
For example, a customer story could require:
{public:true,slug:'acme',headline:'How Acme Reduced Reporting Time',summary:'...',approvedByCustomer:true}The route should use a specifically publishable resource:
const story =await db.customerStories.findFirst({where:{slug,public:true}});const tenant =await db.tenants.findFirst({where:{slug}});This creates a valuable boundary between multi-tenant application scale and public publishing scale.
JavaScript frameworks make it easy to create clickable elements.
That doesn’t mean everything clickable should be a generic container.
<divonClick={()=>router.push('/integrations/slack')}>Slack Integration</div>when the UI represents navigation.
<Linkhref="/integrations/slack">Slack Integration</Link>Semantic navigation improves expected browser behavior and makes relationships between public re
A logical public hierarchy might be:
Integrations├── Slack├── GitHub└── NotionSolutions├── Agencies├── Developers└── EnterprisesDocumentation├── Authentication├── Webhooks└── API ReferenceLinks between these reelated information
Search engines benefit from much of the same structure users do.
Keep Search Concerns Out of Private Tenant Routes
Your public architecture may need titles, canonicals, structured data, descriptive content, and navigation.
An authenticated dashboard has different priorities.
/app/company-81/private-reportdoesn’t become more useful because it has carefully optimized search metadata.
In many applications, private routes should not be publicly indexable at all.
More importantly, authentication should be the actual protection.
A robots directive is not access control.
<metaname="robots"content="noindex">to protect sensitive tenant information.
If a document is private, authorization should prevent an unauthorized HTTP request from obtaining it.
Search configuration and security solve different problems.
SEO work becomes frustrating for development teams when recommendations arrive as statements such as:
Improve crawlability.Optimize page experience.Fix metadata.Improve internal linking.Those aren’t implementation tasks.
A useful ticket describes observable behavior.
Return a real 404 for unknown /integrations/[slug] routes instead of rendering the generic integration template with 200 OK.
Generate unique canonical URLs from validated integration slugs instead of inheriting the homepage canonical from the marketing layout.
Move feature-page primary content from a client-side useEffect() request into the server-rendered route so the initial response includes the headline and description.
Whether those findings come from internal developers, an SEO specialist, or a SaaS SEO Services, they become useful to an engineering team only when they are translated into specific behaviors that can be implemented and tested.
That also gives developers the opportunity to reject recommendations that conflict with the actual architecture.
SEO shouldn’t bypass normal engineering review.
Treat Performance as Part of the Public Architecture
Marketing pages often become slower over time because they accumulate third-party JavaScript.
A public SaaS page may eventually load:
AnalyticsChat widgetA/B testingSession recordingAdvertising pixelsConsent managerPersonalizationCRM trackingEach addition may appear small in isolation.
Together they can increase JavaScript execution and interaction latency.
The development team should know which scripts are loaded, where they are loaded, and why they are needed.
Consider loading a tool only on routes that actually use it rather than globally:
exportdefaultfunctionPricingPage(){return(<><PricingCalculator/><PricingAnalytics/></>);}instead of placing every marketing script in the application root layout.
SitePoint’s current guide to Core Web Vitals and Interaction to Next Paint covers practical approaches to reducing long tasks, expensive event handlers, presentation delay, and third-party script impact.
Performance work should primarily improve the user’s experience.
Search visibility is one additional reason to take it seriously.
Test Public Routes During Deployment
Many search-related regressions are deterministic enough to test automatically.
Suppose these routes are particularly important:
const publicRoutes =['/','/pricing','/features/analytics','/integrations/slack','/docs/getting-started'];A Playwright test can verify basic expectations:
for(const route of publicRoutes){test(`${route}is a valid public page`,async({page})=>{const response =await page.goto(`https://example.com${route}`);expect(response?.status()).toBe(200);awaitexpect(page.locator('h1')).toHaveCount(1);awaitexpect(page).toHaveTitle(/.+/);});}You can inspect canonical URLs:
const canonical = page.locator('link[rel="canonical"]');awaitexpect(canonical).toHaveAttribute('href',/^https://example.com//);Or prevent an accidental production noindex:
const robots = page.locator('meta[name="robots"]');if(await robots.count()){const content =await robots.getAttribute('content');expect(content).not.toContain('noindex');}test('unknown integration returns 404',async({page})=>{const response =await page.goto('https://example.com/integrations/does-not-exist');expect(response?.status()).toBe(404);});These aren’t tests for “SEO rankings.”
They’re regression tests for application behavior.
That makes them appropriate for CI/CD.
Architecture Is the Part Developers Can Control
Search systems change continuously.
Developers cannot control exactly how an external search engine ranks a page.
They can control whether the application behaves coherently.
A well-designed public SaaS surface can guarantee that:
Important resources have stable URLs.Valid routes return successful responses.Missing routes return 404s.Public content is available through the intended rendering strategy.Metadata matches visible content.Canonical URLs are predictable.Private tenant data stays private.Navigation uses meaningful links.Structured data comes from trusted application data.Performance regressions are measurable.Those qualities are useful even without considering search engines.
They make the website easier to maintain, test, debug, share, and navigate.
Conclusion
A multi-tenant SaaS product should not treat its public website as an accidental extension of the authenticated application.
The two surfaces solve different problems.
Private routes need strong authorization, tenant isolation, and application state.
Public routes need stable re behavior, accessible navigation, performance discipline, and a clear publishing model
Next.js gives developers the primitives to build both within the same project, but the architectural boundary still needs to be intentional.
Separate public content from private tenant models. Generate metadata from real page data. Validate dynamic routes. Return correct HTTP responses. Treat structured data as part of the application model. Test critical behavior before deployment.
When those fundamentals are built into the application itself, technical search visibility becomes less of a collection of after-the-fact fixes and more of what it should be: another outcome of sound web engineering.


