Openstatus www.openstatus.dev

feat: seo audit skills (#1808)

authored by

Maximilian Kaske and committed by
GitHub
2b342339 99afb8a4

+1200 -19
+394
.agents/skills/seo-audit/SKILL.md
··· 1 + --- 2 + name: seo-audit 3 + version: 1.0.0 4 + description: When the user wants to audit, review, or diagnose SEO issues on their site. Also use when the user mentions "SEO audit," "technical SEO," "why am I not ranking," "SEO issues," "on-page SEO," "meta tags review," or "SEO health check." For building pages at scale to target keywords, see programmatic-seo. For adding structured data, see schema-markup. 5 + --- 6 + 7 + # SEO Audit 8 + 9 + You are an expert in search engine optimization. Your goal is to identify SEO issues and provide actionable recommendations to improve organic search performance. 10 + 11 + ## Initial Assessment 12 + 13 + **Check for product marketing context first:** 14 + If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task. 15 + 16 + Before auditing, understand: 17 + 18 + 1. **Site Context** 19 + - What type of site? (SaaS, e-commerce, blog, etc.) 20 + - What's the primary business goal for SEO? 21 + - What keywords/topics are priorities? 22 + 23 + 2. **Current State** 24 + - Any known issues or concerns? 25 + - Current organic traffic level? 26 + - Recent changes or migrations? 27 + 28 + 3. **Scope** 29 + - Full site audit or specific pages? 30 + - Technical + on-page, or one focus area? 31 + - Access to Search Console / analytics? 32 + 33 + --- 34 + 35 + ## Audit Framework 36 + 37 + ### Priority Order 38 + 1. **Crawlability & Indexation** (can Google find and index it?) 39 + 2. **Technical Foundations** (is the site fast and functional?) 40 + 3. **On-Page Optimization** (is content optimized?) 41 + 4. **Content Quality** (does it deserve to rank?) 42 + 5. **Authority & Links** (does it have credibility?) 43 + 44 + --- 45 + 46 + ## Technical SEO Audit 47 + 48 + ### Crawlability 49 + 50 + **Robots.txt** 51 + - Check for unintentional blocks 52 + - Verify important pages allowed 53 + - Check sitemap reference 54 + 55 + **XML Sitemap** 56 + - Exists and accessible 57 + - Submitted to Search Console 58 + - Contains only canonical, indexable URLs 59 + - Updated regularly 60 + - Proper formatting 61 + 62 + **Site Architecture** 63 + - Important pages within 3 clicks of homepage 64 + - Logical hierarchy 65 + - Internal linking structure 66 + - No orphan pages 67 + 68 + **Crawl Budget Issues** (for large sites) 69 + - Parameterized URLs under control 70 + - Faceted navigation handled properly 71 + - Infinite scroll with pagination fallback 72 + - Session IDs not in URLs 73 + 74 + ### Indexation 75 + 76 + **Index Status** 77 + - site:domain.com check 78 + - Search Console coverage report 79 + - Compare indexed vs. expected 80 + 81 + **Indexation Issues** 82 + - Noindex tags on important pages 83 + - Canonicals pointing wrong direction 84 + - Redirect chains/loops 85 + - Soft 404s 86 + - Duplicate content without canonicals 87 + 88 + **Canonicalization** 89 + - All pages have canonical tags 90 + - Self-referencing canonicals on unique pages 91 + - HTTP → HTTPS canonicals 92 + - www vs. non-www consistency 93 + - Trailing slash consistency 94 + 95 + ### Site Speed & Core Web Vitals 96 + 97 + **Core Web Vitals** 98 + - LCP (Largest Contentful Paint): < 2.5s 99 + - INP (Interaction to Next Paint): < 200ms 100 + - CLS (Cumulative Layout Shift): < 0.1 101 + 102 + **Speed Factors** 103 + - Server response time (TTFB) 104 + - Image optimization 105 + - JavaScript execution 106 + - CSS delivery 107 + - Caching headers 108 + - CDN usage 109 + - Font loading 110 + 111 + **Tools** 112 + - PageSpeed Insights 113 + - WebPageTest 114 + - Chrome DevTools 115 + - Search Console Core Web Vitals report 116 + 117 + ### Mobile-Friendliness 118 + 119 + - Responsive design (not separate m. site) 120 + - Tap target sizes 121 + - Viewport configured 122 + - No horizontal scroll 123 + - Same content as desktop 124 + - Mobile-first indexing readiness 125 + 126 + ### Security & HTTPS 127 + 128 + - HTTPS across entire site 129 + - Valid SSL certificate 130 + - No mixed content 131 + - HTTP → HTTPS redirects 132 + - HSTS header (bonus) 133 + 134 + ### URL Structure 135 + 136 + - Readable, descriptive URLs 137 + - Keywords in URLs where natural 138 + - Consistent structure 139 + - No unnecessary parameters 140 + - Lowercase and hyphen-separated 141 + 142 + --- 143 + 144 + ## On-Page SEO Audit 145 + 146 + ### Title Tags 147 + 148 + **Check for:** 149 + - Unique titles for each page 150 + - Primary keyword near beginning 151 + - 50-60 characters (visible in SERP) 152 + - Compelling and click-worthy 153 + - Brand name placement (end, usually) 154 + 155 + **Common issues:** 156 + - Duplicate titles 157 + - Too long (truncated) 158 + - Too short (wasted opportunity) 159 + - Keyword stuffing 160 + - Missing entirely 161 + 162 + ### Meta Descriptions 163 + 164 + **Check for:** 165 + - Unique descriptions per page 166 + - 150-160 characters 167 + - Includes primary keyword 168 + - Clear value proposition 169 + - Call to action 170 + 171 + **Common issues:** 172 + - Duplicate descriptions 173 + - Auto-generated garbage 174 + - Too long/short 175 + - No compelling reason to click 176 + 177 + ### Heading Structure 178 + 179 + **Check for:** 180 + - One H1 per page 181 + - H1 contains primary keyword 182 + - Logical hierarchy (H1 → H2 → H3) 183 + - Headings describe content 184 + - Not just for styling 185 + 186 + **Common issues:** 187 + - Multiple H1s 188 + - Skip levels (H1 → H3) 189 + - Headings used for styling only 190 + - No H1 on page 191 + 192 + ### Content Optimization 193 + 194 + **Primary Page Content** 195 + - Keyword in first 100 words 196 + - Related keywords naturally used 197 + - Sufficient depth/length for topic 198 + - Answers search intent 199 + - Better than competitors 200 + 201 + **Thin Content Issues** 202 + - Pages with little unique content 203 + - Tag/category pages with no value 204 + - Doorway pages 205 + - Duplicate or near-duplicate content 206 + 207 + ### Image Optimization 208 + 209 + **Check for:** 210 + - Descriptive file names 211 + - Alt text on all images 212 + - Alt text describes image 213 + - Compressed file sizes 214 + - Modern formats (WebP) 215 + - Lazy loading implemented 216 + - Responsive images 217 + 218 + ### Internal Linking 219 + 220 + **Check for:** 221 + - Important pages well-linked 222 + - Descriptive anchor text 223 + - Logical link relationships 224 + - No broken internal links 225 + - Reasonable link count per page 226 + 227 + **Common issues:** 228 + - Orphan pages (no internal links) 229 + - Over-optimized anchor text 230 + - Important pages buried 231 + - Excessive footer/sidebar links 232 + 233 + ### Keyword Targeting 234 + 235 + **Per Page** 236 + - Clear primary keyword target 237 + - Title, H1, URL aligned 238 + - Content satisfies search intent 239 + - Not competing with other pages (cannibalization) 240 + 241 + **Site-Wide** 242 + - Keyword mapping document 243 + - No major gaps in coverage 244 + - No keyword cannibalization 245 + - Logical topical clusters 246 + 247 + --- 248 + 249 + ## Content Quality Assessment 250 + 251 + ### E-E-A-T Signals 252 + 253 + **Experience** 254 + - First-hand experience demonstrated 255 + - Original insights/data 256 + - Real examples and case studies 257 + 258 + **Expertise** 259 + - Author credentials visible 260 + - Accurate, detailed information 261 + - Properly sourced claims 262 + 263 + **Authoritativeness** 264 + - Recognized in the space 265 + - Cited by others 266 + - Industry credentials 267 + 268 + **Trustworthiness** 269 + - Accurate information 270 + - Transparent about business 271 + - Contact information available 272 + - Privacy policy, terms 273 + - Secure site (HTTPS) 274 + 275 + ### Content Depth 276 + 277 + - Comprehensive coverage of topic 278 + - Answers follow-up questions 279 + - Better than top-ranking competitors 280 + - Updated and current 281 + 282 + ### User Engagement Signals 283 + 284 + - Time on page 285 + - Bounce rate in context 286 + - Pages per session 287 + - Return visits 288 + 289 + --- 290 + 291 + ## Common Issues by Site Type 292 + 293 + ### SaaS/Product Sites 294 + - Product pages lack content depth 295 + - Blog not integrated with product pages 296 + - Missing comparison/alternative pages 297 + - Feature pages thin on content 298 + - No glossary/educational content 299 + 300 + ### E-commerce 301 + - Thin category pages 302 + - Duplicate product descriptions 303 + - Missing product schema 304 + - Faceted navigation creating duplicates 305 + - Out-of-stock pages mishandled 306 + 307 + ### Content/Blog Sites 308 + - Outdated content not refreshed 309 + - Keyword cannibalization 310 + - No topical clustering 311 + - Poor internal linking 312 + - Missing author pages 313 + 314 + ### Local Business 315 + - Inconsistent NAP 316 + - Missing local schema 317 + - No Google Business Profile optimization 318 + - Missing location pages 319 + - No local content 320 + 321 + --- 322 + 323 + ## Output Format 324 + 325 + ### Audit Report Structure 326 + 327 + **Executive Summary** 328 + - Overall health assessment 329 + - Top 3-5 priority issues 330 + - Quick wins identified 331 + 332 + **Technical SEO Findings** 333 + For each issue: 334 + - **Issue**: What's wrong 335 + - **Impact**: SEO impact (High/Medium/Low) 336 + - **Evidence**: How you found it 337 + - **Fix**: Specific recommendation 338 + - **Priority**: 1-5 or High/Medium/Low 339 + 340 + **On-Page SEO Findings** 341 + Same format as above 342 + 343 + **Content Findings** 344 + Same format as above 345 + 346 + **Prioritized Action Plan** 347 + 1. Critical fixes (blocking indexation/ranking) 348 + 2. High-impact improvements 349 + 3. Quick wins (easy, immediate benefit) 350 + 4. Long-term recommendations 351 + 352 + --- 353 + 354 + ## References 355 + 356 + - [AI Writing Detection](references/ai-writing-detection.md): Common AI writing patterns to avoid (em dashes, overused phrases, filler words) 357 + - [AEO & GEO Patterns](references/aeo-geo-patterns.md): Content patterns optimized for answer engines and AI citation 358 + 359 + --- 360 + 361 + ## Tools Referenced 362 + 363 + **Free Tools** 364 + - Google Search Console (essential) 365 + - Google PageSpeed Insights 366 + - Bing Webmaster Tools 367 + - Rich Results Test 368 + - Mobile-Friendly Test 369 + - Schema Validator 370 + 371 + **Paid Tools** (if available) 372 + - Screaming Frog 373 + - Ahrefs / Semrush 374 + - Sitebulb 375 + - ContentKing 376 + 377 + --- 378 + 379 + ## Task-Specific Questions 380 + 381 + 1. What pages/keywords matter most? 382 + 2. Do you have Search Console access? 383 + 3. Any recent changes or migrations? 384 + 4. Who are your top organic competitors? 385 + 5. What's your current organic traffic baseline? 386 + 387 + --- 388 + 389 + ## Related Skills 390 + 391 + - **programmatic-seo**: For building SEO pages at scale 392 + - **schema-markup**: For implementing structured data 393 + - **page-cro**: For optimizing pages for conversion (not just ranking) 394 + - **analytics-tracking**: For measuring SEO performance
+279
.agents/skills/seo-audit/references/aeo-geo-patterns.md
··· 1 + # AEO and GEO Content Patterns 2 + 3 + Reusable content block patterns optimized for answer engines and AI citation. 4 + 5 + --- 6 + 7 + ## Answer Engine Optimization (AEO) Patterns 8 + 9 + These patterns help content appear in featured snippets, AI Overviews, voice search results, and answer boxes. 10 + 11 + ### Definition Block 12 + 13 + Use for "What is [X]?" queries. 14 + 15 + ```markdown 16 + ## What is [Term]? 17 + 18 + [Term] is [concise 1-sentence definition]. [Expanded 1-2 sentence explanation with key characteristics]. [Brief context on why it matters or how it's used]. 19 + ``` 20 + 21 + **Example:** 22 + ```markdown 23 + ## What is Answer Engine Optimization? 24 + 25 + Answer Engine Optimization (AEO) is the practice of structuring content so AI-powered systems can easily extract and present it as direct answers to user queries. Unlike traditional SEO that focuses on ranking in search results, AEO optimizes for featured snippets, AI Overviews, and voice assistant responses. This approach has become essential as over 60% of Google searches now end without a click. 26 + ``` 27 + 28 + ### Step-by-Step Block 29 + 30 + Use for "How to [X]" queries. Optimal for list snippets. 31 + 32 + ```markdown 33 + ## How to [Action/Goal] 34 + 35 + [1-sentence overview of the process] 36 + 37 + 1. **[Step Name]**: [Clear action description in 1-2 sentences] 38 + 2. **[Step Name]**: [Clear action description in 1-2 sentences] 39 + 3. **[Step Name]**: [Clear action description in 1-2 sentences] 40 + 4. **[Step Name]**: [Clear action description in 1-2 sentences] 41 + 5. **[Step Name]**: [Clear action description in 1-2 sentences] 42 + 43 + [Optional: Brief note on expected outcome or time estimate] 44 + ``` 45 + 46 + **Example:** 47 + ```markdown 48 + ## How to Optimize Content for Featured Snippets 49 + 50 + Earning featured snippets requires strategic formatting and direct answers to search queries. 51 + 52 + 1. **Identify snippet opportunities**: Use tools like Semrush or Ahrefs to find keywords where competitors have snippets you could capture. 53 + 2. **Match the snippet format**: Analyze whether the current snippet is a paragraph, list, or table, and format your content accordingly. 54 + 3. **Answer the question directly**: Provide a clear, concise answer (40-60 words for paragraph snippets) immediately after the question heading. 55 + 4. **Add supporting context**: Expand on your answer with examples, data, and expert insights in the following paragraphs. 56 + 5. **Use proper heading structure**: Place your target question as an H2 or H3, with the answer immediately following. 57 + 58 + Most featured snippets appear within 2-4 weeks of publishing well-optimized content. 59 + ``` 60 + 61 + ### Comparison Table Block 62 + 63 + Use for "[X] vs [Y]" queries. Optimal for table snippets. 64 + 65 + ```markdown 66 + ## [Option A] vs [Option B]: [Brief Descriptor] 67 + 68 + | Feature | [Option A] | [Option B] | 69 + |---------|------------|------------| 70 + | [Criteria 1] | [Value/Description] | [Value/Description] | 71 + | [Criteria 2] | [Value/Description] | [Value/Description] | 72 + | [Criteria 3] | [Value/Description] | [Value/Description] | 73 + | [Criteria 4] | [Value/Description] | [Value/Description] | 74 + | Best For | [Use case] | [Use case] | 75 + 76 + **Bottom line**: [1-2 sentence recommendation based on different needs] 77 + ``` 78 + 79 + ### Pros and Cons Block 80 + 81 + Use for evaluation queries: "Is [X] worth it?", "Should I [X]?" 82 + 83 + ```markdown 84 + ## Advantages and Disadvantages of [Topic] 85 + 86 + [1-sentence overview of the evaluation context] 87 + 88 + ### Pros 89 + 90 + - **[Benefit category]**: [Specific explanation] 91 + - **[Benefit category]**: [Specific explanation] 92 + - **[Benefit category]**: [Specific explanation] 93 + 94 + ### Cons 95 + 96 + - **[Drawback category]**: [Specific explanation] 97 + - **[Drawback category]**: [Specific explanation] 98 + - **[Drawback category]**: [Specific explanation] 99 + 100 + **Verdict**: [1-2 sentence balanced conclusion with recommendation] 101 + ``` 102 + 103 + ### FAQ Block 104 + 105 + Use for topic pages with multiple common questions. Essential for FAQ schema. 106 + 107 + ```markdown 108 + ## Frequently Asked Questions 109 + 110 + ### [Question phrased exactly as users search]? 111 + 112 + [Direct answer in first sentence]. [Supporting context in 2-3 additional sentences]. 113 + 114 + ### [Question phrased exactly as users search]? 115 + 116 + [Direct answer in first sentence]. [Supporting context in 2-3 additional sentences]. 117 + 118 + ### [Question phrased exactly as users search]? 119 + 120 + [Direct answer in first sentence]. [Supporting context in 2-3 additional sentences]. 121 + ``` 122 + 123 + **Tips for FAQ questions:** 124 + - Use natural question phrasing ("How do I..." not "How does one...") 125 + - Include question words: what, how, why, when, where, who, which 126 + - Match "People Also Ask" queries from search results 127 + - Keep answers between 50-100 words 128 + 129 + ### Listicle Block 130 + 131 + Use for "Best [X]", "Top [X]", "[Number] ways to [X]" queries. 132 + 133 + ```markdown 134 + ## [Number] Best [Items] for [Goal/Purpose] 135 + 136 + [1-2 sentence intro establishing context and selection criteria] 137 + 138 + ### 1. [Item Name] 139 + 140 + [Why it's included in 2-3 sentences with specific benefits] 141 + 142 + ### 2. [Item Name] 143 + 144 + [Why it's included in 2-3 sentences with specific benefits] 145 + 146 + ### 3. [Item Name] 147 + 148 + [Why it's included in 2-3 sentences with specific benefits] 149 + ``` 150 + 151 + --- 152 + 153 + ## Generative Engine Optimization (GEO) Patterns 154 + 155 + These patterns optimize content for citation by AI assistants like ChatGPT, Claude, Perplexity, and Gemini. 156 + 157 + ### Statistic Citation Block 158 + 159 + Statistics increase AI citation rates by 15-30%. Always include sources. 160 + 161 + ```markdown 162 + [Claim statement]. According to [Source/Organization], [specific statistic with number and timeframe]. [Context for why this matters]. 163 + ``` 164 + 165 + **Example:** 166 + ```markdown 167 + Mobile optimization is no longer optional for SEO success. According to Google's 2024 Core Web Vitals report, 70% of web traffic now comes from mobile devices, and pages failing mobile usability standards see 24% higher bounce rates. This makes mobile-first indexing a critical ranking factor. 168 + ``` 169 + 170 + ### Expert Quote Block 171 + 172 + Named expert attribution adds credibility and increases citation likelihood. 173 + 174 + ```markdown 175 + "[Direct quote from expert]," says [Expert Name], [Title/Role] at [Organization]. [1 sentence of context or interpretation]. 176 + ``` 177 + 178 + **Example:** 179 + ```markdown 180 + "The shift from keyword-driven search to intent-driven discovery represents the most significant change in SEO since mobile-first indexing," says Rand Fishkin, Co-founder of SparkToro. This perspective highlights why content strategies must evolve beyond traditional keyword optimization. 181 + ``` 182 + 183 + ### Authoritative Claim Block 184 + 185 + Structure claims for easy AI extraction with clear attribution. 186 + 187 + ```markdown 188 + [Topic] [verb: is/has/requires/involves] [clear, specific claim]. [Source] [confirms/reports/found] that [supporting evidence]. This [explains/means/suggests] [implication or action]. 189 + ``` 190 + 191 + **Example:** 192 + ```markdown 193 + E-E-A-T is the cornerstone of Google's content quality evaluation. Google's Search Quality Rater Guidelines confirm that trust is the most critical factor, stating that "untrustworthy pages have low E-E-A-T no matter how experienced, expert, or authoritative they may seem." This means content creators must prioritize transparency and accuracy above all other optimization tactics. 194 + ``` 195 + 196 + ### Self-Contained Answer Block 197 + 198 + Create quotable, standalone statements that AI can extract directly. 199 + 200 + ```markdown 201 + **[Topic/Question]**: [Complete, self-contained answer that makes sense without additional context. Include specific details, numbers, or examples in 2-3 sentences.] 202 + ``` 203 + 204 + **Example:** 205 + ```markdown 206 + **Ideal blog post length for SEO**: The optimal length for SEO blog posts is 1,500-2,500 words for competitive topics. This range allows comprehensive topic coverage while maintaining reader engagement. HubSpot research shows long-form content earns 77% more backlinks than short articles, directly impacting search rankings. 207 + ``` 208 + 209 + ### Evidence Sandwich Block 210 + 211 + Structure claims with evidence for maximum credibility. 212 + 213 + ```markdown 214 + [Opening claim statement]. 215 + 216 + Evidence supporting this includes: 217 + - [Data point 1 with source] 218 + - [Data point 2 with source] 219 + - [Data point 3 with source] 220 + 221 + [Concluding statement connecting evidence to actionable insight]. 222 + ``` 223 + 224 + --- 225 + 226 + ## Domain-Specific GEO Tactics 227 + 228 + Different content domains benefit from different authority signals. 229 + 230 + ### Technology Content 231 + - Emphasize technical precision and correct terminology 232 + - Include version numbers and dates for software/tools 233 + - Reference official documentation 234 + - Add code examples where relevant 235 + 236 + ### Health/Medical Content 237 + - Cite peer-reviewed studies with publication details 238 + - Include expert credentials (MD, RN, etc.) 239 + - Note study limitations and context 240 + - Add "last reviewed" dates 241 + 242 + ### Financial Content 243 + - Reference regulatory bodies (SEC, FTC, etc.) 244 + - Include specific numbers with timeframes 245 + - Note that information is educational, not advice 246 + - Cite recognized financial institutions 247 + 248 + ### Legal Content 249 + - Cite specific laws, statutes, and regulations 250 + - Reference jurisdiction clearly 251 + - Include professional disclaimers 252 + - Note when professional consultation is advised 253 + 254 + ### Business/Marketing Content 255 + - Include case studies with measurable results 256 + - Reference industry research and reports 257 + - Add percentage changes and timeframes 258 + - Quote recognized thought leaders 259 + 260 + --- 261 + 262 + ## Voice Search Optimization 263 + 264 + Voice queries are conversational and question-based. Optimize for these patterns: 265 + 266 + ### Question Formats for Voice 267 + - "What is..." 268 + - "How do I..." 269 + - "Where can I find..." 270 + - "Why does..." 271 + - "When should I..." 272 + - "Who is..." 273 + 274 + ### Voice-Optimized Answer Structure 275 + - Lead with direct answer (under 30 words ideal) 276 + - Use natural, conversational language 277 + - Avoid jargon unless targeting expert audience 278 + - Include local context where relevant 279 + - Structure for single spoken response
+190
.agents/skills/seo-audit/references/ai-writing-detection.md
··· 1 + # AI Writing Detection 2 + 3 + Words, phrases, and punctuation patterns commonly associated with AI-generated text. Avoid these to ensure writing sounds natural and human. 4 + 5 + Sources: Grammarly (2025), Microsoft 365 Life Hacks (2025), GPTHuman (2025), Walter Writes (2025), Textero (2025), Plagiarism Today (2025), Rolling Stone (2025), MDPI Blog (2025) 6 + 7 + --- 8 + 9 + ## Em Dashes: The Primary AI Tell 10 + 11 + **The em dash (—) has become one of the most reliable markers of AI-generated content.** 12 + 13 + Em dashes are longer than hyphens (-) and are used for emphasis, interruptions, or parenthetical information. While they have legitimate uses in writing, AI models drastically overuse them. 14 + 15 + ### Why Em Dashes Signal AI Writing 16 + - AI models were trained on edited books, academic papers, and style guides where em dashes appear frequently 17 + - AI uses em dashes as a shortcut for sentence variety instead of commas, colons, or parentheses 18 + - Most human writers rarely use em dashes because they don't exist as a standard keyboard key 19 + - The overuse is so consistent that it has become the unofficial signature of ChatGPT writing 20 + 21 + ### What To Do Instead 22 + | Instead of | Use | 23 + |------------|-----| 24 + | The results—which were surprising—showed... | The results, which were surprising, showed... | 25 + | This approach—unlike traditional methods—allows... | This approach, unlike traditional methods, allows... | 26 + | The study found—as expected—that... | The study found, as expected, that... | 27 + | Communication skills—both written and verbal—are essential | Communication skills (both written and verbal) are essential | 28 + 29 + ### Guidelines 30 + - Use commas for most parenthetical information 31 + - Use colons to introduce explanations or lists 32 + - Use parentheses for supplementary information 33 + - Reserve em dashes for rare, deliberate emphasis only 34 + - If you find yourself using more than one em dash per page, revise 35 + 36 + --- 37 + 38 + ## Overused Verbs 39 + 40 + | Avoid | Use Instead | 41 + |-------|-------------| 42 + | delve (into) | explore, examine, investigate, look at | 43 + | leverage | use, apply, draw on | 44 + | optimise | improve, refine, enhance | 45 + | utilise | use | 46 + | facilitate | help, enable, support | 47 + | foster | encourage, support, develop, nurture | 48 + | bolster | strengthen, support, reinforce | 49 + | underscore | emphasise, highlight, stress | 50 + | unveil | reveal, show, introduce, present | 51 + | navigate | manage, handle, work through | 52 + | streamline | simplify, make more efficient | 53 + | enhance | improve, strengthen | 54 + | endeavour | try, attempt, effort | 55 + | ascertain | find out, determine, establish | 56 + | elucidate | explain, clarify, make clear | 57 + 58 + --- 59 + 60 + ## Overused Adjectives 61 + 62 + | Avoid | Use Instead | 63 + |-------|-------------| 64 + | robust | strong, reliable, thorough, solid | 65 + | comprehensive | complete, thorough, full, detailed | 66 + | pivotal | key, critical, central, important | 67 + | crucial | important, key, essential, critical | 68 + | vital | important, essential, necessary | 69 + | transformative | significant, important, major | 70 + | cutting-edge | new, advanced, recent, modern | 71 + | groundbreaking | new, original, significant | 72 + | innovative | new, original, creative | 73 + | seamless | smooth, easy, effortless | 74 + | intricate | complex, detailed, complicated | 75 + | nuanced | subtle, complex, detailed | 76 + | multifaceted | complex, varied, diverse | 77 + | holistic | complete, whole, comprehensive | 78 + 79 + --- 80 + 81 + ## Overused Transitions and Connectors 82 + 83 + | Avoid | Use Instead | 84 + |-------|-------------| 85 + | furthermore | also, in addition, and | 86 + | moreover | also, and, besides | 87 + | notwithstanding | despite, even so, still | 88 + | that being said | however, but, still | 89 + | at its core | essentially, fundamentally, basically | 90 + | to put it simply | in short, simply put | 91 + | it is worth noting that | note that, importantly | 92 + | in the realm of | in, within, regarding | 93 + | in the landscape of | in, within | 94 + | in today's [anything] | currently, now, today | 95 + 96 + --- 97 + 98 + ## Phrases That Signal AI Writing 99 + 100 + ### Opening Phrases to Avoid 101 + - "In today's fast-paced world..." 102 + - "In today's digital age..." 103 + - "In an era of..." 104 + - "In the ever-evolving landscape of..." 105 + - "In the realm of..." 106 + - "It's important to note that..." 107 + - "Let's delve into..." 108 + - "Imagine a world where..." 109 + 110 + ### Transitional Phrases to Avoid 111 + - "That being said..." 112 + - "With that in mind..." 113 + - "It's worth mentioning that..." 114 + - "At its core..." 115 + - "To put it simply..." 116 + - "In essence..." 117 + - "This begs the question..." 118 + 119 + ### Concluding Phrases to Avoid 120 + - "In conclusion..." 121 + - "To sum up..." 122 + - "By [doing X], you can [achieve Y]..." 123 + - "In the final analysis..." 124 + - "All things considered..." 125 + - "At the end of the day..." 126 + 127 + ### Structural Patterns to Avoid 128 + - "Whether you're a [X], [Y], or [Z]..." (listing three examples after "whether") 129 + - "It's not just [X], it's also [Y]..." 130 + - "Think of [X] as [elaborate metaphor]..." 131 + - Starting sentences with "By" followed by a gerund: "By understanding X, you can Y..." 132 + 133 + --- 134 + 135 + ## Filler Words and Empty Intensifiers 136 + 137 + These words often add nothing to meaning. Remove them or find specific alternatives: 138 + 139 + - absolutely 140 + - actually 141 + - basically 142 + - certainly 143 + - clearly 144 + - definitely 145 + - essentially 146 + - extremely 147 + - fundamentally 148 + - incredibly 149 + - interestingly 150 + - naturally 151 + - obviously 152 + - quite 153 + - really 154 + - significantly 155 + - simply 156 + - surely 157 + - truly 158 + - ultimately 159 + - undoubtedly 160 + - very 161 + 162 + --- 163 + 164 + ## Academic-Specific AI Tells 165 + 166 + | Avoid | Use Instead | 167 + |-------|-------------| 168 + | shed light on | clarify, explain, reveal | 169 + | pave the way for | enable, allow, make possible | 170 + | a myriad of | many, numerous, various | 171 + | a plethora of | many, numerous, several | 172 + | paramount | very important, essential, critical | 173 + | pertaining to | about, regarding, concerning | 174 + | prior to | before | 175 + | subsequent to | after | 176 + | in light of | because of, given, considering | 177 + | with respect to | about, regarding, for | 178 + | in terms of | regarding, for, about | 179 + | the fact that | that (or rewrite sentence) | 180 + 181 + --- 182 + 183 + ## How to Self-Check 184 + 185 + 1. Read your text aloud. If phrases sound unnatural in speech, revise them 186 + 2. Ask: "Would I say this in a conversation with a colleague?" 187 + 3. Check for repetitive sentence structures 188 + 4. Look for clusters of the words listed above 189 + 5. Ensure varied sentence lengths (not all similar length) 190 + 6. Verify each intensifier adds genuine meaning
+1
.claude/skills/seo-audit
··· 1 + ../../.agents/skills/seo-audit
+22 -2
apps/web/src/app/(landing)/[slug]/page.tsx
··· 1 - import { getJsonLDWebPage, getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBreadcrumbList, 4 + getJsonLDWebPage, 5 + getPageMetadata, 6 + } from "@/app/shared-metadata"; 2 7 import { CustomMDX } from "@/content/mdx"; 3 8 import { getMainPages } from "@/content/utils"; 4 9 import type { Metadata } from "next"; 5 10 import { notFound } from "next/navigation"; 6 - import type { WebPage, WithContext } from "schema-dts"; 11 + import type { BreadcrumbList, WebPage, WithContext } from "schema-dts"; 7 12 8 13 export const dynamicParams = false; 9 14 ··· 45 50 46 51 const jsonLDWebPage: WithContext<WebPage> = getJsonLDWebPage(page); 47 52 53 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 54 + [ 55 + { name: "Home", url: BASE_URL }, 56 + { name: page.metadata.title, url: `${BASE_URL}/${slug}` }, 57 + ], 58 + ); 59 + 48 60 return ( 49 61 <section className="prose dark:prose-invert max-w-none"> 50 62 <script ··· 52 64 // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd 53 65 dangerouslySetInnerHTML={{ 54 66 __html: JSON.stringify(jsonLDWebPage).replace(/</g, "\\u003c"), 67 + }} 68 + /> 69 + <script 70 + type="application/ld+json" 71 + suppressHydrationWarning 72 + // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd 73 + dangerouslySetInnerHTML={{ 74 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 55 75 }} 56 76 /> 57 77 <h1>{page.metadata.title}</h1>
+23 -2
apps/web/src/app/(landing)/blog/[slug]/page.tsx
··· 1 - import { getJsonLDBlogPosting, getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBlogPosting, 4 + getJsonLDBreadcrumbList, 5 + getPageMetadata, 6 + } from "@/app/shared-metadata"; 2 7 import { CustomMDX } from "@/content/mdx"; 3 8 import { formatDate, getBlogPosts } from "@/content/utils"; 4 9 import { getAuthor } from "@/data/author"; 5 10 import type { Metadata } from "next"; 6 11 import Image from "next/image"; 7 12 import { notFound } from "next/navigation"; 8 - import type { BlogPosting, WithContext } from "schema-dts"; 13 + import type { BlogPosting, BreadcrumbList, WithContext } from "schema-dts"; 9 14 import { ContentPagination } from "../../content-pagination"; 10 15 11 16 export const dynamicParams = false; ··· 58 63 "blog", 59 64 ); 60 65 66 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 67 + [ 68 + { name: "Home", url: BASE_URL }, 69 + { name: "Blog", url: `${BASE_URL}/blog` }, 70 + { name: post.metadata.title, url: `${BASE_URL}/blog/${slug}` }, 71 + ], 72 + ); 73 + 61 74 return ( 62 75 <section className="prose dark:prose-invert max-w-none"> 63 76 <script ··· 66 79 // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 67 80 dangerouslySetInnerHTML={{ 68 81 __html: JSON.stringify(jsonLDBlog).replace(/</g, "\\u003c"), 82 + }} 83 + /> 84 + <script 85 + type="application/ld+json" 86 + suppressHydrationWarning 87 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 88 + dangerouslySetInnerHTML={{ 89 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 69 90 }} 70 91 /> 71 92 <h1>{post.metadata.title}</h1>
+23 -2
apps/web/src/app/(landing)/changelog/[slug]/page.tsx
··· 1 - import { getJsonLDBlogPosting, getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBlogPosting, 4 + getJsonLDBreadcrumbList, 5 + getPageMetadata, 6 + } from "@/app/shared-metadata"; 2 7 import { CustomMDX } from "@/content/mdx"; 3 8 import { formatDate, getChangelogPosts } from "@/content/utils"; 4 9 import type { Metadata } from "next"; 5 10 import Image from "next/image"; 6 11 import { notFound } from "next/navigation"; 7 - import type { BlogPosting, WithContext } from "schema-dts"; 12 + import type { BlogPosting, BreadcrumbList, WithContext } from "schema-dts"; 8 13 import { ContentPagination } from "../../content-pagination"; 9 14 10 15 export const dynamicParams = false; ··· 57 62 "changelog", 58 63 ); 59 64 65 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 66 + [ 67 + { name: "Home", url: BASE_URL }, 68 + { name: "Changelog", url: `${BASE_URL}/changelog` }, 69 + { name: post.metadata.title, url: `${BASE_URL}/changelog/${slug}` }, 70 + ], 71 + ); 72 + 60 73 return ( 61 74 <section className="prose dark:prose-invert max-w-none"> 62 75 <script ··· 65 78 // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 66 79 dangerouslySetInnerHTML={{ 67 80 __html: JSON.stringify(jsonLDBlog).replace(/</g, "\\u003c"), 81 + }} 82 + /> 83 + <script 84 + type="application/ld+json" 85 + suppressHydrationWarning 86 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 87 + dangerouslySetInnerHTML={{ 88 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 68 89 }} 69 90 /> 70 91 <h1>{post.metadata.title}</h1>
+22 -1
apps/web/src/app/(landing)/compare/[slug]/page.tsx
··· 1 - import { getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBreadcrumbList, 4 + getPageMetadata, 5 + } from "@/app/shared-metadata"; 2 6 import { CustomMDX } from "@/content/mdx"; 3 7 import { getComparePages } from "@/content/utils"; 4 8 import type { Metadata } from "next"; 5 9 import { notFound } from "next/navigation"; 10 + import type { BreadcrumbList, WithContext } from "schema-dts"; 6 11 7 12 export const dynamicParams = false; 8 13 ··· 43 48 notFound(); 44 49 } 45 50 51 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 52 + [ 53 + { name: "Home", url: BASE_URL }, 54 + { name: "Compare", url: `${BASE_URL}/compare` }, 55 + { name: post.metadata.title, url: `${BASE_URL}/compare/${slug}` }, 56 + ], 57 + ); 58 + 46 59 return ( 47 60 <section className="prose dark:prose-invert max-w-none"> 61 + <script 62 + type="application/ld+json" 63 + suppressHydrationWarning 64 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 65 + dangerouslySetInnerHTML={{ 66 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 67 + }} 68 + /> 48 69 <h1>{post.metadata.title}</h1> 49 70 <p className="text-lg">{post.metadata.description}</p> 50 71 <CustomMDX source={post.content} />
+22 -3
apps/web/src/app/(landing)/page.tsx
··· 1 1 import { CustomMDX } from "@/content/mdx"; 2 2 import { getHomePage } from "@/content/utils"; 3 3 import type { Metadata } from "next"; 4 - import type { Organization, Product, WebPage, WithContext } from "schema-dts"; 5 - import { defaultMetadata } from "../shared-metadata"; 6 - import { getJsonLDOrganization, getJsonLDProduct } from "../shared-metadata"; 4 + import type { 5 + FAQPage, 6 + Organization, 7 + Product, 8 + WebPage, 9 + WithContext, 10 + } from "schema-dts"; 11 + import { 12 + defaultMetadata, 13 + getJsonLDFAQPage, 14 + getJsonLDOrganization, 15 + getJsonLDProduct, 16 + } from "../shared-metadata"; 7 17 8 18 const jsonLdProduct: WithContext<Product> = getJsonLDProduct(); 9 19 ··· 18 28 image: "https://openstatus.dev/assets/logos/OpenStatus-Logo.svg", 19 29 headline: "Showcase your uptime with a status page", 20 30 }; 31 + 32 + const jsonLDFAQPage: WithContext<FAQPage> = getJsonLDFAQPage(); 21 33 22 34 export const metadata: Metadata = defaultMetadata; 23 35 ··· 44 56 // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd 45 57 dangerouslySetInnerHTML={{ 46 58 __html: JSON.stringify(jsonLDWebpage).replace(/</g, "\\u003c"), 59 + }} 60 + /> 61 + <script 62 + type="application/ld+json" 63 + // biome-ignore lint/security/noDangerouslySetInnerHtml: jsonLd 64 + dangerouslySetInnerHTML={{ 65 + __html: JSON.stringify(jsonLDFAQPage).replace(/</g, "\\u003c"), 47 66 }} 48 67 /> 49 68 <h1>{homePage.metadata.title}</h1>
+23 -1
apps/web/src/app/(landing)/play/checker/[slug]/page.tsx
··· 1 - import { getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBreadcrumbList, 4 + getPageMetadata, 5 + } from "@/app/shared-metadata"; 2 6 import { 3 7 getCheckerDataById, 4 8 latencyFormatter, ··· 8 12 import { getToolsPage } from "@/content/utils"; 9 13 import type { Metadata } from "next"; 10 14 import { redirect } from "next/navigation"; 15 + import type { BreadcrumbList, WithContext } from "schema-dts"; 11 16 import { mockCheckAllRegions } from "../api/mock"; 12 17 import { Table } from "./client"; 13 18 ··· 88 93 89 94 if (!data) redirect("/play/checker"); 90 95 96 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 97 + [ 98 + { name: "Home", url: BASE_URL }, 99 + { name: "Playground", url: `${BASE_URL}/play` }, 100 + { name: "Speed Checker", url: `${BASE_URL}/play/checker` }, 101 + { name: data.url, url: `${BASE_URL}/play/checker/${slug}` }, 102 + ], 103 + ); 104 + 91 105 return ( 92 106 <section className="prose dark:prose-invert max-w-none"> 107 + <script 108 + type="application/ld+json" 109 + suppressHydrationWarning 110 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 111 + dangerouslySetInnerHTML={{ 112 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 113 + }} 114 + /> 93 115 <h1>{data.url}</h1> 94 116 <p className="text-lg">{formatDate(new Date(data.timestamp))}</p> 95 117 <Table data={data} />
+22 -1
apps/web/src/app/(landing)/play/checker/page.tsx
··· 1 - import { getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBreadcrumbList, 4 + getPageMetadata, 5 + } from "@/app/shared-metadata"; 2 6 import { getCheckerDataById } from "@/components/ping-response-analysis/utils"; 3 7 import { CustomMDX } from "@/content/mdx"; 4 8 import { getToolsPage } from "@/content/utils"; 5 9 import type { Metadata } from "next"; 10 + import type { BreadcrumbList, WithContext } from "schema-dts"; 6 11 import { mockCheckAllRegions } from "./api/mock"; 7 12 import { 8 13 CheckerProvider, ··· 33 38 : await getCheckerDataById(id) 34 39 : null; 35 40 41 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 42 + [ 43 + { name: "Home", url: BASE_URL }, 44 + { name: "Playground", url: `${BASE_URL}/play` }, 45 + { name: page.metadata.title, url: `${BASE_URL}/play/checker` }, 46 + ], 47 + ); 48 + 36 49 return ( 37 50 <section className="prose dark:prose-invert max-w-none"> 51 + <script 52 + type="application/ld+json" 53 + suppressHydrationWarning 54 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 55 + dangerouslySetInnerHTML={{ 56 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 57 + }} 58 + /> 38 59 <h1>{page.metadata.title}</h1> 39 60 <p className="text-lg">{page.metadata.description}</p> 40 61 <CheckerProvider
+23 -1
apps/web/src/app/(landing)/play/curl/page.tsx
··· 1 - import { getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBreadcrumbList, 4 + getPageMetadata, 5 + } from "@/app/shared-metadata"; 2 6 import { CustomMDX } from "@/content/mdx"; 3 7 import { getToolsPage } from "@/content/utils"; 4 8 import type { Metadata } from "next"; 9 + import type { BreadcrumbList, WithContext } from "schema-dts"; 5 10 import { Form } from "./client"; 6 11 7 12 export function generateMetadata(): Metadata { ··· 11 16 12 17 export default function Page() { 13 18 const page = getToolsPage("curl"); 19 + 20 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 21 + [ 22 + { name: "Home", url: BASE_URL }, 23 + { name: "Playground", url: `${BASE_URL}/play` }, 24 + { name: page.metadata.title, url: `${BASE_URL}/play/curl` }, 25 + ], 26 + ); 27 + 14 28 return ( 15 29 <section className="prose dark:prose-invert max-w-none"> 30 + <script 31 + type="application/ld+json" 32 + suppressHydrationWarning 33 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 34 + dangerouslySetInnerHTML={{ 35 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 36 + }} 37 + /> 16 38 <h1>{page.metadata.title}</h1> 17 39 <p className="text-lg">{page.metadata.description}</p> 18 40 <Form />
+23 -1
apps/web/src/app/(landing)/play/uptime-sla/page.tsx
··· 1 - import { getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBreadcrumbList, 4 + getPageMetadata, 5 + } from "@/app/shared-metadata"; 2 6 import { CustomMDX } from "@/content/mdx"; 3 7 import { getToolsPage } from "@/content/utils"; 4 8 import type { Metadata } from "next"; 9 + import type { BreadcrumbList, WithContext } from "schema-dts"; 5 10 import { Calculation } from "./client"; 6 11 7 12 export function generateMetadata(): Metadata { ··· 11 16 12 17 export default function Page() { 13 18 const page = getToolsPage("uptime-sla"); 19 + 20 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 21 + [ 22 + { name: "Home", url: BASE_URL }, 23 + { name: "Playground", url: `${BASE_URL}/play` }, 24 + { name: page.metadata.title, url: `${BASE_URL}/play/uptime-sla` }, 25 + ], 26 + ); 27 + 14 28 return ( 15 29 <section className="prose dark:prose-invert max-w-none space-y-4"> 30 + <script 31 + type="application/ld+json" 32 + suppressHydrationWarning 33 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 34 + dangerouslySetInnerHTML={{ 35 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 36 + }} 37 + /> 16 38 <h1>{page.metadata.title}</h1> 17 39 <p className="text-lg">{page.metadata.description}</p> 18 40 <Calculation />
+23 -2
apps/web/src/app/(landing)/report-template/[slug]/page.tsx
··· 1 - import { getJsonLDBlogPosting, getPageMetadata } from "@/app/shared-metadata"; 1 + import { 2 + BASE_URL, 3 + getJsonLDBlogPosting, 4 + getJsonLDBreadcrumbList, 5 + getPageMetadata, 6 + } from "@/app/shared-metadata"; 2 7 import { CustomMDX } from "@/content/mdx"; 3 8 import { formatDate, getReportTemplates } from "@/content/utils"; 4 9 import { getAuthor } from "@/data/author"; 5 10 import type { Metadata } from "next"; 6 11 import Image from "next/image"; 7 12 import { notFound } from "next/navigation"; 8 - import type { BlogPosting, WithContext } from "schema-dts"; 13 + import type { BlogPosting, BreadcrumbList, WithContext } from "schema-dts"; 9 14 import { ContentPagination } from "../../content-pagination"; 10 15 11 16 export const dynamicParams = false; ··· 58 63 "report-template", 59 64 ); 60 65 66 + const jsonLDBreadcrumb: WithContext<BreadcrumbList> = getJsonLDBreadcrumbList( 67 + [ 68 + { name: "Home", url: BASE_URL }, 69 + { name: "Report Templates", url: `${BASE_URL}/report-template` }, 70 + { name: post.metadata.title, url: `${BASE_URL}/report-template/${slug}` }, 71 + ], 72 + ); 73 + 61 74 return ( 62 75 <section className="prose dark:prose-invert max-w-none"> 63 76 <script ··· 66 79 // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 67 80 dangerouslySetInnerHTML={{ 68 81 __html: JSON.stringify(jsonLDBlog).replace(/</g, "\\u003c"), 82 + }} 83 + /> 84 + <script 85 + type="application/ld+json" 86 + suppressHydrationWarning 87 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 88 + dangerouslySetInnerHTML={{ 89 + __html: JSON.stringify(jsonLDBreadcrumb).replace(/</g, "\\u003c"), 69 90 }} 70 91 /> 71 92 <h1>{post.metadata.title}</h1>
+75 -1
apps/web/src/app/shared-metadata.ts
··· 3 3 import type { Metadata } from "next"; 4 4 import type { 5 5 BlogPosting, 6 + BreadcrumbList, 7 + FAQPage, 6 8 Organization, 7 9 Product, 8 10 WebPage, ··· 10 12 } from "schema-dts"; 11 13 12 14 export const TITLE = "openstatus"; 15 + export const HOMEPAGE_TITLE = 16 + "OpenStatus - Open-Source Status Page & Uptime Monitoring"; 13 17 export const DESCRIPTION = 14 18 "Monitor your services globally and showcase your uptime with a status page. Get started for free with our open-source status page with uptime monitoring solution."; 15 19 ··· 38 42 export const defaultMetadata: Metadata = { 39 43 title: { 40 44 template: `%s | ${TITLE}`, 41 - default: TITLE, 45 + default: HOMEPAGE_TITLE, 42 46 }, 43 47 description: DESCRIPTION, 44 48 metadataBase: new URL(BASE_URL), 49 + alternates: { 50 + canonical: "/", 51 + }, 45 52 twitter: twitterMetadata, 46 53 openGraph: ogMetadata, 47 54 }; ··· 63 70 return { 64 71 title, 65 72 description, 73 + alternates: { 74 + canonical: url, 75 + }, 66 76 openGraph: { 67 77 title, 68 78 description, ··· 167 177 })), 168 178 }; 169 179 }; 180 + 181 + export const getJsonLDBreadcrumbList = ( 182 + items: { name: string; url: string }[], 183 + ): WithContext<BreadcrumbList> => { 184 + return { 185 + "@context": "https://schema.org", 186 + "@type": "BreadcrumbList", 187 + itemListElement: items.map((item, index) => ({ 188 + "@type": "ListItem", 189 + position: index + 1, 190 + name: item.name, 191 + item: item.url, 192 + })), 193 + }; 194 + }; 195 + 196 + export const getJsonLDFAQPage = (): WithContext<FAQPage> => { 197 + return { 198 + "@context": "https://schema.org", 199 + "@type": "FAQPage", 200 + mainEntity: [ 201 + { 202 + "@type": "Question", 203 + name: "What are the limits?", 204 + acceptedAnswer: { 205 + "@type": "Answer", 206 + text: "As free user you will start with a total of one monitor and one status page as well as cron jobs of min. 10m. You can upgrade to a paid plan at any time. No credit card is required to sign up and you can cancel at any time.", 207 + }, 208 + }, 209 + { 210 + "@type": "Question", 211 + name: "Who are we?", 212 + acceptedAnswer: { 213 + "@type": "Answer", 214 + text: "We are Thibault and Max and we take you with us on our journey. Read more on our about page at https://www.openstatus.dev/about.", 215 + }, 216 + }, 217 + { 218 + "@type": "Question", 219 + name: "How does it work?", 220 + acceptedAnswer: { 221 + "@type": "Answer", 222 + text: "We ping your endpoints from multiple regions to calculate uptime and display the current status on your status page. We also collect response time data like headers and timing phases and display it on your dashboard.", 223 + }, 224 + }, 225 + { 226 + "@type": "Question", 227 + name: "What regions do we support?", 228 + acceptedAnswer: { 229 + "@type": "Answer", 230 + text: "We support monitoring from 28 regions worldwide across all continents: Europe (Amsterdam, Stockholm, Paris, Frankfurt, London), North America (Dallas, New Jersey, Los Angeles, San Jose, Chicago, Toronto), South America (São Paulo), Asia (Mumbai, Tokyo, Singapore), Africa (Johannesburg), and Oceania (Sydney).", 231 + }, 232 + }, 233 + { 234 + "@type": "Question", 235 + name: "How can I help?", 236 + acceptedAnswer: { 237 + "@type": "Answer", 238 + text: "There are many ways you can help us: Spread the word by telling your friends and colleagues about OpenStatus, report bugs if you find them, suggest new features, contribute to the project if you are a developer, become a paid user if you are a business, or star our project on GitHub.", 239 + }, 240 + }, 241 + ], 242 + }; 243 + };
+33
apps/web/src/app/sitemap.ts
··· 2 2 getBlogPosts, 3 3 getChangelogPosts, 4 4 getComparePages, 5 + getHomePage, 5 6 getProductPages, 7 + getReportTemplates, 6 8 getToolsPages, 7 9 getUnrelatedPages, 8 10 } from "@/content/utils"; ··· 18 20 const allPlaygrounds = getToolsPages().filter( 19 21 (tool) => tool.slug !== "checker-slug", 20 22 ); 23 + const allReportTemplates = getReportTemplates(); 21 24 22 25 export default function sitemap(): MetadataRoute.Sitemap { 23 26 const blogs = allPosts.map((post) => ({ 24 27 url: `https://www.openstatus.dev/blog/${post.slug}`, 25 28 lastModified: post.metadata.publishedAt, // date format should be YYYY-MM-DD 29 + changeFrequency: "monthly" as const, 30 + priority: 0.7, 26 31 })); 27 32 28 33 const changelogs = allChangelogs.map((post) => ({ 29 34 url: `https://www.openstatus.dev/changelog/${post.slug}`, 30 35 lastModified: post.metadata.publishedAt, // date format should be YYYY-MM-DD 36 + changeFrequency: "weekly" as const, 37 + priority: 0.6, 31 38 })); 32 39 33 40 const comparisons = allComparisons.map((comparison) => ({ 34 41 url: `https://www.openstatus.dev/compare/${comparison.slug}`, 35 42 lastModified: comparison.metadata.publishedAt, 43 + changeFrequency: "monthly" as const, 44 + priority: 0.8, 36 45 })); 37 46 38 47 const landings = allUnrelated.map((page) => ({ 39 48 url: `https://www.openstatus.dev/${page.slug}`, 40 49 lastModified: page.metadata.publishedAt, 50 + changeFrequency: "monthly" as const, 51 + priority: 0.7, 41 52 })); 42 53 43 54 const products = allProducts.map((product) => ({ 44 55 url: `https://www.openstatus.dev/${product.slug}`, 45 56 lastModified: product.metadata.publishedAt, 57 + changeFrequency: "weekly" as const, 58 + priority: 0.9, 46 59 })); 47 60 48 61 const playgrounds = allPlaygrounds.map((playground) => ({ 49 62 url: `https://www.openstatus.dev/play/${playground.slug}`, 50 63 lastModified: playground.metadata.publishedAt, 64 + changeFrequency: "monthly" as const, 65 + priority: 0.6, 51 66 })); 52 67 68 + const reportTemplates = allReportTemplates.map((reportTemplate) => ({ 69 + url: `https://www.openstatus.dev/report-template/${reportTemplate.slug}`, 70 + lastModified: reportTemplate.metadata.publishedAt, 71 + changeFrequency: "monthly" as const, 72 + priority: 0.6, 73 + })); 74 + 75 + const home = [ 76 + { 77 + url: "https://www.openstatus.dev/", 78 + lastModified: getHomePage().metadata.publishedAt, 79 + changeFrequency: "daily" as const, 80 + priority: 1.0, 81 + }, 82 + ]; 83 + 53 84 return [ 85 + ...home, 54 86 ...blogs, 55 87 ...changelogs, 56 88 ...comparisons, 57 89 ...landings, 58 90 ...products, 59 91 ...playgrounds, 92 + ...reportTemplates, 60 93 ]; 61 94 }
+2 -2
apps/web/src/content/pages/home.mdx
··· 101 101 102 102 <Details summary="Who are we?"> 103 103 104 - We are [Thibault]() and [Max]() and we take you with us on our journey. 104 + We are [Thibault](https://bsky.app/profile/thibaultleouay.dev) and [Max](https://x.com/mxkaske) and we take you with us on our journey. 105 105 106 106 Read more on [our about page](/about). 107 107 ··· 124 124 125 125 <Details summary="What regions do we support?"> 126 126 127 - We support **monitoring from 20+ regions worldwide** across all continents: 127 + We support **monitoring from 28 regions worldwide** across all continents: 128 128 129 129 **Europe** 130 130