From d5ea86d56f1d155cbcc1ef1e4c8568844843a7d3 Mon Sep 17 00:00:00 2001 From: toc-assistant Date: Sat, 7 Feb 2026 11:15:10 +0000 Subject: [PATCH] docs: expand cookbook, cheatsheet, and troubleshooting Significantly expanded documentation: - COOKBOOK.md: Complete examples, component reference, production patterns - CHEATSHEET.md: Comprehensive quick reference with all components - TROUBLESHOOTING.md: Full error reference and debugging techniques --- docs/CHEATSHEET.md | 338 ++++++----- docs/COOKBOOK.md | 1188 +++++++++++++++++++++------------------ docs/TROUBLESHOOTING.md | 382 ++++++------- 3 files changed, 1032 insertions(+), 876 deletions(-) diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 572d590..a85ec39 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -4,7 +4,7 @@ One-page reference for common operations. --- -## Setup +## Client Setup ```php use TomGould\AppleNews\Client\AppleNewsClient; @@ -13,9 +13,9 @@ use GuzzleHttp\Psr7\HttpFactory; $factory = new HttpFactory(); $client = AppleNewsClient::create( - keyId: 'KEY_ID', - keySecret: 'SECRET', - httpClient: new Client(), + keyId: 'YOUR_KEY_ID', + keySecret: 'YOUR_BASE64_SECRET', + httpClient: new Client(['timeout' => 30]), requestFactory: $factory, streamFactory: $factory ); @@ -23,130 +23,231 @@ $client = AppleNewsClient::create( --- -## Article Basics +## Article Creation ```php use TomGould\AppleNews\Document\Article; use TomGould\AppleNews\Document\Metadata; -$article = Article::create('id', 'Title', 'en'); +$article = Article::create( + identifier: 'unique-id-123', // URL-safe characters only + title: 'Article Title', + language: 'en', + columns: 7, // Default: 7 + width: 1024 // Default: 1024 +); $article->setMetadata( (new Metadata()) - ->addAuthor('Name') - ->setExcerpt('Summary') + ->addAuthor('Author Name') + ->setExcerpt('Short summary for discovery') ->setDatePublished(new DateTime()) + ->setDateModified(new DateTime()) + ->setCanonicalURL('https://example.com/article') + ->addKeywords(['tag1', 'tag2']) + ->setThumbnailURL('https://example.com/thumb.jpg') + ->setContentGenerationType('none') // 'none' or 'AI' + ->setTransparentToolbar(true) // For immersive headers ); ``` --- -## Components Quick Reference +## Components ### Text + ```php new Title('Title'); -new Heading('Heading', level: 2); // 1-6 -new Body('Paragraph text'); +new Heading('Heading', level: 2); // 1-6 +new Body('Text'); +(new Body('

HTML

'))->setFormat('html'); new Intro('Lead paragraph'); new Caption('Caption text'); new Pullquote('Quote text'); new Quote('Block quote'); -new Byline('By Author'); +new Byline('By Author | Date'); +new Author('Name'); +new Photographer('Name'); +new Illustrator('Name'); ``` ### Media + ```php Photo::fromUrl('https://...'); Photo::fromBundle('image.jpg'); +Photo::fromUrl('...')->setCaption('...')->setAccessibilityCaption('...'); + (new Figure())->setUrl('...')->setCaption('...'); (new Video())->setUrl('...'); (new Audio())->setUrl('...'); -(new Gallery())->addItem(...); +(new Gallery())->addItem($figure1)->addItem($figure2); +ARKit::fromUrl('https://example.com/model.usdz'); ``` -### Embeds +### Social Embeds + ```php new Tweet('https://twitter.com/...'); -new Instagram('https://instagram.com/...'); -new EmbedWebVideo('https://youtube.com/...'); +new Instagram('https://instagram.com/p/...'); +new EmbedWebVideo('https://youtube.com/watch?v=...'); new FacebookPost('https://facebook.com/...'); -new TikTok('https://tiktok.com/...'); +new TikTok('https://tiktok.com/@user/video/...'); ``` ### Structure + ```php -new Container(); // Group components -new Section(); // Article section -new Header(); // Section header -new Divider(); // Visual separator -new Aside('...'); // Sidebar +new Container(); // Group components +new Section(); // Article section +new Header(); // Section header (supports scenes) +new Chapter(); // Chapter division +new Aside('...'); // Sidebar +new Divider(); // Visual separator +new FlexibleSpacer(); // Flexible spacing ``` ### Interactive + ```php -new LinkButton('Text', 'https://...'); -new Map(latitude: 51.5, longitude: -0.1); +new LinkButton('Click Here', 'https://...'); +new ArticleLink::fromArticleId('article-id'); +new Map(latitude: 51.5074, longitude: -0.1278); new DataTable(); +new HTMLTable('...
'); +new BannerAdvertisement(); +new MediumRectangleAdvertisement(); ``` --- -## Styles +## Text Styles ```php use TomGould\AppleNews\Document\Styles\ComponentTextStyle; $style = (new ComponentTextStyle()) - ->setFontName('Helvetica') + ->setFontName('Georgia') ->setFontSize(18) - ->setLineHeight(24) + ->setLineHeight(28) ->setTextColor('#333333') - ->setFontWeight('bold'); + ->setFontWeight('bold') // regular, medium, semibold, bold + ->setFontStyle('italic') // normal, italic + ->setTextAlignment('left') // left, center, right, justified + ->setUnderline(true) + ->setTracking(0) // Letter spacing + ->setParagraphSpacingBefore(12) + ->setParagraphSpacingAfter(12); + +$article->addComponentTextStyle('my-style', $style); +$body->setTextStyle('my-style'); +``` -$article->addComponentTextStyle('styleName', $style); -$component->setTextStyle('styleName'); +### Drop Cap + +```php +$style->setDropCapStyle([ + 'numberOfLines' => 3, + 'numberOfCharacters' => 1, + 'fontName' => 'Georgia-Bold', + 'textColor' => '#007AFF' +]); ``` --- ## Layouts +### Horizontal Stack + +```php +use TomGould\AppleNews\Document\Layouts\HorizontalStackDisplay; + +$container = new Container(); +$container->setContentDisplayObject(new HorizontalStackDisplay()); +$container->addComponent($left); +$container->addComponent($right); +``` + +### Grid + +```php +use TomGould\AppleNews\Document\Layouts\CollectionDisplay; + +$container = new Container(); +$container->setContentDisplayObject( + CollectionDisplay::grid(gutter: 10, minimumWidth: 200) +); +``` + +### Column Layout + ```php -// Inline layout $component->setLayout([ 'columnStart' => 0, 'columnSpan' => 7, - 'margin' => ['top' => 20, 'bottom' => 20] + 'margin' => ['top' => 20, 'bottom' => 20], + 'ignoreDocumentMargin' => true // Full bleed ]); +``` + +--- + +## Dark Mode + +```php +// Text style with dark mode +$style->setConditional([[ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'textColor' => '#FFFFFF' +]]); -// Container display modes -$container->setContentDisplay(['type' => 'horizontal_stack']); -$container->setContentDisplay(['type' => 'collection', 'gutter' => 10]); +// Container background +$container->setStyle([ + 'backgroundColor' => '#FFFFFF', + 'conditional' => [[ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'backgroundColor' => '#1C1C1E' + ]] +]); ``` --- -## Animations +## Animations & Behaviors ```php use TomGould\AppleNews\Document\Animations\*; +use TomGould\AppleNews\Document\Behaviors\*; + +// Animations +$component->setAnimationObject(FadeInAnimation::standard()); +$component->setAnimationObject(MoveInAnimation::fromLeft()); +$component->setAnimationObject(ScaleFadeAnimation::subtle()); + +// Behaviors +$component->setBehaviorObject(Parallax::withFactor(0.5)); +$component->setBehaviorObject(Springy::create()); -$component->setAnimation(FadeInAnimation::standard()); -$component->setAnimation(MoveInAnimation::fromLeft()); -$component->setAnimation(ScaleFadeAnimation::subtle()); +// Header scenes +use TomGould\AppleNews\Document\Scenes\*; +$header->setScene(ParallaxScaleHeader::create()); +$header->setScene(FadingStickyHeader::create()); ``` --- -## Behaviors +## Background Fills ```php -use TomGould\AppleNews\Document\Behaviors\*; +use TomGould\AppleNews\Document\Styles\Fills\*; + +$fill = (new ImageFill('https://...'))->asCover(); +$fill = LinearGradientFill::vertical('#000', '#333'); +$fill = (new VideoFill('https://...'))->setLoop(true); -$component->setBehavior(Parallax::withFactor(0.5)); -$component->setBehavior(Springy::create()); -$component->setBehavior(BackgroundParallax::withFactor(0.8)); +$container->setStyle(['fill' => $fill->jsonSerialize()]); ``` --- @@ -156,27 +257,62 @@ $component->setBehavior(BackgroundParallax::withFactor(0.8)); ```php // Create $response = $client->createArticle($channelId, $article); +$response = $client->createArticle($channelId, $article, $metadata, $assets); $articleId = $response['data']['id']; - -// Create with assets -$client->createArticle($channelId, $article, null, [ - 'bundle://hero.jpg' => '/path/to/hero.jpg' -]); +$revision = $response['data']['revision']; // Read -$data = $client->getArticle($articleId); +$article = $client->getArticle($articleId); -// Update (requires revision) -$revision = $data['data']['revision']; -$client->updateArticle($articleId, $revision, $article); +// Update (requires fresh revision!) +$current = $client->getArticle($articleId); +$client->updateArticle($articleId, $current['data']['revision'], $article); // Delete $client->deleteArticle($articleId); // Search -$results = $client->searchArticlesInChannel($channelId, [ - 'pageSize' => 20 -]); +$results = $client->searchArticlesInChannel($channelId, ['pageSize' => 50]); + +// Channel/Section +$channel = $client->getChannel($channelId); +$quota = $client->getChannelQuota($channelId); +$sections = $client->listSections($channelId); +$client->promoteArticles($sectionId, [$articleId1, $articleId2]); +``` + +--- + +## Request Metadata + +```php +use TomGould\AppleNews\Request\ArticleMetadata; +use TomGould\AppleNews\Enum\MaturityRating; + +$meta = (new ArticleMetadata()) + ->setIsSponsored(false) + ->setIsCandidateToBeFeatured(true) + ->setIsPreview(false) + ->setMaturityRating(MaturityRating::GENERAL) + ->addTargetTerritories(['US', 'GB', 'CA']) + ->addSectionById('section-id'); + +$client->createArticle($channelId, $article, $meta->toArray(), $assets); +``` + +--- + +## Assets + +```php +$article->addComponent(Photo::fromBundle('hero.jpg')); + +$assets = [ + 'bundle://hero.jpg' => '/path/to/hero.jpg', + 'bundle://logo.png' => '/path/to/logo.png', +]; + +$client->createArticle($channelId, $article, null, $assets); ``` --- @@ -190,7 +326,7 @@ use TomGould\AppleNews\Exception\AuthenticationException; try { $client->createArticle($channelId, $article); } catch (AuthenticationException $e) { - // 401/403 + // 401/403 - Check credentials } catch (AppleNewsException $e) { echo $e->getMessage(); echo $e->getErrorCode(); // e.g., 'INVALID_DOCUMENT' @@ -200,87 +336,35 @@ try { --- -## 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'); -``` +// HTML content +(new Body('Bold'))->setFormat('html'); -### Accessibility -```php -$photo->setAccessibilityCaption('Description for screen readers'); -``` +// Full-bleed image +$photo->setLayout(['ignoreDocumentMargin' => true, 'columnSpan' => 7]); -### Full-Bleed Image -```php -$photo->setLayout([ - 'ignoreDocumentMargin' => true, - 'columnStart' => 0, - 'columnSpan' => 7 -]); -``` - ---- - -## Metadata Fields +// Accessibility +$photo->setAccessibilityCaption('Description for screen readers'); -```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'] - ]); +// Debug article JSON +file_put_contents('debug.json', json_encode($article, JSON_PRETTY_PRINT)); ``` --- -## 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']); +## Pre-Publish Checklist -$client->createArticle($channelId, $article, $requestMeta->toArray()); -``` +- [ ] Identifier is URL-safe (a-z, 0-9, _, -) +- [ ] Metadata has excerpt for discovery +- [ ] Images have accessibility captions +- [ ] HTML content has `setFormat('html')` +- [ ] Bundle assets have mapping provided +- [ ] Update uses fresh revision token +- [ ] Sponsored content marked with `setIsSponsored(true)` +- [ ] AI content marked with `setContentGenerationType('AI')` --- -*Tip: Use `json_encode($article, JSON_PRETTY_PRINT)` to debug article structure.* +*Tip: `json_encode($article, JSON_PRETTY_PRINT)` to debug structure* diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md index ef2aaa5..a7c2c64 100644 --- a/docs/COOKBOOK.md +++ b/docs/COOKBOOK.md @@ -1,697 +1,767 @@ # Apple News API Cookbook -Advanced patterns, real-world recipes, and edge case handling for the Apple News API PHP client. +Advanced patterns, real-world recipes, and production-ready examples 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) +1. [Complete Article Examples](#complete-article-examples) +2. [Component Reference](#component-reference) +3. [Layouts & Containers](#layouts--containers) +4. [Styling & Themes](#styling--themes) +5. [Dark Mode Support](#dark-mode-support) +6. [Background Fills](#background-fills) +7. [Animations & Behaviors](#animations--behaviors) +8. [Advertising Integration](#advertising-integration) +9. [API Operations](#api-operations) +10. [Production Patterns](#production-patterns) +11. [Common Pitfalls](#common-pitfalls) +12. [Debugging & Validation](#debugging--validation) --- -## Complex Layouts +## Complete Article Examples -### Horizontal Stack (Side-by-Side Content) +### Basic News Article ```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; + 30]), + requestFactory: $factory, + streamFactory: $factory +); -$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%']) - ); +// 2. Build article with standard 7-column layout +$article = Article::create( + identifier: 'article-' . uniqid(), + title: 'Breaking: Major Discovery Announced', + language: 'en', + columns: 7, + width: 1024 +); -$article->addComponent($container); -``` +// 3. Define reusable text styles +$bodyStyle = (new ComponentTextStyle()) + ->setFontName('Georgia') + ->setFontSize(18) + ->setLineHeight(28) + ->setTextColor('#1a1a1a'); -### Grid Layout (Image Gallery) +$headingStyle = (new ComponentTextStyle()) + ->setFontName('HelveticaNeue-Bold') + ->setFontSize(32) + ->setLineHeight(38) + ->setTextColor('#000000'); + +$article->addComponentTextStyle('body-default', $bodyStyle); +$article->addComponentTextStyle('heading-default', $headingStyle); + +// 4. Set metadata (required for proper indexing) +$article->setMetadata( + (new Metadata()) + ->addAuthor('Jane Smith') + ->addAuthor('John Doe') + ->setExcerpt('Scientists announce a groundbreaking discovery.') + ->setDatePublished(new DateTime()) + ->setDateCreated(new DateTime('-1 hour')) + ->setCanonicalURL('https://example.com/articles/major-discovery') + ->addKeywords(['science', 'discovery', 'breaking news']) + ->setThumbnailURL('https://example.com/images/hero-thumb.jpg') + ->setGeneratorName('MyPublisher') + ->setGeneratorVersion('2.0') + ->setContentGenerationType('none') +); -```php -use TomGould\AppleNews\Document\Layouts\CollectionDisplay; +// 5. Build article content +$article->addComponent(new Title('Breaking: Major Discovery Announced')); -$gallery = new Container(); -$gallery->setContentDisplayObject( - CollectionDisplay::grid(gutter: 10, minimumWidth: 150) +$article->addComponent( + Photo::fromUrl('https://example.com/images/hero.jpg') + ->setCaption('Scientists at the research facility') + ->setAccessibilityCaption('Group of scientists examining equipment') ); -foreach ($images as $url) { - $gallery->addComponent(Photo::fromUrl($url)); -} +$article->addComponent(new Byline('By Jane Smith and John Doe | ' . date('F j, Y'))); +$article->addComponent(new Divider()); -$article->addComponent($gallery); +$article->addComponent( + (new Body('In a stunning announcement today, researchers revealed findings that could revolutionize our understanding.')) + ->setTextStyle('body-default') +); + +$article->addComponent( + (new Heading('The Discovery', level: 2)) + ->setTextStyle('heading-default') +); + +$article->addComponent( + (new Body('

The team spent over three years conducting experiments.

')) + ->setTextStyle('body-default') + ->setFormat('html') +); + +// 6. Publish +$response = $client->createArticle($_ENV['CHANNEL_ID'], $article); +echo "Published: " . $response['data']['id'] . "\n"; +echo "Share URL: " . $response['data']['shareUrl'] . "\n"; ``` -### Two-Column Article Layout +### Photo Gallery Article ```php -$twoColumnLayout = new Container(); -$twoColumnLayout->setContentDisplayObject(new HorizontalStackDisplay()); +use TomGould\AppleNews\Document\Components\{Gallery, Figure, Container}; +use TomGould\AppleNews\Document\Layouts\CollectionDisplay; +use TomGould\AppleNews\Document\Animations\FadeInAnimation; -// Left column - main content (70%) -$mainColumn = new Container(); -$mainColumn - ->setLayout(['minimumWidth' => '70cw']) // 70% column width - ->addComponent(new Body($mainText)); +$article = Article::create('gallery-' . uniqid(), 'Summer Photo Collection', 'en'); -// Right column - sidebar (30%) -$sidebar = new Container(); -$sidebar - ->setLayout(['minimumWidth' => '30cw']) - ->addComponent(new Aside('Related articles...')) - ->addComponent(new LinkButton('Read More', 'https://example.com')); +$article->addComponent(new Title('Summer Photo Collection')); +$article->addComponent(new Body('A visual journey through the best moments of summer.')); -$twoColumnLayout - ->addComponent($mainColumn) - ->addComponent($sidebar); +// Method 1: Built-in Gallery component (horizontal scroll) +$gallery = new Gallery(); +$gallery + ->addItem( + (new Figure()) + ->setUrl('https://example.com/summer/beach.jpg') + ->setCaption('Golden hour at the beach') + ->setAccessibilityCaption('Sun setting over calm ocean waves') + ) + ->addItem( + (new Figure()) + ->setUrl('https://example.com/summer/mountains.jpg') + ->setCaption('Alpine meadows in bloom') + ); -$article->addComponent($twoColumnLayout); -``` +$article->addComponent($gallery); ---- +// Method 2: Grid layout using Container (responsive) +$gridGallery = new Container(); +$gridGallery->setContentDisplayObject( + CollectionDisplay::grid(gutter: 10, minimumWidth: 200) +); + +$images = [ + ['url' => 'https://example.com/img1.jpg', 'caption' => 'Image 1'], + ['url' => 'https://example.com/img2.jpg', 'caption' => 'Image 2'], + ['url' => 'https://example.com/img3.jpg', 'caption' => 'Image 3'], +]; + +foreach ($images as $index => $img) { + $photo = Photo::fromUrl($img['url']) + ->setCaption($img['caption']) + ->setAnimationObject(FadeInAnimation::withDelay($index * 0.1)); + $gridGallery->addComponent($photo); +} -## Nested Containers +$article->addComponent($gridGallery); +``` -### Card Component Pattern +### Social Media Embed Article ```php -use TomGould\AppleNews\Document\Styles\Fills\ColorFill; +use TomGould\AppleNews\Document\Components\{ + Tweet, Instagram, FacebookPost, EmbedWebVideo, TikTok +}; -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; -} +$article = Article::create('social-roundup-' . date('Ymd'), 'What\'s Trending', 'en'); + +$article->addComponent(new Title('What\'s Trending Today')); -// Usage -$cardsContainer = new Container(); -$cardsContainer->setContentDisplayObject( - CollectionDisplay::grid(gutter: 20, minimumWidth: 280) +// Twitter/X embed +$article->addComponent(new Heading('From Twitter', level: 2)); +$article->addComponent(new Tweet('https://twitter.com/NASA/status/1234567890')); + +// Instagram embed +$article->addComponent(new Heading('On Instagram', level: 2)); +$article->addComponent(new Instagram('https://www.instagram.com/p/ABC123xyz/')); + +// YouTube video +$article->addComponent(new Heading('Must-Watch Video', level: 2)); +$article->addComponent( + (new EmbedWebVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')) + ->setCaption('The video everyone is talking about') ); -$cardsContainer - ->addComponent(createCard('Card 1', 'Description...', 'https://...')) - ->addComponent(createCard('Card 2', 'Description...', 'https://...')) - ->addComponent(createCard('Card 3', 'Description...', 'https://...')); +// TikTok embed +$article->addComponent(new TikTok('https://www.tiktok.com/@user/video/1234567890')); -$article->addComponent($cardsContainer); +// Facebook post +$article->addComponent(new FacebookPost('https://www.facebook.com/NASA/posts/123')); ``` -### Collapsible Section +### Long-Form Feature with Immersive Header ```php -$section = new Section(); -$section->setIdentifier('expandable-section'); +use TomGould\AppleNews\Document\Components\{Header, Intro, Pullquote}; +use TomGould\AppleNews\Document\Scenes\ParallaxScaleHeader; +use TomGould\AppleNews\Document\Styles\Fills\ImageFill; +use TomGould\AppleNews\Document\Styles\DocumentStyle; + +$article = Article::create('feature-' . uniqid(), 'The Future of AI', 'en', columns: 12, width: 1280); + +// Document-level styling +$docStyle = new DocumentStyle(); +$docStyle->setBackgroundColor('#FFFFFF'); +$article->setDocumentStyle($docStyle); + +// Metadata with transparent toolbar for immersive effect +$article->setMetadata( + (new Metadata()) + ->addAuthor('Tech Correspondent') + ->setExcerpt('An in-depth look at artificial intelligence.') + ->setDatePublished(new DateTime()) + ->setTransparentToolbar(true) + ->setVideoURL('https://example.com/ai-preview.mp4') +); +// Immersive header with parallax effect $header = new Header(); +$header + ->setScene(ParallaxScaleHeader::create()) + ->setStyle([ + 'fill' => (new ImageFill('https://example.com/ai-hero.jpg')) + ->asCover() + ->setVerticalAlignment('center') + ->jsonSerialize() + ]); + $header->addComponent( - (new Heading('Click to Expand', level: 2)) - ->setAdditions([ - 'type' => 'link', - 'URL' => '#expandable-content' + (new Title('The Future of Artificial Intelligence')) + ->setTextStyle([ + 'textColor' => '#FFFFFF', + 'fontSize' => 48, + 'fontWeight' => 'bold', + 'textShadow' => [ + 'radius' => 15, + 'opacity' => 0.6, + 'color' => '#000000' + ] ]) ); -$content = new Container(); -$content - ->setIdentifier('expandable-content') - ->addComponent(new Body('Hidden content that expands...')); +$article->addComponent($header); + +// Drop cap intro +$article->addComponent( + (new Intro('The machines are learning fast. In laboratories around the world, AI systems are achieving feats that seemed like science fiction.')) + ->setTextStyle([ + 'dropCapStyle' => [ + 'numberOfLines' => 4, + 'numberOfCharacters' => 1, + 'fontName' => 'Georgia-Bold', + 'textColor' => '#007AFF' + ], + 'fontSize' => 20, + 'lineHeight' => 32 + ]) +); -$section - ->addComponent($header) - ->addComponent($content); +// Pull quote +$article->addComponent( + (new Pullquote('"We\'re building new forms of intelligence."')) + ->setTextStyle([ + 'fontName' => 'Georgia-Italic', + 'fontSize' => 28, + 'textColor' => '#007AFF' + ]) +); ``` --- -## Data Tables +## Component Reference + +### Text Components + +| Component | Role | Key Properties | +|-----------|------|----------------| +| `Title` | title | text | +| `Heading` | heading1-6 | text, level (1-6) | +| `Body` | body | text, format (html/none) | +| `Intro` | intro | text, drop cap support | +| `Caption` | caption | text | +| `Pullquote` | pullquote | text | +| `Quote` | quote | text | +| `Byline` | byline | text | +| `Author` | author | text | +| `Photographer` | photographer | text | +| `Illustrator` | illustrator | text | + +### Media Components + +| Component | Factory Methods | +|-----------|-----------------| +| `Photo` | `fromUrl()`, `fromBundle()` | +| `Figure` | `fromUrl()`, `fromBundle()` | +| `Portrait` | - | +| `Logo` | - | +| `Gallery` | `addItem()` | +| `Mosaic` | - | +| `Video` | `fromUrl()`, `fromBundle()` | +| `Audio` | `fromUrl()`, `fromBundle()` | +| `Music` | - | +| `Podcast` | - | +| `ARKit` | `fromUrl()`, `fromBundle()` | + +### Social Embeds + +| Component | Constructor | +|-----------|-------------| +| `Tweet` | `new Tweet($url)` | +| `Instagram` | `new Instagram($url)` | +| `FacebookPost` | `new FacebookPost($url)` | +| `TikTok` | `new TikTok($url)` | +| `EmbedWebVideo` | `new EmbedWebVideo($url)` | + +### Structure Components + +| Component | Key Methods | +|-----------|-------------| +| `Container` | `addComponent()`, `setContentDisplay()` | +| `Section` | `addComponent()` | +| `Header` | `setScene()` | +| `Chapter` | - | +| `Aside` | - | +| `Divider` | - | +| `FlexibleSpacer` | - | + +### Data & Interactive + +| Component | Key Methods | +|-----------|-------------| +| `DataTable` | `setData()`, `setSortBy()` | +| `HTMLTable` | `fromHtml()` | +| `Map` | constructor(lat, lng) | +| `LinkButton` | constructor(text, url) | +| `ArticleLink` | `fromArticleId()` | +| `BannerAdvertisement` | - | +| `MediumRectangleAdvertisement` | - | + +--- + +## Layouts & Containers -### Basic Data Table +### Horizontal Stack ```php -use TomGould\AppleNews\Document\Components\DataTable; +use TomGould\AppleNews\Document\Layouts\HorizontalStackDisplay; -$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], - ], -]); +$container = new Container(); +$container->setContentDisplayObject(new HorizontalStackDisplay()); -$table - ->setShowDescriptorLabels(true) - ->addSortBy('price', 'ascending'); +$container->addComponent( + Photo::fromUrl('https://example.com/sidebar.jpg') + ->setLayout(['minimumWidth' => '40cw']) +); + +$container->addComponent( + (new Body('Article content...')) + ->setLayout(['minimumWidth' => '55cw']) +); -$article->addComponent($table); +$article->addComponent($container); ``` -### Styled Data Table +### Grid Layout ```php -use TomGould\AppleNews\Document\Styles\TableStyle; -use TomGould\AppleNews\Document\Styles\TableCellStyle; -use TomGould\AppleNews\Document\Styles\TableRowStyle; +use TomGould\AppleNews\Document\Layouts\CollectionDisplay; -// 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]) - ); +$grid = new Container(); +$grid->setContentDisplayObject( + CollectionDisplay::grid(gutter: 15, minimumWidth: 250) +); + +for ($i = 0; $i < 6; $i++) { + $grid->addComponent(createCard($i)); +} + +$article->addComponent($grid); +``` + +### Column-Based Layouts + +```php +// Span specific columns (7-column layout) +$component->setLayout([ + 'columnStart' => 1, + 'columnSpan' => 5, +]); -$article->addTableStyle('statsTable', $tableStyle); +// Full-bleed (ignore margins) +$component->setLayout([ + 'ignoreDocumentMargin' => true, + 'columnStart' => 0, + 'columnSpan' => 7, +]); -$table = new DataTable(); -$table - ->setData($tableData) - ->setDataTableStyle('statsTable'); +// With margins +$component->setLayout([ + 'columnStart' => 1, + 'columnSpan' => 5, + 'margin' => ['top' => 20, 'bottom' => 20] +]); ``` --- -## Dark Mode Support +## Styling & Themes -### Automatic Dark Mode Styling +### Text Styles ```php -use TomGould\AppleNews\Document\Conditionals\ConditionalComponentStyle; -use TomGould\AppleNews\Document\Conditionals\ConditionalTextStyle; +use TomGould\AppleNews\Document\Styles\ComponentTextStyle; + +$bodyStyle = (new ComponentTextStyle()) + ->setFontName('Georgia') + ->setFontSize(18) + ->setLineHeight(28) + ->setTextColor('#333333') + ->setFontWeight('regular') + ->setFontStyle('normal') + ->setTextAlignment('left') + ->setTracking(0) + ->setParagraphSpacingBefore(12) + ->setParagraphSpacingAfter(12); + +$article->addComponentTextStyle('body-style', $bodyStyle); +$body = (new Body('Text'))->setTextStyle('body-style'); +``` -// Create styles for both modes -$lightBodyStyle = (new ComponentTextStyle()) - ->setTextColor('#1C1C1E') - ->setFontSize(17); +### Drop Caps -$darkBodyStyle = (new ComponentTextStyle()) - ->setTextColor('#F2F2F7') - ->setFontSize(17) - ->addCondition( - ConditionalTextStyle::darkMode(['textColor' => '#F2F2F7']) - ); +```php +$introStyle = (new ComponentTextStyle()) + ->setFontName('Georgia') + ->setFontSize(20) + ->setDropCapStyle([ + 'numberOfLines' => 3, + 'numberOfCharacters' => 1, + 'fontName' => 'Georgia-Bold', + 'textColor' => '#007AFF', + 'padding' => 5 + ]); +``` -$article->addComponentTextStyle('body', $lightBodyStyle); +### Text Shadows -// Container with dark mode background -$container = new Container(); -$container->setStyle([ - 'backgroundColor' => '#FFFFFF', - 'conditional' => [ +```php +$titleStyle = (new ComponentTextStyle()) + ->setFontName('HelveticaNeue-Bold') + ->setFontSize(48) + ->setTextColor('#FFFFFF') + ->setTextShadow([ + 'radius' => 10, + 'opacity' => 0.5, + 'color' => '#000000', + 'offset' => ['x' => 2, 'y' => 2] + ]); +``` + +--- + +## Dark Mode Support + +### Conditional Text Styles + +```php +$bodyStyle = (new ComponentTextStyle()) + ->setFontName('Georgia') + ->setFontSize(18) + ->setTextColor('#1C1C1E') + ->setConditional([ [ 'conditions' => [['preferredColorScheme' => 'dark']], - 'backgroundColor' => '#1C1C1E' + 'textColor' => '#F2F2F7' ] - ] -]); + ]); ``` -### Complete Dark Mode Article +### Conditional Document Style ```php -// Document-level dark mode -$documentStyle = new DocumentStyle(); -$documentStyle +use TomGould\AppleNews\Document\Styles\DocumentStyle; + +$docStyle = new DocumentStyle(); +$docStyle ->setBackgroundColor('#FFFFFF') ->addConditional([ 'conditions' => [['preferredColorScheme' => 'dark']], 'backgroundColor' => '#000000' ]); -$article->setDocumentStyle($documentStyle); +$article->setDocumentStyle($docStyle); +``` -// Text styles with dark mode variants -$headingStyle = (new ComponentTextStyle()) - ->setTextColor('#000000') - ->setFontWeight('bold') - ->setFontSize(28) - ->setConditional([ +### Conditional Component Styles + +```php +$container = new Container(); +$container->setStyle([ + 'backgroundColor' => '#F5F5F5', + 'conditional' => [ [ 'conditions' => [['preferredColorScheme' => 'dark']], - 'textColor' => '#FFFFFF' + 'backgroundColor' => '#1C1C1E' ] - ]); - -$article->addComponentTextStyle('heading', $headingStyle); + ] +]); ``` --- -## Advertising Integration +## Background Fills -### Banner Ad Placement +### Image Fills ```php -use TomGould\AppleNews\Document\Components\BannerAdvertisement; +use TomGould\AppleNews\Document\Styles\Fills\ImageFill; -// 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)); +$coverFill = (new ImageFill('https://example.com/bg.jpg')) + ->asCover() + ->setVerticalAlignment('center') + ->setHorizontalAlignment('center'); + +$container = new Container(); +$container->setStyle(['fill' => $coverFill->jsonSerialize()]); ``` -### Auto-Placement Configuration +### Gradient Fills ```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] - ] - ] -]); +use TomGould\AppleNews\Document\Styles\Fills\LinearGradientFill; + +$gradient = LinearGradientFill::vertical('#000000', '#333333'); ``` -### Medium Rectangle Ads in Sidebar +### Video Fills ```php -use TomGould\AppleNews\Document\Components\MediumRectangleAdvertisement; +use TomGould\AppleNews\Document\Styles\Fills\VideoFill; -$sidebar = new Container(); -$sidebar - ->setLayout(['minimumWidth' => '300pt']) - ->addComponent(new Heading('Sponsored', level: 4)) - ->addComponent(new MediumRectangleAdvertisement()); +$videoFill = (new VideoFill('https://example.com/bg-video.mp4')) + ->setLoop(true) + ->setStillUrl('https://example.com/poster.jpg'); ``` --- -## Real-World Article Templates +## Animations & Behaviors -### News Article Template +### Animations ```php -function createNewsArticle(array $data): Article -{ - $article = Article::create( - identifier: $data['id'], - title: $data['title'], - language: 'en' - ); +use TomGould\AppleNews\Document\Animations\*; - // 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; -} +$component->setAnimationObject(FadeInAnimation::standard()); +$component->setAnimationObject(MoveInAnimation::fromLeft()); +$component->setAnimationObject(MoveInAnimation::fromRight()); +$component->setAnimationObject(ScaleFadeAnimation::subtle()); +$component->setAnimationObject(FadeInAnimation::withDelay(0.3)); ``` -### Long-Form Feature Template +### Behaviors ```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 - ); +use TomGould\AppleNews\Document\Behaviors\*; - // 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->setBehaviorObject(Parallax::withFactor(0.5)); +$header->setBehaviorObject(BackgroundParallax::withFactor(0.8)); +$component->setBehaviorObject(Springy::create()); +$component->setBehaviorObject(Motion::create()); ``` -### Photo Essay Template +### Scenes (Header Effects) ```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 - ]) - ); - } - } +use TomGould\AppleNews\Document\Scenes\*; - return $article; -} +$header->setScene(FadingStickyHeader::create()); +$header->setScene(ParallaxScaleHeader::create()); ``` --- -## Performance Optimization +## Advertising Integration -### Image Optimization +### Manual Ad Placement ```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); +use TomGould\AppleNews\Document\Components\{ + BannerAdvertisement, MediumRectangleAdvertisement +}; -// 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; -} +$article->addComponent(new Title($title)); +$article->addComponent(new Intro($intro)); +$article->addComponent(new BannerAdvertisement()); +$article->addComponent(new Body($content)); ``` -### Asset Bundling Strategy +### Automatic Ad Placement ```php -// For articles with many images, bundle critical assets -$criticalAssets = [ - 'bundle://hero.jpg' => '/path/to/hero.jpg', - 'bundle://author.jpg' => '/path/to/author.jpg', -]; +use TomGould\AppleNews\Document\Layouts\AdvertisementAutoPlacement; -// 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 +$autoPlacement = (new AdvertisementAutoPlacement()) + ->setEnabled(true) + ->setBannerType('any') + ->setDistanceFromMedia('10vh') + ->setFrequency(5) + ->setLayout(['margin' => ['top' => 20, 'bottom' => 20]]); -$client->createArticle($channelId, $article, null, $criticalAssets); +$article->setAutoplacement([ + 'advertisement' => $autoPlacement->jsonSerialize() +]); ``` --- -## Common Pitfalls +## API Operations -### ❌ Missing Required Properties +### Create Article with Assets ```php -// WRONG: Photo without URL -$photo = new Photo(); // Will fail validation +use TomGould\AppleNews\Request\ArticleMetadata; +use TomGould\AppleNews\Enum\MaturityRating; -// CORRECT: -$photo = Photo::fromUrl('https://example.com/image.jpg'); -``` +$article->addComponent(Photo::fromBundle('hero.jpg')); -### ❌ Invalid Identifier Format +$requestMeta = (new ArticleMetadata()) + ->setIsSponsored(false) + ->setIsCandidateToBeFeatured(true) + ->setMaturityRating(MaturityRating::GENERAL) + ->addTargetTerritories(['US', 'GB', 'CA']) + ->addSectionById('section-123'); -```php -// WRONG: Spaces and special characters -$article = Article::create( - identifier: 'My Article #1', // Invalid! - // ... -); +$assets = [ + 'bundle://hero.jpg' => '/path/to/hero.jpg', +]; -// CORRECT: URL-safe identifiers -$article = Article::create( - identifier: 'my-article-1', - // ... -); +$response = $client->createArticle($channelId, $article, $requestMeta->toArray(), $assets); +$articleId = $response['data']['id']; +$revision = $response['data']['revision']; ``` -### ❌ Forgetting Revision Token on Updates +### Update Article ```php -// WRONG: Update without revision -$client->updateArticle($articleId, null, $article); // Will fail! - -// CORRECT: Get current revision first +// Always get fresh revision $current = $client->getArticle($articleId); $revision = $current['data']['revision']; $client->updateArticle($articleId, $revision, $article); ``` -### ❌ Text Format Mismatch +### Search & Manage ```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'); -``` +// Search +$results = $client->searchArticlesInChannel($channelId, ['pageSize' => 50]); -### ❌ Asset Path Confusion +// Channel operations +$channel = $client->getChannel($channelId); +$quota = $client->getChannelQuota($channelId); +$sections = $client->listSections($channelId); +$client->promoteArticles($sectionId, [$articleId1, $articleId2]); -```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' -]); +// Delete +$client->deleteArticle($articleId); ``` --- -## Debugging Tips +## Production Patterns -### Validate Before Publishing +### CMS Integration ```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/ +class AppleNewsPublisher +{ + public function publishFromCMS(CMSArticle $cmsArticle): string + { + $article = Article::create( + identifier: 'cms-' . $cmsArticle->getId(), + title: $cmsArticle->getTitle(), + language: $cmsArticle->getLocale() + ); + + $metadata = new Metadata(); + $metadata + ->setDatePublished($cmsArticle->getPublishedAt()) + ->setCanonicalURL($cmsArticle->getUrl()) + ->setExcerpt($cmsArticle->getExcerpt()); + + foreach ($cmsArticle->getAuthors() as $author) { + $metadata->addAuthor($author->getName()); + } + + $article->setMetadata($metadata); + + foreach ($cmsArticle->getBlocks() as $block) { + $article->addComponent($this->convertBlock($block)); + } + + $response = $this->client->createArticle($this->channelId, $article); + return $response['data']['id']; + } +} ``` -### Catch Detailed Errors +### Error Handling with Retry ```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)); +function publishWithRetry(AppleNewsClient $client, string $channelId, Article $article, int $maxRetries = 3): array +{ + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + return $client->createArticle($channelId, $article); + } catch (AuthenticationException $e) { + throw $e; // Don't retry auth errors + } catch (AppleNewsException $e) { + if ($e->getCode() === 429) { + sleep(pow(2, $attempt)); + continue; + } + if ($e->getCode() >= 500) { + sleep(1); + continue; + } + throw $e; + } + } + throw new Exception("Max retries exceeded"); } ``` -### Test Locally +### Validation Before Publish ```php -// Create a test harness -function testArticleStructure(Article $article): array +function validateArticle(Article $article): array { $errors = []; $json = $article->jsonSerialize(); - // Check required fields if (empty($json['identifier'])) { $errors[] = 'Missing identifier'; } + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $json['identifier'] ?? '')) { + $errors[] = 'Invalid identifier format'; + } if (empty($json['title'])) { $errors[] = 'Missing title'; } @@ -699,75 +769,101 @@ function testArticleStructure(Article $article): array $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 +## Common Pitfalls + +### ❌ Invalid Identifier + +```php +// WRONG +Article::create('My Article #1', 'Title'); + +// CORRECT +Article::create('my-article-1', 'Title'); +``` -### From Older Versions +### ❌ Missing Revision on Update ```php -// v1.x → v2.x changes: +// WRONG +$client->updateArticle($articleId, null, $article); -// OLD: Array-based layout -$component->setLayout(['columnSpan' => 5]); +// CORRECT +$current = $client->getArticle($articleId); +$client->updateArticle($articleId, $current['data']['revision'], $article); +``` -// NEW: Same, but with type-safe options available -use TomGould\AppleNews\Document\Layouts\ComponentLayout; -$component->setLayoutObject( - (new ComponentLayout())->setColumnSpan(5) -); +### ❌ HTML Without Format -// OLD: Manual JSON for styles -$article->addComponentStyle('myStyle', [ - 'backgroundColor' => '#FFF' -]); +```php +// WRONG - renders as literal text +$body = new Body('

Bold

'); -// NEW: Type-safe style objects -use TomGould\AppleNews\Document\Styles\ComponentStyle; -$article->addComponentStyleObject('myStyle', - (new ComponentStyle())->setBackgroundColor('#FFF') -); +// CORRECT +$body = (new Body('

Bold

'))->setFormat('html'); ``` -### From Raw JSON to This Library +### ❌ Local Path as URL ```php -// If migrating from hand-crafted JSON: +// WRONG +Photo::fromUrl('/var/www/images/hero.jpg'); -// OLD JSON structure: -$json = [ - 'identifier' => '123', - 'title' => 'Test', - 'components' => [ - ['role' => 'body', 'text' => 'Hello'] - ] -]; +// CORRECT +Photo::fromBundle('hero.jpg'); +// + provide mapping: ['bundle://hero.jpg' => '/var/www/images/hero.jpg'] +``` + +### ❌ Missing Accessibility + +```php +// WRONG +Photo::fromUrl('https://example.com/chart.jpg'); + +// CORRECT +Photo::fromUrl('https://example.com/chart.jpg') + ->setAccessibilityCaption('Bar chart showing quarterly revenue growth'); +``` + +--- + +## Debugging & Validation + +### Export JSON + +```php +$json = json_encode($article, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); +file_put_contents('debug-article.json', $json); +``` -// NEW: Use the fluent API -$article = Article::create('123', 'Test', 'en'); -$article->addComponent(new Body('Hello')); +### Detailed Error Logging -// The jsonSerialize() output will match your old structure +```php +try { + $response = $client->createArticle($channelId, $article); +} catch (AppleNewsException $e) { + $log = [ + 'message' => $e->getMessage(), + 'errorCode' => $e->getErrorCode(), + 'keyPath' => $e->getKeyPath(), + ]; + file_put_contents('error.log', print_r($log, true)); + throw $e; +} ``` --- ## 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) +- [Apple News Format Documentation](https://developer.apple.com/documentation/apple_news/apple_news_format) +- [Apple News API Documentation](https://developer.apple.com/documentation/apple_news_api) +- [News Publisher User Guide](https://support.apple.com/guide/news-publisher/welcome/web) --- diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 20a3bd2..a31ed47 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -1,34 +1,36 @@ # Apple News API Troubleshooting Guide -Solutions to common problems when using the Apple News API PHP client. +Comprehensive solutions to common issues. --- ## Authentication Errors -### `AuthenticationException: 401 Unauthorized` +### `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) +**Causes:** Invalid credentials or clock skew. ```php // Verify credentials format -$keyId = 'ABC123'; // Alphanumeric -$secret = 'base64string=='; // Must be base64 +$keyId = 'ABC123DEF456'; // Alphanumeric +$keySecret = 'base64string='; // Must be base64-encoded + +// Check server time sync +echo "Server time: " . date('c'); ``` -### `AuthenticationException: 403 Forbidden` +### `403 Forbidden` -**Cause:** Valid credentials but insufficient permissions. +**Causes:** Valid credentials but wrong channel or 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 +```php +// Verify channel access +try { + $channel = $client->getChannel($channelId); +} catch (AppleNewsException $e) { + echo "Cannot access channel: " . $e->getMessage(); +} +``` --- @@ -36,127 +38,103 @@ $secret = 'base64string=='; // Must be base64 ### `INVALID_DOCUMENT: Invalid identifier` -**Cause:** Article identifier contains invalid characters. - ```php -// ❌ Invalid -$article = Article::create('My Article #1', 'Title'); +// ❌ WRONG +Article::create('My Article #1', 'Title'); +Article::create('article/2024', 'Title'); -// ✅ Valid (URL-safe characters only) -$article = Article::create('my-article-1', 'Title'); -$article = Article::create('article_2024_01_15', 'Title'); +// ✅ CORRECT (URL-safe only) +Article::create('my-article-1', 'Title'); +Article::create('article_2024_01', '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 +// ✅ Correct $photo = Photo::fromUrl('https://example.com/image.jpg'); ``` -### `INVALID_DOCUMENT: Unknown component role` - -**Cause:** Typo in component role or unsupported component. +### `INVALID_DOCUMENT: Invalid text format` ```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' => '...']); +// ❌ HTML without format +$body = new Body('

Bold

'); -// ✅ Use component classes -$article->addComponent(new Body('...')); +// ✅ Specify HTML format +$body = (new Body('

Bold

'))->setFormat('html'); ``` -### `INVALID_DOCUMENT: Invalid text format` - -**Cause:** HTML content without format specification. +### `INVALID_DOCUMENT: Invalid color value` ```php -// ❌ HTML without format -$body = new Body('

Content

'); +// ❌ Invalid +'rgb(255, 0, 0)' // Not supported +'red' // Named colors not supported -// ✅ Specify HTML format -$body = (new Body('

Content

'))->setFormat('html'); +// ✅ Valid +'#FF0000' // 6-digit hex +'#F00' // 3-digit hex +'#FF000080' // 8-digit hex with alpha +``` + +### `INVALID_DOCUMENT: Invalid URL` + +```php +// ❌ Invalid +Photo::fromUrl('/local/path'); +Photo::fromUrl('file:///path'); -// ✅ Or use plain text (default) -$body = new Body('Plain content without HTML tags'); +// ✅ Valid +Photo::fromUrl('https://example.com/image.jpg'); +Photo::fromBundle('image.jpg'); // For local files ``` --- ## Asset Errors -### `INVALID_DOCUMENT: Asset not found` - -**Cause:** Bundle reference doesn't match provided assets. +### `Asset not found: bundle://...` ```php -// ❌ Mismatch -$photo = Photo::fromBundle('hero.jpg'); +// ❌ Key mismatch +$article->addComponent(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'); +// ✅ Keys must match exactly $client->createArticle($channelId, $article, null, [ - 'bundle://hero.jpg' => '/path/to/hero.jpg' // Correct + '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 +**Causes:** URL inaccessible, unsupported format, file too large. ```bash -# Test accessibility +# Verify URL accessibility curl -I https://example.com/image.jpg ``` +**Supported formats:** JPEG, PNG, GIF +**Recommended size:** < 20MB for images + --- -## Update Errors +## Update & Revision 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 +// ✅ Always get fresh revision $current = $client->getArticle($articleId); $freshRevision = $current['data']['revision']; $client->updateArticle($articleId, $freshRevision, $article); @@ -164,110 +142,73 @@ $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 +function articleExists(AppleNewsClient $client, string $articleId): bool +{ + try { + $client->getArticle($articleId); + return true; + } catch (AppleNewsException $e) { + return $e->getCode() !== 404 ? throw $e : false; + } } ``` --- -## Layout Issues +## Layout & Rendering Issues -### Components Not Rendering - -**Cause:** Empty or null components. +### Components Not Appearing ```php // ❌ Empty body is ignored $article->addComponent(new Body('')); -// ✅ Only add components with content -if (!empty($text)) { +// ✅ Only add non-empty content +if (!empty(trim($text))) { $article->addComponent(new Body($text)); } ``` -### Layout Shifts in Apple News - -**Cause:** Missing dimension hints for media. +### Layout Shifts ```php -// ❌ No dimensions = layout shift +// ❌ No dimensions $photo = Photo::fromUrl('https://...'); -// ✅ Provide dimensions or min-height -$photo = Photo::fromUrl('https://...'); +// ✅ Provide dimension hints $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 +// ✅ Register 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' - ]]); +// ❌ Wrong format +$style->setConditional(['darkMode' => true, 'textColor' => '#FFF']); + +// ✅ Correct format +$style->setConditional([[ + 'conditions' => [['preferredColorScheme' => 'dark']], + 'textColor' => '#FFFFFF' +]]); ``` --- -## Network/HTTP Errors - -### `cURL error: Connection timed out` +## Network & HTTP Errors -**Solutions:** -1. Check internet connectivity -2. Verify firewall allows outbound HTTPS -3. Increase timeout in HTTP client +### `Connection timed out` ```php $httpClient = new Client([ @@ -276,103 +217,138 @@ $httpClient = new Client([ ]); ``` -### `cURL error: SSL certificate problem` +### `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 -]); +```bash +sudo update-ca-certificates ``` --- -## Debugging Steps +## Rate Limiting -### 1. Export Article JSON +### `429 Too Many Requests` ```php -$json = json_encode($article, JSON_PRETTY_PRINT); -file_put_contents('debug-article.json', $json); +function publishWithBackoff($client, $channelId, $article, $maxRetries = 5) +{ + for ($attempt = 0; $attempt < $maxRetries; $attempt++) { + try { + return $client->createArticle($channelId, $article); + } catch (AppleNewsException $e) { + if ($e->getCode() !== 429) throw $e; + sleep(pow(2, $attempt)); // 1s, 2s, 4s, 8s, 16s + } + } + throw new Exception("Max retries exceeded"); +} ``` -### 2. Validate with Apple's Tool +--- -Use [Apple News Format Validator](https://developer.apple.com/news-publisher/) to check JSON structure. +## Debugging Techniques -### 3. Enable Detailed Logging +### Export Article JSON ```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')); +$json = json_encode($article, JSON_PRETTY_PRINT); +file_put_contents('debug-article.json', $json); +``` -$stack = HandlerStack::create(); -$stack->push(Middleware::log($logger, new MessageFormatter('{req_body}\n{res_body}'))); +### Validate Before Publishing -$httpClient = new Client(['handler' => $stack]); +```php +function validateArticle(Article $article): array +{ + $errors = []; + $json = $article->jsonSerialize(); + + if (empty($json['identifier'])) { + $errors[] = 'Missing identifier'; + } + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $json['identifier'] ?? '')) { + $errors[] = 'Invalid identifier format'; + } + if (empty($json['title'])) { + $errors[] = 'Missing title'; + } + if (empty($json['components'])) { + $errors[] = 'No components'; + } + + return $errors; +} ``` -### 4. Check Error Details +### Detailed Error Logging ```php try { - $client->createArticle($channelId, $article); + $response = $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()); + $log = [ + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'errorCode' => $e->getErrorCode(), + 'keyPath' => $e->getKeyPath(), + 'article' => json_encode($article, JSON_PRETTY_PRINT), + ]; + file_put_contents('apple-news-error.log', print_r($log, true)); + throw $e; } ``` --- -## Rate Limits +## Environment Issues -### `429 Too Many Requests` +### PHP Version -**Cause:** Exceeded API rate limits. +**Requirement:** PHP 8.1+ + +```bash +php -v # Should show 8.1.x or higher +``` -**Solutions:** -1. Implement exponential backoff -2. Batch operations where possible -3. Cache channel/section data +### Required Extensions ```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; - } +$required = ['json', 'curl', 'openssl', 'mbstring']; +foreach ($required as $ext) { + if (!extension_loaded($ext)) { + echo "Missing: {$ext}\n"; } } ``` +### Memory for Large Assets + +```php +ini_set('memory_limit', '512M'); +``` + +--- + +## Error Code Reference + +| Code | Meaning | Solution | +|------|---------|----------| +| 401 | Unauthorized | Check API credentials | +| 403 | Forbidden | Check channel permissions | +| 404 | Not Found | Verify article/channel ID | +| 409 | Conflict | Get fresh revision token | +| 429 | Rate Limited | Implement backoff | +| 500+ | Server Error | Retry with backoff | + --- ## 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) +1. **[COOKBOOK.md](COOKBOOK.md)** - Advanced patterns +2. **[CHEATSHEET.md](CHEATSHEET.md)** - Quick reference +3. **[Apple Docs](https://developer.apple.com/documentation/apple_news)** - Official reference +4. **[News Previewer](https://developer.apple.com/news-publisher/)** - Validate JSON +5. **[GitHub Issues](https://github.com/tomgould/apple-news-api/issues)** - Report bugs ---