Skip to main content

Site Management

๐Ÿ“– Overviewโ€‹

internal/api/v1/sites/services/sites.js manages the complete website lifecycle through integration with the Duda platform. It handles active site retrieval with smart caching, template browsing, publishing/unpublishing workflows, site deletion, analytics, SSO access, and form collection.

File Path: internal/api/v1/sites/services/sites.js

๐Ÿ—„๏ธ Collections Usedโ€‹

๐Ÿ“š Full Schema: See Database Collections Documentation

agency_websitesโ€‹

  • Operations: Read/Write for site management and caching
  • Model: shared/models/agency-website.js
  • Usage Context: Stores website records with status tracking, builder integration, and auto-refresh timestamps

agency_website_templatesโ€‹

  • Operations: Read for template browsing and site creation
  • Model: shared/models/agency-website-template.js
  • Usage Context: Available templates for creating new websites

instasitesโ€‹

  • Operations: Read via aggregation for multi-type site support
  • Model: shared/models/instasite.js
  • Usage Context: Quick landing pages linked to business contacts

contactsโ€‹

  • Operations: Aggregation pipeline for instasite association
  • Model: shared/models/contact.js
  • Usage Context: Business information for instasites lookup

_store.productsโ€‹

  • Operations: Read with price population
  • Model: shared/models/store-product.js
  • Usage Context: Website pricing information for template variations

๐Ÿ”„ Data Flowโ€‹

sequenceDiagram
participant Client
participant Controller
participant Service
participant Database
participant Duda

Client->>Controller: GET /sites/active
Controller->>Service: getActiveSites()
Service->>Database: Find published sites

loop For expired cache
Service->>Duda: GET /sites/{id}
Duda-->>Service: Site details
Service->>Duda: GET /content/{id}
Duda-->>Service: Site content
Service->>Database: Update + refresh_at
end

Service->>Database: Aggregate instasites
Service-->>Controller: Combined sites
Controller-->>Client: Sites array

๐Ÿ”ง Business Logic & Functionsโ€‹

Site Retrieval & Cachingโ€‹


getActiveSites({ account_id, business, authToken, additional_options })โ€‹

Purpose: Retrieves all active (published) websites for an account, including both agency sites and instasites, with automatic cache refresh for stale data.

Parameters:

  • account_id (ObjectId) - Account identifier
  • business (Object) - Business information for instasite lookup
    • business.id (ObjectId) - Business ID for aggregation
  • authToken (String) - DashClicks authentication token
  • additional_options (Object, optional) - MongoDB projection for selective field loading (e.g., exclude thumbnails)

Returns: Promise<Array> - Combined array of agency websites and instasites

Business Logic Flow:

  1. Query Agency Sites

    • Find all AgencyWebsite records with status 'PUBLISHED'
    • Apply additional_options projection if provided
    • Tag results with type: 'agencysite'
  2. Check Cache Expiration

    • For each agency site, check if refresh_at <= Date.now()
    • Expired sites trigger Duda API refresh
  3. Parallel Cache Refresh

    • Batch expired sites into concurrent API calls
    • Fetch site details: GET ${DUDA_ENDPOINT}/sites/${builder_id}
    • Fetch site content: GET ${DUDA_ENDPOINT}/content/${builder_id}
    • Update database with fresh data + new refresh_at (+1 hour)
  4. Aggregate Instasites

    • Complex MongoDB pipeline to find instasites linked to account businesses
    • Match business by ID, lookup instasites with status 'PUBLISHED'
    • Tag results with type: 'instasite'
  5. Combine and Return

    • Merge agency sites and instasites arrays
    • Return unified website list

Key Business Rules:

  • Cache duration: 1 hour per site
  • Parallel processing: All refreshes run concurrently with Promise.all()
  • Graceful degradation: Individual refresh failures don't block other sites
  • Type distinction: Clear tagging for frontend filtering

Example Usage:

const sites = await siteService.getActiveSites({
account_id: req.account_id,
business: req.business,
authToken: req.authToken,
});
// Returns: [{ ...site, type: 'agencysite' }, { ...site, type: 'instasite' }]

Side Effects:

  • โš ๏ธ Updates agency_websites collection with refreshed data
  • โš ๏ธ Makes external API calls to Duda platform
  • โš ๏ธ May generate multiple concurrent HTTP requests

Purpose: Retrieves available website templates with pricing information. Returns unpublished work-in-progress site if one exists, otherwise shows template gallery.

Parameters:

  • account_id (ObjectId) - Account identifier
  • search (String, optional) - Filter templates by title (case-insensitive regex)

Returns: Promise<{ data: Array, pricing: Object }> - Templates/site with pricing details

Business Logic Flow:

  1. Fetch Pricing Data

    • Query StoreProduct for "Websites" product with platform_type 'dashclicks'
    • Populate prices array
    • Filter prices by metadata: type == 'informational' && location == 'store'
    • Extract price_ids and product_id
  2. Check for Work-in-Progress

    • Look for unpublished site belonging to account
    • If found, return it as sole variation (user should continue editing)
    • Populate template_id for display
  3. Fetch Templates (if no unpublished site)

    • Query all AgencyWebsiteTemplate records
    • Apply search filter if provided: title: { $regex: search, $options: 'i' }
  4. Enrich Template Data

    • Add template title from template_id.title
    • Construct Duda preview URLs for desktop/tablet/mobile
    • Duplicate thumbnails/previews at root level (Fulfillment Center 3.0 compatibility)
  5. Validate Pricing

    • Throw 404 error if no pricing data found
    • Ensures website creation can proceed

Key Business Rules:

  • Work-in-progress takes precedence over template gallery
  • Preview URLs use Duda's preview system with device query parameters
  • Pricing must exist or creation fails
  • Search applies case-insensitive regex matching

Error Handling:

  • Throws notFound() if pricing data missing: "No prices found for the agency website"

Example Usage:

const { data, pricing } = await siteService.getVariations(account_id, 'restaurant');
// Returns: { data: [...templates], pricing: { price_ids: [...], product_id: '...' }}

Publishing & Unpublishingโ€‹


publish({ id, account_id, authToken })โ€‹

Purpose: Publishes an unpublished website, making it live. Prevents multiple published sites per account.

Parameters:

  • id (ObjectId) - Site ID to publish
  • account_id (ObjectId) - Account identifier
  • authToken (String) - Authentication token

Returns: Promise<Object> - Published site data

Business Logic Flow:

  1. Duplicate Check

    • Query for existing published site on account
    • Throw badRequest() if found: "There is already an active site present on this account"
  2. Find Target Site

    • Query site by ID and account_id
    • Throw notFound() if not found: "The site does not exist"
  3. Call Duda API

    • PUT ${DUDA_ENDPOINT}/sites/${builder_id}/publish
    • Uses dual authentication headers (Bearer + x-duda-token)
  4. Update Database

    • Set site.status = 'PUBLISHED'
    • Save changes
  5. Return Updated Site

    • Convert to JSON and return

Key Business Rules:

  • Only one published site allowed per account
  • Site must belong to requesting account
  • Requires system-level scope permission

Error Handling:

  • badRequest(400): Duplicate published site
  • notFound(404): Site doesn't exist

Side Effects:

  • โš ๏ธ Updates site status in database
  • โš ๏ธ Makes external Duda API call
  • โš ๏ธ TODO: Should send socket notification (commented in code)

unpublish(account_id, authToken)โ€‹

Purpose: Unpublishes the currently published website, taking it offline.

Parameters:

  • account_id (ObjectId) - Account identifier
  • authToken (String) - Authentication token

Returns: Promise<Object> - Unpublished site data

Business Logic Flow:

  1. Find Published Site

    • Query for site with status 'PUBLISHED'
    • Throw notFound() if none found: "No active site found"
  2. Call Duda API

    • PUT ${DUDA_ENDPOINT}/sites/${builder_id}/unpublish
    • Dual authentication headers
  3. Update Database

    • Set site.status = 'UNPUBLISHED'
    • Save changes
  4. Return Updated Site

Key Business Rules:

  • Can only unpublish if published site exists
  • Status change persisted before returning

Error Handling:

  • notFound(404): No active site to unpublish

Site Deletionโ€‹


deleteSite({ account_id, account, authToken })โ€‹

Purpose: Safely deletes an unpublished website from both DashClicks and Duda. Includes multiple safety checks.

Parameters:

  • account_id (ObjectId) - Account identifier
  • account (Object) - Full account document
    • account.main (Boolean) - Must be true for deletion
  • authToken (String) - Authentication token

Returns: Promise<void>

Business Logic Flow:

  1. Main Account Check

    • Verify account.main === true
    • Throw forbidden() if false: "This account cannot perform this operation"
  2. Published Site Protection

    • Check if any published site exists
    • Throw forbidden() if found: "Cannot perform this operation on the published site"
  3. Find Unpublished Site

    • Query for unpublished site
    • Throw notFound() if none: "Cannot find any unpublished website"
  4. Attempt Unpublish (defensive)

    • Try PUT ${DUDA_ENDPOINT}/sites/${builder_id}/unpublish
    • Catch and ignore errors (site may already be unpublished)
  5. Delete from Database

    • AgencyWebsite.deleteMany({ account_id })
    • Removes all site records for account
  6. Delete from Duda

    • DELETE ${DUDA_ENDPOINT}/sites/${builder_id}
    • Permanently removes site from platform

Key Business Rules:

  • Only main accounts can delete sites
  • Cannot delete published sites (must unpublish first)
  • Defensive unpublish attempt prevents errors
  • Database deletion before Duda deletion

Error Handling:

  • forbidden(403): Not main account or published site exists
  • notFound(404): No unpublished site found

Known Issues:

  • โš ๏ธ TODO: Should use MongoDB transaction to prevent data inconsistency if Duda deletion fails after database deletion

Analytics & Dataโ€‹


analytics({ query, account_id, authToken })โ€‹

Purpose: Retrieves website analytics data from Duda platform for the published site.

Parameters:

  • query (Object) - Analytics query parameters
    • query.limit (Number, optional) - Result limit
    • query.from (String, optional) - Start date (ISO format)
    • query.to (String, optional) - End date
    • query.dimension (String, optional) - 'system' or 'geo'
    • query.result (String, optional) - 'traffic' or 'activities'
    • query.dateGranularity (String, optional) - 'DAYS', 'WEEKS', 'MONTHS', 'YEARS'
  • account_id (ObjectId) - Account identifier
  • authToken (String) - Authentication token

Returns: Promise<Object|Array> - Analytics data from Duda (raw passthrough)

Business Logic Flow:

  1. Find Published Site

    • Query for site with status 'PUBLISHED'
    • Return empty array if none found (graceful degradation)
  2. Call Duda Analytics API

    • GET ${DUDA_ENDPOINT}/sites/${builder_id}/analytics
    • Pass all query parameters through
    • Use dual authentication headers
  3. Return Raw Data

    • Pass through Duda's response unchanged

Key Business Rules:

  • Only published sites have analytics
  • Graceful degradation: Returns [] instead of error if no site
  • Query parameters forwarded directly to Duda

Error Handling:

  • No explicit errors thrown - returns empty array if no site
  • Duda API errors bubble up naturally

SSO & Formsโ€‹


sso({ id, query, account_id, authToken })โ€‹

Purpose: Generates a single sign-on URL for accessing the Duda site editor without separate authentication.

Parameters:

  • id (ObjectId) - Site ID
  • query (Object) - SSO query parameters
    • query.target (String, required) - Must be 'EDITOR'
    • query.limit (Number, optional) - Token validity duration
  • account_id (ObjectId) - Account identifier
  • authToken (String) - Authentication token

Returns: Promise<Object> - Duda SSO response with URL and token

Business Logic Flow:

  1. Find Site

    • Query by ID and account_id (any status)
    • Throw notFound() if not found: "Site not found"
  2. Call Duda SSO API

    • GET ${DUDA_ENDPOINT}/accounts/${builder_account}/sso
    • Include query params + site_name: builder_id
    • Dual authentication headers
  3. Return SSO Data

    • Pass through Duda's response (URL, token, expires_at)

Key Business Rules:

  • Works for both published and unpublished sites
  • Target must be 'EDITOR' (uppercase)
  • Token has expiration (configurable via limit param)

forms({ id, query, account_id, authToken })โ€‹

Purpose: Retrieves form submission data collected from the website.

Parameters:

  • id (ObjectId) - Site ID
  • query (Object) - Additional query parameters for Duda
  • account_id (ObjectId) - Account identifier
  • authToken (String) - Authentication token

Returns: Promise<Object> - Form submissions from Duda

Business Logic Flow:

  1. Find Site

    • Query by ID and account_id
    • Throw notFound() if not found: "Site not found"
  2. Call Duda Forms API

    • GET ${DUDA_ENDPOINT}/sites/${builder_id}/forms
    • Include query params + site_name: builder_id
    • Dual authentication headers
  3. Return Form Data

    • Pass through Duda's response unchanged

๐Ÿ”€ Integration Pointsโ€‹

External Servicesโ€‹

Duda Platformโ€‹

  • Endpoint: process.env.DUDA_INTERNAL_ENDPOINT
  • Authentication: Dual token (Bearer + x-duda-token)
  • Operations:
    • Site publishing/unpublishing
    • Content and detail retrieval
    • Analytics data
    • SSO link generation
    • Form data collection

Authentication Headers:

{
authorization: `Bearer ${authToken}`,
'x-account-id': account_id.toString(),
'x-duda-token': process.env.DUDA_TOKEN
}

Internal Dependenciesโ€‹

  • shared/models/agency-website.js - Site data model
  • shared/models/agency-website-template.js - Template model
  • shared/models/instasite.js - Landing page model
  • shared/utilities/catch-errors.js - Error helpers (forbidden, notFound, badRequest)
  • axios - HTTP client for Duda API

Store Moduleโ€‹

  • Purpose: Website pricing information
  • Integration: Query StoreProduct for "Websites" product
  • Filter: Prices with metadata.type == 'informational' && metadata.location == 'store'

๐Ÿงช Edge Cases & Special Handlingโ€‹

Case: Cache Expired During High Trafficโ€‹

Condition: Multiple sites need refresh simultaneously
Handling: Parallel processing with Promise.all(), errors don't block other sites
Performance: Prevents sequential bottleneck

Case: Unpublished Site as Templateโ€‹

Condition: User has work-in-progress site
Handling: Return unpublished site instead of template gallery
Reason: User should continue editing rather than start new site

Case: No Pricing Dataโ€‹

Condition: StoreProduct query returns empty or no informational prices
Handling: Throw notFound() to prevent site creation without pricing
Impact: Creation flow fails gracefully

Case: Duda API Timeoutโ€‹

Condition: External API call times out or fails
Handling: Error bubbles up to controller, logged to Sentry
Recovery: Frontend can retry request

Case: Delete After Database Success But Duda Failureโ€‹

Condition: Database deletion succeeds but Duda API call fails
Handling: Currently unhandled - creates orphaned Duda site
TODO: Wrap in MongoDB transaction for atomicity


โš ๏ธ Important Notesโ€‹

  • ๐Ÿšจ Transaction Missing: deleteSite needs transaction support (commented TODO)
  • ๐Ÿ”„ Socket Notification: publish should send real-time notification (commented TODO)
  • ๐Ÿ—„๏ธ Cache Strategy: 1-hour cache duration, stored in database not Redis
  • ๐Ÿ” Dual Auth: All Duda calls require both DashClicks token and Duda token
  • ๐Ÿ“Š Graceful Analytics: Returns empty array instead of error when no site found
  • ๐ŸŽฏ Type Tagging: Sites tagged as 'agencysite' or 'instasite' for frontend filtering
  • โšก Parallel Refresh: Multiple site refreshes run concurrently for performance

  • Parent Module: Sites Module
  • Related Service: Template Management
  • Controller: internal/api/v1/sites/controllers/sites.js
  • Routes: internal/api/v1/sites/routes/sites.js
๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM