Skip to content

Add create generic book mode conference note example under examples directory #39

@Yukaii

Description

@Yukaii

Below is a sample script for generating a book mode cover and each book mode HackMD note. Book mode is a Markdown note that contains links to each note page.

  1. Make it more generic by placing constants at the beginning of the file, and add comments and a README.
  2. Implement it in TypeScript and install the tsx package to allow users to run it without tsc.
  3. Generate a sample sessions.json file based on the script.
  4. Add a detailed README.
'use strict'

// Load environment variables from .env file in project root
require('dotenv').config()

const _ = require('lodash')
const moment = require('moment')
const { API } = require('@hackmd/api')

const annoncementNote = '@DevOpsDay/rkO2jyLMlg'
const teamPath = 'DevOpsDay'

// Define permission constants (since we can't import the enums)
const NotePermissionRole = {
  OWNER: 'owner',
  SIGNED_IN: 'signed_in',
  GUEST: 'guest'
}

// Load and process session data
let sessionList = require('./sessions.json')
  .filter(s => s.session_type && s.session_type !== null) // Filter out null session types
  .map(s => {
    const speakers = s.speaker.map(speaker => {
      return speaker.speaker.public_name
    }).join(' & ')

    return {
      id: s.id,
      title: s.title + (speakers ? " - " + speakers : ""),
      tags: s.tags || [],
      startDate: moment(s.started_at).valueOf(),
      day: moment(s.started_at).format('MM/DD'),
      startTime: moment(s.started_at).format('HH:mm'),
      endTime: moment(s.finished_at).format('HH:mm'),
      sessionType: s.session_type,
      classroom: s.classroom?.tw_name || s.classroom?.en_name || 'TBD',
      language: s.language || 'en',
      difficulty: s.difficulty || 'General'
    }
  })
  .sort((a, b) => (a.startDate - b.startDate))

function nest(seq, keys) {
  if (!keys.length)
    return seq;
  const [first, ...rest] = keys
  return _.mapValues(_.groupBy(seq, first), function (value) {
    return nest(value, rest)
  });
}

const api = new API(process.env.HACKMD_ACCESS_TOKEN, process.env.HACKMD_API_ENDPOINT)

// Extract the host from the API endpoint for generating correct URLs
function getHackMDHost() {
  const apiEndpoint = process.env.HACKMD_API_ENDPOINT || 'https://hackmd.io'
  try {
    const url = new URL(apiEndpoint)
    return `${url.protocol}//${url.host}`
  } catch (error) {
    console.warn('Failed to parse HACKMD_API_ENDPOINT, falling back to https://hackmd.io')
    return 'https://hackmd.io'
  }
}

async function main() {
  console.log(`Processing ${sessionList.length} sessions...`)

  // Create individual session notes
  for (let data of sessionList) {
    const noteData = {
      title: `${data.title}`,
      content: `# ${data.title}

{%hackmd ${annoncementNote} %}

## 筆記區
> 從這開始記錄你的筆記

## 討論區
> 歡迎在此進行討論

## 相關連結
- [DevOpsDays Taipei 2025 官方網站](https://devopsdays.tw/)

###### tags: \`DevOpsDays Taipei 2025\`
`,
      readPermission: NotePermissionRole.GUEST,
      writePermission: NotePermissionRole.SIGNED_IN
    }

    try {
      const note = await api.createTeamNote(teamPath, noteData)
      data.noteUrl = note.shortId
      console.log(`Created note for: ${data.title}`)
    } catch (error) {
      console.error(`Failed to create note for ${data.title}:`, error.message)
      data.noteUrl = 'error'
    }
  }

  // Output session URLs for reference
  const hackmdHost = getHackMDHost()
  const sessionUrls = sessionList
    .filter(s => s.noteUrl !== 'error')
    .map(s => ({
      id: s.id,
      url: `${hackmdHost}/${s.noteUrl}`,
      title: s.title
    }))

  console.log('\n=== Session URLs ===')
  console.log(JSON.stringify(sessionUrls, null, 2))

  // Create nested structure for the main book
  const nestedSessions = nest(sessionList.filter(s => s.noteUrl !== 'error'), ['day', 'startTime'])

  const bookContent = generateBookContent(nestedSessions, 1)

  // Create main conference book
  const mainBookContent = `DevOpsDays Taipei 2025 共同筆記
===

## 歡迎來到 DevOpsDays Taipei 2025!

- [歡迎來到 DevOpsDays!](/@DevOpsDay/ry9DnJIfel)
- [DevOpsDays 2025 官方網站](https://devopsdays.tw/) [target=_blank]
- [HackMD 快速入門](https://hackmd.io/s/BJvtP4zGX)
- [HackMD 會議功能介紹](https://hackmd.io/s/BJHWlNQMX)

## 議程筆記

${bookContent}

## 相關資源

- [DevOps Taiwan Community](https://www.facebook.com/groups/DevOpsTaiwan/)
- [活動照片分享區](#)
- [問題回饋](#)

###### tags: \`DevOpsDays Taipei 2025\`
`

  try {
    const mainBook = await api.createTeamNote(teamPath, {
      title: 'DevOpsDays Taipei 2025 共同筆記',
      content: mainBookContent,
      readPermission: NotePermissionRole.GUEST,
      writePermission: NotePermissionRole.SIGNED_IN
    })

    console.log('\n=== Main Conference Book ===')
    console.log(`${hackmdHost}/${mainBook.shortId}`)
  } catch (error) {
    console.error('Failed to create main book:', error.message)
  }
}

function generateBookContent(sessions, layer) {
  const days = Object.keys(sessions).sort()
  let content = ""

  if (Array.isArray(sessions[days[0]])) {
    // This is the leaf level (sessions) - flatten all sessions and sort chronologically
    let allSessions = []
    for (let timeSlot of days) {
      allSessions = allSessions.concat(sessions[timeSlot])
    }
    // Sort all sessions by start time
    const sortedSessions = _.sortBy(allSessions, ['startTime'])

    for (let session of sortedSessions) {
      if (session.noteUrl && session.noteUrl !== 'error') {
        content += `- ${session.startTime} ~ ${session.endTime} [${session.title}](/${session.noteUrl}) (${session.classroom})\n`
      }
    }
    return content
  } else {
    // This is a grouping level
    for (let day of days) {
      content += `${new Array(layer).fill("#").join("")} ${day}\n\n`
      content += generateBookContent(sessions[day], layer + 1)
    }
    return content
  }
}

// Run the script
if (require.main === module) {
  main().catch(console.error)
}

module.exports = { main, generateBookContent }

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions