diff --git a/GEO-AUDIT-REPORT.md b/GEO-AUDIT-REPORT.md new file mode 100644 index 00000000..30e93df7 --- /dev/null +++ b/GEO-AUDIT-REPORT.md @@ -0,0 +1,299 @@ +# GEO Audit Report: certified.app + +**Date:** 2026-04-06 +**URL:** https://certified.app +**Business Type:** SaaS — Identity & Authentication Platform +**Operated By:** Hypercerts Foundation + +--- + +## Composite GEO Score: 20/100 (Critical) + +| Category | Weight | Score | Weighted | +|----------|--------|-------|----------| +| AI Citability & Visibility | 25% | 14/100 | 3.5 | +| Brand Authority Signals | 20% | 12/100 | 2.4 | +| Content Quality & E-E-A-T | 20% | 28/100 | 5.6 | +| Technical Foundations | 15% | 38/100 | 5.7 | +| Structured Data | 10% | 5/100 | 0.5 | +| Platform Optimization | 10% | 18/100 | 1.8 | +| **TOTAL** | **100%** | | **19.5 -> 20** | + +**Score Interpretation:** The site is virtually invisible to AI search engines. The homepage — the only marketing content page — is entirely client-rendered and produces a blank loading spinner for all AI crawlers. There is no robots.txt, no sitemap, no llms.txt, no structured data, and near-zero brand presence on platforms AI models use for entity recognition. + +--- + +## Platform Readiness + +| Platform | Score | Status | +|----------|-------|--------| +| Google AI Overviews | 22/100 | Poor | +| Google Gemini | 16/100 | Critical | +| Bing Copilot | 15/100 | Critical | +| Perplexity AI | 14/100 | Critical | +| ChatGPT Web Search | 12/100 | Critical | + +--- + +## Top Findings + +### CRITICAL — Homepage is invisible to AI crawlers + +The homepage component (`src/components/landing/home-client.tsx`) is marked `"use client"` and loads all sections via `next/dynamic`. The raw HTML that AI crawlers receive is: + +```html +
+
+ +
+ +
+``` + +No headings, no text, no links. GPTBot, ClaudeBot, PerplexityBot, and all other AI crawlers see **nothing**. The Terms, Privacy, and DSA pages are correctly server-rendered. + +### CRITICAL — No crawl infrastructure exists + +| File | Status | +|------|--------| +| robots.txt | 404 — Missing | +| sitemap.xml | 404 — Missing | +| llms.txt | 404 — Missing | +| canonical tags | Missing on all pages | +| OG/Twitter meta | Missing on all pages | + +### CRITICAL — Zero structured data + +No JSON-LD, no Microdata, no RDFa on any page. Missing: Organization, SoftwareApplication, WebSite, FAQPage, BreadcrumbList, speakable. AI models have no structured way to understand what Certified is. + +### CRITICAL — Near-zero brand recognition for AI models + +| Platform | Present? | +|----------|----------| +| Wikipedia/Wikidata | No | +| Reddit | No | +| YouTube | No | +| LinkedIn | Unknown | +| GitHub | Yes (34 repos, 47 followers) | +| Product Hunt / G2 / Capterra | No | + +The brand name "Certified" is a common English word, making entity disambiguation nearly impossible without strong external signals. + +### HIGH — Thin content with weak E-E-A-T + +- Homepage: ~250 words of marketing copy (well below 300-word minimum) +- Total site content: ~7,050 words, of which ~6,800 are legal text +- No about page, no blog, no docs, no changelog +- No author bylines, no team credentials +- Zero statistics or data points anywhere +- No product screenshots + +### HIGH — Subpage metadata is generic + +Pages `/terms`, `/privacy`, and `/dsa` inherit the root layout defaults: +- Title: "Certified" +- Description: "Your identity, everywhere." + +These should have unique, page-specific titles and descriptions. + +--- + +## E-E-A-T Breakdown + +| Dimension | Score | Key Gap | +|-----------|-------|---------| +| Experience | 3/25 | No case studies, no original data, no first-hand accounts | +| Expertise | 7/25 | No author bylines, no technical depth on landing page | +| Authoritativeness | 6/25 | No about page, no external citations, no media mentions | +| Trustworthiness | 14/25 | Strong legal pages, HTTPS, but no contact info on homepage | + +--- + +## Citability Assessment + +The highest-scoring content blocks are FAQ answers at 24-26/100. No content block on the site reaches the citation-ready threshold of 70/100. Key problems: + +- Zero statistical density across the entire site +- No self-contained explanatory paragraphs (everything is 1-sentence fragments) +- No "What is Certified?" definition block +- Client-side rendering makes even the best content invisible to crawlers + +--- + +## Technical Details + +| Check | Result | +|-------|--------| +| HTTPS | Yes (required for .app TLD) | +| HSTS | Present (max-age=63072000) | +| CSP | Missing | +| X-Frame-Options | Missing | +| X-Content-Type-Options | Missing | +| SSR | Homepage: No (CSR bailout). Subpages: Yes | +| Framework | Next.js App Router on Vercel | +| Mobile | Good (Tailwind responsive, viewport meta correct) | +| URL structure | Excellent (clean, flat, descriptive) | +| LCP risk | High (homepage requires full JS execution) | +| CLS risk | Medium (no image dimensions, dynamic imports) | + +--- + +## Prioritized Action Plan + +### Quick Wins (Low effort, high impact) + +| # | Action | Impact | Platforms Affected | +|---|--------|--------|--------------------| +| 1 | Create `src/app/robots.ts` allowing all crawlers + sitemap reference | Crawl infrastructure | All 5 | +| 2 | Create `src/app/sitemap.ts` listing all public routes | Page discovery | All 5 | +| 3 | Add `public/llms.txt` describing the site for AI systems | AI context | ChatGPT, Perplexity, Claude | +| 4 | Add page-specific metadata to /terms, /privacy, /dsa | Indexability | All 5 | +| 5 | Add OG and Twitter Card meta tags to all pages | Social/AI previews | All 5 | +| 6 | Add canonical URLs to all pages | Duplicate prevention | All 5 | +| 7 | Link to GitHub org from footer | Entity signal (Microsoft property) | Bing Copilot, ChatGPT | + +### Medium-Term (Medium effort, critical impact) + +| # | Action | Impact | Platforms Affected | +|---|--------|--------|--------------------| +| 8 | **Fix homepage SSR** — extract landing sections from `"use client"` HomeClient into server components. Keep only interactive elements (sign-in, FAQ accordion) as client islands. This is the #1 priority. | Makes all landing content visible to crawlers | All 5 | +| 9 | Add JSON-LD structured data: Organization (Hypercerts Foundation + sameAs), SoftwareApplication (Certified), WebSite, FAQPage, BreadcrumbList | Entity recognition | All 5 | +| 10 | Create an `/about` page (800-1200 words): who operates Certified, team, mission, AT Protocol explanation, founding story | E-E-A-T, citability | All 5 | +| 11 | Add a "What is Certified?" definition block to the homepage — 2-3 self-contained sentences an AI can extract as a canonical answer | Citability | All 5 | +| 12 | Add statistics and specificity: partner app count, account creation time, protocol details, uptime, any measurable claims | Citability | All 5 | +| 13 | Add security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) via next.config.ts | Security/trust | Google, Bing | +| 14 | Register with Bing Webmaster Tools + implement IndexNow | Bing indexing | Bing Copilot | + +### Strategic (High effort, long-term impact) + +| # | Action | Impact | Platforms Affected | +|---|--------|--------|--------------------| +| 15 | Pursue Wikipedia/Wikidata entry for Hypercerts Foundation | Entity recognition (strongest signal) | ChatGPT, Perplexity, Gemini | +| 16 | Build content hub: blog/docs covering "What is AT Protocol?", "Decentralized identity explained", "Certified vs. Sign in with Google", etc. | Topical authority | All 5 | +| 17 | Create YouTube explainer video(s) | Google ecosystem presence | Gemini, Google AIO | +| 18 | Seed community presence: Reddit (r/selfhosted, r/privacy, r/atproto), Product Hunt, developer forums | Brand mentions | Perplexity, ChatGPT | +| 19 | Consider a more distinctive brand qualifier (e.g., "Certified by Hypercerts" or "Certified ID") in public-facing contexts | Entity disambiguation | All 5 | + +--- + +## Recommended JSON-LD (Ready to Implement) + +### Organization — Root Layout + +```json +{ + "@context": "https://schema.org", + "@type": "Organization", + "name": "Hypercerts Foundation", + "legalName": "Hypercerts Foundation", + "url": "https://hypercerts.org", + "logo": "https://certified.app/assets/certified_brandmark_black.png", + "description": "A Delaware nonstock corporation that develops open infrastructure for the hypercerts ecosystem, operating the Certified identity platform.", + "address": { + "@type": "PostalAddress", + "streetAddress": "1209 Orange St.", + "addressLocality": "Wilmington", + "addressRegion": "DE", + "postalCode": "19801", + "addressCountry": "US" + }, + "contactPoint": { + "@type": "ContactPoint", + "email": "legal@hypercerts.org", + "contactType": "legal" + }, + "sameAs": [ + "https://github.com/hypercerts-org" + ] +} +``` + +### SoftwareApplication — Homepage + +```json +{ + "@context": "https://schema.org", + "@type": "SoftwareApplication", + "name": "Certified", + "url": "https://certified.app", + "applicationCategory": "SecurityApplication", + "operatingSystem": "Web", + "description": "Create your Certified identity and use one account across partner apps. No passwords, no lock-in. Built on AT Protocol for decentralized identity.", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + }, + "creator": { + "@type": "Organization", + "name": "Hypercerts Foundation", + "url": "https://hypercerts.org" + }, + "isAccessibleForFree": true +} +``` + +### WebSite — Root Layout + +```json +{ + "@context": "https://schema.org", + "@type": "WebSite", + "name": "Certified", + "url": "https://certified.app", + "description": "Create your Certified identity and use one account across partner apps. No passwords, no lock-in.", + "publisher": { + "@type": "Organization", + "name": "Hypercerts Foundation" + }, + "inLanguage": "en" +} +``` + +--- + +## Recommended llms.txt + +``` +# Certified + +> Certified is an identity platform built on AT Protocol, operated by Hypercerts Foundation. It provides passwordless authentication and portable user profiles across partner applications. + +## Main + +- [Home](https://certified.app): Create your Certified identity and use one account across partner apps +- [Terms of Service](https://certified.app/terms): Service terms for Certified and certified.one infrastructure +- [Privacy Policy](https://certified.app/privacy): Data processing practices, GDPR compliance, cookie policy +- [DSA Compliance](https://certified.app/dsa): Digital Services Act compliance information + +## About + +- [Hypercerts Foundation](https://hypercerts.org): Parent organization building open-source protocols for impact funding +- [GitHub](https://github.com/hypercerts-org): Open source repositories for Certified and related projects + +## Partner Apps + +- [Ma Earth](https://maearth.com): Collective funding for regenerating Earth +- [GainForest](https://gainforest.earth): AI-powered forest monitoring and conservation rewards +- [Simocracy](https://simocracy.org): Democratic governance with verifiable identity +- [Hyperboards](https://hyperboards.org): Visual leaderboards for impact contributions +``` + +--- + +## Key Source Files + +| File | Issue | +|------|-------| +| `src/components/landing/home-client.tsx` | `"use client"` — causes SSR bailout, all landing content invisible to crawlers | +| `src/app/page.tsx` | Renders HomeClient — homepage has no server-rendered content | +| `src/app/layout.tsx` | Missing: OG tags, Twitter cards, canonical, JSON-LD, security headers | +| `src/app/terms/page.tsx` | No metadata export (inherits generic defaults) | +| `src/app/privacy/page.tsx` | No metadata export (inherits generic defaults) | +| `src/app/dsa/page.tsx` | No metadata export (inherits generic defaults) | +| `next.config.ts` | No security headers configured | +| `public/` | Missing: robots.txt, sitemap.xml, llms.txt | + +--- + +*Generated by GEO Audit Tool — 2026-04-06* diff --git a/public/assets/certified-hero-1200x630.png b/public/assets/certified-hero-1200x630.png new file mode 100644 index 00000000..6ed17935 Binary files /dev/null and b/public/assets/certified-hero-1200x630.png differ diff --git a/public/assets/guilloche_01.svg b/public/assets/guilloche_01.svg deleted file mode 100644 index 4ce0e332..00000000 --- a/public/assets/guilloche_01.svg +++ /dev/null @@ -1,532 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/assets/guilloche_02.svg b/public/assets/guilloche_02.svg index c1542b7d..51ebca2b 100644 --- a/public/assets/guilloche_02.svg +++ b/public/assets/guilloche_02.svg @@ -1,808 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/public/assets/guilloche_03.svg b/public/assets/guilloche_03.svg deleted file mode 100644 index 0c135456..00000000 --- a/public/assets/guilloche_03.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/public/assets/guilloche_04.svg b/public/assets/guilloche_04.svg deleted file mode 100644 index e9212f38..00000000 --- a/public/assets/guilloche_04.svg +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/llms.txt b/public/llms.txt index 55d052f7..739ae801 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -4,7 +4,7 @@ ## Main -- [Home](https://certified.app): Create your Certified identity and use one account across partner apps +- [Home](https://certified.app/welcome): Create your Certified identity and use one account across partner apps - [About](https://certified.app/about): About Certified and the Hypercerts Foundation - [Terms of Service](https://certified.app/terms): Service terms for Certified and certified.one infrastructure - [Privacy Policy](https://certified.app/privacy): Data processing practices, GDPR compliance, cookie policy diff --git a/src/app/api/auth/callback-handler/route.ts b/src/app/api/auth/callback-handler/route.ts index db384573..98332e27 100644 --- a/src/app/api/auth/callback-handler/route.ts +++ b/src/app/api/auth/callback-handler/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server" import { Agent } from "@atproto/api" import { getOAuthClient } from "@/lib/auth/oauth-client" -import { createSession } from "@/lib/auth/session" +import { createSession, deleteSession } from "@/lib/auth/session" /** Collections that should always have a "self" record after sign-in */ const PROFILE_COLLECTIONS = [ @@ -17,6 +17,8 @@ export async function GET(request: NextRequest) { const client = await getOAuthClient() const { session } = await client.callback(params) + // Invalidate any existing session to prevent session fixation + await deleteSession().catch(() => {}) await createSession(session.did) // Best-effort: ensure profile records exist (don't fail sign-in if this errors) @@ -24,14 +26,14 @@ export async function GET(request: NextRequest) { const oauthSession = await client.restore(session.did) const agent = new Agent(oauthSession) await ensureProfileRecords(agent, session.did) - } catch { - // Silently ignore — profile seeding is not critical for sign-in + } catch (err) { + console.error("[Auth] Profile seeding failed for", session.did, err) } return NextResponse.json({ did: session.did }) } catch (err) { - const message = err instanceof Error ? err.message : "Unknown error" - return NextResponse.json({ error: message }, { status: 500 }) + console.error("[Auth] Callback error:", err) + return NextResponse.json({ error: "Authentication failed" }, { status: 500 }) } } diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 32f37e77..f052695e 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -53,7 +53,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Invalid mode" }, { status: 400 }) } catch (err) { - const message = err instanceof Error ? err.message : "Unknown error" + const raw = err instanceof Error ? err.message : "" + const message = raw.includes("Invalid") || raw.includes("invalid") + ? raw + : "Authentication failed" return NextResponse.json({ error: message }, { status: 400 }) } } diff --git a/src/app/globals.css b/src/app/globals.css index 1c6f4aaf..1424a6bd 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,32 @@ @tailwind components; @tailwind utilities; +/* Skip navigation link — visible only on focus */ +.skip-nav { + position: absolute; + left: -9999px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; + z-index: 9999; + padding: 8px 16px; + background: var(--color-navy); + color: #fff; + font-size: 0.875rem; + font-family: var(--font-inter); + text-decoration: none; + border-radius: 0 0 4px 0; +} +.skip-nav:focus { + position: fixed; + top: 0; + left: 0; + width: auto; + height: auto; + overflow: visible; +} + :root { /* Core */ --color-primary: #111111; @@ -44,12 +70,14 @@ --color-success-text: #047857; --color-warning: #F5A623; --color-error: #ba1a1a; - --color-info: var(--color-accent); - /* Outline */ - --color-outline: #7e7576; --color-outline-variant: #cfc4c5; + /* Warning */ + --color-warning-bg: #fef9e7; + --color-warning-border: #f5d880; + --color-warning-text: #7a6420; + /* Transitions */ --transition-fast: 150ms ease-out; --transition-base: 250ms ease-out; @@ -833,7 +861,7 @@ h1, h2, h3, h4, h5, h6 { @media (max-width: 768px) { .signin-modal { - width: 100vw; + width: 100%; max-width: none; border-radius: 0; min-height: auto; @@ -2527,22 +2555,22 @@ h1, h2, h3, h4, h5, h6 { display: flex; gap: 10px; padding: 12px 14px; - background: #fef9e7; - border: 1px solid #f5d880; + background: var(--color-warning-bg); + border: 1px solid var(--color-warning-border); border-radius: var(--radius); margin-bottom: 20px; } .domain-modal__info-box p { font-size: 0.75rem; - color: #7a6420; + color: var(--color-warning-text); line-height: 1.5; margin: 0; } .domain-modal__info-icon { flex-shrink: 0; - color: #c5a630; + color: var(--color-warning-border); margin-top: 1px; } @@ -2717,7 +2745,7 @@ h1, h2, h3, h4, h5, h6 { @media (max-width: 768px) { .domain-modal { - width: 100vw; + width: 100%; max-width: none; border-radius: 0; max-height: 100vh; @@ -3028,8 +3056,8 @@ h1, h2, h3, h4, h5, h6 { } .profile-fallback-note { - background: #fef9e7; - border: 1px solid #f5d76e; + background: var(--color-warning-bg); + border: 1px solid var(--color-warning-border); border-radius: var(--radius); padding: 12px 16px; margin-top: 16px; @@ -3037,7 +3065,7 @@ h1, h2, h3, h4, h5, h6 { .profile-fallback-note p { font-size: 0.8125rem; - color: #7d6608; + color: var(--color-warning-text); line-height: 1.5; margin: 0; } @@ -3891,10 +3919,6 @@ h1, h2, h3, h4, h5, h6 { animation: spin 600ms linear infinite; } -@keyframes spin { - to { transform: rotate(360deg); } -} - .handle-search__dropdown { position: absolute; top: 100%; diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 00000000..c2a32852 Binary files /dev/null and b/src/app/icon.png differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dd70f5be..2048d363 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -129,8 +129,9 @@ export default function RootLayout({ + Skip to main content -
+
{children}