Skip to content

Commit 40ff374

Browse files
Algolia, PostHog and Google Analytics (#203)
* Algolia and GA * added search filters * added search filters * added search filters * changed license error title * increased search results * removed search parameters * Added old redirects * Added Posthog analytics * fixed duplicate redirect * fix: convert variables to envs * fix: formatting --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
1 parent 889df68 commit 40ff374

File tree

23 files changed

+1286
-1668
lines changed

23 files changed

+1286
-1668
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# PostHog Analytics
2+
VITE_POSTHOG_KEY=
3+
4+
# Algolia Search
5+
VITE_ALGOLIA_APP_ID=
6+
VITE_ALGOLIA_API_KEY=
7+
VITE_ALGOLIA_INDEX_NAME=

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ examples-temp
1515
node_modules
1616
pnpm-global
1717
TODOs.md
18-
*.timestamp-*.mjs
18+
*.timestamp-*.mjs
19+
.env
20+
.env.local
21+
.env.*.local

docs/.vitepress/config.mts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
1-
import { defineConfig } from 'vitepress'
1+
import { defineConfig, type HeadConfig } from 'vitepress'
22
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
33
import { withMermaid } from 'vitepress-plugin-mermaid'
4+
import { readFileSync } from 'node:fs'
5+
import { resolve } from 'node:path'
6+
7+
function loadEnvVar(key: string): string | undefined {
8+
// process.env takes precedence (CI/hosting platforms set vars here)
9+
if (key in process.env) return process.env[key] || undefined
10+
// Fall back to .env file for local development
11+
try {
12+
const envFile = readFileSync(resolve(process.cwd(), '.env'), 'utf-8')
13+
const match = envFile.match(new RegExp(`^${key}=(.+)$`, 'm'))
14+
return match?.[1]?.trim()
15+
} catch {
16+
return undefined
17+
}
18+
}
19+
20+
const posthogKey = loadEnvVar('VITE_POSTHOG_KEY')
21+
const algoliaAppId = loadEnvVar('VITE_ALGOLIA_APP_ID')
22+
const algoliaApiKey = loadEnvVar('VITE_ALGOLIA_API_KEY')
23+
const algoliaIndexName = loadEnvVar('VITE_ALGOLIA_INDEX_NAME')
24+
25+
const searchConfig = algoliaAppId && algoliaApiKey && algoliaIndexName
26+
? {
27+
provider: 'algolia' as const,
28+
options: {
29+
appId: algoliaAppId,
30+
apiKey: algoliaApiKey,
31+
indexName: algoliaIndexName,
32+
insights: true
33+
}
34+
}
35+
: { provider: 'local' as const }
36+
37+
const posthogHead: HeadConfig[] = posthogKey
38+
? [
39+
['script', {}, `
40+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
41+
posthog.init('${posthogKey}',{api_host:'https://us.posthog.com', opt_out_capturing_by_default: true, persistence: 'memory'});
42+
`]
43+
]
44+
: []
445

546
export default withMermaid(defineConfig({
647
markdown: {
@@ -45,6 +86,22 @@ export default withMermaid(defineConfig({
4586
head: [
4687
['link', { rel: 'icon', href: '/logo/favicon-32x32.png' }],
4788

89+
// Google Analytics with Consent Mode v2
90+
['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-JF828SKW90' }],
91+
['script', {}, `window.dataLayer = window.dataLayer || [];
92+
function gtag(){dataLayer.push(arguments);}
93+
gtag('consent', 'default', {
94+
'analytics_storage': 'denied',
95+
'ad_storage': 'denied',
96+
'ad_user_data': 'denied',
97+
'ad_personalization': 'denied'
98+
});
99+
gtag('js', new Date());
100+
gtag('config', 'G-JF828SKW90');`],
101+
102+
// PostHog Analytics (loaded only when VITE_POSTHOG_KEY is set)
103+
...posthogHead,
104+
48105
// SEO: Basic meta tags
49106
['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }],
50107
['meta', { name: 'author', content: 'Plane' }],
@@ -619,9 +676,7 @@ export default withMermaid(defineConfig({
619676
{ icon: 'linkedin', link: 'https://www.linkedin.com/company/planepowers/' }
620677
],
621678

622-
search: {
623-
provider: 'local'
624-
},
679+
search: searchConfig,
625680

626681
editLink: {
627682
pattern: 'https://github.com/makeplane/developer-docs/edit/main/:path'
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted } from 'vue'
3+
4+
const STORAGE_KEY = 'plane-docs-cookie-consent'
5+
const showBanner = ref(false)
6+
7+
function getConsent(): string | null {
8+
try {
9+
return localStorage.getItem(STORAGE_KEY)
10+
} catch {
11+
return null
12+
}
13+
}
14+
15+
function grantConsent() {
16+
if (typeof window === 'undefined') return
17+
18+
// Google Analytics
19+
if (typeof window.gtag === 'function') {
20+
window.gtag('consent', 'update', {
21+
analytics_storage: 'granted'
22+
})
23+
}
24+
25+
// PostHog (may not be loaded if VITE_POSTHOG_KEY is unset)
26+
if (window.posthog?.opt_in_capturing) {
27+
window.posthog.opt_in_capturing()
28+
}
29+
}
30+
31+
function denyConsent() {
32+
if (typeof window === 'undefined') return
33+
34+
// Google Analytics — consent stays denied by default, no update needed
35+
36+
// PostHog (may not be loaded if VITE_POSTHOG_KEY is unset)
37+
if (window.posthog?.opt_out_capturing) {
38+
window.posthog.opt_out_capturing()
39+
}
40+
}
41+
42+
function accept() {
43+
try {
44+
localStorage.setItem(STORAGE_KEY, 'granted')
45+
} catch {}
46+
grantConsent()
47+
showBanner.value = false
48+
}
49+
50+
function decline() {
51+
try {
52+
localStorage.setItem(STORAGE_KEY, 'denied')
53+
} catch {}
54+
denyConsent()
55+
showBanner.value = false
56+
}
57+
58+
onMounted(() => {
59+
const consent = getConsent()
60+
if (consent === 'granted') {
61+
grantConsent()
62+
} else if (consent === 'denied') {
63+
denyConsent()
64+
} else {
65+
showBanner.value = true
66+
}
67+
})
68+
</script>
69+
70+
<template>
71+
<Transition name="consent">
72+
<div v-if="showBanner" class="consent-banner">
73+
<p class="consent-text">
74+
We use cookies and analytics to improve your experience. You can accept or decline non-essential cookies.
75+
</p>
76+
<div class="consent-actions">
77+
<button class="consent-btn consent-btn-decline" @click="decline">Decline</button>
78+
<button class="consent-btn consent-btn-accept" @click="accept">Accept</button>
79+
</div>
80+
</div>
81+
</Transition>
82+
</template>
83+
84+
<style scoped>
85+
.consent-banner {
86+
position: fixed;
87+
bottom: 20px;
88+
left: 50%;
89+
transform: translateX(-50%);
90+
z-index: 100;
91+
display: flex;
92+
align-items: center;
93+
gap: 16px;
94+
max-width: 680px;
95+
width: calc(100% - 32px);
96+
padding: 14px 20px;
97+
background: var(--vp-c-bg-soft);
98+
border: 1px solid var(--vp-c-divider);
99+
border-radius: 12px;
100+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
101+
}
102+
103+
.consent-text {
104+
margin: 0;
105+
font-size: 13px;
106+
line-height: 1.5;
107+
color: var(--vp-c-text-2);
108+
flex: 1;
109+
}
110+
111+
.consent-actions {
112+
display: flex;
113+
gap: 8px;
114+
flex-shrink: 0;
115+
}
116+
117+
.consent-btn {
118+
padding: 6px 16px;
119+
border-radius: 6px;
120+
font-size: 13px;
121+
font-weight: 500;
122+
cursor: pointer;
123+
border: none;
124+
white-space: nowrap;
125+
transition: background 0.15s ease;
126+
}
127+
128+
.consent-btn-decline {
129+
background: transparent;
130+
color: var(--vp-c-text-2);
131+
border: 1px solid var(--vp-c-divider);
132+
}
133+
134+
.consent-btn-decline:hover {
135+
background: var(--vp-c-bg-mute);
136+
}
137+
138+
.consent-btn-accept {
139+
background: var(--vp-c-brand-1);
140+
color: #fff;
141+
}
142+
143+
.consent-btn-accept:hover {
144+
opacity: 0.9;
145+
}
146+
147+
@media (max-width: 540px) {
148+
.consent-banner {
149+
flex-direction: column;
150+
align-items: stretch;
151+
gap: 12px;
152+
}
153+
.consent-actions {
154+
justify-content: flex-end;
155+
}
156+
}
157+
158+
/* Transition */
159+
.consent-enter-active {
160+
transition: opacity 0.3s ease, transform 0.3s ease;
161+
}
162+
.consent-leave-active {
163+
transition: opacity 0.2s ease, transform 0.2s ease;
164+
}
165+
.consent-enter-from {
166+
opacity: 0;
167+
transform: translateX(-50%) translateY(20px);
168+
}
169+
.consent-leave-to {
170+
opacity: 0;
171+
transform: translateX(-50%) translateY(20px);
172+
}
173+
</style>

docs/.vitepress/theme/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import DefaultTheme from 'vitepress/theme'
22
import type { Theme } from 'vitepress'
3-
import { onMounted, nextTick } from 'vue'
3+
import { onMounted, nextTick, h } from 'vue'
44
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
55

66
import './style.css'
@@ -11,6 +11,7 @@ import CodePanel from './components/CodePanel.vue'
1111
import ResponsePanel from './components/ResponsePanel.vue'
1212
import Card from './components/Card.vue'
1313
import CardGroup from './components/CardGroup.vue'
14+
import CookieConsent from './components/CookieConsent.vue'
1415

1516
/**
1617
* Adds 'api-page' class to hide the aside on API reference pages
@@ -108,6 +109,11 @@ function updateHashOnTabClick(event: Event) {
108109

109110
export default {
110111
extends: DefaultTheme,
112+
Layout() {
113+
return h(DefaultTheme.Layout, null, {
114+
'layout-bottom': () => h(CookieConsent)
115+
})
116+
},
111117
enhanceApp({ app, router }) {
112118
enhanceAppWithTabs(app)
113119

0 commit comments

Comments
 (0)