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.
For the current operator/contributor guide and rendered architecture diagrams, see Shared Content Project Guide.
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: The branch setting is still useful metadata for local workflows and explicit fetches, but the current deployment flow no longer relies on git submodule update --remote alone. The live build now receives the exact content SHA through repository_dispatch and checks out that specific revision for reproducibility.
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 - Pass the dispatched content ref/SHA into a reusable deploy workflow
- Resolve the content submodule to that exact revision before the build starts
For wikip.co (/.github/workflows/generator.yml):
1 | on: |
Update the site workflow to call the reusable deploy workflow:
1 | jobs: |
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 repo- Passing
content_shathrough the dispatch payload makes the content selection reproducible - A reusable workflow keeps the build/publish logic in one place instead of copying shell steps between site repos
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, including the exact content SHA - Site repos receive the event and trigger their workflows
- Site workflows:
- Check out the site repo itself, then initialize
publicshallowly and the content submodule with full history - Resolve the content submodule to the dispatched SHA
- Restore markdown mtimes from Git history so Hexo
updated_option: 'mtime'stays meaningful - Build Hexo site
- Push generated HTML to public submodule
- Build and push Docker image
- Send email notification
- Check out the site repo itself, then initialize
Common Issues and Solutions
Issue: Rebuild picked up the wrong content revision
Problem: A site rebuild that simply tracks main can drift away from the exact content commit that triggered it.
Solution: Pass content_ref and content_sha through repository_dispatch, then detach the content submodule at that SHA inside the reusable workflow.
Reasoning: The site build becomes deterministic and can be reproduced later.
Issue: Hexo updated dates drift after CI rebuilds
Problem: When Hexo uses updated_option: 'mtime', a fresh checkout makes many files look newly updated unless mtimes are restored from Git history.
Solution: Initialize the content submodule with full history and then restore each markdown file’s mtime from its last modifying commit before running the Hexo build.
Reasoning: This preserves meaningful updated ordering without changing the global permalink strategy.
Issue: public submodule becomes unpopulated after the build
Problem: hexo clean deletes the configured output directory, which in this setup is the public submodule worktree.
Solution: Preserve the public/.git file across the clean/build step so the generated output lands back into the same submodule worktree.
Reasoning: The workflow can clean and rebuild static output without severing the Git identity needed for the publish step.
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 keeping content selection deterministic through content_sha dispatch payloads and preserving Hexo update dates through Git-based mtime restoration.
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