Framer + Supabase guide: Dynamic CMS View Counter
Date:
In modern web development, displaying engagement metrics like view counts on articles or CMS pages is a common requirement. This guide will walk you through creating a dynamic, customizable view counter component in Framer, powered by a Supabase backend (database and Edge Functions). We'll cover everything from database setup to creating a flexible Framer Code Component with UI controls for appearance.
What We'll Build:
A view counter that displays text (e.g., "Views: 123").
The counter will increment only once per user session for a given page to prevent inflation.
Data will be stored and managed in a Supabase PostgreSQL database.
Server-side logic will be handled by a Supabase Edge Function.
The Framer component will allow customization of prefix text, suffix text, font, font size, and text color via the Framer UI.
Step 1: Setting Up the Supabase Database
First, we need a table in our Supabase database to store the view counts for each page.
Navigate to your Supabase project dashboard (app.supabase.com).
Go to the SQL Editor (under the Database section in the left sidebar).
Click New query and run the following SQL to create the page_views table:
This table stores a unique page_id (which we'll link to our CMS page slugs), the view_count, and timestamps for creation and last view. The UNIQUE constraint on page_id ensures we only have one row tracking views for each unique page.
Step 2: Creating the Supabase Edge Function
Edge Functions will handle the server-side logic for incrementing view counts and fetching them. This is crucial for security and reliable data operations.
In your Supabase dashboard, go to Database > Functions (or simply "Functions" in the newer UI).
Click Create a new function. Name it record-page-view.
Shared CORS Helper (Optional but Recommended):
Cross-Origin Resource Sharing (CORS) is a security mechanism. Since your Framer site will be on a different domain than your Supabase functions, you need to tell your function it's okay to accept requests from Framer.
In the function editor sidebar, create a new folder named _shared. Inside this folder, create a new file named cors.ts:record-page-view Function Code:
Paste the following code into your record-page-view/index.ts file:SQL Helper Function (Highly Recommended):
For atomic and reliable increments (especially important if multiple users view a page around the same time), create the following PostgreSQL function using the Supabase SQL Editor:Run this query once in your SQL Editor. This creates a reusable function in your database that the Edge Function can call.
Function Settings & Deployment:
In the settings for your record-page-view function within the Supabase dashboard, add your SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY as environment variables. You can find these in your Supabase Project Settings > API.
Under the function's settings, ensure JWT Verification is set to "No verification required" or "Anon key required". This is because our Framer component will call the function using the public anon key.
Click Deploy (or the equivalent button). Once deployed, Supabase will provide an Invocation URL for this function. Copy this URL; you'll need it for the Framer component.
Understanding Supabase Edge Functions in Our View Counter
Before we build the Framer component, let's quickly understand the role and benefits of using Supabase Edge Functions for our view counter.
What are Supabase Edge Functions?
Supabase Edge Functions are server-side TypeScript functions (running on Deno) that you can write and deploy directly within your Supabase project. They are "edge" functions because they are deployed globally on a distributed network, meaning they run physically close to your users, resulting in lower latency for the function execution itself.
Why Use an Edge Function Here?
For our view counter, using an Edge Function offers several key advantages over trying to interact with the database directly from the Framer component (client-side):
Security:
Protecting Credentials: Our Edge Function uses the SUPABASE_SERVICE_ROLE_KEY. This key has full administrative access to your database and should never be exposed in client-side code (like a Framer component). The Edge Function acts as a secure intermediary. The Framer component only needs the public anon key to invoke the function.
Controlled Access: The Edge Function defines exactly what operations can be performed (incrementing a view, getting a count). You don't expose your entire database schema or arbitrary query capabilities to the client.
Server-Side Logic:
Atomic Operations: Incrementing a counter reliably, especially with potential concurrent users, is best done server-side. Our increment_page_view SQL function, called by the Edge Function, ensures that view counts are updated correctly and atomically (as a single, indivisible operation), preventing race conditions where multiple increments might overwrite each other.
Data Validation & Transformation: While simple in this example, Edge Functions can perform complex data validation or transformations before writing to or reading from the database.
Performance (Reduced Client Burden & Potential Latency Improvement):
Reduced Client-Side Load: Offloads database interaction logic from the user's browser.
Edge Proximity: While your database might be in one region, the Edge Function itself can run closer to the user. For data-fetching operations that don't require complex computations before hitting the DB, this means the initial network hop can be faster.
Abstraction & Maintainability:
The Framer component interacts with a simple API endpoint (the Edge Function URL). It doesn't need to know the specifics of the database table structure or SQL queries. If you change your database schema or backend logic later, you might only need to update the Edge Function, potentially without any changes to the client-side Framer component.
How Our record-page-view Function Works:
Invocation: The Framer component sends an HTTP POST request to the Edge Function's unique URL. This request includes:
The apikey header (your Supabase anon key) for basic authentication allowing the function to be called.
A JSON body containing the pageId (e.g., the CMS slug) and an action (either incrementAndGet or getCount).
Authentication & Authorization (at the Gateway):
Supabase's gateway first checks the apikey header to ensure it's a valid key for your project.
If JWT Verification were enabled (it's not strictly for anon key calls), further checks would occur.
Function Execution:
Environment Variables: The function securely retrieves SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from its pre-configured environment variables.
Supabase Admin Client: It initializes a Supabase client instance using the service_role_key. This client has privileged access necessary to interact with the database according to the function's logic.
Processing the Request:
It parses the pageId and action from the request body.
If action is incrementAndGet: It calls the increment_page_view PostgreSQL function (via supabaseAdmin.rpc(...)). This SQL function handles the logic of either inserting a new page record with view_count = 1 or incrementing the view_count for an existing page.
If action is getCount: It directly queries the page_views table for the view_count associated with the given pageId.
Sending the Response:
It constructs a JSON response containing the viewCount.
This response is sent back to the Framer component.
CORS headers (from _shared/cors.ts) are included to allow the Framer site (running on a different domain) to successfully receive and process this response.
In essence, the Edge Function acts as a secure and specialized API endpoint for managing page views, shielding the database and providing a clean interface for the client-side Framer component.
Step 3: Building the Framer Code Component (ViewCounter.tsx)
Now, let's create the text-only React component in Framer.
In your Framer project, go to Assets > Code > New File. Name it ViewCounter.tsx.
Paste the following code:
This component includes:
State management for viewCount, isLoading, and error.
An useEffect hook that intelligently fetches or increments the view count. It uses localStorage to track if a page has been viewed in the current browser session, preventing multiple increments on page refresh.
Clear property controls defined with addPropertyControls for all necessary configurations and visual styling
(prefix/suffix text, font family/style/weight, font size, and text color).
Logic to display a placeholder ("123") in the Framer canvas to avoid live API calls during the design phase and provide visual feedback.
Step 4: Using the ViewCounter in Your Framer CMS
After saving ViewCounter.tsx, it will appear in your Assets panel in Framer (under "Code").
Drag your new ViewCounter component onto your CMS page template (this is the page that defines the layout for individual CMS items like blog posts or product pages).
Select the ViewCounter instance on the canvas.
In the Properties Panel on the right, you will see the custom controls we defined. Configure them as follows:
Page ID: This is the most critical field for unique tracking. Connect this field to the Slug field of your CMS collection (e.g., if your collection is "Blog Posts", you'd find the "Slug" variable under "Blog Posts -> Slug" when clicking the "+" icon next to the Page ID field).
Supabase URL: Enter your Supabase project URL (e.g., https://<your-project-ref>.supabase.co).
Supabase Anon Key: Enter your Supabase project's public anonymous key (the anon public key).
View Function URL: Paste the Invocation URL of your record-page-view Supabase Edge Function that you copied earlier.
Prefix Text: (Optional) Text to display before the count, like "Views: " or "Reads: ".
Font (Text): Use Framer's font picker to choose the font family, weight, and style.
Font Size (Text): Set the desired font size for the counter.
Text Color: Choose the color for the counter text.
Suffix Text: (Optional) Text to display after the count, like " views" or leave blank if your prefix already includes it.
How it Works - The Flow:
A user navigates to a CMS page on your Framer site.
The ViewCounter component mounts on that page.
Client-Side Check: The component first checks the browser's localStorage for a key like framerViewCounter_pageViewed_THE-PAGE-SLUG.
Determine Action:
If the key exists and is true, the page has already been "counted" in this session. The component will ask the Edge Function to just getCount.
If the key doesn't exist, this is a new view for the session. The component will ask the Edge Function to incrementAndGet.
API Call: The component makes an HTTP POST request to your record-page-view Edge Function, sending the pageId and the determined action.
Edge Function Processing:
The function receives the request.
Using the secure service_role_key, it interacts with your page_views Supabase table.
If incrementAndGet, it calls the increment_page_view SQL function (or the fallback). This either creates a new row for the page_id with view_count = 1 or increments the view_count for an existing page.
If getCount, it simply queries the current view_count.
The Edge Function returns the latest viewCount as JSON.
Update UI & Local Storage:
The ViewCounter component receives the viewCount and updates its display.
If the view was incremented (incrementAndGet), the component sets the localStorage key (framerViewCounter_pageViewed_THE-PAGE-SLUG to true), so subsequent refreshes or revisits within the same session won't re-increment.
Further Enhancements & Considerations:
This is a very simple implementation of a view counter, it is made to show how you can implement supabase into your Framer workflow. Here are things you can do to enhance the view counter fi you want to proceed with using it:
More Sophisticated "Session" or "Unique User" Tracking: localStorage is tied to a specific browser and can be cleared. For more robust unique user tracking over longer periods or across devices, consider integrating Supabase Authentication (even anonymous auth) to assign persistent user IDs.
Error Handling: The current component shows a simple "Error" message. You could enhance this with more specific error displays or retry mechanisms.
Accessibility (A11y): For purely decorative counters, ensure they don't interfere with screen readers. If the count is important information, ensure it's clearly conveyed.
Debouncing/Throttling: For extremely high-traffic sites where even session-based counting might generate many initial hits, you could explore debouncing increments on the client-side before sending to the Edge Function, though the per-session logic already handles most common scenarios.
Data Privacy: Be mindful of any data privacy regulations (like GDPR) if you were to extend this to track more specific user data. The current IP-based session tracking is generally low-impact.