diff --git a/src/app.d.ts b/src/app.d.ts index c94ea08..a55a2be 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -2,7 +2,8 @@ import type { KVNamespace, D1Database, ExecutionContext, - CacheStorage + CacheStorage, + AnalyticsEngineDataset } from '@cloudflare/workers-types'; import type { OAuthSession } from '@atcute/oauth-node-client'; import type { Client } from '@atcute/client'; @@ -27,6 +28,7 @@ declare global { OAUTH_SESSIONS: KVNamespace; OAUTH_STATES: KVNamespace; DB: D1Database; + ANALYTICS: AnalyticsEngineDataset; CLIENT_ASSERTION_KEY: string; COOKIE_SECRET: string; CRON_SECRET: string; diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 0000000..c3a043a --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,36 @@ +type PageviewEvent = { + did: string; + handle: string; + page: string; + request: Request; +}; + +const BOT_PATTERN = /bot|crawl|spider|slurp|facebookexternalhit|embedly|preview/i; + +function refererHost(referer: string | null): string { + if (!referer) return ''; + try { + return new URL(referer).hostname; + } catch { + return ''; + } +} + +export function logPageview( + platform: App.Platform | undefined, + { did, handle, page, request }: PageviewEvent +): void { + const ae = platform?.env?.ANALYTICS; + if (!ae) return; + + const ua = request.headers.get('user-agent') ?? ''; + if (BOT_PATTERN.test(ua)) return; + + const country = request.headers.get('cf-ipcountry') ?? ''; + const referer = refererHost(request.headers.get('referer')); + + ae.writeDataPoint({ + indexes: [did], + blobs: [did, handle, page, country, referer] + }); +} diff --git a/src/lib/website/Account.svelte b/src/lib/website/Account.svelte index 582e81d..dc79667 100644 --- a/src/lib/website/Account.svelte +++ b/src/lib/website/Account.svelte @@ -4,8 +4,7 @@ import { getHandleOrDid } from '$lib/atproto/methods'; import type { WebsiteData } from '$lib/types'; import { Avatar, Button, Popover } from '@foxui/core'; - import CustomDomainModal, { customDomainModalState } from '$lib/website/CustomDomainModal.svelte'; - import SettingsModal, { settingsModalState } from '$lib/website/SettingsModal.svelte'; + import { settingsOverlayState } from '$lib/website/settings/SettingsOverlay.svelte'; let { data = $bindable() @@ -39,23 +38,12 @@ variant="ghost" onclick={() => { settingsPopoverOpen = false; - settingsModalState.show(); + settingsOverlayState.show(); }}>Settings - - - - - {/if} diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index d24925a..f85f0e1 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -23,6 +23,7 @@ import Context from './Context.svelte'; import Head from './Head.svelte'; import Account from './Account.svelte'; + import SettingsOverlay from './settings/SettingsOverlay.svelte'; import EditBar from './EditBar.svelte'; import SaveModal from './SaveModal.svelte'; import FloatingEditButton from './FloatingEditButton.svelte'; @@ -387,6 +388,7 @@ /> + diff --git a/src/lib/website/MadeWithBlento.svelte b/src/lib/website/MadeWithBlento.svelte index 8f77a1d..a03ddf0 100644 --- a/src/lib/website/MadeWithBlento.svelte +++ b/src/lib/website/MadeWithBlento.svelte @@ -3,10 +3,25 @@
- made with blento +
+ made with blento +
+
+ Imprint + · + Privacy +
diff --git a/src/lib/website/settings/SettingsOverlay.svelte b/src/lib/website/settings/SettingsOverlay.svelte new file mode 100644 index 0000000..009ec4e --- /dev/null +++ b/src/lib/website/settings/SettingsOverlay.svelte @@ -0,0 +1,91 @@ + + + + +{#if settingsOverlayState.visible} +
+ +
+
+

Settings

+ +
+ + +
+ + +
+
+ {#if settingsOverlayState.activeSection === 'layout'} + + {:else if settingsOverlayState.activeSection === 'domain'} + + {:else if settingsOverlayState.activeSection === 'account'} + + {/if} +
+
+
+{/if} diff --git a/src/lib/website/settings/sections/AccountSection.svelte b/src/lib/website/settings/sections/AccountSection.svelte new file mode 100644 index 0000000..26ed978 --- /dev/null +++ b/src/lib/website/settings/sections/AccountSection.svelte @@ -0,0 +1,99 @@ + + +

Account

+ +
+ +
+ +
+

Danger Zone

+ +

+ Delete all your Blento data (cards, pages, sections, and settings). This does + not delete your account — only your Blento site data stored in your PDS. +

+ + {#if deleteError} +

{deleteError}

+ {/if} + + {#if !deleteConfirm} + + {:else} +

+ Are you sure? This cannot be undone. +

+
+ + +
+ {/if} +
diff --git a/src/lib/website/settings/sections/CustomDomainSection.svelte b/src/lib/website/settings/sections/CustomDomainSection.svelte new file mode 100644 index 0000000..d785391 --- /dev/null +++ b/src/lib/website/settings/sections/CustomDomainSection.svelte @@ -0,0 +1,250 @@ + + +{#if step === 'current'} +

Custom Domain

+ +
+ {currentDomain} +
+ +
+ + + +
+{:else if step === 'input'} +

Custom Domain

+ +
+ +
+ +
+ + +
+{:else if step === 'instructions'} +

Set up your domain

+ +

+ Add a CNAME record for your domain pointing to: +

+ +
+ blento-proxy.fly.dev + +
+ +
+ + +
+{:else if step === 'verifying'} +

Verifying...

+ +

+ Checking DNS records and verifying your domain. +

+ +
+ + + + + Verifying... +
+{:else if step === 'removing'} +

Removing...

+ +
+ + + + + Removing domain... +
+{:else if step === 'success'} +

Domain verified!

+ +

+ Your custom domain {domain} has been set up successfully. +

+ +
+ +
+{:else if step === 'error'} +

Verification failed

+ +

+ {errorMessage} +

+ {#if errorHint} +

+ {errorHint} +

+ {/if} + +
+ + +
+{/if} diff --git a/src/lib/website/settings/sections/LayoutSection.svelte b/src/lib/website/settings/sections/LayoutSection.svelte new file mode 100644 index 0000000..9c251c8 --- /dev/null +++ b/src/lib/website/settings/sections/LayoutSection.svelte @@ -0,0 +1,82 @@ + + +

Layout Sync

+

+ Control how desktop and mobile layouts stay in sync. +

+ +
+ {#each options as option (option.key)} + {@const isSelected = option.key === (selected === undefined ? 'automatic' : selected)} + + {/each} +
diff --git a/src/routes/(auth)/oauth/callback/+server.ts b/src/routes/(auth)/oauth/callback/+server.ts index 63bedee..0f31252 100644 --- a/src/routes/(auth)/oauth/callback/+server.ts +++ b/src/routes/(auth)/oauth/callback/+server.ts @@ -33,5 +33,5 @@ export const GET: RequestHandler = async ({ url, platform, cookies, request }) = const profile = await getDetailedProfile({ did }).catch(() => undefined); const actor = profile?.handle && profile.handle !== 'handle.invalid' ? profile.handle : did; - redirect(303, `/${actor}`); + redirect(303, `/${actor}/edit`); }; diff --git a/src/routes/(legal)/+layout.svelte b/src/routes/(legal)/+layout.svelte new file mode 100644 index 0000000..91ba69a --- /dev/null +++ b/src/routes/(legal)/+layout.svelte @@ -0,0 +1,26 @@ + + +
+
+ + ← Back to blento + + +
+ {@render children()} +
+ + +
+
diff --git a/src/routes/(legal)/imprint/+page.svelte b/src/routes/(legal)/imprint/+page.svelte new file mode 100644 index 0000000..621b973 --- /dev/null +++ b/src/routes/(legal)/imprint/+page.svelte @@ -0,0 +1,97 @@ + + Imprint · blento + + + +

Imprint

+

Last updated: April 18, 2026

+ +

+ Information in accordance with § 5 TMG (German Telemedia Act) and § 18 MStV (German + Interstate Media Treaty). +

+ +

Operator

+

Florian Killius

+ +

Contact

+ + +

Responsible for Content

+

+ Responsible for content according to § 18 Abs. 2 MStV:
+ Florian Killius +

+ +

EU Dispute Resolution

+

+ The European Commission provides a platform for online dispute resolution (OS): + https://ec.europa.eu/consumers/odr. We are not obliged and not willing to participate in dispute resolution proceedings before a + consumer arbitration board (§ 36 VSBG). +

+ +

Reporting Illegal Content (DSA)

+

+ In accordance with Article 16 of the EU Digital Services Act (Regulation (EU) 2022/2065), anyone + may report content that they consider illegal by sending a notice to + hello@blento.app. Please include: +

+
    +
  • + a sufficiently substantiated explanation of why you believe the content is illegal under EU or + Member State law; +
  • +
  • the exact URL(s) of the content;
  • +
  • + your name and email address (unless the report concerns offences under Arts. 3–7 of Directive + 2011/93/EU, in which case anonymity is allowed); +
  • +
  • a statement, in good faith, that the information in the notice is accurate and complete.
  • +
+

+ Submitting a complete notice gives us "actual knowledge" under Art. 6 DSA, and we will act + expeditiously to assess and, where appropriate, remove or disable access to the content. You will + receive an acknowledgement and, where feasible, a statement of reasons. +

+ +

Disclaimer

+ +

Liability for Content

+

+ The content of this site has been created with the utmost care. However, no guarantee is given for + the accuracy, completeness, or timeliness of the content. As a service provider, we are + responsible for our own content on these pages under general law. We are not, however, obliged to + monitor transmitted or stored third-party information, or to investigate circumstances that + indicate illegal activity. +

+ +

Liability for Links

+

+ Our site contains links to external websites of third parties, over whose content we have no + influence. Therefore, we cannot assume any liability for this external content. The respective + provider or operator of the linked pages is always responsible for the content of the linked + pages. +

+ +

User-Generated Content

+

+ blento renders content authored by its users and stored in their own Personal Data Servers on the + AT Protocol network. Responsibility for such content lies with the respective user. If you believe + user-generated content accessed via blento infringes your rights or violates applicable law, + please contact us at hello@blento.app. +

+ +

Copyright

+

+ Content created by the operator of this site is subject to copyright law. Reproduction, + processing, distribution, or any form of commercialisation of such material beyond the scope of + copyright law requires the prior written consent of its respective author or creator. +

diff --git a/src/routes/(legal)/privacy/+page.svelte b/src/routes/(legal)/privacy/+page.svelte new file mode 100644 index 0000000..3854ed9 --- /dev/null +++ b/src/routes/(legal)/privacy/+page.svelte @@ -0,0 +1,250 @@ + + Privacy Policy · blento + + + +

Privacy Policy

+

Last updated: April 18, 2026

+ +

1. Overview

+

+ blento is built on the AT Protocol. Your site content lives in your own Personal Data Server (PDS) + under your atmosphere account. This policy explains what limited data blento itself handles and + how we comply with the EU General Data Protection Regulation (GDPR), the German Federal Data + Protection Act (BDSG), and the German Telecommunications-Digital-Services Data Protection Act + (TDDDG, formerly TTDSG). +

+ +

2. Definitions

+
    +
  • + AT Protocol (atproto): the open decentralized protocol that the Service is + built on. See + atproto.com. +
  • +
  • + Atmosphere: the open network of applications and services built on the AT Protocol. + Bluesky is one application in the atmosphere; blento is another. +
  • +
  • + Atmosphere account: your identity on the atmosphere, represented by a DID (decentralized + identifier) and a handle. You can use the same account across any atmosphere service, including blento. +
  • +
  • + Personal Data Server (PDS): the server that hosts the data for your atmosphere account. + It stores your records (including the cards you create on blento) and is the canonical home of your + content. You can self-host your PDS or use one operated by a provider. +
  • +
+ +

3. Controller

+

The controller responsible for processing under Art. 4(7) GDPR is:

+

+ Florian Killius
+ Email: hello@blento.app +

+

+ We have not appointed a data protection officer; we are below the thresholds of § 38 BDSG. + For any data-protection inquiries, contact the address above. +

+ +

4. Categories of Data and Legal Bases

+ +

4.1 Authentication (atproto OAuth)

+

+ When you sign in, we initiate an OAuth flow with your PDS. We receive and store, in a session + cookie and in server-side session storage, an access token, a refresh token, your DID, and your + handle. We do not receive or store your password. +

+
    +
  • Purpose: authenticating you and keeping you signed in.
  • +
  • + Legal basis: Art. 6(1)(b) GDPR (performance of the service you requested). +
  • +
  • + Retention: until you sign out or the refresh token expires / is revoked. +
  • +
  • + Storage of auth tokens on your device: legally necessary for the service you requested + under § 25(2) Nr. 2 TDDDG; no consent required. +
  • +
+ +

4.2 Your DID and handle

+

Used to load and save your bento grid records from and to your PDS.

+
    +
  • Legal basis: Art. 6(1)(b) GDPR.
  • +
  • Retention: for as long as your account is active.
  • +
+ +

4.3 Cached content (Cloudflare KV)

+

+ To speed up rendering and reduce load on third-party APIs, we cache public PDS records, profile + data, and third-party card data in Cloudflare Workers KV. Typical cache lifetimes range from 1 + hour (e.g. Last.fm listens, events) to 24 hours (profile and PDS records), up to 30 days for + generated Open Graph images. Cached entries expire automatically. +

+
    +
  • Purpose: performance and reduction of third-party API load.
  • +
  • + Legal basis: Art. 6(1)(f) GDPR (legitimate interest in operating the Service efficiently). +
  • +
  • Retention: as stated above; expires automatically.
  • +
+ +

4.4 Server and request logs

+

+ Cloudflare, our hosting provider, processes standard request metadata (IP address, user agent, + timestamp, URL) for security, abuse prevention, and delivery of the Service. We do not maintain + long-term logs ourselves; Cloudflare's default retention applies (typically a few days). +

+
    +
  • + Legal basis: Art. 6(1)(f) GDPR (legitimate interest in a secure and reliable service). +
  • +
+ +

4.5 Aggregate analytics

+

+ We collect cookieless, aggregate pageview statistics using Cloudflare Workers Analytics Engine. + Each pageview records the visited page, a coarse country code derived from the visitor's IP + address (the IP itself is not stored by us), and the hostname of the referring website if any. + Individual visitors are not tracked across pages or sessions, and no persistent identifier is + stored on your device. +

+
    +
  • Purpose: understanding aggregate site usage to guide improvements.
  • +
  • + Legal basis: Art. 6(1)(f) GDPR (legitimate interest in measuring usage in a privacy-preserving + way). +
  • +
  • + Retention: up to 90 days, after which Cloudflare automatically deletes the data. +
  • +
+ +

4.6 Third-party embeds

+

+ Cards can embed content from third parties (Bluesky, YouTube, GitHub, Last.fm, map providers, + etc.). When a page containing such a card is loaded, the visitor's browser makes requests to those + providers, which may process the visitor's IP address and other request metadata under their own + privacy policies. We do not control that processing. +

+ +

5. Recipients and Processors

+
    +
  • + Cloudflare, Inc. (USA) — hosting, CDN, Workers runtime, KV storage. Acts as a processor + under Art. 28 GDPR based on Cloudflare's Data Processing Addendum. +
  • +
  • + Your PDS provider (the operator of your atmosphere account's data server) — receives + your records when you save them. +
  • +
  • + Third-party card providers (e.g. Bluesky, GitHub, YouTube, Last.fm, map providers) + — when embedded, they receive visitor request data directly. +
  • +
+ +

6. International Data Transfers

+

Cloudflare processes data in the United States and globally. Transfers are safeguarded by:

+
    +
  • + Cloudflare's certification under the EU-US Data Privacy Framework (adequacy decision + of the European Commission of 10 July 2023); and +
  • +
  • EU Standard Contractual Clauses (Art. 46(2)(c) GDPR) as a fallback.
  • +
+

+ Third-party card providers process data in their own jurisdictions under their respective + safeguards. +

+ +

7. Cookies and Similar Technologies

+

+ We use only strictly necessary cookies / local storage entries for authentication (§ 25(2) + Nr. 2 TDDDG). We do not use cookies or tracking technologies for analytics, advertising, or + profiling. No consent banner is required. +

+ +

8. Your Rights

+

Under the GDPR you have the following rights:

+
    +
  • Right of access (Art. 15 GDPR)
  • +
  • Right to rectification (Art. 16 GDPR)
  • +
  • Right to erasure / "to be forgotten" (Art. 17 GDPR)
  • +
  • Right to restriction of processing (Art. 18 GDPR)
  • +
  • Right to data portability (Art. 20 GDPR)
  • +
  • + Right to object to processing based on legitimate interests (Art. 21 GDPR) — including a + general right to object at any time, on grounds relating to your particular situation +
  • +
  • + Right not to be subject to automated decision-making (Art. 22 GDPR) — we do not carry out + any such processing +
  • +
  • + Right to withdraw consent at any time, where processing is based on consent (Art. 7(3) GDPR) + — we currently do not rely on consent for any processing +
  • +
+

+ To exercise any of these rights, email hello@blento.app. +

+ +

9. Right to Lodge a Complaint

+

+ You have the right to lodge a complaint with a supervisory authority (Art. 77 GDPR). The competent + authority for us is: +

+

+ Berliner Beauftragte für Datenschutz und Informationsfreiheit (BlnBDI)
+ Alt-Moabit 59–61, 10555 Berlin, Germany
+ www.datenschutz-berlin.de +

+

+ You may also lodge a complaint with any other supervisory authority, in particular in the Member + State of your habitual residence. +

+ +

10. Data Deletion

+

+ Because your site content lives in your PDS, you can delete it directly via any atproto client or + by editing your site. Cached copies on our side expire automatically. For account deletion or + requests covering data beyond what you can delete yourself, email + hello@blento.app. +

+ +

11. Children

+

+ The Service is not directed at children. In Germany, the consent of a holder of parental + responsibility is required for children under the age of 16 (Art. 8 GDPR). We do not knowingly + process personal data of children under 16. +

+ +

12. What We Don't Do

+
    +
  • We do not sell your personal data.
  • +
  • We do not run advertising or cross-site tracking.
  • +
  • We do not build behavioural profiles of you.
  • +
  • We do not carry out automated decision-making in the sense of Art. 22 GDPR.
  • +
+ +

13. Changes

+

+ We may update this policy. Material changes will be announced on this page, and where they + materially affect your rights we will give reasonable advance notice. +

+ +

14. Contact

+

Questions about this policy? Reach out via:

+ diff --git a/src/routes/(legal)/terms/+page.svelte b/src/routes/(legal)/terms/+page.svelte new file mode 100644 index 0000000..509dca2 --- /dev/null +++ b/src/routes/(legal)/terms/+page.svelte @@ -0,0 +1,183 @@ + + Terms of Service · blento + + + +

Terms of Service

+

Last updated: April 18, 2026

+ +

1. Scope and Acceptance

+

+ These Terms of Service ("Terms") govern the use of blento ("the Service"), operated by Florian + Killius, Berlin, Germany ("we", "us"). Contact details and the full operator address are set out + in our Imprint. +

+

+ By signing in to or otherwise using the Service, you confirm that you have read these Terms and + agree to be bound by them. If you do not agree, please do not use the Service. These Terms are + made available in full on this page at all times and you can save or print them before you + proceed. +

+ +

2. Definitions

+
    +
  • + AT Protocol (atproto): the open decentralized protocol that the Service is + built on. See + atproto.com. +
  • +
  • + Atmosphere: the open network of applications and services built on the AT Protocol. + Bluesky is one application in the atmosphere; blento is another. +
  • +
  • + Atmosphere account: your identity on the atmosphere, represented by a DID and a handle. +
  • +
  • + Personal Data Server (PDS): the server that hosts the data for your atmosphere account. + Content you create on blento is stored in your PDS and is the canonical copy. +
  • +
+ +

3. The Service

+

+ blento is a bento-grid website builder powered by the AT Protocol. Content you create is stored in + your PDS under your atmosphere account; blento does not host the primary copy of that data. The + Service is provided free of charge; we reserve the right to change, suspend, or discontinue + features with reasonable notice. +

+ +

4. Your Account

+

+ You authenticate via atproto OAuth using your atmosphere account. You are responsible for + maintaining the security of your account and for all activity that occurs through it. You must be + at least 16 years old to use the Service, or you must have the consent of a holder of parental + responsibility. +

+ +

5. User Content

+

+ You retain all ownership rights in the content you create. You are solely responsible for the + content you publish and confirm you have the necessary rights to share it. By publishing content + through the Service, you grant us a worldwide, non-exclusive, royalty-free licence to host, cache, + reproduce, and display that content solely for the purpose of operating the Service, for as long + as you keep it published. +

+

+ Content that is illegal, infringes third-party rights, or violates our acceptable-use rules may be + removed from public rendering on the Service at our discretion. Because your canonical copy lives + in your PDS, our removal affects only how blento renders it; you retain access to the data itself. +

+ +

6. Acceptable Use

+

You agree not to use the Service to:

+
    +
  • Violate any applicable law or regulation;
  • +
  • Publish content that infringes intellectual-property or privacy rights;
  • +
  • Distribute malware, phishing, or other harmful material;
  • +
  • Harass, threaten, or harm others, or incite hatred or violence;
  • +
  • Attempt to disrupt, overload, or compromise the Service or circumvent access controls.
  • +
+ +

7. Reporting Illegal Content (DSA)

+

+ In accordance with Article 16 of the EU Digital Services Act (Regulation (EU) 2022/2065), anyone + may notify us of content they consider illegal by emailing + hello@blento.app with: +

+
    +
  • a substantiated explanation of why you believe the content is illegal;
  • +
  • the exact URL(s);
  • +
  • + your name and email (unless reporting offences under Arts. 3–7 of Directive 2011/93/EU); +
  • +
  • a good-faith statement that the information is accurate and complete.
  • +
+

+ We will acknowledge receipt, assess the report in good faith, and act expeditiously where + appropriate. Where feasible, we will provide a statement of reasons for any action taken. +

+ +

8. Disclaimer

+

+ The Service is provided as is and as available. We do not warrant that the Service will be + uninterrupted, error-free, or that cached third-party content will be current or accurate. + Statutory warranty rights under German law remain unaffected. +

+ +

9. Liability

+

+ We are liable without limitation for damages caused by intent or gross negligence, for injury to + life, body, or health, under the German Product Liability Act (ProdHaftG), to the extent of any + warranty we have expressly given, and in any other case of mandatory statutory liability. +

+

+ For damages caused by ordinary negligence, we are liable only where we breach an essential + contractual obligation — that is, an obligation whose fulfilment makes the proper + performance of the contract possible in the first place and on whose observance you may regularly + rely ("Kardinalpflichten"). Our liability in such cases is limited to damages that are typical for + this kind of contract and reasonably foreseeable. +

+

Any further liability for ordinary negligence is excluded.

+

+ Because the Service is provided free of charge, the typical use case involves no paid services. + Nothing in this section restricts liability that cannot be restricted or excluded under mandatory + law. +

+ +

10. Termination

+

+ You may stop using the Service at any time. On request to + hello@blento.app we will delete any data we hold about your account + that cannot be deleted by you directly via your PDS. We may suspend or terminate accounts that violate + these Terms, with notice where reasonably possible. +

+ +

11. Changes to These Terms

+

+ We may update these Terms where necessary, for example to reflect legal changes or new features. + We will notify you of material changes at least 30 days before they take effect, by posting the + updated Terms on this page with a new "last updated" date and, where you have an active account + with a reachable contact, by a reasonable additional means. If you do not object within 30 days + after notification, the updated Terms become effective; you have the right to terminate your use + of the Service during this period if you do not agree. +

+ +

12. Governing Law and Venue

+

+ These Terms are governed by the laws of the Federal Republic of Germany, excluding the UN + Convention on Contracts for the International Sale of Goods. If you are a consumer with habitual + residence in another EU Member State, mandatory consumer-protection rules of that state remain + unaffected. +

+

+ Exclusive place of jurisdiction for all disputes arising from or in connection with these Terms is + Berlin, Germany, to the extent you are a merchant, a legal person under public law, or a special + fund under public law, or you have no general place of jurisdiction in Germany. For consumers, the + statutory places of jurisdiction apply. +

+ +

13. Consumer Dispute Resolution

+

+ The European Commission provides an online dispute resolution platform (OS): + https://ec.europa.eu/consumers/odr. We are neither obliged nor willing to participate in a dispute-resolution procedure before a + consumer arbitration board (§ 36 VSBG). +

+ +

14. Severability

+

+ Should any provision of these Terms be or become invalid or unenforceable, the validity of the + remaining provisions shall not be affected. +

+ +

15. Contact

+

Questions about these Terms? Reach out via:

+ diff --git a/src/routes/[[actor=actor]]/(pages)/+layout.server.ts b/src/routes/[[actor=actor]]/(pages)/+layout.server.ts index b906045..1ef6ed9 100644 --- a/src/routes/[[actor=actor]]/(pages)/+layout.server.ts +++ b/src/routes/[[actor=actor]]/(pages)/+layout.server.ts @@ -3,6 +3,7 @@ import { env } from '$env/dynamic/private'; import { error } from '@sveltejs/kit'; import { createCache } from '$lib/cache'; import { getActor } from '$lib/actor.js'; +import { logPageview } from '$lib/analytics'; export async function load({ params, platform, request, locals, route, setHeaders }) { if (env.PUBLIC_IS_SELFHOSTED) error(404); @@ -28,5 +29,14 @@ export async function load({ params, platform, request, locals, route, setHeader setHeaders({ 'Cache-Control': 'private, no-store' }); } + if (!isInteractiveRoute) { + logPageview(platform, { + did: data.did, + handle: data.handle, + page: params.page ?? 'self', + request + }); + } + return data; } diff --git a/src/routes/[[actor=actor]]/api/reindex/+server.ts b/src/routes/[[actor=actor]]/api/reindex/+server.ts new file mode 100644 index 0000000..28a2b45 --- /dev/null +++ b/src/routes/[[actor=actor]]/api/reindex/+server.ts @@ -0,0 +1,54 @@ +import { json } from '@sveltejs/kit'; +import { contrail, ensureInit } from '$lib/contrail'; +import { listRecords } from '$lib/atproto/methods'; +import { collections } from '$lib/atproto/settings'; +import { getActor } from '$lib/actor'; +import type { Did } from '@atcute/lexicons'; +import { dev } from '$app/environment'; + +export async function GET({ params, platform, locals, request }) { + if (!locals.did) { + return json({ error: 'Not authenticated' }, { status: 401 }); + } + + const actor = await getActor({ request, paramActor: params.actor, platform }); + if (!actor) { + return json({ error: 'Actor not found' }, { status: 404 }); + } + + if (actor !== locals.did && !dev) { + return json({ error: 'You can only reindex your own account' }, { status: 403 }); + } + + const db = platform?.env?.DB; + if (!db) { + return json({ error: 'Database not available' }, { status: 503 }); + } + + const did = actor as Did; + await ensureInit(db); + + // Fetch all records from the user's PDS across all tracked collections + const uris: string[] = []; + + await Promise.all( + collections.map(async (collection) => { + try { + const records = await listRecords({ did, collection, limit: 0 }); + for (const record of records) { + uris.push(record.uri); + } + } catch { + // Collection may not exist for this user — skip + } + }) + ); + + if (uris.length === 0) { + return json({ ok: true, indexed: 0, deleted: 0 }); + } + + const result = await contrail.notify(uris, db); + + return json({ ok: true, indexed: result.indexed, deleted: result.deleted }); +} diff --git a/wrangler.jsonc b/wrangler.jsonc index 036969c..a882e82 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -64,6 +64,12 @@ "remote": true } ], + "analytics_engine_datasets": [ + { + "binding": "ANALYTICS", + "dataset": "blento_pageviews" + } + ], "triggers": { "crons": ["*/5 * * * *"] } -- 2.51.2 From 1c7fd9be6d0e10dc492f9ea5b11c8d75bf6d8e33 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 18 Apr 2026 10:32:53 +0200 Subject: [PATCH 2/2] remove inspect --- src/lib/components/card-command/CardCommand.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/components/card-command/CardCommand.svelte b/src/lib/components/card-command/CardCommand.svelte index df1b65e..13187d1 100644 --- a/src/lib/components/card-command/CardCommand.svelte +++ b/src/lib/components/card-command/CardCommand.svelte @@ -38,8 +38,6 @@ ) ); - $inspect(filteredCardDefs, 'filteredCardDefs'); - let cardDefGroups = $derived([ 'Core', ...Array.from(