"Apple Help in 2015"

The last time I had to build a brand new help file was some time ago — maybe even ten years ago — and in the world of software, that’s an age.

For the past few months I’ve been working hard on a new release of iDefrag, version 5, and as part of this I’m rewriting the documentation. Rather than using hand-written HTML like I did before, I’ve chosen this time around to use a documentation generator, Sphinx. The advantages of this approach include:

  • Built-in support for indexing and cross-referencing.

  • The ability to write the documententation in plain text.

  • Keeps the presentation details separate from the content (via theming and templates).

  • Supports multiple output formats, not just HTML.

The current version of Sphinx doesn’t directly support building Apple Help Books, but I’ve submitted a pull request to fix that so hopefully by the time you read this you’ll be able to do

$ sphinx-quickstart

fill in some fields and then do

$ make applehelp

to generate a help book.

(If you do do that, you’ll want to edit your conf.py file quite a bit, and you probably don’t want to use the default theme either.)

Anyway, all of the Sphinx related stuff was fine, and worked as documented. Unlike Apple Help, which doesn’t. I spent an entire day struggling to make a help book that actually worked, and most of that is because of problems with the documentation.

Let’s start with the Info.plist. Apple gives this not particularly helpful table:

Key Exact or sample value
CFBundleDevelopmentRegion en_us
CFBundleIdentifier com.mycompany.surfwriter.help
CFBundleInfoDictionaryVersion 6.0
CFBundleName SurfWriter
CFBundlePackageType BNDL
CFBundleShortVersionString 1
CFBundleSignature hbwr
CFBundleVersion 1
HPDBookAccessPath SurfWriter.html
HPDBookIconPath shrd/SurfIcn.png
HPDBookIndexPath SurfWriter.helpindex
HPDBookKBProduct surfwriter1
HPDBookKBURL https://mycompany.com/kbsearch.py?p='product'&q='query'&l='lang'
HPDBookRemoteURL https://help.mycompany.com/snowleopard/com.mycompany.surfwriter.help/r1
HPDBookTitle SurfWriter Help
HPDBookType 3
HPDBookTopicListCSSPath sty/topiclist.css
HPDBookTopicListTemplatePath sty/topiclist.xquery

There are two serious problems with the table above. The first is that some of it is wrong(!), and the second is that it doesn’t indicate which values are sample values and which are required.

Here’s what you actually need:

Key Value
CFBundleDevelopmentRegion en-us
CFBundleIdentifier your help bundle identifier
CFBundleInfoDictionaryVersion 6.0
CFBundlePackageType BNDL
CFBundleShortVersionString your short version string - e.g. 1.2.3 (108)
CFBundleSignature hbwr
CFBundleVersion your version - e.g. 108
HPDBookAccessPath _access.html (see below)
HPDBookIndexPath the name of your help index file
HPDBookTitle the title of your help file
HPDBookType 3

The first thing to note is that CFBundleDevelopmentRegion should have a hyphen, not an underscore. Apple’s utilities generate this properly, but the documentation is wrong.

The second thing to note is that in spite of the documentation implying that you can use your help bundle identifier to refer to your help bundle (which would, admittedly, make sense), you can’t. You need to use the HPDBookTitle value. Oh, and ignore any references to AppleTitle meta tags. You don’t need those.

The third thing relates to HPDBookAccessPath. The file referred to there must be a valid XHTML file. In particular, it cannot be an HTML5 document — that will simply not work, and the error messages you get on the system console are completely uninformative.

The best solution I’ve come up with for this particular problem, as I want to generate modern HTML output, is to make a file called _access.html and put the following in it:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Title Goes Here</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="robots" content="noindex" />
    <meta http-equiv="refresh" content="0;url=index.html" />
  </head>
  <body>
  </body>
</html>

This means that both helpd and the help indexer (hiutil) are happy, and I can write my index page using modern HTML. Incidentally, Apple appears to be using a similar trick in the help for the current version of Mail. Obviously you can change the index.html in the above to whatever you need.

In your application bundle, you need to fill in the following keys

Key Value
CFBundleHelpBookFolder The path of your help book relative to Resources - e.g. SurfWriter.help
CFBundleHelpBookName The value from HPDBookTitle, above

Note that while the HPDBookTitle is displayed to the user, it can be localised using InfoPlist.strings. Note also that you absolutely cannot, contrary to what the documentation implies, give a bundle ID here. It just doesn’t work. You could however, if you wanted, write an InfoPlist.strings file like this:

HPDBookTitle = "SurfWriter Help"

then put the bundle ID in as the HPDBookTitle in the Info.plist.

Oh, and if you think you’re going to be able to double-click a help book to preview it, think again. That won’t work. Instead, you need either to use it from within your application, or you can put it in ~/Library/Documentation/Help (you might have to make that folder) and double-click it in there. Why? Because help files are indexed and you can only open them if they’re registered in the index.

One other thing that isn’t really documented at all is what exactly the HPDBookRemoteURL will do for you. There’s some handwaving about being able to offer remote content updates, but how the URL is used is skirted over. Well, if you do set HPDBookRemoteURL, Help Viewer will essentially expect it to point at a copy of the Resources folder of your bundle; so if you have HPDBookRemoteURL set to http://example.com/foo/bar/, then you’re going to get requests like http://example.com/foo/bar/en.lproj/index.html (and so on).

Useful update (Feb 29th 2016)

You may have noticed that Help Viewer has a button to toggle the table of contents in your help file. Matt Shepherd did a bit of work looking into this and it turns out that it’s controlled by a Javascript API — see Matt’s gist for more information.