Skip to content

feat: add Markdown support for job descriptions and implement MarkdownDescription component#39

Merged
JoachimLK merged 1 commit intomainfrom
feat/markdown-support
Feb 24, 2026
Merged

feat: add Markdown support for job descriptions and implement MarkdownDescription component#39
JoachimLK merged 1 commit intomainfrom
feat/markdown-support

Conversation

@JoachimLK
Copy link
Copy Markdown
Contributor

@JoachimLK JoachimLK commented Feb 24, 2026

Summary

  • What does this PR change?
  • Why is this needed?

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features
    • Job descriptions now support markdown formatting, enabling richer text presentation and improved readability across job listings, detail pages, and application pages.

@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-39 February 24, 2026 07:33 Destroyed
@railway-app
Copy link
Copy Markdown

railway-app Bot commented Feb 24, 2026

🚅 Deployed to the applirank-pr-39 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Feb 24, 2026 at 7:36 am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

A new Vue component MarkdownDescription is introduced to render markdown content using the MDC library. The component is integrated across three job display pages (dashboard, job listing, and job apply), replacing plain text rendering. A utility function converts markdown to plain text for SEO metadata generation. The @nuxtjs/mdc dependency is added to support markdown rendering.

Changes

Cohort / File(s) Summary
Markdown Rendering Component
app/components/MarkdownDescription.vue, package.json
New Vue component for rendering markdown with MDC, configured to disable HTML, syntax highlighting, and table of contents. New dependency @nuxtjs/mdc ^0.20.1 added.
Job Pages Updated
app/pages/dashboard/jobs/[id]/index.vue, app/pages/jobs/[slug]/apply.vue, app/pages/jobs/[slug]/index.vue
Replaced plain text rendering of job descriptions with the MarkdownDescription component. Added markdown-to-plain-text conversion utility in the jobs slug index page for SEO metadata generation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 Hop, skip, jump! Markdown's here to stay,
Plain text be gone, descriptions now display,
With MDC magic, our pages transform,
Rendered prose breaks the ordinary norm! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing the required information. The template sections are present but completely unfilled—no summary of changes or reasons, no type of change selected, and no validation checkboxes marked. Fill in the Summary section with what was changed and why, select the appropriate Type of change (likely Feature), and check relevant Validation items to confirm testing and any necessary documentation updates.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding Markdown support for job descriptions and implementing a new MarkdownDescription component, which aligns with the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/markdown-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/jobs/[slug]/apply.vue (1)

17-21: ⚠️ Potential issue | 🟡 Minor

SEO description still passes raw Markdown to the meta tag.

Line 19 slices job.value?.description directly, so markdown syntax characters (##, **, -, etc.) will appear verbatim in the <meta name="description"> tag and in social card previews. The companion index.vue in this same PR addresses this with the markdownToPlainText helper — apply the same treatment here for consistency.

🐛 Proposed fix

Extract markdownToPlainText to a shared utility (e.g. ~/utils/markdownToPlainText.ts) and import it in both pages, then:

+import { markdownToPlainText } from '~/utils/markdownToPlainText'
+
 useSeoMeta({
   title: computed(() => job.value ? `Apply — ${job.value.title}` : 'Apply — Applirank'),
-  description: computed(() => job.value?.description?.slice(0, 160) ?? 'Submit your application'),
+  description: computed(() => markdownToPlainText(job.value?.description).slice(0, 160) || 'Submit your application'),
   robots: 'noindex, nofollow',
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/jobs/`[slug]/apply.vue around lines 17 - 21, The SEO description is
passing raw Markdown (job.value?.description) into useSeoMeta; update the
computed description in the useSeoMeta call to convert markdown to plain text
before slicing by importing and using the markdownToPlainText helper (the same
utility used by the index.vue page) so the computed (() => job.value ? ... )
uses markdownToPlainText(job.value.description) then slice(0,160) with fallback
text, leaving robots unchanged.
🧹 Nitpick comments (2)
app/pages/jobs/[slug]/index.vue (1)

16-31: Extract markdownToPlainText to a shared utility to avoid duplication.

The same helper is needed in apply.vue (and potentially other pages). Defining it locally inside <script setup> means it either gets copy-pasted or apply.vue continues to use raw markdown in its meta description (as is currently the case).

Move to app/utils/markdownToPlainText.ts and import via ~/utils/markdownToPlainText in both index.vue and apply.vue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/jobs/`[slug]/index.vue around lines 16 - 31, Extract the local
function markdownToPlainText into a shared utility module (export it as the
default or named export from a new markdownToPlainText utility) and replace the
inline definition in index.vue with an import of that utility; then update
apply.vue to import and use the same function so both pages call the shared
markdownToPlainText (ensure the implementation is identical and preserve current
regex behavior and return type).
app/components/MarkdownDescription.vue (1)

9-20: Move the static parserOptions object to module scope.

parserOptions is a constant, non-reactive value but is currently declared inside <script setup>, so a new object is allocated on every component instantiation. Move it to module scope (outside <script setup>) so it is created once.

♻️ Proposed refactor
+import type { MDCParseOptions } from '@nuxtjs/mdc'
+
+const parserOptions: MDCParseOptions = {
+  rehype: {
+    options: { allowDangerousHtml: false },
+    plugins: { 'rehype-raw': false },
+  },
+  highlight: false,
+  toc: false,
+}
+
 <script setup lang="ts">
-import type { MDCParseOptions } from '@nuxtjs/mdc'
-import MDC from '@nuxtjs/mdc/runtime/components/MDC.vue'
+// MDC is auto-imported via the `@nuxtjs/mdc` Nuxt module
 
 const props = defineProps<{
   value?: string | null
 }>()
 
-const parserOptions: MDCParseOptions = {
-  rehype: {
-    options: { allowDangerousHtml: false },
-    plugins: { 'rehype-raw': false },
-  },
-  highlight: false,
-  toc: false,
-}
-
 const normalizedValue = computed(() => props.value ?? '')
 </script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/MarkdownDescription.vue` around lines 9 - 20, The
parserOptions constant is being recreated for every component instance because
it’s declared inside <script setup>; move the parserOptions declaration to
module scope (outside the <script setup> block) so it is allocated once at
module load; locate the parserOptions symbol in MarkdownDescription.vue and
cut/paste the entire const parserOptions: MDCParseOptions = { ... } to the
top-level of the module (above the <script setup> block), keeping its type and
imports intact and leaving the component code to reference parserOptions
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/MarkdownDescription.vue`:
- Around line 25-30: The component renders untrusted markdown but isn't wrapped
in a NuxtErrorBoundary; wrap the MDC usage inside a <NuxtErrorBoundary> in
MarkdownDescription.vue so rendering errors are caught and a fallback is shown,
e.g., provide a fallback-slot or default error content for the boundary; update
the template that currently renders <MDC :value="normalizedValue"
:parser-options="parserOptions" .../> to be the child of NuxtErrorBoundary and
ensure the boundary exposes a user-friendly fallback for failures.
- Line 29: The prose container's class uses old Tailwind v3 modifier ordering
(`hover:prose-a:text-brand-700`) which applies hover to the whole container;
update the modifier order to Tailwind v4 syntax so link hover targets individual
anchors (replace the `hover:prose-a:text-brand-700` token with
`prose-a:hover:text-brand-700` in the class string inside the
MarkdownDescription.vue component) so only the hovered <a> receives the color
change.
- Line 3: Add the '@nuxtjs/mdc' module to the Nuxt config and remove the private
runtime component import: open nuxt.config.ts and add '@nuxtjs/mdc' to the
modules array so the package is registered and its components are auto-imported,
then in app/components/MarkdownDescription.vue delete the explicit import line
"import MDC from '@nuxtjs/mdc/runtime/components/MDC.vue'" so the <MDC>
component uses the global auto-imported version.

In `@app/pages/jobs/`[slug]/index.vue:
- Around line 16-31: The function markdownToPlainText currently strips all
underscores via .replace(/[*_~]/g, ''), which destroys snake_case; replace that
blanket removal with targeted regexes that only remove markdown
emphasis/strong/strikethrough markers while preserving underscores inside words.
Concretely, in markdownToPlainText replace the .replace(/[*_~]/g, '') step with
targeted replacements such as removing strong markers
.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2'), removing emphasis markers
.replace(/(\*|_)(?=\S)([\s\S]*?\S)\1/g, '$2'), and removing strikethrough
markers .replace(/(~{2})(?=\S)([\s\S]*?\S)\1/g, '$2') so underscores within
words (e.g., snake_case) remain unchanged.

In `@package.json`:
- Around line 21-22: The package.json contains an explicit "@nuxtjs/mdc"
dependency that is redundant because "@nuxt/content" already includes it; either
remove the "@nuxtjs/mdc" entry from package.json to avoid duplication, or if you
intentionally use MDC beyond markdown rendering, add "'@nuxtjs/mdc'" to the
modules array in nuxt.config.ts so the module is registered (check the modules
array symbol in nuxt.config.ts) and keep package.json accordingly.

---

Outside diff comments:
In `@app/pages/jobs/`[slug]/apply.vue:
- Around line 17-21: The SEO description is passing raw Markdown
(job.value?.description) into useSeoMeta; update the computed description in the
useSeoMeta call to convert markdown to plain text before slicing by importing
and using the markdownToPlainText helper (the same utility used by the index.vue
page) so the computed (() => job.value ? ... ) uses
markdownToPlainText(job.value.description) then slice(0,160) with fallback text,
leaving robots unchanged.

---

Nitpick comments:
In `@app/components/MarkdownDescription.vue`:
- Around line 9-20: The parserOptions constant is being recreated for every
component instance because it’s declared inside <script setup>; move the
parserOptions declaration to module scope (outside the <script setup> block) so
it is allocated once at module load; locate the parserOptions symbol in
MarkdownDescription.vue and cut/paste the entire const parserOptions:
MDCParseOptions = { ... } to the top-level of the module (above the <script
setup> block), keeping its type and imports intact and leaving the component
code to reference parserOptions unchanged.

In `@app/pages/jobs/`[slug]/index.vue:
- Around line 16-31: Extract the local function markdownToPlainText into a
shared utility module (export it as the default or named export from a new
markdownToPlainText utility) and replace the inline definition in index.vue with
an import of that utility; then update apply.vue to import and use the same
function so both pages call the shared markdownToPlainText (ensure the
implementation is identical and preserve current regex behavior and return
type).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26edd37 and 4961084.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • app/components/MarkdownDescription.vue
  • app/pages/dashboard/jobs/[id]/index.vue
  • app/pages/jobs/[slug]/apply.vue
  • app/pages/jobs/[slug]/index.vue
  • package.json

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { MDCParseOptions } from '@nuxtjs/mdc'
import MDC from '@nuxtjs/mdc/runtime/components/MDC.vue'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check the current state of the file
cat -n app/components/MarkdownDescription.vue

Repository: applirank/applirank

Length of output: 1044


🏁 Script executed:

# Check nuxt.config.ts for `@nuxtjs/mdc` registration
head -100 nuxt.config.ts

Repository: applirank/applirank

Length of output: 3622


🏁 Script executed:

# Search for MDC imports and usage across the codebase
rg -n "MDC|@nuxtjs/mdc" --type vue --type ts app/

Repository: applirank/applirank

Length of output: 90


🏁 Script executed:

# Check the `@nuxtjs/mdc` package structure and exports
find node_modules/@nuxtjs/mdc -name "package.json" | head -1 | xargs cat | grep -A 20 '"exports"'

Repository: applirank/applirank

Length of output: 122


🏁 Script executed:

# Check package.json for `@nuxtjs/mdc` dependency
grep -n "mdc" package.json

Repository: applirank/applirank

Length of output: 95


🏁 Script executed:

# Search for MDC usage with correct syntax (no type flag)
rg -n "MDC|@nuxtjs/mdc" app/

Repository: applirank/applirank

Length of output: 383


🏁 Script executed:

# Check if `@nuxtjs/mdc` is listed as dependency
cat package.json | grep -A 50 '"dependencies"'

Repository: applirank/applirank

Length of output: 865


🌐 Web query:

@nuxtjs/mdc v0.20 MDC component import public API documentation

💡 Result:

For @nuxtjs/mdc v0.20.x, the documented public “component import” / runtime API is here:

  • Nuxt Modules docs for @nuxtjs/mdc (kept in sync with v0.20.x): Rendering section documents the exposed renderer components <MDC>, <MDCRenderer>, <MDCSlot>, and shows importing parseMarkdown from @nuxtjs/mdc/runtime. [1]
  • Upstream repository README (same content): documents the same public components + runtime imports, and notes MDC component resolution like ::alertcomponents/mdc/Alert.vue. [2]

What the docs say is public to import/use

Renderer components (Nuxt app):

  • <MDC />
  • <MDCRenderer />
  • <MDCSlot /> [1][2]

Runtime JS API (importable):

  • parseMarkdown from @nuxtjs/mdc/runtime [1][2]
  • (Also shown in the docs’ “Vue (non-Nuxt) project” section): createMarkdownParser, rehypeHighlight, createShikiHighlighter from @nuxtjs/mdc/runtime, and importing the SFC directly: @nuxtjs/mdc/runtime/components/MDCRenderer.vue. [1][2]

Types:

  • MDCParserResult type import from @nuxtjs/mdc is shown in the docs’ Vue-project example. [1][2]

Sources: [1] [2]


🌐 Web query:

@nuxtjs/mdc nuxt.config registration module setup

💡 Result:

1) Install

npx nuxi@latest module add mdc

(or install manually with your package manager, then configure as below) [1]

2) Register the module in nuxt.config

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc'],
})

This is the standard way to register Nuxt modules via the modules array. [1][2]

3) (Optional) Configure @nuxtjs/mdc

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc'],
  mdc: {
    // e.g. remarkPlugins, rehypePlugins, headings, highlight, components...
  },
})

[1]

Sources: [1] [2]


Register @nuxtjs/mdc module in nuxt.config.ts, then remove private runtime import.

The @nuxtjs/mdc package is installed but not registered in the modules array. Add '@nuxtjs/mdc' to the modules array in nuxt.config.ts. Once registered, the MDC component will be auto-imported globally, and the explicit private import from @nuxtjs/mdc/runtime/components/MDC.vue can be removed.

♻️ Proposed fix — register module and remove private path import

In nuxt.config.ts:

 modules: ['@nuxtjs/seo', '@nuxt/content'],
+        '@nuxtjs/mdc',

In app/components/MarkdownDescription.vue:

 <script setup lang="ts">
 import type { MDCParseOptions } from '@nuxtjs/mdc'
-import MDC from '@nuxtjs/mdc/runtime/components/MDC.vue'

With @nuxtjs/mdc registered, <MDC> will be available as a globally auto-imported component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/MarkdownDescription.vue` at line 3, Add the '@nuxtjs/mdc'
module to the Nuxt config and remove the private runtime component import: open
nuxt.config.ts and add '@nuxtjs/mdc' to the modules array so the package is
registered and its components are auto-imported, then in
app/components/MarkdownDescription.vue delete the explicit import line "import
MDC from '@nuxtjs/mdc/runtime/components/MDC.vue'" so the <MDC> component uses
the global auto-imported version.

Comment on lines +25 to +30
<template>
<MDC
:value="normalizedValue"
:parser-options="parserOptions"
class="prose prose-sm max-w-none prose-headings:mt-5 prose-headings:mb-2 prose-p:my-3 prose-ul:my-3 prose-ol:my-3 prose-li:my-1.5 prose-a:text-brand-600 hover:prose-a:text-brand-700 dark:prose-invert"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing NuxtErrorBoundary wrapper.

The component renders untrusted user-authored markdown. A rendering error in the MDC pipeline (e.g. from malformed content) will bubble up and crash the parent page without a fallback. Wrap with <NuxtErrorBoundary>.

♻️ Proposed fix
 <template>
+  <NuxtErrorBoundary>
     <MDC
       :value="normalizedValue"
       :parser-options="parserOptions"
       class="prose prose-sm max-w-none ..."
     />
+    <template `#error`>
+      <p class="text-sm text-surface-500 whitespace-pre-wrap">{{ normalizedValue }}</p>
+    </template>
+  </NuxtErrorBoundary>
 </template>

As per coding guidelines: "Use NuxtErrorBoundary wrapper to catch component-level errors with fallback UI" (app/components/**/*.vue).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<template>
<MDC
:value="normalizedValue"
:parser-options="parserOptions"
class="prose prose-sm max-w-none prose-headings:mt-5 prose-headings:mb-2 prose-p:my-3 prose-ul:my-3 prose-ol:my-3 prose-li:my-1.5 prose-a:text-brand-600 hover:prose-a:text-brand-700 dark:prose-invert"
/>
<template>
<NuxtErrorBoundary>
<MDC
:value="normalizedValue"
:parser-options="parserOptions"
class="prose prose-sm max-w-none prose-headings:mt-5 prose-headings:mb-2 prose-p:my-3 prose-ul:my-3 prose-ol:my-3 prose-li:my-1.5 prose-a:text-brand-600 hover:prose-a:text-brand-700 dark:prose-invert"
/>
<template `#error`>
<p class="text-sm text-surface-500 whitespace-pre-wrap">{{ normalizedValue }}</p>
</template>
</NuxtErrorBoundary>
</template>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/MarkdownDescription.vue` around lines 25 - 30, The component
renders untrusted markdown but isn't wrapped in a NuxtErrorBoundary; wrap the
MDC usage inside a <NuxtErrorBoundary> in MarkdownDescription.vue so rendering
errors are caught and a fallback is shown, e.g., provide a fallback-slot or
default error content for the boundary; update the template that currently
renders <MDC :value="normalizedValue" :parser-options="parserOptions" .../> to
be the child of NuxtErrorBoundary and ensure the boundary exposes a
user-friendly fallback for failures.

<MDC
:value="normalizedValue"
:parser-options="parserOptions"
class="prose prose-sm max-w-none prose-headings:mt-5 prose-headings:mb-2 prose-p:my-3 prose-ul:my-3 prose-ol:my-3 prose-li:my-1.5 prose-a:text-brand-600 hover:prose-a:text-brand-700 dark:prose-invert"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check Tailwind configuration in nuxt.config.ts
fd nuxt.config -type f | head -5

Repository: applirank/applirank

Length of output: 233


🏁 Script executed:

# Find and examine MarkdownDescription.vue
fd MarkdownDescription.vue -type f

Repository: applirank/applirank

Length of output: 233


🏁 Script executed:

# Check package.json for Tailwind version
cat package.json | grep -A 5 -B 5 tailwind

Repository: applirank/applirank

Length of output: 621


🏁 Script executed:

# Find the file correctly
fd MarkdownDescription

Repository: applirank/applirank

Length of output: 101


🏁 Script executed:

# Check the exact content around line 29
cat -n app/components/MarkdownDescription.vue | sed -n '20,35p'

Repository: applirank/applirank

Length of output: 523


🏁 Script executed:

# Check nuxt.config.ts for Tailwind v4 setup
cat -n nuxt.config.ts | grep -A 10 -B 5 tailwind

Repository: applirank/applirank

Length of output: 1398


🏁 Script executed:

# Search for any Tailwind or typography documentation/comments in the codebase
rg "prose-a|hover:prose" --type vue --type ts -A 2 -B 2

Repository: applirank/applirank

Length of output: 90


🏁 Script executed:

# Check if there are any other prose examples in the codebase
rg "prose prose-" --type vue -A 2 -B 2 | head -50

Repository: applirank/applirank

Length of output: 90


🌐 Web query:

@tailwindcss/typography Tailwind v4 prose-a hover modifier order syntax

💡 Result:

In Tailwind v4 with @tailwindcss/typography, when stacking an element modifier like prose-a:* with a variant like hover:*, you generally put the variant last if you want the variant to apply to the link itself:

  • Hover the <a> itself (most common):

    <article class="prose prose-a:text-cyan-600 prose-a:hover:text-cyan-500">
      <a href="#">Link</a>
    </article>

    This compiles like “a:hover { … }” within prose. [1]

  • Hover the whole prose container to affect links:

    <article class="prose hover:prose-a:text-cyan-500">
      <a href="#">Link</a>
    </article>

    This applies when the container is hovered (not the link specifically). [2]

In Tailwind v3, the recommended stacking order was the opposite (variant first), which is why older examples can look “reversed” after upgrading. [1]

Sources: [1] [2]


Swap prose-a hover modifier order to Tailwind v4 syntax — links styled on individual link hover, not container hover.

In Tailwind CSS v4 with @tailwindcss/typography, the correct modifier order is prose-a:hover:text-* (variant last) to style individual <a> elements on their own hover. The current code hover:prose-a:text-brand-700 uses the v3 syntax and applies the hover effect to the entire prose container instead of individual links, causing all links to change colour when hovering anywhere over the component.

🐛 Fix — swap modifier order to Tailwind v4 syntax
-    class="prose prose-sm max-w-none prose-headings:mt-5 prose-headings:mb-2 prose-p:my-3 prose-ul:my-3 prose-ol:my-3 prose-li:my-1.5 prose-a:text-brand-600 hover:prose-a:text-brand-700 dark:prose-invert"
+    class="prose prose-sm max-w-none prose-headings:mt-5 prose-headings:mb-2 prose-p:my-3 prose-ul:my-3 prose-ol:my-3 prose-li:my-1.5 prose-a:text-brand-600 prose-a:hover:text-brand-700 dark:prose-invert"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/MarkdownDescription.vue` at line 29, The prose container's
class uses old Tailwind v3 modifier ordering (`hover:prose-a:text-brand-700`)
which applies hover to the whole container; update the modifier order to
Tailwind v4 syntax so link hover targets individual anchors (replace the
`hover:prose-a:text-brand-700` token with `prose-a:hover:text-brand-700` in the
class string inside the MarkdownDescription.vue component) so only the hovered
<a> receives the color change.

Comment on lines +16 to +31
function markdownToPlainText(markdown?: string | null): string {
if (!markdown) return ''

return markdown
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`([^`]+)`/g, '$1')
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
.replace(/^#{1,6}\s+/gm, '')
.replace(/^\s{0,3}[-*+]\s+/gm, '')
.replace(/^\s{0,3}\d+\.\s+/gm, '')
.replace(/[*_~]/g, '')
.replace(/\n+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

markdownToPlainText strips underscores from all text, corrupting snake_case terms.

Line 27's character class [*_~] removes every _ character regardless of context. A job description containing terms like full_time, snake_case_variable, or any technical name with underscores will have those underscores silently dropped in the generated SEO metadata. For example, "full_time position" becomes "fulltime position".

A targeted approach that only strips emphasis/bold markers in context would be safer.

🐛 Proposed fix — replace lone emphasis markers only, preserve underscores inside words
-    .replace(/[*_~]/g, '')
+    .replace(/\*\*([^*]+)\*\*/g, '$1')   // bold **text**
+    .replace(/\*([^*]+)\*/g, '$1')        // italic *text*
+    .replace(/~~([^~]+)~~/g, '$1')        // strikethrough ~~text~~
+    .replace(/(?<!\w)_([^_]+)_(?!\w)/g, '$1') // italic _text_ (not mid-word)
+    .replace(/(?<!\w)__([^_]+)__(?!\w)/g, '$1') // bold __text__
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/jobs/`[slug]/index.vue around lines 16 - 31, The function
markdownToPlainText currently strips all underscores via .replace(/[*_~]/g, ''),
which destroys snake_case; replace that blanket removal with targeted regexes
that only remove markdown emphasis/strong/strikethrough markers while preserving
underscores inside words. Concretely, in markdownToPlainText replace the
.replace(/[*_~]/g, '') step with targeted replacements such as removing strong
markers .replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2'), removing emphasis
markers .replace(/(\*|_)(?=\S)([\s\S]*?\S)\1/g, '$2'), and removing
strikethrough markers .replace(/(~{2})(?=\S)([\s\S]*?\S)\1/g, '$2') so
underscores within words (e.g., snake_case) remain unchanged.

Comment thread package.json
Comment on lines 21 to +22
"@nuxt/content": "^3.11.2",
"@nuxtjs/mdc": "^0.20.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine the nuxt configuration
find . -name "nuxt.config.*" -type f 2>/dev/null | head -5

Repository: applirank/applirank

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Check if package-lock.json or yarn.lock exists to see resolved versions
ls -la package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null | head -5

Repository: applirank/applirank

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Examine the current package.json to see the full dependency list
cat -n package.json | head -50

Repository: applirank/applirank

Length of output: 1792


🏁 Script executed:

#!/bin/bash
# Search for `@nuxt/content` in node_modules to check its declared dependencies
find node_modules -path "*/node_modules/@nuxt/content/package.json" -maxdepth 3 2>/dev/null | xargs cat 2>/dev/null | grep -A 5 -B 5 "@nuxtjs/mdc" || echo "File not found or `@nuxtjs/mdc` not listed"

Repository: applirank/applirank

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Read nuxt.config.ts to check for mdc module registration
cat -n nuxt.config.ts

Repository: applirank/applirank

Length of output: 6328


🏁 Script executed:

#!/bin/bash
# Search package-lock.json for `@nuxtjs/mdc` to see resolved versions and dependencies
grep -A 5 -B 2 '"@nuxtjs/mdc"' package-lock.json | head -50

Repository: applirank/applirank

Length of output: 652


🏁 Script executed:

#!/bin/bash
# Check if `@nuxt/content` actually lists `@nuxtjs/mdc` as a dependency in package-lock.json
grep -A 20 '"@nuxt/content": {' package-lock.json | grep -E '(version|@nuxtjs/mdc)' | head -20

Repository: applirank/applirank

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check `@nuxt/content` documentation or package.json to see if it declares mdc as a module auto-import
grep -r "nuxt.modules\|auto.register\|registerModule" node_modules/@nuxt/content --include="*.json" --include="*.ts" --include="*.js" 2>/dev/null | head -20

Repository: applirank/applirank

Length of output: 45


🌐 Web query:

@nuxt/content v3.11 @nuxtjs/mdc auto-registration module setup

💡 Result:

Goal: auto-register Vue components for use in Markdown (MDC) with @nuxt/content v3.11 (and optionally @nuxtjs/mdc)

1) Recommended setup for Nuxt Content v3.11 (Content renders MDC for you)

Install / enable Content:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/content']
})

Nuxt Content lets you use Vue components inside Markdown with MDC syntax. Components placed in components/content/ are available to Markdown without extra work. [1]

Auto-registration rule of thumb

  • For Markdown usage: put components in components/content/ → usable in .md via MDC (::my-component ... ::). [1]
  • If you also want to use those same components in Vue templates via auto-import/global registration: explicitly register that directory as global (Nuxt Content v3 no longer makes components/content global for you outside Markdown). [2]

Example (global registration for using them in Vue templates too):

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/content'],

  components: [
    // your normal components auto-import (default)
    '~/components',

    // make content components globally available in Vue templates too (optional)
    { path: '~/components/content', global: true }
  ]
})

(Using them in Markdown still works either way.) [1][2]

2) If you want to use @nuxtjs/mdc directly (outside Content pages, or custom rendering)

Install and enable:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/mdc']
})

With @nuxtjs/mdc, MDC block components like ::alert ... :: resolve from components/mdc/Alert.vue by convention. [3][4]

3) Minimal directory conventions (quick reference)

  • components/content/* → easiest path for Nuxt Content Markdown components. [1]
  • components/mdc/* → convention used by @nuxtjs/mdc examples (e.g. ::alert). [3][4]

Sources: Nuxt Content Markdown/MDC docs (components/content + global note) [1], Nuxt Content v3 migration note about components/content not being globally registered outside markdown [2], Nuxt MDC module/docs + repo example showing components/mdc/Alert.vue resolution [3][4].


Remove the redundant @nuxtjs/mdc dependency or clarify its purpose and register it in nuxt.config.ts.

@nuxt/content@3.11.2 already includes @nuxtjs/mdc as an internal dependency (both resolve to ^0.20.1 with no version mismatch risk). The explicit @nuxtjs/mdc entry in package.json is redundant—@nuxt/content handles MDC rendering for Markdown files without requiring separate module registration.

If @nuxtjs/mdc is intentionally added for custom MDC usage outside of @nuxt/content, it must be registered in nuxt.config.ts's modules array (line 18). Currently, it is not declared as a module, which means its auto-registration features are inactive.

Action: Either remove @nuxtjs/mdc from package.json if unused, or add '@nuxtjs/mdc' to the modules array if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 21 - 22, The package.json contains an explicit
"@nuxtjs/mdc" dependency that is redundant because "@nuxt/content" already
includes it; either remove the "@nuxtjs/mdc" entry from package.json to avoid
duplication, or if you intentionally use MDC beyond markdown rendering, add
"'@nuxtjs/mdc'" to the modules array in nuxt.config.ts so the module is
registered (check the modules array symbol in nuxt.config.ts) and keep
package.json accordingly.

@JoachimLK JoachimLK merged commit db56cd5 into main Feb 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant