Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/src/App/src/Articles/Handler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let private getArticlesPage (services:Services) : HttpHandler =
do! pushUrl ds "/articles"
return Some ctx
else
return! renderPage page "nav-articles" next ctx
return! renderPage services page "nav-articles" next ctx
}

let private getArticlePage (services:Services) (id:string) : HttpHandler =
Expand All @@ -39,15 +39,15 @@ let private getArticlePage (services:Services) (id:string) : HttpHandler =
do! pushUrl ds url
return Some ctx
else
return! renderPage page "nav-articles" next ctx
return! renderPage services page "nav-articles" next ctx
| None ->
let page = notFoundPage
if ctx.IsDatastar then
let ds = ctx.GetService<IDatastarService>()
do! patchElement ds page
return Some ctx
else
return! renderPage page "nav-articles" next ctx
return! renderPage services page "nav-articles" next ctx
}

let handler (services:Services) : HttpHandler =
Expand Down
5 changes: 3 additions & 2 deletions app/src/App/src/Common/Handler.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module App.Common.Handler

open App.Common.View
open App.ServiceRegistry
open Giraffe
open FSharp.ViewEngine
open Microsoft.AspNetCore.Http
Expand All @@ -21,9 +22,9 @@ let pushUrl (ds:IDatastarService) (url:string) = task {
do! ds.ExecuteScriptAsync(js)
}

let renderPage (page:HtmlElement) (selectedNav:string) : HttpHandler =
let renderPage (services:Services) (page:HtmlElement) (selectedNav:string) : HttpHandler =
fun next ctx -> task {
let doc = Document.primary(page, selectedNav=selectedNav)
let doc = Document.primary(page, services.config.googleAnalytics.measurementId, selectedNav)
let html = Render.toHtmlDocString doc
return! htmlString html next ctx
}
42 changes: 40 additions & 2 deletions app/src/App/src/Common/View.fs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ module ArticleCard =
_class "mt-2 text-xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
a {
_href url
_dataOn ("click", $"@get('{url}')")
_dataOn ("click__prevent", $"@get('{url}')")
_class "hover:text-emerald-600 dark:hover:text-emerald-400"
article'.title
}
Expand Down Expand Up @@ -266,7 +266,7 @@ module Page =
div { _id "page"; _class "min-h-screen bg-gray-100 dark:bg-gray-900"; page }

type Document =
static member primary (page:HtmlElement, ?selectedNav:string) =
static member primary (page:HtmlElement, googleAnalyticsMeasurementId:string, ?selectedNav:string) =
let selectedNav = defaultArg selectedNav ""
html {
_lang "en"
Expand All @@ -275,6 +275,9 @@ type Document =
meta { _charset "UTF-8" }
meta { _name "viewport"; _content "width=device-width, initial-scale=1.0" }
script { js "let t=localStorage.getItem('theme');if(t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark')}" }
script {
js $"window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){{dataLayer.push(arguments);}};gtag('consent','default',{{analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'}});window.loadGoogleAnalytics=window.loadGoogleAnalytics||function(){{if(window.__gaLoaded)return;window.__gaLoaded=true;var s=document.createElement('script');s.async=true;s.src='https://www.googletagmanager.com/gtag/js?id={googleAnalyticsMeasurementId}';document.head.appendChild(s);gtag('js',new Date());gtag('config','{googleAnalyticsMeasurementId}');}};window.applyAnalyticsConsent=window.applyAnalyticsConsent||function(v){{if(v==='accepted'){{gtag('consent','update',{{analytics_storage:'granted',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'}});window.loadGoogleAnalytics();}}else{{gtag('consent','update',{{analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'}});}}}};window.setAnalyticsConsent=window.setAnalyticsConsent||function(v){{localStorage.setItem('analytics-consent',v);window.applyAnalyticsConsent(v);var b=document.getElementById('cookie-consent-banner');if(b)b.classList.add('hidden');}};document.addEventListener('DOMContentLoaded',function(){{var saved=localStorage.getItem('analytics-consent');var banner=document.getElementById('cookie-consent-banner');if(saved==='accepted'||saved==='declined'){{window.applyAnalyticsConsent(saved);if(banner)banner.classList.add('hidden');}}else if(banner){{banner.classList.remove('hidden');}}}});"
}
link { _href "/css/compiled.css"; _rel "stylesheet" }
link { _href "/css/prism.css"; _rel "stylesheet" }
script { _type "module"; _src "/scripts/tailwindplus-elements.1.js" }
Expand All @@ -289,6 +292,41 @@ type Document =
page
Footer.primary
}
div {
_id "cookie-consent-banner"
_class "hidden fixed inset-x-0 bottom-0 z-50 border-t border-gray-300 bg-white/95 p-4 shadow-2xl backdrop-blur dark:border-gray-700 dark:bg-gray-900/95"
_role "dialog"
_ariaLive "polite"
div {
_class "mx-auto flex max-w-5xl flex-col gap-4 md:flex-row md:items-center md:justify-between"
div {
_class "max-w-3xl"
p {
_class "text-sm font-semibold text-gray-900 dark:text-gray-100"
"Analytics cookies"
}
p {
_class "mt-1 text-sm text-gray-600 dark:text-gray-300"
"I use Google Analytics to measure how this site is used. You can accept or reject analytics cookies, and the site will work either way."
}
}
div {
_class "flex flex-col gap-2 sm:flex-row"
button {
_type "button"
_class "inline-flex items-center justify-center rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:cursor-pointer dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
_onclick "setAnalyticsConsent('declined')"
"Reject"
}
button {
_type "button"
_class "inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-500 hover:cursor-pointer dark:bg-emerald-500 dark:hover:bg-emerald-400"
_onclick "setAnalyticsConsent('accepted')"
"Accept"
}
}
}
}
script { js "function getInitialTheme(){return localStorage.getItem('theme')||'system'};function applyTheme(t){var d=document.documentElement,isDark=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);d.classList.toggle('dark',isDark)};function setTheme(t){localStorage.setItem('theme',t);applyTheme(t)}" }
}
}
9 changes: 9 additions & 0 deletions app/src/App/src/Config.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ module ServerConfig =
let load () =
{ url = Env.variableOrDefault "SERVER_URL" "https://localhost:5000" }

type GoogleAnalyticsConfig =
{ measurementId:string }

module GoogleAnalyticsConfig =
let load () =
{ measurementId = Env.variable "GOOGLE_ANALYTICS_MEASUREMENT_ID" }

type Config =
{ debug:bool
appName:string
server:ServerConfig
seq:SeqConfig
googleAnalytics:GoogleAnalyticsConfig
sqlite:Sqlite.Config
notion:Notion.Config }

Expand All @@ -31,5 +39,6 @@ module Config =
appName = "andymeier"
server = ServerConfig.load ()
seq = SeqConfig.load ()
googleAnalytics = GoogleAnalyticsConfig.load ()
sqlite = Sqlite.Config.load ()
notion = Notion.Config.load () }
2 changes: 1 addition & 1 deletion app/src/App/src/Index/Handler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let private getHomePage (services:Services) : HttpHandler =
do! pushUrl ds "/"
return Some ctx
else
return! renderPage page "nav-home" next ctx
return! renderPage services page "nav-home" next ctx
}

let handler (services:Services) : HttpHandler =
Expand Down
2 changes: 1 addition & 1 deletion app/src/App/src/Projects/Handler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let private getPage (services:Services) : HttpHandler =
do! pushUrl ds "/projects"
return Some ctx
else
return! renderPage page "nav-projects" next ctx
return! renderPage services page "nav-projects" next ctx
}

let handler (services:Services) : HttpHandler =
Expand Down
2 changes: 1 addition & 1 deletion app/src/App/src/Services/Handler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let private getPage (services:Services) : HttpHandler =
do! pushUrl ds "/services"
return Some ctx
else
return! renderPage page "nav-services" next ctx
return! renderPage services page "nav-services" next ctx
}

let handler (services:Services) : HttpHandler =
Expand Down
1 change: 1 addition & 0 deletions app/src/Tests/Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<Compile Include="NotionParseTests.fs" />
<Compile Include="NotionPageTests.fs" />
<Compile Include="NotionServiceTests.fs" />
<Compile Include="ViewTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
Expand Down
26 changes: 26 additions & 0 deletions app/src/Tests/ViewTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module ViewTests

open App.Common.View
open Expecto
open FSharp.ViewEngine
open type Html

[<Tests>]
let tests =
testList "Document" [
test "includes consent banner and delayed google analytics loading" {
let doc = Document.primary(div { "Hello" }, "G-TEST123", "nav-home")

let html = Render.toHtmlDocString doc

Expect.stringContains html "<title>Andy Meier</title>" "Expected page to render"
Expect.stringContains html "selectedNav: 'nav-home'" "Expected nav signal to render"
Expect.stringContains html "cookie-consent-banner" "Expected consent banner"
Expect.stringContains html "Reject" "Expected reject action"
Expect.stringContains html "Accept" "Expected accept action"
Expect.stringContains html "gtag('consent','default',{analytics_storage:'denied'" "Expected denied-by-default consent mode"
Expect.stringContains html "localStorage.setItem('analytics-consent',v)" "Expected consent to be persisted"
Expect.stringContains html "https://www.googletagmanager.com/gtag/js?id=G-TEST123" "Expected deferred gtag script source"
Expect.stringContains html "gtag('config','G-TEST123');" "Expected GA config call after consent"
}
]
6 changes: 6 additions & 0 deletions pulumi/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ export const notionConfig = {
articlesDatabaseId: rawNotionConfig.require('articlesDatabaseId'),
apiKey: rawNotionConfig.requireSecret('apiKey')
}

const rawGoogleAnalyticsConfig = new pulumi.Config('googleAnalytics')

export const googleAnalyticsConfig = {
measurementId: rawGoogleAnalyticsConfig.require('measurementId')
}
1 change: 1 addition & 0 deletions pulumi/src/k8s/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let appSecret = new k8s.core.v1.Secret('app', {
SQLITE_PATH: '/data/app.db',
NOTION_ARTICLES_DATABASE_ID: config.notionConfig.articlesDatabaseId,
NOTION_API_KEY: config.notionConfig.apiKey,
GOOGLE_ANALYTICS_MEASUREMENT_ID: config.googleAnalyticsConfig.measurementId
}
}, { provider })

Expand Down
Loading