Ok, here’s the scenario. I had a static blog (much like this one - the same code in fact) but needed to let someone add blog posts who wasn’t a coder. And that’s probably a common requirement. And why content management systems (CMS) exist.
The writer was fairly comfortable with markdown, the format I was already using. I just needed a way for them to add markdown files to the repository.
I could try and teach them git, and set them up on GitHub. But I can’t imagine that would go down well. Plus I’d have to give them access to the entire repository (content + code) and, things could get messy!
So I need a CMS.
But I don’t want to spend much time changing the code since I’m pretty happy with the current setup. And I don’t want to be tied into an expensive plan, since this was for a personal project.
First of all, there are plenty of options out there for a CMS. An actual service that is designed as a CMS. And there are lots of headless ones designed specifically for this use case (which Dropbox isn’t). But not many you could spin up in a couple of hours.
Why not use a git-based CMS?
The best options I could find with minimal set-up (as in, I could just keep my git repo more-or-less as it was, and just add a little extra config) were git-based CMS options.
And the two that stood out to me were:
- Netlify CMS
- Forestry CMS
But, by the title of this blog post, you already know that they didn't work out!
It's not that they aren't good solutions, they are! I'm sure with a bit of extra time and effort I could have gotten to where I needed to be. But I kind of felt like I was late to the party with both.
Netlify CMS
I started with Netlify CMS and got the initial UI set up in minutes. So far so excellent. But I ran into an issue where it wasn’t quite integrating with Netlify Identity perfectly (on logout the CMS would still appear logged in behind the identity widget). It wasn’t broken as such, but I couldn’t figure out how to set up an alternative way to integrate Identity (or another auth provider) quickly, so I gave up.
I’d certainly consider looking at Netlify CMS again when I have more time. It has some nice features for a git-based CMS, like workflows for reviewing and approving unpublished posts.
But I’m not sure how active it is, the GitHub issues and forum posts don't seem to get responded to very quickly. And that's understandable, it's a free product after all!
Forestry.io
Next, I gave Forestry a shot. It looks great and has a sweet free plan, perfect to get started on a hobby project.
Unfortunately, because I had built my static site builder (instead of the sensible option of using an existing framework), I had some specific requirements regarding filenames. And, although Forestry was super quick to set up even with a custom build, the filename was automatically generated from the title front matter. The writer would have to change the filename manually, and I'd need them to remember to do that. On every post. I couldn’t find a way to automatically create a slug for the filename, based on the publish date, for example.
I also couldn’t get some of the nice features like draft posts to work. I could add a custom toggle, but it couldn't clearly show which post was a draft in the UI unless I clicked in and checked each post.
Both of these issues are because it's a custom static site. For Jekyll and Hugo projects, everything is wired up nicely out of the box. And so it looks like a great service if you use either of those static builders.
However, I was reluctant to spend a lot more time researching if I could fix the above issue since it looks like Forestry will be getting replaced by Tina CMS in the future.
So that’s git-based CMS out the window (on this occasion). I’m one hour down, and I only have an hour left to get a content system wired up!
Introducing Dropbox CMS
I already use Dropbox to share files, and so does the writer who will be adding blog posts. So I thought, kinda joking, could I use Dropbox?! It turns out that I could! And I was pleasantly surprised at how well it worked out. It even calls a webhook when files are added/changed to trigger a new build!
Sending huge thanks to Jim Nielsen whose Netlibox repository inspired much of my code.
Dropbox has an API (the DBX platform), and that’s what we use to create a Dropbox CMS.
There’s one big caveat with my setup:
- It only gives one person access
You can get it to work with more people, but I created a Dropbox App with the "App folder" permission to restrict access to only one folder (that’s linked to one account and not shareable). Multiple people could use the App, but the contents of the app folder wouldn’t be shared, that would be unique to each Dropbox account.
To get Dropbox to work with more people, your app would need to have wider permissions so you can use a shared folder instead. I’ll explain that shortly.
Here’s how we will set up your Dropbox CMS:
- Create an app in the Dropbox Developer App Console
- Link it to a Dropbox Account (whoever needs to add the files)
- Use the Dropbox API to fetch the files at build time
Create the Dropbox app
Ok, first things first. We need a folder for the markdown posts to go in, and an API that lets us fetch the content.
You can head over to the Dropbox App Console, and create an app for this. Click the Create app button.
Choose Scoped access and then App folder.
If you go for wider Full Dropbox permissions, you might need to tweak some of the following instructions.
Now name your app and click the Create app button.
Dropbox app settings
You can keep the app in development mode (unless you're planning to ship your new CMS to the world!). If you're just going to be using the CMS yourself, you can keep "Development users" set to "Only you". If you want to get content from another user (like I did) then go ahead and click the Enable additional users button.
You will need to read the files (so you can get the content), so switch over to the permission tab and give your app the following permissions.
- account_info.read
- files.metadata.read
- files.content.read
Ok, you're nearly ready to use it! And it's only taken 5 minutes of setup so far!
Sharing the app
If you're using the app yourself, switch back to the settings tab and in the Generated access token section click the Generate button.
If you are sharing the app with another user, and you've enabled additional users like the instructions above, you need to share the app with them to get an access code from them.
If you want to be fancy, before you do this you can add a logo as an app icon under the branding tab, and the other person will see your custom icon instead of the jigsaw icon shown in the next screenshots.
Since we're just in development mode, rather than use the API to handle it, you can send them a URL:
https://www.dropbox.com/oauth2/authorize?client_id=<APP_KEY>&token_access_type=offline&response_type=code
Replace <APP_KEY>
with your Dropbox app key (from the settings tab).
They will probably get a warning message, asking if they are sure, and then a confirmation.
After confirming they want to give your app access, they will get an access code.
If you get an access token with sl
at the start, or find that the access codes only last for 4 hours or less, you may need to follow some extra steps to generate a long-lived access token for Dropbox apps.
Wire it up
Now that you have authorised the app and generated an access token, you should have a new app folder on your Dropbox account. Find it in Dropbox under Apps > Your App Name.
If you're getting a code off another user (from the sharing the app section) this is just an access code. They won't have the folder yet, but they will once you covert the access code into an access token.
In the app folder, add your markdown files (or whatever other content you are using it for here).
For example, if you currently have your blog posts in a /blog
or /posts
directory on your git repository, you can create a /blog
or /posts
directory in your app folder.
Now we can wire up the content to the static site.
I'm going to add just one new file (dropbox.js
) to my static site to do this (and it will work with whatever static builder you use!).
We're going to need the Dropbox SDK, so let's install it.
npm install --save-dev dropbox
You'll need your access token (that you generated earlier) saved as a DBX_ACCESS_TOKEN
environment variable.
If you're getting a code off another user (from the sharing the app section) this is just an access code. It links the user up with your Dropbox access, but you'll need to covert the access code into an refresh token and then scroll down the next code block to wire it up.
And here's the code for our dropbox.js
file.
const fs = require('fs');
const { Dropbox } = require('dropbox');
const dbx = new Dropbox({ accessToken: process.env.DBX_ACCESS_TOKEN });
// Clean anything up that exists already, since we'll be re-building this folder
// everytime we run a build
const postsDirectory = __dirname + '/../posts';
fs.rmSync(postsDirectory, { recursive: true, force: true });
fs.mkdirSync(postsDirectory, { recursive: true })
// get blog posts
dbx.filesListFolder({ path: '/posts' }).then((response) => {
response.entries.forEach(entry => {
const { name, path_lower } = entry;
if (entry[".tag"] === "file") {
dbx.filesDownload({ path: path_lower }).then(data => {
const filename = path.resolve(postsDirectory, name);
const filecontents = data.fileBinary.toString();
fs.writeFileSync(filename, filecontents, function(err) {
if (err) {
console.error(err);
return false;
}
});
})
.catch(error => {
console.log("Error: file failed to download", name, error);
});
}
});
}).catch((err) => {
console.log(err);
});
Much of this orginal code comes from Jim Nielsen's Netlibox, but I'm going to make some changes in the next code block (and explain why).
The code removes anything from the /posts
directory (so the Dropbox CMS will be the single source of truth for content). It then gets all the files in the /posts
directory on Dropbox and adds them to the local /posts
directory. This means your existing build process (that reads from /posts
) can run after, and all the posts will be there. Update the directory name if your posts were kept somewhere else, like /blog
.
This will work with our initial access token, but there's a small tweak we need to make so that the code works long-term. Because the token will expire, we want our build to keep access forever.
First, you'll need to get a refresh token so that you can keep your API access active. You can automate getting a refresh token in the Dropbox SDK, but since I was doing this for one user, I found a manual workaround. Here's a guide to manually generate a long-lived access token for Dropbox apps.
If you need the refresh token for someone other than yourself, (like I was), you will need them to get a fresh valid token with the URL from before https://www.dropbox.com/oauth2/authorize?client_id=<APP_KEY>&token_access_type=offline&response_type=code
and then follow the above guide before it expires to get a refresh token.
Once you have the refresh token for the user who is adding the content, you can update the dropbox.js
script:
I also added some error throwing so that the build fails if there is a problem fetching content.
const fs = require('fs');
const { Dropbox } = require('dropbox');
const dbx = new Dropbox({
clientId: '<APP_KEY>',
clientSecret: process.env.DBX_CLIENT_SECRET,
refreshToken: process.env.DBX_REFRESH_TOKEN
});
// Clean anything up that exists already, since we'll be re-building this folder
// everytime we run a build
const postsDirectory = __dirname + '/../posts';
fs.rmSync(postsDirectory, { recursive: true, force: true });
fs.mkdirSync(postsDirectory, { recursive: true })
// get blog posts
dbx.filesListFolder({ path: '/posts' }).then((response) => {
response.entries.forEach(entry => {
const { name, path_lower } = entry;
if (entry[".tag"] === "file") {
dbx.filesDownload({ path: path_lower }).then(data => {
const filename = path.resolve(postsDirectory, name);
const filecontents = data.fileBinary.toString();
fs.writeFileSync(filename, filecontents, function(error) {
if (error) {
console.error(error);
throw new Error("Failed to write Dropbox file to build", error.message);
}
});
})
.catch(error => {
console.log(error);
throw new Error("Dropbox file failed to download", name, error.message);
});
}
});
}).catch((error) => {
console.log(error);
throw new Error("Dropbox posts folder failed to list files", error.message);
});
Add your app key instead of <APP_KEY>
, and the environment variables for the DBX_CLIENT_SECRET
(get this from the app settings page under App secret) and DBX_REFRESH_TOKEN
(the refresh access token we just got).
Automate fetching from the Dropbox CMS in your package.json
and run it before your build.
"scripts": {
"build": "npm run fetch && build-command",
"fetch": "node dropbox.js",
},
Trigger builds with Dropbox webhooks
Let's go the extra mile, and trigger a fresh build when content is changed or added to the app folder. If you use a static hosting provider that provides build hooks, like Netlify or Vercel, you can trigger it with a Dropbox webhook.
In your Dropbox app, under the settings tab, scroll down to the Webhooks section.
Here you can add a URL that Dropbox will call whenever changes are made.
You might be tempted to put a build hook in here, but it won't work like that because Dropbox requires you to verify the URL you provide by responding with a string they send you.
But luckily, I found a solution written by jimneils in a Netlify Function. Here's the Dropbox webhook function that will verify the request, and trigger a build.