GDPR Landing Pages with Disk and Azure Export
Build a GDPR-compliant landing page in BptLandingPage's visual editor, then persist the project file to local disk or Azure Blob Storage and reload it for future edits.
.bptl project file. Inside the editor you also
configure a GDPR consent popup that ships with the page. To persist the project across browser sessions
— for staff editing or for multi-tenant landing pages — you route the editor's
OnSave bytes to whatever storage you prefer: local disk, Azure Blob Storage, or anything else.
Part 1: GDPR support
Step 1: Enable the GDPR popup
Inside the editor, open the Project settings panel and tick GDPR popup.
The editor exposes the full GdprPopupSettings object through a structured form:
| Setting | Options | What it controls |
|---|---|---|
RenderLocation |
BottomBanner, TopBanner, CenterModal, BottomLeftCorner, BottomRightCorner | Where the popup appears on the visitor's first view. |
ConsentCategories |
Necessary (always on, locked), Functional, Analytics, Marketing | Per-category toggles the visitor sees. Necessary cannot be opted out of. |
FrameworkBadge |
DataPrivacyFramework, IsoIec27701, IabTcf, IcoUk | Optional compliance badge rendered next to the popup — signals which framework you follow. |
ExternalProvider |
None, OneTrust, CookieBot, CookieInformation, Ecomply, TrustArc, HubSpot | If non-None, the popup defers to an externally-hosted consent script instead of rendering its own UI. |
ConsentEventName |
String (default bpt-consent-changed) |
The CustomEvent name dispatched on window whenever consent state changes. |
Step 2: Listen for consent on the customer page
The exported page exposes a global window.bptConsent API. Use it from your own analytics or
marketing scripts to gate behavior on consent:
// Read the current consent state on page load.
const consent = window.bptConsent.getConsent();
if (consent?.categories?.analytics) {
// Fire your analytics pixel.
}
// React to changes during the visit.
window.addEventListener("bpt-consent-changed", e => {
if (e.detail.categories.marketing) {
loadRemarketingTags();
}
});
// Programmatically update consent (e.g. from a "Cookie settings" link in your footer).
window.bptConsent.setConsent({ marketing: true, analytics: true });
// Re-open the popup so the visitor can change their mind.
window.bptConsent.show();Step 3: Export the GDPR module as standalone JS
Once the popup is configured, the editor's "Export GDPR Module (.js)" button produces a self-contained JavaScript file. Host it on your CDN and reference it from any page that needs the same consent UI — the same configuration can be reused across an unlimited number of landing pages.
<!-- on any page that should show the same popup -->
<script src="https://cdn.example.com/consent/v1.js" defer></script>Part 2: Persistence — disk and Azure
Step 4: Capture the editor's save bytes
The editor saves to the native .bptl format, which is a self-contained project envelope.
Wire SaveMode="Both" so the bytes flow to your callback in addition to (or instead of) the
browser download:
@page "/admin/landing/{Slug}"
@using Bpt.Components.Tools
@inject ILandingPageStore Store
<BptLandingPage @bind-Value="_project"
Height="85vh"
Mode="LandingPageMode.Edit"
EnableSave="true"
EnableExport="true"
SaveMode="LandingPageSaveMode.Both"
OnSave="HandleSave" />
@code {
[Parameter] public string Slug { get; set; } = "";
private LandingPageProject? _project;
protected override async Task OnInitializedAsync()
{
var bytes = await Store.LoadAsync(Slug);
if (bytes is { Length: > 0 })
_project = BptlSerializer.Deserialize(bytes);
}
private async Task HandleSave(byte[] bytes)
{
await Store.SaveAsync(Slug, bytes);
}
}Step 5a: Persist to local disk
The simplest store is the server's filesystem. Drop the bytes in wwwroot/landingpages/ and
you're done:
public sealed class DiskLandingPageStore : ILandingPageStore
{
private readonly string _root;
public DiskLandingPageStore(IWebHostEnvironment env)
{
_root = Path.Combine(env.ContentRootPath, "landingpages");
Directory.CreateDirectory(_root);
}
public async Task SaveAsync(string slug, byte[] bytes)
{
var safeSlug = Path.GetFileName(slug); // strip any path traversal
var path = Path.Combine(_root, $"{safeSlug}.bptl");
await File.WriteAllBytesAsync(path, bytes);
}
public async Task<byte[]?> LoadAsync(string slug)
{
var safeSlug = Path.GetFileName(slug);
var path = Path.Combine(_root, $"{safeSlug}.bptl");
return File.Exists(path) ? await File.ReadAllBytesAsync(path) : null;
}
}Path.GetFileName (or a stricter regex) before composing a
file path — otherwise a slug like "../../etc/passwd" reads or writes outside your
intended directory.
Step 5b: Persist to Azure Blob Storage
For multi-tenant SaaS, multi-server deployments, or when content needs to be CDN-fronted, write to a blob
container instead. Add the Azure.Storage.Blobs NuGet package and swap the implementation:
using Azure.Storage.Blobs;
public sealed class AzureLandingPageStore : ILandingPageStore
{
private readonly BlobContainerClient _container;
public AzureLandingPageStore(IConfiguration config)
{
var connStr = config["Azure:Storage:ConnectionString"]
?? throw new InvalidOperationException("Missing Azure:Storage:ConnectionString");
_container = new BlobContainerClient(connStr, "landingpages");
_container.CreateIfNotExists();
}
public async Task SaveAsync(string slug, byte[] bytes)
{
var blob = _container.GetBlobClient($"{slug}.bptl");
using var ms = new MemoryStream(bytes);
await blob.UploadAsync(ms, overwrite: true);
}
public async Task<byte[]?> LoadAsync(string slug)
{
var blob = _container.GetBlobClient($"{slug}.bptl");
if (!await blob.ExistsAsync()) return null;
var response = await blob.DownloadContentAsync();
return response.Value.Content.ToArray();
}
}Register your chosen implementation in Program.cs:
// Local dev
builder.Services.AddSingleton<ILandingPageStore, DiskLandingPageStore>();
// Production
builder.Services.AddSingleton<ILandingPageStore, AzureLandingPageStore>();Step 6: Serve the page publicly
The .bptl file is the editable project format. To render a public-facing page from it,
load the same project bytes into a read-only BptLandingPage instance:
@page "/lp/{Slug}"
@using Bpt.Components.Tools
<BptLandingPage Value="_project"
Mode="LandingPageMode.View"
ReadOnly="true"
Minified="true" />
@code {
[Parameter] public string Slug { get; set; } = "";
private LandingPageProject? _project;
protected override async Task OnInitializedAsync()
{
var bytes = await Store.LoadAsync(Slug);
if (bytes is { Length: > 0 })
_project = BptlSerializer.Deserialize(bytes);
}
}/admin/landing/...) behind [Authorize(Roles="Editor")] and
viewer routes (/lp/...) anonymous. Same component, different Mode, different
authorization — that's the whole separation.