├── .editorconfig ├── .gitignore ├── README.md ├── adonisrc.ts ├── assets ├── app.css └── app.js ├── bin ├── build.ts ├── download_sponsors.ts └── serve.ts ├── content ├── .DS_Store ├── config.json └── docs │ ├── db.json │ └── introduction.md ├── package-lock.json ├── package.json ├── public └── _redirects ├── src ├── bootstrap.ts └── collections.ts ├── templates ├── docs.edge ├── elements │ └── img.edge ├── layouts │ └── main.edge └── partials │ ├── detect_color_mode.edge │ ├── logo.edge │ ├── logo_mobile.edge │ └── sponsors.edge ├── tsconfig.json ├── vite.config.js └── vscode_grammars ├── dotenv.tmLanguage.json └── main.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies and AdonisJS build 2 | node_modules 3 | build 4 | 5 | # Secrets 6 | .env 7 | .env.local 8 | .env.production.local 9 | .env.development.local 10 | 11 | # Frontend assets compiled code 12 | public/hot 13 | public/build 14 | 15 | # Build tools specific 16 | npm-debug.log 17 | yarn-error.log 18 | 19 | # Editors specific 20 | .fleet 21 | .idea 22 | .vscode 23 | 24 | # Static export 25 | dist 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docs boilerplate 2 | 3 | The boilerplate repo we use across AdonisJS projects to create a documentation website. The boilerplate allows for maximum customization without getting verbose. 4 | 5 | ## Why not use something like VitePress? 6 | I have never been a big fan of a frontend first tooling when rendering markdown files to static HTML. I still remember the Gridsome and Gatsby days, when it was considered normal to use GraphQL to build a static website 😇. 7 | 8 | With that said, the [feature set around rendering markdown](https://vitepress.dev/guide/markdown) feels modern and refreshing with frontend tooling. But, the underlying libraries are not limited to the frontend ecosystem, and you can use them within any JavaScript project. 9 | 10 | So, if I have all the tools at my disposal, why not build and use something simple that does not change with the new wave of innovation in the frontend ecosystem? 11 | 12 | ## Workflow 13 | The docs boilerplate is built around the following workflow requirements. 14 | 15 | - Create a highly customizable markdown rendering pipeline. I need control over rendering every markdown element and tweaking its HTML output per my requirements. This is powered by [@dimerapp/markdown](https://github.com/dimerapp/markdown) and [@dimerapp/edge](https://github.com/dimerapp/edge) packages. 16 | 17 | - Use [Shiki](https://github.com/shikijs/shiki) for styling codeblocks. Shiki uses VSCode themes and grammar for syntax highlighting and requires zero frontend code. 18 | 19 | - Use a [base HTML and CSS theme](https://github.com/dimerapp/docs-theme) to avoid re-building documentation websites from scratch every time. But still allow customizations to add personality to each website. 20 | 21 | - Use a dumb JSON file to render the docs sidebar (JSON database file). Scanning files & folders and sorting them by some convention makes refactoring a lot harder. 22 | 23 | - Allow linking to markdown files and auto-resolve their URLs when rendering to HTML. 24 | 25 | - Allow keeping images and videos next to markdown content and auto-resolve their URLs when rendering to HTML. 26 | 27 | ## Folder structure 28 | 29 | ``` 30 | . 31 | ├── assets 32 | │ ├── app.css 33 | │ └── app.js 34 | ├── bin 35 | │ ├── build.ts 36 | │ └── serve.ts 37 | ├── content 38 | │ ├── docs 39 | │ └── config.json 40 | ├── src 41 | │ ├── bootstrap.ts 42 | │ └── collections.ts 43 | ├── templates 44 | │ ├── elements 45 | │ ├── layouts 46 | │ ├── partials 47 | │ └── docs.edge 48 | ├── vscode_grammars 49 | │ ├── dotenv.tmLanguage.json 50 | │ └── main.ts 51 | ├── package-lock.json 52 | ├── package.json 53 | ├── README.md 54 | ├── tsconfig.json 55 | └── vite.config.js 56 | ``` 57 | 58 | ### The assets directory 59 | 60 | The `assets` directory has the CSS and frontend JavaScript entry point files. Mainly, we import additional packages and the [base theme](https://github.com/dimerapp/docs-theme) inside these files. However, feel free to tweak these files to create a more personalized website. 61 | 62 | ### The bin directory 63 | 64 | The `bin` directory has two script files to start the development server and export the docs to static HTML files. These scripts boot the AdonisJS framework under the hood. 65 | 66 | ### The content directory 67 | The `content` directory contains the markdown and database JSON files. We organize markdown files into collections, each with its database file. 68 | 69 | You can think of collections as different documentation areas on the website. For example: You can create a **collection for docs**, a **collection for API** reference, and a **collection for config reference**. 70 | 71 | See also: [Creating new collections](#creating-new-collections) 72 | 73 | ### The src directory 74 | The `src` directory has a `bootstrap` file to wire everything together. We do not hide the bootstrap process inside some packages. This is because we want the final projects to have complete control over configuring, pulling in extra packages, or removing unused features. 75 | 76 | The `collections.ts` file is used to define one or more collections. 77 | 78 | ### The templates directory 79 | The `templates` directory contains the Edge templates used for rendering HTML. 80 | 81 | - The `docs` template renders a conventional documentation layout with the header, sidebar, content, and table of contents. You may use the same template across multiple collections. 82 | - The logos are kept as SVG inside the `partials/logo.edge` and `partials/logo_mobile.edge` files. 83 | - The base HTML fragment is part of the `layouts/main.edge` file. Feel free to add custom meta tags or scripts/fonts inside this file. 84 | 85 | ### The vscode_grammars directory 86 | The `vscode_grammars` directory contains a collection of custom VSCode languages you want to use inside your project. 87 | 88 | See also: [Using custom VSCode grammars](#using-custom-vscode-grammars) 89 | 90 | ## Usage 91 | Clone the repo from Github. We recommend using [degit](https://www.npmjs.com/package/degit), which downloads the repo without git history. 92 | 93 | ```sh 94 | npx degit dimerapp/docs-boilerplate 95 | ``` 96 | 97 | Install dependencies 98 | 99 | ```sh 100 | cd 101 | npm i 102 | ``` 103 | 104 | Run the development server. 105 | 106 | ```sh 107 | npm run dev 108 | ``` 109 | 110 | And visit [http://localhost:3333/docs/introduction](http://localhost:3333/docs/introduction) URL to view the website in the browser. 111 | 112 | ## Adding content 113 | By default, we create a `docs` collection with an `introduction.md` file inside it. 114 | 115 | As a first step, you should open the `content/docs/db.json` file and add all the entries for your documentation. Defining entries by hand may feel tedious at first, but it will allow easier customization in the future. 116 | 117 | A typical database entry has the following properties. 118 | 119 | ```json 120 | { 121 | "permalink": "introduction", 122 | "title": "Introduction", 123 | "contentPath": "./introduction.md", 124 | "category": "Guides" 125 | } 126 | ``` 127 | 128 | - `permalink`: The unique URL for the doc. The collection prefix will be applied to the permalink automatically. See the `src/collection.ts` file for the collection prefix. 129 | - `title`: The title to display in the sidebar. 130 | - `contentPath`: A relative path to the markdown file. 131 | - `category`: The grouping category for the doc. 132 | 133 | Once you have defined all the entries, create markdown files and write some real content. 134 | 135 | ## Changing website config 136 | 137 | We use a very minimal configuration file to update certain website sections. The config is stored inside the `content/config.json` file. 138 | 139 | ```json 140 | { 141 | "links": { 142 | "home": { 143 | "title": "Your project name", 144 | "href": "/" 145 | }, 146 | "github": { 147 | "title": "Your project on Github", 148 | "href": "https://github.com/dimerapp" 149 | } 150 | }, 151 | "fileEditBaseUrl": "https://github.com/dimerapp/docs-boilerplate/blob/develop", 152 | "copyright": "Your project legal name" 153 | } 154 | ``` 155 | 156 | - `links`: The object has two fixed links. The homepage and the Github project URL. 157 | 158 | - `fileEditBaseUrl`: The base URL for the file on Github. This is used inside the content footer to display the **Edit on Github** link. 159 | 160 | - `copyright`: The name of display in the Copyright footer. 161 | 162 | - `menu`: Optionally, you can define a header menu as an array of objects. 163 | ```json 164 | { 165 | "menu": [ 166 | { 167 | "href": "/docs/introduction", 168 | "title": "Docs", 169 | }, 170 | { 171 | "href": "https://blog.project.com", 172 | "title": "Blog", 173 | }, 174 | { 175 | "href": "https://github.com/project/releases", 176 | "title": "Releases", 177 | } 178 | ] 179 | } 180 | ``` 181 | 182 | - `search`: Optionally, you can define config for the Algolia search. 183 | ```json 184 | { 185 | "search": { 186 | "appId": "", 187 | "indexName": "", 188 | "apiKey": "" 189 | } 190 | } 191 | ``` 192 | 193 | ## Creating new collections 194 | You may create multiple collections by defining them inside the `src/collections.ts` file. 195 | 196 | A collection is defined using the `Collection` class. The class accepts the URL to the database file. Also, call `collection.boot` once you have configured the collection. 197 | 198 | ```ts 199 | // Docs 200 | const docs = new Collection() 201 | .db(new URL('../content/docs/db.json', import.meta.url)) 202 | .useRenderer(renderer) 203 | .urlPrefix('/docs') 204 | 205 | await docs.boot() 206 | 207 | // API reference 208 | const apiReference = new Collection() 209 | .db(new URL('../content/api_reference/db.json', import.meta.url)) 210 | .useRenderer(renderer) 211 | .urlPrefix('/api') 212 | 213 | await apiReference.boot() 214 | 215 | export const collections = [docs, apiReference] 216 | ``` 217 | 218 | ## Using custom VSCode grammar 219 | You may add custom VSCode languages support by defining them inside the `vscode_grammars/main.ts` file. Each entry must adhere to the `ILanguageRegistration` interface from [Shiki](https://github.com/shikijs/shiki/blob/main/docs/languages.md). 220 | 221 | ## Changing the markdown code blocks theme 222 | 223 | The code blocks theme is defined using the Markdown renderer instance created inside the `src/bootstrap.ts` file. You can either use one of the [pre-defined themes or a custom theme](https://github.com/dimerapp/shiki/tree/next#using-different-themes). 224 | 225 | ```ts 226 | export const renderer = new Renderer(view, pipeline) 227 | .codeBlocksTheme('material-theme-palenight') 228 | ``` 229 | 230 | ## Customizing CSS 231 | 232 | The [base docs theme](https://github.com/dimerapp/docs-theme) makes extensive use of CSS variables, therefore you can tweak most of the styling by defining a new set of variables. 233 | 234 | If you want to change colors, we recommend looking at [Radix Colors](https://www.radix-ui.com/docs/colors/getting-started/installation), because this is what we have used for the default styling. 235 | 236 | ## Customizing HTML 237 | 238 | The HTML output is not 100% customizable since we are not creating a generic docs generator for the rest of the world. The boilerplate is meant to be used under constraints. 239 | 240 | However, you can still control the layout, because all sections of the page are exported as Edge component and you can place them anywhere in the DOM. Do check the `templates/docs.edge` file to see how everything is used. 241 | 242 | ### Header slots 243 | 244 | You may pass the following component slots to the website header. 245 | 246 | - `logo (required)`: Content for the logo to display on Desktop viewport. 247 | 248 | - `logoMobile (required)`: Content for the logo to display on Mobile viewport. 249 | 250 | - `popupMenu (optional)`: Define custom markup for the popup menu trigger. The 251 | trigger is displayed in mobile view only. 252 | ```edge 253 | @component('docs::header', contentConfig) 254 | @slots('popMenu') 255 | Open popup menu 256 | @end 257 | @end 258 | ``` 259 | 260 | - `themeSwitcher (optional)`: Define custom markup for the theme switcher button. 261 | ```edge 262 | @component('docs::header', contentConfig) 263 | @slots('themeSwitcher') 264 | Dark 265 | Light 266 | @end 267 | @end 268 | ``` 269 | 270 | - `github (optional)`: Define custom markup for the github link in the header. 271 | ```edge 272 | @component('docs::header', contentConfig) 273 | @slots('github') 274 | Github (11K+ Stars) 275 | @end 276 | @end 277 | ``` 278 | 279 | ## Deployment 280 | The docs boilerplate allows you to create a static build and deploy it on any CDN including Netlify, Cloudflare pages and so on. 281 | 282 | You can create the static build using the `npm run export` command. The command runs the following actions. 283 | 284 | - Create a production build for assets using Vite. 285 | - Convert collections to HTML pages. 286 | - Copy everything to the `dist` directory. 287 | 288 | Once the build is created, you can deploy the `dist` directory as it has everything to serve the website. 289 | 290 | ### Environment variables 291 | 292 | When creating the build, you must set the `APP_URL` environment variable to generate correct links for the `og:url` and `twitter:url` meta tags. The env variable should point to the production URL of your website. 293 | -------------------------------------------------------------------------------- /adonisrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@adonisjs/core/app' 2 | 3 | export default defineConfig({ 4 | typescript: true, 5 | directories: { 6 | views: 'templates' 7 | }, 8 | providers: [ 9 | () => import('@adonisjs/core/providers/app_provider'), 10 | () => import('@adonisjs/core/providers/edge_provider'), 11 | () => import('@adonisjs/vite/vite_provider'), 12 | () => import('@adonisjs/static/static_provider'), 13 | ], 14 | metaFiles: [ 15 | { 16 | pattern: './public/**/*', 17 | reloadServer: false 18 | } 19 | ] 20 | }) 21 | -------------------------------------------------------------------------------- /assets/app.css: -------------------------------------------------------------------------------- 1 | @import 'unpoly'; 2 | @import '@docsearch/css'; 3 | @import '@dimerapp/docs-theme/styles'; 4 | -------------------------------------------------------------------------------- /assets/app.js: -------------------------------------------------------------------------------- 1 | import 'unpoly' 2 | import Alpine from 'alpinejs' 3 | import mediumZoom from 'medium-zoom' 4 | import docsearch from '@docsearch/js' 5 | import { tabs } from 'edge-uikit/tabs' 6 | import Persist from '@alpinejs/persist' 7 | import { 8 | initZoomComponent, 9 | initBaseComponents, 10 | initSearchComponent, 11 | } from '@dimerapp/docs-theme/scripts' 12 | 13 | import.meta.glob(['../content/**/*.png', '../content/**/*.jpeg', '../content/**/*.jpg']) 14 | 15 | Alpine.plugin(tabs) 16 | Alpine.plugin(Persist) 17 | Alpine.plugin(initBaseComponents) 18 | Alpine.plugin(initSearchComponent(docsearch)) 19 | Alpine.plugin(initZoomComponent(mediumZoom)) 20 | Alpine.start() 21 | -------------------------------------------------------------------------------- /bin/build.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Development server entrypoint 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The "server.ts" file is the entrypoint for starting the AdonisJS HTTP 7 | | server. Either you can run this file directly or use the "serve" 8 | | command to run this file and monitor file changes 9 | | 10 | */ 11 | 12 | import 'reflect-metadata' 13 | import { Ignitor } from '@adonisjs/core' 14 | import { defineConfig } from '@adonisjs/vite' 15 | 16 | /** 17 | * URL to the application root. AdonisJS need it to resolve 18 | * paths to file and directories for scaffolding commands 19 | */ 20 | const APP_ROOT = new URL('../', import.meta.url) 21 | 22 | /** 23 | * The importer is used to import files in context of the 24 | * application. 25 | */ 26 | const IMPORTER = (filePath: string) => { 27 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 28 | return import(new URL(filePath, APP_ROOT).href) 29 | } 30 | return import(filePath) 31 | } 32 | 33 | /** 34 | * Exports collection to HTML files 35 | */ 36 | async function exportHTML() { 37 | const { collections } = await import('#src/collections') 38 | const { default: ace } = await import('@adonisjs/core/services/ace') 39 | const { default: app } = await import('@adonisjs/core/services/app') 40 | 41 | for (let collection of collections) { 42 | for (let entry of collection.all()) { 43 | try { 44 | const output = await entry.writeToDisk(app.makePath('dist'), { collection, entry }) 45 | ace.ui.logger.action(`create ${output.filePath}`).succeeded() 46 | } catch (error) { 47 | ace.ui.logger.action(`create ${entry.permalink}`).failed(error) 48 | } 49 | } 50 | } 51 | } 52 | 53 | const application = new Ignitor(APP_ROOT, { importer: IMPORTER }) 54 | .tap((app) => { 55 | app.initiating(() => { 56 | app.useConfig({ 57 | appUrl: process.env.APP_URL || '', 58 | app: { 59 | appKey: 'zKXHe-Ahdb7aPK1ylAJlRgTefktEaACi', 60 | http: {}, 61 | }, 62 | logger: { 63 | default: 'app', 64 | loggers: { 65 | app: { 66 | enabled: true, 67 | }, 68 | }, 69 | }, 70 | vite: defineConfig({}), 71 | }) 72 | }) 73 | }) 74 | .createApp('console') 75 | 76 | await application.init() 77 | await application.boot() 78 | await application.start(exportHTML) 79 | -------------------------------------------------------------------------------- /bin/download_sponsors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Script to download sponsors 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This script downloads the sponsors JSON from the pre-configured URLs 7 | | configured inside the "content/config.json" file. 8 | | 9 | */ 10 | 11 | import { request } from 'undici' 12 | import { readFile, writeFile } from 'node:fs/promises' 13 | 14 | /** 15 | * The file path to the config.json file 16 | */ 17 | const CONFIG_FILE_PATH = new URL('../content/config.json', import.meta.url) 18 | 19 | /** 20 | * The file path to the sponsors.json file. The output will be written 21 | * here. 22 | */ 23 | const SPONSORS_FILE_PATH = new URL('../content/sponsors.json', import.meta.url) 24 | 25 | export async function downloadSponsors() { 26 | console.log('starting to download sponsors...') 27 | 28 | try { 29 | const fileContents = await readFile(CONFIG_FILE_PATH, 'utf-8') 30 | const sources = JSON.parse(fileContents).sponsors_sources 31 | let sponsorsList: any[] = [] 32 | 33 | /** 34 | * No sources configured. So going to create an empty 35 | * sponsors.json file. 36 | */ 37 | if (sources.length === 0) { 38 | console.log('skipping download. No sources found...') 39 | await writeFile(SPONSORS_FILE_PATH, JSON.stringify(sponsorsList)) 40 | return 41 | } 42 | 43 | /** 44 | * Processing sponsors 45 | */ 46 | for (let source of sources) { 47 | const { body } = await request(source) 48 | const sponsors = await body.json() 49 | if (Array.isArray(sponsors)) { 50 | sponsorsList = sponsorsList.concat(sponsors) 51 | console.log(`Downloaded "${sponsors.length} sponsors" from "${source}"`) 52 | } 53 | } 54 | 55 | await writeFile(SPONSORS_FILE_PATH, JSON.stringify(sponsorsList)) 56 | } catch (error) { 57 | if (error.code === 'ENOENT') { 58 | console.warn('Cannot download sponsors list. Unable to find "content/config.json" file') 59 | return 60 | } 61 | 62 | console.error(error) 63 | } 64 | } 65 | 66 | await downloadSponsors() 67 | -------------------------------------------------------------------------------- /bin/serve.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Development server entrypoint 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The "server.ts" file is the entrypoint for starting the AdonisJS HTTP 7 | | server. Either you can run this file directly or use the "serve" 8 | | command to run this file and monitor file changes 9 | | 10 | */ 11 | 12 | import 'reflect-metadata' 13 | import { Ignitor } from '@adonisjs/core' 14 | import { readFile } from 'node:fs/promises' 15 | import { defineConfig } from '@adonisjs/vite' 16 | import type { ApplicationService } from '@adonisjs/core/types' 17 | import { defineConfig as defineHttpConfig } from '@adonisjs/core/http' 18 | 19 | /** 20 | * URL to the application root. AdonisJS need it to resolve 21 | * paths to file and directories for scaffolding commands 22 | */ 23 | const APP_ROOT = new URL('../', import.meta.url) 24 | 25 | /** 26 | * The importer is used to import files in context of the 27 | * application. 28 | */ 29 | const IMPORTER = (filePath: string) => { 30 | if (filePath.startsWith('./') || filePath.startsWith('../')) { 31 | return import(new URL(filePath, APP_ROOT).href) 32 | } 33 | return import(filePath) 34 | } 35 | 36 | /** 37 | * Defining routes for development server 38 | */ 39 | async function defineRoutes(app: ApplicationService) { 40 | const { default: server } = await import('@adonisjs/core/services/server') 41 | const { collections } = await import('#src/collections') 42 | const { default: router } = await import('@adonisjs/core/services/router') 43 | 44 | server.use([() => import('@adonisjs/static/static_middleware')]) 45 | const redirects = await readFile(app.publicPath('_redirects'), 'utf-8') 46 | const redirectsCollection = redirects.split('\n').reduce( 47 | (result, line) => { 48 | const [from, to] = line.split(' ') 49 | result[from] = to 50 | return result 51 | }, 52 | {} as Record 53 | ) 54 | 55 | router.get('*', async ({ request, response }) => { 56 | if (redirectsCollection[request.url()]) { 57 | return response.redirect(redirectsCollection[request.url()]) 58 | } 59 | 60 | for (let collection of collections) { 61 | await collection.refresh() 62 | const entry = collection.findByPermalink(request.url()) 63 | if (entry) { 64 | return entry.render({ collection, entry }).catch((error) => { 65 | console.log(error) 66 | }) 67 | } 68 | } 69 | 70 | return response.notFound('Page not found') 71 | }) 72 | } 73 | 74 | new Ignitor(APP_ROOT, { importer: IMPORTER }) 75 | .tap((app) => { 76 | app.initiating(() => { 77 | app.useConfig({ 78 | appUrl: process.env.APP_URL || '', 79 | app: { 80 | appKey: 'zKXHe-Ahdb7aPK1ylAJlRgTefktEaACi', 81 | http: defineHttpConfig({}), 82 | }, 83 | static: { 84 | enabled: true, 85 | etag: true, 86 | lastModified: true, 87 | dotFiles: 'ignore', 88 | }, 89 | logger: { 90 | default: 'app', 91 | loggers: { 92 | app: { 93 | enabled: true, 94 | }, 95 | }, 96 | }, 97 | vite: defineConfig({}), 98 | }) 99 | }) 100 | 101 | app.starting(defineRoutes) 102 | }) 103 | .httpServer() 104 | .start() 105 | .catch(console.error) 106 | -------------------------------------------------------------------------------- /content/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimerapp/docs-boilerplate/ca017fa8a197990560ebaecf2552cdebe2bc4615/content/.DS_Store -------------------------------------------------------------------------------- /content/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "home": { 4 | "title": "Your project name", 5 | "href": "/" 6 | }, 7 | "github": { 8 | "title": "Your project on Github", 9 | "href": "https://github.com/dimerapp" 10 | } 11 | }, 12 | "sponsors_sources": [ 13 | ], 14 | "search": { 15 | }, 16 | "advertising_sponsors": [ 17 | { 18 | "link": "", 19 | "logo": "", 20 | "logo_dark": "", 21 | "logo_styles": "" 22 | } 23 | ], 24 | "fileEditBaseUrl": "https://github.com/dimerapp/docs-boilerplate/blob/develop", 25 | "copyright": "Your project legal name" 26 | } 27 | -------------------------------------------------------------------------------- /content/docs/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "permalink": "introduction", 4 | "title": "Introduction", 5 | "contentPath": "./introduction.md", 6 | "category": "Guides" 7 | } 8 | ] -------------------------------------------------------------------------------- /content/docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Docs boilerplate 2 | 3 | The boilerplate repo we use across AdonisJS projects to create a documentation website. The boilerplate allows for maximum customization without getting verbose. 4 | 5 | ## Why not use something like VitePress? 6 | I have never been a big fan of a frontend first tooling when rendering markdown files to static HTML. I still remember the Gridsome and Gatsby days, when it was considered normal to use GraphQL to build a static website 😇. 7 | 8 | With that said, the [feature set around rendering markdown](https://vitepress.dev/guide/markdown) feels modern and refreshing with frontend tooling. But, the underlying libraries are not limited to the frontend ecosystem, and you can use them within any JavaScript project. 9 | 10 | So, if I have all the tools at my disposal, why not build and use something simple that does not change with the new wave of innovation in the frontend ecosystem? 11 | 12 | ## Workflow 13 | The docs boilerplate is built around the following workflow requirements. 14 | 15 | - Create a highly customizable markdown rendering pipeline. I need control over rendering every markdown element and tweaking its HTML output per my requirements. This is powered by [@dimerapp/markdown](https://github.com/dimerapp/markdown) and [@dimerapp/edge](https://github.com/dimerapp/edge) packages. 16 | 17 | - Use [Shiki](https://github.com/shikijs/shiki) for styling codeblocks. Shiki uses VSCode themes and grammar for syntax highlighting and requires zero frontend code. 18 | 19 | - Use a [base HTML and CSS theme](https://github.com/dimerapp/docs-theme) to avoid re-building documentation websites from scratch every time. But still allow customizations to add personality to each website. 20 | 21 | - Use a dumb JSON file to render the docs sidebar (JSON database file). Scanning files & folders and sorting them by some convention makes refactoring a lot harder. 22 | 23 | - Allow linking to markdown files and auto-resolve their URLs when rendering to HTML. 24 | 25 | - Allow keeping images and videos next to markdown content and auto-resolve their URLs when rendering to HTML. 26 | 27 | ## Folder structure 28 | 29 | ``` 30 | . 31 | ├── assets 32 | │ ├── app.css 33 | │ └── app.js 34 | ├── bin 35 | │ ├── build.ts 36 | │ └── serve.ts 37 | ├── content 38 | │ ├── docs 39 | │ └── config.json 40 | ├── src 41 | │ ├── bootstrap.ts 42 | │ └── collections.ts 43 | ├── templates 44 | │ ├── elements 45 | │ ├── layouts 46 | │ ├── partials 47 | │ └── docs.edge 48 | ├── vscode_grammars 49 | │ ├── dotenv.tmLanguage.json 50 | │ └── main.ts 51 | ├── package-lock.json 52 | ├── package.json 53 | ├── README.md 54 | ├── tsconfig.json 55 | └── vite.config.js 56 | ``` 57 | 58 | ### The assets directory 59 | 60 | The `assets` directory has the CSS and frontend JavaScript entry point files. Mainly, we import additional packages and the [base theme](https://github.com/dimerapp/docs-theme) inside these files. However, feel free to tweak these files to create a more personalized website. 61 | 62 | ### The bin directory 63 | 64 | The `bin` directory has two script files to start the development server and export the docs to static HTML files. These scripts boot the AdonisJS framework under the hood. 65 | 66 | ### The content directory 67 | The `content` directory contains the markdown and database JSON files. We organize markdown files into collections, each with its database file. 68 | 69 | You can think of collections as different documentation areas on the website. For example: You can create a **collection for docs**, a **collection for API** reference, and a **collection for config reference**. 70 | 71 | See also: [Creating new collections](#creating-new-collections) 72 | 73 | ### The src directory 74 | The `src` directory has a `bootstrap` file to wire everything together. We do not hide the bootstrap process inside some packages. This is because we want the final projects to have complete control over configuring, pulling in extra packages, or removing unused features. 75 | 76 | The `collections.ts` file is used to define one or more collections. 77 | 78 | ### The templates directory 79 | The `templates` directory contains the Edge templates used for rendering HTML. 80 | 81 | - The `docs` template renders a conventional documentation layout with the header, sidebar, content, and table of contents. You may use the same template across multiple collections. 82 | - The logos are kept as SVG inside the `partials/logo.edge` and `partials/logo_mobile.edge` files. 83 | - The base HTML fragment is part of the `layouts/main.edge` file. Feel free to add custom meta tags or scripts/fonts inside this file. 84 | 85 | ### The vscode_grammars directory 86 | The `vscode_grammars` directory contains a collection of custom VSCode languages you want to use inside your project. 87 | 88 | See also: [Using custom VSCode grammars](#using-custom-vscode-grammars) 89 | 90 | ## Usage 91 | Clone the repo from Github. We recommend using [degit](https://www.npmjs.com/package/degit), which downloads the repo without git history. 92 | 93 | ```sh 94 | npx degit dimerapp/docs-boilerplate 95 | ``` 96 | 97 | Install dependencies 98 | 99 | ```sh 100 | cd 101 | npm i 102 | ``` 103 | 104 | Run the development server. 105 | 106 | ```sh 107 | npm run dev 108 | ``` 109 | 110 | And visit [http://localhost:3333/docs/introduction](http://localhost:3333/docs/introduction) URL to view the website in the browser. 111 | 112 | ## Adding content 113 | By default, we create a `docs` collection with an `introduction.md` file inside it. 114 | 115 | As a first step, you should open the `content/docs/db.json` file and add all the entries for your documentation. Defining entries by hand may feel tedious at first, but it will allow easier customization in the future. 116 | 117 | A typical database entry has the following properties. 118 | 119 | ```json 120 | { 121 | "permalink": "introduction", 122 | "title": "Introduction", 123 | "contentPath": "./introduction.md", 124 | "category": "Guides" 125 | } 126 | ``` 127 | 128 | - `permalink`: The unique URL for the doc. The collection prefix will be applied to the permalink automatically. See the `src/collection.ts` file for the collection prefix. 129 | - `title`: The title to display in the sidebar. 130 | - `contentPath`: A relative path to the markdown file. 131 | - `category`: The grouping category for the doc. 132 | 133 | Once you have defined all the entries, create markdown files and write some real content. 134 | 135 | ## Changing website config 136 | 137 | We use a very minimal configuration file to update certain website sections. The config is stored inside the `content/config.json` file. 138 | 139 | ```json 140 | { 141 | "links": { 142 | "home": { 143 | "title": "Your project name", 144 | "href": "/" 145 | }, 146 | "github": { 147 | "title": "Your project on Github", 148 | "href": "https://github.com/dimerapp" 149 | } 150 | }, 151 | "fileEditBaseUrl": "https://github.com/dimerapp/docs-boilerplate/blob/develop", 152 | "copyright": "Your project legal name" 153 | } 154 | ``` 155 | 156 | - `links`: The object has two fixed links. The homepage and the Github project URL. 157 | 158 | - `fileEditBaseUrl`: The base URL for the file on Github. This is used inside the content footer to display the **Edit on Github** link. 159 | 160 | - `copyright`: The name of display in the Copyright footer. 161 | 162 | - `menu`: Optionally, you can define a header menu as an array of objects. 163 | ```json 164 | { 165 | "menu": [ 166 | { 167 | "href": "/docs/introduction", 168 | "title": "Docs", 169 | }, 170 | { 171 | "href": "https://blog.project.com", 172 | "title": "Blog", 173 | }, 174 | { 175 | "href": "https://github.com/project/releases", 176 | "title": "Releases", 177 | } 178 | ] 179 | } 180 | ``` 181 | 182 | - `search`: Optionally, you can define config for the Algolia search. 183 | ```json 184 | { 185 | "search": { 186 | "appId": "", 187 | "indexName": "", 188 | "apiKey": "" 189 | } 190 | } 191 | ``` 192 | 193 | ## Creating new collections 194 | You may create multiple collections by defining them inside the `src/collections.ts` file. 195 | 196 | A collection is defined using the `Collection` class. The class accepts the URL to the database file. Also, call `collection.boot` once you have configured the collection. 197 | 198 | ```ts 199 | // Docs 200 | const docs = new Collection() 201 | .db(new URL('../content/docs/db.json', import.meta.url)) 202 | .useRenderer(renderer) 203 | .urlPrefix('/docs') 204 | 205 | await docs.boot() 206 | 207 | // API reference 208 | const apiReference = new Collection() 209 | .db(new URL('../content/api_reference/db.json', import.meta.url)) 210 | .useRenderer(renderer) 211 | .urlPrefix('/api') 212 | 213 | await apiReference.boot() 214 | 215 | export const collections = [docs, apiReference] 216 | ``` 217 | 218 | ## Using custom VSCode grammar 219 | You may add custom VSCode languages support by defining them inside the `vscode_grammars/main.ts` file. Each entry must adhere to the `ILanguageRegistration` interface from [Shiki](https://github.com/shikijs/shiki/blob/main/docs/languages.md). 220 | 221 | ## Changing the markdown code blocks theme 222 | 223 | The code blocks theme is defined using the Markdown renderer instance created inside the `src/bootstrap.ts` file. You can either use one of the [pre-defined themes or a custom theme](https://github.com/dimerapp/shiki/tree/next#using-different-themes). 224 | 225 | ```ts 226 | export const renderer = new Renderer(view, pipeline) 227 | .codeBlocksTheme('material-theme-palenight') 228 | ``` 229 | 230 | ## Customizing CSS 231 | 232 | The [base docs theme](https://github.com/dimerapp/docs-theme) makes extensive use of CSS variables, therefore you can tweak most of the styling by defining a new set of variables. 233 | 234 | If you want to change colors, we recommend looking at [Radix Colors](https://www.radix-ui.com/docs/colors/getting-started/installation), because this is what we have used for the default styling. 235 | 236 | ## Customizing HTML 237 | 238 | The HTML output is not 100% customizable since we are not creating a generic docs generator for the rest of the world. The boilerplate is meant to be used under constraints. 239 | 240 | However, you can still control the layout, because all sections of the page are exported as Edge component and you can place them anywhere in the DOM. Do check the `templates/docs.edge` file to see how everything is used. 241 | 242 | ### Header slots 243 | 244 | You may pass the following component slots to the website header. 245 | 246 | - `logo (required)`: Content for the logo to display on Desktop viewport. 247 | 248 | - `logoMobile (required)`: Content for the logo to display on Mobile viewport. 249 | 250 | - `popupMenu (optional)`: Define custom markup for the popup menu trigger. The 251 | trigger is displayed in mobile view only. 252 | ```edge 253 | @component('docs::header', contentConfig) 254 | @slots('popMenu') 255 | Open popup menu 256 | @end 257 | @end 258 | ``` 259 | 260 | - `themeSwitcher (optional)`: Define custom markup for the theme switcher button. 261 | ```edge 262 | @component('docs::header', contentConfig) 263 | @slots('themeSwitcher') 264 | Dark 265 | Light 266 | @end 267 | @end 268 | ``` 269 | 270 | - `github (optional)`: Define custom markup for the github link in the header. 271 | ```edge 272 | @component('docs::header', contentConfig) 273 | @slots('github') 274 | Github (11K+ Stars) 275 | @end 276 | @end 277 | ``` 278 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonisjs-web-stater-kit", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "export": "vite build && npm run download:sponsors && node --loader=ts-node/esm bin/build.ts", 9 | "postexport": "copyfiles -u 1 public/* public/**/* dist", 10 | "download:sponsors": "node --loader=ts-node/esm bin/download_sponsors.ts", 11 | "start": "node bin/test.js", 12 | "serve": "node --loader=ts-node/esm bin/serve.ts", 13 | "dev": "concurrently \"vite\" \"npm run serve\"", 14 | "test": "node ace test" 15 | }, 16 | "imports": { 17 | "#src/*": "./src/*.js" 18 | }, 19 | "devDependencies": { 20 | "@adonisjs/assembler": "^7.1.1", 21 | "@adonisjs/tsconfig": "^1.2.1", 22 | "@adonisjs/vite": "^2.0.2", 23 | "@alpinejs/persist": "^3.13.5", 24 | "@dimerapp/content": "^5.0.0", 25 | "@dimerapp/docs-theme": "^4.0.4", 26 | "@dimerapp/edge": "^5.0.0", 27 | "@docsearch/css": "^3.5.2", 28 | "@docsearch/js": "^3.5.2", 29 | "@swc/core": "^1.4.1", 30 | "@types/node": "^20.11.17", 31 | "alpinejs": "^3.13.5", 32 | "collect.js": "^4.36.1", 33 | "concurrently": "^8.2.2", 34 | "copyfiles": "^2.4.1", 35 | "edge-uikit": "^1.0.0-1", 36 | "medium-zoom": "^1.1.0", 37 | "pino-pretty": "^10.3.1", 38 | "reflect-metadata": "^0.2.1", 39 | "ts-node": "^10.9.2", 40 | "typescript": "^5.3.3", 41 | "undici": "^6.6.2", 42 | "unpoly": "^3.7.3", 43 | "vite": "^5.1.2" 44 | }, 45 | "dependencies": { 46 | "@adonisjs/core": "^6.2.3", 47 | "@adonisjs/static": "^1.1.1", 48 | "edge.js": "^6.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | / /docs/introduction 2 | /docs /docs/introduction 3 | -------------------------------------------------------------------------------- /src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Bootstrap 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The bootstrap file configures everything needed to render markdown with 7 | | extreme control over the rendering pipeline 8 | | 9 | */ 10 | 11 | import edge from 'edge.js' 12 | import uiKit from 'edge-uikit' 13 | import collect from 'collect.js' 14 | import { dimer } from '@dimerapp/edge' 15 | import { readFile } from 'node:fs/promises' 16 | import { RenderingPipeline } from '@dimerapp/edge' 17 | import { Collection, Renderer } from '@dimerapp/content' 18 | import { docsHook, docsTheme } from '@dimerapp/docs-theme' 19 | 20 | import grammars from '../vscode_grammars/main.js' 21 | 22 | type CollectionEntry = Exclude, undefined> 23 | 24 | edge.use(dimer) 25 | edge.use(docsTheme) 26 | edge.use(uiKit) 27 | 28 | /** 29 | * Globally loads the config file 30 | */ 31 | edge.global('getConfig', async () => 32 | JSON.parse(await readFile(new URL('../content/config.json', import.meta.url), 'utf-8')) 33 | ) 34 | 35 | /** 36 | * Globally loads the sponsors file 37 | */ 38 | edge.global('getSponsors', async () => 39 | JSON.parse(await readFile(new URL('../content/sponsors.json', import.meta.url), 'utf-8')) 40 | ) 41 | 42 | /** 43 | * Returns sections for a collection 44 | */ 45 | edge.global('getSections', function (collection: Collection, entry: CollectionEntry) { 46 | const entries = collection.all() 47 | 48 | return collect(entries) 49 | .groupBy('meta.category') 50 | .map((items, key) => { 51 | return { 52 | title: key, 53 | isActive: entry.meta.category === key, 54 | items: items 55 | .filter((item: CollectionEntry & { draft?: boolean }) => { 56 | return !item.meta.draft 57 | }) 58 | .map((item: CollectionEntry) => { 59 | return { 60 | href: item.permalink, 61 | title: item.title, 62 | isActive: item.permalink === entry.permalink, 63 | } 64 | }) 65 | .all(), 66 | } 67 | }) 68 | .all() 69 | }) 70 | 71 | /** 72 | * Configuring rendering pipeline 73 | */ 74 | const pipeline = new RenderingPipeline() 75 | pipeline.use(docsHook).use((node) => { 76 | if (node.tagName === 'img') { 77 | return pipeline.component('elements/img', { node }) 78 | } 79 | }) 80 | 81 | // 'css-variables' | 'dark-plus' | 'dracula-soft' | 'dracula' | 'github-dark-dimmed' | 'github-dark' | 'github-light' | 'hc_light' | 'light-plus' | 'material-theme-darker' | 'material-theme-lighter' | 'material-theme-ocean' | 'material-theme-palenight' | 'material-theme' | 'min-dark' | 'min-light' | 'monokai' | 'nord' | 'one-dark-pro' | 'poimandres' | 'rose-pine-dawn' | 'rose-pine-moon' | 'rose-pine' | 'slack-dark' | 'slack-ochin' | 'solarized-dark' | 'solarized-light' | 'vitesse-dark' | 'vitesse-light'; 82 | 83 | /** 84 | * Configuring renderer 85 | */ 86 | export const renderer = new Renderer(edge, pipeline) 87 | .codeBlocksTheme('material-theme-palenight') 88 | .useTemplate('docs') 89 | 90 | /** 91 | * Adding grammars 92 | */ 93 | grammars.forEach((grammar) => renderer.registerLanguage(grammar)) 94 | -------------------------------------------------------------------------------- /src/collections.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Collections 4 | |-------------------------------------------------------------------------- 5 | | 6 | | Collections represents multiple sources of documentation. For example: 7 | | Guides can be one collection, blog can be another, and API docs can 8 | | be another collection 9 | | 10 | */ 11 | 12 | import { Collection } from '@dimerapp/content' 13 | import { renderer } from './bootstrap.js' 14 | 15 | const docs = new Collection() 16 | .db(new URL('../content/docs/db.json', import.meta.url)) 17 | .useRenderer(renderer) 18 | .urlPrefix('/docs') 19 | 20 | await docs.boot() 21 | 22 | export const collections = [docs] 23 | -------------------------------------------------------------------------------- /templates/docs.edge: -------------------------------------------------------------------------------- 1 | @component('layouts/main', { file }) 2 | @let(siteConfig = await getConfig()) 3 | 4 | @component('docs::header', siteConfig) 5 | @slot('logo') 6 | @include('partials/logo') 7 | @end 8 | 9 | @slot('logoMobile') 10 | @include('partials/logo_mobile') 11 | @end 12 | @end 13 | 14 |
15 | @!component('docs::sidebar', { 16 | collapsible: false, 17 | sections: getSections(collection, entry) 18 | }) 19 | 20 |
21 | @!component('docs::content_header', { title: file.frontmatter.title }) 22 | 23 | @component('docs::content', { 24 | fileEditUrl: `${siteConfig.fileEditBaseUrl}/${app.relativePath(file.filePath)}`, 25 | copyright: siteConfig.copyright 26 | }) 27 | @!component('docs::doc_errors', { messages: file.messages }) 28 | @!component('dimer_contents', { nodes: file.ast.children, pipeline })~ 29 | @end 30 | 31 | @if(file.toc) 32 | @component('docs::toc', { sponsors: siteConfig.advertising_sponsors }) 33 | @!component('dimer_element', { node: file.toc, pipeline })~ 34 | @end 35 | @end 36 |
37 |
38 | @end 39 | -------------------------------------------------------------------------------- /templates/elements/img.edge: -------------------------------------------------------------------------------- 1 |
2 | @if(node.properties.src.startsWith('http://') || node.properties.src.startsWith('https://')) 3 | 4 | @else 5 | 6 | @end 7 |
8 | -------------------------------------------------------------------------------- /templates/layouts/main.edge: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | {{ file.frontmatter.title }} 13 | @if(file.frontmatter.summary) 14 | 15 | @end 16 | 17 | 18 | 19 | 20 | @if(file.frontmatter.summary) 21 | 22 | @end 23 | 24 | 25 | 26 | @if(file.frontmatter.summary) 27 | 28 | @end 29 | 30 | 31 | 32 | 36 | 37 | @vite(['assets/app.css', 'assets/app.js']) 38 | @include('partials/detect_color_mode') 39 | 40 | 41 | 42 | {{{ await $slots.main() }}} 43 | 44 | 45 | -------------------------------------------------------------------------------- /templates/partials/detect_color_mode.edge: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /templates/partials/logo.edge: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/partials/logo_mobile.edge: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/partials/sponsors.edge: -------------------------------------------------------------------------------- 1 | @let(sponsors = await getSponsors()) 2 | 3 |
4 |

5 | Explain sponsorship program here 6 |

7 | 8 | {{-- Rendering Github sponsors as per their selected tier --}} 9 | @!component('docs::elements/sponsors', { 10 | sponsors, 11 | tier: 'gold', 12 | title: 'Gold sponsors', 13 | finder: (sponsor) => { 14 | return !sponsor.isOneTime && sponsor.monthlyDollars > 29 15 | } 16 | }) 17 | 18 | @!component('docs::elements/sponsors', { 19 | sponsors, 20 | tier: 'silver', 21 | title: 'Silver sponsors', 22 | finder: (sponsor) => { 23 | return !sponsor.isOneTime && sponsor.monthlyDollars === 29 24 | } 25 | }) 26 | 27 | @!component('docs::elements/sponsors', { 28 | sponsors, 29 | tier: 'basic', 30 | title: 'Sponsors', 31 | finder: (sponsor) => { 32 | return !sponsor.isOneTime && sponsor.monthlyDollars >= 19 && sponsor.monthlyDollars < 29 33 | } 34 | }) 35 | 36 | @!component('docs::elements/sponsors', { 37 | sponsors, 38 | tier: 'basic', 39 | title: 'Backers', 40 | finder: (sponsor) => { 41 | return !sponsor.isOneTime && sponsor.monthlyDollars >= 0 && sponsor.monthlyDollars < 19 42 | } 43 | }) 44 | 45 | @!component('docs::elements/sponsors', { 46 | sponsors, 47 | tier: 'previous', 48 | title: 'Past sponsors', 49 | finder: (sponsor) => { 50 | return sponsor.monthlyDollars === -1 || sponsor.isOneTime 51 | } 52 | }) 53 |
54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.app.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import adonisjs from '@adonisjs/vite/client' 3 | 4 | export default defineConfig({ 5 | plugins: [adonisjs({ 6 | entrypoints: ['./assets/app.js', './assets/app.css'], 7 | reload: ['content/**/*', 'templates/**/*.edge'] 8 | })] 9 | }) 10 | -------------------------------------------------------------------------------- /vscode_grammars/dotenv.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DotENV", 3 | "scopeName": "source.env", 4 | "fileTypes": [ 5 | ".env", 6 | ".env-sample", 7 | ".env.example", 8 | ".env.local", 9 | ".env.dev", 10 | ".env.test", 11 | ".env.testing", 12 | ".env.production", 13 | ".env.prod" 14 | ], 15 | "uuid": "09d4e117-0975-453d-a74b-c2e525473f97", 16 | "patterns": [ 17 | { 18 | "comment": "Comments - starts with #", 19 | "match": "(#).*$\\n?", 20 | "name": "comment.line.number-sign.env", 21 | "captures": { 22 | "1": { 23 | "name": "punctuation.definition.comment.env" 24 | } 25 | } 26 | }, 27 | { 28 | "comment": "Strings (double)", 29 | "name": "string.quoted.double.env", 30 | "begin": "(\\\")", 31 | "beginCaptures": { 32 | "1": { 33 | "name": "punctuation.definition.string.begin.env" 34 | } 35 | }, 36 | "patterns": [ 37 | { 38 | "include": "#interpolation" 39 | }, 40 | { 41 | "include": "#variable" 42 | }, 43 | { 44 | "include": "#escape-characters" 45 | } 46 | ], 47 | "end": "(\\\")", 48 | "endCaptures": { 49 | "1": { 50 | "name": "punctuation.definition.string.end" 51 | } 52 | } 53 | }, 54 | { 55 | "comment": "Strings (single)", 56 | "name": "string.quoted.single.env", 57 | "begin": "(\\')", 58 | "beginCaptures": { 59 | "1": { 60 | "name": "punctuation.definition.string.begin.env" 61 | } 62 | }, 63 | "end": "(\\')", 64 | "endCaptures": { 65 | "1": { 66 | "name": "punctuation.definition.string.end" 67 | } 68 | } 69 | }, 70 | { 71 | "comment": "Assignment Operator", 72 | "match": "(?<=[\\w])\\s?=", 73 | "name": "keyword.operator.assignment.env" 74 | }, 75 | { 76 | "comment": "Variable", 77 | "match": "([\\w]+)(?=\\s?\\=)", 78 | "name": "variable.other.env" 79 | }, 80 | { 81 | "comment": "Keywords", 82 | "match": "(?i)\\s?(export)", 83 | "name": "keyword.other.env" 84 | }, 85 | { 86 | "comment": "Constants", 87 | "match": "(?i)(?<=\\=)\\s?(true|false|null)", 88 | "name": "constant.language.env" 89 | }, 90 | { 91 | "comment": "Numeric", 92 | "match": "\\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b", 93 | "name": "constant.numeric.env" 94 | } 95 | ], 96 | "repository": { 97 | "interpolation": { 98 | "comment": "Template Syntax: \"foo ${bar} {$baz}\"", 99 | "begin": "(\\$\\{|\\{)", 100 | "beginCaptures": { 101 | "1": { 102 | "name": "string.interpolated.env keyword.other.template.begin.env" 103 | } 104 | }, 105 | "patterns": [ 106 | { 107 | "match": "(?x)(\\$+)?([a-zA-Z_\\x{7f}-\\x{ff}][a-zA-Z0-9_\\x{7f}-\\x{ff}]*?\\b)", 108 | "captures": { 109 | "1": { 110 | "name": "punctuation.definition.variable.env variable.other.env" 111 | }, 112 | "2": { 113 | "name": "variable.other.env" 114 | } 115 | } 116 | } 117 | ], 118 | "end": "(\\})", 119 | "endCaptures": { 120 | "1": { 121 | "name": "string.interpolated.env keyword.other.template.end.env" 122 | } 123 | } 124 | }, 125 | "variable": { 126 | "patterns": [ 127 | { 128 | "match": "(?x)(\\$+)([a-zA-Z_\\x{7f}-\\x{ff}][a-zA-Z0-9_\\x{7f}-\\x{ff}]*?\\b)", 129 | "captures": { 130 | "1": { 131 | "name": "punctuation.definition.variable.env variable.other.env" 132 | }, 133 | "2": { 134 | "name": "variable.other.env" 135 | } 136 | } 137 | } 138 | ] 139 | }, 140 | "escape-characters": { 141 | "patterns": [ 142 | { 143 | "match": "\\\\[nrt\\\\\\$\\\"\\']", 144 | "name": "constant.character.escape.env" 145 | } 146 | ] 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /vscode_grammars/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | VSCode grammars 4 | |-------------------------------------------------------------------------- 5 | | 6 | | Export any custom VSCode languages from this file that you want to 7 | | use inside markdown codeblocks. 8 | | 9 | */ 10 | 11 | import { fileURLToPath } from 'node:url' 12 | import type { ILanguageRegistration } from '@dimerapp/shiki' 13 | 14 | export default [ 15 | { 16 | path: fileURLToPath(new URL('./dotenv.tmLanguage.json', import.meta.url)), 17 | scopeName: 'source.env', 18 | id: 'dotenv', 19 | }, 20 | ] satisfies ILanguageRegistration[] 21 | --------------------------------------------------------------------------------