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:
- Update your file architecture
- Launch your first deployment
- 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:
- Setup
.lume/entrypoint.ts
- Create a
deploy
task indeno.json
- Add deployment configuration in
deno.json
- 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.tsimport 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 theproject
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.
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, yourdeno.json
has been updated with theproject
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:
- Add our access token into a secret
- Configure workflow triggers
- Create a
build
job for site generation - 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.
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.ymlname: 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.ymlname: 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.ymlname: 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:
- mathiasvr/command-output will expose the
STDOUT
of our command. - actions/github-script will execute a Javascript without the
STDOUT
content and create a PR comment.
// my-awesome-blog/.github/workflows/deploy.ymlname: 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:
- Read Deno Deploy documentation
- Follow Lume - Deployment section for your specific use case
Happy blogging 🎉!
Thanks a lot to Tatiana for the help and the feedback in this blog post.