FeaturesTemplatesShowcaseTownie
AI
BlogDocsPricing
Log inSign up
lightweight
lightweightglimpse2-runbook-test
Remix of lightweight/glimpse2-runbook
Public
Like
glimpse2-runbook-test
Home
Code
7
_townie
13
backend
7
frontend
1
shared
1
.vtignore
deno.json
H
main.tsx
Branches
3
Pull requests
Remixes
History
Environment variables
5
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
_townie
/
08-related.md
Code
/
_townie
/
08-related.md
Search
9/3/2025
Viewing readonly version of main branch: v37
View latest version
08-related.md

Complete Implementation Instructions: Enhanced /glimpse/:id Endpoint with Related Content and Nested Blocks

Overview

Enhance the existing /glimpse/:id endpoint to fetch and attach related pages from the "Glimpse content" relation property, sorted by "Order" property, including full page blocks/content with all nested blocks recursively fetched to support callouts, code blocks, lists, tables, columns, and other rich content structures.

Understanding the Existing Patterns

Service Response Pattern

Services return consistent structure:

// Success: {success: true, data: response, timestamp: string}
// Failure: {success: false, error: string, timestamp: string}

Controller Pattern

Controllers check result.success and handle accordingly:

  • If !result.success: Return 500 error with details
  • If result.success: Return full service response (includes success/data/timestamp)

Error Handling Philosophy

"All or nothing" - operations either succeed completely or fail completely. No partial success states.

Implementation Steps

Step 1: Add Required Service Functions

Add to /backend/services/notion.service.ts before the updatePageUrl function:

async function getAllBlocksRecursively(blockId: string): Promise<any[]> {
  const response = await notion.blocks.children.list({
    block_id: blockId,
    page_size: 100 // Get more blocks per request
  });

  const blocks = response.results;

  // For each block, check if it has children and fetch them recursively
  for (const block of blocks) {
    if (block.has_children) {
      try {
        block.children = await getAllBlocksRecursively(block.id);
      } catch (error) {
        // If fetching children fails, log but don't fail the entire operation
        console.warn(`Failed to fetch children for block ${block.id}:`, error.message);
        block.children = [];
      }
    }
  }

  return blocks;
}

export async function getPageBlocks(pageId: string) {
  try {
    const blocks = await getAllBlocksRecursively(pageId);
    return {
      success: true,
      data: { results: blocks },
      timestamp: new Date().toISOString(),
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      timestamp: new Date().toISOString(),
    };
  }
}

export async function getMultiplePages(pageIds: string[]) {
  try {
    // Fetch all pages concurrently
    const pagePromises = pageIds.map(async (pageId) => {
      const [pageResult, blocksResult] = await Promise.all([
        notion.pages.retrieve({ page_id: pageId }),
        getAllBlocksRecursively(pageId)
      ]);

      return {
        id: pageId,
        page: pageResult,
        blocks: { results: blocksResult } // Maintain same structure as before
      };
    });

    const results = await Promise.all(pagePromises);

    return {
      success: true,
      data: results,
      timestamp: new Date().toISOString(),
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      timestamp: new Date().toISOString(),
    };
  }
}

Step 2: Update Controller Import

In /backend/controllers/glimpse.controller.ts, update the import:

import { getPageById, getMultiplePages } from "../services/notion.service.ts";

Step 3: Replace Controller Function

Replace the entire glimpseHandler function in /backend/controllers/glimpse.controller.ts:

export async function glimpseHandler(c: Context) {
  const id = c.req.param("id");

  if (!id) {
    return c.json({ error: "Demo ID is required" }, 400);
  }

  // Fetch the main page
  const result = await getPageById(id);

  if (!result.success) {
    return c.json({ error: "Failed to fetch demo data", details: result.error }, 500);
  }

  // Filter out button properties from the main page
  if (result.data?.properties) {
    const filteredProperties = Object.fromEntries(
      Object.entries(result.data.properties).filter(([key, value]) => value?.type !== "button")
    );

    result.data.properties = filteredProperties;
  }

  // Extract related page IDs from "Glimpse content" relation property
  const glimpseContentProperty = result.data?.properties?.["Glimpse content"];
  let glimpseContent = [];

  if (glimpseContentProperty?.type === "relation" && glimpseContentProperty.relation?.length > 0) {
    const relatedPageIds = glimpseContentProperty.relation.map((rel: any) => rel.id);

    // Fetch all related pages with their content
    const relatedPagesResult = await getMultiplePages(relatedPageIds);

    if (!relatedPagesResult.success) {
      return c.json({
        error: "Failed to fetch related pages",
        details: relatedPagesResult.error
      }, 500);
    }

    // Process and sort the related pages
    glimpseContent = relatedPagesResult.data
      .map((item: any) => {
        // Filter out button properties from related pages too
        const filteredProperties = item.page.properties ?
          Object.fromEntries(
            Object.entries(item.page.properties).filter(([key, value]) => (value as any)?.type !== "button")
          ) : {};

        return {
          id: item.id,
          properties: filteredProperties,
          blocks: item.blocks.results
        };
      })
      // Sort by Order property
      .sort((a: any, b: any) => {
        const orderA = a.properties?.Order?.number || 0;
        const orderB = b.properties?.Order?.number || 0;
        return orderA - orderB;
      });
  }

  // Add the glimpse content to the response
  result.data.glimpseContent = glimpseContent;

  return c.json(result);
}

Step 4: Update Documentation

Update /backend/routes/glimpse/README.md:

# Glimpse Routes

This directory contains routes for the glimpse functionality.

## Routes

### GET /:id

- **Purpose**: Returns Notion page data as JSON with related content (filtered for data consumption)
- **Authentication**: Required (Google OAuth)
- **Parameters**: `id` - Notion page ID
- **Response**: Service response object with Notion page data and related content
- **Filtering**: Removes UI-specific properties (type: "button") for cleaner data consumption

#### Enhanced Functionality

The endpoint now fetches and includes related pages from the "Glimpse content" relation property:

1. **Main Page**: Returns the requested page with filtered properties
2. **Related Content**: Fetches all pages referenced in the "Glimpse content" relation
3. **Content Blocks**: Includes full page blocks/content for each related page with nested blocks recursively fetched
4. **Nested Content**: Supports callouts, code blocks, lists, tables, columns, and other nested block structures
5. **Sorting**: Related pages are sorted by their "Order" property (ascending)
6. **Error Handling**: Returns failure if main page OR any related page fails to fetch

#### Response Structure

```json
{
  "success": true,
  "data": {
    "id": "main-page-id",
    "properties": { ... }, // Main page properties (buttons filtered out)
    "glimpseContent": [
      {
        "id": "related-page-1",
        "properties": { "Order": { "number": 1 }, ... }, // Related page properties (buttons filtered out)
        "blocks": [ ... ] // Full page content blocks with nested structures (callouts, lists, tables, etc.)
      },
      {
        "id": "related-page-2",
        "properties": { "Order": { "number": 2 }, ... },
        "blocks": [ ... ] // Full page content blocks with nested structures
      }
    ]
  },
  "timestamp": "2025-07-27T12:00:00.000Z"
}

Nested Block Structure

Blocks with children will have a children property containing their nested blocks:

{
  "type": "bulleted_list_item",
  "bulleted_list_item": { ... },
  "has_children": true,
  "children": [
    {
      "type": "bulleted_list_item",
      "bulleted_list_item": { ... },
      "has_children": false
    }
  ]
}

Error Cases

  • Main page not found: Returns {success: false, error: "...", timestamp: "..."}
  • Related page fetch fails: Returns {success: false, error: "Failed to fetch related pages", timestamp: "..."}
  • Individual block children fail: Logs warning, continues with empty children array
  • No related content: Returns successful response with empty glimpseContent array

This is the same functionality as /views/glimpse/:id but mounted at /glimpse/:id for convenience.

#### Update main `/README.md` Views section:

Replace:
```markdown
### Views
- `GET /views/glimpse/:id` - Get Notion page data as JSON (filtered for data consumption)

With:

### Views
- `GET /views/glimpse/:id` - Get Notion page data with related content as JSON (filtered for data consumption)
- `GET /glimpse/:id` - Same as above, alternative endpoint

Update /backend/services/README.md Examples section:

Replace:

## Examples

- `getDatabaseById(id)` - retrieve specific database from Notion
- `searchNotionDatabases()` - search for all databases
- `updatePageUrl(pageId, url)` - update a Notion page's URL property

With:

## Examples

- `getDatabaseById(id)` - retrieve specific database from Notion
- `getPageById(id)` - retrieve specific page from Notion
- `getPageBlocks(id)` - retrieve page content blocks from Notion (with nested blocks recursively)
- `getMultiplePages(ids)` - batch retrieve multiple pages with their blocks from Notion (with nested blocks recursively)
- `searchNotionDatabases()` - search for all databases
- `updatePageUrl(pageId, url)` - update a Notion page's URL property

Key Implementation Details

Property Names

  • Relation property: "Glimpse content" (exact match required)
  • Sort property: "Order" (exact match required)

Nested Block Handling

  • Recursive fetching: Uses getAllBlocksRecursively() to fetch all nested blocks
  • Error resilience: Individual block failures don't break the entire operation
  • Performance optimization: Uses page_size: 100 for fewer API calls
  • Complete structure: Supports all Notion block types with children (callouts, lists, tables, columns, toggles, etc.)

Error Handling Strategy

  • All or nothing: If any related page fails, entire request fails
  • Individual block resilience: If fetching children of a specific block fails, logs warning but continues
  • Consistent responses: Always use service response pattern
  • Detailed errors: Include specific error details in failure responses

Performance Considerations

  • Concurrent fetching: Use Promise.all() for parallel page fetching
  • Recursive API calls: More requests due to nested block fetching
  • Batch operations: Single service call handles multiple pages
  • Reasonable limits: Designed for up to 10 related pages
  • Graceful degradation: Individual block failures don't break entire response

Data Processing

  • Button filtering: Remove type: "button" properties from all pages
  • Sorting: Sort by Order.number property (ascending, default 0 for missing)
  • Block extraction: Use blocks.results from recursive fetch
  • Nested structure: Blocks with children have children array property

Response Structure

  • Backward compatible: Existing clients continue to work
  • Additive: New glimpseContent array added to existing response
  • Consistent: Maintains service response pattern with success/data/timestamp
  • Rich content: Full nested block structures for complete content representation

Supported Nested Block Types

  • Lists: Bulleted, numbered, to-do items with nested items
  • Callouts: With nested content blocks
  • Code blocks: With syntax highlighting
  • Tables: With cell content
  • Columns: With nested blocks in each column
  • Toggles: With hidden/expandable content
  • Quotes: With nested formatting
  • Synced blocks: With synchronized content
  • Any other block type: That supports children

Testing Strategy

  1. Test with no related content: Should return empty glimpseContent array
  2. Test with related content: Should return sorted pages with complete nested blocks
  3. Test nested structures: Verify callouts, lists, tables render with full hierarchy
  4. Test error cases: Invalid page IDs should return failure responses
  5. Test individual block failures: Should continue with warnings, not fail entirely
  6. Test authentication: Unauthenticated requests should show login page
  7. Test performance: Monitor response times with deeply nested content

Block Structure Examples

Nested List Example

{
  "type": "bulleted_list_item",
  "bulleted_list_item": {
    "rich_text": [{"plain_text": "Parent item"}]
  },
  "has_children": true,
  "children": [
    {
      "type": "bulleted_list_item",
      "bulleted_list_item": {
        "rich_text": [{"plain_text": "Child item"}]
      },
      "has_children": false
    }
  ]
}

Callout with Nested Content Example

{
  "type": "callout",
  "callout": {
    "rich_text": [{"plain_text": "Important note"}],
    "icon": {"emoji": "💡"}
  },
  "has_children": true,
  "children": [
    {
      "type": "paragraph",
      "paragraph": {
        "rich_text": [{"plain_text": "Additional details inside callout"}]
      }
    }
  ]
}

This implementation provides complete nested block structures that will properly represent all rich content types including callouts, code blocks, lists, tables, columns, and any other nested Notion block structures.

Go to top
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Product
FeaturesPricing
Developers
DocsStatusAPI ExamplesNPM Package Examples
Explore
ShowcaseTemplatesNewest ValsTrending ValsNewsletter
Company
AboutBlogCareersBrandhi@val.town
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.