Merge pull request #6118 from menloresearch/rp/jan-docs-v2-blog

adding handbook, blog, and changelog to jan docs v2
This commit is contained in:
Ramon Perez 2025-08-11 15:49:01 +10:00 committed by GitHub
commit 4d3c84f1c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
223 changed files with 9476 additions and 399 deletions

View File

@ -35,28 +35,28 @@ const socials = [
]
const menus = [
{
name: 'Product',
child: [
{
menu: 'Download',
path: '/download',
},
{
menu: 'Changelog',
path: '/changelog',
},
],
},
{
name: 'For Developers',
child: [
{
menu: 'Documentation',
path: '/docs',
},
],
},
// {
// name: 'Product',
// child: [
// {
// menu: 'Download',
// path: '/download',
// },
// {
// menu: 'Changelog',
// path: '/changelog',
// },
// ],
// },
// {
// name: 'For Developers',
// child: [
// {
// menu: 'Documentation',
// path: '/docs',
// },
// ],
// },
{
name: 'Community',
child: [
@ -71,7 +71,7 @@ const menus = [
external: true,
},
{
menu: 'Twitter',
menu: 'X/Twitter',
path: 'https://twitter.com/jandotai',
external: true,
},
@ -86,8 +86,8 @@ const menus = [
name: 'Company',
child: [
{
menu: 'About',
path: '/about',
menu: 'Menlo',
path: 'https://menlo.ai',
},
{
menu: 'Blog',
@ -158,8 +158,8 @@ export default function Footer() {
return (
<div className="flex-shrink-0 relative overflow-hidden w-full">
<div className="grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-6">
<div className="col-span-2">
<div className="grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-12">
<div className="col-span-2 lg:col-span-3">
<div className="flex items-center space-x-2 mb-3">
<LogoMark />
<h2 className="text-lg font-semibold dark:text-white text-black">
@ -209,9 +209,10 @@ export default function Footer() {
</div>
</div>
</div>
<div className="hidden lg:block lg:col-span-3"></div>
{menus.map((menu, i) => {
return (
<div key={i} className="lg:text-right">
<div key={i} className="lg:text-right lg:col-span-3">
<h2 className="mb-2 font-bold dark:text-gray-300 text-black">
{menu.name}
</h2>

View File

@ -33,10 +33,6 @@
"layout": "raw"
}
},
"about": {
"type": "page",
"title": "About"
},
"blog": {
"type": "page",
"title": "Blog",

View File

@ -1,15 +1,44 @@
// @ts-check
import { defineConfig } from 'astro/config'
import starlight from '@astrojs/starlight'
import starlightThemeRapide from 'starlight-theme-rapide'
import starlightThemeNext from 'starlight-theme-next'
// import starlightThemeRapide from 'starlight-theme-rapide'
import starlightSidebarTopics from 'starlight-sidebar-topics'
import mermaid from 'astro-mermaid'
import { fileURLToPath } from 'url'
import path, { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// https://astro.build/config
export default defineConfig({
// Deploy to the new v2 subdomain
site: 'https://v2.jan.ai',
// No 'base' property is needed, as this will be deployed to the root of the subdomain.
vite: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/components': path.resolve(__dirname, './src/components'),
'@/layouts': path.resolve(__dirname, './src/layouts'),
'@/assets': path.resolve(__dirname, './src/assets'),
'@/content': path.resolve(__dirname, './src/content'),
'@/styles': path.resolve(__dirname, './src/styles'),
'@/utils': path.resolve(__dirname, './src/utils'),
},
},
assetsInclude: [
'**/*.jpg',
'**/*.jpeg',
'**/*.png',
'**/*.gif',
'**/*.svg',
'**/*.webp',
],
optimizeDeps: {
exclude: ['@astrojs/starlight'],
},
},
integrations: [
mermaid({
theme: 'default',
@ -17,14 +46,16 @@ export default defineConfig({
}),
starlight({
title: '👋 Jan',
favicon: 'jan2.png',
plugins: [
starlightThemeRapide(),
// starlightThemeRapide(),
starlightThemeNext(),
starlightSidebarTopics(
[
{
label: 'Jan Desktop',
link: '/',
link: '/jan/',
icon: 'rocket',
items: [
{
@ -108,25 +139,10 @@ export default defineConfig({
{
label: 'Local Server',
items: [
{ label: 'Introduction', link: '/local-server/' },
{ label: 'Server Setup', slug: 'local-server/api-server' },
{
label: 'Jan Data Folder',
slug: 'local-server/data-folder',
},
{ label: 'Server Settings', slug: 'local-server/settings' },
{
label: 'Llama.cpp Server',
slug: 'local-server/llama-cpp',
},
{
label: 'Server Troubleshooting',
slug: 'local-server/troubleshooting',
},
{
label: 'Integrations',
label: 'All',
collapsed: true,
autogenerate: { directory: 'local-server/integrations' },
autogenerate: { directory: 'local-server' },
},
],
},
@ -144,17 +160,100 @@ export default defineConfig({
{
label: 'Jan Mobile',
link: '/mobile/',
badge: { text: 'Coming Soon', variant: 'caution' },
badge: { text: 'Soon', variant: 'caution' },
icon: 'phone',
items: [{ label: 'Overview', slug: 'mobile' }],
},
{
label: 'Jan Server',
link: '/server/',
badge: { text: 'Coming Soon', variant: 'caution' },
badge: { text: 'Soon', variant: 'caution' },
icon: 'forward-slash',
items: [{ label: 'Overview', slug: 'server' }],
},
{
label: 'Handbook',
link: '/handbook/',
icon: 'open-book',
items: [
{ label: 'Welcome', slug: 'handbook' },
{
label: 'About Jan',
items: [
{
label: 'Why does Jan Exist?',
collapsed: true,
autogenerate: { directory: 'handbook/why' },
},
{
label: 'How we make Money',
collapsed: true,
autogenerate: { directory: 'handbook/money' },
},
{
label: 'Who We Hire',
collapsed: true,
autogenerate: { directory: 'handbook/who' },
},
{
label: "Jan's Philosophies",
collapsed: true,
autogenerate: { directory: 'handbook/philosophy' },
},
{
label: 'Brand & Identity',
collapsed: true,
autogenerate: { directory: 'handbook/brand' },
},
],
},
{
label: 'How We Work',
items: [
{
label: 'Team Roster',
collapsed: true,
autogenerate: { directory: 'handbook/team' },
},
{
label: "Jan's Culture",
collapsed: true,
autogenerate: { directory: 'handbook/culture' },
},
{
label: 'How We Build',
collapsed: true,
autogenerate: { directory: 'handbook/how' },
},
{
label: 'How We Sell',
collapsed: true,
autogenerate: { directory: 'handbook/sell' },
},
],
},
{
label: 'HR',
items: [
{
label: 'HR Lifecycle',
collapsed: true,
autogenerate: { directory: 'handbook/lifecycle' },
},
{
label: 'HR Policies',
collapsed: true,
autogenerate: { directory: 'handbook/hr' },
},
{
label: 'Compensation',
collapsed: true,
autogenerate: { directory: 'handbook/comp' },
},
],
},
],
},
],
{
exclude: [

View File

@ -14,8 +14,10 @@
"sharp": "^0.34.3",
"starlight-openapi": "^0.19.1",
"starlight-sidebar-topics": "^0.6.0",
"starlight-theme-next": "^0.3.2",
"starlight-theme-rapide": "^0.5.1",
"starlight-videos": "^0.3.0",
"unist-util-visit": "^5.0.0",
},
},
},
@ -1076,6 +1078,8 @@
"starlight-sidebar-topics": ["starlight-sidebar-topics@0.6.0", "", { "dependencies": { "picomatch": "^4.0.2" }, "peerDependencies": { "@astrojs/starlight": ">=0.32.0" } }, "sha512-ysmOR7zaHYKtk18/mpW4MbEMDioR/ZBsisu9bdQrq0v9BlHWpW7gAdWlqFWO9zdv1P7l0Mo1WKd0wJ0UtqOVEQ=="],
"starlight-theme-next": ["starlight-theme-next@0.3.2", "", { "peerDependencies": { "@astrojs/starlight": ">=0.34" } }, "sha512-GQGhZ67nZ09pWVQoecl1N+H/1EUkUOvLVpjqOCHlkSotCblwrWrj4guEsdF9aKkNqiyTi6zzwZ5sxQospvdHOg=="],
"starlight-theme-rapide": ["starlight-theme-rapide@0.5.1", "", { "peerDependencies": { "@astrojs/starlight": ">=0.34.0" } }, "sha512-QRF6mzcYHLEX5UpUvOPXVVwISS298siIJLcKextoMLhXcnF12nX+IYJ0LNxFk9XaPbX9uDXIieSBJf5Pztkteg=="],
"starlight-videos": ["starlight-videos@0.3.0", "", { "dependencies": { "@astro-community/astro-embed-youtube": "^0.5.6", "hastscript": "^9.0.0", "iso8601-duration": "^2.1.2", "srt-parser-2": "^1.2.3", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@astrojs/starlight": ">=0.34.0" } }, "sha512-1yvFUEn3P+ZjuGr5COswQp14cZdIvsGjg9lqDIyW5clCrZaBiDMSNPLYngyQozaDbrublEp6/V9HbJR6sGnSOA=="],

View File

@ -20,8 +20,10 @@
"sharp": "^0.34.3",
"starlight-openapi": "^0.19.1",
"starlight-sidebar-topics": "^0.6.0",
"starlight-theme-next": "^0.3.2",
"starlight-theme-rapide": "^0.5.1",
"starlight-videos": "^0.3.0"
"starlight-videos": "^0.3.0",
"unist-util-visit": "^5.0.0"
},
"packageManager": "yarn@1.22.22"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,140 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const blogDir = path.join(__dirname, '..', 'src', 'content', 'blog')
// Function to convert filename to a valid JavaScript variable name
function toVariableName(filename) {
// Remove extension and special characters, convert to camelCase
const base = path.basename(filename, path.extname(filename))
let varName = base
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
.replace(/[^a-zA-Z0-9]/g, '')
.replace(/^./, (c) => c.toLowerCase())
// If the variable name starts with a number, prefix with 'img'
if (/^[0-9]/.test(varName)) {
varName = 'img' + varName.charAt(0).toUpperCase() + varName.slice(1)
}
return varName
}
// Function to process a single MDX file
function processMDXFile(filePath) {
console.log(`Processing: ${filePath}`)
let content = fs.readFileSync(filePath, 'utf-8')
// Find all image references
const imageRegex = /!\[([^\]]*)\]\((\.\/_assets\/[^)]+)\)/g
const images = []
let match
while ((match = imageRegex.exec(content)) !== null) {
const altText = match[1]
const imagePath = match[2]
const filename = path.basename(imagePath)
const varName = toVariableName(filename) + 'Img'
// Check if we already have this image
if (!images.find((img) => img.varName === varName)) {
images.push({
varName,
path: imagePath,
altText,
originalMatch: match[0],
})
}
}
if (images.length === 0) {
console.log(` No images found in ${path.basename(filePath)}`)
return
}
console.log(` Found ${images.length} images`)
// Find where to insert imports (after existing imports or frontmatter)
const frontmatterEnd = content.indexOf('---', content.indexOf('---') + 3) + 3
let importInsertPosition = frontmatterEnd
// Check if there are already imports
const existingImportRegex = /^import\s+.*$/gm
const imports = content.match(existingImportRegex)
if (imports && imports.length > 0) {
// Find the last import
const lastImport = imports[imports.length - 1]
importInsertPosition = content.indexOf(lastImport) + lastImport.length
}
// Generate import statements
const importStatements = images
.map((img) => `import ${img.varName} from '${img.path}';`)
.join('\n')
// Insert imports
if (imports && imports.length > 0) {
// Add to existing imports
content =
content.slice(0, importInsertPosition) +
'\n' +
importStatements +
content.slice(importInsertPosition)
} else {
// Add new import section after frontmatter
content =
content.slice(0, frontmatterEnd) +
'\n\n' +
importStatements +
'\n' +
content.slice(frontmatterEnd)
}
// Replace all image references with JSX img tags
images.forEach((img) => {
// Create regex for this specific image
const specificImageRegex = new RegExp(
`!\\[([^\\]]*)\\]\\(${img.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`,
'g'
)
content = content.replace(specificImageRegex, (match, altText) => {
return `<img src={${img.varName}.src} alt="${altText || img.altText}" />`
})
})
// Write the updated content back
fs.writeFileSync(filePath, content)
console.log(` ✓ Updated ${path.basename(filePath)}`)
}
// Process all MDX files in the blog directory
function processAllBlogPosts() {
const files = fs.readdirSync(blogDir)
const mdxFiles = files.filter((file) => file.endsWith('.mdx'))
console.log(`Found ${mdxFiles.length} MDX files in blog directory\n`)
mdxFiles.forEach((file) => {
const filePath = path.join(blogDir, file)
try {
processMDXFile(filePath)
} catch (error) {
console.error(`Error processing ${file}:`, error.message)
}
})
console.log('\n✨ All blog posts processed!')
}
// Run the script
processAllBlogPosts()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

View File

@ -0,0 +1,230 @@
---
export interface Props {
src: string;
alt: string;
caption?: string;
width?: number;
height?: number;
loading?: 'lazy' | 'eager';
class?: string;
}
const {
src,
alt,
caption,
width,
height,
loading = 'lazy',
class: className = ''
} = Astro.props;
// Handle different image path formats
let imageSrc = src;
// If the path starts with ./ or ../, it's a relative path from the MDX file
if (src.startsWith('./') || src.startsWith('../')) {
// Remove the leading ./ or ../
imageSrc = src.replace(/^\.\.?\//, '');
// Prepend the blog content path if it doesn't include it
if (!imageSrc.includes('/content/blog/')) {
imageSrc = `/src/content/blog/${imageSrc}`;
}
} else if (!src.startsWith('http') && !src.startsWith('/')) {
// For paths without ./ prefix, assume they're relative to blog content
imageSrc = `/src/content/blog/${src}`;
}
---
<figure class={`blog-image-container ${className}`}>
<img
src={imageSrc}
alt={alt}
width={width}
height={height}
loading={loading}
class="blog-image"
/>
{caption && (
<figcaption class="blog-image-caption">{caption}</figcaption>
)}
</figure>
<style>
.blog-image-container {
margin: 2rem 0;
text-align: center;
width: 100%;
}
.blog-image {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
border-radius: 0.75rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
border: 1px solid var(--sl-color-gray-5);
background: var(--sl-color-bg);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.blog-image:hover {
transform: scale(1.01);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.blog-image-caption {
margin-top: 0.75rem;
font-size: 0.875rem;
color: var(--sl-color-text-muted);
font-style: italic;
line-height: 1.5;
padding: 0 1rem;
}
/* Loading state */
.blog-image[loading="lazy"] {
background: linear-gradient(
90deg,
var(--sl-color-gray-5) 0%,
var(--sl-color-gray-4) 50%,
var(--sl-color-gray-5) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Dark mode adjustments */
:global(.dark) .blog-image {
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -1px rgba(0, 0, 0, 0.2);
border-color: var(--sl-color-gray-6);
}
:global(.dark) .blog-image:hover {
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.4),
0 4px 6px -2px rgba(0, 0, 0, 0.2);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.blog-image-container {
margin: 1.5rem 0;
}
.blog-image {
border-radius: 0.5rem;
}
.blog-image-caption {
font-size: 0.8125rem;
padding: 0 0.5rem;
}
}
@media (max-width: 480px) {
.blog-image-container {
margin: 1rem 0;
}
.blog-image {
border-radius: 0.375rem;
}
}
/* Full-width variant */
.blog-image-container.full-width {
margin-left: calc(-50vw + 50%);
margin-right: calc(-50vw + 50%);
max-width: 100vw;
}
.blog-image-container.full-width .blog-image {
border-radius: 0;
max-width: min(100%, 1400px);
}
/* Inline variant for small icons/images */
.blog-image-container.inline {
display: inline-block;
margin: 0 0.25rem;
vertical-align: middle;
}
.blog-image-container.inline .blog-image {
display: inline-block;
max-height: 1.5em;
width: auto;
border: none;
box-shadow: none;
border-radius: 0;
margin: 0;
}
.blog-image-container.inline .blog-image:hover {
transform: none;
box-shadow: none;
}
/* Gallery variant */
.blog-image-container.gallery {
display: inline-block;
width: calc(50% - 0.5rem);
margin: 0.5rem 0.25rem;
}
@media (max-width: 768px) {
.blog-image-container.gallery {
width: 100%;
margin: 1rem 0;
}
}
/* Error state */
.blog-image.error {
background: var(--sl-color-gray-5);
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.blog-image.error::after {
content: '🖼️ Image not found';
position: absolute;
color: var(--sl-color-text-muted);
font-size: 0.875rem;
}
</style>
<script>
// Handle image loading errors
document.addEventListener('DOMContentLoaded', () => {
const images = document.querySelectorAll('.blog-image');
images.forEach(img => {
img.addEventListener('error', function() {
this.classList.add('error');
this.alt = 'Image could not be loaded';
});
});
});
</script>

View File

@ -0,0 +1,87 @@
---
export interface Props {
title?: string;
description?: string;
buttonText?: string;
buttonLink?: string;
variant?: 'primary' | 'secondary' | 'gradient';
align?: 'left' | 'center' | 'right';
}
const {
title = "Ready to get started?",
description = "Download Jan and start running AI models locally on your device.",
buttonText = "Download Jan",
buttonLink = "https://jan.ai",
variant = 'primary',
align = 'center'
} = Astro.props;
const variantClasses = {
primary: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
secondary: 'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800',
gradient: 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border-purple-200 dark:border-purple-800'
};
const alignClasses = {
left: 'text-left',
center: 'text-center',
right: 'text-right'
};
const buttonVariantClasses = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-200 dark:hover:bg-gray-300 text-white dark:text-gray-900',
gradient: 'bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white'
};
---
<div class={`cta-blog ${variantClasses[variant]} ${alignClasses[align]} border rounded-xl p-6 lg:p-8 my-8`}>
<div class="max-w-2xl mx-auto">
{title && (
<h3 class="text-2xl lg:text-3xl font-bold mb-3 text-gray-900 dark:text-gray-100">
{title}
</h3>
)}
{description && (
<p class="text-gray-700 dark:text-gray-300 mb-6 text-lg">
{description}
</p>
)}
<a
href={buttonLink}
class={`inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium transition-all duration-200 ${buttonVariantClasses[variant]} shadow-sm hover:shadow-md`}
target="_blank"
rel="noopener noreferrer"
>
{buttonText}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
</div>
<style>
.cta-blog {
position: relative;
overflow: hidden;
}
.cta-blog::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
pointer-events: none;
}
:global(.dark) .cta-blog::before {
background: radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%);
}
</style>

View File

@ -0,0 +1,85 @@
---
export interface Props {
type?: 'info' | 'warning' | 'error' | 'success' | 'note';
emoji?: string;
children?: any;
}
const { type = 'note', emoji } = Astro.props;
const typeConfig = {
info: {
bgColor: 'bg-blue-50 dark:bg-blue-900/20',
borderColor: 'border-blue-200 dark:border-blue-800',
textColor: 'text-blue-900 dark:text-blue-200',
defaultEmoji: ''
},
warning: {
bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
borderColor: 'border-yellow-200 dark:border-yellow-800',
textColor: 'text-yellow-900 dark:text-yellow-200',
defaultEmoji: '⚠️'
},
error: {
bgColor: 'bg-red-50 dark:bg-red-900/20',
borderColor: 'border-red-200 dark:border-red-800',
textColor: 'text-red-900 dark:text-red-200',
defaultEmoji: '🚨'
},
success: {
bgColor: 'bg-green-50 dark:bg-green-900/20',
borderColor: 'border-green-200 dark:border-green-800',
textColor: 'text-green-900 dark:text-green-200',
defaultEmoji: '✅'
},
note: {
bgColor: 'bg-gray-50 dark:bg-gray-900/20',
borderColor: 'border-gray-200 dark:border-gray-800',
textColor: 'text-gray-900 dark:text-gray-200',
defaultEmoji: '📝'
}
};
const config = typeConfig[type] || typeConfig.note;
const displayEmoji = emoji || config.defaultEmoji;
---
<div class={`callout ${config.bgColor} ${config.borderColor} ${config.textColor} border rounded-lg p-4 my-4`}>
<div class="flex items-start gap-3">
<span class="text-xl flex-shrink-0" aria-hidden="true">{displayEmoji}</span>
<div class="callout-content flex-1">
<slot />
</div>
</div>
</div>
<style>
.callout {
font-size: 0.9375rem;
line-height: 1.6;
}
.callout-content :global(p) {
margin: 0;
}
.callout-content :global(p:not(:last-child)) {
margin-bottom: 0.5rem;
}
.callout-content :global(a) {
text-decoration: underline;
font-weight: 500;
}
.callout-content :global(code) {
font-size: 0.875em;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
background-color: rgba(0, 0, 0, 0.05);
}
:global(.dark) .callout-content :global(code) {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

View File

@ -0,0 +1,36 @@
---
export interface Props {
title: string;
date: string;
ogImage?: string;
}
const { title, date, ogImage } = Astro.props;
// Format the date nicely
const formattedDate = new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
---
<header class="changelog-header mb-8">
{ogImage && (
<div class="mt-6">
<img
src={ogImage}
alt={title}
class="w-full rounded-lg shadow-lg"
loading="lazy"
/>
</div>
)}
</header>
<style>
.changelog-header {
border-bottom: 1px solid var(--sl-color-gray-5);
padding-bottom: 2rem;
}
</style>

View File

@ -4,6 +4,17 @@
import Search from '@astrojs/starlight/components/Search.astro';
import ThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro';
import { Icon } from '@astrojs/starlight/components';
// Determine if we're on a docs page based on the current path
const currentPath = Astro.url.pathname;
const isDocsPage = currentPath.startsWith('/jan/') ||
currentPath.startsWith('/mobile/') ||
currentPath.startsWith('/server/') ||
currentPath.startsWith('/local-server/') ||
currentPath === '/' ||
currentPath === '/index' ||
currentPath === '/docs' ||
currentPath === '/docs/';
---
<div class="sl-nav-wrapper">
@ -11,18 +22,24 @@ import { Icon } from '@astrojs/starlight/components';
<!-- Left side with title and links -->
<div class="sl-nav__left">
<!-- Site title/logo -->
<a href="/" class="sl-nav__title">
<a href="https://jan.ai" class="sl-nav__title">
👋 Jan
</a>
<!-- Main navigation links -->
<div class="sl-nav__links">
<a href="/products" class="sl-nav__link">
<!-- Main navigation links - hide on mobile for docs pages -->
<div class={`sl-nav__links ${isDocsPage ? 'docs-page' : 'custom-page'}`}>
<a href="/products" class="sl-nav__link" data-nav-item="products">
Products
</a>
<a href="/docs/" class="sl-nav__link">
<a href="/jan" class="sl-nav__link" data-nav-item="docs">
Docs
</a>
<a href="/handbook" class="sl-nav__link" data-nav-item="handbook">
Handbook
</a>
<a href="/blog" class="sl-nav__link" data-nav-item="blog">
Blog
</a>
</div>
</div>
@ -33,82 +50,113 @@ import { Icon } from '@astrojs/starlight/components';
</div>
</div>
<!-- Right side items (API Reference, theme toggle, social links, etc.) -->
<!-- Right side items -->
<div class="sl-nav__end">
<!-- API Reference moved to right -->
<a href="/api-reference" class="sl-nav__link">
<!-- Changelog and API Reference links -->
<a href="/changelog" class={`sl-nav__link sl-nav__api-link ${isDocsPage ? 'docs-page' : 'custom-page'}`} data-nav-item="changelog">
Changelog
</a>
<a href="/api-reference" class={`sl-nav__link sl-nav__api-link ${isDocsPage ? 'docs-page' : 'custom-page'}`} data-nav-item="api">
API Reference
</a>
<!-- Theme toggle -->
<ThemeSelect />
<!-- Theme toggle - always visible on desktop -->
<div class="sl-nav__theme">
<ThemeSelect />
</div>
<!-- Social links -->
<div class="sl-nav__social">
<a href="https://github.com/menloresearch/jan" class="sl-nav__social-link" aria-label="GitHub">
<Icon name="github"/>
</a>
</div>
<div class="sl-nav__social">
<a href="https://twitter.com/jandotai" class="sl-nav__social-link" aria-label="X">
<Icon name="x.com"/>
<Icon name="x.com"/>
</a>
</div>
<div class="sl-nav__social">
<a href="https://discord.com/invite/FTk2MvZwJH" class="sl-nav__social-link" aria-label="Discord">
<Icon name="discord"/>
<Icon name="discord"/>
</a>
</div>
<!-- Mobile hamburger menu - only for custom pages -->
{!isDocsPage && (
<div class="sl-nav__hamburger">
<button id="hamburger-btn" class="hamburger-button" aria-label="Toggle navigation menu">
<span class="hamburger-icon">☰</span>
</button>
<div id="hamburger-menu" class="hamburger-menu">
<a href="/products" class="hamburger-link">Products</a>
<a href="/jan" class="hamburger-link">Docs</a>
<a href="/handbook" class="hamburger-link">Handbook</a>
<a href="/blog" class="hamburger-link">Blog</a>
<a href="/changelog" class="hamburger-link">Changelog</a>
<a href="/api-reference" class="hamburger-link">API Reference</a>
<div class="hamburger-social">
<a href="https://github.com/menloresearch/jan" class="hamburger-social-link" aria-label="GitHub">
<Icon name="github"/>
</a>
<a href="https://twitter.com/jandotai" class="hamburger-social-link" aria-label="X">
<Icon name="x.com"/>
</a>
<a href="https://discord.com/invite/FTk2MvZwJH" class="hamburger-social-link" aria-label="Discord">
<Icon name="discord"/>
</a>
</div>
<div class="hamburger-theme">
<ThemeSelect />
</div>
</div>
</div>
)}
</div>
</nav>
</div>
<style>
/* Base navigation styles */
.sl-nav-wrapper {
background: var(--sl-color-bg-nav);
border-bottom: 1px solid var(--sl-color-hairline-shade);
position: fixed;
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1000;
padding: 0;
margin: 0;
z-index: 999; /* High z-index to stay above ToC and other elements */
background: var(--sl-color-bg-nav);
border-bottom: 1px solid var(--sl-color-hairline);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.sl-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
width: 100%;
max-width: 100%;
margin: 0 auto;
height: var(--sl-nav-height, 3.5rem);
}
.sl-nav__center {
display: flex;
align-items: center;
flex: 1;
display: flex;
justify-content: center;
max-width: 500px;
margin: 0 2rem;
max-width: 600px;
margin: 0 1rem;
}
.sl-nav__left {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 1.5rem;
}
.sl-nav__title {
color: var(--sl-color-text);
text-decoration: none;
font-size: 1.25rem;
font-size: 1.125rem;
font-weight: 600;
line-height: 1;
margin: 0;
padding: 0;
color: var(--sl-color-white);
text-decoration: none;
display: flex;
align-items: center;
white-space: nowrap;
}
.sl-nav__title:hover {
@ -118,40 +166,44 @@ import { Icon } from '@astrojs/starlight/components';
.sl-nav__links {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: 0.5rem;
gap: 1.5rem;
}
.sl-nav__link {
color: var(--sl-color-text);
color: var(--sl-color-gray-3);
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
transition: all 0.15s ease;
font-size: 0.9375rem;
transition: color 0.2s ease;
white-space: nowrap;
}
.sl-nav__link:hover {
color: #000 !important;
background: var(--sl-color-bg-accent);
color: var(--sl-color-white);
text-decoration: none;
}
/* Dark mode hover text should be white for contrast */
html[data-theme="dark"] .sl-nav__link:hover {
color: #fff !important;
/* Light theme adjustments - better contrast */
html[data-theme="light"] .sl-nav__link {
color: #374151; /* Darker gray for better readability */
}
/* Active state styling for better contrast */
html[data-theme="light"] .sl-nav__link:hover {
color: var(--sl-color-accent);
}
html[data-theme="light"] .sl-nav__title {
color: #111827; /* Very dark gray, almost black */
}
/* Active link styling */
.sl-nav__link.active {
color: #000 !important;
background: var(--sl-color-bg-accent) !important;
font-weight: 600 !important;
color: var(--sl-color-text-accent);
font-weight: 500;
}
/* Dark mode active state */
html[data-theme="dark"] .sl-nav__link.active {
color: #fff !important;
/* Light theme active link */
html[data-theme="light"] .sl-nav__link.active {
color: var(--sl-color-text-accent);
}
.sl-nav__end {
@ -161,16 +213,39 @@ import { Icon } from '@astrojs/starlight/components';
}
.sl-nav__search {
display: flex;
align-items: center;
width: 100%;
min-width: 300px;
max-width: 400px;
position: relative;
}
/* Make the actual search input wider */
/* Fix search input styling for light mode */
.sl-nav__search :global(input) {
min-width: 300px !important;
width: 100% !important;
width: 100%;
padding: 0.5rem 1rem;
}
/* Fix search bar in light mode with more specific selectors */
html[data-theme="light"] .sl-nav__search :global(.pagefind-ui__search-input),
html[data-theme="light"] .sl-nav__search :global(input[type="search"]),
html[data-theme="light"] .sl-nav__search :global(input) {
background: #f9fafb !important;
background-color: #f9fafb !important;
border: 1px solid #d1d5db !important;
color: #111827 !important;
}
html[data-theme="light"] .sl-nav__search :global(.pagefind-ui__search-input:focus),
html[data-theme="light"] .sl-nav__search :global(input[type="search"]:focus),
html[data-theme="light"] .sl-nav__search :global(input:focus) {
background: #ffffff !important;
background-color: #ffffff !important;
border-color: var(--sl-color-accent) !important;
color: #111827 !important;
}
html[data-theme="light"] .sl-nav__search :global(.pagefind-ui__search-input::placeholder),
html[data-theme="light"] .sl-nav__search :global(input::placeholder) {
color: #9ca3af !important;
}
.sl-nav__social {
@ -180,143 +255,407 @@ import { Icon } from '@astrojs/starlight/components';
}
.sl-nav__social-link {
color: var(--sl-color-text-muted);
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
transition: all 0.15s ease;
padding: 0.375rem;
color: var(--sl-color-gray-3);
transition: color 0.2s ease;
text-decoration: none;
}
.sl-nav__social-link:hover {
color: var(--sl-color-text);
background: var(--sl-color-bg-accent);
color: var(--sl-color-white);
text-decoration: none;
}
/* Hide links on mobile, show hamburger menu instead */
@media (max-width: 768px) {
/* Light theme social links - better contrast */
html[data-theme="light"] .sl-nav__social-link {
color: #6b7280; /* Medium gray for icons */
}
html[data-theme="light"] .sl-nav__social-link:hover {
color: var(--sl-color-accent);
}
/* Theme selector container */
.sl-nav__theme {
display: flex;
align-items: center;
}
/* Hide only the "Select theme" label text, keep dropdown visible */
.sl-nav__theme :global(starlight-theme-select) {
position: relative;
}
/* Hide only the screen reader text */
.sl-nav__theme :global(starlight-theme-select .sr-only) {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
/* Style the select dropdown with visible text */
.sl-nav__theme :global(starlight-theme-select select) {
font-size: 0.875rem !important;
color: var(--sl-color-text) !important;
background: transparent !important;
border: none !important;
cursor: pointer !important;
padding-right: 1.5rem !important;
}
/* Ensure options are visible with proper contrast */
.sl-nav__theme :global(starlight-theme-select option) {
font-size: 0.875rem !important;
color: var(--sl-color-black) !important;
background: var(--sl-color-white) !important;
}
/* Light mode select text color */
html[data-theme="light"] .sl-nav__theme :global(starlight-theme-select select) {
color: var(--sl-color-gray-5) !important;
}
/* Hide the icon to prevent overlap with text */
.sl-nav__theme :global(starlight-theme-select svg) {
display: none;
}
/* Hamburger menu styles */
.sl-nav__hamburger {
display: none;
position: relative; /* Changed from static to contain the menu properly */
}
.hamburger-button {
background: transparent;
border: none;
color: var(--sl-color-white);
font-size: 1.5rem;
cursor: pointer;
padding: 0.375rem;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
}
/* Fix hamburger button visibility in light mode */
html[data-theme="light"] .hamburger-button {
color: #374151; /* Darker gray for better visibility */
}
.hamburger-button:hover {
color: var(--sl-color-text-accent);
}
html[data-theme="light"] .hamburger-button:hover {
color: var(--sl-color-text-accent);
}
.hamburger-menu {
display: none;
position: absolute;
top: calc(100% + 0.5rem); /* Add small gap below navbar */
right: 0;
background: var(--sl-color-bg-nav);
border: 1px solid var(--sl-color-hairline);
border-radius: 0.5rem;
padding: 1rem;
min-width: 200px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000; /* Ensure menu stays above other content */
}
/* Light mode menu background */
html[data-theme="light"] .hamburger-menu {
background: var(--sl-color-bg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.hamburger-menu.show {
display: block;
}
.hamburger-link {
display: block;
padding: 0.5rem 1rem;
color: var(--sl-color-gray-3);
text-decoration: none;
transition: color 0.2s ease, background 0.2s ease;
border-radius: 0.25rem;
}
/* Light mode hamburger links */
html[data-theme="light"] .hamburger-link {
color: #374151; /* Consistent with nav links */
}
.hamburger-link:hover {
color: var(--sl-color-white);
background: var(--sl-color-gray-6);
}
html[data-theme="light"] .hamburger-link:hover {
color: var(--sl-color-accent);
background: var(--sl-color-gray-6);
}
.hamburger-social {
display: flex;
gap: 0.5rem;
padding: 0.5rem 1rem;
margin-top: 0.5rem;
border-top: 1px solid var(--sl-color-hairline);
}
.hamburger-social-link {
display: flex;
align-items: center;
padding: 0.375rem;
color: var(--sl-color-gray-3);
transition: color 0.2s ease;
text-decoration: none;
}
html[data-theme="light"] .hamburger-social-link {
color: #6b7280; /* Consistent with nav social links */
}
.hamburger-social-link:hover {
color: var(--sl-color-white);
}
html[data-theme="light"] .hamburger-social-link:hover {
color: var(--sl-color-accent);
}
.hamburger-theme {
padding: 0.5rem 1rem;
margin-top: 0.5rem;
border-top: 1px solid var(--sl-color-hairline);
}
/* Search component integration styles */
.sl-nav__search :global(.pagefind-ui) {
--pagefind-ui-scale: 0.9;
}
.sl-nav__search :global(.pagefind-ui__search-input) {
background: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
color: var(--sl-color-white);
border-radius: 0.375rem;
font-size: 0.875rem;
padding: 0.375rem 1rem;
transition: border-color 0.2s ease, background 0.2s ease;
}
/* Override any Starlight defaults for search in dark mode */
html[data-theme="dark"] .sl-nav__search :global(.pagefind-ui__search-input),
html[data-theme="dark"] .sl-nav__search :global(input[type="search"]),
html[data-theme="dark"] .sl-nav__search :global(input) {
background: #1f2937 !important;
background-color: #1f2937 !important;
border: 1px solid #374151 !important;
color: #f3f4f6 !important;
}
.sl-nav__search :global(.pagefind-ui__search-input:hover) {
border-color: var(--sl-color-gray-4);
background: var(--sl-color-gray-5);
}
.sl-nav__search :global(.pagefind-ui__search-input:focus) {
outline: none;
border-color: var(--sl-color-text-accent);
background: var(--sl-color-bg);
}
.sl-nav__search :global(.pagefind-ui__search-input::placeholder) {
color: var(--sl-color-gray-3);
}
/* Responsive styles */
/* Tablet view */
@media (max-width: 900px) {
.sl-nav__links {
display: none;
gap: 1rem;
}
.sl-nav__left {
gap: 0.25rem;
flex-shrink: 0;
.sl-nav__link {
font-size: 0.875rem;
}
.sl-nav__social {
display: none;
}
}
/* Mobile view - differentiate between docs and custom pages */
@media (max-width: 768px) {
.sl-nav {
padding: 0.5rem 1rem;
}
.sl-nav__center {
margin: 0 0.5rem;
max-width: 200px;
flex-shrink: 1;
}
.sl-nav__end {
gap: 0.25rem;
flex-shrink: 0;
}
.sl-nav__search {
min-width: 150px;
}
.sl-nav__search :global(input) {
min-width: 150px !important;
}
/* Hide API Reference on very small screens */
.sl-nav__end > .sl-nav__link {
/* For docs pages - hide custom nav items on mobile, let Starlight handle it */
.sl-nav__links.docs-page {
display: none;
}
/* Make social links smaller on mobile */
.sl-nav__social-link {
width: 1.5rem;
height: 1.5rem;
.sl-nav__api-link.docs-page {
display: none;
}
/* For custom pages - hide nav items but show hamburger */
.sl-nav__links.custom-page {
display: none;
}
.sl-nav__api-link.custom-page {
display: none;
}
.sl-nav__theme {
display: none;
}
/* Show hamburger only for custom pages */
.sl-nav__hamburger {
display: block;
}
}
/* Responsive adjustments */
/* Small mobile */
@media (max-width: 640px) {
.sl-nav {
padding: 0.5rem 0.75rem;
gap: 0.5rem;
}
.sl-nav__left {
gap: 0.5rem;
}
.sl-nav__center {
flex: 1;
margin: 0 0.5rem;
}
.sl-nav__end {
gap: 0.5rem;
}
.sl-nav__title {
font-size: 1rem;
white-space: nowrap;
}
.sl-nav__left {
gap: 0.125rem;
}
.sl-nav__center {
margin: 0 0.25rem;
max-width: 150px;
}
.sl-nav__search {
min-width: 120px;
}
.sl-nav__search :global(input) {
min-width: 120px !important;
font-size: 0.875rem;
}
.sl-nav__end {
gap: 0.125rem;
flex-shrink: 0;
min-width: fit-content;
}
/* Hide some social links on very small screens to make room */
.sl-nav__social:nth-child(n+2) {
display: none;
}
/* Hide theme toggle on very small screens */
:global(starlight-theme-select) {
display: none !important;
.sl-nav__search :global(.pagefind-ui__search-input) {
padding: 0.25rem 0.75rem;
font-size: 0.8125rem;
}
}
/* Ensure page content doesn't hide behind fixed navbar */
/* Make sure content doesn't jump */
:global(body) {
padding-top: var(--sl-nav-height, 3.5rem) !important;
padding-top: 0;
}
/* Override any conflicting Starlight body padding */
:global(.sl-layout) {
padding-top: 0 !important;
padding-top: 0;
}
/* Reduce top margin on main content area */
/* Adjust main content positioning */
:global(main.sl-content) {
margin-top: 0 !important;
padding-top: 0.5rem !important;
margin-top: 0;
}
/* Target the main content wrapper */
:global(.sl-content__inner) {
padding-top: 0 !important;
padding-top: 1rem;
}
</style>
<script>
// Add active state highlighting based on current page
const currentPath = window.location.pathname;
const links = document.querySelectorAll('.sl-nav__link');
document.addEventListener('DOMContentLoaded', function() {
// Add active state highlighting based on current page
const currentPath = window.location.pathname;
const links = document.querySelectorAll('.sl-nav__link');
links.forEach(link => {
const href = link.getAttribute('href');
if (href && currentPath.startsWith(href)) {
// Add active class for CSS styling
link.classList.add('active');
links.forEach(link => {
const href = link.getAttribute('href');
if (href) {
// Check for exact match or if current path starts with the link href
if (currentPath === href ||
(href !== '/' && currentPath.startsWith(href))) {
link.classList.add('active');
}
}
});
// Theme handling for custom pages
// Listen for theme changes from Starlight's ThemeSelect
const observeThemeChanges = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
const newTheme = document.documentElement.getAttribute('data-theme');
// Theme is already set by Starlight, we just need to ensure it persists
localStorage.setItem('starlight-theme', newTheme === 'dark' ? 'dark' : 'light');
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
};
observeThemeChanges();
// Hamburger menu functionality (only for custom pages)
const hamburgerBtn = document.getElementById('hamburger-btn');
const hamburgerMenu = document.getElementById('hamburger-menu');
if (hamburgerBtn && hamburgerMenu) {
// Toggle menu on button click
hamburgerBtn.addEventListener('click', function(e) {
e.stopPropagation();
hamburgerMenu.classList.toggle('show');
});
// Close menu when clicking outside
document.addEventListener('click', function(e) {
if (!hamburgerBtn.contains(e.target) && !hamburgerMenu.contains(e.target)) {
hamburgerMenu.classList.remove('show');
}
});
// Close menu when clicking a link
const hamburgerLinks = hamburgerMenu.querySelectorAll('.hamburger-link');
hamburgerLinks.forEach(link => {
link.addEventListener('click', function() {
hamburgerMenu.classList.remove('show');
});
});
// Close menu on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && hamburgerMenu.classList.contains('show')) {
hamburgerMenu.classList.remove('show');
}
});
}
});
</script>

View File

@ -0,0 +1,233 @@
---
export interface Props {
class?: string;
showStats?: boolean;
downloadCount?: string;
}
const { class: className, showStats = false, downloadCount = '3.8M+' } = Astro.props;
// Download links for different platforms
const downloadLinks = {
'mac-intel': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-mac-x64-0.5.14.dmg',
'mac-arm': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-mac-arm64-0.5.14.dmg',
'windows': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-win-x64-0.5.14.exe',
'linux-deb': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-linux-amd64-0.5.14.deb',
'linux-appimage': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-linux-x86_64-0.5.14.AppImage'
};
---
<div class={`download-container ${className || ''}`}>
<div class="download-section inline-flex items-center gap-2">
<!-- Main Download Button -->
<button
id="main-download-btn"
class="download-btn inline-flex items-center gap-2 px-5 py-2.5 bg-black dark:bg-white text-white dark:text-black rounded-lg hover:opacity-90 transition-all duration-200 font-medium shadow-sm hover:shadow-md"
data-download-url=""
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<span id="platform-text" class="text-base">Download for your platform</span>
<span id="file-size" class="text-xs opacity-70"></span>
</button>
<!-- Dropdown Toggle -->
<details class="download-dropdown relative">
<summary class="dropdown-toggle cursor-pointer p-2.5 bg-gray-100 dark:bg-gray-800 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors h-full flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<!-- Dropdown Menu -->
<div class="dropdown-menu absolute top-full mt-1 right-0 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-1 min-w-[250px] z-50">
<div class="font-semibold text-xs text-gray-500 dark:text-gray-400 px-3 py-1.5 border-b border-gray-200 dark:border-gray-700">
All Platforms
</div>
<!-- macOS Options -->
<div class="py-1">
<div class="text-xs text-gray-500 dark:text-gray-400 px-3 py-0.5 uppercase tracking-wide">macOS</div>
<a href={downloadLinks['mac-arm']}
class="platform-option block px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm"
data-platform="mac-arm">
<span class="font-medium">Apple Silicon</span>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">(M1/M2/M3)</span>
</a>
<a href={downloadLinks['mac-intel']}
class="platform-option block px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm"
data-platform="mac-intel">
<span class="font-medium">Intel</span>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">(x64)</span>
</a>
</div>
<!-- Windows -->
<div class="py-1 border-t border-gray-200 dark:border-gray-700">
<div class="text-xs text-gray-500 dark:text-gray-400 px-3 py-0.5 uppercase tracking-wide">Windows</div>
<a href={downloadLinks['windows']}
class="platform-option block px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm"
data-platform="windows">
<span class="font-medium">Windows</span>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">(x64)</span>
</a>
</div>
<!-- Linux Options -->
<div class="py-1 border-t border-gray-200 dark:border-gray-700">
<div class="text-xs text-gray-500 dark:text-gray-400 px-3 py-0.5 uppercase tracking-wide">Linux</div>
<a href={downloadLinks['linux-deb']}
class="platform-option block px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm"
data-platform="linux-deb">
<span class="font-medium">Debian/Ubuntu</span>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">(.deb)</span>
</a>
<a href={downloadLinks['linux-appimage']}
class="platform-option block px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm"
data-platform="linux-appimage">
<span class="font-medium">AppImage</span>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">(Universal)</span>
</a>
</div>
<!-- View All Releases -->
<div class="pt-1 border-t border-gray-200 dark:border-gray-700">
<a href="https://github.com/janhq/jan/releases"
target="_blank"
rel="noopener noreferrer"
class="block px-3 py-1.5 text-blue-600 dark:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-xs">
View all releases →
</a>
</div>
</div>
</details>
</div>
{showStats && (
<p class="text-gray-600 dark:text-gray-400 mt-3 text-center text-sm">
<span class="text-yellow-600 font-semibold">{downloadCount}</span> downloads | Free & Open Source
</p>
)}
</div>
<style>
.download-dropdown[open] .dropdown-menu {
animation: slideDown 0.2s ease-out;
}
.download-dropdown summary::-webkit-details-marker {
display: none;
}
.download-dropdown summary {
list-style: none;
}
.download-btn:active {
transform: scale(0.98);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Close dropdown when clicking outside */
.download-dropdown:not([open]) .dropdown-menu {
display: none;
}
</style>
<script>
// Platform detection and download handling
document.addEventListener('DOMContentLoaded', () => {
const platformText = document.getElementById('platform-text');
const fileSize = document.getElementById('file-size');
const mainDownloadBtn = document.getElementById('main-download-btn');
// Platform detection
function detectPlatform() {
const platform = navigator.platform.toLowerCase();
const userAgent = navigator.userAgent.toLowerCase();
let detectedPlatform = 'linux-deb';
let platformName = 'Linux (deb)';
let downloadSize = '(69.7 MB)';
if (platform.includes('mac') || userAgent.includes('mac')) {
// Check if Apple Silicon
if (userAgent.includes('arm') || userAgent.includes('apple')) {
detectedPlatform = 'mac-arm';
platformName = 'macOS (Apple Silicon)';
downloadSize = '(73.2 MB)';
} else {
detectedPlatform = 'mac-intel';
platformName = 'macOS (Intel)';
downloadSize = '(75.8 MB)';
}
} else if (platform.includes('win') || userAgent.includes('win')) {
detectedPlatform = 'windows';
platformName = 'Windows';
downloadSize = '(71.4 MB)';
}
return { platform: detectedPlatform, name: platformName, size: downloadSize };
}
// Set initial platform
const detected = detectPlatform();
if (platformText) {
platformText.textContent = `Download for ${detected.name}`;
}
if (fileSize) {
fileSize.textContent = detected.size;
}
// Get download URL based on platform
const downloadUrls = {
'mac-intel': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-mac-x64-0.5.14.dmg',
'mac-arm': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-mac-arm64-0.5.14.dmg',
'windows': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-win-x64-0.5.14.exe',
'linux-deb': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-linux-amd64-0.5.14.deb',
'linux-appimage': 'https://github.com/janhq/jan/releases/download/v0.5.14/jan-linux-x86_64-0.5.14.AppImage'
};
if (mainDownloadBtn) {
mainDownloadBtn.setAttribute('data-download-url', downloadUrls[detected.platform]);
// Handle main button click
mainDownloadBtn.addEventListener('click', () => {
const url = mainDownloadBtn.getAttribute('data-download-url');
if (url) {
window.location.href = url;
}
});
}
// Handle dropdown platform clicks
document.querySelectorAll('.platform-option').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const url = e.currentTarget.getAttribute('href');
if (url) {
window.location.href = url;
}
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
const dropdown = document.querySelector('.download-dropdown');
if (dropdown && !dropdown.contains(e.target)) {
dropdown.removeAttribute('open');
}
});
});
</script>

View File

@ -0,0 +1,112 @@
---
export interface Props {
class?: string;
}
const { class: className } = Astro.props;
---
<div class={`steps-container ${className || ''}`}>
<slot />
</div>
<style>
.steps-container {
margin: 2rem 0;
padding: 1.5rem;
background-color: var(--sl-color-gray-6);
border-radius: 0.5rem;
border: 1px solid var(--sl-color-gray-5);
}
/* Light mode adjustments */
:global([data-theme="light"]) .steps-container {
background-color: var(--sl-color-gray-7);
border-color: var(--sl-color-gray-6);
}
/* Style the headers inside steps */
.steps-container :global(h2),
.steps-container :global(h3) {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--sl-color-text-accent);
font-weight: 600;
}
.steps-container :global(h2:first-child),
.steps-container :global(h3:first-child) {
margin-top: 0;
}
/* Style lists inside steps */
.steps-container :global(ul) {
margin: 0.5rem 0;
padding-left: 1.5rem;
list-style: none;
}
.steps-container :global(ul li) {
position: relative;
margin: 0.5rem 0;
padding-left: 1.25rem;
}
/* Custom bullet points */
.steps-container :global(ul li::before) {
content: '→';
position: absolute;
left: 0;
color: var(--sl-color-text-accent);
font-weight: bold;
}
/* Style links inside steps */
.steps-container :global(a) {
color: var(--sl-color-text);
text-decoration: none;
transition: color 0.2s ease;
font-weight: 500;
}
.steps-container :global(a:hover) {
color: var(--sl-color-text-accent);
text-decoration: underline;
}
/* Add spacing between step groups */
.steps-container :global(h2:not(:first-child)),
.steps-container :global(h3:not(:first-child)) {
padding-top: 1rem;
border-top: 1px solid var(--sl-color-hairline);
}
/* Numbered steps styling */
.steps-container :global(h2) {
counter-increment: step-counter;
position: relative;
padding-left: 2.5rem;
}
.steps-container {
counter-reset: step-counter;
}
.steps-container :global(h2::before) {
content: counter(step-counter);
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 2rem;
height: 2rem;
background-color: var(--sl-color-accent);
color: var(--sl-color-white);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,60 @@
---
export interface Props {
id: string;
title?: string;
class?: string;
}
const { id, title = 'YouTube video player', class: className } = Astro.props;
// Extract video ID and handle both formats:
// - Simple ID: "4mvHgLy_YV8"
// - ID with params: "4mvHgLy_YV8?si=74cmdMmcH3gmpv0R"
const videoId = id.split('?')[0];
const params = id.includes('?') ? '?' + id.split('?')[1] : '';
---
<div class={`youtube-container ${className || ''}`}>
<iframe
src={`https://www.youtube.com/embed/${videoId}${params}`}
title={title}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
loading="lazy"
></iframe>
</div>
<style>
.youtube-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
margin: 1.5rem 0;
border-radius: 0.5rem;
background-color: var(--sl-color-gray-6);
}
.youtube-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
border-radius: 0.5rem;
}
/* Light mode adjustments */
:global([data-theme="light"]) .youtube-container {
background-color: var(--sl-color-gray-7);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Ensure proper spacing in content */
:global(.sl-markdown-content) .youtube-container {
margin: 2rem 0;
}
</style>

View File

@ -1,8 +1,38 @@
import { defineCollection } from 'astro:content';
import { defineCollection, z } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
import { videosSchema } from 'starlight-videos/schemas';
const changelogSchema = z.object({
title: z.string(),
description: z.string(),
date: z.date(),
version: z.string().optional(),
image: z.string().optional(),
gif: z.string().optional(),
video: z.string().optional(),
featured: z.boolean().default(false),
});
const blogSchema = z.object({
title: z.string(),
description: z.string(),
date: z.date(),
tags: z.string().optional(),
categories: z.string().optional(),
author: z.string().optional(),
ogImage: z.string().optional(),
featured: z.boolean().default(false),
});
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema({ extend: videosSchema }) }),
changelog: defineCollection({
type: 'content',
schema: changelogSchema,
}),
blog: defineCollection({
type: 'content',
schema: blogSchema,
}),
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Some files were not shown because too many files have changed in this diff Show More