Skip to content

Commit 0a8a644

Browse files
authored
Merge pull request #32 from basicmachines-co/docs-updates-2026-07
Docs overhaul: v0.22 accuracy pass, teams/partners sections, full-site review
2 parents 36ba002 + 2b33a87 commit 0a8a644

123 files changed

Lines changed: 5243 additions & 4658 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docs-check.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Docs checks
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
docs:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 22
16+
cache: npm
17+
- run: npm ci
18+
- run: npm run check

CLAUDE.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,18 @@ just deploy production # Deploy to production environment
9292
│ ├── app.config.ts # Docus configuration
9393
│ └── components/ # Custom Vue components
9494
├── content/ # Documentation pages (Markdown)
95-
│ ├── 1.start-here/ # Getting started guides
96-
│ ├── 2.whats-new/ # Release notes
97-
│ ├── 3.cloud/ # Cloud documentation
98-
│ ├── 4.local/ # Local installation
99-
│ ├── 5.concepts/ # Core concepts
100-
│ ├── 6.integrations/ # Integration guides
101-
│ ├── 7.how-to/ # How-to guides
102-
│ └── 8.reference/ # Technical reference
95+
│ ├── 0.welcome.md # Landing page (/ redirects to /welcome)
96+
│ ├── 01.start-here/ # Getting started guides
97+
│ ├── 02.whats-new/ # Announcements + changelog
98+
│ ├── 03.cloud/ # Cloud documentation
99+
│ ├── 04.teams/ # Teams documentation
100+
│ ├── 05.partners/ # MSP partner program
101+
│ ├── 06.local/ # Local installation
102+
│ ├── 07.concepts/ # Core concepts
103+
│ ├── 08.integrations/ # Integration guides
104+
│ ├── 09.how-to/ # How-to guides
105+
│ └── 10.reference/ # Technical reference
106+
# Nav order sorts numeric prefixes lexically — keep them zero-padded
103107
├── public/ # Static assets
104108
├── server/ # Server routes (API)
105109
├── nuxt.config.ts # Nuxt configuration
@@ -174,11 +178,10 @@ pip install basic-memory
174178

175179
When updating docs for a new Basic Memory release, update all of the following:
176180

177-
1. **Homepage version badge**`content/index.md`: update the version text (e.g., `v0.22 →`) and the `to:` link to point to the new release notes page
178-
2. **Release notes page** — new minor/major: replace the previous version's page — rename `content/2.whats-new/1.v<OLD>.md` to `1.v<NEW>.md` and rewrite it for the new release. Only the latest release gets a dedicated page (the changelog covers older releases), so the left nav must never show two version entries. Update any inbound links to the old release URL: latest-release cards point to the new page; references to version-specific behavior point to the GitHub release tag. Patch release: append a short note to the current version's page instead
179-
3. **Changelog** — the `*.changelog.md` page under `content/2.whats-new/` auto-fetches from the GitHub releases API, no manual update needed
180-
4. **Feature docs** — if the release adds user-facing features, update the relevant guide and reference pages (`content/3.cloud/`, `content/9.reference/`, etc.)
181-
5. **Deploy** — push to main auto-deploys to development; production requires manual workflow dispatch via GitHub Actions
181+
1. **Welcome page What's New callout**`content/0.welcome.md`: update the version text (e.g., `v0.22`) and the headline; the link stays `/whats-new/changelog`
182+
2. **Changelog page**`content/2.whats-new/1.changelog.md`: add a section for the new minor/major version at the top, linking the GitHub release tag(s). Patch release: add a short bullet list under its minor version's section instead. The `::github-releases` block at the bottom auto-fetches full release notes from the GitHub API — no manual update needed there. There are no per-version pages — deep links to version-specific behavior point at GitHub release tags
183+
3. **Feature docs** — if the release adds user-facing features, update the relevant guide and reference pages (`content/3.cloud/`, `content/9.reference/`, etc.)
184+
4. **Deploy** — push to main auto-deploys to development; production requires manual workflow dispatch via GitHub Actions
182185

183186
## Documentation Status & Priorities
184187

app/app.config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,17 @@ export default defineAppConfig({
4747
x: 'https://x.com/basic_memory',
4848
discord: 'https://discord.gg/tyvKNccgqN',
4949
reddit: 'https://www.reddit.com/r/basicmemory',
50+
linkedin: 'https://www.linkedin.com/company/basicmemory/',
5051
},
5152
toc: {
5253
bottom: {
53-
title: 'Community',
54+
title: 'Contact Us',
5455
links: [
56+
{ label: 'Contact Support', to: '/reference/contact-support' },
57+
{ label: 'Discord', to: 'https://discord.gg/tyvKNccgqN' },
5558
{ label: 'GitHub', to: 'https://github.com/basicmachines-co/basic-memory' },
5659
{ label: 'X / Twitter', to: 'https://x.com/basic_memory' },
57-
{ label: 'Discord', to: 'https://discord.gg/tyvKNccgqN' },
60+
{ label: 'LinkedIn', to: 'https://www.linkedin.com/company/basicmemory/' },
5861
{ label: 'Reddit', to: 'https://www.reddit.com/r/basicmemory' },
5962
],
6063
},

app/components/content/Mermaid.vue

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,28 @@ async function renderMermaid() {
6262
// Dynamic import to keep bundle size down when mermaid isn't used
6363
const { renderMermaid: render } = await import('beautiful-mermaid')
6464
65-
// Use different colors for light/dark mode
66-
const svg = await render(props.code.trim(), {
67-
bg: isDark.value ? '#1e293b' : '#ffffff',
68-
fg: isDark.value ? '#e2e8f0' : '#18181b',
69-
transparent: true,
70-
})
65+
// Enriched theme matching the site palette (orange primary, stone neutral)
66+
const svg = await render(props.code.trim(), isDark.value
67+
? {
68+
bg: '#1c1917', // stone-900
69+
fg: '#e7e5e4', // stone-200
70+
accent: '#f97316', // orange-500 — arrow heads, highlights
71+
line: '#78716c', // stone-500 — connectors
72+
muted: '#a8a29e', // stone-400 — edge labels, secondary text
73+
surface: '#292524', // stone-800 — node fill
74+
border: '#57534e', // stone-600 — node stroke
75+
transparent: true,
76+
}
77+
: {
78+
bg: '#ffffff',
79+
fg: '#292524', // stone-800
80+
accent: '#ea580c', // orange-600
81+
line: '#a8a29e', // stone-400
82+
muted: '#78716c', // stone-500
83+
surface: '#fafaf9', // stone-50
84+
border: '#d6d3d1', // stone-300
85+
transparent: true,
86+
})
7187
7288
svgContent.value = svg
7389
} catch (e) {

app/components/content/ThemeImage.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<span class="block">
3-
<img :src="light" :alt="alt" class="block dark:hidden rounded-md" />
4-
<img :src="dark" :alt="alt" class="hidden dark:block rounded-md" />
3+
<img :src="light" :alt="alt" class="block dark:hidden rounded-md border border-neutral-300 dark:border-neutral-700" />
4+
<img :src="dark" :alt="alt" class="hidden dark:block rounded-md border border-neutral-300 dark:border-neutral-700" />
55
</span>
66
</template>
77

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* Signup attribution capture — port of basicmemory.com's
3+
* src/lib/signup-attribution.ts. Writes the shared
4+
* `bmc_signup_attribution_v1` cookie on `.basicmemory.com` so signups at
5+
* app.basicmemory.com can be attributed to a docs first touch. Keep the
6+
* cookie name, shape, and semantics in sync with the marketing site.
7+
*/
8+
9+
const COOKIE_NAME = 'bmc_signup_attribution_v1'
10+
const COOKIE_DAYS = 90
11+
const SELF_DOMAINS = ['basicmemory.com', 'app.basicmemory.com', 'localhost']
12+
const PASSIVE_CHANNELS = [
13+
'unknown',
14+
'reddit',
15+
'google',
16+
'facebook',
17+
'microsoft',
18+
'x_twitter',
19+
'youtube',
20+
'referral',
21+
] as const
22+
23+
type PassiveChannel = (typeof PASSIVE_CHANNELS)[number]
24+
25+
interface PassiveAttribution {
26+
event_id: string
27+
captured_at: string
28+
landing_url: string
29+
landing_path: string
30+
referrer: string | null
31+
referrer_host: string | null
32+
utm_source: string | null
33+
utm_medium: string | null
34+
utm_campaign: string | null
35+
utm_term: string | null
36+
utm_content: string | null
37+
gclid: string | null
38+
fbclid: string | null
39+
msclkid: string | null
40+
rdt_cid: string | null
41+
rdt_click_id: string | null
42+
passive_channel: PassiveChannel
43+
}
44+
45+
interface SignupAttributionState {
46+
first_touch: PassiveAttribution
47+
latest_touch: PassiveAttribution
48+
}
49+
50+
function safeUrl(raw: string): URL | null {
51+
try {
52+
return new URL(raw)
53+
} catch {
54+
return null
55+
}
56+
}
57+
58+
function normalizeValue(value: string | null | undefined): string | null {
59+
if (!value) return null
60+
const normalized = value.trim().toLowerCase()
61+
return normalized.length > 0 ? normalized : null
62+
}
63+
64+
function cookieValue(name: string): string | null {
65+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
66+
const match = document.cookie.match(new RegExp(`(?:^|; )${escapedName}=([^;]*)`))
67+
if (!match || !match[1]) return null
68+
69+
try {
70+
return decodeURIComponent(match[1])
71+
} catch {
72+
return match[1]
73+
}
74+
}
75+
76+
function isPassiveChannel(value: unknown): value is PassiveChannel {
77+
return typeof value === 'string' && PASSIVE_CHANNELS.includes(value as PassiveChannel)
78+
}
79+
80+
function isTouch(value: unknown): value is PassiveAttribution {
81+
if (!value || typeof value !== 'object') return false
82+
const candidate = value as Partial<PassiveAttribution>
83+
return typeof candidate.event_id === 'string'
84+
&& typeof candidate.captured_at === 'string'
85+
&& typeof candidate.landing_url === 'string'
86+
&& typeof candidate.landing_path === 'string'
87+
&& isPassiveChannel(candidate.passive_channel)
88+
}
89+
90+
function isAttributionState(value: unknown): value is SignupAttributionState {
91+
if (!value || typeof value !== 'object') return false
92+
const candidate = value as Partial<SignupAttributionState>
93+
return isTouch(candidate.first_touch) && isTouch(candidate.latest_touch)
94+
}
95+
96+
function readAttributionCookie(): SignupAttributionState | null {
97+
const rawValue = cookieValue(COOKIE_NAME)
98+
if (!rawValue) return null
99+
100+
try {
101+
const parsed = JSON.parse(rawValue)
102+
return isAttributionState(parsed) ? parsed : null
103+
} catch {
104+
return null
105+
}
106+
}
107+
108+
function isSelfDomain(hostname: string): boolean {
109+
return SELF_DOMAINS.some(domain => hostname === domain || hostname.endsWith(`.${domain}`))
110+
}
111+
112+
function referrerHost(): string | null {
113+
const referrer = normalizeValue(document.referrer)
114+
if (!referrer) return null
115+
116+
const parsedReferrer = safeUrl(referrer)
117+
if (!parsedReferrer) return null
118+
119+
const hostname = parsedReferrer.hostname.trim().toLowerCase()
120+
if (!hostname || isSelfDomain(hostname)) return null
121+
return hostname
122+
}
123+
124+
function sourceToPassiveChannel(source: string | null): PassiveChannel | null {
125+
if (!source) return null
126+
127+
if (source.includes('reddit') || source === 'rdt') return 'reddit'
128+
if (source.includes('google')) return 'google'
129+
if (source.includes('facebook') || source === 'fb' || source.includes('instagram')) {
130+
return 'facebook'
131+
}
132+
if (source.includes('bing') || source.includes('microsoft')) return 'microsoft'
133+
if (source.includes('twitter') || source === 'x') return 'x_twitter'
134+
if (source.includes('youtube') || source.includes('youtu')) return 'youtube'
135+
136+
return null
137+
}
138+
139+
function passiveChannel(values: {
140+
utmSource: string | null
141+
rdtCid: string | null
142+
rdtClickId: string | null
143+
gclid: string | null
144+
fbclid: string | null
145+
msclkid: string | null
146+
referrerHost: string | null
147+
}): PassiveChannel {
148+
const utmChannel = sourceToPassiveChannel(values.utmSource)
149+
if (utmChannel) return utmChannel
150+
if (values.utmSource) return 'referral'
151+
if (values.rdtCid || values.rdtClickId) return 'reddit'
152+
if (values.gclid) return 'google'
153+
if (values.fbclid) return 'facebook'
154+
if (values.msclkid) return 'microsoft'
155+
if (values.referrerHost) return sourceToPassiveChannel(values.referrerHost) ?? 'referral'
156+
return 'unknown'
157+
}
158+
159+
function eventId(): string {
160+
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
161+
return window.crypto.randomUUID()
162+
}
163+
return `evt_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`
164+
}
165+
166+
function buildTouch(): PassiveAttribution {
167+
const url = new URL(window.location.href)
168+
const utmSource = normalizeValue(url.searchParams.get('utm_source'))
169+
const rdtCid = normalizeValue(url.searchParams.get('rdt_cid'))
170+
const rdtClickId = normalizeValue(cookieValue('_rdt_click_id'))
171+
const gclid = normalizeValue(url.searchParams.get('gclid'))
172+
const fbclid = normalizeValue(url.searchParams.get('fbclid'))
173+
const msclkid = normalizeValue(url.searchParams.get('msclkid'))
174+
const host = referrerHost()
175+
176+
return {
177+
event_id: eventId(),
178+
captured_at: new Date().toISOString(),
179+
landing_url: `${url.origin}${url.pathname}`,
180+
landing_path: url.pathname,
181+
referrer: host,
182+
referrer_host: host,
183+
utm_source: utmSource,
184+
utm_medium: normalizeValue(url.searchParams.get('utm_medium')),
185+
utm_campaign: normalizeValue(url.searchParams.get('utm_campaign')),
186+
utm_term: normalizeValue(url.searchParams.get('utm_term')),
187+
utm_content: normalizeValue(url.searchParams.get('utm_content')),
188+
gclid,
189+
fbclid,
190+
msclkid,
191+
rdt_cid: rdtCid,
192+
rdt_click_id: rdtClickId,
193+
passive_channel: passiveChannel({
194+
utmSource,
195+
rdtCid,
196+
rdtClickId,
197+
gclid,
198+
fbclid,
199+
msclkid,
200+
referrerHost: host,
201+
}),
202+
}
203+
}
204+
205+
function hasSignal(touch: PassiveAttribution): boolean {
206+
return touch.passive_channel !== 'unknown'
207+
|| !!touch.referrer_host
208+
|| !!touch.utm_source
209+
|| !!touch.utm_medium
210+
|| !!touch.utm_campaign
211+
|| !!touch.utm_term
212+
|| !!touch.utm_content
213+
|| !!touch.gclid
214+
|| !!touch.fbclid
215+
|| !!touch.msclkid
216+
|| !!touch.rdt_cid
217+
|| !!touch.rdt_click_id
218+
}
219+
220+
function writeAttributionCookie(state: SignupAttributionState): void {
221+
const expires = new Date()
222+
expires.setTime(expires.getTime() + COOKIE_DAYS * 24 * 60 * 60 * 1000)
223+
224+
let cookie = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(state))}`
225+
cookie += `;expires=${expires.toUTCString()};path=/;SameSite=Lax`
226+
227+
const hostname = window.location.hostname.toLowerCase()
228+
if (hostname === 'basicmemory.com' || hostname.endsWith('.basicmemory.com')) {
229+
cookie += ';domain=.basicmemory.com'
230+
}
231+
if (window.location.protocol === 'https:') {
232+
cookie += ';Secure'
233+
}
234+
235+
document.cookie = cookie
236+
}
237+
238+
function captureSignupAttribution(): void {
239+
const current = readAttributionCookie()
240+
const captured = buildTouch()
241+
const state = current
242+
? {
243+
first_touch: current.first_touch,
244+
latest_touch: hasSignal(captured) ? captured : current.latest_touch,
245+
}
246+
: {
247+
first_touch: captured,
248+
latest_touch: captured,
249+
}
250+
251+
writeAttributionCookie(state)
252+
}
253+
254+
export default defineNuxtPlugin(() => {
255+
captureSignupAttribution()
256+
useRouter().afterEach(() => {
257+
captureSignupAttribution()
258+
})
259+
})

0 commit comments

Comments
 (0)