-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add Markdown support for job descriptions and implement MarkdownDescription component #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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' | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check Tailwind configuration in nuxt.config.ts
fd nuxt.config -type f | head -5Repository: applirank/applirank Length of output: 233 🏁 Script executed: # Find and examine MarkdownDescription.vue
fd MarkdownDescription.vue -type fRepository: applirank/applirank Length of output: 233 🏁 Script executed: # Check package.json for Tailwind version
cat package.json | grep -A 5 -B 5 tailwindRepository: applirank/applirank Length of output: 621 🏁 Script executed: # Find the file correctly
fd MarkdownDescriptionRepository: 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 tailwindRepository: 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 2Repository: 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 -50Repository: applirank/applirank Length of output: 90 🌐 Web query:
💡 Result: In Tailwind v4 with
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 🐛 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 |
||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Missing 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 ♻️ 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 27's character class 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 |
||
|
|
||
| const jobDescriptionPlain = computed(() => markdownToPlainText(job.value?.description)) | ||
|
|
||
| // ───────────────────────────────────────────── | ||
| // SEO — Meta tags (title, description, OG, Twitter) | ||
| // ───────────────────────────────────────────── | ||
|
|
@@ -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(() => { | ||
|
|
@@ -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, | ||
|
|
@@ -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 --> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check the current state of the file cat -n app/components/MarkdownDescription.vueRepository: applirank/applirank
Length of output: 1044
🏁 Script executed:
# Check nuxt.config.ts for `@nuxtjs/mdc` registration head -100 nuxt.config.tsRepository: applirank/applirank
Length of output: 3622
🏁 Script executed:
Repository: applirank/applirank
Length of output: 90
🏁 Script executed:
Repository: applirank/applirank
Length of output: 122
🏁 Script executed:
Repository: applirank/applirank
Length of output: 95
🏁 Script executed:
Repository: applirank/applirank
Length of output: 383
🏁 Script executed:
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:
<MDC>,<MDCRenderer>,<MDCSlot>, and shows importingparseMarkdownfrom@nuxtjs/mdc/runtime. [1]::alert→components/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):
parseMarkdownfrom@nuxtjs/mdc/runtime[1][2]createMarkdownParser,rehypeHighlight,createShikiHighlighterfrom@nuxtjs/mdc/runtime, and importing the SFC directly:@nuxtjs/mdc/runtime/components/MDCRenderer.vue. [1][2]Types:
MDCParserResulttype import from@nuxtjs/mdcis shown in the docs’ Vue-project example. [1][2]Sources: [1] [2]
🌐 Web query:
@nuxtjs/mdc nuxt.config registration module setup💡 Result:
1) Install
(or install manually with your package manager, then configure as below) [1]
2) Register the module in
nuxt.configThis is the standard way to register Nuxt modules via the
modulesarray. [1][2]3) (Optional) Configure
@nuxtjs/mdc[1]
Sources: [1] [2]
Register
@nuxtjs/mdcmodule innuxt.config.ts, then remove private runtime import.The
@nuxtjs/mdcpackage is installed but not registered in the modules array. Add'@nuxtjs/mdc'to themodulesarray innuxt.config.ts. Once registered, theMDCcomponent will be auto-imported globally, and the explicit private import from@nuxtjs/mdc/runtime/components/MDC.vuecan 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/mdcregistered,<MDC>will be available as a globally auto-imported component.🤖 Prompt for AI Agents