Photo by Rubaitul Azad on Unsplash
What I built
For the Hackathon I created a markdown based blogging website which triggers a preconfigured GitHub Codespace for writing new blog posts. To assist with writing the posts, I also created a new VSCode extension which uses OpenAI APIs to rewrite your sentences. This extension is also preconfigured in the repo's codespaces. Finally, on new commits a GitHub action kicks in to generate a static version of the website and deploy it to GitHub Pages.
Category Submission:
Maintainer Must-Haves
DIY Deployments
Wacky Wildcards
App Link
https://ra-jeev.github.io/gh-blog/
Screenshots
The VSCode Extension Screenshots
Description
gh-blog is a markdown based blog website. It utilizes the GitHub APIs to directly trigger the start of a preconfigured GitHub Codespace, thus allowing us to add new posts or edit the existing ones. To safeguard the trigger it requires you to login to the website as an admin.
The current features set include the following
- A markdown based blog / content website template
- A hidden login page for signing in to the website as an admin
- A button click trigger to securely start a preconfigured codespace from a server environment
- A brand new OpenAI Writer Assistant preconfigured in the codespace for rewriting your sentences / paragraphs
- A Github Action to generate the static website on every push commit to the repository and deploy it to GitHub Pages
Link to Source Code
My Website
My website made by Content Wind theme.
Setup
npm install
Development
npm run dev
Then open http://localhost:3000 to see your app.
Deployment
Learn more how to deploy on Nuxt docs.
ra-jeev / write-assist-ai
OpenAI-powered Text Rewriter for VS Code. Works with Markdown, LaTeX and text files. Fully customizable through settings.
Write Assist AI
The WriteAssistAI extension for VSCode utilizes the OpenAI APIs to offer AI-powered writing assistance for markdown, LaTeX and plain text files. It comes with some default actions to rephrase the selected text, or perform tasks like tone change, summarize, expand etc. These actions are completely configurable through the extension's settings.
π― Features
This AI text assistant provides a range of writing styles for you to select from. To access these styles, and other features, simply select the text you want to rewrite in a supported file. Then, click on the Code Actions π‘ tooltip and choose the desired action.
Current feature list:
- Rewrite the text using different tones. You can choose from professional, casual, formal, friendly, informative, and authoritative tones.
- Rephrase selected text
- Suggest headlines for selected text
- Summarize selected text
- Expand selected text (make it verbose)
- Shorten selected text (make it concise)
You can modify theβ¦
Permissive License
Both the repositories have permissive MIT Licenses.
Background (What made you decide to build this particular app? What inspired you?)
AI is current hype, and I've been feeling a lot of FOMO about it. So, I wanted to use it for the hackathon. I also wanted to create something tangible which can be used by others, or at the least be a starting point. Both of these desires guided me towards creating an AI Writer Assistant and a blogging website template. I chose to use Nuxt3 Content
to build the template as it is based on markdown files which can be edited easily from inside a codespace.
How I built it (How did you utilize GitHub Actions or GitHub Codespaces? Did you learn something new along the way? Pick up a new skill?)
Creating the VSCode Extensison
The first step in the journey was to create the extension as I wanted to use it inside the codespace. But there was a problem, I have never created an extension before.
After some Googling I found this helpful guide on how to create your first extension from VSCode. I wanted this extension to have the following features
- Work for markdown files
- It should create some kind of overlay on text selection.
To achieve #1, I needed to mention the activationEvents
in the package.json
of the extension.
"activationEvents": [
"onLanguage:markdown"
],
To achieve #2, I found out that I can use the CodeActionKind
provider. So my activate function takes the below form
export function activate(context: vscode.ExtensionContext) {
const writeAssist: WriteAssistAI = new WriteAssistAI();
const aiActionProvider = vscode.languages.registerCodeActionsProvider(
'markdown',
writeAssist,
{
providedCodeActionKinds: WriteAssistAI.providedCodeActionKinds,
}
);
context.subscriptions.push(aiActionProvider);
for (const command of writeAssist.commands) {
context.subscriptions.push(
vscode.commands.registerCommand(command, () =>
writeAssist.handleAction(command)
)
);
}
}
Whenever any text is selected provideCodeActions
from WriteAssistAI
class is called and we provide the available actions (the rewrite options we see in the above screenshots). And when the user selects one of the actions, we call OpenAI API to rewrite the selected text in the user selected tone.
For allowing users to use their OpenAI API Key, I needed to create a setting for it in the package.json
file.
"contributes": {
"configuration": {
"title": "Write Assist AI",
"properties": {
"writeAssistAi.openAiApiKey": {
"type": "string",
"default": "",
"description": "Enter you OpenAI API Key here"
},
"writeAssistAi.maxTokens": {
"type": "number",
"default": 1200,
"description": "Enter the maximum tokens to use for each OpenAI API call"
}
}
}
},
For further information and the complete implementation, you can check the code in the attached repository.
Creating the Blog Website
Nuxt provides a great content module for creating markdown based blogs/websites. I started with the Content Wind theme created by the Nuxt team, and then used some of the components from the alpine theme to create the final website.
You can get started by using the below command
npx nuxi init -t themes/content-wind my-website
For more information on configuring the theme to your taste, you can visit this link
Configuring GitHub Codespaces
Since this is a markdown based website, it is quite handy to use GitHub Codespaces for adding / editing markdown files. And to use the VSCode extension I created (and the other helpful extensions), you'll need to configure the codespace manually. But there is a better way, create a devcontainer.json
file inside the .devcontainer
folder in the root of the repo.
This is the configuration I used for the container
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "gh-blog",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-bullseye",
"hostRequirements": {
"cpus": 4
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000],
"portsAttributes": {
"3000": {
"label": "Application",
"onAutoForward": "openPreview"
}
},
"waitFor": "onCreateCommand",
// install the dependencies automatically
"updateContentCommand": "yarn install",
"postCreateCommand": "",
// As soon as the container is attached run the nuxt server in dev mode
"postAttachCommand": "yarn dev",
// Add the needed customizations & extensions
"customizations": {
"vscode": {
"extensions": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ra-jeev.write-assist-ai",
"DavidAnson.vscode-markdownlint"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
Trigger Codespace Start from website
After configuring the container I created a codespace once from the GitHub website. Now whenever I want to create new posts, I wanted to start this codespace programmatically from the website itself. But not everyone should be able to trigger this, so I needed to add authentication to the website, and create an admin user.
To achieve the above requirement I used Firebase Auth (and Rowy for adding the ADMIN claims to my account).
How Rowy helps here?
Without Rowy, to create an Admin user you'll need to spin up a new Firebase Cloud function (relevant documentation) which will set the Admin claims in the IdToken. Since my blog has one owner (me), I simply used the same emailId, which I'm going to use for my blog, to create a project on Rowy (it links to your existing Firebase project). Rowy internally uses Firebase Authentication, and sets the needed claims automatically for that email id.
These are the claims returned by firebase auth (calling user.getIdTokenResult()
) after using Rowy
As you can see, the IdToken has two roles ["OWNER", "ADMIN"] assigned to it inside the claims
attribute. Just using Rowy for creating an account and a project in their dashboard was enough to give me this. If we need to add more admins, or create new roles (say Editors), then we can do so easily using the Rowy project's workspace.
For admin users, a New Post
button gets visible in the App Toolbar. On the button click we call a Firebase Cloud Function
which has a preconfigured GitHub Fine Grained Access Token
(with codespaces_lifecycle_admin
permisssion).
Below is the code for triggering start of an existing codespace (the codespace name is also stored as an environment variable of the function) for a user
const functions = require('firebase-functions');
const { Octokit } = require('@octokit/rest');
const admin = require('firebase-admin');
const cors = require('cors');
const { defineSecret, defineString } = require('firebase-functions/params');
const githubToken = defineSecret('github_token');
const codespaceName = defineString('GITHUB_CODESPACE_NAME');
admin.initializeApp();
exports.startCodespaces = functions
.region('europe-west1')
.runWith({ secrets: [githubToken] })
.https.onRequest(async (req, resp) => {
functions.logger.info('Incoming startCodespaces req!', {
structuredData: true,
});
cors({ origin: true })(req, resp, async () => {
// Verify that the calling user is an ADMIN
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer ')
) {
const idToken = req.headers.authorization.split('Bearer ')[1];
const decodedToken = await admin.auth().verifyIdToken(idToken);
functions.logger.log('decodedToken', decodedToken);
if (decodedToken.roles && decodedToken.roles.includes('ADMIN')) {
const octokit = new Octokit({
auth: githubToken.value(),
});
functions.logger.log('codespace name:', codespaceName.value());
try {
const codespace =
await octokit.codespaces.startForAuthenticatedUser({
codespace_name: codespaceName.value(),
});
functions.logger.log('got some codespace response', codespace);
return resp.status(200).send(JSON.stringify(codespace.data));
} catch (error) {
functions.logger.error(error);
resp
.status(500)
.send(error.message || 'Failed to start the workspace');
}
}
}
resp.status(401).send('Not authorized');
});
});
This function returns the WEB_URL of the codespace, which we use to open the codespace in a new window.
Before using the Codespace, do not forget to create the needed environment variables by going to your repository settings -> Secret and variables -> Codespaces
.
Adding GitHub Action for Auto Deployments
Now we just need a way to automatically deploy our changes to GitHub Pages whenever a new commit is made. I used the below action file to do this
name: Deploy to GitHub Pages
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Detect package manager
id: detect-package-manager
run: |
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
echo "manager=yarn" >> $GITHUB_OUTPUT
echo "command=install" >> $GITHUB_OUTPUT
exit 0
elif [ -f "${{ github.workspace }}/package.json" ]; then
echo "manager=npm" >> $GITHUB_OUTPUT
echo "command=ci" >> $GITHUB_OUTPUT
exit 0
else
echo "Unable to determine package manager"
exit 1
fi
- name: Setup node env
uses: actions/setup-node@v3
with:
node-version: 18
cache: ${{ steps.detect-package-manager.outputs.manager }}
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Restore cache
uses: actions/cache@v3
with:
path: |
.output/public
.nuxt
key: ${{ runner.os }}-nuxt-build-${{ hashFiles('.output/public') }}
restore-keys: |
${{ runner.os }}-nuxt-build-
- name: Install dependencies
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
- name: Generate the static files
run: ${{ steps.detect-package-manager.outputs.manager }} run generate
env:
# Setting an environment variable with the value of a configuration variable
FIREBASE_API_KEY: ${{ vars.FIREBASE_API_KEY }}
FIREBASE_AUTH_DOMAIN: ${{ vars.FIREBASE_AUTH_DOMAIN }}
FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }}
FIREBASE_STORAGE_BUCKET: ${{ vars.FIREBASE_STORAGE_BUCKET }}
FIREBASE_MESSAGING_SENDER_ID: ${{ vars.FIREBASE_MESSAGING_SENDER_ID }}
FIREBASE_APP_ID: ${{ vars.FIREBASE_APP_ID }}
START_CODESPACE_FN_URL: ${{ vars.START_CODESPACE_FN_URL }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: .output/public
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
The above action has the following differences from the official Nuxt Deploy to GitHub Pages Action
- The official action uses
static_site_generator: nuxt
setting in theSetup Pages
step above. This doesn't work forNuxt3
so we need to remove that. - Any occurrence of "dist" folder needs to be replaced with
.output/public
as this is where the statically generated website files are present now - Since we have some environment variables, we need to create them in the repo settings, and use them here while building
Note: If you're not hosting it on the .github.io domain then you'll need to configure the app baseURL
inside your nuxt.config.ts
file.
export default defineNuxtConfig({
extends: 'content-wind',
app: {
baseURL: '/gh-blog/', // baseURL: '/<repository_name>/'
},
})
And done. Now our website will be auto built by the Github Action and deployed to GitHub pages.
GitHub Action for creating drafts on dev.to
We can also create a new GitHub Action for automatically creating drafts (or directly publish) on DEV. To do that we need to get an API Key for the Dev Community APIs. You can visit this link to know more about how to create an API Key, and also how to use the APIs.
This is the action which gets triggered only if there has been a change in the content/blog
folder (that is where we keep our blog posts). We also restrict the change to markdown files only.
name: Create drafts on dev.to
on:
push:
branches:
- main
paths:
- 'content/blog/**.md'
permissions:
contents: read
jobs:
new-posts-check:
runs-on: ubuntu-latest
outputs:
num_added_files: ${{ steps.get_new_files.outputs.num_added_files }}
new_files: ${{ steps.get_new_files.outputs.new_files }}
steps:
- name: Get Files Added
id: get_new_files
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/compare/${{ github.event.before }}...${{ github.event.after }} > /tmp/diff_data.json
new_files=$(jq -c '.files | map(select(.status == "added" and (.filename | startswith("content/blog"))) | {filename})' /tmp/diff_data.json)
echo "new files = $new_files"
num_added_files=$(echo "$new_files" | jq length)
echo "num_added_files = $num_added_files"
echo "num_added_files=$num_added_files" >> "$GITHUB_OUTPUT"
echo "new_files=$new_files" >> "$GITHUB_OUTPUT"
parse-and-create-drafts:
needs: new-posts-check
runs-on: ubuntu-latest
if: needs.new-posts-check.outputs.num_added_files > 0
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup node env
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm install axios gray-matter
- name: Parse markdown files and create drafts
env:
DEV_API_KEY: ${{ secrets.DEV_API_KEY }}
GH_PAGES_WEB_URL: 'https://${{github.repository_owner}}.github.io/${{github.event.repository.name}}'
run: |
node ./scripts/parseAndCreateDrafts.js '${{ needs.new-posts-check.outputs.new_files }}'
If triggered, the action checks if any new files were added to the said folder (we're not handling edits or deletes for now). We do this using the gh api
. If the answer is in the affirmative, then we parse those new files and create drafts using the /api/articles
endpoint. Please remember to add your DEV_API_KEY
secret in the repo action secrets.
And this is the script file which uses gray-matter
to parse the markdown files, and then posts them to DEV using axios
.
const matter = require('gray-matter');
const { readFileSync } = require('fs');
const axios = require('axios');
// Function to create a draft on dev.to
async function createDraftPostOnDev(article) {
try {
const res = await axios.post(
'https://dev.to/api/articles',
{ article },
{
headers: {
'api-key': process.env.DEV_API_KEY,
accept: 'application/vnd.forem.api-v1+json',
},
}
);
console.log(`Article posted successfully: ${res.data.url}`);
return res.data;
} catch (error) {
console.error('Failed to post the article:', error);
}
}
// Function to parse Markdown file using gray-matter
function parseMarkdownFile(filename) {
try {
const fileContent = readFileSync(filename, 'utf8');
const { data, content } = matter(fileContent);
const post = {
title: data.title,
description: data.description,
body_markdown: content,
canonical_url:
process.env.GH_PAGES_WEB_URL +
filename.split('content')[1].replace('.md', ''),
};
if (data.image?.src) {
post.main_image = process.env.GH_PAGES_WEB_URL + data.image.src;
}
console.log('final post', post);
return post;
} catch (error) {
console.error('Error:', error);
}
}
const main = async () => {
const args = process.argv.slice(2);
const filenames = JSON.parse(args[0]);
const res = [];
for (const file of filenames) {
const post = parseMarkdownFile(file.filename);
if (post) {
const result = await createDraftPostOnDev(post);
if (result) {
res.push(result);
}
// Wait for 5 seconds before posting another article
// This is to avoid getting 429 Too Many Requests error
// from the dev.to API
await new Promise((r) => setTimeout(r, 5000));
}
}
console.log('res:', res);
};
main();
Additional Resources/Info
Some of the resources which helped me:
- Creating your first VSCode Extension
- VSCode CodeActions Sample
- Content Wind Nuxt theme
- Configuring your dev container
Further Enhancements
- Currently there is no way to subscribe to the blog and get notified on new posts. I'm planning to use a GitHub Action and Rowy (with its SendGrid integration) to add this functionality
- When a post is edited, automatically update the post on
The Dev Platform
. - Implement a way to add comments to articles.
- Further enhance the WriteAssistAI and add more refactoring options
Conclusion
GitHub Codespaces provide a very handy and easy way to get stated from any machine without configuring it for development first. Overall it was a great experience to use Codespaces and GitHub Actions to automate a part of the workflow. I thoroughly enjoyed creating the two projects.
I hope you liked reading the article. Do share your thoughts in the comments section. :-)
Top comments (0)