Fusite Web Low-Level Design
Design Documents
Last updated August 23, 2025
Fusite Web Low-Level Design
Based on the Fusite High-Level Design and API Low-Level Design documents, this low-level design outlines the web repository implementation for static site generation that consumes the Fusite API.
1. Overview
The Fusite Web application serves as a static site generator that:
- Shared Notion Access: Uses the fusite-commons package for Notion SDK access
- Static Site Generator: Builds HTML pages from Notion data
- Asset Pipeline: Processes and optimizes images and assets
- Deployment System: Uploads generated site to S3 and invalidates CloudFront
The web application is built with:
- Runtime: Bun for build tooling and package management
- Framework: Vanilla TypeScript (no heavy frontend framework)
- Templates: Handlebars for HTML templating
- Styling: Tailwind CSS for styling
- Data Source: Shared fusite-commons package (same as API)
- Deployment: AWS S3 + CloudFront
- Build Environment: Local builds (GitHub Actions or local machine)
2. Project Structure
/web
bunfig.toml # Bun configuration
package.json # Dependencies and scripts (includes fusite-commons)
tsconfig.json # TypeScript configuration
tailwind.config.js # Tailwind CSS configuration
/src
/templates # HTML template system
layouts/ # Base layouts
base.hbs # Main HTML layout
page.hbs # Standard page layout
article.hbs # Article-specific layout
pages/ # Page templates
home.hbs # Homepage template
about.hbs # About page template
projects.hbs # Projects listing template
project.hbs # Individual project template
chronicles.hbs # Fuschronicles listing template (maps to /fuschronicles)
chronicle.hbs # Individual fuschronicle template (maps to /fuschronicles/{slug})
404.hbs # 404 error page
partials/ # Reusable components
header.hbs # Site header
footer.hbs # Site footer
nav.hbs # Navigation
project-card.hbs # Project card component
chronicle-card.hbs # Chronicle card component
breadcrumbs.hbs # Breadcrumb navigation
/styles # CSS and styling
main.css # Main stylesheet (Tailwind entry)
components.css # Custom component styles
utilities.css # Custom utility classes
/assets # Static assets
/images # Static images (logos, icons)
/fonts # Web fonts
/build # Build system
builder.ts # Main build orchestrator
renderer.ts # Template rendering engine
seo.ts # SEO metadata generation
sitemap.ts # Sitemap.xml generation
robots.ts # Robots.txt generation
feed.ts # RSS feed generation
assets.ts # Asset processing and optimization
deploy.ts # S3 deployment and CloudFront invalidation
cache.ts # Build-time caching layer
/transformers # Web-specific transformers
notionToHtml.ts # Convert Notion content to HTML for static pages
imageProcessor.ts # Process and optimize images for web
/utils # Shared utilities
config.ts # Build configuration
logger.ts # Build logging
slug.ts # Slug utilities
date.ts # Date formatting utilities
markdown.ts # Markdown processing (if needed)
/routes # Route generation
generator.ts # Route mapping and generation
types.ts # Route type definitions
/dist # Generated site output (gitignored)
/cache # Build cache directory (gitignored)
/.github
/workflows
build-deploy.yml # GitHub Actions workflow
/commons # Shared package between API and Web
package.json # Commons package definition
index.ts # Main exports
/src
/notion # Notion SDK access layer
client.ts # Notion SDK initialization
repositories/ # Data access layer
project.ts # Project database operations
task.ts # Task database operations
chronicle.ts # Chronicle database operations
about.ts # About page operations
services/ # Business logic layer
projectService.ts # Project business logic
taskService.ts # Task business logic
chronicleService.ts # Chronicle business logic
contentService.ts # Content transformation logic
transformers/ # Base data transformers
notionToApi.ts # Convert Notion blocks to API format
slugGenerator.ts # Generate and validate slugs
types.ts # Shared type definitions
/utils # Shared utilities
validation.ts # Input validation schemas
errors.ts # Custom error classes
3. Data Types and Notion Integration
3. Data Models and Types
3.1 Shared Data Types from Commons
The Web project uses shared data types from the fusite-commons
package:
typescript
// Import shared types from commons package
import {
Project,
Fuschronicle,
ProjectTask,
AboutPage,
ProjectStatus,
ChronicleType,
TaskStatus,
NotionBlock,
} from "fusite-commons"
// Web-specific types for static generation
export interface HtmlContent {
type:
| "heading_1"
| "heading_2"
| "heading_3"
| "paragraph"
| "bulleted_list_item"
| "numbered_list_item"
| "code"
| "quote"
| "image"
| "video"
| "file"
content: string
html: string // Pre-rendered HTML
annotations?: {
bold?: boolean
italic?: boolean
strikethrough?: boolean
underline?: boolean
code?: boolean
color?: string
}
href?: string
src?: string
alt?: string
caption?: string
}
3.2 Build-Time Data Types
typescript
// Site data aggregated for build
interface SiteData {
about: AboutPage
projects: Project[]
chronicles: Fuschronicle[]
tasks: Map<string, ProjectTask[]> // keyed by project ID
}
// Page generation context
interface PageContext {
title: string
description: string
canonical: string
openGraph: OpenGraphMeta
structuredData?: StructuredData
breadcrumbs?: Breadcrumb[]
lastModified?: string
}
// Route definition
interface Route {
path: string
template: string
data: any
context: PageContext
priority: number
changeFreq: "daily" | "weekly" | "monthly" | "yearly"
}
// Build result
interface BuildResult {
success: boolean
routes: Route[]
assets: GeneratedAsset[]
errors: BuildError[]
buildTime: number
cacheHits: number
cacheMisses: number
}
interface GeneratedAsset {
source: string
destination: string
size: number
hash: string
}
interface ProjectMetrics {
totalTasks: number
completedTasks: number
totalEstimatedTime: number
totalActualTime: number
firstTaskDate?: string
lastTaskDate?: string
completionRate: number
averageTaskTime: number
}
4. Core Components
4.1 Shared Notion Data Access
The Web project uses the fusite-commons
package for all Notion data access, providing:
- Repositories: Project, Task, Chronicle, About page data access
- Services: Business logic and content transformation
- Transformers: Convert Notion blocks to web-ready format
- Types: Shared data structures across API and Web
typescript
// Import shared components
import {
ProjectService,
ChronicleService,
TaskService,
AboutService,
} from "fusite-commons"
class WebBuilder {
constructor(
private projectService: ProjectService,
private chronicleService: ChronicleService,
private taskService: TaskService,
private aboutService: AboutService
) {}
async buildSite() {
const projects = await this.projectService.getPublicProjects()
const chronicles = await this.chronicleService.getPublishedChronicles()
// Transform and render...
}
}
4.2 Web-Specific Transformers (/src/transformers/
)
notionToHtml.ts
typescript
import { notionToApi } from "fusite-commons"
export class NotionToHtmlTransformer {
constructor(private imageProcessor: ImageProcessor)
async transformBlocks(blocks: NotionBlock[]): Promise<HtmlContent[]>
async processImages(content: HtmlContent[]): Promise<HtmlContent[]>
private transformBlock(block: NotionBlock): HtmlContent
private renderToHtml(content: HtmlContent): string
private processImageBlock(block: NotionBlock): Promise<HtmlContent>
private applyAnnotations(text: string, annotations: any): string
}
imageProcessor.ts
typescript
export class ImageProcessor {
async optimizeImage(imageUrl: string): Promise<OptimizedImage>
async generateResponsiveImages(imageUrl: string): Promise<ResponsiveImage[]>
async downloadAndOptimize(url: string, outputPath: string): Promise<string>
private compressImage(buffer: Buffer, quality: number): Promise<Buffer>
private generateSrcSet(images: ResponsiveImage[]): string
}
4.3 Template Rendering (/src/templates/
)
renderer.ts
typescript
export class TemplateRenderer {
private handlebars: HandlebarsEnvironment
private templatesDir: string
constructor(templatesDir: string)
async render(
template: string,
data: any,
context: PageContext
): Promise<string>
async renderPage(route: Route): Promise<string>
// Specific page renderers
async renderHome(data: HomePageData, context: PageContext): Promise<string>
async renderAbout(about: AboutPage, context: PageContext): Promise<string>
async renderProjects(
projects: Project[],
context: PageContext
): Promise<string>
async renderProject(
project: Project,
related: RelatedData,
context: PageContext
): Promise<string>
async renderChronicles(
chronicles: Fuschronicle[],
context: PageContext
): Promise<string>
async renderChronicle(
chronicle: Fuschronicle,
navigation: NavigationData,
context: PageContext
): Promise<string>
async render404(context: PageContext): Promise<string>
private registerHelpers(): void
private registerPartials(): void
private processContent(content: HtmlContent[]): string
}
interface HomePageData {
featuredProjects: Project[]
latestChronicles: Fuschronicle[]
about?: AboutPage
}
interface RelatedData {
chronicles: Fuschronicle[]
tasks?: ProjectTask[]
metrics?: ProjectMetrics
}
interface NavigationData {
prev?: Fuschronicle
next?: Fuschronicle
}
4.4 Build System (/src/build/
)
builder.ts
typescript
import {
ProjectService,
ChronicleService,
TaskService,
AboutService,
} from "fusite-commons"
export class SiteBuilder {
private projectService: ProjectService
private chronicleService: ChronicleService
private taskService: TaskService
private aboutService: AboutService
private renderer: TemplateRenderer
private cache: BuildCache
private config: BuildConfig
constructor(config: BuildConfig)
async buildSite(): Promise<BuildResult>
async buildPage(route: Route): Promise<void>
private async loadSiteData(): Promise<SiteData>
private async generateRoutes(data: SiteData): Promise<Route[]>
private async processAssets(): Promise<GeneratedAsset[]>
private async writeStaticFiles(
routes: Route[],
assets: GeneratedAsset[]
): Promise<void>
private async generateSitemap(routes: Route[]): Promise<void>
private async generateRobotsTxt(): Promise<void>
private async generateRSSFeed(chronicles: Fuschronicle[]): Promise<void>
}
interface BuildConfig {
outputDir: string
cacheDir: string
assetsDir: string
templatesDir: string
publicUrl: string
enableCache: boolean
}
seo.ts
typescript
export class SEOGenerator {
private siteConfig: SiteConfig
constructor(siteConfig: SiteConfig)
generatePageMeta(data: PageMetaInput): PageContext
generateOpenGraph(data: OGInput): OpenGraphMeta
generateStructuredData(data: StructuredDataInput): StructuredData
generateBreadcrumbs(path: string, data?: any): Breadcrumb[]
// Page-specific SEO generators
generateProjectSEO(project: Project): PageContext
generateChronicleSeO(chronicle: Fuschronicle): PageContext
generateListingSEO(type: "projects" | "chronicles", data: any[]): PageContext
}
interface SiteConfig {
siteName: string
siteDescription: string
siteUrl: string
author: string
twitterHandle?: string
defaultImage?: string
}
interface OpenGraphMeta {
title: string
description: string
image: string
url: string
type: string
siteName: string
}
interface StructuredData {
"@context": string
"@type": string
[key: string]: any
}
interface Breadcrumb {
name: string
url: string
}
4.4 Route Generation (/src/routes/
)
generator.ts
typescript
export class RouteGenerator {
private seoGenerator: SEOGenerator
constructor(seoGenerator: SEOGenerator)
async generateAllRoutes(data: SiteData): Promise<Route[]>
// Static routes
generateHomeRoute(data: SiteData): Route
generateAboutRoute(about: AboutPage): Route
generateProjectsRoute(projects: Project[]): Route
generateFuschroniclesRoute(chronicles: Fuschronicle[]): Route // Maps to /fuschronicles
generate404Route(): Route
// Dynamic routes
generateProjectRoutes(projects: Project[], data: SiteData): Route[]
// Generates:
// - /projects/{slug} (main project page)
// - /projects/{slug}/timeline (chronicles filtered by project relation)
// - /projects/{slug}/backlog (optional, from project tasks)
generateFuschronicleRoutes(chronicles: Fuschronicle[]): Route[] // Maps to /fuschronicles/{slug}
private calculatePriority(path: string): number
private getChangeFrequency(
path: string
): "daily" | "weekly" | "monthly" | "yearly"
}
4.6 Asset Processing (/src/build/assets.ts
)
typescript
export class AssetProcessor {
private config: AssetConfig
constructor(config: AssetConfig)
async processImages(content: HtmlContent[]): Promise<ProcessedAsset[]>
async optimizeImage(imageUrl: string): Promise<OptimizedImage>
async generateResponsiveImages(imageUrl: string): Promise<ResponsiveImageSet>
async copyStaticAssets(): Promise<GeneratedAsset[]>
async generateAssetManifest(assets: GeneratedAsset[]): Promise<void>
private downloadImage(url: string): Promise<Buffer>
private resizeImage(buffer: Buffer, sizes: number[]): Promise<ResizedImage[]>
private uploadToS3(asset: ProcessedAsset): Promise<string>
}
interface AssetConfig {
staticDir: string
outputDir: string
s3Bucket: string
s3Prefix: string
imageSizes: number[]
imageQuality: number
}
interface ProcessedAsset {
original: string
optimized: string
formats: string[]
sizes: { width: number; height: number; url: string }[]
}
5. Build Pipeline
5.1 Build Process Flow
1. Load Configuration
├── Shared commons package configuration
├── Build settings
└── Deployment config
2. Fetch Data from Notion (via fusite-commons)
├── Projects (public only)
├── Fuschronicles (all)
├── Tasks (by project)
└── About page
3. Process Content
├── Transform Notion blocks to HTML
├── Download and optimize images
├── Generate responsive image sets
└── Process static assets
4. Generate Routes (following high-level design route map)
├── Static routes (/, /about, /projects, /fuschronicles)
├── Dynamic project routes (/projects/{slug}, /projects/{slug}/timeline, /projects/{slug}/backlog)
├── Dynamic fuschronicle routes (/fuschronicles/{slug})
└── Error pages (/404)
5. Render Pages
├── Apply templates with data
├── Generate SEO metadata
├── Process internal links
└── Optimize HTML
6. Generate Supplementary Files
├── Sitemap.xml
├── Robots.txt
├── RSS feed
└── Asset manifest
7. Deploy to S3
├── Upload HTML files
├── Upload processed assets
├── Set appropriate headers
└── Invalidate CloudFront cache
5.2 Build Scripts
package.json
scripts:
json
{
"scripts": {
"build": "bun run src/build/cli.ts",
"build:dev": "bun run src/build/cli.ts --env=dev",
"build:prod": "bun run src/build/cli.ts --env=prod",
"deploy": "bun run build:prod && bun run src/build/deploy.ts",
"dev": "bun run build:dev && bun run serve",
"serve": "bun run src/dev/server.ts",
"preview": "bun run build && bun run serve",
"clean": "rm -rf dist cache",
"validate": "bun run src/build/validate.ts"
}
}
6. Configuration
6.1 Environment Variables
bash
# Notion Configuration (handled by fusite-commons package)
# See API Low-Level Design for complete Notion configuration details
NOTION_TOKEN=secret_xxx
NOTION_DATABASE_ABOUT=xxx
NOTION_DATABASE_PROJECT=xxx
NOTION_DATABASE_TASK=xxx
NOTION_DATABASE_CHRONICLE=xxx
# Site Configuration (Web-specific)
SITE_URL=https://fuscripts.com
SITE_NAME="Fuscripts"
SITE_DESCRIPTION="Personal projects and development logs"
SITE_AUTHOR="Fusca"
# Build Configuration (Web-specific)
BUILD_ENV=production
ENABLE_CACHE=true
CACHE_TTL_HOURS=6
# AWS Configuration (for deployment)
AWS_REGION=us-east-1
S3_BUCKET=fusite-static
S3_ASSETS_PREFIX=assets/
CLOUDFRONT_DISTRIBUTION_ID=optional_cf_id
# Development
DEV_PORT=3000
DEV_OPEN_BROWSER=true
6.2 Build Configuration
build.config.ts
typescript
// Note: Notion configuration is handled by fusite-commons package
export interface BuildConfig {
site: {
url: string
name: string
description: string
author: string
defaultImage: string
}
build: {
outputDir: string
cacheDir: string
enableCache: boolean
cacheTtl: number
}
assets: {
staticDir: string
s3Bucket: string
s3Prefix: string
imageSizes: number[]
imageQuality: number
}
deployment: {
s3Bucket: string
cloudfrontDistId?: string
invalidatePaths: string[]
}
}
7. Templates and Styling
7.1 Handlebars Helpers
typescript
// Custom Handlebars helpers
export function registerHelpers(handlebars: HandlebarsEnvironment): void {
// Date formatting
handlebars.registerHelper("formatDate", (date: string, format: string) => {
return formatDate(new Date(date), format)
})
// Content rendering
handlebars.registerHelper("renderContent", (content: HtmlContent[]) => {
return new handlebars.SafeString(processHtmlContent(content))
})
// URL helpers
handlebars.registerHelper("projectUrl", (slug: string) => {
return `/projects/${slug}`
})
handlebars.registerHelper("chronicleUrl", (slug: string) => {
return `/chronicles/${slug}`
})
// Conditional helpers
handlebars.registerHelper("ifEquals", (a: any, b: any, options: any) => {
return a === b ? options.fn(this) : options.inverse(this)
})
// Asset helpers
handlebars.registerHelper("assetUrl", (path: string) => {
return `/assets/${path}`
})
}
7.2 Tailwind Configuration
tailwind.config.js
javascript
module.exports = {
content: ["./src/templates/**/*.hbs", "./src/styles/**/*.css"],
theme: {
extend: {
colors: {
primary: {
50: "#f0f9ff",
500: "#3b82f6",
900: "#1e3a8a",
},
},
typography: {
DEFAULT: {
css: {
maxWidth: "none",
color: "#374151",
a: {
color: "#3b82f6",
"&:hover": {
color: "#1d4ed8",
},
},
},
},
},
},
},
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
}
8. Deployment and CI/CD
8.1 GitHub Actions Workflow
.github/workflows/build-deploy.yml
yaml
name: Build and Deploy Fusite
on:
push:
branches: [main]
schedule:
- cron: "0 */6 * * *" # Rebuild every 6 hours
workflow_dispatch:
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build site
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ABOUT: ${{ secrets.NOTION_DATABASE_ABOUT }}
NOTION_DATABASE_PROJECT: ${{ secrets.NOTION_DATABASE_PROJECT }}
NOTION_DATABASE_TASK: ${{ secrets.NOTION_DATABASE_TASK }}
NOTION_DATABASE_CHRONICLE: ${{ secrets.NOTION_DATABASE_CHRONICLE }}
SITE_URL: ${{ vars.SITE_URL }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: bun run build:prod
- name: Deploy to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_BUCKET: ${{ vars.S3_BUCKET }}
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
run: bun run deploy
8.2 Deployment Script
src/build/deploy.ts
typescript
export class Deployer {
private s3Client: S3Client
private cloudfrontClient?: CloudFrontClient
private config: DeploymentConfig
constructor(config: DeploymentConfig)
async deployToS3(buildDir: string): Promise<DeploymentResult>
async invalidateCloudFront(paths: string[]): Promise<void>
private async uploadFile(filePath: string, key: string): Promise<void>
private getContentType(filePath: string): string
private getCacheControl(filePath: string): string
private async syncDirectory(
localDir: string,
s3Prefix: string
): Promise<UploadResult[]>
}
interface DeploymentResult {
success: boolean
filesUploaded: number
errors: DeploymentError[]
invalidationId?: string
deploymentTime: number
}
9. Development Workflow
9.1 Development Server
typescript
// src/dev/server.ts
export class DevServer {
private port: number
private buildDir: string
constructor(config: DevConfig)
async start(): Promise<void>
async rebuild(): Promise<void>
async watchForChanges(): Promise<void>
private serveStatic(request: Request): Response
private handle404(): Response
}
9.2 Build Validation
typescript
// src/build/validate.ts
export class BuildValidator {
async validateBuild(buildDir: string): Promise<ValidationResult>
private async checkHtmlValidity(
htmlFiles: string[]
): Promise<ValidationError[]>
private async checkInternalLinks(htmlFiles: string[]): Promise<LinkError[]>
private async checkAssets(assetFiles: string[]): Promise<AssetError[]>
private async checkSEO(htmlFiles: string[]): Promise<SEOError[]>
}
10. Error Handling and Monitoring
10.1 Build Error Types
typescript
export class BuildError extends Error {
constructor(message: string, public phase: BuildPhase, public details?: any) {
super(message)
}
}
export class ApiError extends BuildError {
constructor(message: string, public endpoint: string, details?: any) {
super(message, "api-fetch", details)
}
}
export class TemplateError extends BuildError {
constructor(message: string, public template: string, details?: any) {
super(message, "template-render", details)
}
}
export class AssetError extends BuildError {
constructor(message: string, public asset: string, details?: any) {
super(message, "asset-process", details)
}
}
type BuildPhase =
| "config"
| "api-fetch"
| "template-render"
| "asset-process"
| "route-generate"
| "deploy"
10.2 Build Monitoring
typescript
export class BuildMonitor {
async trackBuildStart(): Promise<string>
async trackBuildEnd(buildId: string, result: BuildResult): Promise<void>
async trackError(buildId: string, error: BuildError): Promise<void>
async generateBuildReport(buildId: string): Promise<BuildReport>
private logBuildMetrics(result: BuildResult): void
private notifyOnFailure(error: BuildError): Promise<void>
}
interface BuildReport {
buildId: string
duration: number
success: boolean
routesGenerated: number
assetsProcessed: number
cacheEfficiency: number
errors: BuildError[]
}
11. Summary
This low-level design provides a comprehensive blueprint for implementing the Fusite Web static site generator that:
- Shares Core Logic: Uses the
fusite-commons
package to avoid duplication with the API - Follows High-Level Route Map: Implements all routes defined in the high-level design (
/
,/about
,/projects
,/fuschronicles
, etc.) - Optimized for Performance: Generates static HTML with efficient asset processing and CDN deployment
- SEO-Ready: Implements comprehensive metadata, sitemaps, and structured data
- Maintainable: Clear separation between shared business logic (commons) and web-specific concerns (templates, assets, deployment)
The Web project serves as the public-facing website while the API serves other internal projects, both leveraging the same reliable Notion data access layer.