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 || '/';
---
`,
'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;
---
`,
'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;