Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions app/components/MarkdownDescription.vue
Original file line number Diff line number Diff line change
@@ -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.


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>

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

⚠️ 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 +25 to +30
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.

</template>
2 changes: 1 addition & 1 deletion app/pages/dashboard/jobs/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ const applicantsInWindow = computed(() =>
<!-- Description -->
<div class="rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 p-5 mb-4">
<h2 class="text-sm font-semibold text-surface-700 dark:text-surface-300 mb-2">Description</h2>
<p v-if="job.description" class="text-sm text-surface-600 dark:text-surface-400 whitespace-pre-wrap">{{ job.description }}</p>
<MarkdownDescription v-if="job.description" :value="job.description" />
<p v-else class="text-sm text-surface-400 dark:text-surface-500 italic">No description provided.</p>
</div>

Expand Down
6 changes: 3 additions & 3 deletions app/pages/jobs/[slug]/apply.vue
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ const typeLabels: Record<string, string> = {
{{ job.location }}
</span>
</div>
<p v-if="job.description" class="mt-4 text-sm text-surface-600 dark:text-surface-400 whitespace-pre-wrap">
{{ job.description }}
</p>
<div v-if="job.description" class="mt-4">
<MarkdownDescription :value="job.description" />
</div>
</div>

<hr class="border-surface-200 dark:border-surface-800 mb-8" />
Expand Down
27 changes: 22 additions & 5 deletions app/pages/jobs/[slug]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@ const { data: job, status: fetchStatus, error: fetchError } = useFetch(
{ key: `public-job-detail-${jobSlug}` },
)

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()
}
Comment on lines +16 to +31
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.


const jobDescriptionPlain = computed(() => markdownToPlainText(job.value?.description))

// ─────────────────────────────────────────────
// SEO — Meta tags (title, description, OG, Twitter)
// ─────────────────────────────────────────────
Expand All @@ -23,7 +42,7 @@ useSeoMeta({
if (!job.value) return 'View job details and apply'
const loc = job.value.location ? ` in ${job.value.location}` : ''
const org = job.value.organizationName ? ` at ${job.value.organizationName}` : ''
return `Apply for ${job.value.title}${org}${loc}. ${job.value.description?.slice(0, 120) ?? ''}`.trim()
return `Apply for ${job.value.title}${org}${loc}. ${jobDescriptionPlain.value.slice(0, 120)}`.trim()
}),
ogTitle: computed(() => job.value ? `${job.value.title} — Hiring Now` : 'Job Details'),
ogDescription: computed(() => {
Expand Down Expand Up @@ -64,7 +83,7 @@ watchEffect(() => {
const posting: Record<string, unknown> = {
'@type': 'JobPosting',
'title': j.title,
'description': j.description ?? j.title,
'description': jobDescriptionPlain.value || j.title,
'datePosted': j.createdAt,
'employmentType': mapEmploymentType(j.type),
'directApply': true,
Expand Down Expand Up @@ -227,9 +246,7 @@ function formatSalary(min?: number | null, max?: number | null, currency?: strin
<!-- Description -->
<div v-if="job.description" class="mb-8">
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-3">About this role</h2>
<div class="text-sm text-surface-700 dark:text-surface-300 leading-relaxed whitespace-pre-wrap">
{{ job.description }}
</div>
<MarkdownDescription :value="job.description" />
</div>

<!-- Custom questions preview -->
Expand Down
Loading