Skip to content

Docs/updates to docs layout #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,20 @@ export default defineConfig({
}),
icon(),
sitemap({
filter: (page) => !page.includes('404'),
customPages: [
'https://docs.kinde.com/'
]
filter: (page) => {
// Include all documentation pages
if (page.includes('/docs/')) return true;
// Include main pages
if (page === '' || page === '/') return true;
// Include API pages
if (page.includes('/kinde-apis/')) return true;
// Exclude admin or private pages
if (page.includes('/admin/')) return false;
return true;
},
changefreq: 'weekly',
priority: 0.7,
lastmod: new Date()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use actual content modification dates instead of build time for lastmod

Using new Date() sets the same modification date (build time) for all pages in the sitemap, which doesn't reflect actual content changes and could mislead search engines about update frequency.

Consider using actual file modification dates or the updated metadata field from your content files for more accurate lastmod values.

Apply this approach to use actual modification dates:

-      lastmod: new Date()
+      lastmod: (page) => {
+        // Use the actual modification date from content metadata if available
+        // This would require passing entry data to the sitemap generation
+        return undefined; // Let Astro determine from file system
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In astro.config.mjs at line 334, replace the static assignment of lastmod using
new Date() with dynamic values reflecting actual content modification dates.
Retrieve the last modified timestamp from each content file's metadata or file
system modification time and assign that to lastmod to ensure the sitemap
accurately represents when each page was last updated.

}),
AutoImport({
imports: [
Expand Down
20 changes: 18 additions & 2 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
User-agent: *
Allow: /

# Sitemap
Sitemap: https://docs.kinde.com/sitemap-0.xml
# Sitemap location
Sitemap: https://docs.kinde.com/sitemap-0.xml

# Crawl delay for respectful crawling
Crawl-delay: 1

# Allow all major search engines
User-agent: Googlebot
Allow: /

User-agent: Bingbot
Allow: /

User-agent: Slurp
Allow: /

# Disallow admin or private areas (if any exist in the future)
Disallow: /admin/
15 changes: 14 additions & 1 deletion src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@ export const collections = {
page_id: z.string().uuid(),
relatedArticles: z.string().array().optional().nullable(),
app_context: z.array(z.any()).optional(),
social_sharing_image_url: z.string().optional()
social_sharing_image_url: z.string().optional(),
// Enhanced metadata for SEO - made more flexible to match existing content
metadata: z.object({
topics: z.array(z.string()).optional(),
sdk: z.array(z.string()).optional(),
languages: z.array(z.string()).optional(),
audience: z.array(z.string()).optional(),
complexity: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
keywords: z.array(z.string()).optional(),
updated: z.union([z.string(), z.date()]).optional(), // Allow both string and date
featured: z.boolean().optional(),
deprecated: z.boolean().optional(),
'ai-summary': z.string().optional()
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use consistent camelCase naming for all metadata fields

The field 'ai-summary' uses kebab-case while all other metadata fields use camelCase. This inconsistency requires special handling and makes the API less uniform.

-          'ai-summary': z.string().optional()
+          aiSummary: z.string().optional()

Note: This change will require updates in other files that reference this field.

📝 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.

Suggested change
'ai-summary': z.string().optional()
aiSummary: z.string().optional()
🤖 Prompt for AI Agents
In src/content.config.ts at line 25, rename the metadata field 'ai-summary' to
use camelCase as 'aiSummary' to maintain consistent naming conventions across
all metadata fields. After renaming, update all references to this field in
other files to reflect the new camelCase name to avoid breaking changes.

}).optional()
})
})
}),
Expand Down
38 changes: 32 additions & 6 deletions src/pages/docs-suggestions.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,38 @@ export const GET: APIRoute = () => {

return new Response(
JSON.stringify(
Object.keys(docs).map((url) => ({
page_id: docs[url].frontmatter.page_id,
title: docs[url].frontmatter.title,
url: "https://docs.kinde.com" + url.split("/content/docs")[1].split(".")[0] + "/",
app_context: docs[url].frontmatter.app_context
})),
Object.keys(docs).map((url) => {
const frontmatter = docs[url].frontmatter;
const metadata = frontmatter.metadata || {};

return {
page_id: frontmatter.page_id,
title: frontmatter.title,
description: frontmatter.description,
url: "https://docs.kinde.com" + url.split("/content/docs")[1].split(".")[0] + "/",
app_context: frontmatter.app_context,
// Enhanced metadata for search indexing
topics: metadata.topics || [],
keywords: metadata.keywords || [],
audience: metadata.audience || [],
complexity: metadata.complexity,
sdk: metadata.sdk || [],
languages: metadata.languages || [],
updated: metadata.updated,
featured: metadata.featured,
deprecated: metadata.deprecated,
ai_summary: metadata['ai-summary'],
// SEO-friendly content snippets
content_preview: frontmatter.description || metadata['ai-summary'] || '',
search_terms: [
...(metadata.keywords || []),
...(metadata.topics || []),
...(metadata.audience || []),
frontmatter.title,
metadata.complexity
].filter(Boolean).join(' ')
};
}),
null,
2
)
Expand Down
114 changes: 105 additions & 9 deletions src/starlight-overrides/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,115 @@
import Default from "@astrojs/starlight/components/Head.astro";
import type {Props} from "@astrojs/starlight/props";

const { entry } = Astro.props;
const hasCustomOGImage = Astro.props.entry.data.head.find((t) =>
t.attrs.property === "og:image" ? true : false
);
---

{
import.meta.env.PUBLIC_IS_ANALYTICS_ENABLED === "true" && (
<script data-domain="kinde.com" src="https://kinde.com/js/script.tagged-events.js" defer />
)
// Extract metadata from frontmatter - handle both patterns
// Pattern 1: metadata wrapper object
// Pattern 2: fields directly in frontmatter
const metadata = entry.data.metadata || {};
const directFields = entry.data as any;

// Combine both sources, with metadata wrapper taking precedence
const keywords = metadata.keywords || directFields.keywords || [];
const topics = metadata.topics || directFields.topics || [];
const audience = metadata.audience || directFields.audience || [];
const complexity = metadata.complexity || directFields.complexity;
const updated = metadata.updated || directFields.updated;
const featured = metadata.featured || directFields.featured;
const deprecated = metadata.deprecated || directFields.deprecated;
const aiSummary = metadata['ai-summary'] || directFields['ai-summary'];

// Handle updated field - convert date to string if needed
const updatedString = updated instanceof Date ? updated.toISOString() : updated;

// Combine keywords from different sources
const allKeywords = [
...keywords,
...topics,
...audience,
complexity,
entry.data.title,
entry.data.description
].filter(Boolean);

// Create structured data for better SEO
const structuredData: any = {
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": entry.data.title,
"description": entry.data.description,
"keywords": allKeywords.join(", "),
"author": {
"@type": "Organization",
"name": "Kinde"
},
"publisher": {
"@type": "Organization",
"name": "Kinde",
"url": "https://kinde.com"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": Astro.url.href
},
"articleSection": topics.join(", "),
"articleBody": entry.data.description,
"wordCount": entry.data.description?.length || 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inaccurate wordCount - using character count instead of word count

The wordCount field is set to the character length of the description, not the actual word count. This provides misleading information to search engines.

Either calculate the actual word count or remove this field:

-  "wordCount": entry.data.description?.length || 0,
+  "wordCount": entry.data.description?.split(/\s+/).filter(Boolean).length || 0,

Or remove it entirely if an accurate count from the full content isn't available:

-  "wordCount": entry.data.description?.length || 0,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/starlight-overrides/Head.astro at line 61, the wordCount is incorrectly
calculated using the character length of the description. To fix this, replace
the current assignment with a calculation that counts the actual number of words
in the description string, for example by splitting the string on whitespace and
counting the resulting array length. If an accurate word count cannot be derived
from the description, consider removing the wordCount field entirely to avoid
misleading information.

"dateModified": updatedString,
"inLanguage": "en-US",
"isAccessibleForFree": true,
"educationalLevel": complexity || "Beginner",
"audience": {
"@type": "Audience",
"audienceType": audience.join(", ")
}
};

// Add featured/deprecated status if available
if (featured !== undefined) {
structuredData.isPartOf = {
"@type": "CreativeWork",
"isFeatured": featured
};
}

if (deprecated !== undefined) {
structuredData.isDeprecated = deprecated;
}
---

<Default {...Astro.props} />

<!-- Enhanced SEO Meta Tags -->
<meta name="keywords" content={allKeywords.join(", ")} />
<meta name="article:section" content={topics.join(", ")} />
<meta name="article:tag" content={keywords.join(", ")} />
<meta name="article:difficulty" content={complexity || "Beginner"} />
{updatedString && <meta name="article:modified_time" content={updatedString} />}
{featured && <meta name="article:featured" content={featured.toString()} />}
{deprecated && <meta name="article:deprecated" content={deprecated.toString()} />}

<!-- Enhanced Open Graph Tags -->
<meta property="og:keywords" content={allKeywords.join(", ")} />
<meta property="og:article:section" content={topics.join(", ")} />
<meta property="og:article:tag" content={keywords.join(", ")} />
{complexity && <meta property="og:article:difficulty" content={complexity} />}
{updatedString && <meta property="og:article:modified_time" content={updatedString} />}

<!-- Enhanced Twitter Card Tags -->
<meta name="twitter:description" content={entry.data.description} />
{allKeywords.length > 0 && <meta name="twitter:keywords" content={allKeywords.join(", ")} />}

<!-- Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />

<Default {...Astro.props}>
<slot />
</Default>
<!-- AI Summary for enhanced search -->
{aiSummary && <meta name="ai-summary" content={aiSummary} />}

{!hasCustomOGImage && <meta property="og:image" content={`${Astro.url.href}og-image.png`} />}
<!-- Custom OG Image if not already set -->
{!hasCustomOGImage && entry.data.social_sharing_image_url && (
<meta property="og:image" content={entry.data.social_sharing_image_url} />
)}