const DEFAULT_TITLE = 'Foam Site'; const DEFAULT_DESCRIPTION = 'Exported from a Foam knowledge base.'; export const STARLIGHT_TEMPLATE_FILES = { '.gitignore': ['node_modules/', 'dist/', '.astro/'].join('\n') + '\n', 'package.json': JSON.stringify( { name: 'foam-exported-site', version: '0.0.0', private: true, scripts: { dev: 'astro dev', build: 'astro build', preview: 'astro preview', }, dependencies: { '@astrojs/starlight': '^0.38.3', astro: '^6.1.7', }, }, null, 2 ) + '\n', 'astro.config.mjs': `import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; const siteConfigUrl = new URL('./generated/site-config.json', import.meta.url); const siteConfig = fs.existsSync(siteConfigUrl) ? JSON.parse(fs.readFileSync(siteConfigUrl, 'utf8')) : {}; function deSlugify(slug) { return slug.replace(/[-_]+/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()); } function buildSidebar(dir, prefix = '') { return fs.readdirSync(dir, { withFileTypes: true }) .filter(e => !e.name.startsWith('_') && !e.name.startsWith('.')) .sort((a, b) => a.name.localeCompare(b.name)) .map(entry => { if (entry.isDirectory()) { const slug = prefix ? \`\${prefix}/\${entry.name}\` : entry.name; return { label: deSlugify(entry.name), items: buildSidebar(path.join(dir, entry.name), slug), }; } const slug = (prefix ? \`\${prefix}/\` : '') + entry.name.replace(/\\.md$/, ''); return { slug: slug === 'index' ? '' : slug }; }); } const docsDir = fileURLToPath(new URL('./src/content/docs', import.meta.url)); const sidebar = buildSidebar(docsDir); export default defineConfig({ site: siteConfig.siteUrl, integrations: [ starlight({ title: siteConfig.title ?? '${DEFAULT_TITLE}', description: siteConfig.description ?? '${DEFAULT_DESCRIPTION}', social: [], sidebar, components: { Footer: './src/components/FoamFooter.astro', PageFrame: './src/components/FoamPageFrame.astro', PageSidebar: './src/components/FoamPageSidebar.astro', Sidebar: './src/components/FoamSidebar.astro', }, pagination: false, favicon: '/favicon.svg', customCss: ['./src/styles/custom.css'], }), ], }); `, 'src/components/FoamPageSidebar.astro': `--- import MobileTableOfContents from 'virtual:starlight/components/MobileTableOfContents'; import TableOfContents from 'virtual:starlight/components/TableOfContents'; import siteConfig from '../../generated/site-config.json'; const { toc } = Astro.locals.starlightRoute; const basePath = (import.meta.env.BASE_URL || '/').replace(/\\/+$/, ''); const pathname = Astro.url.pathname.replace(/\\/+$/, '') || '/'; const currentRoute = basePath && pathname.startsWith(basePath) ? pathname.slice(basePath.length) || '/' : pathname; const selectedRoute = currentRoute === '/' && siteConfig.homepageRoute ? siteConfig.homepageRoute : currentRoute; --- <> {toc && (
)} `, 'src/components/FoamPageFrame.astro': `--- import MobileMenuToggle from 'virtual:starlight/components/MobileMenuToggle'; const { hasSidebar } = Astro.locals.starlightRoute; ---
\t{ \t\thasSidebar && ( \t\t\t \t\t) \t} \t
`, 'src/components/FoamSidebar.astro': `--- import type { Props } from '@astrojs/starlight/props'; import Default from '@astrojs/starlight/components/Sidebar.astro'; import Search from 'virtual:starlight/components/Search'; import ThemeSelect from 'virtual:starlight/components/ThemeSelect'; import siteConfig from '../../generated/site-config.json'; const props = Astro.props as Props; const homepageHref = import.meta.env.BASE_URL || '/'; ---
{siteConfig.title ?? 'Foam Site'}
`, 'src/components/FoamFooter.astro': `--- import type { Props } from '@astrojs/starlight/props'; import Default from '@astrojs/starlight/components/Footer.astro'; const props = Astro.props as Props; ---
Published with Foam
`, 'src/styles/custom.css': ` :root { --sl-nav-height: 0rem; } /* Reposition the mobile menu button since there is no nav bar */ starlight-menu-button button { top: calc((var(--sl-mobile-toc-height) - var(--sl-menu-button-size)) / 2); } .backlinks { border-top: 1px solid var(--sl-color-hairline-light); margin-top: 2rem; padding-top: 1rem; } .backlinks-label { color: var(--sl-color-white); font-size: var(--sl-text-sm); font-weight: 600; letter-spacing: 0.05em; margin-bottom: 0.5rem; text-transform: uppercase; } .backlinks a { display: block; font-size: var(--sl-text-sm); } .note-properties { border-bottom: 1px solid var(--sl-color-hairline-light); display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 1.5rem; padding-bottom: 1rem; } .note-property-row { align-items: baseline; display: flex; flex-wrap: wrap; font-size: var(--sl-text-sm); gap: 0.4rem; } .note-property-key { color: var(--sl-color-text-secondary); flex-shrink: 0; font-weight: 600; min-width: 6rem; } .note-property-value { align-items: center; color: var(--sl-color-text); display: flex; flex-wrap: wrap; gap: 0.3rem; } .note-property-chip { background: var(--sl-color-bg-nav); border: 1px solid var(--sl-color-hairline-light); border-radius: 999px; color: var(--sl-color-text-accent); font-size: var(--sl-text-xs); padding: 0.1rem 0.55rem; } /* TOC: style like left sidebar */ starlight-toc nav h2 { color: var(--sl-color-white); font-size: var(--sl-text-xs); font-weight: 600; letter-spacing: 0.075em; text-transform: uppercase; margin: 0 0 0.5rem; } starlight-toc a { color: var(--sl-color-gray-2); font-size: var(--sl-text-sm); text-decoration: none; } starlight-toc a:hover { background-color: var(--sl-color-bg-sidebar); color: var(--sl-color-white); } starlight-toc a[aria-current='true'] { background-color: var(--sl-color-bg-sidebar); color: var(--sl-color-text-accent); font-weight: 600; } `, 'src/content.config.ts': `import { defineCollection } from 'astro:content'; import { docsLoader } from '@astrojs/starlight/loaders'; import { docsSchema } from '@astrojs/starlight/schema'; export const collections = { docs: defineCollection({ loader: docsLoader(), schema: docsSchema(), }), }; `, 'tsconfig.json': JSON.stringify( { extends: 'astro/tsconfigs/strict', }, null, 2 ) + '\n', } as const;