Skip to content

Latest commit

 

History

History
1063 lines (816 loc) · 38.1 KB

File metadata and controls

1063 lines (816 loc) · 38.1 KB

Facet Design Document

Version: 1.1 Status: Active Last Updated: 2026-01-26


Table of Contents

  1. Vision & Principles
  2. Core Concepts
  3. Data Model
  4. View System Design
  5. Routing & URL Philosophy
  6. Sharing & Privacy Model
  7. Import & Enrichment Architecture
  8. Admin UX Philosophy
  9. Public UX Philosophy
  10. Security Invariants
  11. Extensibility Boundaries
  12. Known Tradeoffs
  13. Feature Gaps & Work
  14. Print & Export System

1. Vision & Principles

1.1 What Facet Is

Facet is a self-hosted, privacy-respecting, single-owner personal profile platform. It is best understood as:

"A self-hosted, fully owned alternative to LinkedIn profiles — with views, versions, and sharing — built for individuals, not networks."

1.2 Core Philosophy

Principle Description
Single-owner by design One person owns this instance. There are no users, no teams, no multi-tenancy.
Self-hosted Runs on your infrastructure. One container, one port, one volume to backup.
Fully owned data All data lives in SQLite. Export, backup, delete at will.
Quiet and intentional No notifications, no feeds, no engagement metrics.
Powerful but not loud Rich features that stay out of the way until needed.
Not social No comments, likes, follows, or messaging.
Privacy first Explicit sharing, no accidental disclosure, zero tracking.

1.3 Mental Model

Think of Facet as:

  • LinkedIn profile (the content part, not the network)
  • Resume + portfolio + personal site combined
  • With versions ("views") for different audiences:
    • Recruiter view (focused on employment history)
    • Conference view (speaking and projects)
    • Consulting view (skills and case studies)
    • Internal view (team-specific context)
    • Personal view (friends and family)
  • With strong privacy controls and explicit sharing
  • With zero accidental exposure — unlisted and private content requires explicit access

This is something a thoughtful adult would host for themselves, not a SaaS product.

1.4 Explicit Non-Goals

Facet will never include:

  • Social features (feeds, likes, comments, follows)
  • Messaging or notifications
  • Multi-tenant SaaS features
  • Analytics dashboards or engagement metrics
  • Advertising or growth loops
  • User tracking beyond basic access logging

2. Core Concepts

2.1 Profile

The Profile is the singleton identity record. There is exactly one profile per Facet instance. It contains:

  • Identity: Name, headline, location, avatar
  • Narrative: Summary (Markdown)
  • Contact: Email, links (GitHub, LinkedIn, etc.)
  • Visibility: Public, unlisted, or private

The profile is the "global" identity that views (facets) inherit from and can override.

2.2 Content Collections

Content is organized into typed collections:

Collection Purpose Key Fields
Experience Work history Company, title, dates, description, bullets
Projects Portfolio items Title, summary, tech stack, links, cover image
Education Academic background Institution, degree, field, dates
Certifications Professional certs Name, issuer, dates, credential ID
Awards Recognition and achievements Title, issuer, date, description
Skills Categorized skills Name, category, proficiency level
Posts Blog/writing Title, content (Markdown), published date
Talks Speaking engagements Title, event, date, slides/video URLs
Custom Content User-defined sections Title, content (Markdown), icon
Contact Methods Protected contact info Type, value, protection level
Testimonials Social proof from others Name, company, relationship, content

Each content item has:

  • visibility: public / unlisted / private
  • is_draft: Draft items are never shown publicly
  • sort_order: Manual ordering within collections

2.3 Views

Views are curated versions of your profile for different audiences. A view defines:

  1. What to show: Which sections and items to include
  2. How to order: Custom ordering of sections and items
  3. What to override: Per-view headline, summary, CTA
  4. Who can access: Visibility level and authentication

Views are the core differentiator of Facet. They enable:

  • Showing different content to recruiters vs. conference attendees
  • Tailoring messaging for different contexts
  • Sharing private content with specific people via tokens

2.4 Share Tokens

Share tokens enable controlled access to unlisted views:

  • Generated by admin, returned exactly once
  • Stored as HMAC hash (raw token never stored)
  • Optional expiration date and usage limits
  • Revocable at any time
  • Accessed via /s/<token> URLs that redirect to clean canonical URLs

2.5 Sources

Sources represent external origins of content (currently GitHub repos):

  • Track import origin for refresh capability
  • Enable field-level locking (user customizations preserved)
  • Generate Import Proposals for human review

2.6 AI Providers (Optional)

AI Providers are user-supplied credentials for optional enrichment:

  • Support for OpenAI, Anthropic, Ollama, custom endpoints
  • API keys encrypted at rest (AES-256-GCM)
  • Never auto-publish — all AI output goes through review
  • Privacy modes: full README, summary only, none

3. Data Model

3.1 Current Schema

The current implementation uses PocketBase collections with the following structure:

profile (singleton)
├── name, headline, location
├── summary (Markdown)
├── hero_image, avatar (files)
├── contact_email, contact_links (JSON)
└── visibility (public|unlisted|private)

experience
├── company, title, location
├── start_date, end_date
├── description (Markdown), bullets (JSON), skills (JSON)
├── media (files)
├── visibility, is_draft, sort_order
└── (password_hash - for item-level protection)

projects
├── title, slug (unique), summary
├── description (Markdown), tech_stack (JSON), links (JSON)
├── media, cover_image (files)
├── categories (JSON)
├── visibility, is_draft, is_featured, sort_order
├── source_id (FK→sources), field_locks (JSON), last_sync
└── (password_hash)

education
├── institution, degree, field
├── start_date, end_date, description
└── visibility, is_draft, sort_order

certifications
├── name, issuer, issue_date, expiry_date
├── credential_id, credential_url
└── visibility, is_draft, sort_order

skills
├── name, category, proficiency (expert|proficient|familiar)
└── visibility, sort_order

posts
├── title, slug (unique), excerpt
├── content (Markdown), cover_image
├── tags (JSON), published_at
└── visibility, is_draft

talks
├── title, event, event_url, date, location
├── description, slides_url, video_url
└── visibility, is_draft, sort_order

views
├── name, slug (unique, required)
├── description (internal note)
├── visibility (public|unlisted|password|private)
├── password_hash (for password visibility)
├── hero_headline, hero_summary (overrides)
├── cta_text, cta_url
├── sections (JSON: section configs with items)
├── is_active, is_default
└── (theme, custom CSS)

share_tokens
├── view_id (FK→views)
├── token_prefix (12 chars, indexed)
├── token_hash (HMAC-SHA256)
├── name (label)
├── expires_at, max_uses, use_count
├── is_active, last_used_at

sources
├── type (github)
├── identifier (owner/repo)
├── project_id (FK→projects, optional)
├── last_sync, sync_status, sync_log

ai_providers
├── name, type (openai|anthropic|ollama|custom)
├── api_key, api_key_encrypted
├── base_url, model
├── is_default, is_active
├── last_test, test_status

import_proposals
├── source_id (FK→sources)
├── project_id (FK→projects, optional for new)
├── proposed_data (JSON)
├── diff (JSON)
├── ai_enriched (boolean)
├── status (pending|applied|rejected)
└── applied_fields (JSON)

settings (key-value)
├── key (unique)
└── value (JSON)

3.2 Proposed Schema Extensions

Phase 1: Profile Completeness

Add to profile:

  • pronouns: Optional, displayed with name
  • tagline: Short one-liner (distinct from headline)
  • resume_pdf: Auto-generated or uploaded resume

Phase 2: Enhanced Experience/Projects

Add to experience:

  • company_logo: File upload for logos
  • employment_type: Full-time, part-time, contract, etc.

Add to projects:

  • status: Active, completed, archived, abandoned
  • year_started, year_ended: Simplified date display
  • collaborators: JSON array of names

Phase 3: Awards & Publications

New collections:

  • awards: name, issuer, date, description, visibility
  • publications: title, venue, date, url, type (paper|book|article), visibility

Phase 4: Custom Sections

New collection:

  • custom_sections: name, slug, content (Markdown), sort_order, visibility

This allows users to add arbitrary content sections without schema changes.


4. View System Design

4.1 Why Views Exist

Views solve the audience problem: different people need to see different things.

A recruiter cares about employment history and skills. A conference organizer cares about talks and projects. A potential client cares about case studies and testimonials. Your family wants the personal touch.

Without views, you either:

  1. Show everything to everyone (privacy loss)
  2. Manually maintain multiple profiles (synchronization hell)
  3. Use visibility toggles (loses context, awkward)

Views provide curated lenses onto a single source of truth.

4.2 What Views Control

Aspect Default Behavior View Override
Sections All visible sections Explicit include/exclude
Items All visible items in section Explicit item selection
Section Order Default order Custom per-view section ordering
Item Order sort_order field Custom per-view item ordering
Item Content Source record values Per-item field overrides
Headline Profile headline hero_headline override
Summary Profile summary hero_summary override
CTA None cta_text + cta_url
Theme System default (Future: per-view theme)

4.3 Section Configuration

Views store section configuration as JSON:

{
  "sections": [
    {
      "section": "experience",
      "enabled": true,
      "items": ["id1", "id2", "id3"],
      "order": 1,
      "itemConfig": {
        "id1": {
          "order": 0,
          "overrides": {
            "title": "Senior UX Designer",
            "description": "Led user research initiatives..."
          }
        },
        "id2": { "order": 1 },
        "id3": { "order": 2 }
      }
    },
    {
      "section": "projects",
      "enabled": true,
      "items": [],
      "order": 2
    },
    {
      "section": "education",
      "enabled": false
    }
  ]
}

Design Decision: Empty items array means "include all visible items" (filtered by visibility and draft status). This avoids manual updates when adding new content.

4.4 Item-Level Overrides

Views can override specific fields on individual items without modifying the source record. This enables audience-specific framing of the same experience.

Use Case: Career Pivot

A product designer who straddles UX and Instructional Design can create two views from the same job history:

Source Record: "Product Designer @ Acme Corp"
├── title: "Product Designer"
├── description: "Designed products and training materials"
├── bullets: ["Led design system", "Created onboarding", "User research"]

View: "UX Designer"
└── Overrides:
    ├── title: "Senior UX Designer"
    ├── description: "Led user-centered design initiatives..."
    └── bullets: ["Led design system", "Conducted 50+ user interviews", ...]

View: "Instructional Designer"
└── Overrides:
    ├── title: "Learning Experience Designer"
    ├── description: "Created scalable training programs..."
    └── bullets: ["Developed onboarding curriculum", "Built LMS integrations", ...]

Overridable Fields by Collection

Collection Overridable Fields
Experience title, description, bullets
Projects title, summary, description
Education degree, field, description
Skills (none - include/exclude only)
Certifications (none - include/exclude only)
Talks title, description
Posts (none - include/exclude only)

Not Overridable: Company names, dates, institutions, issuers, URLs. These are factual and should remain consistent across views.

Override Inheritance

Render Priority (highest wins):
1. View itemConfig.overrides → If field has override, use it
2. Source record value → Otherwise, use original

UI Indicators

The admin UI should clearly show:

  • Which items have overrides (visual badge)
  • Which specific fields are overridden (inline indicator)
  • "Reset to original" action to clear overrides
  • Side-by-side comparison of original vs. override

4.5 Default View Behavior

The homepage (/) renders the default view:

  1. Find view where is_default=true AND is_active=true AND visibility='public'
  2. Fallback: First public active view by creation date
  3. Fallback: Legacy homepage (all public content aggregated)

Invariant: Only ONE view can have is_default=true. This is enforced by backend hooks that clear other defaults when setting a new one.

4.6 View Inheritance Model

Views inherit from the profile and override selectively:

Profile (global identity)
    ├── name: "Jane Smith"           ← Always used
    ├── headline: "Software Engineer" ← Can be overridden per view
    ├── summary: "I build things..."  ← Can be overridden per view
    └── contact_links: [...]          ← Always used

View: "Recruiter"
    ├── hero_headline: "Senior Engineer | 10+ Years Experience"  ← Override
    ├── hero_summary: "Looking for backend roles..."             ← Override
    └── cta_text: "Download Resume" → cta_url: "/resume.pdf"     ← Addition

Not Overridable: Name, avatar, contact links. These are identity, not messaging.


5. Routing & URL Philosophy

5.1 URL Model

Facet uses LinkedIn-style canonical URLs:

Route Purpose Example
/ Default public view Homepage
/<slug> Named view (canonical) /recruiter, /conference
/s/<token> Share link entry Validates, sets cookie, redirects
/v/<slug> Legacy route 301 redirects to /<slug>

5.2 Design Rationale

Root-level slugs (/recruiter not /v/recruiter):

  • Cleaner URLs for sharing
  • More professional appearance
  • LinkedIn uses this pattern (linkedin.com/in/username)

Share link redirect (/s/<token>/<slug>):

  • Token never appears in final URL
  • Token not leaked via browser history
  • Token not leaked via Referer headers
  • Cookie enables subsequent requests

5.3 Reserved Slugs

These slugs cannot be used for views (enforced at frontend and backend):

admin, api, s, v, projects, posts, talks,
_app, _, assets, static,
favicon.ico, robots.txt, sitemap.xml,
health, healthz, ready, login, logout,
auth, oauth, callback, home, index, default, profile

Protection Layers:

  1. Frontend param matcher: Invalid slugs don't match [slug=slug] route
  2. Backend hook: Returns 400 when creating views with reserved slugs

5.4 Slug Validation Rules

A valid view slug must:

  • Not be empty
  • Not be in the reserved list (case-insensitive)
  • Match pattern: ^[a-zA-Z0-9][a-zA-Z0-9_-]*$
  • Not start with underscore or hyphen
  • Be at most 100 characters

6. Sharing & Privacy Model

6.1 Visibility Levels

Level Access HTTP Behavior Use Case
public Anyone 200 OK General audience
unlisted Share token required 200 with token, 404 without Specific people
password Password JWT required Password prompt, then 200 Protected content
private Admin only 404 (never 401/403) Internal/draft

Security Principle: Private and unlisted content returns 404, not 401/403, to prevent discovery of existence.

6.2 Share Token Flow

1. Admin generates token for unlisted view
   POST /api/share/generate
   → Returns raw token ONCE (never stored)

2. Admin shares URL: https://example.com/s/<token>

3. User visits /s/<token>:
   - Server validates token (HMAC verification)
   - Sets httpOnly cookie (me_share_token)
   - 302 redirect to /<slug>
   - Token NOT in final URL

4. Subsequent requests to /<slug>:
   - Token read from cookie
   - Sent via X-Share-Token header
   - View data returned with sections

6.3 Token Security Properties

Property Implementation
Storage HMAC-SHA256 hash, not raw token
Lookup Token prefix (12 chars) for O(1) indexed query
Comparison Constant-time HMAC verification
View-bound Each token tied to specific view_id
Expiration Optional expires_at timestamp
Usage limits Optional max_uses with use_count
Revocation is_active flag for instant deactivation
Error responses Uniform "invalid token" for all failure modes

6.4 Password Protection Flow

1. User visits /protected-view (no JWT cookie):
   - Server returns { requiresPassword: true }
   - Client shows password prompt

2. User submits password:
   POST /api/password/check
   - bcrypt comparison against password_hash
   - Returns signed JWT (1-hour expiry)

3. Client stores JWT in httpOnly cookie

4. Subsequent requests include JWT:
   - Authorization: Bearer <jwt>
   - Server validates signature, expiry, view_id
   - Returns view data

6.5 JWT Claims

{
  "vid": "<view-id>",
  "iss": "facet",
  "aud": "view-access",
  "iat": 1234567890,
  "exp": 1234571490,
  "jti": "<random-id>"
}

7. Import & Enrichment Architecture

7.1 Import Pipeline

┌─────────────────────────────────────────────────────────┐
│                    GitHub Import Flow                    │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  1. PREVIEW: Admin enters repo URL                       │
│     POST /api/github/preview                             │
│     → Fetch metadata, languages, topics, README          │
│     → Display to user before import                      │
│                                                          │
│  2. IMPORT: Admin confirms import                        │
│     POST /api/github/import                              │
│     → Create Source record                               │
│     → Optionally invoke AI enrichment                    │
│     → Create ImportProposal (pending)                    │
│                                                          │
│  3. REVIEW: Admin reviews each field                     │
│     /admin/review/[id]                                   │
│     → Apply / Ignore / Lock per field                    │
│     → Edit values before applying                        │
│                                                          │
│  4. APPLY: Create/update Project                         │
│     POST /api/proposals/{id}/apply                       │
│     → Apply selected fields                              │
│     → Update field_locks on project                      │
│     → Link source to project                             │
│                                                          │
│  5. REFRESH: Sync later                                  │
│     POST /api/github/refresh/{sourceId}                  │
│     → Fetch fresh data                                   │
│     → Respect field_locks                                │
│     → Create new proposal for review                     │
│                                                          │
└─────────────────────────────────────────────────────────┘

7.2 Field Locking

When a user customizes a field (edits it beyond the imported value), they can "lock" it:

{
  "field_locks": {
    "title": false,     // Can be updated on refresh
    "summary": true,    // Locked - preserved on refresh
    "description": true // Locked
  }
}

Behavior: Locked fields are skipped during refresh import. Unlocked fields appear in the new proposal for review.

7.3 AI Enrichment

AI enrichment is optional and user-controlled:

  1. User provides their own API keys (encrypted at rest)
  2. User chooses privacy mode:
    • Full: Send full README to AI
    • Summary: Send first 500 chars only
    • None: No AI, use raw GitHub data
  3. AI output goes to proposal, not directly to project
  4. User reviews and can edit every AI-generated field

AI Guardrails (in prompt):

  • "Do not invent metrics or statistics"
  • "Stay factual, avoid marketing language"
  • "Use neutral, professional tone"

8. Admin UX Philosophy

8.1 Design Principles

Principle Implementation
Calm, not CMS No dashboard for its own sake. Information density is earned.
Empty ≠ broken First-run shows welcoming copy, not error states or zeros.
No analytics theater No graphs showing "0 views this week".
Tending, not managing Editing should feel like gardening your profile, not filling forms.

8.2 Empty States

When content is missing, show helpful guidance:

  • Dashboard empty: "This is your space. You might start by adding your profile..."
  • No views: "Views let you show different versions of your profile to different audiences."
  • No activity: "Nothing here yet — and that's okay."
  • No AI providers: "AI enrichment is optional. Add your own API key if you'd like AI-assisted summaries."

8.3 Admin Navigation

/admin                         Dashboard (overview)
/admin/homepage                Homepage configuration

-- Your Information --
/admin/contacts                Contact methods (with protection levels)
/admin/experience              Manage work history
/admin/projects                Manage portfolio
/admin/education               Manage education
/admin/certifications          Manage certifications
/admin/awards                  Manage awards
/admin/skills                  Manage skills
/admin/custom                  Custom content sections
/admin/import                  GitHub import & Resume AI parsing

-- Your Voice --
/admin/posts                   Blog posts
/admin/talks                   Speaking engagements

-- Testimonials --
/admin/testimonials            Manage testimonials
/admin/testimonials/requests   Request links

-- Views & Sharing --
/admin/views                   Manage curated views/facets
/admin/views/new               Create new view
/admin/views/[id]              Edit specific view
/admin/tokens                  Manage share tokens
/admin/media                   Media library

-- Settings --
/admin/settings                Main settings
/admin/settings/account        Account & security
/admin/settings/appearance     Appearance settings
/admin/settings/general        General settings
/admin/settings/analytics      Analytics (Google Analytics)
/admin/settings/integrations   AI providers & integrations
/admin/settings/tags           Admin tags
/admin/settings/about          About Facet (changelog, version)

8.4 Visual Design

  • Consistent icons: SVG icon set in $lib/icons.ts, no emojis
  • Destructive actions: Red styling, confirmation dialogs
  • Loading states: Skeleton loaders, not spinners everywhere
  • Dark mode: Full support via Tailwind dark classes

9. Public UX Philosophy

9.1 Design Principles

Principle Implementation
Readability first Clean typography, generous whitespace
Fast SSR, minimal JS, optimized images
Accessible Semantic HTML, ARIA labels, keyboard nav
Printable Print-friendly CSS for resume printing
SEO basics Meta tags, Open Graph, canonical URLs

9.2 Public Page Structure

┌────────────────────────────────────────┐
│           Profile Hero                  │
│  Avatar  Name  Headline  Location       │
│           Summary (Markdown)            │
│           CTA Button (if view has one)  │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│           Experience Section            │
│  [Cards with company, title, dates]     │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│           Projects Section              │
│  [Grid of project cards with covers]    │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│           Education Section             │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│           Skills Section                │
│  [Grouped by category]                  │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│              Footer                     │
│  Contact links  •  Facet                │
└────────────────────────────────────────┘

9.3 Password Prompt

For password-protected views:

  • Minimal, clean prompt
  • No indication of what content is behind it
  • Single password field + submit
  • Error: "Incorrect password" (not "view not found")

10. Security Invariants

10.1 Non-Negotiable Properties

Invariant Enforcement
Private content returns 404 Never 401/403 for private/unlisted without token
Tokens never stored raw HMAC hash only in database
API keys encrypted at rest AES-256-GCM with derived key
No public collection access All collections require auth for direct API
Rate limiting on auth endpoints Strict tier (5/min) on password check

10.2 Collection Access Model

Deny-by-default: All PocketBase collections require authentication for direct HTTP access.

Public data flows through custom endpoints:
  /api/view/{slug}/access  → View metadata
  /api/view/{slug}/data    → View content (with visibility checks)
  /api/homepage            → Legacy aggregated public content

These endpoints use server-side queries that bypass collection rules,
allowing them to serve public content while maintaining access control.

10.3 Key Derivation

From master ENCRYPTION_KEY:

  • Encryption key: SHA256(master + ":encryption")
  • HMAC key: SHA256(master + ":hmac")
  • JWT signing key: SHA256(master + ":jwt")

10.4 Rate Limiting Tiers

Tier Rate Burst Endpoints
Strict 5/min 3 /api/password/check
Moderate 10/min 5 /api/share/validate
Normal 60/min 10 /api/view/{slug}/*, /api/homepage

11. Extensibility Boundaries

11.1 What Can Be Extended

Extension Point Mechanism
New import sources Add to sources.type enum, create service
New AI providers Add to ai_providers.type enum, extend AIService
Custom sections Future: custom_sections collection
Themes Future: Per-view CSS/theme selection

11.2 What Should NOT Be Extended

Boundary Reason
Multi-user Fundamental single-owner assumption
Social features Violates core philosophy
Tracking/analytics Privacy-first commitment
SaaS features Self-hosted by design

11.3 Plugin Architecture

Facet does not currently support plugins. Future extensibility may include:

  • Custom Go hooks in a designated directory
  • Theme packages (CSS + layout overrides)
  • Import adapters for new sources

12. Known Tradeoffs

12.1 Accepted Limitations

Tradeoff Reason
SQLite only Perfect for single-user, no need for horizontal scaling
Single container Simplest deployment, one port to expose
No CDN for files Local file storage keeps it simple; S3 optional
No JWT revocation Stateless tokens; 1-hour expiry is acceptable
PocketBase pre-v1.0 Widely used, pinned version, documented upgrade path

12.2 Explicit Constraints

Constraint Implication
One profile per instance Family members need separate instances
No scheduled sync GitHub refresh is manual (could add cron job)
No granular permissions Admin is all-or-nothing
No version history Changes overwrite; backup externally

13. Feature Gaps & Future Work

13.1 Current Gaps

Based on codebase analysis, these features are incomplete or missing:

Gap Current State Impact
Project detail pages Route exists, no implementation ✅ Complete: /projects/{slug}
Post/blog pages Collection exists, no public UI ✅ Complete: /posts/{slug}
Talks section Collection exists, no UI ✅ Complete: Public display with video embeds
Certifications section Collection exists, no UI ✅ Complete: Public display with issuer grouping
View editor Basic, no drag-drop ✅ Complete: Full editor with section/item selection
Media library No implementation ✅ Complete: /admin/media with listing, filters, delete
Share token management UI Listed in views page, no full UI ✅ Complete: /admin/tokens with full CRUD
Scheduled GitHub sync Not implemented Manual refresh only
Resume PDF export Not implemented ✅ Complete: Print stylesheet + AI resume generation
Theme customization Not implemented ✅ Partial: Accent colors, custom CSS, dark mode
Demo mode Not implemented ✅ Complete: "The Doctor" profile with one-click toggle
Audit logging Minimal Cannot review full access history
Testimonials Not implemented ✅ Complete: Full system with request links, verification
Custom content Not implemented ✅ Complete: User-defined content sections
Contact protection Not implemented ✅ Complete: 4-tier protection, per-view visibility
Version notifications Not implemented ✅ Complete: Check GitHub for updates

13.2 Proposed Priority Order

  1. Core content pages: Project details, posts, talks, certifications ✅ Complete
  2. View editor core: Section toggles, item selection, hero overrides ✅ Complete
  3. View editor improvements: Drag-drop ordering, preview pane ✅ Complete
  4. Share token management: Full CRUD UI with usage stats ✅ Complete
  5. Resume export: Generate PDF from profile/view ✅ Complete (AI generation + print)
  6. Theme system: Light/dark modes, color customization ✅ Complete (accent colors, custom CSS)
  7. Scheduled sync: Cron-based GitHub refresh
  8. Media library: Image optimization (thumb/WebP/srcset), storage insights ✅ Complete
  9. Demo mode: Production-safe showcase persona with multiple views ✅ Complete
  10. Audit log: Access history for share tokens (minimal currently)
  11. Testimonials: Social proof collection ✅ Complete
  12. Contact protection: Per-view contact visibility ✅ Complete
  13. 🔜 View Analytics Dashboard: Surface existing usage data
  14. 🔜 QR Codes: Generate for views/share links
  15. 🔜 Webhooks: Notify external services on events

14. Print & Export System

14.1 Print Stylesheet

Facet includes a comprehensive print stylesheet that optimizes public views for printing and PDF generation via the browser's native Print function (Ctrl+P / Cmd+P).

Design Goals

Goal Implementation
ATS-friendly Clean text, minimal formatting, semantic structure
One-click print Print button visible on public views
Readable Optimized typography, appropriate spacing
Complete All visible content printed, nothing hidden
No dark mode Forces light colors for printing

Print Optimizations

  1. Hidden Elements

    • Theme toggle button
    • Print button itself
    • Interactive elements (hover states)
    • Background gradients and decorative images
  2. Typography Adjustments

    • Serif font for body text (better for printing)
    • Slightly smaller base font size
    • Increased line height for readability
    • Black text on white background
  3. Layout Changes

    • Full-width content (no max-width constraints)
    • Reduced padding and margins
    • Cards rendered without shadows/borders
    • Page breaks avoided inside sections
  4. Link Handling

    • URLs displayed after link text
    • Contact links shown with full URLs
    • External links marked appropriately

CSS Structure

@media print {
  /* Force light mode colors */
  * { color-adjust: exact; -webkit-print-color-adjust: exact; }

  /* Hide UI elements */
  .print\\:hidden { display: none !important; }

  /* Typography */
  body { font-family: Georgia, serif; font-size: 11pt; }

  /* Layout */
  main { max-width: 100%; padding: 0; }
  .card { box-shadow: none; border: 1px solid #e5e7eb; }

  /* Page breaks */
  section { page-break-inside: avoid; }
  article { page-break-inside: avoid; }
}

14.2 Print Button

A print button is displayed on public view pages (not admin). The button:

  • Appears in a non-intrusive location (top-right, near theme toggle)
  • Has a printer icon for clarity
  • Triggers window.print() on click
  • Is hidden when printing (class print:hidden)

14.3 Future Enhancements

These export features are planned for later phases:

Feature Status Notes
Server-side PDF Deferred Would require headless browser or Go PDF library
Data export (JSON) Deferred Export all profile data for backup
Data export (YAML) Deferred Export in Facet format
Static HTML snapshot Deferred Self-contained offline version

Appendix A: API Reference

Public Endpoints

Method Path Rate Limit Description
GET /api/default-view Normal Get default view slug
GET /api/view/{slug}/access Normal Get view access requirements
GET /api/view/{slug}/data Normal Get view content
GET /api/project/{slug} Normal Get public project details
GET /api/homepage Normal Legacy aggregated content
POST /api/share/validate Moderate Validate share token
POST /api/password/check Strict Validate view password

Authenticated Endpoints

Method Path Description
POST /api/share/generate Generate new share token
POST /api/share/revoke/{id} Revoke share token
POST /api/github/preview Preview GitHub repo
POST /api/github/import Import GitHub repo
POST /api/github/refresh/{id} Refresh source
POST /api/ai/test/{id} Test AI provider
POST /api/ai/enrich Enrich content with AI
POST /api/proposals/{id}/apply Apply import proposal
POST /api/proposals/{id}/reject Reject import proposal
POST /api/password/set Set view password

Appendix B: Environment Variables

Variable Required Default Description
ENCRYPTION_KEY Yes 32-byte hex key for encryption
PORT No 8080 Public port
APP_URL No http://localhost:8080 Public URL
TRUST_PROXY No false Trust proxy headers for IP
ADMIN_EMAILS No Comma-separated admin allowlist
ADMIN_ENABLED No false Enable PocketBase admin UI
DATA_PATH No ./data Database and uploads path
SEED_DATA No Seed mode: dev for dev profile, unset for none
LOG_LEVEL No info Logging verbosity

This document is the authoritative design reference for Facet. Update it as the product evolves.