Remark Plugin Implementation Plan for Astro Content

Sources

Implementation Plan for Remark and Rehype Plugins in Astro

1. Configuration Setup

1.1 Astro Config Integration

typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remarkCallouts from './src/utils/markdown/remark-callout-handler';
import remarkAsf from './src/utils/markdown/remark-asf';
import remarkBacklinks from './src/utils/markdown/remark-backlinks';
import remarkImages from './src/utils/markdown/remark-images';

export default defineConfig({
  markdown: {
    remarkPlugins: [
      remarkCallouts,
      [remarkAsf, { /* options */ }],
      remarkBacklinks,
      remarkImages
    ],
    rehypePlugins: [
      // Add any rehype plugins here
    ]
  }
});

2. Plugin Implementation

2.1 Base Plugin Structure

typescript
// src/utils/markdown/remark-callout-handler.ts
import { visit } from 'unist-util-visit';
import type { Plugin } from 'unified';
import type { Root } from 'mdast';
import { detectMarkdownCallouts } from './callouts/detectMarkdownCallouts';
import { transformCalloutStructure } from './callouts/transformCalloutStructure';

const remarkCallouts: Plugin<[], Root> = () => {
  return (tree) => {
    visit(tree, 'blockquote', (node, index, parent) => {
      // 1. Detect callout content
      const calloutData = detectMarkdownCallouts(node);
      if (!calloutData) return;

      // 2. Transform node structure
      transformCalloutStructure(node, calloutData);
    });

    return tree;
  };
};

export default remarkCallouts;

2.2 Detection Phase

typescript
// src/utils/markdown/callouts/detectMarkdownCallouts.ts
export function detectMarkdownCallouts(node: Node) {
  // Extract first line text
  const firstLine = node.children[0]?.value || '';
  const calloutMatch = firstLine.match(/^\[!(\w+)\]\s*(.*)$/);
  if (!calloutMatch) return null;

  // Capture all content
  const content = node.children
    .slice(1)
    .map(child => child.value)
    .join('\n');

  return {
    type: calloutMatch[1],
    title: calloutMatch[2] || calloutMatch[1],
    content
  };
}

2.3 Transformation Phase

typescript
// src/utils/markdown/callouts/transformCalloutStructure.ts
export function transformCalloutStructure(node: Node, data: CalloutData) {
  // Convert blockquote to callout
  node.type = 'callout';
  node.data = {
    hName: 'article',
    hProperties: {
      className: ['callout', `callout-${data.type.toLowerCase()}`],
      'data-type': data.type
    }
  };

  // Structure content
  node.children = [
    {
      type: 'element',
      tagName: 'header',
      properties: { className: ['callout-header'] },
      children: [{ type: 'text', value: data.title }]
    },
    {
      type: 'element',
      tagName: 'div',
      properties: { className: ['callout-content'] },
      children: node.children.slice(1) // Preserve original content structure
    }
  ];
}

3. Integration with Content Collections

3.1 Collection Configuration

typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const vocabularyCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    slug: z.string().optional(),
    aliases: z.array(z.string()).default([])
  })
});

export const collections = {
  vocabulary: vocabularyCollection
};

3.2 Page Integration

astro
---
// src/pages/more-about/[vocabulary].astro
import { getCollection, type CollectionEntry } from 'astro:content';
import Layout from '@layouts/Layout.astro';
import OneArticleOnPage from '@components/articles/OneArticleOnPage.astro';

export async function getStaticPaths() {
  const vocabularyEntries = await getCollection('vocabulary');
  return vocabularyEntries.map(entry => ({
    params: { vocabulary: entry.data.slug || entry.id },
    props: { entry }
  }));
}

const { entry } = Astro.props;
const { Content } = await entry.render();
---

<Layout title={entry.data.title || entry.id}>
  <OneArticleOnPage
    title={entry.data.title || entry.id}
    content={Content}
  />
</Layout>

4. Component Styling

4.1 Base Callout Styles

css
/* src/styles/callouts.css */
.callout {
  border-left: 4px solid var(--callout-color);
  margin: 1.5rem 0;
  padding: 1rem;
  background: var(--callout-bg);
}

.callout-header {
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: var(--callout-header-color);
}

.callout-content {
  color: var(--callout-content-color);
}

/* Type-specific styles */
.callout-note {
  --callout-color: #3b82f6;
  --callout-bg: #eff6ff;
}

.callout-warning {
  --callout-color: #f59e0b;
  --callout-bg: #fffbeb;
}

5. Debug Utilities

5.1 AST Debug Component

astro
---
// src/components/Debug.astro
interface Props {
  ast: any;
}

const { ast } = Astro.props;
---

{import.meta.env.DEV && (
  <pre class="debug-ast">
    {JSON.stringify(ast, null, 2)}
  </pre>
)}

Implementation Steps

  1. Phase 1: Base Setup
    • Configure remark plugins in astro.config.mjs
    • Create basic plugin structure
    • Implement detection logic
  2. Phase 2: Transformation
    • Implement node transformation
    • Add proper HAST conversion
    • Test with simple callouts
  3. Phase 3: Content Integration
    • Update collection configuration
    • Modify page component
    • Test with actual content
  4. Phase 4: Styling & Polish
    • Add base callout styles
    • Implement type-specific styles
    • Add debug utilities

Success Criteria

  1. Callouts are properly detected in markdown
  2. AST transformation preserves all content
  3. Styling is applied correctly
  4. Debug tools show proper transformation
  5. Content collections work seamlessly