An open API service indexing awesome lists of open source software.

https://github.com/evesunmaple/rehype-smart-links

A rehype plugin for Astro that adds different styling for internal and external links
https://github.com/evesunmaple/rehype-smart-links

astro rehype rehype-plugin

Last synced: 28 days ago
JSON representation

A rehype plugin for Astro that adds different styling for internal and external links

Awesome Lists containing this project

README

        

[English](README.md) | [中文](README.zh-CN.md)

# rehype-smart-links

A rehype plugin for Astro that adds different styles to internal and external links:

- Internal links (pointing to existing pages): Default style with customizable class
- Internal links (pointing to non-existent pages): Red style with customizable class (similar to Wikipedia's broken links)
- External links: Adds "↗" icon (or custom content) and sets target="\_blank", with customizable class

## Installation

```bash
# npm
npm install rehype-smart-links

# yarn
yarn add rehype-smart-links

# pnpm
pnpm add rehype-smart-links
```

## Usage

### Basic Configuration

Add the plugin to your Astro configuration:

```js
// astro.config.mjs
import { defineConfig } from "astro/config";
import rehypeSmartLinks from "rehype-smart-links";

export default defineConfig({
markdown: {
rehypePlugins: [
// Basic usage (default settings)
rehypeSmartLinks,

// Or with custom options
[
rehypeSmartLinks,
{
content: { type: "text", value: "↗" },
internalLinkClass: "internal-link",
externalLinkClass: "external-link",
brokenLinkClass: "broken-link",
contentClass: "external-icon",
target: "_blank",
rel: "noopener noreferrer",
publicDir: "./dist",
routesFile: "./.smart-links-routes.json",
includeFileExtensions: ["html", "pdf", "zip"], // Only include specific file types
includeAllFiles: false // Set to true to include all file types
}
]
]
}
});
```

### Two-Phase Build Process (Recommended)

For accurate detection of valid internal links, a two-phase build process is recommended:

#### Method 1: Using the Built-in CLI Command (Recommended)

rehype-smart-links provides a built-in CLI command to simplify the routes file generation process:

1. Add a build script to your `package.json`:

```json
{
"scripts": {
"build:with-routes": "astro build && rehype-smart-links build && astro build"
}
}
```

2. Run the script to execute the two-phase build:

```bash
npm run build:with-routes
```

This command will:

1. First build your site
2. Use the `rehype-smart-links build` command to scan the build output and generate a routes file
3. Build the site again, this time using the generated routes information

The CLI command supports the following options:

```
Options:
-d, --dir Build directory path (default: "./dist")
-o, --output Output path for the routes file (default: "./.smart-links-routes.json")
-a, --all Include all file types (default: false)
-e, --extensions File extensions to include (default: ["html"])
-h, --help Show help information
```

#### Method 2: Using the API Functions

You can also write a custom build script:

1. First build the site and create a routes mapping file:

```js
// In your build script
import { generateRoutesFile } from "rehype-smart-links";

// First perform a preliminary build
await build();

// Then generate a routes file from the build output directory
generateRoutesFile("./dist", "./.smart-links-routes.json", {
includeAllFiles: true, // Include all file types
// Or only include specific file types
includeFileExtensions: ["html", "pdf", "zip"]
});

// Finally perform the final build
await build();
```

2. Add a build script to your `package.json`:

```json
{
"scripts": {
"build": "node ./scripts/build-with-routes.js"
}
}
```

3. Create a build script (e.g., `scripts/build-with-routes.js`):

```js
import { execSync } from "node:child_process";
import { generateRoutesFile } from "rehype-smart-links";

// Phase 1: Initial build
console.log("[PHASE 1] Initial build...");
execSync("astro build", { stdio: "inherit" });

// Generate routes mapping file
console.log("[PHASE 2] Generating routes map...");
generateRoutesFile("./dist", "./.smart-links-routes.json", {
includeAllFiles: true // Include all file types
});

// Phase 2: Build again with routes information
console.log("[PHASE 3] Final build with routes...");
execSync("astro build", { stdio: "inherit" });

console.log("[SUCCESS] Build complete!");
```

## Customizing Link Structure

In addition to adding classes, you can fully customize the HTML structure of the links:

```js
import rehypeSmartLinks from "rehype-smart-links";

export default defineConfig({
markdown: {
rehypePlugins: [
[
rehypeSmartLinks,
{
wrapperTemplate: (node, type, className) => {
// Create tooltip wrapper
if (type === "external") {
// Example structure for external links
const tooltip = {
type: "element",
tagName: "div",
properties: {
className: ["tooltip"],
dataTooltip: "This is an external link"
},
children: [node]
};

// You can also modify the original node
if (className) {
node.properties.className
= [...(node.properties.className || []), className];
}

return tooltip;
}
else if (type === "broken") {
// Example structure for broken links
const wrapper = {
type: "element",
tagName: "span",
properties: {
className: ["broken-link-wrapper"],
dataError: "Page doesn't exist"
},
children: [node]
};

// Add a warning icon
node.children.push({
type: "element",
tagName: "span",
properties: { className: ["warning-icon"] },
children: [{ type: "text", value: "⚠" }]
});

return wrapper;
}

// Only add class for internal links
if (className) {
node.properties.className
= [...(node.properties.className || []), className];
}

return node;
}
}
]
]
}
});
```

This approach allows you to create completely different HTML structures for different types of links, not just add class names, making it ideal for use with component libraries like DaisyUI and TailwindCSS.

## Styling

Add CSS styles for different link types:

```css
/* Default style for internal links */
.internal-link {
/* Custom styles */
}

/* External links with icons */
.external-link {
/* Custom styles */
}
.external-link .external-icon {
margin-left: 0.25em;
font-size: 0.75em;
}

/* Style for broken links (similar to Wikipedia) */
.broken-link {
color: red;
}
```

## Options

| Option | Type | Default | Description |
| ----------------------------- | ------------------------------------ | ------------------------------- | ----------------------------------------------------- |
| `content` | `{ type: string, value: string }` | `{ type: 'text', value: '↗' }` | Content to add after external links |
| `internalLinkClass` | `string` | `'internal-link'` | Class for internal links to existing pages |
| `externalLinkClass` | `string` | `'external-link'` | Class for external links |
| `brokenLinkClass` | `string` | `'broken-link'` | Class for internal links to non-existent pages |
| `contentClass` | `string` | `'external-icon'` | Class for the content element added to external links |
| `target` | `string` | `'_blank'` | Target attribute for external links |
| `rel` | `string` | `'noopener noreferrer'` | Rel attribute for external links |
| `publicDir` | `string` | `'./dist'` | Path to the build output directory |
| `routesFile` | `string` | `'./.smart-links-routes.json'` | Path to the routes mapping file |
| `includeFileExtensions` | `string[]` | `['html']` | List of file extensions to include |
| `includeAllFiles` | `boolean` | `false` | Set to true to include all file types |
| `wrapperTemplate` | `(node, type, className) => Element` | `undefined` | Template function for custom link structure |
| `customInternalLinkTransform` | `(node) => void` | `undefined` | Custom transform function for internal links |
| `customExternalLinkTransform` | `(node) => void` | `undefined` | Custom transform function for external links |
| `customBrokenLinkTransform` | `(node) => void` | `undefined` | Custom transform function for broken links |

## Advanced Customization

### Using Custom Transform Functions

In addition to `wrapperTemplate`, you can use separate transform functions for finer control:

```js
import rehypeSmartLinks from "rehype-smart-links";

// Example custom transform function for external links
function customExternalLinkTransform(node) {
// Add custom icon or structure
node.properties.class = [...(node.properties.class || []), "my-external-link"];
node.properties.target = "_blank";
node.properties.rel = "noopener";

// Add custom SVG icon
const svgIcon = {
type: "element",
tagName: "span",
properties: { class: "custom-icon" },
children: [{ type: "text", value: "🔗" }]
};

node.children.push(svgIcon);
}

export default {
markdown: {
rehypePlugins: [
[
rehypeSmartLinks,
{
customExternalLinkTransform
}
]
]
}
};
```

### Using with TailwindCSS

```js
// Example using TailwindCSS class names
const tailwindWrapper = (node, type, className) => {
// Save original link content
const linkChildren = [...node.children];

// Clear original link content
node.children = [];

if (type === "external") {
// Add Tailwind class names for external links
node.properties.className = ["text-blue-500", "hover:text-blue-700", "inline-flex", "items-center", "gap-1"];

// Add original content
node.children = [
...linkChildren,
{
type: "element",
tagName: "svg",
properties: {
className: ["w-4", "h-4"],
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor"
},
children: [{
type: "element",
tagName: "path",
properties: {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: "2",
d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
},
children: []
}]
}
];

return node;
}
else if (type === "broken") {
// Create broken link wrapper
const wrapper = {
type: "element",
tagName: "span",
properties: {
className: ["group", "relative", "inline-block"]
},
children: [
{
...node,
properties: {
...node.properties,
className: ["text-red-500", "underline", "underline-offset-2", "decoration-wavy", "decoration-red-500"]
},
children: linkChildren
},
{
type: "element",
tagName: "span",
properties: {
className: ["invisible", "group-hover:visible", "absolute", "bottom-full", "left-1/2", "-translate-x-1/2", "bg-red-100", "text-red-800", "text-xs", "px-2", "py-1", "rounded", "whitespace-nowrap"]
},
children: [{ type: "text", value: "Page doesn't exist" }]
}
]
};

return wrapper;
}
else {
// Add Tailwind class names for internal links
node.properties.className = ["text-green-600", "hover:text-green-800", "transition-colors"];
node.children = linkChildren;
return node;
}
};
```

## Testing

This plugin includes a comprehensive test suite to ensure functionality works as expected.

### Running Tests

```bash
# Install dependencies first
npm install

# Run the tests
npm test
```

### Adding Test Cases

If you're experiencing an issue or want to add a new test case:

1. Add a new test case to `tests/cases/testCases.ts` following the existing pattern.

2. Run the tests to verify your test case:

```bash
npm test
```

3. The test report will be generated at `tests/results/report.html` with visual comparison between expected and actual outputs.

### Reporting Issues

If you find a bug or have a feature request, please [open an issue](https://github.com/yourusername/rehype-smart-links/issues) with:

1. A clear description of the problem
2. Steps to reproduce (or ideally, a test case that fails)
3. Expected vs. actual behavior
4. Version information for rehype-smart-links and your environment

Pull requests are always welcome!