diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md new file mode 100644 index 0000000..572d590 --- /dev/null +++ b/docs/CHEATSHEET.md @@ -0,0 +1,286 @@ +# Apple News API Quick Reference + +One-page reference for common operations. + +--- + +## Setup + +```php +use TomGould\AppleNews\Client\AppleNewsClient; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; + +$factory = new HttpFactory(); +$client = AppleNewsClient::create( + keyId: 'KEY_ID', + keySecret: 'SECRET', + httpClient: new Client(), + requestFactory: $factory, + streamFactory: $factory +); +``` + +--- + +## Article Basics + +```php +use TomGould\AppleNews\Document\Article; +use TomGould\AppleNews\Document\Metadata; + +$article = Article::create('id', 'Title', 'en'); + +$article->setMetadata( + (new Metadata()) + ->addAuthor('Name') + ->setExcerpt('Summary') + ->setDatePublished(new DateTime()) +); +``` + +--- + +## Components Quick Reference + +### Text +```php +new Title('Title'); +new Heading('Heading', level: 2); // 1-6 +new Body('Paragraph text'); +new Intro('Lead paragraph'); +new Caption('Caption text'); +new Pullquote('Quote text'); +new Quote('Block quote'); +new Byline('By Author'); +``` + +### Media +```php +Photo::fromUrl('https://...'); +Photo::fromBundle('image.jpg'); +(new Figure())->setUrl('...')->setCaption('...'); +(new Video())->setUrl('...'); +(new Audio())->setUrl('...'); +(new Gallery())->addItem(...); +``` + +### Embeds +```php +new Tweet('https://twitter.com/...'); +new Instagram('https://instagram.com/...'); +new EmbedWebVideo('https://youtube.com/...'); +new FacebookPost('https://facebook.com/...'); +new TikTok('https://tiktok.com/...'); +``` + +### Structure +```php +new Container(); // Group components +new Section(); // Article section +new Header(); // Section header +new Divider(); // Visual separator +new Aside('...'); // Sidebar +``` + +### Interactive +```php +new LinkButton('Text', 'https://...'); +new Map(latitude: 51.5, longitude: -0.1); +new DataTable(); +``` + +--- + +## Styles + +```php +use TomGould\AppleNews\Document\Styles\ComponentTextStyle; + +$style = (new ComponentTextStyle()) + ->setFontName('Helvetica') + ->setFontSize(18) + ->setLineHeight(24) + ->setTextColor('#333333') + ->setFontWeight('bold'); + +$article->addComponentTextStyle('styleName', $style); +$component->setTextStyle('styleName'); +``` + +--- + +## Layouts + +```php +// Inline layout +$component->setLayout([ + 'columnStart' => 0, + 'columnSpan' => 7, + 'margin' => ['top' => 20, 'bottom' => 20] +]); + +// Container display modes +$container->setContentDisplay(['type' => 'horizontal_stack']); +$container->setContentDisplay(['type' => 'collection', 'gutter' => 10]); +``` + +--- + +## Animations + +```php +use TomGould\AppleNews\Document\Animations\*; + +$component->setAnimation(FadeInAnimation::standard()); +$component->setAnimation(MoveInAnimation::fromLeft()); +$component->setAnimation(ScaleFadeAnimation::subtle()); +``` + +--- + +## Behaviors + +```php +use TomGould\AppleNews\Document\Behaviors\*; + +$component->setBehavior(Parallax::withFactor(0.5)); +$component->setBehavior(Springy::create()); +$component->setBehavior(BackgroundParallax::withFactor(0.8)); +``` + +--- + +## API Operations + +```php +// Create +$response = $client->createArticle($channelId, $article); +$articleId = $response['data']['id']; + +// Create with assets +$client->createArticle($channelId, $article, null, [ + 'bundle://hero.jpg' => '/path/to/hero.jpg' +]); + +// Read +$data = $client->getArticle($articleId); + +// Update (requires revision) +$revision = $data['data']['revision']; +$client->updateArticle($articleId, $revision, $article); + +// Delete +$client->deleteArticle($articleId); + +// Search +$results = $client->searchArticlesInChannel($channelId, [ + 'pageSize' => 20 +]); +``` + +--- + +## Error Handling + +```php +use TomGould\AppleNews\Exception\AppleNewsException; +use TomGould\AppleNews\Exception\AuthenticationException; + +try { + $client->createArticle($channelId, $article); +} catch (AuthenticationException $e) { + // 401/403 +} catch (AppleNewsException $e) { + echo $e->getMessage(); + echo $e->getErrorCode(); // e.g., 'INVALID_DOCUMENT' + echo $e->getKeyPath(); // e.g., 'components[0].text' +} +``` + +--- + +## Dark Mode + +```php +// Conditional style +$style->setConditional([[ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'textColor' => '#FFFFFF' +]]); + +// Component background +$container->setStyle([ + 'backgroundColor' => '#FFFFFF', + 'conditional' => [[ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'backgroundColor' => '#1C1C1E' + ]] +]); +``` + +--- + +## Common Patterns + +### HTML Content +```php +(new Body('

HTML content

')) + ->setFormat('html'); +``` + +### Accessibility +```php +$photo->setAccessibilityCaption('Description for screen readers'); +``` + +### Full-Bleed Image +```php +$photo->setLayout([ + 'ignoreDocumentMargin' => true, + 'columnStart' => 0, + 'columnSpan' => 7 +]); +``` + +--- + +## Metadata Fields + +```php +$metadata = (new Metadata()) + ->addAuthor('Name') + ->setExcerpt('Summary') + ->setDatePublished(new DateTime()) + ->setDateModified(new DateTime()) + ->addKeywords(['tag1', 'tag2']) + ->setContentGenerationType('none') // AI disclosure + ->setCampaignData(['campaign' => 'value']) + ->setVideoUrl('https://...') // Video thumbnail in feeds + ->setTransparentToolbar(true) // For immersive headers + ->setLinks([ + ['URL' => 'https://...', 'type' => 'website'] + ]); +``` + +--- + +## Request Metadata + +```php +use TomGould\AppleNews\Request\ArticleMetadata; +use TomGould\AppleNews\Enum\MaturityRating; + +$requestMeta = (new ArticleMetadata()) + ->setIsSponsored(false) + ->setIsCandidateToBeFeatured(true) + ->setMaturityRating(MaturityRating::GENERAL) + ->addTargetTerritories(['US', 'GB', 'CA']) + ->addSections(['section-id']); + +$client->createArticle($channelId, $article, $requestMeta->toArray()); +``` + +--- + +*Tip: Use `json_encode($article, JSON_PRETTY_PRINT)` to debug article structure.* diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md new file mode 100644 index 0000000..ef2aaa5 --- /dev/null +++ b/docs/COOKBOOK.md @@ -0,0 +1,774 @@ +# Apple News API Cookbook + +Advanced patterns, real-world recipes, and edge case handling for the Apple News API PHP client. + +--- + +## Table of Contents + +1. [Complex Layouts](#complex-layouts) +2. [Nested Containers](#nested-containers) +3. [Data Tables](#data-tables) +4. [Dark Mode Support](#dark-mode-support) +5. [Advertising Integration](#advertising-integration) +6. [Real-World Article Templates](#real-world-article-templates) +7. [Performance Optimization](#performance-optimization) +8. [Common Pitfalls](#common-pitfalls) +9. [Debugging Tips](#debugging-tips) +10. [Migration Guide](#migration-guide) + +--- + +## Complex Layouts + +### Horizontal Stack (Side-by-Side Content) + +```php +use TomGould\AppleNews\Document\Components\Container; +use TomGould\AppleNews\Document\Components\Body; +use TomGould\AppleNews\Document\Components\Photo; +use TomGould\AppleNews\Document\Layouts\HorizontalStackDisplay; + +$container = new Container(); +$container + ->setContentDisplayObject(new HorizontalStackDisplay()) + ->addComponent( + Photo::fromUrl('https://example.com/thumbnail.jpg') + ->setLayout(['minimumWidth' => '30%']) + ) + ->addComponent( + (new Body('Article summary text goes here...')) + ->setLayout(['minimumWidth' => '60%']) + ); + +$article->addComponent($container); +``` + +### Grid Layout (Image Gallery) + +```php +use TomGould\AppleNews\Document\Layouts\CollectionDisplay; + +$gallery = new Container(); +$gallery->setContentDisplayObject( + CollectionDisplay::grid(gutter: 10, minimumWidth: 150) +); + +foreach ($images as $url) { + $gallery->addComponent(Photo::fromUrl($url)); +} + +$article->addComponent($gallery); +``` + +### Two-Column Article Layout + +```php +$twoColumnLayout = new Container(); +$twoColumnLayout->setContentDisplayObject(new HorizontalStackDisplay()); + +// Left column - main content (70%) +$mainColumn = new Container(); +$mainColumn + ->setLayout(['minimumWidth' => '70cw']) // 70% column width + ->addComponent(new Body($mainText)); + +// Right column - sidebar (30%) +$sidebar = new Container(); +$sidebar + ->setLayout(['minimumWidth' => '30cw']) + ->addComponent(new Aside('Related articles...')) + ->addComponent(new LinkButton('Read More', 'https://example.com')); + +$twoColumnLayout + ->addComponent($mainColumn) + ->addComponent($sidebar); + +$article->addComponent($twoColumnLayout); +``` + +--- + +## Nested Containers + +### Card Component Pattern + +```php +use TomGould\AppleNews\Document\Styles\Fills\ColorFill; + +function createCard(string $title, string $body, string $imageUrl): Container +{ + $card = new Container(); + + // Card background + $card->setStyle([ + 'backgroundColor' => '#FFFFFF', + 'border' => [ + 'all' => ['width' => 1, 'color' => '#E0E0E0'] + ], + 'mask' => ['type' => 'corners', 'radius' => 12] + ]); + + $card + ->addComponent(Photo::fromUrl($imageUrl)) + ->addComponent( + (new Heading($title, level: 3)) + ->setLayout(['margin' => ['top' => 15, 'left' => 15, 'right' => 15]]) + ) + ->addComponent( + (new Body($body)) + ->setLayout(['margin' => ['left' => 15, 'right' => 15, 'bottom' => 15]]) + ); + + return $card; +} + +// Usage +$cardsContainer = new Container(); +$cardsContainer->setContentDisplayObject( + CollectionDisplay::grid(gutter: 20, minimumWidth: 280) +); + +$cardsContainer + ->addComponent(createCard('Card 1', 'Description...', 'https://...')) + ->addComponent(createCard('Card 2', 'Description...', 'https://...')) + ->addComponent(createCard('Card 3', 'Description...', 'https://...')); + +$article->addComponent($cardsContainer); +``` + +### Collapsible Section + +```php +$section = new Section(); +$section->setIdentifier('expandable-section'); + +$header = new Header(); +$header->addComponent( + (new Heading('Click to Expand', level: 2)) + ->setAdditions([ + 'type' => 'link', + 'URL' => '#expandable-content' + ]) +); + +$content = new Container(); +$content + ->setIdentifier('expandable-content') + ->addComponent(new Body('Hidden content that expands...')); + +$section + ->addComponent($header) + ->addComponent($content); +``` + +--- + +## Data Tables + +### Basic Data Table + +```php +use TomGould\AppleNews\Document\Components\DataTable; + +$table = new DataTable(); +$table->setData([ + 'descriptors' => [ + ['identifier' => 'name', 'dataType' => 'string', 'label' => 'Product'], + ['identifier' => 'price', 'dataType' => 'number', 'label' => 'Price ($)'], + ['identifier' => 'stock', 'dataType' => 'integer', 'label' => 'In Stock'], + ], + 'records' => [ + ['name' => 'Widget A', 'price' => 19.99, 'stock' => 150], + ['name' => 'Widget B', 'price' => 29.99, 'stock' => 75], + ['name' => 'Widget C', 'price' => 9.99, 'stock' => 300], + ], +]); + +$table + ->setShowDescriptorLabels(true) + ->addSortBy('price', 'ascending'); + +$article->addComponent($table); +``` + +### Styled Data Table + +```php +use TomGould\AppleNews\Document\Styles\TableStyle; +use TomGould\AppleNews\Document\Styles\TableCellStyle; +use TomGould\AppleNews\Document\Styles\TableRowStyle; + +// Define table style +$tableStyle = new TableStyle(); +$tableStyle + ->setHeaderRowStyle( + (new TableRowStyle()) + ->setBackgroundColor('#007AFF') + ->setTextStyle(['textColor' => '#FFFFFF', 'fontWeight' => 'bold']) + ) + ->setRowStyle( + (new TableRowStyle()) + ->setBackgroundColor('#FFFFFF') + ->setDivider(['width' => 1, 'color' => '#E0E0E0']) + ) + ->setCellStyle( + (new TableCellStyle()) + ->setPadding(12) + ->setTextStyle(['fontSize' => 14]) + ); + +$article->addTableStyle('statsTable', $tableStyle); + +$table = new DataTable(); +$table + ->setData($tableData) + ->setDataTableStyle('statsTable'); +``` + +--- + +## Dark Mode Support + +### Automatic Dark Mode Styling + +```php +use TomGould\AppleNews\Document\Conditionals\ConditionalComponentStyle; +use TomGould\AppleNews\Document\Conditionals\ConditionalTextStyle; + +// Create styles for both modes +$lightBodyStyle = (new ComponentTextStyle()) + ->setTextColor('#1C1C1E') + ->setFontSize(17); + +$darkBodyStyle = (new ComponentTextStyle()) + ->setTextColor('#F2F2F7') + ->setFontSize(17) + ->addCondition( + ConditionalTextStyle::darkMode(['textColor' => '#F2F2F7']) + ); + +$article->addComponentTextStyle('body', $lightBodyStyle); + +// Container with dark mode background +$container = new Container(); +$container->setStyle([ + 'backgroundColor' => '#FFFFFF', + 'conditional' => [ + [ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'backgroundColor' => '#1C1C1E' + ] + ] +]); +``` + +### Complete Dark Mode Article + +```php +// Document-level dark mode +$documentStyle = new DocumentStyle(); +$documentStyle + ->setBackgroundColor('#FFFFFF') + ->addConditional([ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'backgroundColor' => '#000000' + ]); + +$article->setDocumentStyle($documentStyle); + +// Text styles with dark mode variants +$headingStyle = (new ComponentTextStyle()) + ->setTextColor('#000000') + ->setFontWeight('bold') + ->setFontSize(28) + ->setConditional([ + [ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'textColor' => '#FFFFFF' + ] + ]); + +$article->addComponentTextStyle('heading', $headingStyle); +``` + +--- + +## Advertising Integration + +### Banner Ad Placement + +```php +use TomGould\AppleNews\Document\Components\BannerAdvertisement; + +// After article intro +$article->addComponent(new Title($title)); +$article->addComponent(new Intro($intro)); +$article->addComponent(new BannerAdvertisement()); // Auto-placed banner +$article->addComponent(new Body($content)); +``` + +### Auto-Placement Configuration + +```php +// Configure automatic ad placement +$article->setAutoplacement([ + 'advertisement' => [ + 'enabled' => true, + 'bannerType' => 'any', + 'distanceFromMedia' => '10vh', // 10% viewport height from media + 'frequency' => 5, // Every 5 components + 'layout' => [ + 'margin' => ['top' => 20, 'bottom' => 20] + ] + ] +]); +``` + +### Medium Rectangle Ads in Sidebar + +```php +use TomGould\AppleNews\Document\Components\MediumRectangleAdvertisement; + +$sidebar = new Container(); +$sidebar + ->setLayout(['minimumWidth' => '300pt']) + ->addComponent(new Heading('Sponsored', level: 4)) + ->addComponent(new MediumRectangleAdvertisement()); +``` + +--- + +## Real-World Article Templates + +### News Article Template + +```php +function createNewsArticle(array $data): Article +{ + $article = Article::create( + identifier: $data['id'], + title: $data['title'], + language: 'en' + ); + + // Metadata + $article->setMetadata( + (new Metadata()) + ->addAuthor($data['author']) + ->setExcerpt($data['excerpt']) + ->setDatePublished(new DateTime($data['published_at'])) + ->setDateModified(new DateTime($data['updated_at'] ?? 'now')) + ->setContentGenerationType('none') + ->addKeywords($data['tags'] ?? []) + ); + + // Hero section + $article->addComponent(new Title($data['title'])); + + if (!empty($data['hero_image'])) { + $article->addComponent( + (new Figure()) + ->setUrl($data['hero_image']) + ->setCaption($data['hero_caption'] ?? '') + ->setAccessibilityCaption($data['hero_alt'] ?? $data['title']) + ); + } + + // Byline + $article->addComponent( + (new Byline(sprintf('By %s | %s', + $data['author'], + (new DateTime($data['published_at']))->format('F j, Y') + ))) + ); + + // Article body (handle paragraphs) + foreach (explode("\n\n", $data['body']) as $paragraph) { + if (trim($paragraph)) { + $article->addComponent(new Body(trim($paragraph))); + } + } + + return $article; +} +``` + +### Long-Form Feature Template + +```php +function createFeatureArticle(array $data): Article +{ + $article = Article::create( + identifier: $data['id'], + title: $data['title'], + language: 'en', + columns: 12, // More columns for complex layouts + width: 1280 + ); + + // Immersive header with parallax + $header = new Header(); + $header + ->setScene(ParallaxScaleHeader::create()) + ->setStyle([ + 'fill' => [ + 'type' => 'image', + 'URL' => $data['hero_image'], + 'fillMode' => 'cover' + ] + ]) + ->addComponent( + (new Title($data['title'])) + ->setTextStyle([ + 'textColor' => '#FFFFFF', + 'fontSize' => 48, + 'textShadow' => [ + 'radius' => 10, + 'opacity' => 0.5, + 'color' => '#000000' + ] + ]) + ); + + $article->addComponent($header); + + // Drop cap intro + $article->addComponent( + (new Intro($data['intro'])) + ->setTextStyle([ + 'dropCapStyle' => [ + 'numberOfLines' => 3, + 'numberOfCharacters' => 1, + 'fontName' => 'Georgia-Bold' + ] + ]) + ); + + // Content sections with pull quotes + foreach ($data['sections'] as $section) { + $article->addComponent(new Heading($section['title'], level: 2)); + + if (!empty($section['pullquote'])) { + $article->addComponent(new Pullquote($section['pullquote'])); + } + + $article->addComponent(new Body($section['content'])); + + if (!empty($section['image'])) { + $article->addComponent( + (new Figure()) + ->setUrl($section['image']) + ->setCaption($section['caption'] ?? '') + ->setAnimation(FadeInAnimation::standard()) + ); + } + } + + return $article; +} +``` + +### Photo Essay Template + +```php +function createPhotoEssay(array $data): Article +{ + $article = Article::create( + identifier: $data['id'], + title: $data['title'], + language: 'en' + ); + + $article->addComponent(new Title($data['title'])); + $article->addComponent(new Intro($data['intro'])); + + foreach ($data['photos'] as $index => $photo) { + // Full-bleed image + $figure = (new Figure()) + ->setUrl($photo['url']) + ->setCaption($photo['caption']) + ->setLayout([ + 'ignoreDocumentMargin' => true, + 'columnStart' => 0, + 'columnSpan' => 7 + ]) + ->setAnimation( + $index % 2 === 0 + ? MoveInAnimation::fromLeft() + : MoveInAnimation::fromRight() + ); + + $article->addComponent($figure); + + // Optional commentary + if (!empty($photo['commentary'])) { + $article->addComponent( + (new Body($photo['commentary'])) + ->setLayout([ + 'columnStart' => 1, + 'columnSpan' => 5 + ]) + ); + } + } + + return $article; +} +``` + +--- + +## Performance Optimization + +### Image Optimization + +```php +// Always specify dimensions when known +$photo = Photo::fromUrl('https://example.com/image.jpg'); +$photo->setLayout([ + 'minimumHeight' => '300pt', // Prevents layout shift +]); + +// Use explicit dimensions for faster rendering +$photo->setExplicitDimensions(width: 1200, height: 800); + +// Prefer WebP for smaller file sizes +// Apple News handles format conversion, but starting with optimized images helps +``` + +### Lazy Component Loading + +```php +// For very long articles, consider chunking +function createLongArticle(array $sections): Article +{ + $article = Article::create(/* ... */); + + foreach ($sections as $index => $section) { + // Add a divider between sections for visual breaks + if ($index > 0) { + $article->addComponent(new Divider()); + } + + // Each section as a container for isolation + $sectionContainer = new Container(); + $sectionContainer->setIdentifier("section-{$index}"); + + // ... add section content + + $article->addComponent($sectionContainer); + } + + return $article; +} +``` + +### Asset Bundling Strategy + +```php +// For articles with many images, bundle critical assets +$criticalAssets = [ + 'bundle://hero.jpg' => '/path/to/hero.jpg', + 'bundle://author.jpg' => '/path/to/author.jpg', +]; + +// Non-critical images can be remote URLs +// Apple News will fetch and cache them +$article->addComponent(Photo::fromBundle('hero.jpg')); // Bundled +$article->addComponent(Photo::fromUrl('https://cdn/1.jpg')); // Remote + +$client->createArticle($channelId, $article, null, $criticalAssets); +``` + +--- + +## Common Pitfalls + +### ❌ Missing Required Properties + +```php +// WRONG: Photo without URL +$photo = new Photo(); // Will fail validation + +// CORRECT: +$photo = Photo::fromUrl('https://example.com/image.jpg'); +``` + +### ❌ Invalid Identifier Format + +```php +// WRONG: Spaces and special characters +$article = Article::create( + identifier: 'My Article #1', // Invalid! + // ... +); + +// CORRECT: URL-safe identifiers +$article = Article::create( + identifier: 'my-article-1', + // ... +); +``` + +### ❌ Forgetting Revision Token on Updates + +```php +// WRONG: Update without revision +$client->updateArticle($articleId, null, $article); // Will fail! + +// CORRECT: Get current revision first +$current = $client->getArticle($articleId); +$revision = $current['data']['revision']; +$client->updateArticle($articleId, $revision, $article); +``` + +### ❌ Text Format Mismatch + +```php +// WRONG: HTML in plain text mode +$body = new Body('

Some bold text

'); +// Will render literally as "

Some bold text

" + +// CORRECT: Set format explicitly +$body = (new Body('

Some bold text

')) + ->setFormat('html'); +``` + +### ❌ Asset Path Confusion + +```php +// WRONG: Local path as URL +$photo = Photo::fromUrl('/var/www/images/hero.jpg'); // Won't work! + +// CORRECT: Use bundle for local files +$photo = Photo::fromBundle('hero.jpg'); +// Then provide the mapping when publishing: +$client->createArticle($channelId, $article, null, [ + 'bundle://hero.jpg' => '/var/www/images/hero.jpg' +]); +``` + +--- + +## Debugging Tips + +### Validate Before Publishing + +```php +// Export to JSON for inspection +$json = json_encode($article, JSON_PRETTY_PRINT); +file_put_contents('debug-article.json', $json); + +// Validate with Apple's format +// https://developer.apple.com/news-publisher/ +``` + +### Catch Detailed Errors + +```php +try { + $response = $client->createArticle($channelId, $article); +} catch (AppleNewsException $e) { + echo "Error: " . $e->getMessage() . "\n"; + echo "Code: " . $e->getErrorCode() . "\n"; + echo "KeyPath: " . $e->getKeyPath() . "\n"; // Shows exactly which field failed + + // Log the full request for debugging + file_put_contents('failed-article.json', json_encode($article, JSON_PRETTY_PRINT)); +} +``` + +### Test Locally + +```php +// Create a test harness +function testArticleStructure(Article $article): array +{ + $errors = []; + $json = $article->jsonSerialize(); + + // Check required fields + if (empty($json['identifier'])) { + $errors[] = 'Missing identifier'; + } + if (empty($json['title'])) { + $errors[] = 'Missing title'; + } + if (empty($json['components'])) { + $errors[] = 'No components'; + } + + // Check component structure + foreach ($json['components'] ?? [] as $i => $component) { + if (empty($component['role'])) { + $errors[] = "Component {$i} missing role"; + } + } + + return $errors; +} +``` + +--- + +## Migration Guide + +### From Older Versions + +```php +// v1.x → v2.x changes: + +// OLD: Array-based layout +$component->setLayout(['columnSpan' => 5]); + +// NEW: Same, but with type-safe options available +use TomGould\AppleNews\Document\Layouts\ComponentLayout; +$component->setLayoutObject( + (new ComponentLayout())->setColumnSpan(5) +); + +// OLD: Manual JSON for styles +$article->addComponentStyle('myStyle', [ + 'backgroundColor' => '#FFF' +]); + +// NEW: Type-safe style objects +use TomGould\AppleNews\Document\Styles\ComponentStyle; +$article->addComponentStyleObject('myStyle', + (new ComponentStyle())->setBackgroundColor('#FFF') +); +``` + +### From Raw JSON to This Library + +```php +// If migrating from hand-crafted JSON: + +// OLD JSON structure: +$json = [ + 'identifier' => '123', + 'title' => 'Test', + 'components' => [ + ['role' => 'body', 'text' => 'Hello'] + ] +]; + +// NEW: Use the fluent API +$article = Article::create('123', 'Test', 'en'); +$article->addComponent(new Body('Hello')); + +// The jsonSerialize() output will match your old structure +``` + +--- + +## Additional Resources + +- [Apple News Format Specification](https://developer.apple.com/documentation/applenewsformat) +- [Apple News Publisher Guide](https://support.apple.com/guide/news-publisher/welcome/web) +- [API Reference](https://developer.apple.com/documentation/applenewsapi) + +--- + +*Last updated: 2026-02-07* diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..20a3bd2 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,379 @@ +# Apple News API Troubleshooting Guide + +Solutions to common problems when using the Apple News API PHP client. + +--- + +## Authentication Errors + +### `AuthenticationException: 401 Unauthorized` + +**Cause:** Invalid API credentials or malformed signature. + +**Solutions:** +1. Verify your Key ID and Secret are correct +2. Ensure the secret is base64-encoded (as provided by Apple) +3. Check server time sync (HMAC signatures are time-sensitive) + +```php +// Verify credentials format +$keyId = 'ABC123'; // Alphanumeric +$secret = 'base64string=='; // Must be base64 +``` + +### `AuthenticationException: 403 Forbidden` + +**Cause:** Valid credentials but insufficient permissions. + +**Solutions:** +1. Verify the channel ID belongs to your account +2. Check API key permissions in Apple News Publisher +3. Ensure the article isn't violating content policies + +--- + +## Document Validation Errors + +### `INVALID_DOCUMENT: Invalid identifier` + +**Cause:** Article identifier contains invalid characters. + +```php +// ❌ Invalid +$article = Article::create('My Article #1', 'Title'); + +// ✅ Valid (URL-safe characters only) +$article = Article::create('my-article-1', 'Title'); +$article = Article::create('article_2024_01_15', 'Title'); +``` + +### `INVALID_DOCUMENT: components[X] missing required field` + +**Cause:** A component is missing required properties. + +```php +// ❌ Photo without URL +$photo = new Photo(); + +// ✅ Photo with URL +$photo = Photo::fromUrl('https://example.com/image.jpg'); +``` + +### `INVALID_DOCUMENT: Unknown component role` + +**Cause:** Typo in component role or unsupported component. + +```php +// Components are created via classes, not arrays +// If you see this error, you may be mixing raw JSON with the library + +// ❌ Raw array with typo +$article->addComponent(['role' => 'boddy', 'text' => '...']); + +// ✅ Use component classes +$article->addComponent(new Body('...')); +``` + +### `INVALID_DOCUMENT: Invalid text format` + +**Cause:** HTML content without format specification. + +```php +// ❌ HTML without format +$body = new Body('

Content

'); + +// ✅ Specify HTML format +$body = (new Body('

Content

'))->setFormat('html'); + +// ✅ Or use plain text (default) +$body = new Body('Plain content without HTML tags'); +``` + +--- + +## Asset Errors + +### `INVALID_DOCUMENT: Asset not found` + +**Cause:** Bundle reference doesn't match provided assets. + +```php +// ❌ Mismatch +$photo = Photo::fromBundle('hero.jpg'); +$client->createArticle($channelId, $article, null, [ + 'bundle://image.jpg' => '/path/to/hero.jpg' // Wrong key! +]); + +// ✅ Keys must match +$photo = Photo::fromBundle('hero.jpg'); +$client->createArticle($channelId, $article, null, [ + 'bundle://hero.jpg' => '/path/to/hero.jpg' // Correct +]); +``` + +### `INVALID_DOCUMENT: Invalid URL format` + +**Cause:** Malformed or inaccessible image URL. + +```php +// ❌ Local path (not accessible to Apple) +Photo::fromUrl('/var/www/images/hero.jpg'); + +// ❌ Internal network +Photo::fromUrl('http://192.168.1.100/image.jpg'); + +// ✅ Public URL +Photo::fromUrl('https://example.com/images/hero.jpg'); + +// ✅ Or use bundle for local files +Photo::fromBundle('hero.jpg'); +``` + +### `ASSET_INGEST_FAILED` + +**Cause:** Apple couldn't download or process the asset. + +**Solutions:** +1. Verify the URL is publicly accessible +2. Check image format (JPEG, PNG, GIF supported) +3. Ensure file isn't too large (< 20MB recommended) +4. Confirm no authentication/CORS blocking + +```bash +# Test accessibility +curl -I https://example.com/image.jpg +``` + +--- + +## Update Errors + +### `CONFLICT: Revision mismatch` + +**Cause:** Article was modified since you retrieved it. + +```php +// ❌ Using stale revision +$client->updateArticle($articleId, $oldRevision, $article); + +// ✅ Get fresh revision before updating +$current = $client->getArticle($articleId); +$freshRevision = $current['data']['revision']; +$client->updateArticle($articleId, $freshRevision, $article); +``` + +### `NOT_FOUND: Article not found` + +**Cause:** Article was deleted or ID is incorrect. + +```php +// Double-check the article ID format +$articleId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // UUID format + +// Verify it exists before updating +try { + $client->getArticle($articleId); +} catch (AppleNewsException $e) { + // Article doesn't exist +} +``` + +--- + +## Layout Issues + +### Components Not Rendering + +**Cause:** Empty or null components. + +```php +// ❌ Empty body is ignored +$article->addComponent(new Body('')); + +// ✅ Only add components with content +if (!empty($text)) { + $article->addComponent(new Body($text)); +} +``` + +### Layout Shifts in Apple News + +**Cause:** Missing dimension hints for media. + +```php +// ❌ No dimensions = layout shift +$photo = Photo::fromUrl('https://...'); + +// ✅ Provide dimensions or min-height +$photo = Photo::fromUrl('https://...'); +$photo->setLayout(['minimumHeight' => '300pt']); +``` + +### Content Cut Off + +**Cause:** Column span issues. + +```php +// ✅ Ensure content spans available columns +$article = Article::create('id', 'title', 'en', columns: 7); + +// Component should fit within 7 columns +$component->setLayout([ + 'columnStart' => 0, + 'columnSpan' => 7 // Max for 7-column layout +]); +``` + +--- + +## Style Issues + +### Styles Not Applied + +**Cause:** Style name mismatch or missing registration. + +```php +// ❌ Using unregistered style +$body->setTextStyle('myStyle'); + +// ✅ Register the style first +$article->addComponentTextStyle('myStyle', $styleObject); +$body->setTextStyle('myStyle'); +``` + +### Dark Mode Not Working + +**Cause:** Missing conditional configuration. + +```php +// ❌ Only light mode defined +$style->setTextColor('#000000'); + +// ✅ Include dark mode conditional +$style + ->setTextColor('#000000') + ->setConditional([[ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'textColor' => '#FFFFFF' + ]]); +``` + +--- + +## Network/HTTP Errors + +### `cURL error: Connection timed out` + +**Solutions:** +1. Check internet connectivity +2. Verify firewall allows outbound HTTPS +3. Increase timeout in HTTP client + +```php +$httpClient = new Client([ + 'timeout' => 60, + 'connect_timeout' => 10, +]); +``` + +### `cURL error: SSL certificate problem` + +**Solutions:** +1. Update CA certificates: `sudo update-ca-certificates` +2. Ensure PHP has valid CA bundle + +```php +$httpClient = new Client([ + 'verify' => '/path/to/cacert.pem', // If custom CA needed +]); +``` + +--- + +## Debugging Steps + +### 1. Export Article JSON + +```php +$json = json_encode($article, JSON_PRETTY_PRINT); +file_put_contents('debug-article.json', $json); +``` + +### 2. Validate with Apple's Tool + +Use [Apple News Format Validator](https://developer.apple.com/news-publisher/) to check JSON structure. + +### 3. Enable Detailed Logging + +```php +use GuzzleHttp\Middleware; +use GuzzleHttp\MessageFormatter; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('apple-news'); +$logger->pushHandler(new StreamHandler('apple-news.log')); + +$stack = HandlerStack::create(); +$stack->push(Middleware::log($logger, new MessageFormatter('{req_body}\n{res_body}'))); + +$httpClient = new Client(['handler' => $stack]); +``` + +### 4. Check Error Details + +```php +try { + $client->createArticle($channelId, $article); +} catch (AppleNewsException $e) { + echo "Message: " . $e->getMessage() . "\n"; + echo "Code: " . $e->getErrorCode() . "\n"; + echo "KeyPath: " . $e->getKeyPath() . "\n"; + + // Log the full response + print_r($e->getResponse()); +} +``` + +--- + +## Rate Limits + +### `429 Too Many Requests` + +**Cause:** Exceeded API rate limits. + +**Solutions:** +1. Implement exponential backoff +2. Batch operations where possible +3. Cache channel/section data + +```php +// Simple retry with backoff +function publishWithRetry($client, $channelId, $article, $maxRetries = 3) { + for ($i = 0; $i < $maxRetries; $i++) { + try { + return $client->createArticle($channelId, $article); + } catch (AppleNewsException $e) { + if ($e->getCode() === 429 && $i < $maxRetries - 1) { + sleep(pow(2, $i)); // 1s, 2s, 4s + continue; + } + throw $e; + } + } +} +``` + +--- + +## Getting Help + +1. Check the [README](../README.md) for basic usage +2. Review the [Cookbook](COOKBOOK.md) for advanced patterns +3. Consult [Apple's documentation](https://developer.apple.com/documentation/applenewsformat) +4. Open an issue on [GitHub](https://github.com/tomgould/apple-news-api/issues) + +--- + +*Last updated: 2026-02-07*