Add & redesign friggframework.org marketing site under website/#630
Conversation
Migrate the static marketing site (previously in the lefthook--demo-frigg-application repo) into the Frigg core monorepo so it can be iterated on alongside the framework and deployed from the next branch via Netlify. - website/ contains the static site (index.html, css/js/fonts/plugins/assets), accordions.json copy, and the Netlify serverless functions in friggframework-api/ (subscribe, submission-created). - Root netlify.toml sets base = "website" with the functions directory and the api/feedback redirects preserved from the original deploy config. - Add website/README.md and website/.gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
The shortcut icon pointed to assets/img/favicon.svg, which does not exist in the site assets. Point it at the actual file, assets/img/frigg-favicon.svg. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
Rebuild the friggframework.org landing page from the old Bootstrap/jQuery version into a single, self-contained, dependency-free page: - New positioning: agents build the integrations; every integration is also an MCP tool your agents can call (typed capabilities / mcp-tool surface); own your stack and your pipes; borrow the wisdom of the crowd; zero-to-production in an afternoon; notes on easy adoption. - Creative "woven pipes" design (nod to Frigg, "weaver of the clouds"): animated weave canvas, live agent-session terminal, real integration-icon marquee, editorial "the shift" blocks, a numbered zero-to-prod timeline. - Full light / dark / system theme toggle via CSS custom-property tokens (prefers-color-scheme default + persisted data-theme override). - Self-hosted fonts (Bricolage Grotesque, Hanken Grotesk, JetBrains Mono) as woff2 — no external font requests. Analytics (GA + PostHog) preserved. - Signup form keeps the exact field names + form-name the submission-created function reads, so the Netlify Forms flow is unchanged. - Prune ~8.7MB of now-unused vendor assets (Bootstrap, jQuery, plugins, legacy fonts, old css/js, accordions.json); site drops from ~9.8MB to ~1.4MB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35143847 | Triggered | PostHog Project API Key | e082e5d | website/index.html | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
| const res = await fetch(process.env.WEBHOOK_URL, { | ||
| method: 'POST', | ||
| body: JSON.stringify({email: payload.email.trim(), | ||
| slackInvite: payload.slackInvite, | ||
| emailUpdates: payload.emailUpdates}), | ||
| contentType: 'application/json' | ||
| }) |
There was a problem hiding this comment.
The contentType property is not a valid option for the fetch API. This will be silently ignored, causing the Zapier webhook request to be sent without the correct Content-Type header, which may cause the webhook to fail.
const res = await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: payload.email.trim(),
slackInvite: payload.slackInvite,
emailUpdates: payload.emailUpdates
})
})| const res = await fetch(process.env.WEBHOOK_URL, { | |
| method: 'POST', | |
| body: JSON.stringify({email: payload.email.trim(), | |
| slackInvite: payload.slackInvite, | |
| emailUpdates: payload.emailUpdates}), | |
| contentType: 'application/json' | |
| }) | |
| const res = await fetch(process.env.WEBHOOK_URL, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({email: payload.email.trim(), | |
| slackInvite: payload.slackInvite, | |
| emailUpdates: payload.emailUpdates}) | |
| }) | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Already resolved in the current head — subscribe.js sends headers: { 'Content-Type': 'application/json' }, not the invalid contentType option, so the request goes out with the correct content type.
Generated by Claude Code
- Reposition "own your pipes" as cloud-agnostic: primarily AWS today, with adopters on GCP, Azure, and self-hosted containers. - New "infrastructure writes itself" block: the Frigg app definition self-scaffolds its own resources (queues, functions, VPC, encryption, OAuth) from the integrations you define. - New "however you run it" section covering the three real adopter patterns: product integrations for end customers, internal business-process automation, and personal / home-lab automation. - New Left Hook commercial-license band (open core): MIT for everyone, plus a commercial license for support, maintenance, and premium/heavy-duty connectors, plugins & extensions. - Blend Left Hook branding: orange (#ff5e00) is now the secondary accent (hero gradient, threads, timeline), a navy commercial band, and the official Left Hook hook mark in the commercial band and footer. - Hero meta updated to "MIT + commercial" and "Runs on your cloud"; nav gains Uses + Commercial. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
- Header brand is now a full "Frigg by Left Hook" lockup: the Frigg mark and wordmark, then the official Left Hook hook mark and "Left Hook" wordmark. - Fix a mobile header overflow: on <=680px the Docs button is hidden and the primary CTA shortens to "Join" so the brand, CTA, and theme toggle all fit; below 460px the "by / Left Hook" words drop, leaving the two marks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
| exports.handler = async function (event, context, callback) { | ||
| const payload = JSON.parse(event.body).payload | ||
| console.log(JSON.parse(event.body)) | ||
| console.log(payload) | ||
| console.log(payload.data["slack-invite"]) | ||
| const email = payload.data.email | ||
| const slackInvite = !!payload.data["slack-invite"] | ||
| const emailUpdates = !!payload.data["update-emails"] | ||
| console.log(email, slackInvite, emailUpdates) | ||
| const res = await fetch('https://api.friggframework.org/subscribe', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json;charset=utf-8' | ||
| }, | ||
| body: JSON.stringify({email, slackInvite, emailUpdates}) | ||
| }) | ||
| console.log(res) | ||
| } |
There was a problem hiding this comment.
The function does not return a response. Netlify Functions must return an object with statusCode, headers, and body properties, or the function will fail.
Fix by adding a return statement:
const res = await fetch('https://api.friggframework.org/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({email, slackInvite, emailUpdates})
})
console.log(res)
return {
statusCode: 200,
body: JSON.stringify({ message: 'processed' })
}| exports.handler = async function (event, context, callback) { | |
| const payload = JSON.parse(event.body).payload | |
| console.log(JSON.parse(event.body)) | |
| console.log(payload) | |
| console.log(payload.data["slack-invite"]) | |
| const email = payload.data.email | |
| const slackInvite = !!payload.data["slack-invite"] | |
| const emailUpdates = !!payload.data["update-emails"] | |
| console.log(email, slackInvite, emailUpdates) | |
| const res = await fetch('https://api.friggframework.org/subscribe', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json;charset=utf-8' | |
| }, | |
| body: JSON.stringify({email, slackInvite, emailUpdates}) | |
| }) | |
| console.log(res) | |
| } | |
| exports.handler = async function (event, context, callback) { | |
| const payload = JSON.parse(event.body).payload | |
| console.log(JSON.parse(event.body)) | |
| console.log(payload) | |
| console.log(payload.data["slack-invite"]) | |
| const email = payload.data.email | |
| const slackInvite = !!payload.data["slack-invite"] | |
| const emailUpdates = !!payload.data["update-emails"] | |
| console.log(email, slackInvite, emailUpdates) | |
| const res = await fetch('https://api.friggframework.org/subscribe', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json;charset=utf-8' | |
| }, | |
| body: JSON.stringify({email, slackInvite, emailUpdates}) | |
| }) | |
| console.log(res) | |
| return { | |
| statusCode: 200, | |
| body: JSON.stringify({ message: 'processed' }) | |
| } | |
| } | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Already handled in the current head — submission-created.js ends with return { statusCode: 200, body: JSON.stringify({ message: 'processed' }) }, so it returns a valid Netlify Functions response object.
Generated by Claude Code
Content / POV:
- Lead with the strong opinion that an integration is more than moving data.
New "primitives" section: ad hoc endpoints, queues, provider-native code
(Attio, HubSpot, Zapier, Zendesk, Salesforce) with shipped helpers, and
in-app UI via the emerging Fenestra spec.
- Add a "speaks the specs your tools know" strip: OpenAPI, AsyncAPI, Arazzo,
Overlays, JSON Schema, MCP, and Fenestra (new), positioned as sitting
alongside the standards rather than replacing them.
- Fix the use-cases intro that wrongly called Frigg "a framework for moving
data between systems," which contradicted the new POV.
- Nav gains Primitives.
Voice (per Left Hook content style guide):
- Remove all em-dashes (was 29; now 0), using periods, commas, and
parentheses instead.
- Lower the hype: reword the "backlog was a people problem / now it's an
agent's job" headline toward judgment ("deciding which to build is still
yours") and drop "the wheel builds itself" flourish.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
A spec-sheet section listing the real framework stack, pulled from the repo. Where there's a genuine choice, it's rendered as an orange "or": - Data store: PostgreSQL or MongoDB (both via Prisma ORM; Mongoose is no longer a declared dependency on next). - Encryption: AWS KMS or AES-256 (field-level, bring your own keys). - Cloud: AWS today, or GCP / Azure / self-hosted container. Other rows: Node.js 22+ / JavaScript, AWS Lambda, Serverless Framework (osls) + esbuild, generated serverless.yml + CloudFormation, AWS SQS, EventBridge Scheduler + cron events, SSM Parameter Store + Secrets Manager, Express via serverless-http, OAuth2 / API key / Basic, JSON Schema (JSONForms) + React admin UI, Jest / nock / in-memory DB, npm workspaces / Lerna / Nx, and the hexagonal/DDD architecture. Nav gains Stack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
Frigg natively supports OpenTelemetry (core ships @opentelemetry/api + SDK for traces and metrics with OTLP HTTP exporters; api-module requests are wrapped in telemetry spans). Add an Observability row to the stack spec sheet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
Embed an inline monochrome SVG sprite (Simple Icons, CC0) and render a brand glyph inside each stack chip in currentColor, so logos are theme-safe and add no external requests. Node.js, JavaScript, Serverless, esbuild, PostgreSQL, MongoDB, Prisma, Express, React, Jest, OpenTelemetry, npm, Lerna, Nx, Google Cloud, Docker, and JSON all use their marks; AWS-family chips (Lambda, SQS, KMS, SSM, Secrets Manager, CloudFormation, EventBridge) and Azure use a neutral cloud glyph since those marks are trademarked and unavailable. The Cloud row's alternative is split into GCP / Azure / container chips so each gets its logo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
- subscribe.js: the Zapier webhook fetch used an invalid `contentType` option (silently ignored). Send a proper `Content-Type: application/json` header instead (flagged by Graphite). - submission-created.js: return a 200 response object from the Netlify function handler instead of falling off the end (flagged by Graphite). - index.html: replace decorative Math.random() (weave canvas + terminal timing, not a security context) with a small deterministic generator, clearing the SonarCloud PRNG findings that were failing the security quality gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
New /roadmap page built from real data on the next branch: - Framework roadmap: all 27 ADRs (docs/architecture-decisions) as filterable cards (search, status All/Proposed/Shipped, theme), each linking to the ADR on GitHub. Data in website/roadmap/data/adrs.json. - API module directory: the full 224-API Left Hook catalog, searchable by name/provider/tag with category + complexity filters. Data in website/roadmap/data/apis.json. - Hybrid voting: live counts via Netlify Blobs (friggframework-api/vote.js + votes.js), one vote per browser (localStorage), plus a per-item "Request" / "Discuss" link to GitHub issues/discussions. Votes degrade gracefully to 0 when functions aren't running (e.g. local static preview). - Reuses the site's design system, fonts, and light/dark/system toggle. Wire-up: homepage nav + footer + the get-started "See the roadmap" link now point at /roadmap/ (previously roadmap.friggframework.org). Data snapshots and the voting functions were produced by parallel subagents; the page and integration were assembled against fixed contracts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
… roadmap - Logos: pull the 213 matching brand logos from Left Hook's marketing assets (keyed by catalog slug) into website/roadmap/logos/ and render each in a white chip on the directory cards, with a monogram fallback for the few APIs that have no logo. - Real library status: cross-reference the 224 catalog APIs against the actual friggframework/api-module-library packages (v1-ready + needs-updating). 35 APIs that ship a module now get a truthful green "In the library" badge, plus a new All / In library / Candidates filter. Non-built APIs keep an effort badge (Low/Medium/High). - Category grouping: the directory now groups cards under category headers with per-category counts (largest first); the category dropdown narrows to one group. Filter and grouping in one. Data (apis.json) gains logo, built, and library fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
A small concierge that answers questions about the framework, helps sketch an integration, and acts as a roadmap guide, with helpful Left Hook context. - website/friggframework-api/assistant.js: Netlify function on the AI Gateway (@anthropic-ai/sdk auto-reads ANTHROPIC_API_KEY + ANTHROPIC_BASE_URL). Grounded system prompt covering the framework, stack, primitives, roadmap themes, and Left Hook services. Per-IP rolling rate limit via Netlify Blobs (fails open), input clamps, history trimming, and graceful offline degradation when the gateway isn't configured. Model is env-overridable (ASSISTANT_MODEL); defaults to opus. A FREYA-SEAM marks where a Freya session will later drive the loop. - Chat widget on the homepage and roadmap page: launcher + panel, reuses the existing design tokens, dark/light aware, keyboard + reduced-motion friendly, full-screen on mobile. Model output is escaped before rendering; only links and code spans are formatted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113LVpu5eMEkoCmx3zuCHiq
Session handoff — on-site "Ask Frigg" assistant + Freya seamThis note is the context bundle for a new session that will wire the assistant to the Freya framework. The Freya repo isn't in this session's scope and can't be added retroactively, so the follow-up work needs a fresh session with the Freya repo added. Everything below is what that session needs to pick up cleanly. What exists now (this PR)The marketing site was migrated into It already works end to end as a single grounded Claude call. Freya is the next iteration: replace the one-shot call with a Freya-driven loop that retrieves ADR / API / catalog data at runtime via tools/MCP instead of relying on the baked-in static knowledge blob. Files to know
The Freya seam (the important part)
Direction for the Freya version: retrieve, don't hardcodeRight now grounding is a curated static string (
So the target architecture: small static system prompt (voice + rules + framework primitives) + Freya tool calls for anything that changes. Retire most of the Config the new session should preserve
Steps for the new session
Still outstanding (deploy, not code)
Generated by Claude Code |
Color: the accent was a made-up emerald (#2f8f68). Retune it to Frigg's actual brand green — the sage #71a087 from the Frigg logo/favicon, with #2c704f for emphasis and a lifted #84b39b for dark ground. Left Hook's orange (#ff5e00 / --dawn) stays a subtle secondary accent. Applied across the homepage and roadmap token blocks plus the incidental terminal/glow references. Bot: rename the on-site assistant from "Ask Frigg" to "Ask Freya" (label, title, aria-label, greeting on both pages) and give the server-side persona the name Freya, setting up the Freya-powered iteration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ixfrejnfZWd8TPZamZYdv
Replace the one-shot Anthropic Messages call at the FREYA-SEAM with the Freya
agent framework. Freya (@freyaframework/*) is an unpublished, ESM,
pnpm-workspace monorepo, so it's vendored as a prebuilt self-contained bundle
rather than an install-time dependency.
- website/tools/freya-vendor/: bundling seed (entry.mjs) + esbuild driver
(build.mjs) + README. entry.mjs wires a minimal runtime — AnthropicLLM over
the AI Gateway, in-memory memory/session/ontology adapters, FakeEmbedding —
and a small roadmap-retrieval tool surface (catalog_stats, search_adrs,
search_apis) over the committed ADR/API catalog. Exports
runTurn({systemPrompt, model, messages, data}) -> string.
- website/friggframework-api/lib/freya-runtime.mjs: the generated bundle
(deep-imports keep transformers-js embeddings and Postgres out of the graph;
their optional deps stay external behind lazy imports never hit here).
- assistant.js: answer() now dynamic-imports the bundle and drives a tool-using
turn, seeding a session from the widget's history. The HTTP contract, rate
limiter, and offline path are unchanged. KNOWLEDGE trimmed where the tools now
provide live catalog/ADR facts; system prompt steers the agent to its tools.
- netlify.toml: ship the bundle + roadmap JSON via included_files.
An empty ontology keeps memory capture a no-op, so a turn stays one Anthropic
call plus tool iterations. Verified end to end against a mock gateway: offline
path (200/offline), 405 on non-POST, and a full tool loop (tool_use -> tool
result -> grounded answer) through the real handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ixfrejnfZWd8TPZamZYdv
Replace the empty placeholder ontology with a typed Frigg domain model (Platform, ApiModule, Integration, Primitive, Capability, Adr, Visitor) with relationships and enum vocabulary. Freya renders it into the system prompt each turn, so the agent reasons in Frigg's real vocabulary and its tool arguments (category / status / complexity) line up with the catalog. Prompt-only grounding for now; the same typed entities make Freya's memory capture meaningful once the site has a durable memory store. Rebuilt the vendored bundle; verified the ontology renders into the system prompt and the offline / 405 / live tool-loop paths still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ixfrejnfZWd8TPZamZYdv
| if (m.content) { | ||
| contentBlocks.push({ type: "text", text: m.content }); | ||
| } | ||
| const toolUseId = m.metadata?.toolUseId || m.toolInvocations[0].toolName + "_" + Math.random().toString(36).slice(2); |
The generated website/friggframework-api/lib/freya-runtime.mjs is a prebuilt esbuild artifact of the Freya framework, not hand-authored source. SonarCloud was flagging it (e.g. Math.random for a fallback tool-use id — a false positive in a non-security context). Add a .sonarcloud.properties (the Automatic Analysis config file, which does not disable automatic analysis the way a sonar-project.properties would) excluding the vendored bundle from issues and duplication analysis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ixfrejnfZWd8TPZamZYdv
|


Summary
Migrates the friggframework.org marketing site into the Frigg core monorepo (from
lefthook--demo-frigg-application) so it can be iterated on alongside the framework and deployed from Netlify, and rebuilds the page for the current framework story.What's here
website/— the marketing site as a single, self-contained static page (inline CSS + vanilla JS, no build step, no framework runtime), plus the Netlify serverless functions inwebsite/friggframework-api/(subscribe,submission-created).netlify.toml(repo root) —base = "website", functions dir, and the originalapi./feedback.redirects preserved from the previous deploy config.Redesign highlights
mcp-toolsurface); own your stack and your pipes; wisdom of the crowd; zero-to-production in an afternoon; notes on easy adoption.prefers-color-schemedefault + persisteddata-themeoverride).form-namethesubmission-createdfunction reads, so the Netlify Forms flow is unchanged.Notes for reviewers
website/dir + rootnetlify.toml); it touches no packages. I intentionally did not add therelease/prereleaselabels so it doesn't trigger an npm prerelease — add them only if your release tooling expects them here.Test plan
netlify.tomlbase/publish/functions resolve correctly in a Netlify buildsubmission-createdfunctionapi./feedback.) behave as before🤖 Generated with Claude Code
Generated by Claude Code