do_blog.js

Look at this thing! I made it in a number of days and it brings me joy!

More seriously, this a script for individuals who want a blog but don't want to go through the hassle of updating it, either through some third party or (more annoyingly) by hand. Oh, the horror of website making (affectionate).

Basically, it takes in a page that looks like this:


<script defer src=do_blog.js></script>

<div id=blog></div>
<div id=blog-archive hidden>

  # Hello, this is my first post!

  2024-12-16

  [tag] hello world, first post

  Taking the time to inform everyone that Rise of the Teenage Mutant Ninja Turtles exists. That is all

  [tag] rottmnt
  
  [sign]

</div>

And turns it into this:

Example, no CSS

For transparency's sake, I'm leaving this example here to show the result with a single post.

It doesn't look like much, but it's functional and much nicer when it has its own webpage and CSS. And, of course, you can build more webpage around the blog. Sort of like I built this page around the example.

The script, however, does assume that the page's main content is the blog, so it changes the title to the blog's title, which is why this page is titled Blog instead of do_blog.js.

I tried to make it as customizable as I could, so if you want to see how well it really works out as a blogging tool, check out this example blog or my blog!

Another example, with CSS

The rest of this page functions as documentation for its usage and customization.

Get it

EDIT: Due to popular demand (from myself, I demanded it), I've made a stylesheet for the souls who have no more CSSing in them for the day (or their life). Understandable.

To see how the CSS looks, check out the Example Blog I made.

Download:

Or copy the code below:

do_blog.js
do_blog.css

Put the script at the end of your body or in your head with the defer attribute. The only set up you need is the script and two divs.


<script src="do_blog.js" defer></script>

<div id=blog></div>

<div id=blog-archive hidden></div>

Blog it

The only div you actually need to touch is the #blog-archive. Skip lines whenever you want to make a new paragraph, a new post, or use a command. The basic format of a blog post:


# Blog Title

date time

The rest of the post

The # and date time are mandatory!

The # indicates a new blog post.

Date time can be in date time string format (YYYY-MM-DDTHH:mm:ss.sssZ) or in milliseconds since the epoch. For example, it is Monday, December 16, 2024 13:47 for me.

YYYY-MM-DDTHH:mm:ss.sssZ
2024-12-16T18:47:22.874Z
Console: new Date().toISOString()
Highly recommended, as it is standard for browsers to read this format
Note: Just YYYY-MM-DD works fine too, but the browser assumes the local timezone. Time can be iffy in the web.
Milliseconds since the epoch
1734374842874
Console: new Date().getTime()
Not as recommended, not mandatory for a browser to read this and return a valid date.

I have provided two commands.

[tag] words, more words, so many words
Adds the list of comma separated words/phrases to the post's tags. This way, a reader may search the posts by tag if they wish.
[sign]
If the line is otherwise empty, this displays your signature in its place. The next section goes about how you can customize your signature.

The commands may be used as many times per post as you want.

Customize it

Finally, there are quite a few things to customize, though there is no real need to if one doesn't want to.

JavaScript

All you need to do is make an object named MY_BLOG in a script that loads before do_blog.js. The following example displays all the customizable options and their defaults.

<script>

MY_BLOG = {

  // blogging experience
  new_post: '#',
  tag_cmd: '[tag]',
  sign_cmd: '[sign]',
  
  title: 'Blog',
  
  no_posts_msg: 'This person has not posted yet! Come back later :3',
  none_tagged_msg: 'There are no posts under this tag!',
  
  // blog post
  title_is_link: true,
  link_text: '[link]', // if title is not link
  tags_in_footer: false,
  cut_long_posts: .75 * window.innerHeight,
  img_height: 300,
  read_more: 'Read more',
  
  replacers: [],
  newTitle: (post) => { // when post has no title
    const temp = document.createElement('div');
    DO_BLOG.replacers.forEach ( e => {
      const arr = post.body.split(e[0]);
      temp.innerHTML = arr.join(e[1]);
    });
    const arr = temp.textContent.slice(0,20).split(/\s+/);
    arr.pop();
    return arr.join(' ') + '…';
  },
  getDate: (date) => {
    return date.toUTCString().slice(0,16);
  },
  signature: (post) => {
    return 'Posted ' + post.date.toLocaleDateString();
  },
  
  // nav
  links_heading: 'Links',
  skip_to_content: 'Skip to content',
  open_navs: 2,
  months: [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December'
  ],
  navTitle: (post) => {
    return post.date.toLocaleDateString();
  },
  
  // tag form
  tag_label: 'Tag: ',
  all_tags: '--All tags--',
  tag_search: 'Go',
  
  // sort form
  sort_old_to_new: 'Sort old to new',
  sort_new_to_old: 'Sort new to old',
  
  // search pages nav
  load_limit: 10,
  pagination_limit: 6,
  skip_pagination: 'Skip pagination',
  previous_page: 'Prev',
  page_label: 'Page: ',
  next_page: 'Next',
  
  // breadcrumb nav
  breadcrumb_trail: '/',
  breadcrumb_article: 'Article',
  breadcrumb_tag: 'Tag',
  
  // article nav
  next_post: '← Next',
  post_nav_separator: ' | ',
  previous_post: 'Prev →',
  
  // aria-label (for pagination and tags)
  aria_goto_page: 'Goto page ',
  aria_current_page: 'Current page, page ',
  aria_page_of: ['Page ', ' of '],
  aria_pagination: 'Pagination',
  aria_breadcrumbs: 'Breadcrumbs',
  aria_tags: 'Tags',
  
  do_onload: (posts) => {},

}

</script>

Let's go over the details of each option

new_post: '#'
String. Put this at the beginning of a line to indicate a new post. The rest of the line is the post's title, though you don't need to include a title if you don't want to. Keep in mind, posts are split using regex. This is put through RegExp (regex), so some characters may need to be escaped with a double backslash (\\).
tag_cmd: '[tag]'
String. No regex. Put this at the beginning of a line to indicate the rest of the line is a comma separated list of tags.
sign_cmd: '[sign]'
String. No regex. Put this and only this on a line and it will output a paragraph with class signature containing your signature, which you can change in the signature property.
title: 'Blog'
String. The blog's title. This will be used in the breadcrumbs navigation that appears when you filter by tag or click on an article link. The title element's text will also be set to this.
no_posts_msg: 'This person has not posted yet! Come back later :3'
String. Message appears when your #blog-archive is empty or does not exist.
none_tagged_msg: 'There are no posts under this tag!'
String. Message appears when (somehow) the reader has filtered by a tag that has no related posts.
title_is_link: true
Boolean. By default, blog titles double as links to the blog article's page. If this is set to false, then a separate link will be created in the header.
link_text: '[link]'
String. If a blog post has an empty title or title_is_link is false, then a link with this inner HTML will be shown in the article's header.
tags_in_footer: false
Boolean. By default, tags are shown in the header. If this is set to true, the tags are shown in the footer, at the bottom of the post.
cut_long_posts: .75 * window.innerHeight
Number. The height (in pixels) at which blog posts are cut in the search page. Once exceeded, a read more button appears at the bottom of the article's body. If this is set to zero (0), blog posts will not be cut off.
img_height: 300
Number or String. Images can take some time to load, so their height will look like zero when the function is deciding if the post should be cut off. If an image has no height attribute and has not loaded yet, its height attribute will temporarily be set to this.
show_more: 'Show more'
String. The show more button added at the end of an article's body when it has been cut off. This is mostly here in case you want to change the language, though you can also put an image if you think that would look better on your blog!
replacers: []
Array: [String, String]
Array: [Regex, String]
Just before creating the post article, the post's body is split by the replacers' first element and then joined back together with the second.
Example with ['foo','bar']:
  1. 'This foo is a bar foo bar of bar.'
  2. ['This ', ' is a bar ', ' bar of bar.']
  3. 'This bar is a bar bar bar of bar.'
newTitle: (post) => { ... },
String or Function. By default, when a post does not have a title, its visible title is automatically decided using the first 20 characters of its text content. You can change this so it instead becomes something, like 'Untitled' or the post's date. The post argument is a Post object, which has the following properties and methods:
  • id: String
  • title: String
  • date: Date
  • body: String
  • tags: Array: [String]
  • visible_tags: Array: [String]
  • getArticle: Function: returns HTMLElement
  • getBodyHTML: Function: returns String
  • getTitleHTML: Function: returns String (note: this method calls newTitle)
getDate: (date) => { return date.toUTCString().slice(0,16); }
Function. The function should return a String. This is what the date looks like inside an article post.
signature: (post) => { return 'Posted ' + post.date.toLocaleDateString(); }
String or Function. If a function, it should return a String to act as the blogger's signature. The post argument is an object with the following properties:
  • title: String
  • date: Date
  • id: String
links_heading: 'Links'
String. Navigation links heading. Mostly here for language.
skip_to_content: 'Skip to content'
String. Skip to content link below the Nav links heading. Mostly here for language.
open_navs: 2
Number. The number of details elements to open in the navigation. These are opened in order, so the details opened will be the first year to appear in the nav, and then the months in that year, then the second year, and so on.
months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]
Array: [String x 12]
The months in the navigation and in the search page will be printed like this. Mostly here for language.
navTitle: (post) => { return post.date.toLocaleDateString(); }
Function. Should return a String. The post's title will be printed like this in the navigation. The post argument is a Post object, so one may choose to use the post.title or follow the default's example and use post.date to change how the blog post's link appears.
tag_label: 'Tag: '
all_tags: '--All tags--'
tag_search: 'Go'
sort_old_to_new: 'Sort old to new'
sort_new_to_old: 'Sort new to old'
String. All of these are here for language. Feel free to go wild anyways.
load_limit: 10
Number. The number of blog posts that can appears in one search page at a time. If the number of posts exceeds this number, the results are paginated.
pagination_limit: 6
Number. The number of links allowed in the pagination navigation. If the number is exceeded, the navigation becomes a select element with previous and next links.
skip_pagination: 'Skip pagination'
previous_page: 'Prev'
page_label: 'Page: '
next_page: 'Next'
String. Here for language.
breadcrumb_trail: '/'
String. Breadcrumb navigation appears when filtering by tag or after clicking on an article link. This is the separator between breadcrumbs.
breadcrumb_article: 'Article'
String. Here for language. An article page has a breadcrumb navigation that looks something like:
Blog / Article / Blog Title
breadcrumb_tag: 'Tag'
String. Here for language. When filtering by tag, a search page has a breadcrumb navigation that looks like:
Blog / Tag / tag name
next_post: '← Next'
post_nav_separator: ' | '
previous_post: 'Prev →'
String. Here for language or style. These appear at the top and bottom of an article post's page, to travel between posts without needing to return to the blog page.
aria_goto_page: 'Goto page '
aria_current_page: 'Current page, page '
aria_page_of: ['Page ', ' of ']
aria_pagination: 'Pagination'
aria_breadcrumbs: 'Breadcrumbs'
aria_tags: 'Tags'
String (and an Array of two Strings). Here for language. Pagination is not super intuitive for screen readers, but using the aria-label attribute, the pagination nav and links should read:
Navigation page 1 of 8; Pagination. Link current page, page 1. Link goto page 2. Link goto page 3.
And so on. (Or something like that)
The attribute is also used for the list of tags, so screen readers know why there's suddenly a list of random words.
do_onload: (posts) => {}
Function. This function is is called once the page finishes loading. The posts argument is an Array of all the Post objects (every blog post). Perhaps you want to pin a post? Then you can search for that post by id, date, title, or tag, and use the Post.getArticle method to append it wherever your pinned posts go. For example:
I want to search for all the posts tagged 'pin' and insert their article in the search results, after the navigation and before the actual results:
do_onload: posts => {
  const pinned = posts.filter( p => {
    return p.tags.indexOf('pin') > -1
  });
  const firstSection = document.querySelector('#blog-content .section');
  pinned.forEach( p => {
    firstSection.parentElement.insertBefore(p, firstSection);
  });
}

CSS

Here's the general format do_blog should result in. I've included, hopefully, an example of everything, so just know that before wondering why there are two page navs or why there are tags in the header and the footer.

#blog.blog
div.blog-bar
nav#blog-nav

h2.links "Links"

a.skip-to-content "Skip to content"

details.year

summary "2000"

ul.months
li
details.month

summary "January"

ul.posts
li

a "1/1/2000"

hr

form#tag-form
label#tag-search "Tag: "

select

button "Go"

form#sort-form

button "Sort old to new"

div#blog-content.blog-main
nav.breadcrumbs

a.breadcrumb "Blog"

span.trail "/"

span.breadcrumb "Tag"

span.trail "/"

a.current.breadcrumb "hashtag"

nav.top.pagination.link-navigation

a.skip-to-content "Skip pagination"

a.current "1"

a "2"

hr

nav.top.pagination.select-navigation

a.skip-to-content "Skip pagination"

a.previous-page "Prev"

label "Page: "

select

a.next-page "Next"

hr

h2.year.section "2000"

h3.month.section "January"

article.post.cut-off
header.header
h1.title

a "Blog Title"

p.date

time "Sat, 01 Jan 2000"

a.blog-link "[link]"

ul.tags
li.tag

a "hashtag"

div.body.cut-off

p "This is a cut off blog post paragraph"

button.show-more "Show more"

footer
ul.tags
li.tag

a "hashtag"

nav.bottom.pagination.link-navigation

hr

a.current "1"

a "2"

nav.bottom.pagination.page-navigation

hr

a.previous-page "Prev"

label "Page: "

select

a.next-page "Next"

#blog.article
nav.breadcrumbs

a.breadcrumb "Blog"

span.trail "/"

span.breadcrumb "Article"

span.trail "/"

a.current.breadcrumb "Blog Title"

nav.top.post-navigation

a.next-post "← Next"

span.separator " | "

a.previous-post "Prev →"

article.post
header.header

h1.title "Blog Title"

p.date

time "Sat, 01 Jan 2000"

ul.tags
li.tag

a "hashtag"

div.body

p "This is a full blog post paragraph"

footer
ul.tags
li.tag

a "hashtag"

nav.bottom.post-navigation

a.next-post "← Next"

span.separator " | "

a.previous-post "Prev →"