← Corpus / lost-in-public / prompt

Remark Plugin Implementation Plan for Astro Content

Create a custom remark plugin to enhance markdown processing in Astro with extended syntax features

Path
prompts/render-logic/Remark-Plugin-Implementation.md
Authors
Michael Staton
Augmented with
Windsurf Cascade on Claude 3.5 Sonnet
Tags
Render-Logic · Extended-Markdown · Remark-Plugin · Astro
Sources

https://www.namchee.dev/posts/upgrading-astro-code-snippets/

https://younagi.dev/blog/remark-card/

[[Tooling/Software Development/Programming Languages/Libraries/Remark.js]]

Implementation Plan for Remark and Rehype Plugins in Astro

1. Configuration Setup

1.1 Astro Config Integration

// 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

// 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

// 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

// 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

// 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

---
// 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

/* 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

---
// 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