Technical Architecture: How the Platform Works

Technical Architecture: How the Platform Works

Technical Architecture: How the Platform Works

Understanding how the Bookworm Light theme adapter transforms your JSON content into a fully-rendered static site helps you leverage its capabilities effectively. This deep technical dive covers the entire pipeline from content ingestion to final build output.

System Overview

The platform consists of several interconnected components:

┌─────────────────┐    ┌──────────────────┐    ┌────────────────┐
│   Start Folder  │───▶│  Theme Adapter   │───▶│  Astro Build   │
│   (JSON files)  │    │  (TypeScript)    │    │  (Static HTML) │
└─────────────────┘    └──────────────────┘    └────────────────┘
        │                       │                       │
   site.json              config.json              index.html
   pages/*.json           menu.json               blog/*.html
   markdown/              theme.json              authors/*.html
                          content/*.md            categories/*.html

The Content Pipeline

Stage 1: Content Ingestion

The pipeline begins by reading your project’s content files:

interface StartProject {
  site: Site;           // site.json configuration
  pages: Page[];        // pages/*.json content files
  assets: Asset[];      // images, documents, etc.
}

async function ingestContent(projectPath: string): Promise<StartProject> {
  const site = await readJson(`${projectPath}/site.json`);
  const pages = await readPageDirectory(`${projectPath}/pages`);
  const assets = await scanAssetDirectory(`${projectPath}/assets`);
  
  return { site, pages, assets };
}

Stage 2: Site Configuration Generation

The adapter generates configuration files that control the theme:

async generateSiteConfig(site: Site, outputDir: string): Promise<string[]> {
  const configDir = join(outputDir, 'src', 'config');
  await mkdir(configDir, { recursive: true });
  
  // Generate config.json - site title, metadata, contact info
  const configJson = this.buildConfigJson(site);
  await writeFile(join(configDir, 'config.json'), JSON.stringify(configJson, null, 2));
  
  // Generate menu.json - navigation structure
  const menuJson = this.buildMenuJson(site);
  await writeFile(join(configDir, 'menu.json'), JSON.stringify(menuJson, null, 2));
  
  // Generate theme.json - colors, typography
  const themeJson = this.buildThemeJson(site.designTokens);
  await writeFile(join(configDir, 'theme.json'), JSON.stringify(themeJson, null, 2));
  
  return [/* generated file paths */];
}

Stage 3: Content Transformation

Each page goes through transformation to match the theme’s expected format:

transformPage(page: Page, site: Site): ThemePageProps {
  return {
    title: page.title,
    description: page.description || '',
    url: page.url,
    meta_title: page.seo?.title || `${page.title} | ${site.name}`,
    date: this.formatDate(this.extractDate(page)),
    image: this.extractImagePath(page),
    categories: this.extractCategories(page),
    authors: this.extractAuthorsArray(page),  // KEY: Array format
    tags: this.extractTags(page),
    draft: page.metadata?.draft === true,
  };
}

Stage 4: Markdown Generation

Transformed content is written as Markdown with YAML frontmatter:

private toMarkdown(frontmatter: Frontmatter, body: string): string {
  const yaml = this.toYaml(frontmatter);
  return `---\n${yaml}---\n\n${body}`;
}

private toYaml(obj: Record<string, unknown>): string {
  let yaml = '';
  for (const [key, value] of Object.entries(obj)) {
    if (Array.isArray(value)) {
      yaml += `${key}:\n`;
      for (const item of value) {
        yaml += `  - ${this.escapeYamlString(String(item))}\n`;
      }
    } else if (typeof value === 'string') {
      yaml += `${key}: ${this.escapeYamlString(value)}\n`;
    }
    // ... handle other types
  }
  return yaml;
}

Stage 5: Astro Build

Finally, Astro compiles everything into static HTML:

astro build

Key Architecture Decisions

Why JSON Source Files?

We chose JSON for content definition because:

  1. Schema Validation: JSON Schema provides compile-time validation
  2. Tooling Support: Excellent editor support with autocomplete
  3. API Compatibility: Easy to generate from headless CMS or APIs
  4. Diffability: Version control shows clear content changes
  5. Transformation: Simple parsing for adapter transformation

Why Astro?

Astro provides optimal characteristics for content sites:

  1. Zero JS by Default: HTML ships without JavaScript overhead
  2. Partial Hydration: Add interactivity only where needed
  3. Content Collections: Built-in typed content management
  4. Framework Agnostic: Use React, Vue, Svelte components as needed
  5. Performance: Automatic image optimization, prefetching

Why TypeScript Adapters?

TypeScript adapters offer:

  1. Type Safety: Catch transformation errors at build time
  2. IDE Support: Autocomplete and inline documentation
  3. Refactoring: Safe changes across large codebases
  4. Testing: Easy unit testing of transformations
  5. Documentation: Types serve as living documentation

Configuration File Deep Dive

config.json Structure

interface BookwormConfigJson {
  site: {
    title: string;           // Browser tab title
    base_url: string;        // Full site URL
    base_path: string;       // URL path prefix
    trailing_slash: boolean; // URL formatting
    favicon: string;         // Favicon path
    logo: string;           // Logo image path
    logo_width: string;     // Logo dimensions
    logo_height: string;
    logo_text: string;      // Fallback text when no logo
  };
  settings: {
    pagination: number;     // Posts per page
  };
  metadata: {
    meta_author: string;    // Default author for SEO
    meta_image: string;     // Default OG image
    meta_description: string; // Default description
  };
  google_tag_manager: {
    enable: boolean;
    gtm_id: string;
  };
  params: {
    contact_form_action: string;
    copyright: string;      // Footer copyright text
  };
  contactinfo: {
    address: string;
    email: string;
    phone: string;
  };
}
interface BookwormMenuJson {
  main: Array<{
    name: string;           // Display label
    url: string;            // Link destination
    hasChildren?: boolean;  // Dropdown indicator
    children?: Array<{      // Dropdown items
      name: string;
      url: string;
    }>;
  }>;
  footer: Array<{
    name: string;
    url: string;
  }>;
}

Critical: The menu.json COMPLETELY REPLACES the theme’s default navigation. Only items defined in site.navigation[] appear in the rendered site.

theme.json Structure

interface BookwormThemeJson {
  colors: {
    default: {
      theme_color: {
        primary: string;    // Brand color
        body: string;       // Page background
        border: string;     // Border color
        light: string;      // Muted background
        dark: string;       // Dark elements
      };
      text_color: {
        text: string;       // Body text
        'text-dark': string; // Headings
        'text-light': string; // Subtle text
      };
    };
    darkmode?: { /* same structure */ };
  };
  fonts: {
    font_family: {
      primary: string;      // Body font (Google Fonts format)
      primary_type: string; // serif | sans-serif | monospace
      secondary?: string;   // Heading font
      secondary_type?: string;
    };
    font_size: {
      base: string;        // Base font size
      scale: string;       // Type scale multiplier
    };
  };
}

Author System Architecture

The Authors Array Pattern

Bookworm Light’s key differentiator is treating authors as an array:

// Schema definition
authors: z.array(z.string()).default(['admin'])

// Frontmatter output
---
authors:
  - sarah-mitchell
  - james-chen
---

Author Extraction Logic

private extractAuthorsArray(page: Page): string[] {
  const metadata = page.metadata || {};

  // Already an array - use directly
  if (Array.isArray(metadata.authors)) {
    return metadata.authors as string[];
  }

  // Single author string - convert to array
  if (typeof metadata.author === 'string') {
    return [this.slugify(metadata.author)];
  }

  // Legacy array under 'author' key
  if (Array.isArray(metadata.author)) {
    return metadata.author.map((a) =>
      typeof a === 'string' ? this.slugify(a) : 'admin'
    );
  }

  return ['admin'];
}

Automatic Stub Generation

The adapter tracks all referenced authors and generates stub files:

async generateContentFiles(pages: Page[], site: Site, outputDir: string) {
  const authorsNeeded = new Set<string>();

  // Collect authors from all pages
  for (const page of pages) {
    const authors = this.extractAuthorsArray(page);
    authors.forEach(author => authorsNeeded.add(author));
  }

  // Generate stub files for missing authors
  for (const authorSlug of authorsNeeded) {
    const authorPath = join(outputDir, 'src/content/authors', `${authorSlug}.md`);
    if (!existsSync(authorPath)) {
      const stub = this.buildAuthorFrontmatter(authorSlug);
      await writeFile(authorPath, this.toMarkdown(stub, ''));
    }
  }
}

Design Token Resolution

Token Mapping Strategy

Design tokens map from site.json to theme.json:

private buildThemeJson(designTokens?: DesignTokens): BookwormThemeJson {
  // Start with theme defaults
  const themeColors = {
    primary: '#01AD9F',
    body: '#fff',
    border: '#D5D5D5',
    light: '#FAFAFA',
    dark: '#152035',
  };

  // Override with user tokens
  if (designTokens?.colors?.brand?.primary) {
    themeColors.primary = designTokens.colors.brand.primary;
  }
  if (designTokens?.colors?.background?.page) {
    themeColors.body = designTokens.colors.background.page;
  }
  // ... continue mapping

  return { colors: { default: { theme_color: themeColors, /* ... */ } } };
}

CSS Override Generation

Some tokens require CSS custom property overrides:

private async generateAccentColorOverrides(
  tokens: DesignTokens,
  site: Site,
  outputDir: string
): Promise<string> {
  const lines: string[] = [];

  if (tokens.colors?.brand?.accent) {
    lines.push(`  --color-accent: ${tokens.colors.brand.accent};`);
  }
  if (tokens.colors?.brand?.secondary) {
    lines.push(`  --color-secondary: ${tokens.colors.brand.secondary};`);
  }

  const css = `:root {\n${lines.join('\n')}\n}`;
  await writeFile(join(outputDir, 'src/styles/token-overrides.css'), css);
}

Performance Considerations

Build-Time Optimization

  1. Parallel Processing: Independent pages transform concurrently
  2. Incremental Builds: Changed files trigger partial rebuilds
  3. Asset Hashing: Cache-busted assets for optimal caching
  4. Image Optimization: Automatic responsive image generation

Runtime Performance

  1. Zero JavaScript: Content pages ship no JS by default
  2. CSS Inlining: Critical CSS inlined in HTML head
  3. Prefetching: Links prefetch on hover
  4. Edge Caching: Static files cache at CDN edge

Error Handling

Validation Layer

Zod schemas validate content before transformation:

const pageSchema = z.object({
  id: z.string(),
  title: z.string(),
  url: z.string(),
  template: z.enum(['article', 'page']).optional(),
  metadata: z.object({
    authors: z.array(z.string()).optional(),
    categories: z.array(z.string()).optional(),
    date: z.string().optional(),
  }).optional(),
});

function validatePage(page: unknown): Page {
  const result = pageSchema.safeParse(page);
  if (!result.success) {
    throw new Error(`Invalid page: ${result.error.message}`);
  }
  return result.data;
}

Graceful Degradation

The adapter provides sensible defaults for missing data:

  • Missing authors → ['admin']
  • Missing categories → ['others']
  • Missing dates → Current date
  • Missing images → No image rendered

Testing Strategy

Unit Tests

Transformation functions are thoroughly unit tested:

describe('extractAuthorsArray', () => {
  it('handles array format', () => {
    const page = { metadata: { authors: ['alice', 'bob'] } };
    expect(adapter.extractAuthorsArray(page)).toEqual(['alice', 'bob']);
  });

  it('converts single author to array', () => {
    const page = { metadata: { author: 'Alice Smith' } };
    expect(adapter.extractAuthorsArray(page)).toEqual(['alice-smith']);
  });

  it('defaults to admin', () => {
    const page = { metadata: {} };
    expect(adapter.extractAuthorsArray(page)).toEqual(['admin']);
  });
});

Integration Tests

End-to-end tests verify the complete pipeline:

it('generates valid Astro site', async () => {
  const result = await adapter.generate(testProject, outputDir);
  
  // Verify generated files exist
  expect(existsSync(join(outputDir, 'src/config/config.json'))).toBe(true);
  expect(existsSync(join(outputDir, 'src/config/menu.json'))).toBe(true);
  
  // Verify Astro builds successfully
  const { exitCode } = await exec('npm run build', { cwd: outputDir });
  expect(exitCode).toBe(0);
});

Conclusion

The Bookworm Light theme adapter architecture prioritizes:

  1. Correctness: Type-safe transformations prevent runtime errors
  2. Flexibility: Design tokens enable extensive customization
  3. Performance: Static output delivers optimal load times
  4. Maintainability: Clear separation of concerns simplifies updates

Understanding this architecture helps you troubleshoot issues, extend functionality, and make the most of the platform’s capabilities.