Publish your blog with Deno Deploy

by furiouzz

8 min read

In a previous post, we talked about how to create a blog with Deno and Lume. In this post, we will make it accessible publicly with Deno Deploy.

To follow this post, you need a file structure similar to the one created in the section Create files and directories. Let me remind you how it looks like:

my-awesome-blog/
├── .lume/
   └── config.ts
├── content/
   ├── posts/
   ├── _data.yml
   └── 2024-01-01.md
   └── pages/
       ├── _data.yml
       └── about.md
├── _data.yml
└── deno.json

I used Lume, a static site generator, to generate our blog. So I have configured our deno.json file with frequently used scripts into the tasks field and our necessary libraries into the imports field.

// my-awesome-blog/deno.json
{ "imports": { "lume/": "https://deno.land/x/lume@v2.0.1/", "blog/": "https://deno.land/x/lume_theme_simple_blog@v0.10.2/" }, "tasks": { "lume": "echo \"import 'lume/cli.ts'\" | deno run --unstable -A - --config .lume/config.ts", "build": "deno task lume", "dev": "deno task lume -s" } }

Our objective is to run a web server which will serve the directory .lume/_site, produced by the build task. The server will be deployed to Deno Deploy.

What is Deno Deploy?

Deno Deploy is a service for hosting Javascript application. Your application is hosted on server geographically close to your users, which enable low latency and fast response times.

Deno Deploy has a free plan for side-project, like a blog.

Lume has an interesting Deployment section for deployment on most popular services, including Deno Deploy. However we will not follow the Deno Deploy section because it uses a Github action.

For this post, I prefer to use deployctl, a command line for Deno Deploy. Even if we will use Github Actions as our CI/CD tool, it will be easier to adapt the automation section to your CI/CD pipeline.

CI/CD stands for Continuous Integration and Continuous Delivery/Deployment.

As of January 2024, Deno Deploy use Github as its primary integration. You need a Github account to connect to Deno Dashboard.

Here is the plan:

  1. Update your file architecture
  2. Launch your first deployment
  3. Automate deployment with a CI/CD pipeline

File architecture

In our targeted file architecture, we will create two new files:

  • .lume/entrypoint.ts, the script executed by Deno Deploy
  • .github/workflows/deploy.yml , the Github workflow for site generation and deployment
my-awesome-blog/
├── .github/
   └── workflows/
   └── deploy.yml
├── .lume/
   ├── entrypoint.ts
   └── config.ts
├── content/
   ├── posts/
   ├── _data.yml
   └── 2024-01-01.md
   └── pages/
       ├── _data.yml
       └── about.md
├── _data.yml
└── deno.json

First deployment

Take the following steps to configure deployctl:

  1. Setup .lume/entrypoint.ts
  2. Create a deploy task in deno.json
  3. Add deployment configuration in deno.json
  4. Generate an access token on Deno Dashboard

Let's start with our entrypoint. The entrypoint will start a web server and will serve .lume/_site/ directory.

Edit .lume/entrypoint.ts:

// my-awesome-blog/.lume/entrypoint.ts
import Server from "https://deno.land/x/lume@v2.0.2/core/server.ts"; const server = new Server({ root: `${Deno.cwd()}/.lume/_site`, port: 8000, }) server.start();

Let's add a task preview to run the .lume/entrypoint.ts:

// my-awesome-blog/deno.json
{ "imports": { "lume/": "https://deno.land/x/lume@v2.0.1/", "blog/": "https://deno.land/x/lume_theme_simple_blog@v0.10.2/" }, "tasks": { "lume": "echo \"import 'lume/cli.ts'\" | deno run --unstable -A - --config .lume/config.ts", "build": "deno task lume", "preview": "deno run -A .lume/entrypoint.ts", "dev": "deno task lume -s" } }

The preview task mainly exists for testing your server locally. Deno Deploy will run .lume/entrypoint.ts by itself.

Now run these commands below to build and serve your blog at http://localhost:8000/:

$ deno task build
$ deno task preview

Now we will add a deploy task and a new deploy field for configuration.

The new deploy field will contains following configurations:

  • project is the name of your project. It can be whatever you want.
  • entrypoint targets the script file executed by Deno Deploy.
  • include lists files and directories to be deployed.
  • exclude lists files and directories to not be deployed.
// my-awesome-blog/deno.json
{ "imports": { "lume/": "https://deno.land/x/lume@v2.0.1/", "blog/": "https://deno.land/x/lume_theme_simple_blog@v0.10.2/" }, "deploy": { "project": "YOUR_PROJECT_NAME", "entrypoint": ".lume/entrypoint.ts", "include": [ "./.lume/entrypoint.ts", "./.lume/_site" ], "exclude": [ "**/node_modules" ] }, "tasks": { "deploy": "deno run --env -A https://deno.land/x/deploy@1.10.3/deployctl.ts deploy", "lume": "echo \"import 'lume/cli.ts'\" | deno run --unstable -A - --config .lume/config.ts", "build": "deno task lume", "preview": "deno run -A .lume/entrypoint.ts", "dev": "deno task lume -s" } }

Pay attention to the YOUR_PROJECT_NAME value. You must replace it with your own project name or remove this line. If you choose to remove the line, Deno Deploy will generate a name for you and will set the project field with the project id.

Here is our last step. Go to account settings, then look for Access tokens section. Click on New Access Token button.

access tokens section with new acces token button

Once generated, run the command below with your access token:

$ export DENO_DEPLOY_TOKEN=YOUR_ACCESS_TOKEN

Now we are ready to launch your first deployment:

$ deno task deploy

Tada 🎉! Your blog is deployed.

You have an output similar to the one below:

 Production deployment complete.
 Updated config file '/workspaces/my-awesome-blog/deno.json'.

View at:
 - https://YOUR_PROJECT_NAME-DEPLOYMENT_ID.deno.dev
 - https://YOUR_PROJECT_NAME.deno.dev

Let me explain these lines:

  • This is our first deployment, so Deno Deploy deploy to production by default.
  • If you omitted the project field, your deno.json has been updated with the project field setted.
  • You can visit your application at two URLs: the preview URL and the production URL.

As it is our first deployment, the two URLs target the same deployment specified by DEPLOYMENT_ID. If you run the deploy task a second time, you only have the preview URL with a different DEPLOYMENT_ID.

To promote a deployment to production, you have to specify the --prod flag.

$ deno task deploy --prod

Now https://YOUR_PROJECT_NAME.deno.dev targets the new deployment.

At the moment, only new deployment can be promoted to production with deployctl.

If you want to promote a specific deployment, you have to:

  • Connect to the Deno Dashboard
  • Go to Projects and select your project
  • Go to Deployments
  • Click on the three dot icon near the deployment you want to promote

Automate deployment

To automate our deployment, we will create a CI/CD pipeline with Github workflow.

We will apply the follwing rules to our workflow:

  • When we push to the main branch, the deployment is promoted to production.
  • When we push to a pull request targeting the main branch, the deployment is promoted to preview.

We will follow these steps to setup our Github workflow:

  1. Add our access token into a secret
  2. Configure workflow triggers
  3. Create a build job for site generation
  4. Create a deploy job for deployment

First, let's add our secret!

Click on Settings, the button is placed in the navigation bar of your repository.

Settings button on Github repository

On the sidebar on the left, look for the Security section, unfold Secrets and variables and click on Actions.

Click on New repository secret.

Specify DENO_DEPLOY_TOKEN in the name field and set your access token in the value field. Then click on the Add secret button.

Now let's edit github/workflows/deploy.yml:

// my-awesome-blog/.github/workflows/deploy.yml
name: Deploy on: # Triggered when you push to `main` push: branches: - main # Triggered when you have to PR pointing to main pull_request: branches: - main

Now let's create our first build job:

// my-awesome-blog/.github/workflows/deploy.yml
name: Deploy on: # (truncated...) jobs: # Job for generating .lume/_site directory and its files build: name: Build runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v4 - name: Install Deno uses: denoland/setup-deno@v1 with: deno-version: v1.x - name: Build run: deno task build - name: Archive _site Artifact uses: actions/upload-artifact@master with: name: _site path: .lume/_site/

Then let's create our second deploy job, executed after the build job. This job will promote the deployment to production only when we push to the main branch:

// my-awesome-blog/.github/workflows/deploy.yml
name: Deploy on: # (truncated...) jobs: # Job for generating .lume/_site directory and its files build: # (truncated...) # Job for deploying .lume/_site deploy: name: Deploy needs: build runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v4 - name: Install Deno uses: denoland/setup-deno@v1 with: deno-version: v1.x - name: Download Artifact uses: actions/download-artifact@master with: name: _site path: .lume/_site/ - name: Upload to Deno Deploy env: DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }} run: deno task deploy ${{ endsWith(github.ref, '/main') && '--prod' || '' }}

Push your changes, click on Actions tab of your repository

then check your deployment.

Create a PR comment

Now it would be interesting to create to our PR with our deployment URLs.

To create this comment, we will create another step with some Javascript code. Also, we need to edit our deployment step to catch the STDOUT of our command.

For that, we will use two more actions:

// my-awesome-blog/.github/workflows/deploy.yml
name: Deploy on: # (truncated) jobs: # Job for generating .lume/_site directory and its files build: # (truncated) # Job for deploying .lume/_site deploy: name: Deploy needs: build runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v4 - name: Install Deno uses: denoland/setup-deno@v1 with: deno-version: v1.x - name: Download Artifact uses: actions/download-artifact@master with: name: _site path: .lume/_site/ - name: Upload to Deno Deploy env: DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }} run: deno task deploy ${{ endsWith(github.ref, '/main') && '--prod' || '' }} id: deploy uses: mathiasvr/command-output@v2.0.0 with: run: deno task deploy ${{ endsWith(github.ref, '/main') && '--prod' || '' }} - name: Create comment with deploy output if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const output = ` ${{ steps.deploy.outputs.stdout }} *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`* `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: output })

What's next?

Now we have everything to create pages and posts, then to deploy your blog online on a server.

For more information:

Happy blogging 🎉!

Thanks a lot to Tatiana for the help and the feedback in this blog post.