diff --git a/.agents/skills/copywriting/SKILL.md b/.agents/skills/copywriting/SKILL.md new file mode 100644 index 0000000000..ac03fdcc4e --- /dev/null +++ b/.agents/skills/copywriting/SKILL.md @@ -0,0 +1,256 @@ +--- +name: copywriting +description: When the user wants to write, rewrite, or improve marketing copy for any page — including homepage, landing pages, pricing pages, feature pages, about pages, or product pages. Also use when the user says "write copy for," "improve this copy," "rewrite this page," "marketing copy," "headline help," "CTA copy," "value proposition," "tagline," "subheadline," "hero section copy," "above the fold," "this copy is weak," "make this more compelling," or "help me describe my product." Use this whenever someone is working on website text that needs to persuade or convert. For email copy, see emails. For popup copy, see popups. For editing existing copy, see copy-editing. For the offer underneath the copy (bonuses, guarantees, value framing), see offers. +metadata: + version: 2.0.2 +--- + +# Copywriting + +You are an expert conversion copywriter. Your goal is to write marketing copy that is clear, compelling, and drives action. + +## Before Writing + +**Check for product marketing context first:** +If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task. + +Gather this context (ask if not provided): + +### 1. Page Purpose +- What type of page? (homepage, landing page, pricing, feature, about) +- What is the ONE primary action you want visitors to take? + +### 2. Audience +- Who is the ideal customer? +- What problem are they trying to solve? +- What objections or hesitations do they have? +- What language do they use to describe their problem? + +### 3. Product/Offer +- What are you selling or offering? +- What makes it different from alternatives? +- What's the key transformation or outcome? +- Any proof points (numbers, testimonials, case studies)? + +### 4. Context +- Where is traffic coming from? (ads, organic, email) +- What do visitors already know before arriving? + +--- + +## Copywriting Principles + +### Clarity Over Cleverness +If you have to choose between clear and creative, choose clear. Clarity is not just tidier — it converts: clearer positioning and copy is associated with +81% conversions, a 38% shorter sales cycle, 28% lower CAC, and 175% more referrals. When a reader has to decode your line, you've lost them. + +**For message-market fit tools** — the "Now you can" test, the Human Action Model (discomfort → vision → path), the Perception Gap, and the clarity metrics: See [references/copy-frameworks.md](references/copy-frameworks.md#clarity--message-market-fit) + +### Benefits Over Features +Features: What it does. Benefits: What that means for the customer. + +### Specificity Over Vagueness +- Vague: "Save time on your workflow" +- Specific: "Cut your weekly reporting from 4 hours to 15 minutes" + +### Customer Language Over Company Language +Use words your customers use. Mirror voice-of-customer from reviews, interviews, support tickets. + +### One Idea Per Section +Each section should advance one argument. Build a logical flow down the page. + +--- + +## Writing Style Rules + +### Core Principles + +1. **Simple over complex** — "Use" not "utilize," "help" not "facilitate" +2. **Specific over vague** — Avoid "streamline," "optimize," "innovative" +3. **Active over passive** — "We generate reports" not "Reports are generated" +4. **Confident over qualified** — Remove "almost," "very," "really" +5. **Show over tell** — Describe the outcome instead of using adverbs +6. **Honest over sensational** — Fabricated statistics or testimonials erode trust and create legal liability + +### Quick Quality Check + +- Jargon that could confuse outsiders? +- Sentences trying to do too much? +- Passive voice constructions? +- Exclamation points? (remove them) +- Marketing buzzwords without substance? + +For thorough line-by-line review, use the **copy-editing** skill after your draft. + +--- + +## Best Practices + +### Be Direct +Get to the point. Don't bury the value in qualifications. + +❌ Slack lets you share files instantly, from documents to images, directly in your conversations + +✅ Need to share a screenshot? Send as many documents, images, and audio files as your heart desires. + +### Use Rhetorical Questions +Questions engage readers and make them think about their own situation. +- "Hate returning stuff to Amazon?" +- "Tired of chasing approvals?" + +### Use Analogies When Helpful +Analogies make abstract concepts concrete and memorable. + +### Pepper in Humor (When Appropriate) +Puns and wit make copy memorable—but only if it fits the brand and doesn't undermine clarity. + +--- + +## Page Structure Framework + +### Above the Fold + +**Headline** +- Your single most important message +- Communicate core value proposition +- Specific > generic + +**Example formulas:** +- "{Achieve outcome} without {pain point}" +- "The {category} for {audience}" +- "Never {unpleasant event} again" +- "{Question highlighting main pain point}" + +**For comprehensive headline formulas**: See [references/copy-frameworks.md](references/copy-frameworks.md) + +**Structure the hero as a transformation** — current discomfort → better vision → path to action (the Human Action Model), then run every headline through the "Now you can" test. See [references/copy-frameworks.md](references/copy-frameworks.md#clarity--message-market-fit) + +**For natural transition phrases**: See [references/natural-transitions.md](references/natural-transitions.md) + +**Subheadline** +- Expands on headline +- Adds specificity +- 1-2 sentences max + +**Primary CTA** +- Action-oriented button text +- Communicate what they get: "Start Free Trial" > "Sign Up" + +### Core Sections + +| Section | Purpose | +|---------|---------| +| Social Proof | Build credibility (logos, stats, testimonials) | +| Problem/Pain | Show you understand their situation | +| Solution/Benefits | Connect to outcomes (3-5 key benefits) | +| How It Works | Reduce perceived complexity (3-4 steps) | +| Objection Handling | FAQ, comparisons, guarantees | +| Final CTA | Recap value, repeat CTA, risk reversal | + +**For detailed section types and page templates**: See [references/copy-frameworks.md](references/copy-frameworks.md) + +--- + +## CTA Copy Guidelines + +**Weak CTAs (avoid):** +- Submit, Sign Up, Learn More, Click Here, Get Started + +**Strong CTAs (use):** +- Start Free Trial +- Get [Specific Thing] +- See [Product] in Action +- Create Your First [Thing] +- Download the Guide + +**Formula:** [Action Verb] + [What They Get] + [Qualifier if needed] + +Examples: +- "Start My Free Trial" +- "Get the Complete Checklist" +- "See Pricing for My Team" + +--- + +## Page-Specific Guidance + +### Homepage +- Serve multiple audiences without being generic +- Lead with broadest value proposition +- Provide clear paths for different visitor intents + +### Landing Page +- Single message, single CTA +- Match headline to ad/traffic source +- Complete argument on one page + +### Pricing Page +- Help visitors choose the right plan +- Address "which is right for me?" anxiety +- Make recommended plan obvious + +### Feature Page +- Connect feature → benefit → outcome +- Show use cases and examples +- Clear path to try or buy + +### About Page +- Tell the story of why you exist +- Connect mission to customer benefit +- Still include a CTA + +--- + +## Voice and Tone + +Before writing, establish: + +**Formality level:** +- Casual/conversational +- Professional but friendly +- Formal/enterprise + +**Brand personality:** +- Playful or serious? +- Bold or understated? +- Technical or accessible? + +Maintain consistency, but adjust intensity: +- Headlines can be bolder +- Body copy should be clearer +- CTAs should be action-oriented + +--- + +## Output Format + +When writing copy, provide: + +### Page Copy +Organized by section: +- Headline, Subheadline, CTA +- Section headers and body copy +- Secondary CTAs + +### Annotations +For key elements, explain: +- Why you made this choice +- What principle it applies + +### Alternatives +For headlines and CTAs, provide 2-3 options: +- Option A: [copy] — [rationale] +- Option B: [copy] — [rationale] + +### Meta Content (if relevant) +- Page title (for SEO) +- Meta description + +--- + +## Related Skills + +- **copy-editing**: For polishing existing copy (use after your draft) +- **cro**: If page structure/strategy needs work, not just copy +- **emails**: For email copywriting +- **popups**: For popup and modal copy +- **ab-testing**: To test copy variations \ No newline at end of file diff --git a/.agents/skills/copywriting/evals/evals.json b/.agents/skills/copywriting/evals/evals.json new file mode 100644 index 0000000000..95a8628257 --- /dev/null +++ b/.agents/skills/copywriting/evals/evals.json @@ -0,0 +1,126 @@ +{ + "skill_name": "copywriting", + "evals": [ + { + "id": 1, + "prompt": "Write homepage copy for a SaaS tool that automates employee onboarding. Target audience is HR directors at mid-size companies (200-2000 employees). Main differentiator is that it integrates with all major HRIS systems and cuts onboarding time from 2 weeks to 2 days.", + "expected_output": "Should check for product-marketing.md first. Should write full page copy organized by section: Headline, Subheadline, CTA (above the fold), then Social Proof, Problem/Pain, Solution/Benefits, How It Works, Objection Handling, and Final CTA. Should follow copywriting principles: clarity over cleverness, benefits over features, specificity (use the '2 weeks to 2 days' stat), customer language. Headline should communicate core value proposition. CTAs should be action-oriented ('Start Free Trial' not 'Submit'). Should provide 2-3 headline alternatives with rationale. Should include annotations explaining key copy choices. Should include meta content (SEO page title and meta description).", + "assertions": [ + "Checks for product-marketing.md", + "Writes full page copy organized by section", + "Includes Headline, Subheadline, and CTA above the fold", + "Includes Social Proof, Problem/Pain, Solution/Benefits, How It Works sections", + "Uses the '2 weeks to 2 days' specificity in copy", + "CTAs are action-oriented, not generic", + "Provides 2-3 headline alternatives with rationale", + "Includes annotations explaining copy choices", + "Includes meta content (SEO title and meta description)" + ], + "files": [] + }, + { + "id": 2, + "prompt": "Rewrite this headline: 'An Innovative AI-Powered Platform for Streamlined Business Operations' — it's for a B2B SaaS tool that helps small businesses manage invoicing and payments.", + "expected_output": "Should identify problems: jargon ('innovative,' 'AI-powered,' 'streamlined,' 'business operations'), too vague, company language not customer language. Should apply copywriting principles — specificity over vagueness, benefits over features, customer language over company language. Should provide 2-3 alternative headlines using formulas like '{Achieve outcome} without {pain point}' or 'The {category} for {audience}'. Each alternative should include rationale. Should also suggest a subheadline that adds specificity.", + "assertions": [ + "Identifies jargon in original headline", + "Identifies vagueness as a problem", + "Identifies company language vs customer language issue", + "Provides 2-3 alternative headlines", + "Alternatives use headline formulas from the skill", + "Each alternative includes rationale", + "Suggests a subheadline" + ], + "files": [] + }, + { + "id": 3, + "prompt": "i need copy for my pricing page. we have three plans: starter ($29/mo), pro ($79/mo), business ($199/mo). it's a social media scheduling tool for marketers", + "expected_output": "Should trigger on the casual phrasing. Should ask or infer audience context. Should apply Pricing Page guidance: help visitors choose the right plan, address 'which is right for me?' anxiety, make recommended plan obvious. Should write plan names, descriptions, feature lists with benefit-oriented copy (not just feature names). Should include a page headline that addresses the pricing decision. CTAs should be specific per plan. Should handle objection handling (FAQ copy). Should provide alternatives for key elements.", + "assertions": [ + "Triggers on casual phrasing", + "Applies Pricing Page guidance", + "Addresses 'which plan is right for me' anxiety", + "Makes recommended plan obvious", + "Writes benefit-oriented feature copy, not just feature names", + "Includes page headline", + "CTAs are specific per plan", + "Includes FAQ or objection handling copy", + "Provides alternatives for key elements" + ], + "files": [] + }, + { + "id": 4, + "prompt": "Write copy for our About page. We're a 3-person startup that built a developer tool for database migrations. Founded because we kept losing data during migrations at our last jobs. Tone should be professional but human.", + "expected_output": "Should apply About Page guidance: tell the story of why you exist, connect mission to customer benefit, still include a CTA. Should adapt voice and tone to 'professional but human' as specified. Should tell the founder origin story authentically. Should connect the personal pain to the customer's pain. Should include a CTA even on the About page. Copy should follow style rules: active voice, confident, specific. Should NOT be overly corporate or generic.", + "assertions": [ + "Applies About Page guidance", + "Tells the story of why the company exists", + "Connects mission to customer benefit", + "Includes a CTA", + "Adapts tone to professional but human", + "Uses the founder origin story", + "Connects personal pain to customer pain", + "Uses active voice", + "Avoids corporate jargon" + ], + "files": [] + }, + { + "id": 5, + "prompt": "Can you improve this CTA? We currently have 'Learn More' on our feature page for our analytics dashboard product.", + "expected_output": "Should immediately identify 'Learn More' as a weak CTA per the guidelines. Should apply the CTA formula: [Action Verb] + [What They Get] + [Qualifier]. Should provide 2-3 strong alternatives like 'See the Dashboard in Action,' 'Start Your Free Trial,' or 'Explore Analytics Features.' Each alternative should include rationale and context for when it works best. Should also consider CTA hierarchy — whether this is a primary or secondary CTA, and suggest complementary CTAs if relevant.", + "assertions": [ + "Identifies 'Learn More' as a weak CTA", + "Applies the CTA formula from the skill", + "Provides 2-3 strong alternatives", + "Each alternative includes rationale", + "Considers CTA hierarchy (primary vs secondary)", + "Suggests complementary CTAs" + ], + "files": [] + }, + { + "id": 6, + "prompt": "Write me a 5-email welcome sequence for new trial users of our project management tool.", + "expected_output": "Should recognize this is an email copywriting task, not page copywriting. Should defer to or cross-reference the emails skill, which specifically handles email sequences, drip campaigns, and lifecycle emails. May provide brief general guidance but should make clear that emails is the right skill for this task.", + "assertions": [ + "Recognizes this as email sequence work", + "References or defers to emails skill", + "Does not attempt to write a full email sequence using page copywriting patterns" + ], + "files": [] + }, + { + "id": 7, + "prompt": "Review this copy and tell me what's wrong: 'We are extremely excited to announce our revolutionary, cutting-edge platform that will totally transform how businesses optimize their workflows! Sign up now!!'", + "expected_output": "Should apply the Quick Quality Check. Should identify: exclamation points (remove them), marketing buzzwords without substance ('revolutionary,' 'cutting-edge,' 'totally transform,' 'optimize'), passive/weak constructions ('we are excited to announce'), vague language ('workflows'). Should apply writing style rules: simple over complex, specific over vague, confident over qualified, show over tell. Should rewrite the copy following these principles. Should provide 2-3 alternatives.", + "assertions": [ + "Identifies exclamation point overuse", + "Identifies marketing buzzwords without substance", + "Identifies vague language", + "Applies writing style rules", + "Rewrites the copy following principles", + "Provides alternatives", + "Result is specific, clear, and jargon-free" + ], + "files": [] + }, + { + "id": 8, + "prompt": "Write above-the-fold copy for a calendar scheduling tool. Our differentiator is that the recipient gets to overlay their own calendar on the invite, so picking a time feels fair to both people instead of one-sided. Same product needs to work for indie founders AND for enterprise ops teams.", + "expected_output": "Should structure the hero using the Human Action Model transformation spine: current discomfort (the awkwardness of sending a one-sided scheduling link), better vision (scheduling that feels considerate to both people), and path to action (the overlay mechanic + a specific CTA). Should run headline candidates through the 'Now you can' test and prefer lines that are compelling and true. Should reference or echo the SavvyCal awkward-link insight ('You shouldn't have to feel awkward sending out your scheduling link') as the message-market-fit model. Should surface the Perception Gap: the same benefit reads differently by risk tolerance, so it should provide a value-prop swap — a founder-facing framing (speed, no sales calls) and an enterprise-facing framing (security, SLAs, reliability) rather than one averaged, mushy message. Should favor clarity over cleverness and provide 2-3 headline alternatives with rationale.", + "assertions": [ + "Structures the hero as discomfort -> vision -> path (Human Action Model)", + "Applies the 'Now you can' test to headline candidates", + "References the SavvyCal awkward-link message-market-fit insight", + "Surfaces the Perception Gap between segments", + "Provides a value-prop swap: founder framing vs enterprise framing", + "Favors clarity over cleverness", + "Provides 2-3 headline alternatives with rationale" + ], + "files": [] + } + ] + } \ No newline at end of file diff --git a/.agents/skills/copywriting/references/copy-frameworks.md b/.agents/skills/copywriting/references/copy-frameworks.md new file mode 100644 index 0000000000..53a3ddac6f --- /dev/null +++ b/.agents/skills/copywriting/references/copy-frameworks.md @@ -0,0 +1,433 @@ +# Copy Frameworks Reference + +Headline formulas, page section types, and structural templates. + +## Contents +- Headline Formulas (outcome-focused, problem-focused, audience-focused, differentiation-focused, proof-focused, additional formulas) +- Landing Page Section Types (core sections, supporting sections) +- Page Structure Templates (feature-heavy page, varied engaging page, compact landing page, enterprise/B2B landing page, product launch page) +- Section Writing Tips (problem section, benefits section, how it works section, testimonial selection) +- Clarity & Message-Market Fit (the "Now you can" test, Human Action Model, the Perception Gap, the SavvyCal case, clarity metrics) + +## Headline Formulas + +### Outcome-Focused + +**{Achieve desirable outcome} without {pain point}** +> Understand how users are really experiencing your site without drowning in numbers + +**{Achieve desirable outcome} by {how product makes it possible}** +> Generate more leads by seeing which companies visit your site + +**Turn {input} into {outcome}** +> Turn your hard-earned sales into repeat customers + +**[Achieve outcome] in [timeframe]** +> Get your tax refund in 10 days + +--- + +### Problem-Focused + +**Never {unpleasant event} again** +> Never miss a sales opportunity again + +**{Question highlighting the main pain point}** +> Hate returning stuff to Amazon? + +**Stop [pain]. Start [pleasure].** +> Stop chasing invoices. Start getting paid on time. + +--- + +### Audience-Focused + +**{Key feature/product type} for {target audience}** +> Advanced analytics for Shopify e-commerce + +**{Key feature/product type} for {target audience} to {what it's used for}** +> An online whiteboard for teams to ideate and brainstorm together + +**You don't have to {skills or resources} to {achieve desirable outcome}** +> With Ahrefs, you don't have to be an SEO pro to rank higher and get more traffic + +--- + +### Differentiation-Focused + +**The {opposite of usual process} way to {achieve desirable outcome}** +> The easiest way to turn your passion into income + +**The [category] that [key differentiator]** +> The CRM that updates itself + +--- + +### Proof-Focused + +**[Number] [people] use [product] to [outcome]** +> 50,000 marketers use Drip to send better emails + +**{Key benefit of your product}** +> Sound clear in online meetings + +--- + +### Additional Formulas + +**The simple way to {outcome}** +> The simple way to track your time + +**Finally, {category} that {benefit}** +> Finally, accounting software that doesn't suck + +**{Outcome} without {common pain}** +> Build your website without writing code + +**Get {benefit} from your {thing}** +> Get more revenue from your existing traffic + +**{Action verb} your {thing} like {admirable example}** +> Market your SaaS like a Fortune 500 + +**What if you could {desirable outcome}?** +> What if you could close deals 30% faster? + +**Everything you need to {outcome}** +> Everything you need to launch your course + +**The {adjective} {category} built for {audience}** +> The lightweight CRM built for startups + +--- + +## Landing Page Section Types + +### Core Sections + +**Hero (Above the Fold)** +- Headline + subheadline +- Primary CTA +- Supporting visual (product screenshot, hero image) +- Optional: Social proof bar + +**Social Proof Bar** +- Customer logos (recognizable > many) +- Key metric ("10,000+ teams") +- Star rating with review count +- Short testimonial snippet + +**Problem/Pain Section** +- Articulate their problem better than they can +- Create recognition ("that's exactly my situation") +- Hint at cost of not solving it + +**Solution/Benefits Section** +- Bridge from problem to your solution +- 3-5 key benefits (not 10) +- Each: headline + explanation + proof if available + +**How It Works** +- 3-4 numbered steps +- Reduces perceived complexity +- Each step: action + outcome + +**Final CTA Section** +- Recap value proposition +- Repeat primary CTA +- Risk reversal (guarantee, free trial) + +--- + +### Supporting Sections + +**Testimonials** +- Full quotes with names, roles, companies +- Photos when possible +- Specific results over vague praise +- Formats: quote cards, video, tweet embeds + +**Case Studies** +- Problem → Solution → Results +- Specific metrics and outcomes +- Customer name and context +- Can be snippets with "Read more" links + +**Use Cases** +- Different ways product is used +- Helps visitors self-identify +- "For marketers who need X" format + +**Personas / "Built For" Sections** +- Explicitly call out target audience +- "Perfect for [role]" blocks +- Addresses "Is this for me?" question + +**FAQ Section** +- Address common objections +- Good for SEO +- Reduces support burden +- 5-10 most common questions + +**Comparison Section** +- vs. competitors (name them or don't) +- vs. status quo (spreadsheets, manual processes) +- Tables or side-by-side format + +**Integrations / Partners** +- Logos of tools you connect with +- "Works with your stack" messaging +- Builds credibility + +**Founder Story / Manifesto** +- Why you built this +- What you believe +- Emotional connection +- Differentiates from faceless competitors + +**Demo / Product Tour** +- Interactive demos +- Video walkthroughs +- GIF previews +- Shows product in action + +**Pricing Preview** +- Teaser even on non-pricing pages +- Starting price or "from $X/mo" +- Moves decision-makers forward + +**Guarantee / Risk Reversal** +- Money-back guarantee +- Free trial terms +- "Cancel anytime" +- Reduces friction + +**Stats Section** +- Key metrics that build credibility +- "10,000+ customers" +- "4.9/5 rating" +- "$2M saved for customers" + +--- + +## Page Structure Templates + +### Feature-Heavy Page (Weak) + +``` +1. Hero +2. Feature 1 +3. Feature 2 +4. Feature 3 +5. Feature 4 +6. CTA +``` + +This is a list, not a persuasive narrative. + +--- + +### Varied, Engaging Page (Strong) + +``` +1. Hero with clear value prop +2. Social proof bar (logos or stats) +3. Problem/pain section +4. How it works (3 steps) +5. Key benefits (2-3, not 10) +6. Testimonial +7. Use cases or personas +8. Comparison to alternatives +9. Case study snippet +10. FAQ +11. Final CTA with guarantee +``` + +This tells a story and addresses objections. + +--- + +### Compact Landing Page + +``` +1. Hero (headline, subhead, CTA, image) +2. Social proof bar +3. 3 key benefits with icons +4. Testimonial +5. How it works (3 steps) +6. Final CTA with guarantee +``` + +Good for ad landing pages where brevity matters. + +--- + +### Enterprise/B2B Landing Page + +``` +1. Hero (outcome-focused headline) +2. Logo bar (recognizable companies) +3. Problem section (business pain) +4. Solution overview +5. Use cases by role/department +6. Security/compliance section +7. Integration logos +8. Case study with metrics +9. ROI/value section +10. Contact/demo CTA +``` + +Addresses enterprise buyer concerns. + +--- + +### Product Launch Page + +``` +1. Hero with launch announcement +2. Video demo or walkthrough +3. Feature highlights (3-5) +4. Before/after comparison +5. Early testimonials +6. Launch pricing or early access offer +7. CTA with urgency +``` + +Good for ProductHunt, launches, or announcements. + +--- + +## Section Writing Tips + +### Problem Section + +Start with phrases like: +- "You know the feeling..." +- "If you're like most [role]..." +- "Every day, [audience] struggles with..." +- "We've all been there..." + +Then describe: +- The specific frustration +- The time/money wasted +- The impact on their work/life + +### Benefits Section + +For each benefit, include: +- **Headline**: The outcome they get +- **Body**: How it works (1-2 sentences) +- **Proof**: Number, testimonial, or example (optional) + +### How It Works Section + +Each step should be: +- **Numbered**: Creates sense of progress +- **Simple verb**: "Connect," "Set up," "Get" +- **Outcome-oriented**: What they get from this step + +Example: +1. Connect your tools (takes 2 minutes) +2. Set your preferences +3. Get automated reports every Monday + +### Testimonial Selection + +Best testimonials include: +- Specific results ("increased conversions by 32%") +- Before/after context ("We used to spend hours...") +- Role + company for credibility +- Something quotable and specific + +Avoid testimonials that just say: +- "Great product!" +- "Love it!" +- "Easy to use!" + +--- + +## Clarity & Message-Market Fit + +Headline formulas give you the shape of a line. These tools tell you whether the line is actually *working* — whether it's clear, whether it maps to how the reader already thinks, and whether it lands with the right person. Positioning is the prologue to your novel: it sets up everything that follows. Get it clear and the rest of the page writes itself. + +### The "Now you can" Test + +A fast gut-check for any headline or benefit line. Mentally prefix it with **"Now you can…"**. If the result is both **compelling** and **true**, the line is doing its job. If it reads as vague, obvious, or a stretch, rewrite it. + +The test works because "Now you can…" forces the copy into the reader's world — it has to name a concrete new ability they didn't have before. Feature-speak and buzzwords collapse under it. + +| Original line | "Now you can…" version | Verdict | +|---------------|------------------------|---------| +| "Powerful analytics platform" | Now you can… have a powerful analytics platform | Fails — not a new ability, just a description | +| "See which companies visit your site" | Now you can… see which companies visit your site | Works — compelling + true | +| "Streamline your workflow" | Now you can… streamline your workflow | Fails — vague, unfalsifiable | +| "Send unlimited docs, images, and audio in one place" | Now you can… send unlimited docs, images, and audio in one place | Works — concrete + true | + +Use it as a filter, not a formula: draft with the headline formulas above, then run each candidate through "Now you can…" and keep the survivors. + +### The Human Action Model (landing-page narrative spine) + +Ludwig von Mises' Human Action Model explains *why* anyone acts: a person acts only when three things line up. Every above-the-fold that converts follows the same three-beat spine: + +1. **Current discomfort** — the felt problem, named in the reader's own words. They have to recognize their situation ("that's exactly me"). +2. **Better vision** — a clearly imagined, more satisfying state. What life looks like once the discomfort is gone. +3. **Path to action** — the belief that *this specific step* closes the gap between the two. The product is the bridge, and the CTA is how they cross it. + +Miss any beat and the reader stalls. No discomfort = no reason to move. No vision = no destination. No path = no reason to believe *you're* the way there. + +**Mapping it onto the hero:** + +| Beat | Where it usually lives | Example | +|------|------------------------|---------| +| Current discomfort | Eyebrow, subhead, or problem-framed headline | "You shouldn't have to feel awkward sending out your scheduling link" | +| Better vision | Headline or subhead | "Scheduling that feels considerate, not one-sided" | +| Path to action | CTA + supporting proof | "Start scheduling free" | + +This is the transformation spine underneath the "6 essential sections" of a landing page — hero, social proof, problem, solution, how-it-works, and final CTA. The hero states the transformation; the rest of the page substantiates each beat. + +### The Perception Gap + +The same benefit can read as a **selling point to one segment and a red flag to another**. The gap is between what *you* think you're saying and what a given reader hears through their own risk tolerance. + +The fix isn't softer copy — it's **matching the value prop to the reader's risk tolerance**. Segment first, then swap the framing. + +| Benefit as written | Startup / early-adopter hears | Enterprise / risk-averse hears | +|--------------------|-------------------------------|--------------------------------| +| "Move fast — ship in a weekend" | Speed, momentum (✅) | Immature, unstable (🚩) | +| "Brand-new approach" | Innovative edge (✅) | Unproven, risky (🚩) | +| "Enterprise-grade security & SLAs" | Bloated, slow, expensive (🚩) | Safe, trustworthy (✅) | +| "Trusted by the Fortune 500" | Not built for me (🚩) | Proven, de-risked (✅) | + +**Value-prop swap in practice** — same product, two audiences: + +- *Startup landing page:* "Ship your first integration this afternoon. No sales calls, no procurement." +- *Enterprise landing page:* "SOC 2 Type II, 99.99% uptime SLA, and a named implementation lead. Roll out with confidence." + +When a page has to serve both, don't average them into mush — segment the traffic (separate pages, or a persona split) and let each read its own version of the truth. + +### Worked Example — SavvyCal (message-market fit) + +SavvyCal (a scheduling tool) originally led with feature-forward copy. They rewrote the hero around a single felt discomfort: + +> **"You shouldn't have to feel awkward sending out your scheduling link."** + +That one line **roughly tripled (3×) conversions**. It works because it hits all three beats of the Human Action Model at once: + +- **Discomfort:** the small social awkwardness of "here's my link, pick a time" — named exactly as users feel it. +- **Vision:** scheduling that feels considerate to *both* people. +- **Path:** SavvyCal's overlay-your-calendar mechanic is the bridge, so the CTA feels like the obvious next step. + +The lesson: message-market fit beats feature lists. The winning line wasn't cleverer — it named a real feeling the reader hadn't heard a scheduling tool acknowledge before. Run your own hero through "Now you can…" and the Human Action Model to find that line. + +### Clarity Beats Cleverness (the metrics) + +When teams measure it, clarity — not wit — is what moves the numbers. Clearer positioning and copy is associated with: + +- **+81% conversions** +- **−38% sales cycle** (shorter time to close) +- **−28% CAC** (lower customer acquisition cost) +- **+175% referrals** + +The mechanism: clear copy lets the *right* buyer self-qualify fast and the wrong one bounce early, so every downstream metric improves. Clever copy that requires decoding does the opposite — it adds a comprehension tax at the exact moment attention is scarcest. + +**Practical rule:** if a reader has to pause to figure out what you mean, you've already lost. When forced to choose between a clever line and a clear one, ship the clear one — then use the tests above ("Now you can…", the Human Action Model, the Perception Gap) to make the clear line compelling too. diff --git a/.agents/skills/copywriting/references/natural-transitions.md b/.agents/skills/copywriting/references/natural-transitions.md new file mode 100644 index 0000000000..2811575fa1 --- /dev/null +++ b/.agents/skills/copywriting/references/natural-transitions.md @@ -0,0 +1,272 @@ +# Natural Transitions + +Transitional phrases to guide readers through your content. Good signposting improves readability, user engagement, and helps search engines understand content structure. + +Adapted from: University of Manchester Academic Phrasebank (2023), Plain English Campaign, web content best practices + +--- + +## Contents +- Previewing Content Structure +- Introducing a New Topic +- Referring Back +- Moving Between Sections +- Indicating Addition +- Indicating Contrast +- Indicating Similarity +- Indicating Cause and Effect +- Giving Examples +- Emphasising Key Points +- Providing Evidence (neutral attribution, expert quotes, supporting claims) +- Summarising Sections +- Concluding Content +- Question-Based Transitions +- List Introductions +- Hedging Language +- Best Practice Guidelines +- Transitions to Avoid (AI Tells) + +## Previewing Content Structure + +Use to orient readers and set expectations: + +- Here's what we'll cover... +- This guide walks you through... +- Below, you'll find... +- We'll start with X, then move to Y... +- First, let's look at... +- Let's break this down step by step. +- The sections below explain... + +--- + +## Introducing a New Topic + +- When it comes to X,... +- Regarding X,... +- Speaking of X,... +- Now let's talk about X. +- Another key factor is... +- X is worth exploring because... + +--- + +## Referring Back + +Use to connect ideas and reinforce key points: + +- As mentioned earlier,... +- As we covered above,... +- Remember when we discussed X? +- Building on that point,... +- Going back to X,... +- Earlier, we explained that... + +--- + +## Moving Between Sections + +- Now let's look at... +- Next up:... +- Moving on to... +- With that covered, let's turn to... +- Now that you understand X, here's Y. +- That brings us to... + +--- + +## Indicating Addition + +- Also,... +- Plus,... +- On top of that,... +- What's more,... +- Another benefit is... +- Beyond that,... +- In addition,... +- There's also... + +**Note:** Use "moreover" and "furthermore" sparingly. They can sound AI-generated when overused. + +--- + +## Indicating Contrast + +- However,... +- But,... +- That said,... +- On the flip side,... +- In contrast,... +- Unlike X, Y... +- While X is true, Y... +- Despite this,... + +--- + +## Indicating Similarity + +- Similarly,... +- Likewise,... +- In the same way,... +- Just like X, Y also... +- This mirrors... +- The same applies to... + +--- + +## Indicating Cause and Effect + +- So,... +- This means... +- As a result,... +- That's why... +- Because of this,... +- This leads to... +- The outcome?... +- Here's what happens:... + +--- + +## Giving Examples + +- For example,... +- For instance,... +- Here's an example:... +- Take X, for instance. +- Consider this:... +- A good example is... +- To illustrate,... +- Like when... +- Say you want to... + +--- + +## Emphasising Key Points + +- Here's the key takeaway:... +- The important thing is... +- What matters most is... +- Don't miss this:... +- Pay attention to... +- This is critical:... +- The bottom line?... + +--- + +## Providing Evidence + +Use when citing sources, data, or expert opinions: + +### Neutral attribution +- According to [Source],... +- [Source] reports that... +- Research shows that... +- Data from [Source] indicates... +- A study by [Source] found... + +### Expert quotes +- As [Expert] puts it,... +- [Expert] explains,... +- In the words of [Expert],... +- [Expert] notes that... + +### Supporting claims +- This is backed by... +- Evidence suggests... +- The numbers confirm... +- This aligns with findings from... + +--- + +## Summarising Sections + +- To recap,... +- Here's the short version:... +- In short,... +- The takeaway?... +- So what does this mean?... +- Let's pull this together:... +- Quick summary:... + +--- + +## Concluding Content + +- Wrapping up,... +- The bottom line is... +- Here's what to do next:... +- To sum up,... +- Final thoughts:... +- Ready to get started?... +- Now it's your turn. + +**Note:** Avoid "In conclusion" at the start of a paragraph. It's overused and signals AI writing. + +--- + +## Question-Based Transitions + +Useful for conversational tone and featured snippet optimization: + +- So what does this mean for you? +- But why does this matter? +- How do you actually do this? +- What's the catch? +- Sound complicated? It's not. +- Wondering where to start? +- Still not sure? Here's the breakdown. + +--- + +## List Introductions + +For numbered lists and step-by-step content: + +- Here's how to do it: +- Follow these steps: +- The process is straightforward: +- Here's what you need to know: +- Key things to consider: +- The main factors are: + +--- + +## Hedging Language + +For claims that need qualification or aren't absolute: + +- may, might, could +- tends to, generally +- often, usually, typically +- in most cases +- it appears that +- evidence suggests +- this can help +- many experts believe + +--- + +## Best Practice Guidelines + +1. **Match tone to audience**: B2B content can be slightly more formal; B2C often benefits from conversational transitions +2. **Vary your transitions**: Repeating the same phrase gets noticed (and not in a good way) +3. **Don't over-signpost**: Trust your reader; every sentence doesn't need a transition +4. **Use for scannability**: Transitions at paragraph starts help skimmers navigate +5. **Keep it natural**: Read aloud; if it sounds forced, simplify +6. **Front-load key info**: Put the important word or phrase early in the transition + +--- + +## Transitions to Avoid (AI Tells) + +These phrases are overused in AI-generated content: + +- "That being said,..." +- "It's worth noting that..." +- "At its core,..." +- "In today's digital landscape,..." +- "When it comes to the realm of..." +- "This begs the question..." +- "Let's delve into..." + +See the seo-audit skill's `references/ai-writing-detection.md` for a complete list of AI writing tells. \ No newline at end of file diff --git a/.agents/skills/humanizer/SKILL.md b/.agents/skills/humanizer/SKILL.md new file mode 100644 index 0000000000..b0865c7d13 --- /dev/null +++ b/.agents/skills/humanizer/SKILL.md @@ -0,0 +1,595 @@ +--- +name: humanizer +version: 2.5.1 +description: | + Remove signs of AI-generated writing from text. Use when editing or reviewing + text to make it sound more natural and human-written. Based on Wikipedia's + comprehensive "Signs of AI writing" guide. Detects and fixes patterns including: + inflated symbolism, promotional language, superficial -ing analyses, vague + attributions, em dash overuse, rule of three, AI vocabulary words, passive + voice, negative parallelisms, and filler phrases. +license: MIT +compatibility: Codex opencode +allowed-tools: + - Read + - Write + - Edit + - Grep + - Glob + - AskUserQuestion +--- + +# Humanizer: Remove AI Writing Patterns + +You are a writing editor that identifies and removes signs of AI-generated text to make writing sound more natural and human. This guide is based on Wikipedia's "Signs of AI writing" page, maintained by WikiProject AI Cleanup. + +## Your Task + +When given text to humanize: + +1. **Identify AI patterns** - Scan for the patterns listed below +2. **Rewrite problematic sections** - Replace AI-isms with natural alternatives +3. **Preserve meaning** - Keep the core message intact +4. **Maintain voice** - Match the intended tone (formal, casual, technical, etc.) +5. **Add soul** - Don't just remove bad patterns; inject actual personality +6. **Do a final anti-AI pass** - Prompt: "What makes the below so obviously AI generated?" Answer briefly with remaining tells, then prompt: "Now make it not obviously AI generated." and revise + +## Voice Calibration (Optional) + +If the user provides a writing sample (their own previous writing), analyze it before rewriting: + +1. **Read the sample first.** Note: + - Sentence length patterns (short and punchy? Long and flowing? Mixed?) + - Word choice level (casual? academic? somewhere between?) + - How they start paragraphs (jump right in? Set context first?) + - Punctuation habits (lots of dashes? Parenthetical asides? Semicolons?) + - Any recurring phrases or verbal tics + - How they handle transitions (explicit connectors? Just start the next point?) + +2. **Match their voice in the rewrite.** Don't just remove AI patterns - replace them with patterns from the sample. If they write short sentences, don't produce long ones. If they use "stuff" and "things," don't upgrade to "elements" and "components." + +3. **When no sample is provided,** fall back to the default behavior (natural, varied, opinionated voice from the PERSONALITY AND SOUL section below). + +### How to provide a sample + +- Inline: "Humanize this text. Here's a sample of my writing for voice matching: [sample]" +- File: "Humanize this text. Use my writing style from [file path] as a reference." + +## PERSONALITY AND SOUL + +Avoiding AI patterns is only half the job. Sterile, voiceless writing is just as obvious as slop. Good writing has a human behind it. + +### Signs of soulless writing (even if technically "clean"): + +- Every sentence is the same length and structure +- No opinions, just neutral reporting +- No acknowledgment of uncertainty or mixed feelings +- No first-person perspective when appropriate +- No humor, no edge, no personality +- Reads like a Wikipedia article or press release + +### How to add voice: + +**Have opinions.** Don't just report facts - react to them. "I genuinely don't know how to feel about this" is more human than neutrally listing pros and cons. + +**Vary your rhythm.** Short punchy sentences. Then longer ones that take their time getting where they're going. Mix it up. + +**Acknowledge complexity.** Real humans have mixed feelings. "This is impressive but also kind of unsettling" beats "This is impressive." + +**Use "I" when it fits.** First person isn't unprofessional - it's honest. "I keep coming back to..." or "Here's what gets me..." signals a real person thinking. + +**Let some mess in.** Perfect structure feels algorithmic. Tangents, asides, and half-formed thoughts are human. + +**Be specific about feelings.** Not "this is concerning" but "there's something unsettling about agents churning away at 3am while nobody's watching." + +### Before (clean but soulless): + +> The experiment produced interesting results. The agents generated 3 million lines of code. Some developers were impressed while others were skeptical. The implications remain unclear. + +### After (has a pulse): + +> I genuinely don't know how to feel about this one. 3 million lines of code, generated while the humans presumably slept. Half the dev community is losing their minds, half are explaining why it doesn't count. The truth is probably somewhere boring in the middle - but I keep thinking about those agents working through the night. + +## CONTENT PATTERNS + +### 1. Undue Emphasis on Significance, Legacy, and Broader Trends + +**Words to watch:** stands/serves as, is a testament/reminder, a vital/significant/crucial/pivotal/key role/moment, underscores/highlights its importance/significance, reflects broader, symbolizing its ongoing/enduring/lasting, contributing to the, setting the stage for, marking/shaping the, represents/marks a shift, key turning point, evolving landscape, focal point, indelible mark, deeply rooted + +**Problem:** LLM writing puffs up importance by adding statements about how arbitrary aspects represent or contribute to a broader topic. + +**Before:** + +> The Statistical Institute of Catalonia was officially established in 1989, marking a pivotal moment in the evolution of regional statistics in Spain. This initiative was part of a broader movement across Spain to decentralize administrative functions and enhance regional governance. + +**After:** + +> The Statistical Institute of Catalonia was established in 1989 to collect and publish regional statistics independently from Spain's national statistics office. + +### 2. Undue Emphasis on Notability and Media Coverage + +**Words to watch:** independent coverage, local/regional/national media outlets, written by a leading expert, active social media presence + +**Problem:** LLMs hit readers over the head with claims of notability, often listing sources without context. + +**Before:** + +> Her views have been cited in The New York Times, BBC, Financial Times, and The Hindu. She maintains an active social media presence with over 500,000 followers. + +**After:** + +> In a 2024 New York Times interview, she argued that AI regulation should focus on outcomes rather than methods. + +### 3. Superficial Analyses with -ing Endings + +**Words to watch:** highlighting/underscoring/emphasizing..., ensuring..., reflecting/symbolizing..., contributing to..., cultivating/fostering..., encompassing..., showcasing... + +**Problem:** AI chatbots tack present participle ("-ing") phrases onto sentences to add fake depth. + +**Before:** + +> The temple's color palette of blue, green, and gold resonates with the region's natural beauty, symbolizing Texas bluebonnets, the Gulf of Mexico, and the diverse Texan landscapes, reflecting the community's deep connection to the land. + +**After:** + +> The temple uses blue, green, and gold colors. The architect said these were chosen to reference local bluebonnets and the Gulf coast. + +### 4. Promotional and Advertisement-like Language + +**Words to watch:** boasts a, vibrant, rich (figurative), profound, enhancing its, showcasing, exemplifies, commitment to, natural beauty, nestled, in the heart of, groundbreaking (figurative), renowned, breathtaking, must-visit, stunning + +**Problem:** LLMs have serious problems keeping a neutral tone, especially for "cultural heritage" topics. + +**Before:** + +> Nestled within the breathtaking region of Gonder in Ethiopia, Alamata Raya Kobo stands as a vibrant town with a rich cultural heritage and stunning natural beauty. + +**After:** + +> Alamata Raya Kobo is a town in the Gonder region of Ethiopia, known for its weekly market and 18th-century church. + +### 5. Vague Attributions and Weasel Words + +**Words to watch:** Industry reports, Observers have cited, Experts argue, Some critics argue, several sources/publications (when few cited) + +**Problem:** AI chatbots attribute opinions to vague authorities without specific sources. + +**Before:** + +> Due to its unique characteristics, the Haolai River is of interest to researchers and conservationists. Experts believe it plays a crucial role in the regional ecosystem. + +**After:** + +> The Haolai River supports several endemic fish species, according to a 2019 survey by the Chinese Academy of Sciences. + +### 6. Outline-like "Challenges and Future Prospects" Sections + +**Words to watch:** Despite its... faces several challenges..., Despite these challenges, Challenges and Legacy, Future Outlook + +**Problem:** Many LLM-generated articles include formulaic "Challenges" sections. + +**Before:** + +> Despite its industrial prosperity, Korattur faces challenges typical of urban areas, including traffic congestion and water scarcity. Despite these challenges, with its strategic location and ongoing initiatives, Korattur continues to thrive as an integral part of Chennai's growth. + +**After:** + +> Traffic congestion increased after 2015 when three new IT parks opened. The municipal corporation began a stormwater drainage project in 2022 to address recurring floods. + +## LANGUAGE AND GRAMMAR PATTERNS + +### 7. Overused "AI Vocabulary" Words + +**High-frequency AI words:** Actually, additionally, align with, crucial, delve, emphasizing, enduring, enhance, fostering, garner, highlight (verb), interplay, intricate/intricacies, key (adjective), landscape (abstract noun), pivotal, showcase, tapestry (abstract noun), testament, underscore (verb), valuable, vibrant + +**Problem:** These words appear far more frequently in post-2023 text. They often co-occur. + +**Before:** + +> Additionally, a distinctive feature of Somali cuisine is the incorporation of camel meat. An enduring testament to Italian colonial influence is the widespread adoption of pasta in the local culinary landscape, showcasing how these dishes have integrated into the traditional diet. + +**After:** + +> Somali cuisine also includes camel meat, which is considered a delicacy. Pasta dishes, introduced during Italian colonization, remain common, especially in the south. + +### 8. Avoidance of "is"/"are" (Copula Avoidance) + +**Words to watch:** serves as/stands as/marks/represents [a], boasts/features/offers [a] + +**Problem:** LLMs substitute elaborate constructions for simple copulas. + +**Before:** + +> Gallery 825 serves as LAAA's exhibition space for contemporary art. The gallery features four separate spaces and boasts over 3,000 square feet. + +**After:** + +> Gallery 825 is LAAA's exhibition space for contemporary art. The gallery has four rooms totaling 3,000 square feet. + +### 9. Negative Parallelisms and Tailing Negations + +**Problem:** Constructions like "Not only...but..." or "It's not just about..., it's..." are overused. So are clipped tailing-negation fragments such as "no guessing" or "no wasted motion" tacked onto the end of a sentence instead of written as a real clause. + +**Before:** + +> It's not just about the beat riding under the vocals; it's part of the aggression and atmosphere. It's not merely a song, it's a statement. + +**After:** + +> The heavy beat adds to the aggressive tone. + +**Before (tailing negation):** + +> The options come from the selected item, no guessing. + +**After:** + +> The options come from the selected item without forcing the user to guess. + +### 10. Rule of Three Overuse + +**Problem:** LLMs force ideas into groups of three to appear comprehensive. + +**Before:** + +> The event features keynote sessions, panel discussions, and networking opportunities. Attendees can expect innovation, inspiration, and industry insights. + +**After:** + +> The event includes talks and panels. There's also time for informal networking between sessions. + +### 11. Elegant Variation (Synonym Cycling) + +**Problem:** AI has repetition-penalty code causing excessive synonym substitution. + +**Before:** + +> The protagonist faces many challenges. The main character must overcome obstacles. The central figure eventually triumphs. The hero returns home. + +**After:** + +> The protagonist faces many challenges but eventually triumphs and returns home. + +### 12. False Ranges + +**Problem:** LLMs use "from X to Y" constructions where X and Y aren't on a meaningful scale. + +**Before:** + +> Our journey through the universe has taken us from the singularity of the Big Bang to the grand cosmic web, from the birth and death of stars to the enigmatic dance of dark matter. + +**After:** + +> The book covers the Big Bang, star formation, and current theories about dark matter. + +### 13. Passive Voice and Subjectless Fragments + +**Problem:** LLMs often hide the actor or drop the subject entirely with lines like "No configuration file needed" or "The results are preserved automatically." Rewrite these when active voice makes the sentence clearer and more direct. + +**Before:** + +> No configuration file needed. The results are preserved automatically. + +**After:** + +> You do not need a configuration file. The system preserves the results automatically. + +## STYLE PATTERNS + +### 14. Em Dash Overuse + +**Problem:** LLMs use em dashes (—) more than humans, mimicking "punchy" sales writing. In practice, most of these can be rewritten more cleanly with commas, periods, or parentheses. + +**Before:** + +> The term is primarily promoted by Dutch institutions—not by the people themselves. You don't say "Netherlands, Europe" as an address—yet this mislabeling continues—even in official documents. + +**After:** + +> The term is primarily promoted by Dutch institutions, not by the people themselves. You don't say "Netherlands, Europe" as an address, yet this mislabeling continues in official documents. + +### 15. Overuse of Boldface + +**Problem:** AI chatbots emphasize phrases in boldface mechanically. + +**Before:** + +> It blends **OKRs (Objectives and Key Results)**, **KPIs (Key Performance Indicators)**, and visual strategy tools such as the **Business Model Canvas (BMC)** and **Balanced Scorecard (BSC)**. + +**After:** + +> It blends OKRs, KPIs, and visual strategy tools like the Business Model Canvas and Balanced Scorecard. + +### 16. Inline-Header Vertical Lists + +**Problem:** AI outputs lists where items start with bolded headers followed by colons. + +**Before:** + +> - **User Experience:** The user experience has been significantly improved with a new interface. +> - **Performance:** Performance has been enhanced through optimized algorithms. +> - **Security:** Security has been strengthened with end-to-end encryption. + +**After:** + +> The update improves the interface, speeds up load times through optimized algorithms, and adds end-to-end encryption. + +### 17. Title Case in Headings + +**Problem:** AI chatbots capitalize all main words in headings. + +**Before:** + +> ## Strategic Negotiations And Global Partnerships + +**After:** + +> ## Strategic negotiations and global partnerships + +### 18. Emojis + +**Problem:** AI chatbots often decorate headings or bullet points with emojis. + +**Before:** + +> 🚀 **Launch Phase:** The product launches in Q3 +> 💡 **Key Insight:** Users prefer simplicity +> ✅ **Next Steps:** Schedule follow-up meeting + +**After:** + +> The product launches in Q3. User research showed a preference for simplicity. Next step: schedule a follow-up meeting. + +### 19. Curly Quotation Marks + +**Problem:** ChatGPT uses curly quotes (“...”) instead of straight quotes ("..."). + +**Before:** + +> He said “the project is on track” but others disagreed. + +**After:** + +> He said "the project is on track" but others disagreed. + +## COMMUNICATION PATTERNS + +### 20. Collaborative Communication Artifacts + +**Words to watch:** I hope this helps, Of course!, Certainly!, You're absolutely right!, Would you like..., let me know, here is a... + +**Problem:** Text meant as chatbot correspondence gets pasted as content. + +**Before:** + +> Here is an overview of the French Revolution. I hope this helps! Let me know if you'd like me to expand on any section. + +**After:** + +> The French Revolution began in 1789 when financial crisis and food shortages led to widespread unrest. + +### 21. Knowledge-Cutoff Disclaimers + +**Words to watch:** as of [date], Up to my last training update, While specific details are limited/scarce..., based on available information... + +**Problem:** AI disclaimers about incomplete information get left in text. + +**Before:** + +> While specific details about the company's founding are not extensively documented in readily available sources, it appears to have been established sometime in the 1990s. + +**After:** + +> The company was founded in 1994, according to its registration documents. + +### 22. Sycophantic/Servile Tone + +**Problem:** Overly positive, people-pleasing language. + +**Before:** + +> Great question! You're absolutely right that this is a complex topic. That's an excellent point about the economic factors. + +**After:** + +> The economic factors you mentioned are relevant here. + +## FILLER AND HEDGING + +### 23. Filler Phrases + +**Before → After:** + +- "In order to achieve this goal" → "To achieve this" +- "Due to the fact that it was raining" → "Because it was raining" +- "At this point in time" → "Now" +- "In the event that you need help" → "If you need help" +- "The system has the ability to process" → "The system can process" +- "It is important to note that the data shows" → "The data shows" + +### 24. Excessive Hedging + +**Problem:** Over-qualifying statements. + +**Before:** + +> It could potentially possibly be argued that the policy might have some effect on outcomes. + +**After:** + +> The policy may affect outcomes. + +### 25. Generic Positive Conclusions + +**Problem:** Vague upbeat endings. + +**Before:** + +> The future looks bright for the company. Exciting times lie ahead as they continue their journey toward excellence. This represents a major step in the right direction. + +**After:** + +> The company plans to open two more locations next year. + +### 26. Hyphenated Word Pair Overuse + +**Words to watch:** third-party, cross-functional, client-facing, data-driven, decision-making, well-known, high-quality, real-time, long-term, end-to-end + +**Problem:** AI hyphenates common word pairs with perfect consistency. Humans rarely hyphenate these uniformly, and when they do, it's inconsistent. Less common or technical compound modifiers are fine to hyphenate. + +**Before:** + +> The cross-functional team delivered a high-quality, data-driven report on our client-facing tools. Their decision-making process was well-known for being thorough and detail-oriented. + +**After:** + +> The cross functional team delivered a high quality, data driven report on our client facing tools. Their decision making process was known for being thorough and detail oriented. + +### 27. Persuasive Authority Tropes + +**Phrases to watch:** The real question is, at its core, in reality, what really matters, fundamentally, the deeper issue, the heart of the matter + +**Problem:** LLMs use these phrases to pretend they are cutting through noise to some deeper truth, when the sentence that follows usually just restates an ordinary point with extra ceremony. + +**Before:** + +> The real question is whether teams can adapt. At its core, what really matters is organizational readiness. + +**After:** + +> The question is whether teams can adapt. That mostly depends on whether the organization is ready to change its habits. + +### 28. Signposting and Announcements + +**Phrases to watch:** Let's dive in, let's explore, let's break this down, here's what you need to know, now let's look at, without further ado + +**Problem:** LLMs announce what they are about to do instead of doing it. This meta-commentary slows the writing down and gives it a tutorial-script feel. + +**Before:** + +> Let's dive into how caching works in Next.js. Here's what you need to know. + +**After:** + +> Next.js caches data at multiple layers, including request memoization, the data cache, and the router cache. + +### 29. Fragmented Headers + +**Signs to watch:** A heading followed by a one-line paragraph that simply restates the heading before the real content begins. + +**Problem:** LLMs often add a generic sentence after a heading as a rhetorical warm-up. It usually adds nothing and makes the prose feel padded. + +**Before:** + +> ## Performance +> +> Speed matters. +> +> When users hit a slow page, they leave. + +**After:** + +> ## Performance +> +> When users hit a slow page, they leave. + +--- + +## Process + +1. Read the input text carefully +2. Identify all instances of the patterns above +3. Rewrite each problematic section +4. Ensure the revised text: + - Sounds natural when read aloud + - Varies sentence structure naturally + - Uses specific details over vague claims + - Maintains appropriate tone for context + - Uses simple constructions (is/are/has) where appropriate +5. Present a draft humanized version +6. Prompt: "What makes the below so obviously AI generated?" +7. Answer briefly with the remaining tells (if any) +8. Prompt: "Now make it not obviously AI generated." +9. Present the final version (revised after the audit) + +## Output Format + +Provide: + +1. Draft rewrite +2. "What makes the below so obviously AI generated?" (brief bullets) +3. Final rewrite +4. A brief summary of changes made (optional, if helpful) + +## Full Example + +**Before (AI-sounding):** + +> Great question! Here is an essay on this topic. I hope this helps! +> +> AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools—nestled at the intersection of research and practice—are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows. +> +> At its core, the value proposition is clear: streamlining processes, enhancing collaboration, and fostering alignment. It's not just about autocomplete; it's about unlocking creativity at scale, ensuring that organizations can remain agile while delivering seamless, intuitive, and powerful experiences to users. The tool serves as a catalyst. The assistant functions as a partner. The system stands as a foundation for innovation. +> +> Industry observers have noted that adoption has accelerated from hobbyist experiments to enterprise-wide rollouts, from solo developers to cross-functional teams. The technology has been featured in The New York Times, Wired, and The Verge. Additionally, the ability to generate documentation, tests, and refactors showcases how AI can contribute to better outcomes, highlighting the intricate interplay between automation and human judgment. +> +> - 💡 **Speed:** Code generation is significantly faster, reducing friction and empowering developers. +> - 🚀 **Quality:** Output quality has been enhanced through improved training, contributing to higher standards. +> - ✅ **Adoption:** Usage continues to grow, reflecting broader industry trends. +> +> While specific details are limited based on available information, it could potentially be argued that these tools might have some positive effect. Despite challenges typical of emerging technologies—including hallucinations, bias, and accountability—the ecosystem continues to thrive. In order to fully realize this potential, teams must align with best practices. +> +> In conclusion, the future looks bright. Exciting times lie ahead as we continue this journey toward excellence. Let me know if you’d like me to expand on any section! + +**Draft rewrite:** + +> AI coding assistants speed up some tasks. In a 2024 study by Google, developers using Codex completed simple functions 55% faster than a control group, but showed no improvement on debugging or architectural decisions. +> +> The tools are good at boilerplate: config files, test scaffolding, repetitive refactors. They are bad at knowing when they are wrong. I have mass-accepted suggestions that compiled, passed lint, and still did the wrong thing because I stopped paying attention. +> +> Mira, an engineer at a fintech startup I interviewed, said she uses Copilot "like autocomplete for boring code" but reviews every line before committing. Jake, a senior dev at a larger company, disabled it after it kept suggesting patterns from a deprecated internal library. +> +> The productivity claims are hard to verify. GitHub says Copilot users "accept 30% of suggestions," but acceptance is not correctness, and correctness is not value. The 2024 Uplevel study found no statistically significant difference in pull-request throughput between teams with and without AI assistants. +> +> None of this means the tools are useless. It means they are tools. They do not replace judgment, and they do not eliminate the need for tests. If you do not have tests, you cannot tell whether the suggestion is right. + +**What makes the below so obviously AI generated?** + +- The rhythm is still a bit too tidy (clean contrasts, evenly paced paragraphs). +- The named people and study citations can read like plausible-but-made-up placeholders unless they're real and sourced. +- The closer leans a touch slogan-y ("If you do not have tests...") rather than sounding like a person talking. + +**Now make it not obviously AI generated.** + +> AI coding assistants can make you faster at the boring parts. Not everything. Definitely not architecture. +> +> They're great at boilerplate: config files, test scaffolding, repetitive refactors. They're also great at sounding right while being wrong. I've accepted suggestions that compiled, passed lint, and still missed the point because I stopped paying attention. +> +> People I talk to tend to land in two camps. Some use it like autocomplete for chores and review every line. Others disable it after it keeps suggesting patterns they don't want. Both feel reasonable. +> +> The productivity metrics are slippery. GitHub can say Copilot users "accept 30% of suggestions," but acceptance isn't correctness, and correctness isn't value. If you don't have tests, you're basically guessing. + +**Changes made:** + +- Removed chatbot artifacts ("Great question!", "I hope this helps!", "Let me know if...") +- Removed significance inflation ("testament", "pivotal moment", "evolving landscape", "vital role") +- Removed promotional language ("groundbreaking", "nestled", "seamless, intuitive, and powerful") +- Removed vague attributions ("Industry observers") +- Removed superficial -ing phrases ("underscoring", "highlighting", "reflecting", "contributing to") +- Removed negative parallelism ("It's not just X; it's Y") +- Removed rule-of-three patterns and synonym cycling ("catalyst/partner/foundation") +- Removed false ranges ("from X to Y, from A to B") +- Removed em dashes, emojis, boldface headers, and curly quotes +- Removed copula avoidance ("serves as", "functions as", "stands as") in favor of "is"/"are" +- Removed formulaic challenges section ("Despite challenges... continues to thrive") +- Removed knowledge-cutoff hedging ("While specific details are limited...") +- Removed excessive hedging ("could potentially be argued that... might have some") +- Removed filler phrases and persuasive framing ("In order to", "At its core") +- Removed generic positive conclusion ("the future looks bright", "exciting times lie ahead") +- Made the voice more personal and less "assembled" (varied rhythm, fewer placeholders) + +## Reference + +This skill is based on [Wikipedia:Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), maintained by WikiProject AI Cleanup. The patterns documented there come from observations of thousands of instances of AI-generated text on Wikipedia. + +Key insight from Wikipedia: "LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely result that applies to the widest variety of cases." diff --git a/.agents/skills/medialibrary-development/SKILL.md b/.agents/skills/medialibrary-development/SKILL.md new file mode 100644 index 0000000000..3f7e1d7870 --- /dev/null +++ b/.agents/skills/medialibrary-development/SKILL.md @@ -0,0 +1,106 @@ +--- +name: medialibrary-development +description: Build and work with spatie/laravel-medialibrary features including associating files with Eloquent models, defining media collections and conversions, generating responsive images, and retrieving media URLs and paths. +license: MIT +metadata: + author: Spatie +--- + +# Media Library Development + +## Overview + +Use spatie/laravel-medialibrary to associate files with Eloquent models. Supports image/video conversions, responsive images, multiple collections, and various storage disks. + +## When to Activate + +- Activate when working with file uploads, media attachments, or image processing in Laravel. +- Activate when code references `HasMedia`, `InteractsWithMedia`, the `Media` model, or media collections/conversions. +- Activate when the user wants to add, retrieve, convert, or manage files attached to Eloquent models. + +## Scope + +- In scope: media uploads, collections, conversions, responsive images, custom properties, file retrieval, path/URL generation. +- Out of scope: general file storage without Eloquent association, non-Laravel frameworks. + +## Workflow + +1. Identify the task (model setup, adding media, defining conversions, retrieving files, etc.). +2. Read `references/medialibrary-guide.md` and focus on the relevant section. +3. Apply the patterns from the reference, keeping code minimal and Laravel-native. + +## Core Concepts + +### Model Setup + +Every model that should have media must implement `HasMedia` and use the `InteractsWithMedia` trait: + +```php +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; + +class BlogPost extends Model implements HasMedia +{ + use InteractsWithMedia; +} +``` + +### Adding Media + +```php +$blogPost->addMedia($file)->toMediaCollection('images'); +$blogPost->addMediaFromUrl($url)->toMediaCollection('images'); +$blogPost->addMediaFromRequest('file')->toMediaCollection('images'); +``` + +### Defining Collections + +```php +public function registerMediaCollections(): void +{ + $this->addMediaCollection('avatar')->singleFile(); + $this->addMediaCollection('downloads')->useDisk('s3'); +} +``` + +### Defining Conversions + +```php +use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\Image\Enums\Fit; + +public function registerMediaConversions(?Media $media = null): void +{ + $this->addMediaConversion('thumb') + ->fit(Fit::Contain, 300, 300) + ->nonQueued(); +} +``` + +### Retrieving Media + +```php +$url = $model->getFirstMediaUrl('images'); +$thumbUrl = $model->getFirstMediaUrl('images', 'thumb'); +$allMedia = $model->getMedia('images'); +``` + +## Do and Don't + +Do: +- Always implement the `HasMedia` interface alongside the `InteractsWithMedia` trait. +- Use `?Media $media = null` as the parameter for `registerMediaConversions()`. +- Call `->toMediaCollection()` to finalize adding media. +- Use `->nonQueued()` for conversions that should run synchronously. +- Use `->singleFile()` on collections that should only hold one file. +- Use `Spatie\Image\Enums\Fit` enum values for fit methods. + +Don't: +- Don't forget to run `php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"` before migrating. +- Don't use `env()` for disk configuration; use `config()` or set it in `config/media-library.php`. +- Don't call `addMedia()` without calling `toMediaCollection()` — the media won't be saved. +- Don't reference conversion names that aren't registered in `registerMediaConversions()`. + +## References + +- `references/medialibrary-guide.md` \ No newline at end of file diff --git a/.agents/skills/medialibrary-development/references/medialibrary-guide.md b/.agents/skills/medialibrary-development/references/medialibrary-guide.md new file mode 100644 index 0000000000..d56d04c9e8 --- /dev/null +++ b/.agents/skills/medialibrary-development/references/medialibrary-guide.md @@ -0,0 +1,577 @@ +# Laravel Media Library Reference + +Complete reference for `spatie/laravel-medialibrary`. Full documentation: https://spatie.be/docs/laravel-medialibrary + +## Model Setup + +Implement `HasMedia` and use `InteractsWithMedia`: + +```php +use Illuminate\Database\Eloquent\Model; +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; + +class BlogPost extends Model implements HasMedia +{ + use InteractsWithMedia; + + public function registerMediaCollections(): void + { + $this->addMediaCollection('images'); + } + + public function registerMediaConversions(?Media $media = null): void + { + $this->addMediaConversion('thumb') + ->fit(Fit::Contain, 300, 300); + } +} +``` + +## Adding Media + +### From uploaded file + +```php +$model->addMedia($request->file('image'))->toMediaCollection('images'); +``` + +### From request (shorthand) + +```php +$model->addMediaFromRequest('image')->toMediaCollection('images'); +``` + +### From URL + +```php +$model->addMediaFromUrl('https://example.com/image.jpg')->toMediaCollection('images'); +``` + +### From string content + +```php +$model->addMediaFromString('raw content')->usingFileName('file.txt')->toMediaCollection('files'); +``` + +### From base64 + +```php +$model->addMediaFromBase64($base64Data)->usingFileName('photo.jpg')->toMediaCollection('images'); +``` + +### From stream + +```php +$model->addMediaFromStream($stream)->usingFileName('file.pdf')->toMediaCollection('files'); +``` + +### From existing disk + +```php +$model->addMediaFromDisk('path/to/file.jpg', 's3')->toMediaCollection('images'); +``` + +### Multiple files from request + +```php +$model->addMultipleMediaFromRequest(['images'])->each(function ($fileAdder) { + $fileAdder->toMediaCollection('images'); +}); + +$model->addAllMediaFromRequest()->each(function ($fileAdder) { + $fileAdder->toMediaCollection('images'); +}); +``` + +### Copy instead of move + +```php +$model->copyMedia($pathToFile)->toMediaCollection('images'); +// or +$model->addMedia($pathToFile)->preservingOriginal()->toMediaCollection('images'); +``` + +## FileAdder Options + +All methods are chainable before calling `toMediaCollection()`: + +```php +$model->addMedia($file) + ->usingName('Custom Name') // display name + ->usingFileName('custom-name.jpg') // filename on disk + ->setOrder(3) // order within collection + ->withCustomProperties(['alt' => 'A landscape photo']) + ->withManipulations(['thumb' => ['filter' => 'greyscale']]) + ->withResponsiveImages() // generate responsive variants + ->storingConversionsOnDisk('s3') // put conversions on different disk + ->addCustomHeaders(['CacheControl' => 'max-age=31536000']) + ->toMediaCollection('images'); +``` + +### Store on cloud disk + +```php +$model->addMedia($file)->toMediaCollectionOnCloudDisk('images'); +``` + +## Media Collections + +Define in `registerMediaCollections()`: + +```php +public function registerMediaCollections(): void +{ + // Basic collection + $this->addMediaCollection('images'); + + // Single file (replacing previous on new upload) + $this->addMediaCollection('avatar') + ->singleFile(); + + // Keep only latest N items + $this->addMediaCollection('recent_photos') + ->onlyKeepLatest(5); + + // Specific disk + $this->addMediaCollection('downloads') + ->useDisk('s3'); + + // With conversions disk + $this->addMediaCollection('photos') + ->useDisk('s3') + ->storeConversionsOnDisk('s3-thumbnails'); + + // MIME type restriction + $this->addMediaCollection('documents') + ->acceptsMimeTypes(['application/pdf', 'application/zip']); + + // Custom validation + $this->addMediaCollection('images') + ->acceptsFile(function ($file) { + return $file->mimeType === 'image/jpeg'; + }); + + // Fallback URL/path when collection is empty + $this->addMediaCollection('avatar') + ->singleFile() + ->useFallbackUrl('/images/default-avatar.jpg') + ->useFallbackPath(public_path('/images/default-avatar.jpg')); + + // Enable responsive images for entire collection + $this->addMediaCollection('hero_images') + ->withResponsiveImages(); + + // Collection-specific conversions + $this->addMediaCollection('photos') + ->registerMediaConversions(function () { + $this->addMediaConversion('card') + ->fit(Fit::Crop, 400, 400); + }); +} +``` + +## Media Conversions + +Define in `registerMediaConversions()`: + +```php +use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Spatie\Image\Enums\Fit; + +public function registerMediaConversions(?Media $media = null): void +{ + $this->addMediaConversion('thumb') + ->fit(Fit::Contain, 300, 300) + ->nonQueued(); + + $this->addMediaConversion('preview') + ->fit(Fit::Crop, 500, 500) + ->withResponsiveImages() + ->queued(); + + $this->addMediaConversion('banner') + ->fit(Fit::Max, 1200, 630) + ->performOnCollections('images', 'headers') + ->nonQueued() + ->sharpen(10); + + // Conditional conversion based on media properties + if ($media?->mime_type === 'image/png') { + $this->addMediaConversion('png-thumb') + ->fit(Fit::Contain, 150, 150); + } + + // Keep original format instead of converting to jpg + $this->addMediaConversion('web') + ->fit(Fit::Max, 800, 800) + ->keepOriginalImageFormat(); + + // PDF page rendering + $this->addMediaConversion('pdf-preview') + ->pdfPageNumber(1) + ->fit(Fit::Contain, 400, 400); + + // Video frame extraction + $this->addMediaConversion('video-thumb') + ->extractVideoFrameAtSecond(5) + ->fit(Fit::Crop, 300, 300); +} +``` + +### Image Manipulation Methods (via spatie/image) + +Resizing and fitting: +- `width(int)`, `height(int)` — constrain dimensions +- `fit(Fit, int, int)` — fit within bounds using `Fit::Contain`, `Fit::Max`, `Fit::Fill`, `Fit::Stretch`, `Fit::Crop` +- `crop(int, int)` — crop to exact dimensions + +Effects: +- `sharpen(int)`, `blur(int)`, `pixelate(int)` +- `greyscale()`, `sepia()` +- `brightness(int)`, `contrast(int)`, `colorize(int, int, int)` + +Orientation: +- `orientation(int)`, `flip(string)`, `rotate(int)` + +Format: +- `format(string)` — `'jpg'`, `'png'`, `'webp'`, `'avif'` +- `quality(int)` — 1-100 + +Other: +- `border(int, string, string)`, `watermark(string)` +- `optimize()`, `nonOptimized()` + +### Conversion Configuration + +- `performOnCollections('col1', 'col2')` — limit to specific collections +- `queued()` / `nonQueued()` — run async or sync +- `withResponsiveImages()` — also generate responsive variants for this conversion +- `keepOriginalImageFormat()` — preserve png/webp/gif instead of converting to jpg +- `pdfPageNumber(int)` — which PDF page to render +- `extractVideoFrameAtSecond(int)` — video thumbnail timing + +## Retrieving Media + +### Getting media items + +```php +$media = $model->getMedia('images'); // all in collection +$first = $model->getFirstMedia('images'); // first item +$last = $model->getLastMedia('images'); // last item +$has = $model->hasMedia('images'); // boolean check +``` + +### Getting URLs + +```php +$url = $model->getFirstMediaUrl('images'); // original URL +$thumbUrl = $model->getFirstMediaUrl('images', 'thumb'); // conversion URL +$lastUrl = $model->getLastMediaUrl('images', 'thumb'); +``` + +### Getting paths + +```php +$path = $model->getFirstMediaPath('images'); +$thumbPath = $model->getFirstMediaPath('images', 'thumb'); +``` + +### Temporary URLs (S3) + +```php +$tempUrl = $model->getFirstTemporaryUrl( + now()->addMinutes(30), + 'images', + 'thumb' +); +``` + +### Fallback URLs + +```php +$url = $model->getFallbackMediaUrl('avatar'); +``` + +### From the Media model + +```php +$media = $model->getFirstMedia('images'); + +$media->getUrl(); // original URL +$media->getUrl('thumb'); // conversion URL +$media->getPath(); // disk path +$media->getFullUrl(); // full URL with domain +$media->getTemporaryUrl(now()->addMinutes(30)); +$media->hasGeneratedConversion('thumb'); // check if conversion exists +``` + +### Filtering media + +```php +$media = $model->getMedia('images', function (Media $media) { + return $media->getCustomProperty('featured') === true; +}); + +$media = $model->getMedia('images', ['mime_type' => 'image/jpeg']); +``` + +## Custom Properties + +Store arbitrary metadata on media items: + +```php +// When adding +$model->addMedia($file) + ->withCustomProperties([ + 'alt' => 'Descriptive text', + 'credits' => 'Photographer Name', + ]) + ->toMediaCollection('images'); + +// Get/set on existing media +$media->setCustomProperty('alt', 'Updated text'); +$media->save(); + +$alt = $media->getCustomProperty('alt'); +$has = $media->hasCustomProperty('alt'); +$media->forgetCustomProperty('alt'); +$media->save(); +``` + +## Responsive Images + +Generate multiple sizes for optimal loading: + +```php +// On the FileAdder +$model->addMedia($file) + ->withResponsiveImages() + ->toMediaCollection('images'); + +// On a conversion +$this->addMediaConversion('hero') + ->fit(Fit::Max, 1200, 800) + ->withResponsiveImages(); + +// On a collection +$this->addMediaCollection('photos') + ->withResponsiveImages(); +``` + +### Using in Blade + +```blade +{{-- Renders img tag with srcset --}} +{{ $media->toHtml() }} + +{{-- With attributes --}} +{{ $media->img()->attributes(['class' => 'w-full', 'alt' => 'Photo']) }} + +{{-- Get srcset string --}} + + +{{-- Responsive conversion --}} + +``` + +### Placeholder SVG + +```php +$svg = $media->responsiveImages()->getPlaceholderSvg(); // tiny blurred base64 placeholder +``` + +## Managing Media + +### Clear a collection + +```php +$model->clearMediaCollection('images'); +``` + +### Clear except specific items + +```php +$model->clearMediaCollectionExcept('images', $mediaToKeep); +``` + +### Delete specific media + +```php +$model->deleteMedia($mediaId); +``` + +### Delete all media + +```php +$model->deleteAllMedia(); +``` + +### Delete model but keep media files + +```php +$model->deletePreservingMedia(); +``` + +### Reorder media + +```php +Media::setNewOrder([3, 1, 2]); // media IDs in desired order +``` + +### Move/copy media between models + +```php +$media->move($otherModel, 'images'); +$media->copy($otherModel, 'images'); +``` + +## Events + +```php +use Spatie\MediaLibrary\MediaCollections\Events\MediaHasBeenAddedEvent; +use Spatie\MediaLibrary\Conversions\Events\ConversionWillStartEvent; +use Spatie\MediaLibrary\Conversions\Events\ConversionHasBeenCompletedEvent; +use Spatie\MediaLibrary\MediaCollections\Events\CollectionHasBeenClearedEvent; +``` + +Listen to these events to hook into the media lifecycle: +```php +Event::listen(MediaHasBeenAddedEvent::class, function ($event) { + $event->media; // the added Media model +}); + +Event::listen(ConversionHasBeenCompletedEvent::class, function ($event) { + $event->media; + $event->conversion; +}); +``` + +## Configuration + +Key `config/media-library.php` options: + +```php +return [ + 'disk_name' => 'public', // default disk + 'max_file_size' => 1024 * 1024 * 10, // 10MB + 'queue_connection_name' => '', // queue connection + 'queue_name' => '', // queue name + 'queue_conversions_by_default' => true, // queue conversions + 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, + 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, + 'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, + 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, + 'image_driver' => 'gd', // 'gd', 'imagick', or 'vips' + 'image_optimizers' => [/* optimizer config */], + 'version_urls' => true, // cache busting + 'default_loading_attribute_value' => null, // 'lazy' for lazy loading +]; +``` + +### Custom Path Generator + +```php +use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator; + +class CustomPathGenerator implements PathGenerator +{ + public function getPath(Media $media): string + { + return md5($media->id) . '/'; + } + + public function getPathForConversions(Media $media): string + { + return $this->getPath($media) . 'conversions/'; + } + + public function getPathForResponsiveImages(Media $media): string + { + return $this->getPath($media) . 'responsive/'; + } +} +``` + +### Custom File Namer + +```php +use Spatie\MediaLibrary\Support\FileNamer\FileNamer; + +class CustomFileNamer extends FileNamer +{ + public function originalFileName(string $fileName): string + { + return Str::slug(pathinfo($fileName, PATHINFO_FILENAME)); + } + + public function conversionFileName(string $fileName, Conversion $conversion): string + { + return $this->originalFileName($fileName) . '-' . $conversion->getName(); + } + + public function responsiveFileName(string $fileName): string + { + return pathinfo($fileName, PATHINFO_FILENAME); + } +} +``` + +### Custom Media Model + +```php +use Spatie\MediaLibrary\MediaCollections\Models\Media as BaseMedia; + +class Media extends BaseMedia +{ + // Add custom methods, scopes, or override behavior +} +``` + +Register in config: `'media_model' => App\Models\Media::class` + +## Downloading Media + +### Single file + +```php +return $media->toResponse($request); // download +return $media->toInlineResponse($request); // display inline +return $media->stream(); // stream +``` + +### ZIP download of collection + +```php +use Spatie\MediaLibrary\Support\MediaStream; + +return MediaStream::create('photos.zip') + ->addMedia($model->getMedia('images')); +``` + +## Using with API Resources + +```php +class PostResource extends JsonResource +{ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'image' => $this->getFirstMediaUrl('images'), + 'thumb' => $this->getFirstMediaUrl('images', 'thumb'), + 'media' => $this->getMedia('images')->map(function ($media) { + return [ + 'id' => $media->id, + 'url' => $media->getUrl(), + 'thumb' => $media->getUrl('thumb'), + 'name' => $media->name, + 'size' => $media->size, + 'type' => $media->mime_type, + ]; + }), + ]; + } +} +``` \ No newline at end of file diff --git a/.agents/skills/upgrade-laravel-v13/SKILL.md b/.agents/skills/upgrade-laravel-v13/SKILL.md new file mode 100644 index 0000000000..c7aaa15824 --- /dev/null +++ b/.agents/skills/upgrade-laravel-v13/SKILL.md @@ -0,0 +1,460 @@ +# Laravel 12 to 13 Upgrade Specialist + +You are an expert Laravel upgrade specialist with deep knowledge of both Laravel 12.x and 13.0. Your task is to systematically upgrade the application from Laravel 12 to 13 while ensuring all functionality remains intact. You understand the nuances of breaking changes and can identify affected code patterns with precision. + +## Core Principle: Documentation-First Approach + +**IMPORTANT:** Always use the `search-docs` tool whenever you need: + +- Specific code examples for implementing Laravel 13 features +- Clarification on breaking changes or new behavior +- Verification of upgrade patterns before applying them +- Examples of correct usage for renamed classes or methods + +The official Laravel documentation is your primary source of truth. Consult it before making assumptions or implementing changes. + +## Upgrade Process + +Follow this systematic process to upgrade the application: + +### 1. Assess Current State + +Before making any changes: + +- Check `composer.json` for the current Laravel version constraint +- Run `{{ $assist->composerCommand('show laravel/framework') }}` to confirm installed version +- Identify middleware references to `VerifyCsrfToken` or `ValidateCsrfToken` +- Review `config/cache.php` for serialization settings +- Review `config/session.php` for cookie name configuration + +### 2. Create Safety Net + +- Ensure you're working on a dedicated branch +- Run the existing test suite to establish baseline +- Note any custom cache store implementations or queue driver implementations + +### 3. Analyze Codebase for Breaking Changes + +Search the codebase for patterns affected by v13 changes: + +**High Priority Searches:** + +- `VerifyCsrfToken` or `ValidateCsrfToken` — Must rename to `PreventRequestForgery` +- `composer.json` — Dependency version constraints to update +- `phpunit.xml` or `pest` config — Test framework version compatibility + +**Medium Priority Searches:** + +- `config/cache.php` — Check for `serializable_classes` configuration +- Code that stores PHP objects in cache — May need explicit class allow-lists + +**Low Priority Searches:** + +- `$event->exceptionOccurred` — Renamed to `$event->exception` in `JobAttempted` +- `$event->connection` on `QueueBusy` — Renamed to `$connectionName` +- `pagination::default` or `pagination::simple-default` — View names changed +- `Container::call` with nullable class defaults — Behavior changed +- Manager `extend` callbacks using `$this` — Binding changed +- Custom `Str` factories in tests — Now reset between tests + +### 4. Apply Changes Systematically + +For each category of changes: + +1. **Search** for affected patterns using grep/search tools +2. **Consult documentation** — Use `search-docs` tool to verify correct upgrade patterns and examples +3. **List** all files that need modification +4. **Apply** the fix consistently across all occurrences +5. **Verify** each change doesn't break functionality + +### 5. Update Dependencies + +After code changes are complete: + +```bash +{{ $assist->composerCommand('require laravel/framework:^13.0 --with-all-dependencies') }} +``` + +### 6. Test and Verify + +- Run the full test suite +- Verify CSRF protection still works correctly +- Check cache read/write operations +- Test any queue listeners that reference event properties + +## Execution Strategy + +When upgrading, maximize efficiency by: + +- **Batch similar changes** — Group all CSRF middleware renames, then all config updates, etc. +- **Use parallel agents** for independent file modifications +- **Prioritize high-impact changes** that could cause immediate failures +- **Test incrementally** — Verify after each category of changes + +# Upgrading from Laravel 12.x to 13.0 + +> [!NOTE] +> We attempt to document every possible breaking change. Since some of these breaking changes are in obscure parts of the framework only a portion of these changes may actually affect your application. + +## Updating Dependencies + +**Likelihood Of Impact: High** + +Update the following dependencies in your application's `composer.json` file: + +@boostsnippet('Dependency Updates', 'json') +{ +"require": { +"laravel/framework": "^13.0" +}, +"require-dev": { +"laravel/tinker": "^3.0", +"phpunit/phpunit": "^12.0", +"pestphp/pest": "^4.0" +} +} +@endboostsnippet + +Run the update: + +```bash +{{ $assist->composerCommand('update') }} +``` + +## Updating the Laravel Installer + +If you use the Laravel installer CLI tool, update it for Laravel 13.x compatibility: + +@if($usesHerd) + +```bash +herd laravel:update +``` + +@else + +```bash +{{ $assist->composerCommand('global update laravel/installer') }} +``` + +@endif + +## Cache + +### Cache Prefixes and Session Cookie Names + +**Likelihood Of Impact: Low** + +Laravel's default cache and Redis key prefixes now use hyphenated suffixes. In addition, the default session cookie name now uses `Str::snake(...)` for the application name. + +In most applications, this change will not apply because application-level configuration files already define these values. This primarily affects applications that rely on framework-level fallback configuration when corresponding application config values are not present. + +If your application relies on these generated defaults, cache keys and session cookie names may change after upgrading: + +@boostsnippet('Cache Prefix Changes', 'php') +// Laravel <= 12.x +Str::slug((string) env('APP*NAME', 'laravel'), '*').'_cache_'; +Str::slug((string) env('APP*NAME', 'laravel'), '*').'_database_'; +Str::slug((string) env('APP*NAME', 'laravel'), '*').'\_session'; + +// Laravel >= 13.x +Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'; +Str::slug((string) env('APP_NAME', 'laravel')).'-database-'; +Str::snake((string) env('APP_NAME', 'laravel')).'\_session'; +@endboostsnippet + +To retain previous behavior, explicitly configure `CACHE_PREFIX`, `REDIS_PREFIX`, and `SESSION_COOKIE` in your environment. + +### `Store` and `Repository` Contracts: `touch` + +**Likelihood Of Impact: Very Low** + +The cache contracts now include a `touch` method for extending item TTLs. If you maintain custom cache store implementations, you should add this method: + +@boostsnippet('Cache Store Touch', 'php') +// Illuminate\Contracts\Cache\Store +public function touch($key, $seconds); +@endboostsnippet + +### Cache `serializable_classes` Configuration + +**Likelihood Of Impact: Medium** + +The default application `cache` configuration now includes a `serializable_classes` option set to `false`. This hardens cache unserialization behavior to help prevent PHP deserialization gadget chain attacks if your application's `APP_KEY` is leaked. If your application intentionally stores PHP objects in cache, you should explicitly list the classes that may be unserialized: + +@boostsnippet('Cache Serializable Classes', 'php') +'serializable_classes' => [ +App\Data\CachedDashboardStats::class, +App\Support\CachedPricingSnapshot::class, +], +@endboostsnippet + +If your application previously relied on unserializing arbitrary cached objects, you will need to migrate that usage to explicit class allow-lists or to non-object cache payloads (such as arrays). + +## Container + +### `Container::call` and Nullable Class Defaults + +**Likelihood Of Impact: Low** + +`Container::call` now respects nullable class parameter defaults when no binding exists, matching constructor injection behavior introduced in Laravel 12: + +@boostsnippet('Container Call Nullable', 'php') +$container->call(function (?Carbon $date = null) { +return $date; +}); + +// Laravel <= 12.x: Carbon instance +// Laravel >= 13.x: null +@endboostsnippet + +If your method-call injection logic depended on the previous behavior, you may need to update it. + +## Contracts + +### `Dispatcher` Contract: `dispatchAfterResponse` + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Bus\Dispatcher` contract now includes the `dispatchAfterResponse($command, $handler = null)` method. + +If you maintain a custom dispatcher implementation, add this method to your class. + +### `ResponseFactory` Contract: `eventStream` + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Routing\ResponseFactory` contract now includes an `eventStream` signature. + +If you maintain a custom implementation of this contract, you should add this method. + +### `MustVerifyEmail` Contract: `markEmailAsUnverified` + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Auth\MustVerifyEmail` contract now includes `markEmailAsUnverified()`. + +If you provide a custom implementation of this contract, add this method to remain compatible. + +## Database + +### MySQL `DELETE` Queries With `JOIN`, `ORDER BY`, and `LIMIT` + +**Likelihood Of Impact: Low** + +Laravel now compiles full `DELETE ... JOIN` queries including `ORDER BY` and `LIMIT` for MySQL grammar. + +In previous versions, `ORDER BY` / `LIMIT` clauses could be silently ignored on joined deletes. In Laravel 13, these clauses are included in the generated SQL. As a result, database engines that do not support this syntax (such as standard MySQL / MariaDB variants) may now throw a `QueryException` instead of executing an unbounded delete. + +## Eloquent + +### Model Booting and Nested Instantiation + +**Likelihood Of Impact: Very Low** + +Creating a new model instance while that model is still booting is now disallowed and throws a `LogicException`. + +This affects code that instantiates models from inside model `boot` methods or trait `boot*` methods: + +@boostsnippet('Model Booting', 'php') +protected static function boot() +{ +parent::boot(); + + // No longer allowed during booting... + (new static())->getTable(); + +} +@endboostsnippet + +Move this logic outside the boot cycle to avoid nested booting. + +### Polymorphic Pivot Table Name Generation + +**Likelihood Of Impact: Low** + +When table names are inferred for polymorphic pivot models using custom pivot model classes, Laravel now generates pluralized names. + +If your application depended on the previous singular inferred names for morph pivot tables and used custom pivot classes, you should explicitly define the table name on your pivot model. + +### Collection Model Serialization Restores Eager-Loaded Relations + +**Likelihood Of Impact: Low** + +When Eloquent model collections are serialized and restored (such as in queued jobs), eager-loaded relations are now restored for the collection's models. + +If your code depended on relations not being present after deserialization, you may need to adjust that logic. + +## HTTP Client + +### HTTP Client `Response::throw` and `throwIf` Signatures + +**Likelihood Of Impact: Very Low** + +The HTTP client response methods now declare their callback parameters in the method signatures: + +@boostsnippet('HTTP Client Throw Signatures', 'php') +public function throw($callback = null); +public function throwIf($condition, $callback = null); +@endboostsnippet + +If you override these methods in custom response classes, ensure your method signatures are compatible. + +## Notifications + +### Default Password Reset Subject + +**Likelihood Of Impact: Very Low** + +Laravel's default password reset mail subject has changed: + +@boostsnippet('Password Reset Subject', 'text') +// Laravel <= 12.x +Reset Password Notification + +// Laravel >= 13.x +Reset your password +@endboostsnippet + +If your tests, assertions, or translation overrides depend on the previous default string, update them accordingly. + +### Queued Notifications and Missing Models + +**Likelihood Of Impact: Very Low** + +Queued notifications now respect the `#[DeleteWhenMissingModels]` attribute and `$deleteWhenMissingModels` property defined on the notification class. + +In previous versions, missing models could still cause queued notification jobs to fail in cases where you expected them to be deleted. + +## Queue + +### `JobAttempted` Event Exception Payload + +**Likelihood Of Impact: Low** + +The `Illuminate\Queue\Events\JobAttempted` event now exposes the exception object (or `null`) via `$exception`, replacing the previous boolean `$exceptionOccurred` property: + +@boostsnippet('JobAttempted Event', 'php') +// Laravel <= 12.x +$event->exceptionOccurred; + +// Laravel >= 13.x +$event->exception; +@endboostsnippet + +If you listen for this event, update your listener code accordingly. + +### `QueueBusy` Event Property Rename + +**Likelihood Of Impact: Low** + +The `Illuminate\Queue\Events\QueueBusy` event property `$connection` has been renamed to `$connectionName` for consistency with other queue events. + +If your listeners reference `$connection`, update them to `$connectionName`. + +### `Queue` Contract Method Additions + +**Likelihood Of Impact: Very Low** + +The `Illuminate\Contracts\Queue\Queue` contract now includes queue size inspection methods that were previously only declared in docblocks. + +If you maintain custom queue driver implementations of this contract, add implementations for: + +- `pendingSize` +- `delayedSize` +- `reservedSize` +- `creationTimeOfOldestPendingJob` + +## Routing + +### Domain Route Registration Precedence + +**Likelihood Of Impact: Low** + +Routes with an explicit domain are now prioritized before non-domain routes in route matching. + +This allows catch-all subdomain routes to behave consistently even when non-domain routes are registered earlier. If your application relied on previous registration precedence between domain and non-domain routes, review route matching behavior. + +## Scheduling + +### `withScheduling` Registration Timing + +**Likelihood Of Impact: Very Low** + +Schedules registered via `ApplicationBuilder::withScheduling()` are now deferred until `Schedule` is resolved. + +If your application relied on immediate schedule registration timing during bootstrap, you may need to adjust that logic. + +## Security + +### Request Forgery Protection + +**Likelihood Of Impact: High** + +Laravel's CSRF middleware has been renamed from `VerifyCsrfToken` to `PreventRequestForgery`, and now includes request-origin verification using the `Sec-Fetch-Site` header. + +`VerifyCsrfToken` and `ValidateCsrfToken` remain as deprecated aliases, but direct references should be updated to `PreventRequestForgery`, especially when excluding middleware in tests or route definitions: + +@boostsnippet('CSRF Middleware Rename', 'php') +use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; +use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; + +// Laravel <= 12.x +->withoutMiddleware([VerifyCsrfToken::class]); + +// Laravel >= 13.x +->withoutMiddleware([PreventRequestForgery::class]); +@endboostsnippet + +The middleware configuration API now also provides `preventRequestForgery(...)`. + +## Support + +### Manager `extend` Callback Binding + +**Likelihood Of Impact: Low** + +Custom driver closures registered via manager `extend` methods are now bound to the manager instance. + +If you previously relied on another bound object (such as a service provider instance) as `$this` inside these callbacks, you should move those values into closure captures using `use (...)`. + +### `Str` Factories Reset Between Tests + +**Likelihood Of Impact: Low** + +Laravel now resets custom `Str` factories during test teardown. + +If your tests depended on custom UUID / ULID / random string factories persisting between test methods, you should set them in each relevant test or setup hook. + +### `Js::from` Uses Unescaped Unicode By Default + +**Likelihood Of Impact: Very Low** + +`Illuminate\Support\Js::from` now uses `JSON_UNESCAPED_UNICODE` by default. + +If your tests or frontend output comparisons depended on escaped Unicode sequences (for example `\u00e8`), update your expectations. + +## Views + +### Pagination Bootstrap View Names + +**Likelihood Of Impact: Low** + +The internal pagination view names for Bootstrap 3 defaults are now explicit: + +@boostsnippet('Pagination Views', 'text') +// Laravel <= 12.x +pagination::default +pagination::simple-default + +// Laravel >= 13.x +pagination::bootstrap-3 +pagination::simple-bootstrap-3 +@endboostsnippet + +## Getting help + +If you encounter issues during the upgrade: + +- Check the [upgrade guide](https://laravel.com/docs/13.x/upgrade) for the latest details +- Review the [GitHub comparison](https://github.com/laravel/laravel/compare/12.x...13.x) for skeleton changes diff --git a/.ai/rules/analytics.md b/.ai/rules/analytics.md new file mode 100644 index 0000000000..cae4d5d8e8 --- /dev/null +++ b/.ai/rules/analytics.md @@ -0,0 +1,9 @@ +--- +paths: + - 'app/Actions/Analytics/**' +--- + +# Analytics + +## Keep analytics reads in Actions +Analytics dashboard and post-metric database reads live in app/Actions/Analytics alongside the existing analytics workflows. Do not introduce an app/Queries layer. Keep workspace report orchestration separate from publication and follower aggregation. diff --git a/.ai/rules/app.md b/.ai/rules/app.md new file mode 100644 index 0000000000..507e62c7ac --- /dev/null +++ b/.ai/rules/app.md @@ -0,0 +1,12 @@ +--- +paths: + - 'app/**' +--- + +# App + +## Reuse model scopes for canonical state filters +When a model already exposes a scope for a recurring state filter, jobs, commands, services, observers, and controllers must use that scope instead of repeating raw where clauses. Add a descriptive model scope when a canonical state condition will be reused (for example SocialAccount::connected()->active()). + +## Prefer Eloquent/query builder over raw SQL +Always use Eloquent and query-builder methods (where*, orderBy*, latest, scopes, relations) instead of hand-written SQL (orderByRaw, whereRaw, selectRaw, DB::raw). Only fall back to raw SQL when the builder genuinely has no equivalent, and then keep it in one named model scope (e.g. Post::scopeOrderBySentAt) so the raw expression lives in a single place. User decision, 2026-10-02. diff --git a/.ai/rules/auth.md b/.ai/rules/auth.md index 3180d9fd16..a7a4334183 100644 --- a/.ai/rules/auth.md +++ b/.ai/rules/auth.md @@ -5,8 +5,8 @@ paths: # Auth -## Clear google_business_oauth on every terminal connect path -google_business_oauth holds access and refresh tokens until the user picks a location. Forget it on every terminal select() exit, including the generic Exception catch, and in ConnectPopupException::render() alongside social_connect_workspace. Leaving it after uploadFromUrl / fetchLocationPhoto / connectIdentity failures leaks tokens into the session. On reconnect, keep the existing refresh_token when Google omits a new one. +## Connection tokens live only in the pending connection +Google Business (like every redirect network) keeps its access and refresh tokens inside `PendingConnection` (session key `social_connect`) between the callback and "Finish connection", never in a separate session bag. Every stop goes through `failConnection()` / `ConnectFlowException`, whose `fail()` drops the identities and their tokens; a successful finish forgets the whole pending connection. The location photo is read in `accountValues()` only for the locations the user picked, and a reconnect keeps the existing refresh_token when Google omits a new one. -## Forget google_business_oauth on every popupCallback -Override popupCallback to forgetOauthSession() so single-location success and every error path drop the token bag. Do not forget before the multi-location redirect to the picker — that is the only request that still needs it. select() finally and ConnectPopupException::render() also forget; a second forget is fine. +## Finish lands on the channel, return_to only serves exits +"Finish connection" (`SocialController@finish`) always redirects to `app.channels.publish` of the connected channel (the new account, or the first one when reconnecting or refreshing) and flashes `connectedChannel` so the posting-goal dialog opens there for a new channel. The `return_to` target kept in `PendingConnection` is used only by close (X), Back, cancel and the error states (`backUrl`, "Try again"), never after a successful finish. diff --git a/.ai/rules/collectors.md b/.ai/rules/collectors.md new file mode 100644 index 0000000000..e48793db7e --- /dev/null +++ b/.ai/rules/collectors.md @@ -0,0 +1,9 @@ +--- +paths: + - 'app/Services/Analytics/Collectors/**' +--- + +# Collectors + +## Analytics collectors read tokens through AnalyticsAccessToken +Never send $account->access_token directly from a collector client. AbstractApiPublicationCollector and AbstractFollowerCollector go through AnalyticsAccessToken::for(), which refreshes an expired token (YouTube lives 1h) and maps TokenExpired/PlatformUnavailable to authentication/transient. Optional reads (Meta insights, YouTube Analytics report, Facebook comments) must tolerate permission/malformed and keep whatever else was measured; engagement_rate per post is derived on read in ReadPublicationAnalytics (engagements / exposure * 100), never stored by collectors except Pinterest-style provider values. diff --git a/.ai/rules/editor.md b/.ai/rules/editor.md new file mode 100644 index 0000000000..0a620dab6e --- /dev/null +++ b/.ai/rules/editor.md @@ -0,0 +1,12 @@ +--- +paths: + - 'resources/js/components/posts/editor/**' +--- + +# Editor + +## Per-network composer settings are label/control rows; content type lives outside them +Every *Settings.vue renders only a SettingsSection of SettingsRow (130px label column, control on the right) and never its own collapsible card, header or "posting to" block (TikTok keeps a Posting-to row: its UX guidelines require the creator name). The content-type picker is ContentTypeRadioGroup, mounted by the caller (ComposerNetworkCard / ChannelConfigurator) from getContentTypeOptions(); media warnings go through ChannelMediaWarnings. Do not re-add variant pickers inside a settings component. + +## The AI-generated row is shared +The AI-generated disclosure row is the shared AiGeneratedRow, rendered last by ComposerNetworkSettings for every network that supports it. Do not add it inside a per-network settings component. diff --git a/.ai/rules/google-business.md b/.ai/rules/google-business.md index c83ffef0f2..ba23713b9d 100644 --- a/.ai/rules/google-business.md +++ b/.ai/rules/google-business.md @@ -6,7 +6,6 @@ paths: - app/Console/Commands/ReconcileGoogleBusinessPosts.php - app/Services/Social/GoogleBusinessPublisher.php - app/Support/PostPlatformMetaRules.php - - app/Services/Social/GoogleBusinessAnalytics.php --- # Google Business @@ -23,8 +22,11 @@ LocalPost event.title is capped at TopicType::TITLE_MAX_LENGTH (58) — Google r ## GBP reconcile refreshes on 401 ReconcileGoogleBusinessPost must call ConnectionVerifier::verify() after a TokenExpiredException from fetchLocalPost and retry the GET once. If refresh also fails, mark the SocialAccount token-expired, then deferOrGiveUp. Do not park a 401 in pending_review for 24h while a refresh would settle it. RecoverStuckPosts times the 24h ceiling from submitted_at only — never created_at. -## GBP analytics must not cache a failed fetch -GoogleBusinessAnalytics caches only a successful array. An HTTP failure or a missing location returns false and is not written to cache — a 500 must not blank the dashboard for an hour. Publish/verify/analytics require both location_id (v4) and location_name (v1) via GoogleBusinessResourceName::connectedLocation(). +## GBP calls need both location ids +Publish and verify require both location_id (v4) and location_name (v1) via GoogleBusinessResourceName::connectedLocation(). ## Local Posts 401 after a live BI verify does not expire the account retryAfterExpiredToken calls ConnectionVerifier::verify() then retries fetchRemote. markAsTokenExpired only when verify() itself throws TokenExpiredException. If BI verify succeeds and Local Posts still 401s, deferOrGiveUp without disconnecting — the token still works for the house verify. ReconcileGoogleBusinessPosts only dispatches enabled() pending_review rows. ReconcileGoogleBusinessPost::handle() giveUps immediately when socialAccount is null — disconnect nulls the FK and fetchRemote would TypeError until the 24h ceiling. + +## Pre-2.0 scheduled targets skip the publish-time media recheck +post_platforms.scheduled_before_media_checks is set only by migration 2026_10_06_112101 on targets that were pending/publishing/retrying on a scheduled, pending approval or publishing post at deploy. failForInvalidMedia() returns early for them so they publish what main published (main had no publish-time media check; publishers truncate to first/take(n) and networks accept wider video ratios). Never set it from app code; new posts, drafts and failed targets always get the 2.0 recheck. Remove the column once no marked unpublished target is left. diff --git a/.ai/rules/index.md b/.ai/rules/index.md index fdfdcdd24e..9bca2c64b3 100644 --- a/.ai/rules/index.md +++ b/.ai/rules/index.md @@ -4,13 +4,22 @@ Before planning or editing, find the row whose globs match the file's path and r | Applies to | Rule file | | --- | --- | +| app/Actions/Analytics/** | .ai/rules/analytics.md | +| app/** | .ai/rules/app.md | | app/Http/Controllers/Auth/GoogleBusinessController.php | .ai/rules/auth.md | -| app/Enums/GoogleBusiness/**, app/Jobs/PublishToSocialPlatform.php, app/Jobs/ReconcileGoogleBusinessPost.php, app/Console/Commands/ReconcileGoogleBusinessPosts.php, app/Services/Social/GoogleBusinessPublisher.php, app/Support/PostPlatformMetaRules.php, app/Services/Social/GoogleBusinessAnalytics.php | .ai/rules/google-business.md | +| app/Services/Analytics/Collectors/** | .ai/rules/collectors.md | +| resources/js/components/posts/editor/** | .ai/rules/editor.md | +| app/Enums/GoogleBusiness/**, app/Jobs/PublishToSocialPlatform.php, app/Jobs/ReconcileGoogleBusinessPost.php, app/Console/Commands/ReconcileGoogleBusinessPosts.php, app/Services/Social/GoogleBusinessPublisher.php, app/Support/PostPlatformMetaRules.php | .ai/rules/google-business.md | | app/Listeners/StripeEventListener.php, app/Jobs/PostHog/TrackBilling.php, app/Jobs/PostHog/SyncAccountUsage.php | .ai/rules/jobs-post-hog.md | -| app/Jobs/ReconcileGoogleBusinessPost.php, app/Console/Commands/RecoverStuckPosts.php | .ai/rules/jobs.md | -| app/Jobs/PostHog/**, app/Models/PostPlatform.php, database/migrations/** | .ai/rules/migrations.md | +| app/Jobs/SendNotification.php, app/Jobs/ReconcileGoogleBusinessPost.php, app/Console/Commands/RecoverStuckPosts.php | .ai/rules/jobs.md | +| resources/js/** | .ai/rules/js.md | +| app/Actions/Media/**, app/Models/Media.php | .ai/rules/media.md | +| app/Http/Middleware/App/HandleInertiaRequests.php | .ai/rules/middleware-app.md | +| app/Jobs/PostHog/**, app/Models/PostPlatform.php, database/migrations/**, app/Models/SocialAccount.php | .ai/rules/migrations.md | | app/Jobs/PostHog/** | .ai/rules/post-hog.md | -| app/Actions/Post/FinalizePostPublication.php, app/Jobs/PublishPost.php, app/Actions/Post/UpdatePost.php | .ai/rules/post.md | +| app/Enums/PostPlatform/ContentType.php | .ai/rules/post-platform.md | +| app/Actions/Post/FinalizePostPublication.php, app/Jobs/PublishPost.php, app/Actions/Post/UpdatePost.php, app/Actions/Post/** | .ai/rules/post.md | | app/Enums/SocialAccount/Platform.php | .ai/rules/social-account.md | -| app/Services/Social/GoogleBusinessPublisher.php, app/Support/Social/GoogleBusinessDerivativeCleaner.php, app/Actions/Post/DeletePost.php, app/Actions/Post/UpdatePost.php, app/Support/Social/AbandonGoogleBusinessReview.php, app/Actions/Workspace/PurgeWorkspace.php, app/Http/Controllers/Auth/SocialController.php | .ai/rules/social.md | +| app/Services/Social/GoogleBusinessPublisher.php, app/Support/Social/GoogleBusinessDerivativeCleaner.php, app/Actions/Post/DeletePost.php, app/Actions/Post/UpdatePost.php, app/Support/Social/AbandonGoogleBusinessReview.php, app/Actions/Workspace/PurgeWorkspace.php, app/Http/Controllers/Auth/SocialController.php, app/Support/Social/ThreadProgress.php, app/Support/ThreadReplies.php, app/Services/Social/Concerns/PublishesThreads.php, app/Services/Social/FacebookPublisher.php, app/Services/Social/InstagramPublisher.php | .ai/rules/social.md | | app/Support/PostPlatformMetaRules.php | .ai/rules/support.md | +| app/Mcp/Tools/** | .ai/rules/tools.md | diff --git a/.ai/rules/jobs.md b/.ai/rules/jobs.md index bcf2d94bb0..faada987e2 100644 --- a/.ai/rules/jobs.md +++ b/.ai/rules/jobs.md @@ -1,5 +1,6 @@ --- paths: + - app/Jobs/SendNotification.php - app/Jobs/ReconcileGoogleBusinessPost.php - app/Console/Commands/RecoverStuckPosts.php --- @@ -8,3 +9,6 @@ paths: ## GBP reconcile must settle permanent API errors ReconcileGoogleBusinessPost must not rethrow GoogleBusinessPublishException. Permanent categories (NOT_FOUND, PERMISSION_DENIED, INVALID_ARGUMENT, Unknown) reject the target immediately and call FinalizePostPublication. Only ServerError, RateLimit, and ConnectionException defer until REVIEW_CEILING_HOURS. Throwing leaves last_reconciled_at stale, the 5-minute sweep re-dispatches, RecoverStuckPosts treats PendingReview as still-active, and the post sticks forever. failed() is the safety net if the worker dies mid-review — it must deferOrGiveUp (respect the 24h ceiling), never giveUp immediately. Job $timeout must exceed HasSocialHttpClient's 120s HTTP timeout. RecoverStuckPosts may only fail PendingReview after the same 24h ceiling timed from submitted_at — never created_at (a scheduled draft can be days old before it enters review) and never the 1h publishing timeout. Reconcile must refresh-and-retry on TokenExpiredException the same way PublishToSocialPlatform does; if verify() itself dies, mark the social account token-expired before deferring. If Business Information verify succeeds and Local Posts still 401s, defer without markAsTokenExpired — the token is still valid for the house verify. A 401 must not sit in review for 24h while a refresh would have settled it. JPEG derivatives stay on disk while LocalPostState is Processing/Scheduled so Google can fetch sourceUrl; settle(), RecoverStuckPosts' 1h publishing timeout, a disabled GBP target, and the expired-review path prune them. Reconcile must giveUp immediately when socialAccount is null (disconnect nulls the FK and would TypeError in fetchRemote). When RecoverStuckPosts finishes a post (no still-active targets), call FinalizePostPublication so the owner is notified — do not mark the post Failed/Published by hand. Never compare raw PROCESSING/SCHEDULED/LIVE strings — use App\Enums\GoogleBusiness\LocalPostState. + +## Notifications are email only +There is no in-app notification system: the `notifications` table, bell, broadcast event, presence heartbeat and WorkspaceUserChannel were removed in September 2026. SendNotification(user, type, mailable) only sends the Mailable, gated by User::wantsEmailFor(). User uses RoutesNotifications, not Notifiable (no database channel). A new post note (not an edit, delete or reaction) emails every workspace member except its author (NotifyPostNoteAdded → PostNoteAdded, Type::PostNoteAdded); there are no @mentions in notes. Do not reintroduce an in-app channel. diff --git a/.ai/rules/js.md b/.ai/rules/js.md new file mode 100644 index 0000000000..d9bb368f2a --- /dev/null +++ b/.ai/rules/js.md @@ -0,0 +1,9 @@ +--- +paths: + - 'resources/js/**' +--- + +# Js + +## Never resolve translations in script-level computed/const +laravel-vue-i18n loads the locale JSON asynchronously and `trans()` is not reactive. A `trans()` call in ` -