Overview
This template is best used for static sites such as documentation and portfolio showcase that can be benefited by using Markdown.
The stack is comprised of Vue.js and Nuxt.js as the core technology for building the template, Nuxt Content for content management and supplemented by Tailwind CSS as the styling library.
Installation
To get started, clone the project template from GitHub.
git clone https://github.com/data-miner00/nuxt-content-template.git
Next, install the dependencies with Pnpm.
pnpm i
It is crucial to use the Pnpm package manager with the current pnpm-lock.yaml
file for the time being as there are incompatibilities in the new version of Nuxt and Nuxt Content. However, if you managed to run with Npm or Yarn, by all means.
After the installation is completed, start the development server by running the dev
command.
pnpm dev
Pre-writing
Before writing any articles in the .md
file, it is advised to add the frontmatter code that describes the article so that it can be used when laying out the individual page. The frontmatter example can be found at /templates/frontmatter.yml
file. It is essentially an assortment of custom properties that relates to the article.
title: Title of the Article
description: The subtitle describing the title in more details, should be written in sentence-case
topic: Topic
category: Category
authors:
- name: Contributor 1
avatar: contrib_1.png
- name: Contributor 2
avatar: contrib_2.png
tags:
- tutorial
- guide
- cheatsheet
updatedAt: 2022-11-18T11:37:49.432Z
createdAt: 2022-11-18T11:37:49.432Z
The recommended properties are listed as follows:
title
: The title (h1) of the article/topic.description
: A longer definition that describes the intention and objective of the article.category
: The main topic of the article.tags
: A list of tags that are related to the article.createdAt
: The timestamp of article creation.updatedAt
: The timestamp of article modification.
However, feel free to add in more custom fields that makes most sense to you.
To access the fields, they can be obtained from the useAsyncData
composables as follow.
<script setup lang="ts">
const { data } = await useAsyncData(`content-${path}`, () => {
return queryContent(path).findOne();
});
</script>
<template>
<h1>{{ data?.title }}</h1>
</template>
The example above demonstrates the access of the custom title
attribute from the frontmatter of the article. Other defined values can be retrieved similarly.
Writing Content
In Nuxt, every .vue
file inside the /pages
folder maps directly to the path of the file name from root accordingly. For instance, the pages/home.vue
maps to /home
in the application.
With Nuxt Content, it is possible to map .md
file within the /content
folder directly to root as well. This will require a [...slug].vue
file to be placed within the /pages
folder in order to work.
If there are conflicting names between the .md
file and the .vue
file in both /content
and /pages
folder, the .vue
file in the /pages
folder will get the precedence and annulling the declaration of the .md
file.
For the record, this page (/guide
) is written in .md
within the /content
folder that is then rendered with predefined stylings and layout.
Table of Content
By default, Nuxt content renders <h2>
and <h3>
only in the ToC and will skip any <h1>
that is present to the markdown because <h1>
is recognized as the main title of the article or page. Any subsequent headings in the article will have to start from <h2>
semantically. Hence, it is recommended to use headings <h2>
and above when writing.
This template only renders out the <h2>
tags in the table of content. To visualize this, consider the following example of heading structure within an article.
├─ Heading 1
│ ├─ Subheading A
│ ├─ Subheading B
│ └─ Subheading C
├─ Heading 2
│ ├─ Subheading A
│ ├─ Subheading B
│ └─ Subheading C
├─ Heading 3
└─ Heading 4
In the ToC, the contents generated will be as follows.
├─ Heading 1
├─ Heading 2
├─ Heading 3
└─ Heading 4
To customize this behaviour to include the subheading as well, head over to /pages/[...slug].vue
and uncomment the codes that will generate the subheading.
Ordering Content
The default ordering is by alphabetical order. To customize the orderings, prepend the filename with numbers followed by an immediate .
such as 1.Introduction.md
and increment the count for subsequent files.
content/
1.frameworks/
1.vue.md
2.nuxt.md
2.examples/
1.vercel.md
2.netlify.md
3.heroku.md
index.md
Images
This is how a portrait image looks like within the prose. It is left aligned and will extend to the max width available in the prose. Images that will be served alongside with the app can be placed within the public
directory. For instance, images within the /public/images
folder can be accessed via the path /images/img.jpg
directly.
![my cool image](/images/demo.png)
The landscape image will most probably took over the full width of the container if they have a wide enough resolution.
For images that are located in /assets/images
folder, they will need to be processed by the build tools by referencing them from source code by using require
or any form of import before they will be included in the final build output.
<template>
<img :src="require('~/assets/images/img.jpg')" />
</template>
Tables
This is how the default table looks like.
Age | Person 1 | Person 2 | Person 3 | Average |
---|---|---|---|---|
8 | 6 | 7 | 5 | 6 |
10 | 6.5 | 7.5 | 8.5 | 7.5 |
12 | 8 | 9 | 10 | 9 |
Keyboard
Keyboard keys can be represented with the <kbd>
HTML tag such as Esc, Enter and Ctrl.
LaTeX Support
This template also comes with the support for
\begin{equation}
i \hbar \frac{\partial}{\partial t} \Psi \big(\textbf{r}, t) = \left[- \frac{\hbar^2}{2m}\nabla^2 + V(\textbf{r})\right]\Psi(\textbf{r}, t)
\end{equation}
The
Here is another example of matrix rendered with
\begin{pmatrix*}[r]
-1 & 2 & 3 \\
4 & -5 & 6 \\
7 & 8 & -9
\end{pmatrix*}
Styling
This template opens up a lot of room for customization on top of what's currently provided by Nuxt, Nuxt Content, TailwindCSS and some of my touches to make it great.
Content
The content of the article was styled using Tailwind's Typography plugin (@tailwindcss/typography
) that offers a set of pre-defined styles that is enough the make the article sleak and appealing to read through. The style can be customized on the <article>
tag in /pages/[...slug].vue
by using the prose
keyword.
To style an <h2>
inside the article block, prefix the style with prose-h2:
in the article block where prose
is defined in the class.
<article class="prose prose-stone prose-h2:text-blue-400" />
Besides, to modify how the Markdown is translated to Vue components for rendering, create the specific Prose elements within the /components/content
folder. Nuxt will automatically use those elements to render out the Markdown content instead. The list of available overrides includes ProseCode
, ProseH1
, ProseA
etc. The full list can be inspected in the official GitHub repo.
For your reference, examples of the overridden Prose element in this template are ProseCode
, ProseH2
and ProseH3
that can be found in the /components/content
folder.
Custom Components
Nuxt Content allows the creation of custom components that can be embedded to the Markdown script and render it as HTML altogether. There are a couple of syntax that can be used to feature custom components in the Markdown. The first way to do so is by using the MDC syntax.
For inline components, it can be used by prepending a colon in front of the component's name. This will render the component that takes in no props nor children into the page.
:my-component
For block components, it can be used by prefixing the component's name with a double colon, followed by another closing double colon to signify the end of the component. Block component is the component that can accept Markdown content or another component as it's slot content.
::card
Hello world
::
Nesting is supported for the block component as such.
::hero
:::card
A nested card
::card
A super nested card
::
:::
::
To render for a custom named slot inside the component, use the #slot-name
pattern to outline which content to be rendered within which slot.
::card
The slot default text
#description
This will be rendered within the `description` slot in the component.
::
For a custom component that accepts props, simply enclose the desired key-value pairs within a pair of curly braces immediately after the component's name.
::alert{type="warning" color="default"}
The **alert** component
::
Another way of passing props to a custom component is by using the YAML method that is demonstrated as below.
::alert
---
type: warning
color: default
---
The **alert** component
::
Here is a small example on using the custom component with the MDC syntax. The custom component named Card.vue
is used for the following demo.
::card{title="Awesome title!" footer="This is an inconspicuous footer"}
This is a sample paragraph that is **passed** into the custom card's **default slot** for rendering.
::
The code snippet above will have it's content rendered as below.
This is a sample paragraph that is passed into the custom card's default slot for rendering.
Dark Theme
This template uses Tailwind's class-based Dark Mode for theming. A ready to use theme switching component is already provided. It uses the Nuxt Color Mode's useColorMode composable to take care of the theme switching and persisting the theme preference through the browser's Local Storage API. It is incredibly convenient.
const colorMode = useColorMode();
colorMode.preference = "dark";
colorMode.preference = "light";
colorMode.preference = "system";
Code Blocks
The style of the code blocks can be modified to suits your liking by heading over to the content
section in the nuxt.config.ts
configuration file.
export default defineNuxtConfig({
content: {
documentDriven: true,
highlight: {
theme: {
default: "github-light",
dark: "github-dark",
},
preload: ["cpp", "csharp", "rust", "wenyan"],
},
},
});
In the highlight
section, you can choose the theme that will render out the code blocks for both light and dark mode. Nuxt Content uses Shiki as their code highlighter and it comes along with a wide array of popular themes ready to be used.
The syntax highlighting support are only available for a limited set of languages by default such as HTML, JavaScript, TypeScript and Vue. To enable highlighting for the language of choice, just add in the language identifier into the preload
array.
Tailwind in Markdown
Since Tailwind has been configured to watch for styles in .md
files in the tailwind.config.js
, it can be used freely anywhere in the file. For example, if you want to style a particular word with a text-pink-400
Tailwind class, just wrap it inside a HTML span tag and assign the class name to it.
## My Superfluous Title
Lorem <span class="text-pink-400">ipsum</span> dolor sit amet, adispicing elit.
Another way to style an inline text is by using the syntax as shown below. It is simpler and neat than the previous example.
Lorem [ipsum]{.text-pink-400} dolor sit amet, adispicing elit.
Internationalization
Internationalization is already baked into this template with the help of Nuxt i18n. The language switcher component is also provided that allows easy transition between the languages that is available (in this case English and French) seamlessly.
The url of the non-default locale will be prefixed with it's code whereas the default locale does not require prefixing.
Defining Words
To define a word for the intended languages, there is a section in nuxt.config.ts
named vueI18n
within the i18n
object to define them. For instance, to define the world "welcome" for both English and French, create a property called welcome
within their respective locales object inside messages
with their corresponding value will do.
export default defineNuxtConfig({
i18n: {
vueI18n: {
messages: {
en: {
welcome: "Welcome",
},
fr: {
welcome: "Bienvenue",
},
},
},
},
});
The newly defined word can be used anywhere within the project inside the templates
tag by interpolating with the $t
function that takes in the key of the defined word.
<template>
<p>{{ $t("welcome") }}</p>
</template>
With this in place, Nuxt is smart enough to render out "Welcome" or "Bienvenue" correctly when the language context changed.
Locales File Definition
While the above solution of adding new definition of words in the nuxt.config.ts
file works, it can pose a real problem when the vocabulary grows as the file become cumbersome to maintain.
Fortunately, there is another preferred way to store the language definitions in their own, separate JSON file. With this approach, not only it achieves the Single Responsibility Principle, it also drastically improve the maintainability of the files.
Here is how it is configured in nuxt.config.ts
.
export default defineNuxtConfig({
i18n: {
langDir: "locales",
locales: [
{
code: "en",
file: "en.json",
},
{
code: "fr",
file: "fr.json",
},
],
},
});
The above code tells Nuxt to locate English definition in en.json
file and French definition in fr.json
file inside the locales
folder.
I18n in Nuxt Content
To support internationalization for Markdown based contents from Nuxt Content, create a corresponding folder inside the content
folder with the non-default locale's code and imitate the structure of the base folder.
For example, given I have the following file structure that has English contents, the French contents can be housed in the following manner.
From:
├─ content
│ ├─ blogs
│ │ ├─ blog1.md
│ │ └─ blog2.md
│ ├─ demo.md
│ └─ guide.md
To:
├─ content
│ ├─ blogs
│ │ ├─ blog1.md (English)
│ │ └─ blog2.md (English)
│ ├─ fr
│ │ ├─ blogs
│ │ │ ├─ blog1.md (French)
│ │ │ └─ blog2.md (French)
│ │ ├─ demo.md (French)
│ │ └─ guide.md (French)
│ ├─ demo.md (English)
│ └─ guide.md (English)
By doing this, we are utilizing the behaviour of the prefixed URL for non-default locale and it does the trick. Not the most elegant solution but it works for now.
Internationalized Links
To make sure that every links in the website corresponds to its language counterparts, we have to preprocess the links with the useLocalePath
composable. Here is how it looks like in code.
<script setup lang="ts">
const localePath = useLocalePath();
</script>
<template>
<NuxtLink to="/careers">Before i18n</NuxtLink>
<!-- changed to -->
<NuxtLink :to="localePath('/careers')">After i18n</NuxtLink>
</template>
This will ensures when you are in the English context, the link will redirect you to the normal /careers
page whereas if you are in the French context, it will points to /fr/careers
for its French version.
APA Style References
Additionally, this template also comes with a simple APA style citation component that can be utilized in the Markdown file using the MDC syntax. The examples and styles are crafted base on this Scribbr article.
::apa-reference
---
authors:
- Greenhouse, S
date: 2020, July 30
title: The coronavirus pandemic has intensified systemic economic racism against black Americans
publisher: The New Yorker
url: https://www.newyorker.com/news/news-desk/the-pandemic-has-intensified-systemic-economic-racism-against-black-americans
source: newspaper
---
::