npm package: @oxmo88/next-wordpress-utils
Description: Comprehensive WordPress/Next.js utilities: SEO, sitemaps, video schemas, URL handling, text processing, and multi-locale support
Author: Fan du Real
Latest version: 1.0.0

# @next-wordpress/utils

Comprehensive WordPress/Next.js utilities for SEO, sitemaps, video schemas, URL handling, text processing, and multi-locale support.

## Features

- 🎯 **Configuration-Based**: All utilities accept a site configuration, making them fully reusable across projects
- 🌍 **Multi-Locale Support**: Built-in support for multiple languages and locales
- 📱 **SEO Optimized**: Generate canonical URLs, hreflang tags, OpenGraph metadata, and structured data
- 🎬 **Video Schema**: Automatic video detection and VideoObject structured data generation
- 📄 **Sitemap Utilities**: Priority calculation, URL validation, and sitemap splitting
- 🔤 **Text Processing**: HTML stripping, slug generation, entity decoding, and more
- 📦 **Tree-Shakeable**: Import only what you need with modular exports
- 🔷 **TypeScript**: Full type safety with TypeScript definitions
- 📚 **Dual Format**: Works with both ESM and CommonJS

## Installation

```bash
pnpm add @next-wordpress/utils
```

## Quick Start

### 1. Create Your Site Configuration

First, create a configuration file for your site:

```typescript
// lib/site.config.ts
import { SiteConfig } from '@next-wordpress/utils'

export const siteConfig: SiteConfig = {
  baseUrl: 'https://yourdomain.com',
  siteName: 'Your Site Name',
  defaultLocale: 'en-US',
  locales: {
    'en-US': '',        // Default locale, no prefix
    'es-ES': 'es',      // Spanish: /es/*
    'fr-FR': 'fr',      // French: /fr/*
  },
  pathMappings: {
    article: {
      'en-US': 'news',
      'es-ES': 'noticias',
      'fr-FR': 'actualites',
    },
    page: {
      'en-US': 'pages',
      'es-ES': 'paginas',
      'fr-FR': 'pages',
    },
  },
  metadata: {
    title: 'Your Site Title',
    description: 'Your site description',
    keywords: ['keyword1', 'keyword2'],
    author: 'Your Name',
    twitter: {
      site: '@yourhandle',
      creator: '@yourhandle',
    },
    openGraph: {
      type: 'website',
      locale: 'en_US',
    },
  },
}
```

### 2. Use the Utilities

```typescript
import { getCanonicalUrl, generatePageMetadata } from '@next-wordpress/utils'
import { siteConfig } from '@/lib/site.config'

// Generate canonical URL
const canonicalUrl = getCanonicalUrl('/about', siteConfig)
// Result: "https://yourdomain.com/about"

// Generate page metadata
const metadata = generatePageMetadata(
  {
    title: 'About Us',
    description: 'Learn about our company',
    type: 'website',
  },
  '/about',
  siteConfig
)
```

## Modular Imports

Import only what you need to reduce bundle size:

```typescript
// Import everything
import * as utils from '@next-wordpress/utils'

// Import specific modules
import { generateSeoSlug, stripHtml } from '@next-wordpress/utils/text'
import { getCanonicalUrl } from '@next-wordpress/utils/url'
import { calculateContentPriority } from '@next-wordpress/utils/sitemap'
import { extractVideosFromContent } from '@next-wordpress/utils/video'

// Import SEO convenience module (includes text, url, sitemap utilities)
import {
  generateSeoSlug,
  getCanonicalUrl,
  calculateContentPriority
} from '@next-wordpress/utils/seo'
```

## API Reference

### Configuration Types

#### SiteConfig

Main configuration interface for all utilities.

```typescript
interface SiteConfig {
  baseUrl: string                                    // e.g., "https://yourdomain.com"
  siteName: string                                   // Site name for Open Graph
  defaultLocale: string                              // e.g., "en-US"
  locales: Record<string, string>                    // { "en-US": "", "es-ES": "es" }
  pathMappings?: Record<string, Record<string, string>> // Content type paths per locale
  metadata?: {
    title?: string
    description?: string
    keywords?: string[]
    author?: string
    twitter?: { site?: string; creator?: string }
    openGraph?: { type?: string; locale?: string }
  }
}
```

#### MetadataTemplate

Template for page metadata generation.

```typescript
interface MetadataTemplate {
  title: string
  description: string
  keywords?: string[]
  author?: string
  type?: 'website' | 'article' | 'product'
  image?: string
  publishedTime?: string
  modifiedTime?: string
  section?: string
  tags?: string[]
}
```

### URL Utilities

#### getCanonicalUrl()

Generate canonical URL from path and config.

```typescript
function getCanonicalUrl(path: string = '', config: SiteConfig): string

// Example
const url = getCanonicalUrl('/about', siteConfig)
// Result: "https://yourdomain.com/about"
```

#### getLanguageAlternates()

Generate language alternate URLs for hreflang tags.

```typescript
function getLanguageAlternates(
  basePath: string = '',
  config: SiteConfig
): Record<string, string>

// Example
const alternates = getLanguageAlternates('/about', siteConfig)
// Result: {
//   'en-US': 'https://yourdomain.com/about',
//   'es-ES': 'https://yourdomain.com/es/about',
//   'fr-FR': 'https://yourdomain.com/fr/about',
//   'x-default': 'https://yourdomain.com/about'
// }
```

#### generatePageMetadata()

Generate complete page metadata using config and template.

```typescript
function generatePageMetadata(
  template: MetadataTemplate,
  path: string = '',
  config: SiteConfig
)

// Example
const metadata = generatePageMetadata(
  {
    title: 'About Us',
    description: 'Learn about our company',
    type: 'website',
    image: 'https://yourdomain.com/og-image.jpg',
  },
  '/about',
  siteConfig
)

// Returns Next.js metadata object with:
// - title, description, keywords
// - metadataBase
// - alternates (canonical + hreflang)
// - openGraph
// - twitter card
```

#### getArticleUrl()

Get article URL by slug and locale using config.

```typescript
function getArticleUrl(
  slug: string,
  locale: string,
  contentType: string,
  config: SiteConfig
): string

// Example
const url = getArticleUrl('hello-world-123', 'es-ES', 'article', siteConfig)
// Result: "/es/noticias/hello-world-123"
```

#### getArticleCanonicalUrl()

Build full canonical URL for article.

```typescript
function getArticleCanonicalUrl(
  slug: string,
  locale: string,
  contentType: string,
  config: SiteConfig
): string

// Example
const url = getArticleCanonicalUrl('hello-world-123', 'es-ES', 'article', siteConfig)
// Result: "https://yourdomain.com/es/noticias/hello-world-123"
```

#### parseLocaleFromPathname()

Parse locale from pathname.

```typescript
function parseLocaleFromPathname(
  pathname: string,
  config: SiteConfig
): string

// Example
const locale = parseLocaleFromPathname('/es/noticias/article', siteConfig)
// Result: "es-ES"
```

#### getHomeUrl()

Get home page URL for locale.

```typescript
function getHomeUrl(locale: string, config: SiteConfig): string

// Example
const homeUrl = getHomeUrl('es-ES', siteConfig)
// Result: "/es"
```

#### buildLocalizedPath()

Build localized path.

```typescript
function buildLocalizedPath(
  path: string,
  locale: string,
  config: SiteConfig
): string

// Example
const path = buildLocalizedPath('/about', 'es-ES', siteConfig)
// Result: "/es/about"
```

#### normalizeUrl()

Normalize URL to match config base URL.

```typescript
function normalizeUrl(url: string, config: SiteConfig): string

// Example
const normalized = normalizeUrl('http://olddomain.com/page', siteConfig)
// Result: "https://yourdomain.com/page"
```

#### cn()

Tailwind CSS class merger utility (combines clsx + tailwind-merge).

```typescript
function cn(...inputs: ClassValue[]): string

// Example
const className = cn('px-4', 'py-2', { 'bg-blue-500': isActive })
```

### Text Utilities

#### generateSeoSlug()

Generate SEO-friendly slug from title.

```typescript
function generateSeoSlug(title: string, id: number): string

// Example
const slug = generateSeoSlug('Héllo World! 🎉', 123)
// Result: "hello-world-123"
```

#### extractIdFromSlug()

Extract ID from slug.

```typescript
function extractIdFromSlug(slug: string): number | null

// Example
const id = extractIdFromSlug('hello-world-123')
// Result: 123
```

#### stripHtml()

Strip HTML tags and decode HTML entities.

```typescript
function stripHtml(html: string): string

// Example
const text = stripHtml('<p>Hello &amp; welcome!</p>')
// Result: "Hello & welcome!"
```

#### truncateText()

Truncate text to specified length with ellipsis.

```typescript
function truncateText(
  text: string,
  maxLength: number,
  suffix: string = '...'
): string

// Example
const truncated = truncateText('This is a long text', 10)
// Result: "This is a..."
```

#### extractTextFromHtml()

Extract plain text from HTML.

```typescript
function extractTextFromHtml(html: string): string

// Example
const text = extractTextFromHtml('<p>Hello</p><p>World</p>')
// Result: "Hello World"
```

#### normalizeWhitespace()

Normalize whitespace in text.

```typescript
function normalizeWhitespace(text: string): string

// Example
const normalized = normalizeWhitespace('Hello    \n  World')
// Result: "Hello World"
```

#### toTitleCase()

Convert text to title case.

```typescript
function toTitleCase(text: string): string

// Example
const title = toTitleCase('hello world')
// Result: "Hello World"
```

#### generateExcerpt()

Generate excerpt from text.

```typescript
function generateExcerpt(
  text: string,
  maxLength: number = 160,
  suffix: string = '...'
): string

// Example
const excerpt = generateExcerpt(longText, 100)
```

### Sitemap Utilities

#### calculateContentPriority()

Calculate dynamic priority and changefreq based on content age.

```typescript
function calculateContentPriority(modifiedDate: string | Date): {
  priority: number
  changefreq: ChangeFreq
}

// Example
const { priority, changefreq } = calculateContentPriority('2024-01-15')
// Recent content: { priority: 0.9, changefreq: 'daily' }
// Older content: { priority: 0.5, changefreq: 'monthly' }
```

#### generateSitemapUrl()

Generate a sitemap URL entry.

```typescript
interface SitemapRoute {
  url: string
  lastModified?: string | Date
  changeFrequency?: ChangeFreq
  priority?: number
  alternates?: { languages?: Record<string, string> }
}

function generateSitemapUrl(
  path: string,
  config: SiteConfig,
  modifiedDate?: string | Date,
  overrides?: Partial<SitemapRoute>
): SitemapRoute

// Example
const sitemapEntry = generateSitemapUrl('/about', siteConfig, '2024-01-15')
```

#### sortSitemapUrls()

Sort sitemap URLs by priority (high to low) and lastModified (recent to old).

```typescript
function sortSitemapUrls(urls: SitemapRoute[]): SitemapRoute[]
```

#### validateSitemapUrl()

Validate a sitemap URL entry.

```typescript
function validateSitemapUrl(route: SitemapRoute): boolean
```

#### splitSitemapUrls()

Split sitemap into chunks (Google recommends max 50,000 URLs).

```typescript
function splitSitemapUrls<T>(urls: T[], chunkSize: number = 50000): T[][]

// Example
const chunks = splitSitemapUrls(urls, 50000)
```

### Video Utilities

#### VideoData Interface

```typescript
interface VideoData {
  type: 'youtube' | 'youtube-short' | 'tiktok' | 'instagram-reel' | 'facebook-reel' | 'vimeo' | 'mp4' | 'other'
  embedUrl?: string
  contentUrl?: string
  videoId?: string
  thumbnailUrl?: string
  duration?: string      // ISO 8601 format (e.g., "PT3M10S")
  uploadDate?: string
  name?: string
  description?: string
  isShortForm?: boolean  // True for short-form content (under 60s)
}
```

#### extractVideosFromContent()

Extract all videos from HTML content. Supports:
- YouTube (regular, shorts, youtu.be)
- TikTok
- Instagram Reels
- Facebook Reels
- Vimeo
- Self-hosted MP4

```typescript
function extractVideosFromContent(htmlContent: string): VideoData[]

// Example
const videos = extractVideosFromContent(articleContent)
// Result: [
//   {
//     type: 'youtube',
//     videoId: 'dQw4w9WgXcQ',
//     embedUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
//     contentUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
//     thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg'
//   }
// ]
```

#### extractDurationFromText()

Extract video duration from article text. Looks for patterns like:
- "3 minutes 10 seconds"
- "3:10"
- "3m10s"

```typescript
function extractDurationFromText(text: string): string | undefined

// Example
const duration = extractDurationFromText('Watch this 3:45 video')
// Result: "PT3M45S" (ISO 8601 format)
```

#### generateVideoSchema()

Generate VideoObject structured data for a single video.

```typescript
function generateVideoSchema(
  video: VideoData,
  article: {
    title: string
    excerpt: string
    date: string
    modified: string
    slug: string
    featuredImage: string
  },
  baseUrl: string,
  siteName: string = 'Site'
): any

// Returns Schema.org VideoObject
```

#### generateVideoSchemas()

Main function: Detect videos and generate all VideoObject schemas.

```typescript
function generateVideoSchemas(
  htmlContent: string,
  article: {
    title: string
    excerpt: string
    content: string
    date: string
    modified: string
    slug: string
    featuredImage: string
  },
  baseUrl: string,
  siteName: string = 'Site'
): any[]

// Example
const videoSchemas = generateVideoSchemas(
  article.content,
  article,
  siteConfig.baseUrl,
  siteConfig.siteName
)

// Add to page metadata
export async function generateMetadata() {
  const videoSchemas = generateVideoSchemas(...)

  return {
    // ... other metadata
    other: {
      'application/ld+json': JSON.stringify(videoSchemas)
    }
  }
}
```

#### hasVideos()

Check if content contains any videos.

```typescript
function hasVideos(htmlContent: string): boolean

// Example
if (hasVideos(article.content)) {
  const schemas = generateVideoSchemas(...)
}
```

#### normalizeDateWithTimezone()

Normalize date to ISO 8601 format with timezone.

```typescript
function normalizeDateWithTimezone(dateString: string): string
```

## Usage Examples

### Example 1: Article Page with Full SEO

```typescript
// app/[locale]/news/[slug]/page.tsx
import {
  generatePageMetadata,
  getArticleCanonicalUrl,
  getLanguageAlternates,
  generateVideoSchemas
} from '@next-wordpress/utils'
import { siteConfig } from '@/lib/site.config'

export async function generateMetadata({ params }) {
  const article = await fetchArticle(params.slug)

  // Generate video schemas if article contains videos
  const videoSchemas = generateVideoSchemas(
    article.content,
    article,
    siteConfig.baseUrl,
    siteConfig.siteName
  )

  return generatePageMetadata(
    {
      title: article.title,
      description: article.excerpt,
      type: 'article',
      image: article.featuredImage,
      publishedTime: article.date,
      modifiedTime: article.modified,
      section: article.category,
      tags: article.tags,
    },
    getArticleUrl(params.slug, params.locale, 'article', siteConfig),
    siteConfig
  )
}
```

### Example 2: Dynamic Sitemap

```typescript
// app/sitemap.ts
import { MetadataRoute } from 'next'
import {
  generateSitemapUrl,
  sortSitemapUrls,
  splitSitemapUrls
} from '@next-wordpress/utils'
import { siteConfig } from '@/lib/site.config'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const articles = await fetchAllArticles()

  const articleUrls = articles.map(article =>
    generateSitemapUrl(
      `/news/${article.slug}`,
      siteConfig,
      article.modified
    )
  )

  const staticUrls = [
    generateSitemapUrl('/', siteConfig),
    generateSitemapUrl('/about', siteConfig),
    generateSitemapUrl('/contact', siteConfig),
  ]

  const allUrls = [...staticUrls, ...articleUrls]
  const sortedUrls = sortSitemapUrls(allUrls)

  return sortedUrls
}
```

### Example 3: Article Card with SEO Slug

```typescript
// components/article-card.tsx
import { generateSeoSlug, truncateText, stripHtml } from '@next-wordpress/utils/text'
import { getArticleUrl } from '@next-wordpress/utils/url'
import { siteConfig } from '@/lib/site.config'

export function ArticleCard({ article, locale }) {
  const slug = generateSeoSlug(article.title, article.id)
  const url = getArticleUrl(slug, locale, 'article', siteConfig)
  const excerpt = truncateText(stripHtml(article.excerpt), 150)

  return (
    <a href={url}>
      <h3>{article.title}</h3>
      <p>{excerpt}</p>
    </a>
  )
}
```

### Example 4: Multi-Locale Breadcrumbs

```typescript
// components/breadcrumb.tsx
import { buildLocalizedPath } from '@next-wordpress/utils/url'
import { siteConfig } from '@/lib/site.config'

export function Breadcrumb({ items, locale }) {
  return (
    <nav>
      {items.map((item, index) => {
        const path = buildLocalizedPath(item.href, locale, siteConfig)
        return (
          <a key={index} href={path}>
            {item.label}
          </a>
        )
      })}
    </nav>
  )
}
```

## TypeScript Support

All functions and types are fully typed. Your IDE will provide autocomplete and type checking:

```typescript
import { SiteConfig, MetadataTemplate } from '@next-wordpress/utils'

// Type-safe configuration
const config: SiteConfig = {
  baseUrl: 'https://yourdomain.com',
  // ... TypeScript will validate all properties
}

// Type-safe template
const template: MetadataTemplate = {
  title: 'Page Title',
  description: 'Page description',
  type: 'article', // Only accepts: 'website' | 'article' | 'product'
}
```

## License

MIT

## Contributing

Contributions are welcome! Please ensure all tests pass and add tests for new features.

## Support

For issues and feature requests, please use the GitHub issue tracker.