Skip to content

SEO API

Manage SEO metadata for your site through the DCS portal API.

Overview

The SEO API allows you to:

  • Get/set page metadata (title, description, keywords)
  • Manage Open Graph tags
  • Configure Twitter cards
  • Generate sitemaps

Endpoints

Get Page SEO

Fetch SEO metadata for a page:

http
GET /api/sites/{siteId}/seo/{pageSlug}

Response

json
{
  "pageSlug": "home",
  "title": "Welcome to Acme Corp",
  "description": "Industry-leading solutions for modern businesses",
  "keywords": ["business", "solutions", "enterprise"],
  "canonical": "https://acme.com/",
  "openGraph": {
    "title": "Acme Corp - Business Solutions",
    "description": "Industry-leading solutions",
    "image": "https://acme.com/og-home.jpg",
    "type": "website"
  },
  "twitter": {
    "card": "summary_large_image",
    "title": "Acme Corp",
    "description": "Business solutions",
    "image": "https://acme.com/twitter-home.jpg"
  },
  "updatedAt": "2024-01-15T10:30:00Z"
}

Get All SEO

Fetch SEO for all pages:

http
GET /api/sites/{siteId}/seo

Response

json
{
  "pages": [
    {
      "pageSlug": "home",
      "title": "Welcome",
      "description": "..."
    },
    {
      "pageSlug": "about",
      "title": "About Us",
      "description": "..."
    }
  ],
  "defaults": {
    "titleSuffix": " | Acme Corp",
    "defaultImage": "https://acme.com/og-default.jpg"
  }
}

Update Page SEO

Update SEO for a page (requires authentication):

http
PUT /api/sites/{siteId}/seo/{pageSlug}

Request Body

json
{
  "title": "New Page Title",
  "description": "Updated description for SEO",
  "keywords": ["keyword1", "keyword2"],
  "openGraph": {
    "title": "OG Title",
    "description": "OG Description",
    "image": "https://example.com/og.jpg"
  }
}

Response

json
{
  "success": true,
  "pageSlug": "home",
  "updatedAt": "2024-01-15T10:30:00Z"
}

Get Site Defaults

Fetch site-wide SEO defaults:

http
GET /api/sites/{siteId}/seo/defaults

Response

json
{
  "titleSuffix": " | Acme Corp",
  "defaultDescription": "Industry-leading business solutions",
  "defaultImage": "https://acme.com/og-default.jpg",
  "twitterHandle": "@acmecorp",
  "locale": "en_US"
}

Update Site Defaults

http
PUT /api/sites/{siteId}/seo/defaults

Data Types

SEO Metadata

typescript
interface SEOMetadata {
  title: string
  description: string
  keywords?: string[]
  canonical?: string
  noIndex?: boolean
  noFollow?: boolean
  openGraph?: OpenGraphData
  twitter?: TwitterCardData
  structuredData?: object
}

Open Graph

typescript
interface OpenGraphData {
  title?: string
  description?: string
  image?: string
  imageWidth?: number
  imageHeight?: number
  type?: 'website' | 'article' | 'product'
  locale?: string
  siteName?: string
}

Twitter Card

typescript
interface TwitterCardData {
  card?: 'summary' | 'summary_large_image' | 'app' | 'player'
  title?: string
  description?: string
  image?: string
  site?: string  // @username
  creator?: string  // @username
}

Integration

VitePress Head Config

typescript
// .vitepress/config.ts
export default defineConfig({
  async transformHead(context) {
    const seo = await fetchSEO(context.pageData.relativePath)
    
    return [
      ['meta', { name: 'description', content: seo.description }],
      ['meta', { property: 'og:title', content: seo.openGraph?.title }],
      ['meta', { property: 'og:description', content: seo.openGraph?.description }],
      ['meta', { property: 'og:image', content: seo.openGraph?.image }],
      ['meta', { name: 'twitter:card', content: seo.twitter?.card }]
    ]
  }
})

Vue Head Component

vue
<!-- components/SEOHead.vue -->
<script setup lang="ts">
import { useHead } from '@vueuse/head'
import { useSEO } from '@/lib/use-seo'

const props = defineProps<{
  pageSlug: string
}>()

const { seo, isLoading } = useSEO(props.pageSlug)

useHead({
  title: () => seo.value?.title,
  meta: [
    { name: 'description', content: () => seo.value?.description },
    { property: 'og:title', content: () => seo.value?.openGraph?.title },
    { property: 'og:description', content: () => seo.value?.openGraph?.description },
    { property: 'og:image', content: () => seo.value?.openGraph?.image }
  ]
})
</script>

Build-Time Generation

typescript
// scripts/generate-seo.ts
import { writeFileSync } from 'fs'

async function generateSEOFiles() {
  const response = await fetch(`${API_URL}/sites/${SITE_ID}/seo`)
  const { pages, defaults } = await response.json()
  
  for (const page of pages) {
    const seoJson = JSON.stringify({
      ...defaults,
      ...page
    }, null, 2)
    
    writeFileSync(`public/seo/${page.pageSlug}.json`, seoJson)
  }
}

Sitemap Generation

Request Sitemap

http
GET /api/sites/{siteId}/sitemap.xml

Returns XML sitemap:

xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://acme.com/</loc>
    <lastmod>2024-01-15</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://acme.com/about</loc>
    <lastmod>2024-01-10</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

Configure Sitemap

http
PUT /api/sites/{siteId}/seo/sitemap-config
json
{
  "excludePages": ["admin", "preview"],
  "defaultChangeFreq": "weekly",
  "priorities": {
    "home": 1.0,
    "about": 0.8,
    "blogs": 0.9,
    "contact": 0.6
  }
}

Structured Data

Article Schema

json
{
  "pageSlug": "blog-post",
  "structuredData": {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Article Title",
    "author": {
      "@type": "Person",
      "name": "Author Name"
    },
    "datePublished": "2024-01-15",
    "image": "https://acme.com/article-image.jpg"
  }
}

Organization Schema

json
{
  "pageSlug": "home",
  "structuredData": {
    "@context": "https://schema.org",
    "@type": "Organization",
    "name": "Acme Corp",
    "url": "https://acme.com",
    "logo": "https://acme.com/logo.png",
    "sameAs": [
      "https://twitter.com/acmecorp",
      "https://linkedin.com/company/acmecorp"
    ]
  }
}

Validation

Character Limits

FieldRecommendedMaximum
Title50-60 chars70 chars
Description150-160 chars320 chars
OG Title60 chars95 chars
OG Description110 chars300 chars

Validation Response

json
{
  "valid": false,
  "warnings": [
    {
      "field": "title",
      "message": "Title is 75 characters, recommended max is 60"
    }
  ],
  "errors": [
    {
      "field": "description",
      "message": "Description is required"
    }
  ]
}

Caching

Cache Headers

http
Cache-Control: public, max-age=3600, stale-while-revalidate=300

Cache Invalidation

SEO cache is invalidated when:

  • Page SEO is updated via portal
  • Site defaults change
  • Manual purge requested

Error Handling

StatusMeaning
400Invalid SEO data
401Not authenticated
403No permission for site
404Page not found
422Validation failed

Best Practices

1. Unique Titles

Each page should have a unique, descriptive title:

Home: "Acme Corp - Business Solutions"
About: "About Us - Acme Corp"
Contact: "Contact Sales - Acme Corp"

2. Compelling Descriptions

Write action-oriented descriptions:

❌ "This is the about page for our company"
✅ "Learn how Acme Corp has helped 500+ businesses grow. Meet our team and discover our story."

3. Consistent Branding

Use site defaults for consistency:

json
{
  "titleSuffix": " | Acme Corp",
  "defaultImage": "https://acme.com/og-default.jpg"
}

4. Image Optimization

  • OG images: 1200x630px
  • Twitter images: 1200x675px
  • Keep file size < 1MB

Next Steps

Duff Cloud Services Documentation