1- import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight" ;
2- import pluginRss from "@11ty/eleventy-plugin-rss" ;
3- import eleventyNavigationPlugin from "@11ty/eleventy-navigation" ;
4- import markdownIt from "markdown-it" ;
5- import markdownItAnchor from "markdown-it-anchor" ;
1+ import { readFileSync } from "fs" ;
62import { execSync } from "child_process" ;
7- import { dirname , resolve } from "path" ;
3+ import { dirname , resolve , join } from "path" ;
84import { fileURLToPath } from "url" ;
95
6+ // Import plugin submodules via file paths (workaround for #9:
7+ // virtual templates override local layouts, so we register non-layout
8+ // virtual templates separately. Package only exports "." so we use
9+ // direct file paths.)
10+ import { registerFilters } from "./node_modules/eleventy-plugin-techdoc/lib/filters/index.js" ;
11+ import { registerCollections } from "./node_modules/eleventy-plugin-techdoc/lib/plugins/collections.js" ;
12+ import { registerShortcodes } from "./node_modules/eleventy-plugin-techdoc/lib/shortcodes/index.js" ;
13+ import { configureMarkdown } from "./node_modules/eleventy-plugin-techdoc/lib/plugins/markdown.js" ;
14+ import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight" ;
15+ import rss from "@11ty/eleventy-plugin-rss" ;
16+ import navigation from "@11ty/eleventy-navigation" ;
17+
1018const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
1119const packagesDir = resolve ( __dirname , ".." , "packages" ) ;
1220
13- const supportedLanguages = [ 'en' , 'zh' ] ;
14- const defaultLanguage = 'en' ;
21+ const techdocOptions = {
22+ site : {
23+ name : "dart_node" ,
24+ title : "dart_node - Full-Stack Dart for the JavaScript Ecosystem" ,
25+ url : "https://dartnode.org" ,
26+ description : "Write React, React Native, and Express apps entirely in Dart. One language for frontend, backend, and mobile." ,
27+ author : "dart_node team" ,
28+ themeColor : "#0E7C6B" ,
29+ stylesheet : "/assets/css/styles.css" ,
30+ twitterSite : "@dart_node" ,
31+ twitterCreator : "@dart_node" ,
32+ ogImage : "/assets/images/og-image.png" ,
33+ ogImageWidth : "1200" ,
34+ ogImageHeight : "630" ,
35+ organization : {
36+ name : "dart_node" ,
37+ logo : "/assets/images/og-image.png" ,
38+ sameAs : [
39+ "https://github.com/melbournedeveloper/dart_node" ,
40+ "https://twitter.com/dart_node" ,
41+ "https://pub.dev/publishers/christianfindlay.com/packages"
42+ ]
43+ }
44+ } ,
45+ features : {
46+ blog : true ,
47+ docs : true ,
48+ darkMode : true ,
49+ i18n : true ,
50+ } ,
51+ i18n : {
52+ defaultLanguage : "en" ,
53+ languages : [ "en" , "zh" ] ,
54+ } ,
55+ } ;
1556
1657export default function ( eleventyConfig ) {
17- // Don't use .gitignore to ignore files (we want to process generated docs)
1858 eleventyConfig . setUseGitIgnore ( false ) ;
1959
20- // Configure markdown-it with anchor plugin for header IDs
21- const mdOptions = {
22- html : true ,
23- breaks : false ,
24- linkify : true
25- } ;
60+ // === techdoc plugin features (without layout virtual templates) ===
61+ // We use the plugin's filters, collections, shortcodes, markdown config,
62+ // and bundled plugins, but NOT its layout virtual templates because
63+ // dart_node's layouts are superior (see GitHub issue #9).
2664
27- const mdAnchorOptions = {
28- permalink : markdownItAnchor . permalink . headerLink ( ) ,
29- slugify : ( s ) => s . toLowerCase ( ) . replace ( / \s + / g, '-' ) . replace ( / [ ^ \w - ] + / g, '' ) ,
30- level : [ 1 , 2 , 3 , 4 ]
31- } ;
65+ // Global data (same as plugin sets)
66+ eleventyConfig . addGlobalData ( "techdocOptions" , techdocOptions ) ;
67+ eleventyConfig . addGlobalData ( "supportedLanguages" , techdocOptions . i18n . languages ) ;
68+ eleventyConfig . addGlobalData ( "defaultLanguage" , techdocOptions . i18n . defaultLanguage ) ;
3269
33- const md = markdownIt ( mdOptions ) . use ( markdownItAnchor , mdAnchorOptions ) ;
34- eleventyConfig . setLibrary ( "md" , md ) ;
70+ // Plugin submodules
71+ configureMarkdown ( eleventyConfig ) ;
72+ registerFilters ( eleventyConfig , techdocOptions ) ;
73+ registerCollections ( eleventyConfig , techdocOptions ) ;
74+ registerShortcodes ( eleventyConfig ) ;
3575
36- // Plugins
76+ // Bundled plugins
3777 eleventyConfig . addPlugin ( syntaxHighlight ) ;
38- eleventyConfig . addPlugin ( pluginRss ) ;
39- eleventyConfig . addPlugin ( eleventyNavigationPlugin ) ;
78+ eleventyConfig . addPlugin ( rss ) ;
79+ eleventyConfig . addPlugin ( navigation ) ;
80+
81+ // Plugin structural CSS (no colors - site provides visual styling)
82+ const techdocAssetsDir = join ( __dirname , "node_modules" , "eleventy-plugin-techdoc" , "assets" ) ;
83+ eleventyConfig . addPassthroughCopy ( { [ techdocAssetsDir ] : "techdoc" } ) ;
4084
41- // Passthrough copy for assets
85+ // Register only NON-LAYOUT virtual templates from the plugin
86+ // (feed, sitemap, robots.txt, llms.txt, blog scaffold pages)
87+ // Layouts come from our local src/_includes/layouts/ which are superior.
88+ registerSeoVirtualTemplates ( eleventyConfig ) ;
89+
90+ // === Site-specific config ===
4291 eleventyConfig . addPassthroughCopy ( "src/assets" ) ;
4392 eleventyConfig . addPassthroughCopy ( "src/api" ) ;
44- eleventyConfig . addPassthroughCopy ( "src/robots.txt" ) ;
45- eleventyConfig . addPassthroughCopy ( "src/llms.txt" ) ;
46-
47- // Watch targets
4893 eleventyConfig . addWatchTarget ( "src/assets/" ) ;
4994
50- // Watch READMEs and copy when they change
5195 eleventyConfig . addWatchTarget ( packagesDir ) ;
5296 eleventyConfig . on ( "eleventy.beforeWatch" , ( changedFiles ) => {
5397 if ( changedFiles . some ( f => f . endsWith ( "README.md" ) ) ) {
5498 execSync ( "node scripts/copy-readmes.js" , { stdio : "inherit" } ) ;
5599 }
56100 } ) ;
57101
58- // Collections
59- // English posts only (from src/blog/)
60- eleventyConfig . addCollection ( "posts" , function ( collectionApi ) {
61- return collectionApi . getFilteredByGlob ( "src/blog/*.md" ) . sort ( ( a , b ) => {
62- return b . date - a . date ;
63- } ) ;
64- } ) ;
65-
66- // Chinese posts only (from src/zh/blog/)
67- eleventyConfig . addCollection ( "zhPosts" , function ( collectionApi ) {
68- return collectionApi . getFilteredByGlob ( "src/zh/blog/*.md" ) . sort ( ( a , b ) => {
69- return b . date - a . date ;
70- } ) ;
71- } ) ;
72-
73- eleventyConfig . addCollection ( "docs" , function ( collectionApi ) {
74- return collectionApi . getFilteredByGlob ( "src/docs/**/*.md" ) ;
75- } ) ;
76-
77- // Tag collection - get all unique tags from blog posts
78- eleventyConfig . addCollection ( "tagList" , function ( collectionApi ) {
79- const tagSet = new Set ( ) ;
80- collectionApi . getFilteredByGlob ( "src/blog/*.md" ) . forEach ( post => {
81- ( post . data . tags || [ ] ) . forEach ( tag => {
82- tag !== 'post' && tag !== 'posts' && tagSet . add ( tag ) ;
83- } ) ;
84- } ) ;
85- return [ ...tagSet ] . sort ( ) ;
86- } ) ;
87-
88- // Category collection - get all unique categories from blog posts
89- eleventyConfig . addCollection ( "categoryList" , function ( collectionApi ) {
90- const categorySet = new Set ( ) ;
91- collectionApi . getFilteredByGlob ( "src/blog/*.md" ) . forEach ( post => {
92- post . data . category && categorySet . add ( post . data . category ) ;
93- } ) ;
94- return [ ...categorySet ] . sort ( ) ;
95- } ) ;
96-
97- // Posts by tag - creates a collection for each tag
98- eleventyConfig . addCollection ( "postsByTag" , function ( collectionApi ) {
99- const postsByTag = { } ;
100- collectionApi . getFilteredByGlob ( "src/blog/*.md" ) . forEach ( post => {
101- ( post . data . tags || [ ] ) . forEach ( tag => {
102- tag !== 'post' && tag !== 'posts' && ( postsByTag [ tag ] = postsByTag [ tag ] || [ ] ) . push ( post ) ;
103- } ) ;
104- } ) ;
105- Object . keys ( postsByTag ) . forEach ( tag => {
106- postsByTag [ tag ] . sort ( ( a , b ) => b . date - a . date ) ;
107- } ) ;
108- return postsByTag ;
109- } ) ;
110-
111- // Posts by category - creates a collection for each category
112- eleventyConfig . addCollection ( "postsByCategory" , function ( collectionApi ) {
113- const postsByCategory = { } ;
114- collectionApi . getFilteredByGlob ( "src/blog/*.md" ) . forEach ( post => {
115- post . data . category && ( postsByCategory [ post . data . category ] = postsByCategory [ post . data . category ] || [ ] ) . push ( post ) ;
116- } ) ;
117- Object . keys ( postsByCategory ) . forEach ( cat => {
118- postsByCategory [ cat ] . sort ( ( a , b ) => b . date - a . date ) ;
119- } ) ;
120- return postsByCategory ;
121- } ) ;
122-
123- // Chinese tag collection
124- eleventyConfig . addCollection ( "zhTagList" , function ( collectionApi ) {
125- const tagSet = new Set ( ) ;
126- collectionApi . getFilteredByGlob ( "src/zh/blog/*.md" ) . forEach ( post => {
127- ( post . data . tags || [ ] ) . forEach ( tag => {
128- tag !== 'post' && tag !== 'posts' && tagSet . add ( tag ) ;
129- } ) ;
130- } ) ;
131- return [ ...tagSet ] . sort ( ) ;
132- } ) ;
133-
134- // Chinese category collection
135- eleventyConfig . addCollection ( "zhCategoryList" , function ( collectionApi ) {
136- const categorySet = new Set ( ) ;
137- collectionApi . getFilteredByGlob ( "src/zh/blog/*.md" ) . forEach ( post => {
138- post . data . category && categorySet . add ( post . data . category ) ;
139- } ) ;
140- return [ ...categorySet ] . sort ( ) ;
141- } ) ;
142-
143- // Chinese posts by tag
144- eleventyConfig . addCollection ( "zhPostsByTag" , function ( collectionApi ) {
145- const postsByTag = { } ;
146- collectionApi . getFilteredByGlob ( "src/zh/blog/*.md" ) . forEach ( post => {
147- ( post . data . tags || [ ] ) . forEach ( tag => {
148- tag !== 'post' && tag !== 'posts' && ( postsByTag [ tag ] = postsByTag [ tag ] || [ ] ) . push ( post ) ;
149- } ) ;
150- } ) ;
151- Object . keys ( postsByTag ) . forEach ( tag => {
152- postsByTag [ tag ] . sort ( ( a , b ) => b . date - a . date ) ;
153- } ) ;
154- return postsByTag ;
155- } ) ;
156-
157- // Chinese posts by category
158- eleventyConfig . addCollection ( "zhPostsByCategory" , function ( collectionApi ) {
159- const postsByCategory = { } ;
160- collectionApi . getFilteredByGlob ( "src/zh/blog/*.md" ) . forEach ( post => {
161- post . data . category && ( postsByCategory [ post . data . category ] = postsByCategory [ post . data . category ] || [ ] ) . push ( post ) ;
162- } ) ;
163- Object . keys ( postsByCategory ) . forEach ( cat => {
164- postsByCategory [ cat ] . sort ( ( a , b ) => b . date - a . date ) ;
165- } ) ;
166- return postsByCategory ;
167- } ) ;
168-
169- // Filters
170- eleventyConfig . addFilter ( "dateFormat" , ( dateObj ) => {
171- return new Date ( dateObj ) . toLocaleDateString ( 'en-US' , {
172- year : 'numeric' ,
173- month : 'long' ,
174- day : 'numeric'
175- } ) ;
176- } ) ;
177-
178- eleventyConfig . addFilter ( "isoDate" , ( dateObj ) => {
179- return new Date ( dateObj ) . toISOString ( ) ;
180- } ) ;
181-
182- eleventyConfig . addFilter ( "limit" , ( arr , limit ) => {
183- return arr . slice ( 0 , limit ) ;
184- } ) ;
185-
186- eleventyConfig . addFilter ( "capitalize" , ( str ) => {
187- return str ? str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) : '' ;
188- } ) ;
189-
190- eleventyConfig . addFilter ( "slugify" , ( str ) => {
191- return str ? str . toLowerCase ( ) . replace ( / \s + / g, '-' ) . replace ( / [ ^ \w - ] + / g, '' ) : '' ;
192- } ) ;
193-
194- // i18n filter - get translation by key path
195- eleventyConfig . addFilter ( "t" , ( key , lang = defaultLanguage ) => {
196- const i18n = eleventyConfig . globalData ?. i18n ;
197- if ( ! i18n ) return key ;
198- const langData = i18n [ lang ] || i18n [ defaultLanguage ] ;
199- const keys = key . split ( '.' ) ;
200- let value = langData ;
201- for ( const k of keys ) {
202- value = value ?. [ k ] ;
203- }
204- return value || key ;
205- } ) ;
206-
207- // Get alternate language URL
208- eleventyConfig . addFilter ( "altLangUrl" , ( url , currentLang , targetLang ) => {
209- if ( currentLang === 'en' && targetLang !== 'en' ) {
210- return `/${ targetLang } ${ url } ` ;
211- } else if ( currentLang !== 'en' && targetLang === 'en' ) {
212- return url . replace ( `/${ currentLang } ` , '' ) || '/' ;
213- } else if ( currentLang !== 'en' && targetLang !== 'en' ) {
214- return url . replace ( `/${ currentLang } ` , `/${ targetLang } ` ) ;
215- }
216- return url ;
217- } ) ;
218-
219- // Add global data for languages
220- eleventyConfig . addGlobalData ( "supportedLanguages" , supportedLanguages ) ;
221- eleventyConfig . addGlobalData ( "defaultLanguage" , defaultLanguage ) ;
222-
223- // Shortcodes
224- eleventyConfig . addShortcode ( "year" , ( ) => `${ new Date ( ) . getFullYear ( ) } ` ) ;
225-
226102 return {
227103 dir : {
228104 input : "src" ,
@@ -235,3 +111,30 @@ export default function(eleventyConfig) {
235111 htmlTemplateEngine : "njk"
236112 } ;
237113}
114+
115+ /**
116+ * Register only SEO virtual templates from the techdoc plugin.
117+ * Layouts and blog scaffold pages come from local files (dart_node's are superior).
118+ */
119+ function registerSeoVirtualTemplates ( eleventyConfig ) {
120+ const templatesDir = join (
121+ __dirname , "node_modules" , "eleventy-plugin-techdoc" , "templates"
122+ ) ;
123+
124+ eleventyConfig . addTemplate (
125+ "feed.njk" ,
126+ readFileSync ( join ( templatesDir , "pages/feed.njk" ) , "utf-8" )
127+ ) ;
128+ eleventyConfig . addTemplate (
129+ "sitemap.njk" ,
130+ readFileSync ( join ( templatesDir , "pages/sitemap.njk" ) , "utf-8" )
131+ ) ;
132+ eleventyConfig . addTemplate (
133+ "robots.txt.njk" ,
134+ readFileSync ( join ( templatesDir , "pages/robots.txt.njk" ) , "utf-8" )
135+ ) ;
136+ eleventyConfig . addTemplate (
137+ "llms.txt.njk" ,
138+ readFileSync ( join ( templatesDir , "pages/llms.txt.njk" ) , "utf-8" )
139+ ) ;
140+ }
0 commit comments