Description
This article documents the implementation of a shared content repository architecture using Git submodules and GitHub Actions to manage markdown content across multiple static site deployments. The solution enables centralized content management with automated deployment to multiple frontends.
Architecture Overview
The implementation uses a three-repository structure:
- Content Repository (
wikip-co/content): Central repository containing all markdown files - Site Repository 1 (
wikip-co/wikip.co): Hexo static site using content as a submodule - Site Repository 2 (
anthonyrussano/anthonyrussano.com): Hexo static site using content as a submodule
When markdown files are modified in the content repository, GitHub Actions automatically triggers rebuilds of both site repositories.
Implementation Steps
Step 1: Analyze Existing Content
First, compare the content between both repositories to identify differences:
1 | # Count markdown files in each repo |
Reasoning: Understanding the differences between repositories ensures no content is lost during the merge. In this case, there were only 8 differences:
- 2 files with different content
- 6 unique files/folders across both repos
Step 2: Create Unified Content Repository
Merge content from both repositories into a single unified collection:
1 | # Create unified directory with content from one repo |
Reasoning: Starting with the repo containing more files (anthonyrussano.com with 3,195 files) as the base, then adding unique content from the other repo ensures all content is preserved. For conflicting files, manual review determined which version had more complete information.
Final unified count: 3,198 markdown files
Step 3: Create GitHub Repository for Content
Initialize the unified content as a Git repository and push to GitHub:
1 | cd /home/anthony/content-unified |
Reasoning: Using gh CLI streamlines repository creation and automatically configures the remote. The organization account (wikip-co) provides a neutral namespace for shared content accessible to both sites.
Step 4: Configure Submodules in Site Repositories
Replace the _posts directory in each site repository with the content submodule:
1 | # For wikip.co |
Reasoning: Git submodules allow referencing an external repository at a specific commit. This enables both site repos to share the same content while maintaining their own site configurations, themes, and build processes.
Step 5: Configure Submodule to Track Main Branch
Update .gitmodules to track the main branch:
1 | # Edit .gitmodules in both repos to add: |
Reasoning: By specifying branch = main, git submodule update --remote will fetch the latest commit from the main branch rather than staying locked to a specific commit. This enables automatic content updates.
Step 6: Create Trigger Workflow in Content Repository
Create a GitHub Actions workflow that triggers both site repositories when content changes:
1 | # .github/workflows/trigger-sites.yml |
Reasoning: Using repository_dispatch events allows triggering workflows in other repositories. The workflow only runs when .md files change, avoiding unnecessary builds for infrastructure changes.
Step 7: Set Up GitHub Token
Create and configure a Personal Access Token for triggering workflows:
1 | # The existing gh CLI token already had repo scope |
Reasoning: The repo scope includes permission to trigger repository_dispatch events. Using the existing gh CLI token avoids creating additional tokens, and storing it as a GitHub Secret keeps it secure.
Step 8: Update Site Repository Workflows
Modify the deployment workflows in both site repositories to:
- Listen for
repository_dispatchevents - Update submodules to pull latest content
For wikip.co (/.github/workflows/generator.yml):
1 | on: |
Update the build step:
1 | - name: Build site |
For anthonyrussano.com (.github/workflows/generator.yml):
Same changes as wikip.co, but with master branch instead of main.
Reasoning:
repository_dispatchwith typecontent-updatedallows external triggers from the content repogit submodule update --init --remotefetches the latest commit from the tracked branch (main)- Removing
--mergeflag avoids merge conflicts by simply checking out the latest commit
Step 9: Add Email Notifications (Optional)
Fetch email credentials from Vault and store as GitHub Secrets:
1 | # Fetch credentials from on-prem Vault |
Add email notification step to workflows:
1 | - name: Send email notification |
Reasoning:
- Fetching credentials from Vault maintains a single source of truth
- Storing as GitHub Secrets enables use with GitHub-hosted runners (Vault is only accessible from on-prem runners)
- Using
if: always()ensures notifications are sent even if the build fails - Including run URL in email body provides quick access to build logs
Workflow Execution
When a markdown file is updated in the content repository:
- Content repo workflow triggers on push to main
- GitHub Actions sends
repository_dispatchevents to both site repos - Site repos receive the event and trigger their workflows
- Site workflows:
- Check out code with submodules
- Run
git submodule update --init --remoteto get latest content - Build Hexo site
- Push generated HTML to public submodule
- Build and push Docker image
- Send email notification
Common Issues and Solutions
Issue: “refusing to merge unrelated histories”
Problem: Using git submodule update --remote --merge causes merge errors.
Solution: Remove the --merge flag:
1 | git submodule update --init --remote |
Reasoning: The submodule just needs to check out the latest commit, not merge changes.
Issue: Git push rejected during workflow
Problem: Race condition when pushing to site repo while workflow is running.
Solution: This is cosmetic - the important parts (site generation and deployment to public repo) complete successfully. The final push failure doesn’t affect the deployed site.
Issue: Missing TRIGGER_TOKEN secret
Problem: Content workflow fails with “Bad credentials” error.
Solution: Ensure the token is created and stored:
1 | gh auth token | gh secret set TRIGGER_TOKEN -R wikip-co/content |
Benefits of This Architecture
- Single Source of Truth: All markdown content exists in one repository
- Simplified Content Management: Writers only need to commit to one place
- Automated Deployment: Changes automatically propagate to all sites
- Version Control: Full Git history for all content changes
- Separation of Concerns: Content separated from site configuration and themes
- Flexible Frontends: Each site can have different themes, configurations, or frameworks
Alternative Approaches
Manual Submodule Updates
Instead of automatic triggers, sites could manually update submodules:
1 | cd /home/anthony/wikip.co/site/source/_posts |
Trade-off: More control but requires manual intervention for every content update.
Monorepo Approach
All content and site configurations in a single repository with separate build paths.
Trade-off: Simpler structure but less flexibility for different site configurations and harder to manage permissions.
Content API
Content served via API (headless CMS) instead of Git submodules.
Trade-off: More dynamic but adds infrastructure complexity and potential latency.
Conclusion
This Git submodule approach provides an optimal balance between automation and simplicity for managing shared content across multiple static sites. It leverages GitHub Actions for orchestration while maintaining the simplicity and version control benefits of Git.
The implementation required approximately:
- 3,198 markdown files unified from two repositories
- 3 repositories total (1 content, 2 sites)
- 3 GitHub Actions workflows (1 trigger, 2 deployment)
- 1 Personal Access Token for cross-repository communication