This site is generated using Jekyll , which uses Markdown under the hood, just like nearly every other static site generator.
Markdown is great, but it is inconvenient when proofreading or editing. You must jump from editor to browser, wait for it to regenerate the site, and refresh.
Wouldn’t it be great if you could edit the markdown from the generated pages in the browser and have those changes saved in the source file?
That sounds like an excellent use case (excuse) for the Chrome File System Access API .
Conditionally adding the script
Put this code in the post template.
{% if post . with_editor and site . serving %}
{% include _editor.html%}
{% endif %}
site.serving
is set if jekyll s
is run but not if jekyll b
.
_editor.html
is the include
where well dump the JavaScript for the editor to be activated.
includes/_editor.html
< script src = " https://unpkg.com/showdown/dist/showdown.min.js " >< /script >
< script type = " module " >
import { get , set } from ' https://cdn.jsdelivr.net/npm/idb-keyval@6/+esm ' ;
document . addEventListener ( " DOMContentLoaded " , async () => {
await start ()
});
async function start (){
// showdown converts markdown to html
const converter = new showdown . Converter ()
// have Jekyll inject the path to the page
// Replacing the _linked because that's a symlink and
// it didn't seem to work with links
const path = " _linked/small/jekyll-editing-rendered-posts-using-chrome-filesystem-api.md " . replace ( ' _linked/ ' , '' ). split ( ' / ' )
const filename = path . slice ( - 1 )[ 0 ]
// the directory where the file is
let dirHandle = null ;
// the source file
let fileHandle = null
const init = async function () {
// check if the dir has been previously saved
const directoryHandleOrUndefined = await get ( ' blog ' );
// if it is, and the browser still has permissions we can immediately set it
if ( directoryHandleOrUndefined && await verifyPermission ( directoryHandleOrUndefined )) {
dirHandle = directoryHandleOrUndefined ;
} else {
// otherwise we have to show a dir picker
dirHandle = await window . showDirectoryPicker ({ mode : ' readwrite ' });
await set ( ' blog ' , dirHandle );
}
// for traversing
let index = 0
// then traverse till we get to the correct path
while ( path [ index ] !== filename ){
dirHandle = await dirHandle . getDirectoryHandle ( path [ index ]);
index ++ ;
}
// no error handling yolo, select the right dir the first time ¯\_(ツ)_/¯
// then get the handle to the actual source file
fileHandle = await dirHandle . getFileHandle ( filename , { write : true });
// get the raw content
let content = await getContent ( fileHandle )
// separate out the frontmatter
let frontmatter = content . split ( ' --- ' )[ 1 ]
// and the markdown
let markdown = content . split ( ' --- ' )[ 2 ]
// when it's focused, apply a monospace font, and set the content to the markdown
ar . addEventListener ( ' focus ' , () => {
ar . style . whiteSpace = ' pre-wrap ' ;
ar . style . fontFamily = ' monospace '
ar . style . fontSize = ' 18px '
ar . style . padding = ' 5px '
ar . style . lineHeight = " 25px "
ar . innerHTML = markdown . trim ()
})
// when escape is pressed save (some might want this to be cancel, but for me save worked better)
ar . addEventListener ( ' keydown ' , ( ev ) => {
if ( ev . key === ' Escape ' ) ar . blur ()
})
// so blur, save the content
ar . addEventListener ( ' blur ' , async () => {
// generate the new html
const newMarkdown = ar . innerText
const html = converter . makeHtml ( newMarkdown ). trim ()
// remove the monospace style
ar . style . removeProperty ( ' white-space ' )
ar . style . removeProperty ( ' font-family ' )
ar . style . removeProperty ( ' font-size ' )
ar . style . removeProperty ( ' line-height ' )
ar . style . removeProperty ( ' padding ' )
ar . innerHTML = html
// write the new markdown
const writable = await fileHandle . createWritable ();
await writable . write (
" --- \n " +
frontmatter . trim () + " \n " +
" --- \n " +
newMarkdown + " \n "
);
await writable . close ();
markdown = newMarkdown ;
})
// enable editing
ar . setAttribute ( ' contenteditable ' , true )
// remove the init handler
ar . removeEventListener ( ' click ' , init )
// focus
ar . focus ()
}
// the content container
const ar = document . getElementById ( ' content ' )
// filesystem permissions etc can only be requested after use action
ar . addEventListener ( ' click ' , init )
}
async function verifyPermission ( fileHandle ) {
const options = { mode : ' readwrite ' };
// Check if permission was already granted. If so, return true.
if (( await fileHandle . queryPermission ( options )) === ' granted ' ) {
return true ;
}
// Request permission. If the user grants permission, return true.
if (( await fileHandle . requestPermission ( options )) === ' granted ' ) {
return true ;
}
// The user didn't grant permission, so return false.
return false ;
}
async function getContent ( fs ) {
const file = await fs . getFile ();
const content = await file . text ();
return content ;
}
< /script>
Potential improvements
Stripping formatting on paste – now, I paste using Ctrl + Shift + V
Adding syntax highlighting
When the post contains liquid tags, such as the {%if site.serving%}...
above, those need to be re-rendered by Jekyll. The editor doesn’t break them; it just takes a refresh for them to update.
And if this was interesting, here are some other articles about customizing Jekyll .