Multi-Platform Blog Publisher
🚀 Automatically publish your articles to multiple blogging platforms using GitHub Actions
Table of Contents
- Overview
- Features
- Quick Start
- Repository Structure
- Installation
- Usage
- Configuration
- Content Format
- Setup Instructions
- Usage Examples
- Troubleshooting
- Contributing
- License
- Acknowledgments
Overview
This project automates the process of synchronizing content between GitHub README files and various blogging platforms. Eliminate manual copy-pasting and separate content maintenance—write once in Markdown and publish everywhere!
GitHub README → Blog Post Automation
Supported Platforms
- ✅ Hashnode - Fully integrated
- ✅ Dev.to - Fully integrated
- 🚧 Medium - API restrictions prevent automation
- 🚧 LinkedIn - OAuth limitations prevent API usage
Features
- ⚡ Zero Setup: No need to create API scripts—everything is included
- 🌐 Multi-Platform: Supports Hashnode and Dev.to
- 🧠 Smart Updates: Automatically tracks and updates existing posts
- 🖼️ Image Processing: Converts relative image paths to GitHub raw URLs
- 📝 Flexible Content: Works with any markdown content structure
- 🔒 Secure: All credentials stay in your repository secrets
- ♻️ State Management: Saves post metadata for future updates
Quick Start
1. Fork or Use This Repository
gokulnathan66 article-automation Repo
You can either fork this repository or use it as a GitHub Action in your own repository.
2. Set Up Your Content Repository
Create a repository structure like this:
your-blog-repo/
├── content/ # Your articles directory
│ └── README.md # Your article content
├── .github/workflows/
│ └── publish.yml # Workflow using these actions
├── images/ # Optional: images referenced in articles
│ └── screenshot.png
└── README.md # Project documentation
3. Configure GitHub Secrets
Go to Settings → Secrets and variables → Actions and add the required secrets.
4. Create Workflow File
Create .github/workflows/publish.yml with the provided configuration.
5. Push and Publish! 🎉
Your articles will be automatically published when you push to the main branch.
Repository Structure
article-automation/
├── hashnode-publish/
│ ├── action.yml # Hashnode action definition
│ └── scripts/
│ └── hashnode.js # Hashnode publishing script
├── devto-publish/
│ ├── action.yml # Dev.to action definition
│ └── scripts/
│ └── devto_post.js # Dev.to publishing script
├── api/ # Legacy API scripts
│ ├── hashnode.js
│ └── devto_post.js
├── package.json # Dependencies for scripts
├── image.png # Project logo
└── README.md # This file
Installation
Method 1: Use as GitHub Action (Recommended)
Add this to your workflow file:
- name: Publish to Hashnode
uses: gokulnathan66/article-automation/hashnode-publish@main
with:
hashnode-pat: ${{ secrets.HASHNODE_PAT }}
# ... other configuration
Method 2: Fork and Customize
- Fork this repository
- Customize the scripts in
hashnode-publish/scripts/ordevto-publish/scripts/ - Update action configurations in
action.ymlfiles - Use your forked version in workflows
Usage
GitHub Secrets Configuration
For Hashnode:
-
HASHNODE_PAT- Your Hashnode Personal Access Token -
HASHNODE_PUBLICATION_ID- Your publication ID -
HASHNODE_PUBLICATION_HOST- Your publication domain (e.g.,yourblog.hashnode.dev) -
VAR_EDIT_TOKEN_GIT- GitHub token withrepoandactions:writepermissions
For Dev.to:
-
DEV_TO_API_KEY- Your Dev.to API Key -
VAR_EDIT_TOKEN_GIT- GitHub token withrepoandactions:writepermissions
Workflow Configuration
Create .github/workflows/publish.yml:
name: Multi-Platform Publishing
on:
push:
branches: [main]
paths:
- 'content/**'
- 'README.md'
workflow_dispatch:
env:
NODE_VERSION: '20'
jobs:
publish-hashnode:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout content
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate required secrets
run: |
if [ -z "${{ secrets.HASHNODE_PAT }}" ]; then
echo "Error: HASHNODE_PAT secret is not set"
exit 1
fi
- name: Publish to Hashnode
uses: gokulnathan66/article-automation/hashnode-publish@main
with:
hashnode-pat: ${{ secrets.HASHNODE_PAT }}
hashnode-publication-id: ${{ secrets.HASHNODE_PUBLICATION_ID }}
hashnode-publication-host: ${{ secrets.HASHNODE_PUBLICATION_HOST }}
github-token: ${{ secrets.VAR_EDIT_TOKEN_GIT }}
saved-post-id: ${{ vars.HASHNODE_SAVED_POST_ID }}
saved-post-slug: ${{ vars.HASHNODE_SAVED_POST_SLUG }}
saved-post-title: ${{ vars.HASHNODE_SAVED_POST_TITLE }}
saved-post-url: ${{ vars.HASHNODE_SAVED_POST_URL }}
saved-post-published-at: ${{ vars.HASHNODE_SAVED_POST_PUBLISHED_AT }}
saved-post-updated-at: ${{ vars.HASHNODE_SAVED_POST_UPDATED_AT }}
publish-devto:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout content
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate required secrets
run: |
if [ -z "${{ secrets.DEV_TO_API_KEY }}" ]; then
echo "Error: DEV_TO_API_KEY secret is not set"
exit 1
fi
- name: Publish to Dev.to
uses: gokulnathan66/article-automation/devto-publish@main
with:
devto-api-key: ${{ secrets.DEV_TO_API_KEY }}
github-token: ${{ secrets.VAR_EDIT_TOKEN_GIT }}
saved-post-id: ${{ vars.DEV_TO_SAVED_POST_ID }}
saved-post-title: ${{ vars.DEV_TO_SAVED_POST_TITLE }}
saved-post-url: ${{ vars.DEV_TO_SAVED_POST_URL }}
saved-post-published-at: ${{ vars.DEV_TO_SAVED_POST_PUBLISHED_AT }}
saved-post-updated-at: ${{ vars.DEV_TO_SAVED_POST_UPDATED_AT }}
Configuration
Hashnode Action Inputs
| Input | Required | Default | Description |
|---|---|---|---|
hashnode-pat |
✅ | - | Hashnode Personal Access Token |
hashnode-publication-id |
✅ | - | Publication ID |
hashnode-publication-host |
✅ | - | Publication host |
github-token |
✅ | - | GitHub token |
content-path |
❌ | 'content' |
Path to content files |
node-version |
❌ | '20' |
Node.js version |
saved-post-* |
❌ | - | Previously saved post data for updates |
Dev.to Action Inputs
| Input | Required | Default | Description |
|---|---|---|---|
devto-api-key |
✅ | - | Dev.to API Key |
github-token |
✅ | - | GitHub token |
content-path |
❌ | 'content' |
Path to content files |
node-version |
❌ | '24' |
Node.js version |
saved-post-* |
❌ | - | Previously saved post data for updates |
Content Format
Your markdown files should follow this structure:
# Your Article Title
Your content here with markdown formatting.
## Sections
You can include images, code blocks, and other markdown elements.

### Code Examples
javascript
// Example: Basic error handling in Node.js
try {
const result = processData();
console.log("Processing successful:", result);
} catch (error) {
console.error("Error occurred:", error.message);
process.exit(1);
}
function processData() {
// Your processing logic here
return "Hello, World!";
}
<!-- Add tags at the end (optional) -->
Tags: javascript, tutorial, webdev, githubactions
Tag "github-actions" contains non-alphanumeric or prohibited unicode characters
Important: Tags should be added at the end of your content using the format
Tags: tag1, tag2, tag3. Avoid non-alphanumeric characters in tags.
Setup Instructions
Getting Hashnode Credentials
1. Personal Access Token
- Go to Hashnode → Settings → Developer
- Generate Personal Access Token
- Add as
HASHNODE_PATsecret in your repository
2. Publication Details
- Go to your publication dashboard
- Copy Publication ID from the URL or settings
- Use your publication domain (e.g.,
yourblog.hashnode.dev)
Getting Dev.to API Key
- Go to Dev.to → Settings → Extensions
- Scroll to "DEV Community API Keys"
- Generate a new API key
- Add as
DEV_TO_API_KEYsecret in your repository
Setting up GitHub Token
- Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click "Generate new token" → "Generate new token (classic)"
- Configure the token:
-
Note:
Article Automation Token - Expiration: Choose appropriate duration (30-90 days recommended)
-
Scopes: Select the following:
- ✅
repo(full repository access) - ✅
workflow(update GitHub Action workflows) - ✅
write:packages(if using packages)
- ✅
-
Note:
- Click "Generate token" and copy the token immediately
- Add as
VAR_EDIT_TOKEN_GITsecret in your repository
Security Note:
- Set an expiration date for your GitHub token for better security
- GitHub will notify you before the token expires
- Store the token securely—you won't be able to see it again
- Regenerate tokens periodically as a security best practice
Local Development Setup
For testing locally, create a .env file in your project root:
# Required for Hashnode
HASHNODE_PAT=your_hashnode_personal_access_token
HASHNODE_PUBLICATION_ID=your_publication_id
HASHNODE_PUBLICATION_HOST=yourblog.hashnode.dev
# Required for Dev.to
DEV_TO_API_KEY=your_devto_api_key
# Required for GitHub operations
VAR_EDIT_TOKEN_GIT=your_github_token
# Optional: Content path (defaults to 'content')
CONTENT_PATH=content
⚠️ Important: Never commit your
.envfile to version control. Add it to your.gitignorefile.
Usage Examples
Single Platform Deployment
Hashnode only:
uses: gokulnathan66/article-automation/hashnode-publish@main
Dev.to only:
uses: gokulnathan66/article-automation/devto-publish@main
Custom Content Path
uses: gokulnathan66/article-automation/hashnode-publish@main
with:
# ... other inputs
content-path: 'articles' # Look in 'articles' directory instead
Different Node.js Version
uses: gokulnathan66/article-automation/devto-publish@main
with:
# ... other inputs
node-version: '18' # Use Node.js 18 instead of default 24
How It Works
-
Content Detection: Finds
README.mdin your content directory or repository root -
Title Extraction: Uses the first
# headingas the article title - Image Processing: Converts relative image paths to GitHub raw URLs
-
Tag Processing: Extracts tags from
Tags:line at the end of content -
Smart Publishing:
- Checks for existing posts with the same title
- Creates new post if none exists
- Updates existing post if found
- State Management: Saves post IDs and metadata to repository variables for future updates
Troubleshooting
Common Issues
| Issue | Solution | Additional Steps |
|---|---|---|
| Action not found | Ensure repository is public or you have proper access | Check the action reference path is correct |
| README not found | Check content directory path and verify file exists | Verify content-path input matches your structure |
| API errors | Verify all secrets are correctly configured | Test API keys manually with curl commands |
| Permission errors | Ensure GitHub token has required scopes | Regenerate token with proper permissions |
| Image not loading | Check image paths and ensure they're accessible | Use absolute URLs or verify relative paths |
| Tags not working | Check tag format and characters | Use only alphanumeric characters and hyphens |
| Content not updating | Clear browser cache and check post URLs | Verify the post ID variables are updated |
Debugging Steps
- Check Action Logs: Go to the Actions tab in your repository for detailed execution logs
- Verify Secrets: Ensure all required secrets are set in repository settings
- Test API Keys:
# Test Hashnode API
curl -H "Authorization: Bearer YOUR_TOKEN" https://gql.hashnode.com
# Test Dev.to API
curl -H "api-key: YOUR_API_KEY" https://dev.to/api/articles/me
- Check File Paths: Verify your content structure matches the expected format
- Review Variables: Check repository variables for saved post metadata
Error Messages and Solutions
"Post not found" or "Failed to update"
- Cause: Post metadata variables are incorrect or outdated
- Solution: Delete the post variables from repository settings and republish
"Invalid API key" or "Unauthorized"
- Cause: API keys are expired or incorrect
- Solution: Regenerate API keys and update repository secrets
"File not found" errors
- Cause: Content path configuration doesn't match your repository structure
-
Solution: Update the
content-pathinput in your workflow
"Image upload failed"
- Cause: Images are not accessible or paths are incorrect
- Solution: Use GitHub raw URLs or verify image accessibility
Important Warnings
⚠️ Content Overwriting: When you update something in your blog manually, it will be overwritten by this action. You must update the README, and that will automatically update the blog.
ℹ️ Local Testing: If you want to test locally, create a
.envfile with the required variables.
Frequently Asked Questions
Q: Can I publish to multiple platforms simultaneously?
A: Yes, the workflow supports parallel publishing to both Hashnode and Dev.to. Each platform runs in a separate job.
Q: How do I handle images in my articles?
A: Use relative paths in your markdown (e.g., ). The action automatically converts them to GitHub raw URLs.
Q: Can I schedule automatic publishing?
A: Yes, add a schedule trigger to your workflow:
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM UTC
Q: What happens if my API key expires?
A: The action will fail with an authentication error. Update your repository secrets with a new API key.
Q: Can I customize the publication process?
A: Yes, fork the repository and modify the scripts in the hashnode-publish/scripts/ or devto-publish/scripts/ directories.
Q: How do I prevent certain files from triggering publication?
A: Use the paths filter in your workflow to specify which files should trigger the action.
Contributing
We welcome contributions! Here's how you can help:
Development Setup
- Fork the repository
- Clone your fork
git clone https://github.com/yourusername/article-automation.git
cd article-automation
- Install dependencies
npm install
- Create a feature branch
git checkout -b feature/amazing-feature
- Make your changes
- Test your changes (add tests if applicable)
- Commit your changes
git commit -m 'Add some amazing feature'
- Push to the branch
git push origin feature/amazing-feature
- Open a Pull Request
Contribution Guidelines
Code Standards
- Follow existing code style and conventions
- Use meaningful variable and function names
- Add comprehensive comments for complex logic
- Ensure compatibility with Node.js 18+ and 20+
Pull Request Process
-
Create Feature Branch: Use descriptive names (e.g.,
feature/add-medium-support) - Write Tests: Add unit tests for new functionality
- Update Documentation: Keep README and inline docs current
- Test Thoroughly: Ensure all tests pass and no regressions
- Write Clear Commits: Use conventional commit format
feat: add support for Medium platform
fix: resolve image upload timeout issue
docs: update API configuration examples
Testing Your Changes
# Install dependencies
npm install
# Run tests (if available)
npm test
# Test locally with sample content
node scripts/test-local.js
Areas for Contribution
- 🆕 New platform integrations (Medium, LinkedIn, etc.)
- 🔧 Improved error handling and retry logic
- 📚 Better documentation and examples
- 🧪 Comprehensive test coverage
- 🎨 UI improvements for logs and outputs
- 🔍 Advanced content processing features
Reporting Issues
When reporting issues, please include:
Required Information
- Environment: OS, Node.js version, Action version
- Configuration: Sanitized workflow file (remove secrets)
- Error Details: Complete error messages and stack traces
- Steps to Reproduce: Detailed reproduction steps
- Expected vs Actual: What should happen vs what does happen
Issue Templates
Use the appropriate issue template:
- 🐛 Bug Report: For unexpected behavior or errors
- 💡 Feature Request: For new functionality suggestions
- 📚 Documentation: For documentation improvements
- ❓ Question: For usage questions and support
Example Issue Report
**Bug Description**: Action fails when publishing large images
**Environment**:
- OS: Ubuntu 22.04
- Node.js: 20.x
- Action Version: @main
**Steps to Reproduce**:
1. Add image larger than 5MB to content
2. Push to main branch
3. Action fails with timeout error
**Expected**: Image should be processed and uploaded
**Actual**: Action times out after 5 minutes
**Logs**: [Attach relevant action logs]
License
This project is licensed under the MIT License - see the LICENSE file for details.
What this means:
- ✅ Commercial use
- ✅ Modification
- ✅ Distribution
- ✅ Private use
- ❌ Liability
- ❌ Warranty
Acknowledgments
- Built with GitHub Actions
- Integrates with Hashnode and Dev.to
- Inspired by the need for automated content distribution
- Thanks to all contributors
Made with ❤️ by Gokul Nathan B
Top comments (0)