# Safe Link Placement Guide

**Last Updated:** 2026-01-19

## Overview

This guide outlines safe areas and protected areas for adding internal links to blog posts. Following these guidelines ensures links are placed naturally, improve SEO, and don't break page functionality.

## Protected Areas (Never Add Links)

### Headers (h1-h6)

**Why Protected:**
- Links in headers appear spammy and don't improve SEO
- Headers are structural elements, not content
- Headers should be clean and readable

**Detection:**
- Function: `is_inside_header()` in `link_utils.py`
- Checks if position is inside `<h1>` through `<h6>` tags

**Minimum Distance:**
- Links must be minimum 50 characters from headers
- Function: `is_too_close_to_header()` enforces this

**Example:**
```html
<!-- ❌ WRONG: Link in header -->
<h2>Was ist <a href="/tools/zeiterfassung">Zeiterfassung</a>?</h2>

<!-- ✅ CORRECT: Link in paragraph after header -->
<h2>Was ist Zeiterfassung?</h2>
<p>Die <a href="/tools/zeiterfassung">Zeiterfassung</a> ist ein wichtiger Bestandteil...</p>
```

### FAQ Questions

**Why Protected:**
- Questions are structural elements displayed separately
- Questions should be clean and readable
- Links belong in answers, not questions

**Detection:**
- Function: `check_faq_question_contains_keyword()` in `link_utils.py`
- Checks `faqs[].question` field (plain text)

**Example:**
```json
{
  "faqs": [
    {
      "question": "Was ist Zeiterfassung?",  // ❌ NO LINKS HERE
      "answer": "<p>Die <a href=\"/tools/zeiterfassung\">Zeiterfassung</a> ist...</p>"  // ✅ LINKS OK HERE
    }
  ]
}
```

### Related Content Carousel

**Why Protected:**
- Posts in `related_posts` array already have links via carousel component
- Duplicate inline links should be avoided unless high-value
- Carousel links are navigational, inline links are contextual

**Detection:**
- Function: `check_related_posts_duplicate()` in `add-ahrefs-links-enhanced.py`
- Checks `related_posts[]` array for target URL

**Decision Logic:**
- Function: `should_add_despite_carousel()` determines if link should be added
- Add if:
  - Pillar page (high-value)
  - High PR (35+) AND high volume (>10K)
  - Different anchor text from carousel
- Skip if:
  - Low-value duplicate
  - Same anchor text as carousel

**Example:**
```json
{
  "related_posts": [
    {
      "url": "/tools/zeiterfassung",
      "anchor_text": "Zeiterfassung"
    }
  ]
}
```

If opportunity has same target URL:
- ✅ Add if: Different anchor text ("digitale Zeiterfassung" vs "Zeiterfassung")
- ❌ Skip if: Same anchor text and low-value

### Tool Links (Inline + Carousel)

**Rule:** One inline contextual link to a tool (e.g. in Berechnung section) is distinct from the carousel card. Both are valuable and acceptable – not duplicates.

- **Inline link:** High intent, contextual ("nutze unseren Midijob-Rechner")
- **Carousel card:** Discovery, "Das könnte dich auch interessieren"

**Decision:** Add one primary contextual link per tool per post when content discusses the topic. Carousel presence is complementary, not a reason to skip the inline link.

### Script/Style Tags

**Why Protected:**
- Links in scripts/styles break functionality
- Already protected by `is_inside_script_or_style()` function

### HTML Tag Attributes

**Why Protected:**
- Links in attributes break HTML structure
- Already protected by `is_inside_html_tag()` function

### Existing Links

**Why Protected:**
- Nested links are invalid HTML
- Already protected by `is_inside_existing_link()` function

## Safe Areas (Can Add Links)

### Paragraphs (`<p>`) in `content.html`

**Requirements:**
- Must be actual paragraph tag (`<p>`)
- Must have natural context (minimum 20 characters)
- Must not be header/list fragment
- Detection: `is_in_safe_paragraph()` function

**Example:**
```html
<!-- ✅ CORRECT: Link in paragraph with natural context -->
<p>Die <a href="/tools/zeiterfassung">Zeiterfassung</a> ist ein wichtiger Bestandteil der Arbeitszeitverwaltung.</p>

<!-- ❌ WRONG: Link in paragraph fragment (too short) -->
<p><a href="/tools/zeiterfassung">Zeiterfassung</a></p>
```

### List Items (`<li>`) in `content.html`

**Requirements:**
- Must have sufficient context (minimum 15 characters)
- Must have natural sentence context, not just keyword
- Detection: `is_in_safe_paragraph()` function

**Example:**
```html
<!-- ✅ CORRECT: Link in list item with context -->
<ul>
  <li>Die <a href="/tools/zeiterfassung">Zeiterfassung</a> dokumentiert Arbeitszeiten gesetzeskonform.</li>
</ul>

<!-- ❌ WRONG: Link in list item without context -->
<ul>
  <li><a href="/tools/zeiterfassung">Zeiterfassung</a></li>
</ul>
```

### FAQ Answers (`faqs[].answer`)

**Requirements:**
- FAQ answers are HTML content separate from questions
- Answers can contain contextual links
- Processing: `process_faq_answer_link()` function
- Structure preserved: question/answer separation maintained

**Example:**
```json
{
  "faqs": [
    {
      "question": "Was ist Zeiterfassung?",  // Plain text, no links
      "answer": "<p>Die <a href=\"/tools/zeiterfassung\">Zeiterfassung</a> ist ein System zur Dokumentation der Arbeitszeiten.</p>"  // HTML with links OK
    }
  ]
}
```

### Table Cells (`<td>`)

**Requirements:**
- Only if natural sentence context (rare)
- Must have sufficient text (minimum 20 characters)
- Not recommended unless absolutely necessary

**Example:**
```html
<!-- ✅ CORRECT: Link in table cell with natural sentence -->
<td>Die <a href="/tools/zeiterfassung">Zeiterfassung</a> dokumentiert Arbeitszeiten gesetzeskonform.</td>

<!-- ❌ WRONG: Link in table cell without context -->
<td><a href="/tools/zeiterfassung">Zeiterfassung</a></td>
```

## Best Practices

### Always Add

- **Pillar page links** (even if in carousel)
  - High-value for SEO
  - Example: `/insights/dienstplan/`, `/insights/zeiterfassung/`

- **High PR (35+) to high-traffic targets**
  - Significant SEO value
  - Example: PR 40+ source linking to high-traffic target

- **Contextual mentions in paragraphs**
  - Natural, relevant links
  - Example: Keyword appears naturally in paragraph content

### Review Before Adding

- **Links to posts already in carousel**
  - Check if high-value (pillar, high PR, different anchor)
  - Decision logic: `should_add_despite_carousel()`

- **Links close to headers (<50 chars)**
  - Check if keyword appears elsewhere with more distance
  - Minimum distance: 50 characters

- **Links in list items without paragraph context**
  - Ensure sufficient context (minimum 15 chars)
  - Prefer paragraph links over list item links

### Never Add

- **Links in headers (h1-h6)**
  - Headers should never contain links
  - Check if keyword appears in paragraphs

- **Links in FAQ questions**
  - Questions should never contain links
  - Check if keyword appears in FAQ answer

- **Links in carousel component HTML**
  - Carousel already has links
  - Check `related_posts` array for duplicates

- **Duplicate links with same anchor text**
  - One link per target URL per page (unless high-value with different anchor)

- **Links too close to headers (<50 chars)**
  - Maintain minimum distance for natural flow

- **Links too close to other links (<200 chars)**
  - Prevents clustering and maintains readability

## Implementation Functions

### Protection Functions (`link_utils.py`)

- `is_inside_header(html, position)` - Check if position is inside h1-h6 tag
- `is_in_safe_paragraph(html, position)` - Verify position is in paragraph with context
- `is_too_close_to_header(html, position, min_distance=50)` - Check minimum distance from headers
- `check_faq_question_contains_keyword(faqs_data, keyword)` - Check if keyword in FAQ question
- `is_in_faq_answer(html_content, position, faqs_data)` - Check if position is in FAQ answer

### Carousel Functions (`add-ahrefs-links-enhanced.py`)

- `check_related_posts_duplicate(post_data, target_url)` - Check if target is in carousel
- `should_add_despite_carousel(opp, is_in_carousel, related_post_data)` - Decision logic

### Link Addition Functions

- `add_link_to_html(html_content, keyword, target_url, context, existing_link_positions)` - Add link to content.html
- `process_faq_answer_link(faq_answer_html, keyword, target_url, context)` - Add link to FAQ answer

## Validation Checklist

Before adding a link, verify:

- [ ] Link is NOT in header (h1-h6)
- [ ] Link is NOT in FAQ question
- [ ] Link is NOT in related content carousel (unless high-value)
- [ ] Link is in safe paragraph with natural context
- [ ] Link is minimum 50 characters from headers
- [ ] Link is minimum 200 characters from other links
- [ ] Context is meaningful (50+ characters)
- [ ] Keyword appears naturally in context
- [ ] Anchor text is keyword-relevant (not generic)
- [ ] URL is absolute and correct
- [ ] Link flows naturally in content

## Common Issues

### Issue: Link skipped - Keyword in header

**Cause:** Keyword found in header tag (h1-h6)

**Solution:** This is expected behavior. Headers should never contain links. Check if keyword appears elsewhere in content (paragraphs, FAQ answers).

### Issue: Link skipped - Keyword in FAQ question

**Cause:** Keyword found in FAQ question field

**Solution:** This is expected behavior. FAQ questions should never contain links. Check if keyword appears in FAQ answer instead.

### Issue: Link skipped - Target in carousel

**Cause:** Target URL already in `related_posts` carousel

**Solution:** Check decision logic. Link will be added if high-value (pillar, high PR+volume, different anchor). Otherwise, carousel link is sufficient.

### Issue: Link skipped - Too close to header

**Cause:** Keyword position is less than 50 characters from header

**Solution:** This is expected behavior. Links should maintain minimum distance from headers for natural flow. Check if keyword appears elsewhere with more distance.

## Related Documentation

- [Ahrefs Link Opportunities Process](../../seo/ahrefs-link-opportunities-process.md) - Complete process documentation
- [Shared Patterns](../../../.cursor/rules/shared-patterns.mdc) - Universal patterns and guidelines
- [Blog Content Flow Guidelines](./CONTENT_FLOW_GUIDELINES.md) - Content structure guidelines
