Deployment Guide
Scully generates static HTML files that can be deployed to any static hosting service. This guide covers deployment to popular platforms and best practices.
Pre-Deployment Checklist
Angular app builds successfully (ng build --configuration production)
Scully runs without errors (npx scully --scanRoutes)
All routes are generated in dist/static
Static site tested locally (npx scully serve)
SEO metadata configured (titles, descriptions, meta tags)
404 page configured
Analytics and tracking set up (optional)
Images optimized
Build scripts added to package.json
Build Commands
Basic Build
# Build Angular app
ng build --configuration production
# Generate static pages with Scully
npx scully --scanRoutes
Single Command Build
Add to package.json:
{
"scripts" : {
"build" : "ng build --configuration production" ,
"scully" : "npx scully --scanRoutes" ,
"build:prod" : "npm run build && npm run scully" ,
"serve:static" : "npx scully serve"
}
}
Then run:
Output Directory
Scully generates static files in the outDir (default: ./dist/static):
dist/
├── static/ # Deploy this folder!
│ ├── index.html
│ ├── about/
│ │ └── index.html
│ ├── blog/
│ │ ├── post-1/
│ │ │ └── index.html
│ │ └── post-2/
│ │ └── index.html
│ └── assets/
│ └── (all assets)
└── your-app-name/ # Angular build (not needed for static hosting)
Deploy the dist/static folder, not the dist/your-app-name folder.
Netlify
Netlify is a popular choice for JAMstack sites with automatic deployments from Git.
Create netlify.toml
Create a netlify.toml file in your project root: [ build ]
command = "npm run build:prod"
publish = "dist/static"
[[ redirects ]]
from = "/*"
to = "/index.html"
status = 200
[[ headers ]]
for = "/*"
[ headers . values ]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Connect Repository
Go to Netlify
Click “New site from Git”
Connect your repository (GitHub, GitLab, Bitbucket)
Select your repository
Configure Build Settings
Netlify will auto-detect settings from netlify.toml, but verify:
Build command : npm run build:prod
Publish directory : dist/static
Node version : Set in netlify.toml or UI
Add environment variable: [ build . environment ]
NODE_VERSION = "18"
Deploy
Click “Deploy site”. Netlify will:
Install dependencies
Run build command
Deploy dist/static folder
Provide a URL
Netlify CLI
Deploy manually with Netlify CLI:
# Install Netlify CLI
npm install -g netlify-cli
# Build your site
npm run build:prod
# Deploy
netlify deploy --prod --dir=dist/static
Vercel
Vercel provides excellent Angular support with zero configuration.
Create vercel.json
Create vercel.json in your project root: {
"version" : 2 ,
"buildCommand" : "npm run build:prod" ,
"outputDirectory" : "dist/static" ,
"routes" : [
{
"src" : "/(.*)" ,
"dest" : "/"
}
]
}
Deploy via Git
Go to Vercel
Import your Git repository
Vercel auto-detects Angular
Override settings if needed:
Build Command : npm run build:prod
Output Directory : dist/static
Click “Deploy”
Vercel CLI
# Install Vercel CLI
npm install -g vercel
# Build
npm run build:prod
# Deploy
vercel --prod
GitHub Pages
Host your static site for free on GitHub Pages.
Install angular-cli-ghpages
npm install -g angular-cli-ghpages
Configure Base Href
Update scully.config.ts for GitHub Pages subdirectory: import { ScullyConfig } from '@scullyio/scully' ;
export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-app' ,
outDir: './dist/static' ,
routes: {
'/' : {
type: 'default' ,
baseHref: '/your-repo-name/' , // Important for GitHub Pages
},
},
};
Build with correct base href: ng build --configuration production --base-href /your-repo-name/
npx scully
Deploy
# Deploy dist/static to gh-pages branch
npx angular-cli-ghpages --dir=dist/static
Or manually: cd dist/static
git init
git add -A
git commit -m 'Deploy'
git push -f [email protected] :username/repo.git master:gh-pages
Enable GitHub Pages
Go to repository Settings → Pages
Select gh-pages branch
Click Save
Access at https://username.github.io/repo-name/
Firebase Hosting
Google’s Firebase offers fast global CDN hosting.
Install Firebase CLI
npm install -g firebase-tools
Initialize Firebase
firebase login
firebase init hosting
When prompted:
Public directory : dist/static
Configure as SPA : Yes
Overwrite index.html : No
Configure firebase.json
Update firebase.json: {
"hosting" : {
"public" : "dist/static" ,
"ignore" : [ "firebase.json" , "**/.*" , "**/node_modules/**" ],
"rewrites" : [
{
"source" : "**" ,
"destination" : "/index.html"
}
],
"headers" : [
{
"source" : "**" ,
"headers" : [
{
"key" : "Cache-Control" ,
"value" : "public, max-age=3600"
}
]
}
]
}
}
Deploy
npm run build:prod
firebase deploy
AWS S3 + CloudFront
Host on AWS for enterprise-grade infrastructure.
Create S3 Bucket
aws s3 mb s3://your-bucket-name
Enable static website hosting: aws s3 website s3://your-bucket-name --index-document index.html
Configure Bucket Policy
Create bucket-policy.json: {
"Version" : "2012-10-17" ,
"Statement" : [
{
"Sid" : "PublicReadGetObject" ,
"Effect" : "Allow" ,
"Principal" : "*" ,
"Action" : "s3:GetObject" ,
"Resource" : "arn:aws:s3:::your-bucket-name/*"
}
]
}
Apply: aws s3api put-bucket-policy --bucket your-bucket-name --policy file://bucket-policy.json
Upload Files
npm run build:prod
aws s3 sync dist/static s3://your-bucket-name --delete
Set Up CloudFront (Optional)
Create CloudFront distribution
Set origin to S3 bucket
Configure caching rules
Add custom domain (optional)
Docker
Containerize your static site with nginx.
Create Dockerfile
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build:prod
FROM nginx:alpine
COPY --from=builder /app/dist/static /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [ "nginx" , "-g" , "daemon off;" ]
Create nginx.conf
server {
listen 80 ;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $ uri $ uri / /index.html;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable" ;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
Build and Run
# Build image
docker build -t my-scully-app .
# Run container
docker run -p 8080:80 my-scully-app
CI/CD Pipelines
GitHub Actions
.github/workflows/deploy.yml
name : Build and Deploy
on :
push :
branches : [ main ]
jobs :
build-and-deploy :
runs-on : ubuntu-latest
steps :
- name : Checkout
uses : actions/checkout@v3
- name : Setup Node.js
uses : actions/setup-node@v3
with :
node-version : '18'
cache : 'npm'
- name : Install Dependencies
run : npm ci
- name : Build
run : npm run build:prod
- name : Deploy to Netlify
uses : nwtgck/actions-netlify@v2
with :
publish-dir : './dist/static'
production-branch : main
env :
NETLIFY_AUTH_TOKEN : ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID : ${{ secrets.NETLIFY_SITE_ID }}
GitLab CI
image : node:18
stages :
- build
- deploy
cache :
paths :
- node_modules/
build :
stage : build
script :
- npm ci
- npm run build:prod
artifacts :
paths :
- dist/static
expire_in : 1 week
deploy :
stage : deploy
script :
- npm install -g netlify-cli
- netlify deploy --prod --dir=dist/static --auth=$NETLIFY_AUTH_TOKEN --site=$NETLIFY_SITE_ID
only :
- main
Production Optimizations
Angular Build Optimizations
{
"projects" : {
"your-app" : {
"architect" : {
"build" : {
"configurations" : {
"production" : {
"optimization" : true ,
"outputHashing" : "all" ,
"sourceMap" : false ,
"namedChunks" : false ,
"extractLicenses" : true ,
"vendorChunk" : false ,
"buildOptimizer" : true ,
"budgets" : [
{
"type" : "initial" ,
"maximumWarning" : "2mb" ,
"maximumError" : "5mb"
}
]
}
}
}
}
}
}
}
Scully Production Config
import { ScullyConfig } from '@scullyio/scully' ;
import { removeScripts } from '@scullyio/scully-plugin-remove-scripts' ;
const isProd = process . env . NODE_ENV === 'production' ;
export const config : ScullyConfig = {
projectRoot: './src' ,
outDir: './dist/static' ,
// Optimize for production
maxRenderThreads: isProd ? 8 : 4 ,
ignoreResourceTypes: isProd ? [ 'image' , 'font' , 'media' ] : [],
defaultPostRenderers: isProd
? [ removeScripts , 'seoHrefOptimise' ]
: [ 'seoHrefOptimise' ],
routes: {
// Your routes
},
};
Troubleshooting
Problem : Page refreshes return 404Solution : Configure server rewrites:
Netlify : Add redirects in netlify.toml
Vercel : Add routes in vercel.json
nginx : Use try_files $uri $uri/ /index.html
Firebase : Add rewrites in firebase.json
Problem : Assets not loading, routing brokenSolution :
Build with correct base href: ng build --base-href /path/
Update Scully config baseHref for routes
Ensure assets paths are absolute: /assets/...
Problem : Build succeeds locally but fails in CISolution :
Match Node.js version between local and CI
Use npm ci instead of npm install
Clear npm cache: npm cache clean --force
Check memory limits (increase if needed)
Review error logs for missing dependencies
Problem : Static pages load slowlySolution :
Enable CDN on hosting platform
Optimize images (use WebP, proper sizing)
Enable gzip/brotli compression
Set cache headers for static assets
Remove unused Angular bundles with removeScripts
Lazy load images
Post-Deployment Checklist
All pages accessible and loading correctly
Navigation works between pages
Assets (images, fonts, CSS) loading
SEO meta tags present (view source)
Analytics tracking working
404 page displays correctly
Mobile responsive design working
HTTPS enabled
Custom domain configured (if applicable)
Sitemap accessible
robots.txt configured
Performance tested (Lighthouse, PageSpeed)
Next Steps
Testing Test your site before deployment
Configuration Optimize your Scully configuration
Plugins Add SEO and performance plugins
API Reference Full ScullyConfig API reference